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

质量模块:
- 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
@@ -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; }
}
}