新增质量模块(工艺执行书 / 样茧试缫)与报表中心

质量模块:
- 11 张工艺执行书表:封面、试产、选剥茧(含混茧明细)、试缫与大样、煮茧(含分车间配温、副煮车间)、自动缫(含大样)、后缫
- 10 张样茧试缫表:原料小样/综合样主表 + 选茧、解舒、茧幅、切剖、清洁洁净、茧丝百回纤度
- 服务与接口、主控指针回写(列表 √ 标记)、封面编号自增、报表数据包
- 前端 8 张报表打印版式、7 类工艺单据与 6 类调查录入弹窗、字段驱动表单/表格组件
- 菜单种子新增「质量」目录,样茧子表增加 YjssType 判别列,避免原料小样与综合样自增 Id 重号串数据

报表中心:
- 报表模板管理、锐浪 grhtml5 加载与预览、全屏设计面板、数据源 Provider(落丝记录、职工花名册)

其他:同步工作进度 1-11 文档、开发数据库快照、.gitignore 忽略运行日志目录
This commit is contained in:
yanpeng200212
2026-10-22 10:30:37 +08:00
parent 8c4c464362
commit a8890c81e9
92 changed files with 14785 additions and 70 deletions
@@ -5,6 +5,7 @@ using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using NotificationTypeEntity = F9WebAiTest.Domain.Entities.Notice.NotificationType;
using MenuEntity = F9WebAiTest.Domain.Entities.SYS.RBAC.Menu;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions;
using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles;
using MediatR;
@@ -21,6 +22,7 @@ public class AssignRolePermissionCommandHandler(
IRolePermissionWriteRepository rolePermissionRep,
IPermissionWriteRepository permissionWriteRepository,
IRepository<NotificationTypeEntity> notificationTypeRepository,
IRepository<MenuEntity> menuRepository,
ISnowflakeIdGenerator snowflakeIdGenerator,
IPublisher eventBus) : IRequestHandler<RolePermission_AssignCommand, long>
{
@@ -46,6 +48,15 @@ public class AssignRolePermissionCommandHandler(
command.ResourceIdentity,
command.ResourceId);
// 菜单/操作权限:Code 对齐菜单上声明的权限标识(如 sys:user:add),
// 否则 v-permission 之类按业务权限码判断的地方永远匹配不上
if (command.ResourceType is Resource_Type_Enum.菜单 or Resource_Type_Enum.操作)
{
var menu = await menuRepository.GetByIdAsync(command.ResourceId);
if (menu != null && !string.IsNullOrEmpty(menu.Permission))
dbPermission.SetCode(menu.Permission);
}
// 消息订阅权限:Code 对齐消息类型的 RequiredPermission(业务权限码),保证订阅校验一致
if (command.ResourceType == Resource_Type_Enum.消息订阅)
{
@@ -0,0 +1,62 @@
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Application.Interfaces.Repositories.SYS.Employees;
using MediatR;
namespace F9WebAiTest.Application.CQRS.SYS.Reports.Queries;
/// <summary>职工花名册报表数据查询(示例,对应 DsName = RepDt_Sys_Users)</summary>
public record GetEmployeeDirectoryRepQuery(long DepId, string Like) : IRequest<ReportDto_DataSource>;
public class GetEmployeeDirectoryRepQueryHandler : IRequestHandler<GetEmployeeDirectoryRepQuery, ReportDto_DataSource>
{
private readonly IEmployeeReadRepository _readRepository;
public GetEmployeeDirectoryRepQueryHandler(IEmployeeReadRepository readRepository)
{
_readRepository = readRepository;
}
public async Task<ReportDto_DataSource> Handle(GetEmployeeDirectoryRepQuery query, CancellationToken cancellationToken)
{
var dto = new ReportDto_DataSource
{
Report_Data = new Dictionary<string, ReportDataDto_Data>()
};
// 1. 通过仓储查询职工数据(部门含子部门)
var page = await _readRepository.GetPagedAsync(query.DepId, 0, null, null,
string.IsNullOrWhiteSpace(query.Like) ? "" : query.Like, 1, 1000);
// 2. 部门名称(查询条件展示用)
var depName = query.DepId > 0 ? await _readRepository.GetDepNameAsync(query.DepId) : "";
// 3. 明细数据:属性名须与 .grf 模板 DetailGrid.Recordset.Field 一致
var rows = new List<object>();
foreach (var e in page.Items)
{
rows.Add(new
{
EmployeeCode = e.Code ?? "",
EmployeeName = e.Name ?? "",
Gender = e.Gender ?? "",
DepName = e.DepName ?? "",
WorktypeName = e.WorktypeName ?? "",
Phone = e.Phone ?? "",
RuzhiDate = e.RuzhiDate?.ToString("yyyy-MM-dd") ?? ""
});
}
// 4. 报表参数(模板以 StaticBox 的 Parameter 属性引用)
var mainData = new ReportDataDto_Data { Field_Data = rows };
mainData.Parameter_Data = new
{
部门 = string.IsNullOrWhiteSpace(depName) ? "(全部)" : depName,
关键字 = string.IsNullOrWhiteSpace(query.Like) ? "(全部)" : query.Like,
记录数 = page.Total,
查询时间 = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
dto.Report_Data.Add("main", mainData);
return dto;
}
}
@@ -0,0 +1,149 @@
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.Prod;
using MediatR;
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
namespace F9WebAiTest.Application.CQRS.SYS.Reports.Queries;
/// <summary>
/// 落丝记录报表查询(对应 DsName = RepDt_Prod_Lsjl)。
/// 参数:日期范围 / 车间(含子车间)/ 机组 / 庄口 / 关键字(庄口、规格、颜色)。
/// 输出字段名与 F9 落丝记录报表模板一致(中文列名)。
/// </summary>
public record GetLsjlRepQuery(
DateTime? DateStart,
DateTime? DateEnd,
long DepId,
int Jizu,
long GyzkId,
string Like) : IRequest<ReportDto_DataSource>;
public class GetLsjlRepQueryHandler : IRequestHandler<GetLsjlRepQuery, ReportDto_DataSource>
{
private readonly IRepository<LsjlRecord> _repo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<CraftZhuangKou> _zkRepo;
public GetLsjlRepQueryHandler(
IRepository<LsjlRecord> repo,
IRepository<SysDept.Department> deptRepo,
IRepository<CraftZhuangKou> zkRepo)
{
_repo = repo;
_deptRepo = deptRepo;
_zkRepo = zkRepo;
}
public async Task<ReportDto_DataSource> Handle(GetLsjlRepQuery query, CancellationToken cancellationToken)
{
var all = await _repo.GetAllAsync();
var depts = await _deptRepo.GetAllAsync();
var zks = await _zkRepo.GetAllAsync();
var deptDict = depts.GroupBy(d => d.Id).ToDictionary(g => g.Key, g => g.First().Name);
var zkDict = zks.GroupBy(z => z.Id).ToDictionary(g => g.Key, g => g.First().ZhuangKouName);
var depIds = ResolveDepIds(depts, query.DepId);
IEnumerable<LsjlRecord> rows = all;
if (query.DateStart.HasValue)
{
var start = query.DateStart.Value.Date;
rows = rows.Where(x => x.WorkDate.Date >= start);
}
if (query.DateEnd.HasValue)
{
var end = query.DateEnd.Value.Date;
rows = rows.Where(x => x.WorkDate.Date <= end);
}
if (depIds != null)
rows = rows.Where(x => depIds.Contains(x.DeptId));
if (query.Jizu > 0)
rows = rows.Where(x => x.Jizu == query.Jizu);
if (query.GyzkId > 0)
rows = rows.Where(x => x.GyzkId == query.GyzkId);
if (!string.IsNullOrWhiteSpace(query.Like))
{
var kw = query.Like.Trim();
rows = rows.Where(x =>
deptDict.GetValueOrDefault(x.DeptId, "").Contains(kw) ||
zkDict.GetValueOrDefault(x.GyzkId, "").Contains(kw) ||
(x.Guige ?? "").Contains(kw) ||
(x.Yanse ?? "").Contains(kw));
}
var list = rows
.OrderBy(x => x.WorkDate)
.ThenBy(x => deptDict.GetValueOrDefault(x.DeptId, ""))
.ThenBy(x => x.Jizu)
.ThenBy(x => x.LuosiTime)
.ToList();
// 明细行:键名与 .grf 模板 DetailGrid.Recordset.Field 完全一致(中文列名)
var fieldRows = new List<object>(list.Count);
foreach (var x in list)
{
var depName = deptDict.GetValueOrDefault(x.DeptId, "");
var zkName = zkDict.GetValueOrDefault(x.GyzkId, "");
fieldRows.Add(new Dictionary<string, object>(StringComparer.Ordinal)
{
["日期"] = x.WorkDate.ToString("yyyy-MM-dd"),
["车间"] = depName,
["机组"] = x.Jizu,
["庄口"] = zkName,
["回数"] = x.Huishu,
["颜色"] = x.Yanse ?? "",
["半车"] = x.Isbanche ? "半车" : "整车",
["落丝时间"] = x.LuosiTime ?? "",
["缫丝桶数"] = x.Chetaishu,
// 以下是备选列(模板未使用时不显示,可用于自定义模板)
["规格"] = x.Guige ?? "",
["车台范围"] = x.ChetaiStart > 0 ? $"{x.ChetaiStart}-{x.ChetaiEnd}" : "",
["缫丝时长"] = Math.Round(x.Hours, 2),
["车速"] = Math.Round(x.Chesu, 2),
});
}
var depText = query.DepId > 0 ? deptDict.GetValueOrDefault(query.DepId, "") : "(全部)";
var zkText = query.GyzkId > 0 ? zkDict.GetValueOrDefault(query.GyzkId, "") : "(全部)";
var paramData = new ReportDataDto_Data { Field_Data = fieldRows };
paramData.Parameter_Data = new
{
开始日期 = query.DateStart?.ToString("yyyy-MM-dd") ?? "",
结束日期 = query.DateEnd?.ToString("yyyy-MM-dd") ?? "",
车间 = string.IsNullOrWhiteSpace(depText) ? "(全部)" : depText,
机组 = query.Jizu > 0 ? query.Jizu.ToString() : "(全部)",
庄口 = string.IsNullOrWhiteSpace(zkText) ? "(全部)" : zkText,
记录数 = fieldRows.Count,
查询时间 = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
};
var dto = new ReportDto_DataSource();
dto.Report_Data.Add("main", paramData);
return dto;
}
/// <summary>车间 Id → 自身 + 全部子孙车间 Id(null 表示不限)</summary>
private static HashSet<long>? ResolveDepIds(List<SysDept.Department> depts, long depId)
{
if (depId <= 0) return null;
var children = depts
.GroupBy(d => d.ParentId)
.ToDictionary(g => g.Key, g => g.Select(d => d.Id).ToList());
var result = new HashSet<long> { depId };
var stack = new Stack<long>();
stack.Push(depId);
while (stack.Count > 0)
{
var cur = stack.Pop();
if (!children.TryGetValue(cur, out var kids)) continue;
foreach (var k in kids)
if (result.Add(k)) stack.Push(k);
}
return result;
}
}
@@ -4,6 +4,9 @@ using F9WebAiTest.Application.Services.Auth;
using F9WebAiTest.Application.Services.Base;
using F9WebAiTest.Application.Services.Notice;
using F9WebAiTest.Application.Services.Prod;
using F9WebAiTest.Application.Services.QC;
using F9WebAiTest.Application.Services.Reports;
using F9WebAiTest.Application.Interfaces.Reports;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
@@ -25,6 +28,18 @@ public static class DependencyInjection
services.AddScoped<IHbjyService, HbjyService>();
services.AddScoped<IChejianBanciService, ChejianBanciService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<IReportService, ReportService>();
// 质量模块:工艺执行书 / 样茧试缫
services.AddScoped<IGyzxsService, GyzxsService>();
services.AddScoped<IYjssService, YjssService>();
// 报表数据提供者(按 Config.DsName 解析,自动注册所有实现)
foreach (var t in typeof(DependencyInjection).Assembly.GetTypes()
.Where(t => !t.IsAbstract && typeof(IReportDataProvider).IsAssignableFrom(t)))
{
services.AddScoped(typeof(IReportDataProvider), t);
}
// MediatR:注册命令/查询 Handler
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(NotificationService).Assembly));
@@ -0,0 +1,62 @@
using F9WebAiTest.Domain.Entities.QC.Gyzxs;
namespace F9WebAiTest.Application.Dtos.QC;
/// <summary>工艺执行书主列表行(庄口 + 各子单据完成标记)</summary>
public class GyzxsDocVersionDto
{
public long Id { get; set; }
public long GyzkId { get; set; }
public string ZhuangKouName { get; set; } = "";
public string ZhuangKouCode { get; set; } = "";
public int Year { get; set; }
public int Sort { get; set; }
public long ShichanId { get; set; }
public long FengMianId { get; set; }
public long XbjId { get; set; }
public long XbjSsdyId { get; set; }
public long ZhuJianId { get; set; }
public long ZdsId { get; set; }
public long ZdsDyId { get; set; }
public long HsId { get; set; }
/// <summary>该庄口已录入的规格(自动缫 / 后缫 / 试产 / 试缫大样 均按 庄口+规格 唯一)</summary>
public List<string> Guiges { get; set; } = new();
}
/// <summary>选剥茧(含混茧比例明细)</summary>
public class GyzxsXbjDto
{
public GyzxsXbj Xbj { get; set; } = new();
public List<GyzxsXbjHunJian> HunJian { get; set; } = new();
}
/// <summary>煮茧(含分车间指标、副煮车间结果)</summary>
public class GyzxsZhuJianDto
{
public GyzxsZhuJian ZhuJian { get; set; } = new();
public List<GyzxsZhuJianCheJian> CheJian { get; set; } = new();
public List<GyzxsZhuJianFuZhu> FuZhu { get; set; } = new();
}
/// <summary>自动缫(含大样资料)</summary>
public class GyzxsZdsDto
{
public GyzxsZds Zds { get; set; } = new();
public GyzxsZdsDyzl? Dyzl { get; set; }
}
/// <summary>
/// 报表数据包:Parameters=报表参数区,Main/Sub1/Sub2=结果集。
/// 结构与 Grid++Report 的 Report_Data 对齐,便于日后直接对接报表中心。
/// </summary>
public class GyzxsPrintDto
{
public string ReportName { get; set; } = "";
public string Title { get; set; } = "";
public Dictionary<string, object?> Parameters { get; set; } = new();
public List<Dictionary<string, object?>> Main { get; set; } = new();
public List<Dictionary<string, object?>> Sub1 { get; set; } = new();
public List<Dictionary<string, object?>> Sub2 { get; set; } = new();
}
@@ -0,0 +1,60 @@
using F9WebAiTest.Domain.Entities.QC.Yjss;
namespace F9WebAiTest.Application.Dtos.QC;
/// <summary>样茧试缫主列表行(原料小样 / 综合样共用)</summary>
public class YjssListItemDto
{
public long Id { get; set; }
public int Type { get; set; } // 1=原料小样 2=综合样
public long ZhuangKouId { get; set; } // 原料庄口 / 工艺庄口
public string ZhuangKouName { get; set; } = "";
public DateTime Date { get; set; }
public int SampleType { get; set; } // 原料小样抽样类型:1定价 2入库 3鲜茧
public string SampleTypeName { get; set; } = "";
public decimal ZhongLiang { get; set; }
public decimal HuiChaoLv { get; set; }
public long XjdcId { get; set; }
public int JsdcCt { get; set; }
public long JfdcId { get; set; }
public long QpdcId { get; set; }
public long QjjjId { get; set; }
public int JsbhxdCt { get; set; }
public string Memo { get; set; } = "";
}
/// <summary>茧幅调查(主表 + 明细)</summary>
public class YjssJfdcDto
{
public YjssDcJfdcMain Main { get; set; } = new();
public List<YjssDcJfdcItem> Items { get; set; } = new();
}
/// <summary>茧丝百回纤度(汇总 + 明细)</summary>
public class YjssJsbhxdDto
{
public YjssDcJsbhxdMain Main { get; set; } = new();
public List<YjssDcJsbhxd> Items { get; set; } = new();
}
/// <summary>样茧试缫整页数据(用于报表 / 打印)</summary>
public class YjssFullDto
{
public int Type { get; set; }
public string ZhuangKouName { get; set; } = "";
public DateTime Date { get; set; }
public decimal ZhongLiang { get; set; }
public decimal HuiChaoLv { get; set; }
public string SampleTypeName { get; set; } = "";
public string Memo { get; set; } = "";
public YjssDcXjdc? Xjdc { get; set; }
public List<YjssDcJsdc> Jsdc { get; set; } = new();
public YjssJfdcDto? Jfdc { get; set; }
public YjssDcQpdc? Qpdc { get; set; }
public YjssDcQjjj? Qjjj { get; set; }
public YjssJsbhxdDto? Jsbhxd { get; set; }
/// <summary>报表参数区(中文键,与 F9 RepDt_Qc_Yjss_* 对齐)</summary>
public Dictionary<string, object?> Parameters { get; set; } = new();
}
@@ -0,0 +1,21 @@
namespace F9WebAiTest.Application.Dtos.SYS.Reports;
/// <summary>
/// 报表数据集(对应 F9 的 ReportDataDto_Data):
/// Field_Data 为记录集(每行一个匿名对象,属性名须与 .grf 模板 DetailGrid.Recordset.Field 一致);
/// Parameter_Data 为报表参数(账期 / 车间 / 查询时间等,供模板以 Parameter 方式引用)。
/// </summary>
public class ReportDataDto_Data
{
public List<object> Field_Data { get; set; } = new();
public object? Parameter_Data { get; set; }
}
/// <summary>
/// 报表数据源(对应 F9 的 ReportDto_DataSource):
/// Report_Data 为「数据集名 → 数据集」的字典,如 "main" / "detail"。
/// </summary>
public class ReportDto_DataSource
{
public Dictionary<string, ReportDataDto_Data> Report_Data { get; set; } = new();
}
@@ -0,0 +1,82 @@
namespace F9WebAiTest.Application.Dtos.SYS.Reports;
public record ReportListDto(
long Id,
string Code,
string Name,
string Category,
string Description,
int SortOrder,
bool IsActive,
long ReportFileId,
bool HasTemplate,
DateTime CreatedAt,
DateTime? UpdatedAt);
public record ReportDetailDto(
long Id,
string Code,
string Name,
string Category,
string Description,
string DataSql,
string DataParams,
string Config,
int SortOrder,
bool IsActive,
long ReportFileId,
bool HasTemplate,
DateTime CreatedAt,
DateTime? UpdatedAt);
public record ReportViewDto(
long Id,
string Code,
string Name,
string Category,
string Description,
int SortOrder,
bool HasTemplate);
public class ReportCreateDto
{
public string Code { get; set; } = "";
public string Name { get; set; } = "";
public string? Category { get; set; }
public string? Description { get; set; }
public string? DataSql { get; set; }
public string? DataParams { get; set; }
public string? Config { get; set; }
public int SortOrder { get; set; }
/// <summary>
/// 报表模板(.grf 的 JSON 文本)。与报表配置一起保存,无需再单独上传模板文件。
/// 为空表示本次不改动模板。
/// </summary>
public string? TemplateJson { get; set; }
}
public class ReportUpdateDto
{
public string Code { get; set; } = "";
public string Name { get; set; } = "";
public string? Category { get; set; }
public string? Description { get; set; }
public string? DataSql { get; set; }
public string? DataParams { get; set; }
public string? Config { get; set; }
public int SortOrder { get; set; }
/// <summary>
/// 报表模板(.grf 的 JSON 文本)。与报表配置一起保存,无需再单独上传模板文件。
/// 为空表示本次不改动模板(保留原模板)。
/// </summary>
public string? TemplateJson { get; set; }
}
/// <summary>在线编辑模板源码后直接保存的内容(免下载 / 上传 .grf)</summary>
public class ReportTemplateContentDto
{
/// <summary>模板全文:GR6 时代的 UTF-8 JSON 文本,或 GR7 设计器的 "_WR" 压缩文本</summary>
public string? Content { get; set; }
}
@@ -0,0 +1,55 @@
namespace F9WebAiTest.Application.Dtos.SYS.Reports;
/// <summary>已注册的报表数据源(IReportDataProvider.ReportName)</summary>
public class ReportDataSourceInfoDto
{
public string DsName { get; set; } = "";
}
/// <summary>明细字段(.grf 模板 DetailGrid.Recordset.Field 的映射来源),全部由查询结果推断</summary>
public class ReportFieldDefDto
{
public string Name { get; set; } = "";
public string Label { get; set; } = "";
public string DataType { get; set; } = "String";
}
/// <summary>探测请求:按 DsName(或 DataSql)执行一次查询,识别返回的列</summary>
public class ReportProbeRequestDto
{
/// <summary>数据源名称(IReportDataProvider.ReportName),优先于 DataSql</summary>
public string? DsName { get; set; }
/// <summary>数据 SQL(无 DsName 时用于探测列)</summary>
public string? DataSql { get; set; }
/// <summary>执行探测时传入的参数值(键 = 参数名;报表上已配置的参数由前端带入)</summary>
public Dictionary<string, object?> Params { get; set; } = new();
}
/// <summary>
/// 报表参数(后端 DataSource.Parameter_Data 的键值)。
/// 注意:这是「报表参数」——由查询执行后返回、供模板头展示的统计口径;
/// 与 Config.Parms 里的「查询条件」(q_*,用于查询面板传参)不是一回事。
/// </summary>
public class ReportParamValueDto
{
public string Name { get; set; } = "";
public string Value { get; set; } = "";
}
/// <summary>探测结果:查询返回的明细字段 + 报表参数 + 少量样本行</summary>
public class ReportProbeResultDto
{
public string DsName { get; set; } = "";
public List<ReportFieldDefDto> Fields { get; set; } = new();
/// <summary>报表参数(Parameter_Data 的键值),模板头/合计区直接引用</summary>
public List<ReportParamValueDto> Parameters { get; set; } = new();
public List<Dictionary<string, object?>> Rows { get; set; } = new();
public int Total { get; set; }
/// <summary>探测失败原因</summary>
public string? Error { get; set; }
}
@@ -16,6 +16,7 @@
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
</ItemGroup>
@@ -0,0 +1,22 @@
using F9WebAiTest.Application.Dtos.SYS.Reports;
using MediatR;
namespace F9WebAiTest.Application.Interfaces.Reports;
/// <summary>
/// 报表数据提供者(对应 F9 的 IReportService):
/// 报表配置 Config.DsName 指定查询名称,后端按名称解析到对应实现执行查询。
/// 实现类注册到 DI 后自动被发现(静态 ReportName 属性作为解析键)。
/// 查询实现应通过 MediatR 发送 CQRS Query,由 QueryHandler 组装 ReportDto_DataSource。
/// </summary>
public interface IReportDataProvider
{
/// <summary>查询名称(对应报表配置中的 DsName,如 "RepDt_Sys_Users")</summary>
string ReportName { get; }
/// <summary>
/// 执行查询。pairs 为前端查询控件收集的键值对(键 = Config.Parms[].Name);
/// 返回含记录集(Field_Data)与报表参数(Parameter_Data)的数据源。
/// </summary>
Task<ReportDto_DataSource> GetData(Dictionary<string, object> pairs, IMediator mediator);
}
@@ -0,0 +1,760 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.QC;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.QC.Gyzxs;
namespace F9WebAiTest.Application.Services.QC;
public interface IGyzxsService
{
Task<PageResult<GyzxsDocVersionDto>> GetDocVersionPagedAsync(int pageIndex, int pageSize, int year, string keyword);
Task<GyzxsDocVersionDto> CreateDocVersionAsync(long gyzkId);
Task DeleteDocVersionAsync(long id);
Task<GyzxsFengMian> GetFengMianAsync(long gyzkId);
Task<bool> SaveFengMianAsync(GyzxsFengMian dto);
Task<GyzxsShichan> GetShichanAsync(long gyzkId, string guige);
Task<bool> SaveShichanAsync(GyzxsShichan dto);
Task<GyzxsXbjDto> GetXbjAsync(long gyzkId);
Task<bool> SaveXbjAsync(GyzxsXbjDto dto);
Task<GyzxsXbjSsDy> GetXbjSsDyAsync(long gyzkId, string guige);
Task<bool> SaveXbjSsDyAsync(GyzxsXbjSsDy dto);
Task<GyzxsZhuJianDto> GetZhuJianAsync(long gyzkId);
Task<bool> SaveZhuJianAsync(GyzxsZhuJianDto dto);
Task<GyzxsZdsDto> GetZdsAsync(long gyzkId, string guige);
Task<bool> SaveZdsAsync(GyzxsZdsDto dto);
Task<GyzxsHs> GetHsAsync(long gyzkId, string guige);
Task<bool> SaveHsAsync(GyzxsHs dto);
/// <summary>报表数据(type: fengmian / gyzxs / xbj / shichan / ssdy / zhujian / zds / zdsdy)</summary>
Task<GyzxsPrintDto> GetPrintDataAsync(string type, long gyzkId, string guige);
}
/// <summary>
/// 质量模块 —— 工艺执行书(对应 F9 生产\质量\工艺执行书)。
/// 设计要点:以工艺庄口为主体,7 张子单据各自按「庄口(+规格)」唯一,
/// 任一子单据保存都会 upsert 主控表 qc_gyzxs_doc_version 的对应指针列。
/// </summary>
public class GyzxsService : IGyzxsService
{
private readonly IRepository<CraftZhuangKou> _zkRepo;
private readonly IRepository<GyzxsDocVersion> _versionRepo;
private readonly IRepository<GyzxsFengMian> _fengMianRepo;
private readonly IRepository<GyzxsShichan> _shichanRepo;
private readonly IRepository<GyzxsXbj> _xbjRepo;
private readonly IRepository<GyzxsXbjHunJian> _hunjianRepo;
private readonly IRepository<GyzxsXbjSsDy> _ssdyRepo;
private readonly IRepository<GyzxsZhuJian> _zhuJianRepo;
private readonly IRepository<GyzxsZhuJianCheJian> _zjCheJianRepo;
private readonly IRepository<GyzxsZhuJianFuZhu> _zjFuZhuRepo;
private readonly IRepository<GyzxsZds> _zdsRepo;
private readonly IRepository<GyzxsZdsDyzl> _zdsDyRepo;
private readonly IRepository<GyzxsHs> _hsRepo;
public GyzxsService(
IRepository<CraftZhuangKou> zkRepo,
IRepository<GyzxsDocVersion> versionRepo,
IRepository<GyzxsFengMian> fengMianRepo,
IRepository<GyzxsShichan> shichanRepo,
IRepository<GyzxsXbj> xbjRepo,
IRepository<GyzxsXbjHunJian> hunjianRepo,
IRepository<GyzxsXbjSsDy> ssdyRepo,
IRepository<GyzxsZhuJian> zhuJianRepo,
IRepository<GyzxsZhuJianCheJian> zjCheJianRepo,
IRepository<GyzxsZhuJianFuZhu> zjFuZhuRepo,
IRepository<GyzxsZds> zdsRepo,
IRepository<GyzxsZdsDyzl> zdsDyRepo,
IRepository<GyzxsHs> hsRepo)
{
_zkRepo = zkRepo;
_versionRepo = versionRepo;
_fengMianRepo = fengMianRepo;
_shichanRepo = shichanRepo;
_xbjRepo = xbjRepo;
_hunjianRepo = hunjianRepo;
_ssdyRepo = ssdyRepo;
_zhuJianRepo = zhuJianRepo;
_zjCheJianRepo = zjCheJianRepo;
_zjFuZhuRepo = zjFuZhuRepo;
_zdsRepo = zdsRepo;
_zdsDyRepo = zdsDyRepo;
_hsRepo = hsRepo;
}
// ==================== 主控(庄口年度列表) ====================
public async Task<PageResult<GyzxsDocVersionDto>> GetDocVersionPagedAsync(int pageIndex, int pageSize, int year, string keyword)
{
if (year <= 0) year = DateTime.Today.Year;
var zks = (await _zkRepo.FindAsync(x => !x.IsDeleted && x.BelongingYear == year))
.OrderBy(x => x.Sort).ThenBy(x => x.ZhuangKouCode).ToList();
if (!string.IsNullOrWhiteSpace(keyword))
zks = zks.Where(x => x.ZhuangKouName.Contains(keyword) || x.ZhuangKouCode.Contains(keyword)
|| x.ProductionDisplayName.Contains(keyword)).ToList();
var versions = (await _versionRepo.FindAsync(x => !x.IsDeleted))
.GroupBy(x => x.GyzkId).ToDictionary(g => g.Key, g => g.OrderByDescending(v => v.Id).First());
// 规格集合:来自试产 / 自动缫 / 后缫 / 试缫大样
var guigeMap = new Dictionary<long, HashSet<string>>();
void Collect(IEnumerable<(long GyzkId, string Guige)> rows)
{
foreach (var (gk, gg) in rows)
{
if (string.IsNullOrWhiteSpace(gg)) continue;
if (!guigeMap.TryGetValue(gk, out var set)) guigeMap[gk] = set = new HashSet<string>();
set.Add(gg);
}
}
Collect((await _shichanRepo.FindAsync(x => !x.IsDeleted)).Select(x => (x.GyzkId, x.Guige)));
Collect((await _zdsRepo.FindAsync(x => !x.IsDeleted)).Select(x => (x.GyzkId, x.Guige)));
Collect((await _hsRepo.FindAsync(x => !x.IsDeleted)).Select(x => (x.GyzkId, x.Guige)));
Collect((await _ssdyRepo.FindAsync(x => !x.IsDeleted)).Select(x => (x.GyzkId, x.Guige)));
var total = zks.Count;
var page = zks.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<GyzxsDocVersionDto>
{
Total = total,
PageIndex = pageIndex,
PageSize = pageSize,
Items = page.Select(zk =>
{
versions.TryGetValue(zk.Id, out var v);
guigeMap.TryGetValue(zk.Id, out var gs);
return new GyzxsDocVersionDto
{
Id = v?.Id ?? 0,
GyzkId = zk.Id,
ZhuangKouName = zk.ZhuangKouName,
ZhuangKouCode = zk.ZhuangKouCode,
Year = zk.BelongingYear,
Sort = zk.Sort,
ShichanId = v?.ShichanId ?? 0,
FengMianId = v?.FengMianId ?? 0,
XbjId = v?.XbjId ?? 0,
XbjSsdyId = v?.XbjSsdyId ?? 0,
ZhuJianId = v?.ZhuJianId ?? 0,
ZdsId = v?.ZdsId ?? 0,
ZdsDyId = v?.ZdsDyId ?? 0,
HsId = v?.HsId ?? 0,
Guiges = gs?.OrderBy(x => x).ToList() ?? new List<string>()
};
}).ToList()
};
}
public async Task<GyzxsDocVersionDto> CreateDocVersionAsync(long gyzkId)
{
var zk = await _zkRepo.GetByIdAsync(gyzkId) ?? throw new ServiceException("工艺庄口不存在", 404);
var exist = await _versionRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId);
if (exist == null)
{
exist = new GyzxsDocVersion { GyzkId = gyzkId };
await _versionRepo.AddAsync(exist);
}
return new GyzxsDocVersionDto
{
Id = exist.Id, GyzkId = gyzkId, ZhuangKouName = zk.ZhuangKouName,
ZhuangKouCode = zk.ZhuangKouCode, Year = zk.BelongingYear, Sort = zk.Sort
};
}
public async Task DeleteDocVersionAsync(long id)
{
var v = await _versionRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await _versionRepo.DeleteAsync(v);
}
/// <summary>主控表 upsert:任一子单据保存后回写指针列</summary>
private async Task TouchVersionAsync(long gyzkId, Action<GyzxsDocVersion> apply)
{
var v = await _versionRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId);
if (v == null)
{
v = new GyzxsDocVersion { GyzkId = gyzkId };
apply(v);
await _versionRepo.AddAsync(v);
}
else
{
apply(v);
await _versionRepo.UpdateAsync(v);
}
}
// ==================== 封面 ====================
public async Task<GyzxsFengMian> GetFengMianAsync(long gyzkId)
{
var exist = await _fengMianRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId);
if (exist != null) return exist;
// 新封面:按 F9 规则预生成编号 SJZ-{年度}-工艺-{序号}
var zk = await _zkRepo.GetByIdAsync(gyzkId);
var year = zk?.BelongingYear ?? DateTime.Today.Year;
var prefix = $"SJZ-{year}-工艺-";
var used = (await _fengMianRepo.FindAsync(x => !x.IsDeleted))
.Where(x => x.BianHao != null && x.BianHao.StartsWith(prefix));
var max = 0;
foreach (var f in used)
{
if (int.TryParse(f.BianHao.Substring(prefix.Length), out var n) && n > max) max = n;
}
return new GyzxsFengMian { GyzkId = gyzkId, BianHao = prefix + (max + 1), ShengChanCheJian = "" };
}
public async Task<bool> SaveFengMianAsync(GyzxsFengMian dto)
{
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var exist = await _fengMianRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == dto.GyzkId);
var id = await UpsertAsync(_fengMianRepo, dto, exist);
await TouchVersionAsync(dto.GyzkId, v => v.FengMianId = id);
return true;
}
// ==================== 试产(试缫工艺单) ====================
public async Task<GyzxsShichan> GetShichanAsync(long gyzkId, string guige)
=> await _shichanRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId && x.Guige == (guige ?? ""))
?? new GyzxsShichan { GyzkId = gyzkId, Guige = guige ?? "" };
public async Task<bool> SaveShichanAsync(GyzxsShichan dto)
{
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var exist = await _shichanRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == dto.GyzkId && x.Guige == (dto.Guige ?? ""));
var id = await UpsertAsync(_shichanRepo, dto, exist);
await TouchVersionAsync(dto.GyzkId, v => v.ShichanId = id);
return true;
}
// ==================== 混剥选茧 ====================
public async Task<GyzxsXbjDto> GetXbjAsync(long gyzkId)
{
var e = await _xbjRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId);
var dto = new GyzxsXbjDto { Xbj = e ?? new GyzxsXbj { GyzkId = gyzkId } };
if (e != null)
dto.HunJian = (await _hunjianRepo.FindAsync(x => !x.IsDeleted && x.XbjId == e.Id))
.OrderBy(x => x.Sort).ThenBy(x => x.Id).ToList();
return dto;
}
public async Task<bool> SaveXbjAsync(GyzxsXbjDto dto)
{
if (dto.Xbj.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var exist = await _xbjRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == dto.Xbj.GyzkId);
var id = await UpsertAsync(_xbjRepo, dto.Xbj, exist);
var olds = await _hunjianRepo.FindAsync(x => x.XbjId == id);
if (olds.Count > 0) await _hunjianRepo.HardDeleteRangeAsync(olds);
var rows = dto.HunJian ?? new List<GyzxsXbjHunJian>();
for (var i = 0; i < rows.Count; i++)
{
rows[i].Id = 0;
rows[i].XbjId = id;
rows[i].Sort = i + 1;
await _hunjianRepo.AddAsync(rows[i]);
}
await TouchVersionAsync(dto.Xbj.GyzkId, v => v.XbjId = id);
return true;
}
// ==================== 试缫与大样 ====================
public async Task<GyzxsXbjSsDy> GetXbjSsDyAsync(long gyzkId, string guige)
=> await _ssdyRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId && x.Guige == (guige ?? ""))
?? new GyzxsXbjSsDy { GyzkId = gyzkId, Guige = guige ?? "" };
public async Task<bool> SaveXbjSsDyAsync(GyzxsXbjSsDy dto)
{
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var exist = await _ssdyRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == dto.GyzkId && x.Guige == (dto.Guige ?? ""));
var id = await UpsertAsync(_ssdyRepo, dto, exist);
await TouchVersionAsync(dto.GyzkId, v => v.XbjSsdyId = id);
return true;
}
// ==================== 煮茧 ====================
public async Task<GyzxsZhuJianDto> GetZhuJianAsync(long gyzkId)
{
var e = await _zhuJianRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId);
var dto = new GyzxsZhuJianDto { ZhuJian = e ?? new GyzxsZhuJian { GyzkId = gyzkId } };
if (e != null)
{
dto.CheJian = (await _zjCheJianRepo.FindAsync(x => !x.IsDeleted && x.ZhuJianId == e.Id))
.OrderBy(x => x.Sort).ThenBy(x => x.Id).ToList();
dto.FuZhu = (await _zjFuZhuRepo.FindAsync(x => !x.IsDeleted && x.ZhuJianId == e.Id))
.OrderBy(x => x.Sort).ThenBy(x => x.Id).ToList();
}
return dto;
}
public async Task<bool> SaveZhuJianAsync(GyzxsZhuJianDto dto)
{
if (dto.ZhuJian.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var exist = await _zhuJianRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == dto.ZhuJian.GyzkId);
var id = await UpsertAsync(_zhuJianRepo, dto.ZhuJian, exist);
var oldChe = await _zjCheJianRepo.FindAsync(x => x.ZhuJianId == id);
if (oldChe.Count > 0) await _zjCheJianRepo.HardDeleteRangeAsync(oldChe);
var che = dto.CheJian ?? new List<GyzxsZhuJianCheJian>();
for (var i = 0; i < che.Count; i++)
{
che[i].Id = 0; che[i].ZhuJianId = id; che[i].Sort = i + 1;
await _zjCheJianRepo.AddAsync(che[i]);
}
var oldFz = await _zjFuZhuRepo.FindAsync(x => x.ZhuJianId == id);
if (oldFz.Count > 0) await _zjFuZhuRepo.HardDeleteRangeAsync(oldFz);
var fz = dto.FuZhu ?? new List<GyzxsZhuJianFuZhu>();
for (var i = 0; i < fz.Count; i++)
{
fz[i].Id = 0; fz[i].ZhuJianId = id; fz[i].Sort = i + 1;
await _zjFuZhuRepo.AddAsync(fz[i]);
}
await TouchVersionAsync(dto.ZhuJian.GyzkId, v => v.ZhuJianId = id);
return true;
}
// ==================== 自动缫(含大样) ====================
public async Task<GyzxsZdsDto> GetZdsAsync(long gyzkId, string guige)
{
var e = await _zdsRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId && x.Guige == (guige ?? ""));
var dto = new GyzxsZdsDto { Zds = e ?? new GyzxsZds { GyzkId = gyzkId, Guige = guige ?? "" } };
if (e != null)
dto.Dyzl = await _zdsDyRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.ZdsId == e.Id);
return dto;
}
public async Task<bool> SaveZdsAsync(GyzxsZdsDto dto)
{
if (dto.Zds.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var exist = await _zdsRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == dto.Zds.GyzkId && x.Guige == (dto.Zds.Guige ?? ""));
var id = await UpsertAsync(_zdsRepo, dto.Zds, exist);
var dy = dto.Dyzl;
if (dy != null)
{
var oldDy = await _zdsDyRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.ZdsId == id);
dy.ZdsId = id;
dy.GyzkId = dto.Zds.GyzkId;
if (string.IsNullOrWhiteSpace(dy.Guige)) dy.Guige = dto.Zds.Guige;
var dyId = await UpsertAsync(_zdsDyRepo, dy, oldDy);
await TouchVersionAsync(dto.Zds.GyzkId, v => v.ZdsDyId = dyId);
}
await TouchVersionAsync(dto.Zds.GyzkId, v => v.ZdsId = id);
return true;
}
// ==================== 后缫(工艺执行书) ====================
public async Task<GyzxsHs> GetHsAsync(long gyzkId, string guige)
=> await _hsRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == gyzkId && x.Guige == (guige ?? ""))
?? new GyzxsHs { GyzkId = gyzkId, Guige = guige ?? "", ZhiXingDate = DateTime.Today };
public async Task<bool> SaveHsAsync(GyzxsHs dto)
{
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var exist = await _hsRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.GyzkId == dto.GyzkId && x.Guige == (dto.Guige ?? ""));
var id = await UpsertAsync(_hsRepo, dto, exist);
await TouchVersionAsync(dto.GyzkId, v => v.HsId = id);
return true;
}
// ==================== 报表数据 ====================
public async Task<GyzxsPrintDto> GetPrintDataAsync(string type, long gyzkId, string guige)
{
var zk = await _zkRepo.GetByIdAsync(gyzkId);
var zkName = zk?.ZhuangKouName ?? "";
var dto = new GyzxsPrintDto { ReportName = type };
switch ((type ?? "").ToLowerInvariant())
{
case "fengmian":
{
var e = await GetFengMianAsync(gyzkId);
dto.Title = "桑蚕丝工艺设计书";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["编号"] = string.IsNullOrWhiteSpace(e.BianHao) ? $"SJZ-{zk?.BelongingYear ?? DateTime.Today.Year}-工艺" : e.BianHao;
dto.Parameters["缫制规格"] = e.Guige;
dto.Parameters["原料总量"] = $"{e.YuanLiaoZhongLiang:0.##}吨";
dto.Parameters["生产丝量"] = $"{e.ShengSiZhongLiang:0.##}吨";
dto.Parameters["缫制车间"] = e.ShengChanCheJian;
dto.Parameters["生产时间"] = FormatRange(e.ShengChanStartDate, e.ShengChanEndDate);
dto.Parameters["发送部门"] = e.FaSongBuMen;
dto.Parameters["日期"] = DateTime.Today.ToString("yyyy年MM月dd日");
break;
}
case "gyzxs": // 后缫 = 工艺执行书
{
var e = await GetHsAsync(gyzkId, guige);
dto.Title = "工艺执行书";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["执行时间"] = e.ZhiXingDate?.ToString("yyyy/M/d") ?? "";
dto.Parameters["生丝规格"] = e.Guige;
dto.Parameters["前缫生产车间"] = e.QianSaoCheJian;
dto.Parameters["预缫天数"] = e.YuSaoTianShu;
dto.Parameters["整理规格"] = e.ZhengLiGuiGe;
// 生产指标
dto.Parameters["单产"] = e.SczbDanChan;
dto.Parameters["日产"] = e.SczbRiChan;
dto.Parameters["编丝量"] = e.SczbBianSiLiang;
dto.Parameters["整检量"] = e.SczbZhengJianLiang;
dto.Parameters["生产正品率"] = e.SczbShengChanZhengPinLv;
dto.Parameters["商检正品率"] = e.SczbShangJianZhengPinLv;
dto.Parameters["切断"] = e.SczbQieDuan;
dto.Parameters["效率"] = e.SczbXiaoLv;
dto.Parameters["编检品质疵点"] = e.SczbPinZhiCiDian;
// 工艺条件及控制指标
dto.Parameters["车速"] = e.GyzbCheSu;
dto.Parameters["小䈅待返时间"] = e.GyzbXiaoYuDaiFanShiJian;
dto.Parameters["车间温度"] = e.GyzbCheJianWenDu;
dto.Parameters["车间相对湿度"] = e.GyzbCheJianXiangDuiShiDu;
dto.Parameters["车厢温度"] = e.GyzbCheXiangWenDu;
dto.Parameters["车厢湿度"] = e.GyzbCheXiangShiDu;
dto.Parameters["平衡室温度"] = e.GyzbPingHengShiWenDu;
dto.Parameters["平衡室相对湿度"] = e.GyzbPingHengShiXiangDuiShiDu;
dto.Parameters["小䈅给湿温度"] = e.GyzbXiaoYuGeiShiWenDu;
dto.Parameters["小䈅丝片平衡时间"] = e.GyzbXiaoYuSiPianPingHengShiJian;
dto.Parameters["真空度"] = e.GyzbZhenKongDu;
dto.Parameters["抽气次数"] = e.GyzbChouQiCiShu;
dto.Parameters["大䈅丝片平衡时间"] = e.GyzbDaYuSiPianPingHengShiJian;
dto.Parameters["大䈅丝片回潮率"] = e.GyzbDaYuSiPianHuiChaoLv;
dto.Parameters["生丝回潮率"] = e.GyzbShengSiHuiChaoLv;
dto.Parameters["大䈅丝片宽度"] = e.GyzbDaYuSiPianKuanDu;
dto.Parameters["屑丝对丝率"] = e.GyzbXieSiDuiSiLv;
dto.Parameters["大䈅丝片疵点"] = e.GyzbDaYuSiPianCiDian;
dto.Parameters["主要技术措施"] = e.JiShuCuoShi;
dto.Parameters["设计"] = e.SheJiRen;
dto.Parameters["审核"] = e.ShenHeRen;
dto.Parameters["审批"] = e.PiFuRen;
break;
}
case "xbj":
{
var e = await GetXbjAsync(gyzkId);
var x = e.Xbj;
dto.Title = "混剥选茧工艺执行书";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["换庄时间"] = x.HuanZhuangDate?.ToString("yyyy/M/d") ?? "";
dto.Parameters["缫制规格"] = x.Guige;
dto.Parameters["缫制车间"] = x.CheJian;
dto.Parameters["日产量"] = x.ScRiChanLiang;
dto.Parameters["班产量"] = x.ScBanChanLiang;
dto.Parameters["上车率"] = x.ScShangCheLv;
dto.Parameters["联统茧率"] = x.ScLianTongLv;
dto.Parameters["五级茧率"] = x.ScWuJiLv;
dto.Parameters["下茧茧衣率"] = x.ScXiaJiaoLv;
dto.Parameters["茧衣率"] = x.ScJianYiLv;
dto.Parameters["技术措施要求"] = x.ScJiShuCuoShi;
dto.Parameters["统号茧五级茧率"] = x.QcTongHaoWuJiLv;
dto.Parameters["统号茧下茧率"] = x.QcTongHaoXiaJianLv;
dto.Parameters["五级茧联统茧率"] = x.QcWuJiLianTongLv;
dto.Parameters["五级茧下脚率"] = x.QcWuJiXiaJiaoLv;
dto.Parameters["下脚茧联统率"] = x.QcXiaJiaoLianTongLv;
dto.Parameters["下脚茧次茧率"] = x.QcXiaJiaoCiJianLv;
dto.Parameters["好五茧组产量"] = x.QcZbHaoWuZuChanLiang;
dto.Parameters["六级组产量"] = x.QcZbLiuJiZuChanLiang;
dto.Parameters["棉茧组产量"] = x.QcZbMianJianZuChanLiang;
dto.Parameters["下脚组产量"] = x.QcZbXiaJiaoZuChanLiang;
dto.Parameters["双宫组产量"] = x.QcZbShuangGongZuChanLiang;
dto.Parameters["公斤粒数"] = x.QcZbGongJinLiShu;
dto.Parameters["剥光率"] = x.QcZbBaoGuangLv;
dto.Parameters["质量控制茧衣率"] = x.QcZbJianYiLv;
dto.Parameters["合计包数"] = e.HunJian.Sum(i => i.BaoShu);
dto.Parameters["合计比例"] = e.HunJian.Sum(i => i.BiLi);
dto.Parameters["合计混茧包数"] = e.HunJian.Sum(i => i.HunJianBaoShu);
dto.Sub1 = e.HunJian.Select((i, idx) => new Dictionary<string, object?>
{
["序号"] = idx + 1,
["庄别"] = i.ZhuangBie,
["总包数"] = i.BaoShu,
["比例"] = i.BiLi,
["混茧包数"] = i.HunJianBaoShu
}).ToList();
break;
}
case "shichan":
{
var e = await GetShichanAsync(gyzkId, guige);
dto.Title = "自动缫试缫工艺单";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["试缫日期"] = e.ScDate?.ToString("yyyy/M/d") ?? "";
dto.Parameters["换庄方式"] = e.HuanZhuangFangShi;
dto.Parameters["规格"] = e.Guige;
dto.Parameters["生产车间"] = e.ScCheJian;
dto.Parameters["一茧丝长"] = e.YiJianSiChang;
dto.Parameters["解舒率"] = e.JieShuLv;
dto.Parameters["解舒丝长"] = e.JieShuSiChang;
dto.Parameters["茧层率"] = e.JianCengLv;
dto.Parameters["茧丝纤度"] = e.JianSiXianDu;
dto.Parameters["解舒光折"] = e.JieShuGuangZhe;
dto.Parameters["万吊"] = e.WanDiao;
dto.Parameters["车速"] = e.ZdsCheSu;
dto.Parameters["允许粒数"] = e.ZdsYunXuLiShu;
dto.Parameters["缫折"] = e.ZdsSaoZhe;
dto.Parameters["中心纤度"] = e.ZdsZhongXinXianDu;
dto.Parameters["中心粒符率"] = e.ZdsZhongXinLiFuLv;
dto.Parameters["缫丝温度"] = e.ZdsSaoSiWenDu;
dto.Parameters["计算纤度"] = e.ZdsJiSuanXianDu;
dto.Parameters["允许粒符率"] = e.ZdsYunXuLiFuLv;
dto.Parameters["索绪温度"] = e.ZdsSuoXuWenDu;
dto.Parameters["平均粒数"] = e.ZdsPingJunLiShu;
dto.Parameters["白茧引绪"] = e.ZdsBaiJianYinXu;
dto.Parameters["理绪温度"] = e.ZdsLiXuWenDu;
dto.Parameters["中心粒数"] = e.ZdsZhongXinLiShu;
dto.Parameters["给茧口宽度"] = e.ZdsGeiJianKouKuanDu;
dto.Parameters["落丝笼数"] = e.ZdsLuoSiLongShu;
dto.Parameters["笼茧粒数"] = e.ZdsLongJianLiShu;
dto.Parameters["煮茧车速"] = e.ZjCheSu;
dto.Parameters["蒸煮室温度"] = $"{e.ZjZhengZhuShi1}、{e.ZjZhengZhuShi2}、{e.ZjZhengZhuShi3}";
dto.Parameters["外层中层内层掉绪"] = $"{e.ZjLuoXuWai} {e.ZjLuoXuZhong} {e.ZjLuoXuNei}";
dto.Parameters["调整部温度"] = e.ZjTiaoZhengBuWenDu;
dto.Parameters["出口温度"] = e.ZjChuKouWenDu;
dto.Parameters["附煮车间"] = e.ZjFuZhuCheJian;
dto.Parameters["附煮庄口"] = e.ZjFuZhuGyzkName;
dto.Parameters["设计"] = e.SheJiRen;
dto.Parameters["审核"] = e.ShenHeRen;
dto.Parameters["审批"] = e.PiFuRen;
break;
}
case "ssdy":
{
var e = await GetXbjSsDyAsync(gyzkId, guige);
dto.Title = "(三)试缫与大样资料";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["规格"] = e.Guige;
dto.Parameters["主要技术措施"] = e.JiShuCuoShi;
dto.Parameters["设计"] = e.SheJiRen;
dto.Parameters["审核"] = e.ShenHeRen;
dto.Parameters["审批"] = e.PiFuRen;
dto.Main = new List<Dictionary<string, object?>>
{
new()
{
["项目"] = "试缫",
["上车率"] = e.SsShangCheLv, ["联统率"] = e.SsLianTongLv, ["次茧率"] = e.SsCiJianLv,
["次六级"] = e.SsCiLiuJiLv, ["棉茧率"] = e.SsMianJianLv, ["下脚率"] = e.SsXiaJiaoLv,
["双宫率"] = e.SsShuangGongLv, ["茧衣率"] = e.SsJianYiLv,
["400粒重"] = e.SsLiJianZhong, ["公斤粒数"] = e.SsGongJinLiShu
},
new()
{
["项目"] = "大样",
["上车率"] = e.DyShangCheLv, ["联统率"] = e.DyLianTongLv, ["次茧率"] = e.DyCiJianLv,
["次六级"] = e.DyCiLiuJiLv, ["棉茧率"] = e.DyMianJianLv, ["下脚率"] = e.DyXiaJiaoLv,
["双宫率"] = e.DyShuangGongLv, ["茧衣率"] = e.DyJianYiLv,
["400粒重"] = e.DyLiJianZhong, ["公斤粒数"] = e.DyGongJinLiShu
}
};
break;
}
case "zhujian":
{
var e = await GetZhuJianAsync(gyzkId);
var z = e.ZhuJian;
dto.Title = "煮茧工艺执行书";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["缫制规格"] = z.Guige;
dto.Parameters["换庄时间"] = z.HuanZhuangDate?.ToString("yyyy/M/d") ?? "";
dto.Parameters["接缫方式"] = z.HuanZhuangFangShi;
dto.Parameters["生产车间"] = z.CheJian;
dto.Parameters["煮茧机号"] = z.ZhuJianJi;
// 庄口原料性能
dto.Parameters["公斤粒数"] = z.YlGongJinLiShu;
dto.Parameters["缩绉"] = z.YlSuoZhou;
dto.Parameters["茧层率"] = z.YlJianCengLv;
dto.Parameters["清洁"] = z.YlQingJie;
dto.Parameters["净度"] = z.YlJingDu;
dto.Parameters["粒茧丝长"] = z.YlLiJianSiChang;
dto.Parameters["解舒率"] = z.YlJieShuLv;
dto.Parameters["解舒丝长"] = z.YlJieShuSiChang;
dto.Parameters["茧丝纤度"] = z.YlJianSiXianDu;
dto.Parameters["万米吊糙"] = z.YlWanMiDiaoCao;
dto.Parameters["解舒光折"] = z.YlJieShuGuangZhe;
// 工艺技术指标
dto.Parameters["实缫解舒"] = z.GyzbJieShuLv;
dto.Parameters["实缫万吊"] = z.GyzbWanDiao;
dto.Parameters["工艺清洁"] = z.GyzbQingJie;
dto.Parameters["工艺净度"] = z.GyzbJingDu;
dto.Parameters["丝胶溶失率"] = z.GyzbSiJiaoRongShiLv;
dto.Parameters["组落丝笼数"] = z.GyzbZuLuoSiLongShu;
dto.Parameters["缫折"] = z.GyzbSaoZhe;
dto.Parameters["笼茧粒数"] = z.GyzbLongJianLiShu;
dto.Parameters["每桶茧量"] = z.GyzbMeiTongJianLiang;
dto.Parameters["每包桶数"] = z.GyzbMeiBaoTongShu;
dto.Parameters["每小时用茧笼数"] = z.GyzbMeiXiaoShiYongJianLong;
dto.Parameters["条吐率"] = z.GyzbTiaoTuLv;
// 真空渗透
dto.Parameters["真空度"] = z.ZkstZhenKongDu;
dto.Parameters["吸水温度"] = z.ZkstXiShuiWenDu;
dto.Parameters["达到最高真空度时长"] = z.ZkstDaZhenKongShiChang;
dto.Parameters["液面到最高位时长"] = z.ZkstZuiGaoWeiShiChang;
dto.Parameters["关水后继续真空时长"] = z.ZkstGuanShuiZhenKongShiChang;
dto.Parameters["出茧温度"] = z.ZkstChuJianWenDu;
dto.Parameters["全机回转时间"] = z.ZkstQuanJiHuiZhuanShiJian;
dto.Parameters["主要技术措施"] = z.JiShuCuoShi;
dto.Parameters["设计"] = z.SheJiRen;
dto.Parameters["审核"] = z.ShenHeRen;
dto.Parameters["审批"] = z.PiFuRen;
dto.Sub1 = e.CheJian.Select(c => new Dictionary<string, object?>
{
["车间"] = c.CheJian, ["低温渗透"] = c.DiWenShenTou,
["蒸煮室1"] = c.ZhengZhuShi1, ["蒸煮室2"] = c.ZhengZhuShi2, ["蒸煮室3"] = c.ZhengZhuShi3,
["调整部1"] = c.TiaoZhengBu1, ["调整部2"] = c.TiaoZhengBu2, ["调整部3"] = c.TiaoZhengBu3,
["调整部4"] = c.TiaoZhengBu4, ["调整部5"] = c.TiaoZhengBu5,
["出茧温度"] = c.ChuJianWenDu, ["全机回转时间"] = c.HuiZhuanShiJian
}).ToList();
dto.Sub2 = e.FuZhu.Select(c => new Dictionary<string, object?>
{
["附煮车间"] = c.CheJian, ["解舒"] = c.JieShuLv, ["万吊"] = c.WanDiao, ["缫折"] = c.SaoZhe,
["温度1"] = c.WenDu1, ["温度2"] = c.WenDu2, ["温度3"] = c.WenDu3,
["温度4"] = c.WenDu4, ["温度5"] = c.WenDu5
}).ToList();
break;
}
case "zds":
{
var e = await GetZdsAsync(gyzkId, guige);
var z = e.Zds;
dto.Title = "自动缫车间生产工艺执行书(表一)";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["接缫日期"] = z.HuanZhuangDate?.ToString("yyyy/M/d") ?? "";
dto.Parameters["换庄方式"] = z.HuanZhuangFangShi;
dto.Parameters["规格"] = z.Guige;
dto.Parameters["生产车间"] = z.CheJian;
dto.Parameters["预缫天数"] = z.YuSaoTianShu;
dto.Parameters["预缫丝量"] = z.YuSaoSiLiang;
// 茧质概况
dto.Parameters["一茧丝长"] = z.ZgYiJianSiChang;
dto.Parameters["解舒丝长"] = z.ZgJieShuSiChang;
dto.Parameters["解舒率"] = z.ZgJieShuLv;
dto.Parameters["茧丝纤度"] = z.ZgJianSiXianDu;
dto.Parameters["解舒光折"] = z.ZgJieShuGuangZhe;
dto.Parameters["万米吊糙"] = z.ZgWanMiDiaoCao;
dto.Parameters["清洁分"] = z.ZgQingJie;
dto.Parameters["洁净分"] = z.ZgJingDu;
dto.Parameters["茧层率"] = z.ZgJianCengLv;
dto.Parameters["茧丝纤度均方差"] = z.ZgXianDuJunFangCha;
dto.Parameters["外三百回"] = z.ZgWaiSanBaiHui;
dto.Parameters["中三百回"] = z.ZgZhongSanBaiHui;
dto.Parameters["内三百回"] = z.ZgNeiSanBaiHui;
dto.Parameters["最大开差"] = z.ZgZuiDaKaiCha;
dto.Parameters["内外开差"] = z.ZgNeiWaiKaiCha;
dto.Parameters["白色茧单纤"] = z.ZgBaiSeJianDanXian;
dto.Parameters["灰色茧单纤"] = z.ZgHuiSeJianDanXian;
dto.Parameters["红色茧单纤"] = z.ZgHongSeJianDanXian;
dto.Parameters["落绪外"] = z.ZgLuoXuWai;
dto.Parameters["落绪中"] = z.ZgLuoXuZhong;
dto.Parameters["落绪内"] = z.ZgLuoXuNei;
dto.Parameters["平均茧幅"] = z.ZgPingJunJianFu;
dto.Parameters["茧幅整齐率"] = z.ZgJianFuZhengQiLv;
dto.Parameters["茧丝百回纤度"] = z.ZgJianSiBaiHuiXianDu;
// 生产指标
dto.Parameters["品味"] = z.SczbPinWei;
dto.Parameters["缫折"] = z.SczbSaoZhe;
dto.Parameters["台产"] = z.SczbTaiChan;
dto.Parameters["偏差"] = z.SczbPianCha;
dto.Parameters["总差"] = z.SczbZongCha;
dto.Parameters["清洁"] = z.SczbQingJie;
dto.Parameters["洁净"] = z.SczbJingJie;
dto.Parameters["匀二变化"] = z.SczbYunEr;
dto.Parameters["生产正品率"] = z.SczbZhengPinLv;
// 工艺技术指标
dto.Parameters["车速"] = z.GyzbCheSu;
dto.Parameters["添绪次数"] = z.GyzbTianXuCiShu;
dto.Parameters["运转率"] = z.GyzbYunZhuanLv;
dto.Parameters["中心纤度"] = z.GyzbZhongXinXianDu;
dto.Parameters["设计单纤度"] = z.GyzbSheJiDanXianDu;
dto.Parameters["平均粒数"] = z.GyzbPingJunLiShu;
dto.Parameters["中心粒数"] = z.GyzbZhongXinLiShu;
dto.Parameters["允许粒数"] = z.GyzbYunXuLiShu;
dto.Parameters["引绪"] = z.GyzbYinXu;
dto.Parameters["中心粒符率"] = z.GyzbZhongXinLiFuLv;
dto.Parameters["允许粒符率"] = z.GyzbYunXuLiFuLv;
dto.Parameters["给茧口宽度"] = z.GyzbGeiJianKouKuanDu;
dto.Parameters["条吐率"] = z.GyzbTiaoTuLv;
dto.Parameters["丝鞘长度"] = z.GyzbSiQiaoChangDu;
dto.Parameters["车头汽压"] = z.GyzbCheTouQiYa;
dto.Parameters["索绪温度"] = z.GyzbSuoXuWenDu;
dto.Parameters["理绪温度"] = z.GyzbLiXuWenDu;
dto.Parameters["偏心盘茧量"] = z.GyzbPianXinPanJianLiang;
dto.Parameters["缫汤温度"] = z.GyzbSaoTangWenDu;
dto.Parameters["加茧口茧量"] = z.GyzbJiaJianKouJianLiang;
dto.Parameters["无绪率"] = z.GyzbWuXuLv;
dto.Parameters["蓬糙茧率"] = z.GyzbPengCaoJianLv;
dto.Parameters["加茧均匀率"] = z.GyzbJiaJianJunYunLv;
dto.Parameters["正确添绪率"] = z.GyzbZhengQueTianXuLv;
dto.Parameters["桶茧量"] = z.GyzbTongJianLiang;
dto.Parameters["落丝笼数"] = z.GyzbLuoSiLongShu;
dto.Parameters["设计"] = z.SheJiRen;
dto.Parameters["审核"] = z.ShenHeRen;
dto.Parameters["审批"] = z.PiFuRen;
break;
}
case "zdsdy":
{
var e = await GetZdsAsync(gyzkId, guige);
var d = e.Dyzl ?? new GyzxsZdsDyzl { GyzkId = gyzkId, Guige = guige ?? "" };
dto.Title = "自动缫大样(表二)";
dto.Parameters["庄口名称"] = zkName;
dto.Parameters["规格"] = d.Guige;
dto.Parameters["偏差"] = d.PianCha;
dto.Parameters["清洁"] = d.QingJie;
dto.Parameters["净度"] = d.JingDu;
dto.Parameters["解舒率"] = d.JieShuLv;
dto.Parameters["万米吊糙"] = d.WanMiDiaoCao;
dto.Parameters["缫折"] = d.SaoZhe;
dto.Parameters["技术措施及要求"] = d.JiShuCuoShi;
dto.Parameters["设计"] = d.SheJiRen;
dto.Parameters["审核"] = d.ShenHeRen;
dto.Parameters["审批"] = d.PiFuRen;
break;
}
default:
throw new ServiceException($"未知的报表类型:{type}");
}
return dto;
}
// ==================== 内部工具 ====================
private static async Task<long> UpsertAsync<T>(IRepository<T> repo, T dto, T? exist) where T : class, IEntity
{
if (exist == null)
{
dto.Id = 0;
dto.IsDeleted = false;
await repo.AddAsync(dto);
return dto.Id;
}
dto.Id = exist.Id;
dto.CreatedAt = exist.CreatedAt;
dto.IsDeleted = false;
await repo.UpdateAsync(dto);
return dto.Id;
}
private static string FormatRange(DateTime? start, DateTime? end)
{
if (start == null && end == null) return "";
var s = start?.ToString("yy年M月d日") ?? "";
var e2 = end?.ToString("yy年M月d日") ?? "";
return string.IsNullOrEmpty(e2) ? s : $"{s}-{e2}";
}
}
@@ -0,0 +1,551 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.QC;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.QC.Yjss;
using F9WebAiTest.Domain.Entities.SYS.YuanliaoZhuangkous;
namespace F9WebAiTest.Application.Services.QC;
public interface IYjssService
{
Task<PageResult<YjssListItemDto>> GetPagedAsync(int pageIndex, int pageSize, int type, int year, string keyword);
Task<YjssListItemDto> SaveYlxyAsync(YjssMainYlxy dto);
Task<YjssListItemDto> SaveZhyAsync(YjssMainZhy dto);
Task DeleteAsync(int type, long id);
Task<YjssDcXjdc> GetXjdcAsync(int type, long yjssId);
Task<bool> SaveXjdcAsync(int type, YjssDcXjdc dto);
Task<List<YjssDcJsdc>> GetJsdcAsync(int type, long yjssId);
Task<bool> SaveJsdcAsync(int type, YjssDcJsdc dto);
Task DeleteJsdcAsync(int type, long id);
Task<YjssJfdcDto> GetJfdcAsync(int type, long yjssId);
Task<bool> SaveJfdcAsync(int type, YjssJfdcDto dto);
Task<YjssDcQpdc> GetQpdcAsync(int type, long yjssId);
Task<bool> SaveQpdcAsync(int type, YjssDcQpdc dto);
Task<YjssDcQjjj> GetQjjjAsync(int type, long yjssId);
Task<bool> SaveQjjjAsync(int type, YjssDcQjjj dto);
Task<YjssJsbhxdDto> GetJsbhxdAsync(int type, long yjssId);
Task<bool> SaveJsbhxdAsync(int type, YjssJsbhxdDto dto);
/// <summary>整页数据(含报表参数区):用于「综合样报表 / 原料小样报表 / 解舒调查报表」</summary>
Task<YjssFullDto> GetFullAsync(int type, long yjssId);
/// <summary>解舒调查平均值汇总(跨多条记录,口径对齐 F9 GetDataByYjssCommand)</summary>
Task<Dictionary<string, object?>> GetJsdcSummaryAsync(int type, long yjssId);
}
/// <summary>
/// 质量模块 —— 样茧试缫(对应 F9 生产\质量\样茧试缫)。
/// 两类样品:原料小样(选茧 / 解舒 / 清洁洁净)、综合样(加 茧幅 / 切剖 / 茧丝百回纤度)。
/// 子表统一外键 yjss_id;一对一子表保存后回写主表指针,解舒/百回纤度回写记录数。
/// </summary>
public class YjssService : IYjssService
{
private readonly IRepository<YjssMainYlxy> _ylxyRepo;
private readonly IRepository<YjssMainZhy> _zhyRepo;
private readonly IRepository<YjssDcXjdc> _xjdcRepo;
private readonly IRepository<YjssDcJsdc> _jsdcRepo;
private readonly IRepository<YjssDcJfdcMain> _jfdcRepo;
private readonly IRepository<YjssDcJfdcItem> _jfdcItemRepo;
private readonly IRepository<YjssDcQpdc> _qpdcRepo;
private readonly IRepository<YjssDcQjjj> _qjjjRepo;
private readonly IRepository<YjssDcJsbhxdMain> _jsbhxdMainRepo;
private readonly IRepository<YjssDcJsbhxd> _jsbhxdRepo;
private readonly IRepository<YuanliaoZhuangkou> _ylzkRepo;
private readonly IRepository<CraftZhuangKou> _gyzkRepo;
public YjssService(
IRepository<YjssMainYlxy> ylxyRepo,
IRepository<YjssMainZhy> zhyRepo,
IRepository<YjssDcXjdc> xjdcRepo,
IRepository<YjssDcJsdc> jsdcRepo,
IRepository<YjssDcJfdcMain> jfdcRepo,
IRepository<YjssDcJfdcItem> jfdcItemRepo,
IRepository<YjssDcQpdc> qpdcRepo,
IRepository<YjssDcQjjj> qjjjRepo,
IRepository<YjssDcJsbhxdMain> jsbhxdMainRepo,
IRepository<YjssDcJsbhxd> jsbhxdRepo,
IRepository<YuanliaoZhuangkou> ylzkRepo,
IRepository<CraftZhuangKou> gyzkRepo)
{
_ylxyRepo = ylxyRepo;
_zhyRepo = zhyRepo;
_xjdcRepo = xjdcRepo;
_jsdcRepo = jsdcRepo;
_jfdcRepo = jfdcRepo;
_jfdcItemRepo = jfdcItemRepo;
_qpdcRepo = qpdcRepo;
_qjjjRepo = qjjjRepo;
_jsbhxdMainRepo = jsbhxdMainRepo;
_jsbhxdRepo = jsbhxdRepo;
_ylzkRepo = ylzkRepo;
_gyzkRepo = gyzkRepo;
}
private static string SampleTypeName(int t) => t switch
{
1 => "定价抽样",
2 => "入库抽样",
3 => "鲜茧抽样",
_ => ""
};
// ==================== 主表 ====================
public async Task<PageResult<YjssListItemDto>> GetPagedAsync(int pageIndex, int pageSize, int type, int year, string keyword)
{
if (year <= 0) year = DateTime.Today.Year;
var items = new List<YjssListItemDto>();
var ylzks = (await _ylzkRepo.FindAsync(x => !x.IsDeleted)).ToDictionary(x => x.Id, x => x.Name);
var gyzks = (await _gyzkRepo.FindAsync(x => !x.IsDeleted)).ToDictionary(x => x.Id, x => x.ZhuangKouName);
if (type == 1)
{
var rows = await _ylxyRepo.FindAsync(x => !x.IsDeleted && x.Date.Year == year);
items = rows.Select(x => new YjssListItemDto
{
Id = x.Id, Type = 1, ZhuangKouId = x.YlzkId,
ZhuangKouName = ylzks.GetValueOrDefault(x.YlzkId, ""),
Date = x.Date, SampleType = x.Type, SampleTypeName = SampleTypeName(x.Type),
ZhongLiang = x.ZhongLiang, HuiChaoLv = x.HuiChaoLv,
XjdcId = x.XjdcId, JsdcCt = x.JsdcCt, QjjjId = x.QjjjId, Memo = x.Memo
}).ToList();
}
else
{
var rows = await _zhyRepo.FindAsync(x => !x.IsDeleted && x.Date.Year == year);
items = rows.Select(x => new YjssListItemDto
{
Id = x.Id, Type = 2, ZhuangKouId = x.GyzkId,
ZhuangKouName = gyzks.GetValueOrDefault(x.GyzkId, ""),
Date = x.Date, SampleType = 2, SampleTypeName = "综合样",
ZhongLiang = x.ZhongLiang, XjdcId = x.XjdcId, JsdcCt = x.JsdcCt,
JfdcId = x.JfdcId, QpdcId = x.QpdcId, QjjjId = x.QjjjId,
JsbhxdCt = x.JsbhxdCt, Memo = x.Memo
}).ToList();
}
if (!string.IsNullOrWhiteSpace(keyword))
items = items.Where(x => x.ZhuangKouName.Contains(keyword)).ToList();
items = items.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id).ToList();
var total = items.Count;
return new PageResult<YjssListItemDto>
{
Total = total, PageIndex = pageIndex, PageSize = pageSize,
Items = items.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList()
};
}
public async Task<YjssListItemDto> SaveYlxyAsync(YjssMainYlxy dto)
{
if (dto.YlzkId <= 0) throw new ServiceException("请选择原料庄口");
if (dto.Date == default) dto.Date = DateTime.Today;
var exist = dto.Id > 0 ? await _ylxyRepo.GetByIdAsync(dto.Id) : null;
if (exist == null)
{
dto.Id = 0; dto.IsDeleted = false;
await _ylxyRepo.AddAsync(dto);
}
else
{
dto.CreatedAt = exist.CreatedAt; dto.IsDeleted = false;
await _ylxyRepo.UpdateAsync(dto);
}
var name = (await _ylzkRepo.GetByIdAsync(dto.YlzkId))?.Name ?? "";
return new YjssListItemDto
{
Id = dto.Id, Type = 1, ZhuangKouId = dto.YlzkId, ZhuangKouName = name,
Date = dto.Date, SampleType = dto.Type, SampleTypeName = SampleTypeName(dto.Type),
ZhongLiang = dto.ZhongLiang, HuiChaoLv = dto.HuiChaoLv,
XjdcId = dto.XjdcId, JsdcCt = dto.JsdcCt, QjjjId = dto.QjjjId, Memo = dto.Memo
};
}
public async Task<YjssListItemDto> SaveZhyAsync(YjssMainZhy dto)
{
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
if (dto.Date == default) dto.Date = DateTime.Today;
var exist = dto.Id > 0 ? await _zhyRepo.GetByIdAsync(dto.Id) : null;
if (exist == null)
{
dto.Id = 0; dto.IsDeleted = false;
await _zhyRepo.AddAsync(dto);
}
else
{
dto.CreatedAt = exist.CreatedAt; dto.IsDeleted = false;
await _zhyRepo.UpdateAsync(dto);
}
var name = (await _gyzkRepo.GetByIdAsync(dto.GyzkId))?.ZhuangKouName ?? "";
return new YjssListItemDto
{
Id = dto.Id, Type = 2, ZhuangKouId = dto.GyzkId, ZhuangKouName = name,
Date = dto.Date, SampleType = 2, SampleTypeName = "综合样",
ZhongLiang = dto.ZhongLiang, XjdcId = dto.XjdcId, JsdcCt = dto.JsdcCt,
JfdcId = dto.JfdcId, QpdcId = dto.QpdcId, QjjjId = dto.QjjjId,
JsbhxdCt = dto.JsbhxdCt, Memo = dto.Memo
};
}
public async Task DeleteAsync(int type, long id)
{
if (type == 1)
{
var e = await _ylxyRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await _ylxyRepo.DeleteAsync(e);
}
else
{
var e = await _zhyRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await _zhyRepo.DeleteAsync(e);
}
// 连带软删除子单据(按 主表 Id + 样品类型 精确定位)
foreach (var x in await _xjdcRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _xjdcRepo.DeleteAsync(x);
foreach (var x in await _jsdcRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _jsdcRepo.DeleteAsync(x);
foreach (var x in await _jfdcItemRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _jfdcItemRepo.DeleteAsync(x);
foreach (var x in await _jfdcRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _jfdcRepo.DeleteAsync(x);
foreach (var x in await _qpdcRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _qpdcRepo.DeleteAsync(x);
foreach (var x in await _qjjjRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _qjjjRepo.DeleteAsync(x);
foreach (var x in await _jsbhxdRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _jsbhxdRepo.DeleteAsync(x);
foreach (var x in await _jsbhxdMainRepo.FindAsync(x => !x.IsDeleted && x.YjssId == id && x.YjssType == type)) await _jsbhxdMainRepo.DeleteAsync(x);
}
// ==================== 选茧调查 ====================
public async Task<YjssDcXjdc> GetXjdcAsync(int type, long yjssId)
=> await _xjdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type)
?? new YjssDcXjdc { YjssId = yjssId, YjssType = type };
public async Task<bool> SaveXjdcAsync(int type, YjssDcXjdc dto)
{
dto.YjssType = type;
var exist = await _xjdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == dto.YjssId && x.YjssType == type);
var id = await UpsertAsync(_xjdcRepo, dto, exist);
await SetParentPointerAsync(type, dto.YjssId, xjdcId: id, qjjjId: null, jfdcId: null, qpdcId: null, jsdcCt: null, jsbhxdCt: null);
return true;
}
// ==================== 解舒调查(多条) ====================
public async Task<List<YjssDcJsdc>> GetJsdcAsync(int type, long yjssId)
=> (await _jsdcRepo.FindAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type))
.OrderBy(x => x.XuHao).ThenBy(x => x.Id).ToList();
public async Task<bool> SaveJsdcAsync(int type, YjssDcJsdc dto)
{
dto.YjssType = type;
var exist = dto.Id > 0 ? await _jsdcRepo.GetByIdAsync(dto.Id) : null;
if (exist == null)
{
dto.Id = 0; dto.IsDeleted = false;
if (dto.XuHao <= 0) dto.XuHao = (await GetJsdcAsync(type, dto.YjssId)).Count + 1;
await _jsdcRepo.AddAsync(dto);
}
else
{
dto.CreatedAt = exist.CreatedAt; dto.IsDeleted = false;
await _jsdcRepo.UpdateAsync(dto);
}
var ct = (await _jsdcRepo.FindAsync(x => !x.IsDeleted && x.YjssId == dto.YjssId && x.YjssType == type)).Count;
await SetParentPointerAsync(type, dto.YjssId, null, null, null, null, ct, null);
return true;
}
public async Task DeleteJsdcAsync(int type, long id)
{
var e = await _jsdcRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await _jsdcRepo.DeleteAsync(e);
var ct = (await _jsdcRepo.FindAsync(x => !x.IsDeleted && x.YjssId == e.YjssId && x.YjssType == e.YjssType)).Count;
await SetParentPointerAsync(type, e.YjssId, null, null, null, null, ct, null);
}
// ==================== 茧幅调查 ====================
public async Task<YjssJfdcDto> GetJfdcAsync(int type, long yjssId)
{
var main = await _jfdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type);
var dto = new YjssJfdcDto { Main = main ?? new YjssDcJfdcMain { YjssId = yjssId, YjssType = type } };
if (main != null)
dto.Items = (await _jfdcItemRepo.FindAsync(x => !x.IsDeleted && x.JfdcId == main.Id))
.OrderBy(x => x.Sort).ThenBy(x => x.JianFu).ToList();
return dto;
}
public async Task<bool> SaveJfdcAsync(int type, YjssJfdcDto dto)
{
dto.Main.YjssType = type;
var exist = await _jfdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == dto.Main.YjssId && x.YjssType == type);
var id = await UpsertAsync(_jfdcRepo, dto.Main, exist);
var olds = await _jfdcItemRepo.FindAsync(x => x.JfdcId == id);
if (olds.Count > 0) await _jfdcItemRepo.HardDeleteRangeAsync(olds);
var rows = dto.Items ?? new List<YjssDcJfdcItem>();
for (var i = 0; i < rows.Count; i++)
{
rows[i].Id = 0; rows[i].JfdcId = id; rows[i].YjssId = dto.Main.YjssId;
rows[i].YjssType = type; rows[i].Sort = i + 1;
await _jfdcItemRepo.AddAsync(rows[i]);
}
await SetParentPointerAsync(type, dto.Main.YjssId, null, null, id, null, null, null);
return true;
}
// ==================== 切剖调查 ====================
public async Task<YjssDcQpdc> GetQpdcAsync(int type, long yjssId)
=> await _qpdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type)
?? new YjssDcQpdc { YjssId = yjssId, YjssType = type };
public async Task<bool> SaveQpdcAsync(int type, YjssDcQpdc dto)
{
dto.YjssType = type;
var exist = await _qpdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == dto.YjssId && x.YjssType == type);
var id = await UpsertAsync(_qpdcRepo, dto, exist);
await SetParentPointerAsync(type, dto.YjssId, null, null, null, id, null, null);
return true;
}
// ==================== 清洁洁净 ====================
public async Task<YjssDcQjjj> GetQjjjAsync(int type, long yjssId)
=> await _qjjjRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type)
?? new YjssDcQjjj { YjssId = yjssId, YjssType = type };
public async Task<bool> SaveQjjjAsync(int type, YjssDcQjjj dto)
{
dto.YjssType = type;
var exist = await _qjjjRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == dto.YjssId && x.YjssType == type);
var id = await UpsertAsync(_qjjjRepo, dto, exist);
await SetParentPointerAsync(type, dto.YjssId, null, id, null, null, null, null);
return true;
}
// ==================== 茧丝百回纤度 ====================
public async Task<YjssJsbhxdDto> GetJsbhxdAsync(int type, long yjssId)
{
var main = await _jsbhxdMainRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type);
var dto = new YjssJsbhxdDto { Main = main ?? new YjssDcJsbhxdMain { YjssId = yjssId, YjssType = type } };
if (main != null)
dto.Items = (await _jsbhxdRepo.FindAsync(x => !x.IsDeleted && x.JsbhxdMainId == main.Id))
.OrderBy(x => x.HuiShu).ToList();
return dto;
}
public async Task<bool> SaveJsbhxdAsync(int type, YjssJsbhxdDto dto)
{
dto.Main.YjssType = type;
var exist = await _jsbhxdMainRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == dto.Main.YjssId && x.YjssType == type);
var id = await UpsertAsync(_jsbhxdMainRepo, dto.Main, exist);
var olds = await _jsbhxdRepo.FindAsync(x => x.JsbhxdMainId == id);
if (olds.Count > 0) await _jsbhxdRepo.HardDeleteRangeAsync(olds);
var rows = dto.Items ?? new List<YjssDcJsbhxd>();
for (var i = 0; i < rows.Count; i++)
{
rows[i].Id = 0; rows[i].JsbhxdMainId = id; rows[i].YjssId = dto.Main.YjssId;
rows[i].YjssType = type; rows[i].Sort = i + 1;
await _jsbhxdRepo.AddAsync(rows[i]);
}
await SetParentPointerAsync(type, dto.Main.YjssId, null, null, null, null, null, rows.Count);
return true;
}
// ==================== 整页 / 报表 ====================
public async Task<YjssFullDto> GetFullAsync(int type, long yjssId)
{
var dto = new YjssFullDto { Type = type };
if (type == 1)
{
var m = await _ylxyRepo.GetByIdAsync(yjssId) ?? throw new ServiceException("原料小样不存在", 404);
dto.ZhuangKouName = (await _ylzkRepo.GetByIdAsync(m.YlzkId))?.Name ?? "";
dto.Date = m.Date; dto.ZhongLiang = m.ZhongLiang; dto.HuiChaoLv = m.HuiChaoLv;
dto.SampleTypeName = SampleTypeName(m.Type); dto.Memo = m.Memo;
}
else
{
var m = await _zhyRepo.GetByIdAsync(yjssId) ?? throw new ServiceException("综合样不存在", 404);
dto.ZhuangKouName = (await _gyzkRepo.GetByIdAsync(m.GyzkId))?.ZhuangKouName ?? "";
dto.Date = m.Date; dto.ZhongLiang = m.ZhongLiang; dto.SampleTypeName = "综合样"; dto.Memo = m.Memo;
}
var xjdc = await _xjdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type);
if (xjdc != null) dto.Xjdc = xjdc;
dto.Jsdc = await GetJsdcAsync(type, yjssId);
var jfdcMain = await _jfdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type);
if (jfdcMain != null)
dto.Jfdc = new YjssJfdcDto
{
Main = jfdcMain,
Items = (await _jfdcItemRepo.FindAsync(x => !x.IsDeleted && x.JfdcId == jfdcMain.Id)).OrderBy(x => x.JianFu).ToList()
};
dto.Qpdc = await _qpdcRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type);
dto.Qjjj = await _qjjjRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type);
var bhMain = await _jsbhxdMainRepo.FirstOrDefaultAsync(x => !x.IsDeleted && x.YjssId == yjssId && x.YjssType == type);
if (bhMain != null)
dto.Jsbhxd = new YjssJsbhxdDto
{
Main = bhMain,
Items = (await _jsbhxdRepo.FindAsync(x => !x.IsDeleted && x.JsbhxdMainId == bhMain.Id)).OrderBy(x => x.HuiShu).ToList()
};
BuildParameters(dto);
return dto;
}
public async Task<Dictionary<string, object?>> GetJsdcSummaryAsync(int type, long yjssId)
{
var list = await GetJsdcAsync(type, yjssId);
var valid = list.Where(x => x.YiJianSiChang > 0).ToList();
decimal Avg(Func<YjssDcJsdc, decimal> f)
{
var v = list.Where(x => f(x) > 0).Select(f).ToList();
return v.Count == 0 ? 0 : Math.Round(v.Average(), 2, MidpointRounding.AwayFromZero);
}
return new Dictionary<string, object?>
{
["解舒样茧粒数"] = valid.Select(x => x.JieShuLiShu).DefaultIfEmpty(0).Max(),
["解舒样茧重量"] = valid.Select(x => x.JieShuZhongLiang).DefaultIfEmpty(0).Max(),
["一茧丝长"] = Avg(x => x.YiJianSiChang),
["解舒率"] = Avg(x => x.JieShuLv),
["解舒丝长"] = Avg(x => x.JieShuSiChang),
["茧丝纤度"] = Avg(x => x.JianSiXianDu),
["解舒光折"] = Avg(x => x.JieShuGuangZhe),
["万米吊糙"] = Avg(x => x.XwWanMiDiaoCao),
["落绪外"] = Math.Round(list.Where(x => x.LdfbWai > 0).Select(x => (decimal)x.LdfbWai).DefaultIfEmpty(0).Average(), 0, MidpointRounding.AwayFromZero),
["落绪中"] = Math.Round(list.Where(x => x.LdfbZhong > 0).Select(x => (decimal)x.LdfbZhong).DefaultIfEmpty(0).Average(), 0, MidpointRounding.AwayFromZero),
["落绪内"] = Math.Round(list.Where(x => x.LdfbNei > 0).Select(x => (decimal)x.LdfbNei).DefaultIfEmpty(0).Average(), 0, MidpointRounding.AwayFromZero),
["记录数"] = list.Count
};
}
/// <summary>按 F9 RepDt_Qc_Yjss_* 的中文键组装报表参数区</summary>
private static void BuildParameters(YjssFullDto dto)
{
var p = dto.Parameters;
p["查询时间"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
p["庄口名称"] = dto.ZhuangKouName;
p["日期"] = dto.Date.ToString("yyyy/M/d");
p["样茧重量"] = dto.ZhongLiang;
p["类型"] = dto.SampleTypeName;
p["回潮率"] = dto.HuiChaoLv;
p["备注"] = dto.Memo;
var x = dto.Xjdc;
if (x != null)
{
p["公斤茧粒"] = x.GongJinLiShu;
p["解舒样茧粒数"] = x.JieShuLiShu;
p["解舒样茧重量"] = x.JieShuZhongLiang;
p["茧重联统茧"] = x.ZlLtj; p["茧重好五级茧"] = x.ZlHwj; p["茧重五级茧"] = x.ZlWj;
p["茧重棉茧"] = x.ZlMianJian; p["茧重黄斑"] = x.ZlHuangBan; p["茧重蛆茧"] = x.ZlQuJian;
p["茧重双宫"] = x.ZlShuangGong; p["茧重血茧"] = x.ZlXueJian; p["茧重薄皮"] = x.ZlBaoPi;
p["茧重茧衣"] = x.ZlJianYi; p["茧重霉茧"] = x.ZlMeiJian;
p["比例联统茧"] = x.BlLtj; p["比例好五级茧"] = x.BlHwj; p["比例五级茧"] = x.BlWj;
p["比例棉茧"] = x.BlMianJian; p["比例黄斑"] = x.BlHuangBan; p["比例蛆茧"] = x.BlQuJian;
p["比例双宫"] = x.BlShuangGong; p["比例血茧"] = x.BlXueJian; p["比例薄皮"] = x.BlBaoPi;
p["比例茧衣"] = x.BlJianYi; p["比例霉茧"] = x.BlMeiJian;
}
var q = dto.Qpdc;
if (q != null)
{
p["茧层率"] = q.JianCengLv; p["公量茧层率"] = q.JianCengLvGongLiang; p["脱胶率"] = q.TuoJiaoLv;
p["原量全茧"] = q.YlQuanJian; p["原量茧层"] = q.YlJianCeng; p["原量蛹体"] = q.YlYongTi;
p["干量煮茧后"] = q.GlZhuJian; p["干量茧层"] = q.GlJianCeng; p["干量蛹体"] = q.GlYongTi;
p["粒数僵蚕"] = q.LsJiangCan; p["粒数内染"] = q.LsNeiRan; p["粒数霉蛹"] = q.LsMeiYong; p["粒数毛脚"] = q.LsMaoJiao;
p["比例僵蚕"] = q.BlJiangCan; p["比例内染"] = q.BlNeiRan; p["比例霉蛹"] = q.BlMeiYong; p["比例毛脚"] = q.BlMaoJiao;
}
var jj = dto.Qjjj;
if (jj != null)
{
p["清洁"] = jj.QingJie; p["洁净"] = jj.JingJie; p["片数"] = jj.PianShu;
p["一度"] = jj.YdYiDu; p["二度"] = jj.YdErDu; p["三度"] = jj.YdSanDu;
p["洁净100"] = jj.Jj100; p["洁净95"] = jj.Jj95; p["洁净90"] = jj.Jj90; p["洁净85"] = jj.Jj85;
p["洁净80"] = jj.Jj80; p["洁净75"] = jj.Jj75; p["洁净70"] = jj.Jj70; p["洁净65"] = jj.Jj65; p["洁净60"] = jj.Jj60;
p["特大糙"] = jj.QjTdc; p["废丝"] = jj.QjFs; p["大糙"] = jj.QjDc; p["黏附糙"] = jj.QjNfc;
p["大长结"] = jj.QjDcj; p["重螺旋"] = jj.QjZlx;
p["小糙"] = jj.QjXc; p["长结"] = jj.QjCj; p["螺旋"] = jj.QjLx; p["环结及裂丝"] = jj.QjHjls;
}
if (dto.Jfdc != null)
{
p["平均茧幅"] = dto.Jfdc.Main.PingJunJianFu;
p["最大茧幅"] = dto.Jfdc.Main.ZuiDaJianFu;
p["茧幅整齐率"] = dto.Jfdc.Main.ZhengQiLv;
p["茧幅均方差"] = dto.Jfdc.Main.JunFangCha;
}
if (dto.Jsbhxd != null)
{
p["茧丝纤度均方差"] = dto.Jsbhxd.Main.XianDuJunFangCha;
p["最大开差"] = dto.Jsbhxd.Main.ZuiDaKaiCha;
p["内外开差"] = dto.Jsbhxd.Main.NeiWaiKaiCha;
p["外三百回"] = dto.Jsbhxd.Main.WaiSanBaiHui;
p["中三百回"] = dto.Jsbhxd.Main.ZhongSanBaiHui;
p["内三百回"] = dto.Jsbhxd.Main.NeiSanBaiHui;
p["白色茧单纤"] = dto.Jsbhxd.Main.BaiSeJianDanXian;
p["灰色茧单纤"] = dto.Jsbhxd.Main.HuiSeJianDanXian;
p["红色茧单纤"] = dto.Jsbhxd.Main.HongSeJianDanXian;
}
}
// ==================== 内部工具 ====================
private async Task SetParentPointerAsync(int type, long yjssId,
long? xjdcId, long? qjjjId, long? jfdcId, long? qpdcId, int? jsdcCt, int? jsbhxdCt)
{
if (type == 1)
{
var m = await _ylxyRepo.GetByIdAsync(yjssId);
if (m == null) return;
if (xjdcId.HasValue) m.XjdcId = xjdcId.Value;
if (qjjjId.HasValue) m.QjjjId = qjjjId.Value;
if (jsdcCt.HasValue) m.JsdcCt = jsdcCt.Value;
await _ylxyRepo.UpdateAsync(m);
}
else
{
var m = await _zhyRepo.GetByIdAsync(yjssId);
if (m == null) return;
if (xjdcId.HasValue) m.XjdcId = xjdcId.Value;
if (qjjjId.HasValue) m.QjjjId = qjjjId.Value;
if (jfdcId.HasValue) m.JfdcId = jfdcId.Value;
if (qpdcId.HasValue) m.QpdcId = qpdcId.Value;
if (jsdcCt.HasValue) m.JsdcCt = jsdcCt.Value;
if (jsbhxdCt.HasValue) m.JsbhxdCt = jsbhxdCt.Value;
await _zhyRepo.UpdateAsync(m);
}
}
private static async Task<long> UpsertAsync<T>(IRepository<T> repo, T dto, T? exist) where T : class, IEntity
{
if (exist == null)
{
dto.Id = 0;
dto.IsDeleted = false;
await repo.AddAsync(dto);
return dto.Id;
}
dto.Id = exist.Id;
dto.CreatedAt = exist.CreatedAt;
dto.IsDeleted = false;
await repo.UpdateAsync(dto);
return dto.Id;
}
}
@@ -0,0 +1,41 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Domain.Entities.SYS.Reports;
namespace F9WebAiTest.Application.Services.Reports;
public interface IReportService
{
Task<PageResult<ReportListDto>> GetPagedAsync(string like, string category, bool? isActive, int pageIndex, int pageSize);
Task<ReportDetailDto> GetByIdAsync(long id);
Task<long> CreateAsync(ReportCreateDto dto);
Task UpdateAsync(long id, ReportUpdateDto dto);
Task DeleteAsync(long id);
/// <summary>上传 .grf 模板文件(f9designer:// 本机设计器协议回传用);常规保存走 Create/Update 的 templateJson</summary>
Task UploadTemplateAsync(long id, byte[] data, string fileName);
/// <summary>直接保存模板文本(前端在线编辑模板源码用,免下载 / 上传),返回保存后的字符数</summary>
Task<int> SaveTemplateContentAsync(long id, string content);
/// <summary>读取报表模板内容(ReportFile.DesignContent,.grf 的 JSON 文本);无模板返回 null</summary>
Task<string?> GetTemplateContentAsync(Report report);
/// <summary>用户可见报表(按权限码过滤,admin 返回全部启用报表)</summary>
Task<List<ReportViewDto>> GetVisibleReportsAsync(long userId, bool isAdmin, IReadOnlyCollection<string> permissions);
/// <summary>校验用户是否拥有某报表的查看权限(admin 视为通过)</summary>
Task EnsureReportAccessAsync(long userId, bool isAdmin, IReadOnlyCollection<string> permissions, long reportId);
/// <summary>执行报表数据查询 SQL,返回 Grid++Report 约定的 JSON 形态</summary>
Task<Dictionary<string, object?>> ExecuteDataAsync(Report report, IReadOnlyDictionary<string, object?> parameters);
/// <summary>列出所有启用的报表(角色管理-报表权限分配时使用)</summary>
Task<List<ReportListDto>> GetAllActiveAsync();
/// <summary>数据源清单(IReportDataProvider 注册表 + 各自声明的查询参数),新增报表自动配置用</summary>
Task<List<ReportDataSourceInfoDto>> GetDataSourcesAsync();
/// <summary>探测数据源结构:按 DsName / DataSql 执行一次查询,识别明细列与参数</summary>
Task<ReportProbeResultDto> ProbeAsync(ReportProbeRequestDto request);
}
@@ -0,0 +1,47 @@
using F9WebAiTest.Application.CQRS.SYS.Reports.Queries;
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Application.Interfaces.Reports;
using MediatR;
namespace F9WebAiTest.Application.Services.Reports.Providers;
/// <summary>
/// 落丝记录报表数据提供者(生产-前缫)。
/// DsName = "RepDt_Prod_Lsjl"
/// 参数:q_date_start / q_date_end(日期范围)、q_dep(车间,含子车间)、q_jizu(机组)、
/// q_zk(庄口 Id,选择页取值)、q_like(关键字:庄口 / 规格 / 颜色)。
/// 参数与列完全由查询结果/报表配置决定,Provider 只负责取数。
/// </summary>
public class RepDt_Prod_Lsjl : IReportDataProvider
{
public string ReportName => "RepDt_Prod_Lsjl";
public async Task<ReportDto_DataSource> GetData(Dictionary<string, object> pairs, IMediator mediator)
{
DateTime? dateStart = null;
if (pairs.TryGetValue("q_date_start", out var ds) && ds != null && ds.ToString() is { Length: > 0 } s1
&& DateTime.TryParse(s1, out var d1))
dateStart = d1;
DateTime? dateEnd = null;
if (pairs.TryGetValue("q_date_end", out var de) && de != null && de.ToString() is { Length: > 0 } s2
&& DateTime.TryParse(s2, out var d2))
dateEnd = d2;
long depId = 0;
if (pairs.TryGetValue("q_dep", out var dep) && dep != null && long.TryParse(dep.ToString(), out var d))
depId = d;
int jizu = 0;
if (pairs.TryGetValue("q_jizu", out var jz) && jz != null && int.TryParse(jz.ToString(), out var j))
jizu = j;
long gyzkId = 0;
if (pairs.TryGetValue("q_zk", out var zk) && zk != null && long.TryParse(zk.ToString(), out var z))
gyzkId = z;
var like = pairs.TryGetValue("q_like", out var lk) && lk != null ? lk.ToString() ?? "" : "";
return await mediator.Send(new GetLsjlRepQuery(dateStart, dateEnd, depId, jizu, gyzkId, like));
}
}
@@ -0,0 +1,31 @@
using F9WebAiTest.Application.CQRS.SYS.Reports.Queries;
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Application.Interfaces.Reports;
using MediatR;
namespace F9WebAiTest.Application.Services.Reports.Providers;
/// <summary>
/// 职工花名册数据提供者(对应 F9 的 IReportService 实现):
/// 解析查询参数 → 通过 MediatR 发送 CQRS 查询 → 返回含记录集与报表参数的数据源。
/// DsName = "RepDt_Sys_Users"
/// </summary>
public class RepDt_Sys_Users : IReportDataProvider
{
public string ReportName => "RepDt_Sys_Users";
public async Task<ReportDto_DataSource> GetData(Dictionary<string, object> pairs, IMediator mediator)
{
// 1. 解析输入参数(部门 q_dep、关键字 q_like)
long depId = 0;
if (pairs.TryGetValue("q_dep", out var depObj) && depObj != null && long.TryParse(depObj.ToString(), out var d))
depId = d;
string like = "";
if (pairs.TryGetValue("q_like", out var likeObj) && likeObj != null)
like = likeObj.ToString() ?? "";
// 2. 发送 CQRS 查询获取报表数据
return await mediator.Send(new GetEmployeeDirectoryRepQuery(depId, like));
}
}
@@ -0,0 +1,652 @@
using System.Collections.Concurrent;
using System.Data;
using System.Data.Common;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using FreeSql;
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Application.Interfaces.Reports;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Entities.SYS.Reports;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using MediatR;
using System.Data.SQLite;
namespace F9WebAiTest.Application.Services.Reports;
public class ReportService : IReportService
{
private readonly IRepository<Report> _reportRepo;
private readonly IRepository<ReportFile> _reportFileRepo;
private readonly IRepository<Permission> _permissionRepo;
private readonly IRepository<RolePermission> _rolePermissionRepo;
private readonly IFreeSql _fsql;
private readonly ISnowflakeIdGenerator _snowflake;
private readonly IMediator _mediator;
private readonly Dictionary<string, IReportDataProvider> _dataProviders;
public ReportService(
IRepository<Report> reportRepo,
IRepository<ReportFile> reportFileRepo,
IRepository<Permission> permissionRepo,
IRepository<RolePermission> rolePermissionRepo,
IFreeSql fsql,
ISnowflakeIdGenerator snowflake,
IMediator mediator,
IEnumerable<IReportDataProvider> dataProviders)
{
_reportRepo = reportRepo;
_reportFileRepo = reportFileRepo;
_permissionRepo = permissionRepo;
_rolePermissionRepo = rolePermissionRepo;
_fsql = fsql;
_snowflake = snowflake;
_mediator = mediator;
_dataProviders = dataProviders.ToDictionary(p => p.ReportName, StringComparer.OrdinalIgnoreCase);
}
// ===== 报表管理 =====
public async Task<PageResult<ReportListDto>> GetPagedAsync(string like, string category, bool? isActive, int pageIndex, int pageSize)
{
var query = _fsql.Select<Report>().Where(r => !r.IsDeleted);
if (!string.IsNullOrWhiteSpace(like))
query = query.Where(r => r.Name.Contains(like) || r.Code.Contains(like));
if (!string.IsNullOrWhiteSpace(category))
query = query.Where(r => r.Category == category);
if (isActive.HasValue)
query = query.Where(r => r.IsActive == isActive.Value);
var total = await query.CountAsync();
var rows = await query
.OrderBy(r => r.SortOrder)
.OrderByDescending(r => r.CreatedAt)
.Page(pageIndex, pageSize)
.ToListAsync();
var items = rows.Select(r => new ReportListDto(
r.Id, r.Code, r.Name, r.Category, r.Description, r.SortOrder, r.IsActive,
r.ReportFileId, HasTemplate(r), r.CreatedAt, r.UpdatedAt)).ToList();
return new PageResult<ReportListDto> { Items = items, Total = total, PageIndex = pageIndex, PageSize = pageSize };
}
public async Task<ReportDetailDto> GetByIdAsync(long id)
{
var r = await _reportRepo.FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted)
?? throw new F9NotFoundException("报表不存在");
return ToDetail(r);
}
public async Task<long> CreateAsync(ReportCreateDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Code)) throw new F9OperationException("报表编码不能为空");
if (string.IsNullOrWhiteSpace(dto.Name)) throw new F9OperationException("报表名称不能为空");
ValidateCode(dto.Code);
ValidateConfig(dto.Config);
// 有 DsName 时允许不填 SQL;否则必须为合法 SELECT
if (GetConfigDsName(dto.Config) == null) ValidateSql(dto.DataSql);
if (await _reportRepo.AnyAsync(r => r.Code == dto.Code && !r.IsDeleted))
throw new F9OperationException($"报表编码已存在:{dto.Code}");
var entity = new Report(_snowflake.CreateId(), dto.Code.Trim(), dto.Name.Trim(),
dto.Category ?? "通用", dto.Description ?? "", dto.DataSql ?? "",
dto.DataParams ?? "[]", dto.SortOrder, dto.Config ?? "");
// 模板与配置一起保存(模板实例写入 ReportFile,Report 持有 ReportFileId)
if (!string.IsNullOrWhiteSpace(dto.TemplateJson))
await SaveTemplateAsync(entity, dto.TemplateJson);
await _reportRepo.AddAsync(entity);
await _reportRepo.SaveChangesAsync();
// 同步创建权限单元:report:{code}:visble
var perm = new Permission(_snowflake.CreateId(), Permission_Type_Enum.Visble,
Resource_Type_Enum.报表, entity.Code, entity.Id);
perm.SetCode($"report:{entity.Code}:visble");
await _permissionRepo.AddAsync(perm);
await _permissionRepo.SaveChangesAsync();
return entity.Id;
}
public async Task UpdateAsync(long id, ReportUpdateDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Code)) throw new F9OperationException("报表编码不能为空");
if (string.IsNullOrWhiteSpace(dto.Name)) throw new F9OperationException("报表名称不能为空");
ValidateCode(dto.Code);
ValidateConfig(dto.Config);
if (GetConfigDsName(dto.Config) == null) ValidateSql(dto.DataSql);
var entity = await _reportRepo.FirstOrDefaultAsync(r => r.Id == id && !r.IsDeleted)
?? throw new F9NotFoundException("报表不存在");
if (await _reportRepo.AnyAsync(r => r.Code == dto.Code && r.Id != id && !r.IsDeleted))
throw new F9OperationException($"报表编码已存在:{dto.Code}");
var oldCode = entity.Code;
entity.UpdateBasic(dto.Code.Trim(), dto.Name.Trim(), dto.Category ?? "通用",
dto.Description ?? "", dto.DataSql ?? "", dto.DataParams ?? "[]", dto.SortOrder, dto.Config ?? "");
entity.Activate();
// 模板与配置一起保存;不传模板时保留原模板
if (!string.IsNullOrWhiteSpace(dto.TemplateJson))
await SaveTemplateAsync(entity, dto.TemplateJson);
await _reportRepo.UpdateAsync(entity);
await _reportRepo.SaveChangesAsync();
// 同步更新权限 Code(编码变更时)
if (!string.Equals(oldCode, entity.Code, StringComparison.Ordinal))
{
var perm = await _permissionRepo.FirstOrDefaultAsync(p =>
p.ResourceType == Resource_Type_Enum.报表 && p.ResourceId == entity.Id && !p.IsDeleted);
if (perm != null)
{
perm.SetCode($"report:{entity.Code}:visble");
await _permissionRepo.UpdateAsync(perm);
await _permissionRepo.SaveChangesAsync();
}
}
}
public async Task DeleteAsync(long id)
{
var entity = await _reportRepo.FirstOrDefaultAsync(r => r.Id == id && !r.IsDeleted)
?? throw new F9NotFoundException("报表不存在");
await _reportRepo.DeleteAsync(entity);
await _reportRepo.SaveChangesAsync();
// 级联删除模板文件实例
var files = await _reportFileRepo.FindAsync(f => f.ReportId == id && !f.IsDeleted);
if (files.Count > 0)
{
await _reportFileRepo.DeleteRangeAsync(files);
await _reportFileRepo.SaveChangesAsync();
}
// 同步删除报表关联的权限单元与角色授权
var perms = await _permissionRepo.FindAsync(p => p.ResourceType == Resource_Type_Enum.报表 && p.ResourceId == id && !p.IsDeleted);
if (perms.Count > 0)
{
var permIds = perms.Select(p => p.Id).ToList();
await _rolePermissionRepo.HardDeleteRangeAsync(
(await _rolePermissionRepo.FindAsync(rp => permIds.Contains(rp.PermissionId) && !rp.IsDeleted)));
await _permissionRepo.HardDeleteRangeAsync(perms);
await _permissionRepo.SaveChangesAsync();
}
}
public async Task UploadTemplateAsync(long id, byte[] data, string fileName)
{
if (data == null || data.Length == 0) throw new F9OperationException("模板内容为空");
var entity = await _reportRepo.FirstOrDefaultAsync(r => r.Id == id && !r.IsDeleted)
?? throw new F9NotFoundException("报表不存在");
// .grf 可能是 GR6 时代的 UTF-8 JSON,也可能是 GR7 的 "_WR" 压缩文本,按 BOM 自动识别编码
var text = DecodeTemplateBytes(data);
await SaveTemplateAsync(entity, text);
await _reportRepo.UpdateAsync(entity);
await _reportRepo.SaveChangesAsync();
}
/// <summary>
/// 直接保存模板文本(前端「模板源码」在线编辑后保存)。
/// JSON 格式模板做一次语法校验,避免保存后在设计器 / 渲染端打不开;
/// "_WR" 压缩模板无法在服务端校验,原样保存。
/// </summary>
public async Task<int> SaveTemplateContentAsync(long id, string content)
{
var entity = await _reportRepo.FirstOrDefaultAsync(r => r.Id == id && !r.IsDeleted)
?? throw new F9NotFoundException("报表不存在");
var text = NormalizeTemplateContent(content);
var head = text.TrimStart();
if (head.StartsWith('{') || head.StartsWith('['))
{
try { using var _ = JsonDocument.Parse(text); }
catch (JsonException ex) { throw new F9OperationException($"模板 JSON 不合法:{ex.Message}"); }
}
await SaveTemplateAsync(entity, text);
await _reportRepo.UpdateAsync(entity);
await _reportRepo.SaveChangesAsync();
return text.Length;
}
public async Task<string?> GetTemplateContentAsync(Report report)
{
var file = await FindTemplateFileAsync(report);
return file?.DesignContent;
}
/// <summary>查找报表的模板文件实例(优先 ReportFileId,兼容按 ReportId 查找)</summary>
private async Task<ReportFile?> FindTemplateFileAsync(Report report)
{
if (report.ReportFileId != 0)
{
var byId = await _reportFileRepo.FirstOrDefaultAsync(f => f.Id == report.ReportFileId && !f.IsDeleted);
if (byId != null) return byId;
}
return await _reportFileRepo.FirstOrDefaultAsync(f => f.ReportId == report.Id && !f.IsDeleted);
}
/// <summary>
/// 保存模板:新建或更新 ReportFile.DesignContent,并把 Report.ReportFileId 绑定到该实例。
/// 模板与报表配置一起保存,前端无需再单独上传 .grf 文件。
/// 注意:只修改内存中的 report,不落库(由调用方统一保存)。
/// </summary>
private async Task SaveTemplateAsync(Report report, string templateContent)
{
var content = NormalizeTemplateContent(templateContent);
var file = await FindTemplateFileAsync(report);
if (file != null)
{
file.UpdateContent(content);
await _reportFileRepo.UpdateAsync(file);
await _reportFileRepo.SaveChangesAsync();
}
else
{
file = new ReportFile(_snowflake.CreateId(), report.Id, content);
await _reportFileRepo.AddAsync(file);
await _reportFileRepo.SaveChangesAsync();
}
if (report.ReportFileId != file.Id) report.BindReportFile(file.Id);
}
/// <summary>
/// 规范化模板内容:仅去 BOM / 去掉首尾空白,不做格式校验。
/// .grf 有两种格式:GR6 的 UTF-8 JSON 文本,以及 GR7 的 "_WR" 前缀压缩文本;
/// 两者都由 Grid++Report 的 HTML5 渲染库自行识别(内部 decodeWR),服务端原样保存即可。
/// </summary>
private static string NormalizeTemplateContent(string? content)
{
if (string.IsNullOrWhiteSpace(content)) throw new F9OperationException("报表模板内容为空");
var text = content.Trim();
if (text.Length > 0 && text[0] == '\uFEFF') text = text.Substring(1);
if (string.IsNullOrWhiteSpace(text)) throw new F9OperationException("报表模板内容为空");
return text;
}
/// <summary>按 BOM / 内容特征解码 .grf 字节:UTF-8(BOM) / UTF-16LE(BOM) / UTF-16BE(BOM) / 无 BOM 按 UTF-8</summary>
private static string DecodeTemplateBytes(byte[] data)
{
if (data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
return Encoding.UTF8.GetString(data, 3, data.Length - 3);
if (data.Length >= 2 && data[0] == 0xFF && data[1] == 0xFE)
return Encoding.Unicode.GetString(data, 2, data.Length - 2);
if (data.Length >= 2 && data[0] == 0xFE && data[1] == 0xFF)
return Encoding.BigEndianUnicode.GetString(data, 2, data.Length - 2);
// 无 BOM:先按 UTF-8 严格解码,失败则按 UTF-16LE 尝试
try
{
var strict = new UTF8Encoding(false, true);
return strict.GetString(data);
}
catch (DecoderFallbackException)
{
try { return Encoding.Unicode.GetString(data); }
catch { return Encoding.UTF8.GetString(data); }
}
}
// ===== 用户可见报表 =====
public async Task<List<ReportViewDto>> GetVisibleReportsAsync(long userId, bool isAdmin, IReadOnlyCollection<string> permissions)
{
var all = await _reportRepo.FindAsync(r => r.IsActive && !r.IsDeleted);
IEnumerable<Report> visible;
if (isAdmin)
{
visible = all;
}
else
{
visible = all.Where(r => permissions.Contains($"report:{r.Code}:visble"));
}
return visible
.OrderBy(r => r.SortOrder).ThenBy(r => r.Name)
.Select(r => new ReportViewDto(r.Id, r.Code, r.Name, r.Category, r.Description, r.SortOrder, HasTemplate(r)))
.ToList();
}
public async Task EnsureReportAccessAsync(long userId, bool isAdmin, IReadOnlyCollection<string> permissions, long reportId)
{
if (isAdmin) return;
var report = await _reportRepo.FirstOrDefaultAsync(r => r.Id == reportId && !r.IsDeleted)
?? throw new F9NotFoundException("报表不存在", 404);
if (!report.IsActive) throw new F9NotFoundException("报表已停用", 404);
if (!permissions.Contains($"report:{report.Code}:visble"))
throw new F9OperationException("无权访问该报表");
}
// ===== 数据查询 =====
public async Task<Dictionary<string, object?>> ExecuteDataAsync(Report report, IReadOnlyDictionary<string, object?> parameters)
{
// 1. 优先按 Config.DsName 解析到数据提供者(F9 IReportService 模式)
var dsName = GetConfigDsName(report.Config);
if (!string.IsNullOrWhiteSpace(dsName))
{
if (!_dataProviders.TryGetValue(dsName, out var provider))
throw new F9OperationException($"未找到数据源查询实现:{dsName}");
var pairs = new Dictionary<string, object>(StringComparer.Ordinal);
foreach (var kv in parameters) pairs[kv.Key] = kv.Value!;
var dataSource = await provider.GetData(pairs, _mediator);
return ToGridReportJson(dataSource);
}
// 2. 回退:DataSql + @参数名(仅绑定 SQL 中实际出现的参数,防注入)
if (string.IsNullOrWhiteSpace(report.DataSql))
throw new F9OperationException("报表未配置数据源(Config.DsName 或 DataSql)");
try
{
var sqlTokens = Regex.Matches(report.DataSql, @"@([A-Za-z_][A-Za-z0-9_]*)")
.Select(m => m.Groups[1].Value).Distinct().ToHashSet(StringComparer.Ordinal);
var dbParams = new List<DbParameter>();
foreach (var kv in parameters)
{
if (kv.Key.Length > 0 && sqlTokens.Contains(kv.Key) && kv.Value != null)
dbParams.Add(new SQLiteParameter("@" + kv.Key, kv.Value));
}
var dt = dbParams.Count > 0
? _fsql.Ado.ExecuteDataTable(report.DataSql, dbParams.ToArray())
: _fsql.Ado.ExecuteDataTable(report.DataSql);
var recordset = new List<Dictionary<string, object?>>();
foreach (DataRow row in dt.Rows)
{
var d = new Dictionary<string, object?>(dt.Columns.Count, StringComparer.Ordinal);
foreach (DataColumn col in dt.Columns)
{
var v = row[col];
d[col.ColumnName] = v == DBNull.Value ? null : v;
}
recordset.Add(d);
}
return new Dictionary<string, object?> { ["recordset"] = recordset };
}
catch (F9OperationException) { throw; }
catch (Exception ex)
{
throw new F9OperationException("报表数据查询失败:" + ex.Message);
}
}
/// <summary>
/// 将 F9 的 ReportDto_DataSource 转换为 Grid++Report 约定的 JSON 数据:
/// 单数据集 → { "recordset": [ {...} ] }(兼容无名记录集的模板)
/// 多数据集 → { "main": [ {...} ], "detail": [ {...} ] }(顶层键为数据集名)
/// 报表参数 → 附加 { "parameter": [ { 账期: "...", 车间: "..." } ] }(模板以 Parameter 方式引用)
/// </summary>
private static Dictionary<string, object?> ToGridReportJson(ReportDto_DataSource dataSource)
{
var result = new Dictionary<string, object?>(StringComparer.Ordinal);
var datasets = dataSource.Report_Data
.Where(kv => (kv.Value.Field_Data?.Count ?? 0) > 0)
.ToList();
if (datasets.Count <= 1)
{
// 0 行时也必须输出空 recordset:否则渲染库找不到主记录集,
// 报表参数(Parameter)会被错位渲染到明细区(实测 0 行报表参数值错乱)
result["recordset"] = datasets.Count == 0
? new List<object>()
: datasets[0].Value.Field_Data.Select(ToNameValueDict).Cast<object>().ToList();
}
else
{
foreach (var kv in datasets)
result[kv.Key] = kv.Value.Field_Data.Select(ToNameValueDict).Cast<object>().ToList();
}
// 报表参数:以 Parameter 数据集输出(键名须与 .grf 模板的 Parameter 节点一致,HTML5 渲染器取数组第一项)
var paramData = dataSource.Report_Data.Values
.FirstOrDefault(d => d.Parameter_Data != null)?.Parameter_Data;
if (paramData != null)
result["Parameter"] = new List<object> { ToNameValueDict(paramData) };
return result;
}
/// <summary>
/// 对象 → 字段名/值的字典。
/// 用字典承载是为了保证属性名原样输出(不受 ASP.NET Core 默认 camelCase 序列化策略影响),
/// 从而与 .grf 模板 DetailGrid.Recordset.Field 定义的字段名严格一致。
/// </summary>
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> _rowPropsCache = new();
private static Dictionary<string, object?> ToNameValueDict(object row)
{
if (row is IDictionary<string, object> exists) return exists.ToDictionary(k => k.Key, v => (object?)v.Value);
var props = _rowPropsCache.GetOrAdd(row.GetType(), t => t.GetProperties());
var dict = new Dictionary<string, object?>(props.Length, StringComparer.Ordinal);
foreach (var p in props)
{
if (!p.CanRead) continue;
dict[p.Name] = p.GetValue(row);
}
return dict!;
}
private static string? GetConfigDsName(string configJson)
{
if (string.IsNullOrWhiteSpace(configJson)) return null;
try
{
using var doc = JsonDocument.Parse(configJson);
if (doc.RootElement.TryGetProperty("DsName", out var n) && n.ValueKind == JsonValueKind.String)
return n.GetString();
}
catch { /* 配置 JSON 非法时忽略 */ }
return null;
}
public async Task<List<ReportListDto>> GetAllActiveAsync()
{
var list = await _reportRepo.FindAsync(r => r.IsActive && !r.IsDeleted);
return list
.OrderBy(r => r.SortOrder).ThenBy(r => r.Name)
.Select(r => new ReportListDto(
r.Id, r.Code, r.Name, r.Category, r.Description, r.SortOrder, r.IsActive,
r.ReportFileId, HasTemplate(r), r.CreatedAt, r.UpdatedAt))
.ToList();
}
// ===== 数据源清单 / 结构探测(新增报表时按查询结果自动识别明细列) =====
public Task<List<ReportDataSourceInfoDto>> GetDataSourcesAsync()
{
var list = _dataProviders.Values
.OrderBy(p => p.ReportName, StringComparer.OrdinalIgnoreCase)
.Select(p => new ReportDataSourceInfoDto { DsName = p.ReportName })
.ToList();
return Task.FromResult(list);
}
public async Task<ReportProbeResultDto> ProbeAsync(ReportProbeRequestDto request)
{
var dsName = (request.DsName ?? "").Trim();
var result = new ReportProbeResultDto { DsName = dsName };
_dataProviders.TryGetValue(dsName, out var provider);
// 探测参数只用前端带入的取值(参数本身由用户在报表配置里维护,不在这里生成控件)
var pairs = new Dictionary<string, object>(StringComparer.Ordinal);
foreach (var kv in request.Params)
if (kv.Value != null) pairs[kv.Key] = kv.Value;
try
{
if (provider != null)
await FillFromProviderAsync(result, provider, pairs);
else if (!string.IsNullOrWhiteSpace(request.DataSql))
FillFromSql(result, request.DataSql!, pairs);
else if (!string.IsNullOrWhiteSpace(dsName))
result.Error = $"未找到数据源实现:{dsName}";
else
result.Error = "请先选择数据源(DsName)或填写数据 SQL";
}
catch (F9OperationException ex) { result.Error = ex.Message; }
catch (Exception ex) { result.Error = "数据源探测失败:" + ex.Message; }
return result;
}
/// <summary>通过 IReportDataProvider 执行一次查询,取返回记录集的字段名作为明细列</summary>
private async Task FillFromProviderAsync(ReportProbeResultDto result, IReportDataProvider provider, Dictionary<string, object> pairs)
{
var dataSource = await provider.GetData(pairs, _mediator);
// 报表参数:来自 Provider 返回的 Parameter_Data(不是查询条件,也不是硬编码声明)
var paramDatum = dataSource.Report_Data.FirstOrDefault(kv => kv.Value?.Parameter_Data != null);
if (paramDatum.Value?.Parameter_Data != null)
{
result.Parameters = ToNameValueDict(paramDatum.Value.Parameter_Data)
.Select(kv => new ReportParamValueDto
{
Name = kv.Key,
Value = kv.Value?.ToString() ?? "",
})
.ToList();
}
var datums = dataSource.Report_Data
.Where(kv => kv.Value?.Field_Data is { Count: > 0 })
.ToList();
result.Total = datums.Sum(kv => kv.Value.Field_Data.Count);
if (datums.Count == 0)
{
result.Error = "查询未返回数据,无法识别明细列(可手工添加列)";
return;
}
var rows = datums[0].Value.Field_Data.Take(3).Select(ToNameValueDict).ToList();
result.Rows = rows;
result.Fields = rows[0].Select(kv => new ReportFieldDefDto
{
Name = kv.Key,
Label = kv.Key,
DataType = InferDataType(kv.Value),
}).ToList();
}
/// <summary>直接用 DataSql 执行一次查询(带 @参数),取 DataTable 列作为明细列</summary>
private void FillFromSql(ReportProbeResultDto result, string sql, Dictionary<string, object> pairs)
{
ValidateSql(sql);
var sqlTokens = Regex.Matches(sql, @"@([A-Za-z_][A-Za-z0-9_]*)")
.Select(m => m.Groups[1].Value).Distinct().ToHashSet(StringComparer.Ordinal);
var dbParams = new List<DbParameter>();
foreach (var kv in pairs)
if (sqlTokens.Contains(kv.Key) && kv.Value != null)
dbParams.Add(new SQLiteParameter("@" + kv.Key, kv.Value));
var dt = dbParams.Count > 0
? _fsql.Ado.ExecuteDataTable(sql, dbParams.ToArray())
: _fsql.Ado.ExecuteDataTable(sql);
result.Fields = dt.Columns.Cast<DataColumn>().Select(c => new ReportFieldDefDto
{
Name = c.ColumnName,
Label = c.ColumnName,
DataType = c.DataType.Name,
}).ToList();
result.Total = dt.Rows.Count;
result.Rows = dt.Rows.Cast<DataRow>().Take(3).Select(row =>
{
var d = new Dictionary<string, object?>(dt.Columns.Count, StringComparer.Ordinal);
foreach (DataColumn col in dt.Columns)
{
var v = row[col];
d[col.ColumnName] = v == DBNull.Value ? null : v;
}
return d;
}).ToList();
}
private static string InferDataType(object? value)
{
if (value == null) return "String";
if (value is JsonElement je)
{
return je.ValueKind switch
{
JsonValueKind.Number => "Number",
JsonValueKind.True or JsonValueKind.False => "Boolean",
_ => "String",
};
}
return value switch
{
int or long or short or byte or uint or ulong or ushort or sbyte => "Integer",
decimal or double or float => "Number",
DateTime or DateTimeOffset => "DateTime",
bool => "Boolean",
_ => "String",
};
}
// ===== 工具 =====
private static bool HasTemplate(Report r) => r.ReportFileId != 0;
private static ReportDetailDto ToDetail(Report r) => new(
r.Id, r.Code, r.Name, r.Category, r.Description, r.DataSql, r.DataParams, r.Config,
r.SortOrder, r.IsActive, r.ReportFileId, HasTemplate(r),
r.CreatedAt, r.UpdatedAt);
private static void ValidateCode(string code)
{
if (!Regex.IsMatch(code ?? "", "^[a-zA-Z0-9_]{2,40}$"))
throw new F9OperationException("报表编码仅允许字母/数字/下划线,长度 2-40");
}
private static void ValidateSql(string sql)
{
if (string.IsNullOrWhiteSpace(sql)) throw new F9OperationException("数据集 SQL 不能为空");
var trimmed = sql.Trim();
if (!trimmed.StartsWith("select", StringComparison.OrdinalIgnoreCase))
throw new F9OperationException("数据集 SQL 必须以 SELECT 开头(仅允许查询语句)");
// 禁止分号后跟其他语句
if (trimmed.IndexOf(';') >= 0 && trimmed.IndexOf(';') < trimmed.Length - 1)
throw new F9OperationException("数据集 SQL 不允许包含多条语句");
}
private static void ValidateConfig(string? configJson)
{
if (string.IsNullOrWhiteSpace(configJson)) return;
try
{
using var doc = JsonDocument.Parse(configJson);
if (doc.RootElement.ValueKind != JsonValueKind.Object)
throw new F9OperationException("查询配置(Config)必须是 JSON 对象");
}
catch (JsonException)
{
throw new F9OperationException("查询配置(Config)不是合法 JSON");
}
catch (F9OperationException) { throw; }
}
}