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

质量模块:
- 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,134 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.QC;
using F9WebAiTest.Application.Services.QC;
using F9WebAiTest.Domain.Entities.QC.Gyzxs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.QC;
/// <summary>
/// 质量模块 —— 工艺执行书(对应 F9 生产\质量\工艺执行书)。
/// 主列表按「庄口 + 年度」展示,每张子单据一个入口;保存子单据会联动主控表。
/// </summary>
[ApiController]
[Route("api/qc/gyzxs")]
[Authorize]
public class GyzxsController : ControllerBase
{
private readonly IGyzxsService _svc;
public GyzxsController(IGyzxsService svc) => _svc = svc;
// ---------- 主列表 ----------
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20,
[FromQuery] int year = 0, [FromQuery] string keyword = "")
=> Ok(ApiResult<PageResult<GyzxsDocVersionDto>>.Ok(await _svc.GetDocVersionPagedAsync(pageIndex, pageSize, year, keyword)));
[HttpPost("version")]
public async Task<IActionResult> CreateVersion([FromQuery] long gyzkId)
{
try { return Ok(ApiResult<GyzxsDocVersionDto>.Ok(await _svc.CreateDocVersionAsync(gyzkId), "创建成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("version/{id}")]
public async Task<IActionResult> DeleteVersion(long id)
{
try { await _svc.DeleteDocVersionAsync(id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 封面 ----------
[HttpGet("fengmian")]
public async Task<IActionResult> GetFengMian([FromQuery] long gyzkId)
=> Ok(ApiResult<GyzxsFengMian>.Ok(await _svc.GetFengMianAsync(gyzkId)));
[HttpPost("fengmian")]
public async Task<IActionResult> SaveFengMian([FromBody] GyzxsFengMian dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveFengMianAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 试产(试缫工艺单) ----------
[HttpGet("shichan")]
public async Task<IActionResult> GetShichan([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsShichan>.Ok(await _svc.GetShichanAsync(gyzkId, guige)));
[HttpPost("shichan")]
public async Task<IActionResult> SaveShichan([FromBody] GyzxsShichan dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveShichanAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 混剥选茧 ----------
[HttpGet("xbj")]
public async Task<IActionResult> GetXbj([FromQuery] long gyzkId)
=> Ok(ApiResult<GyzxsXbjDto>.Ok(await _svc.GetXbjAsync(gyzkId)));
[HttpPost("xbj")]
public async Task<IActionResult> SaveXbj([FromBody] GyzxsXbjDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveXbjAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 试缫与大样 ----------
[HttpGet("ssdy")]
public async Task<IActionResult> GetXbjSsDy([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsXbjSsDy>.Ok(await _svc.GetXbjSsDyAsync(gyzkId, guige)));
[HttpPost("ssdy")]
public async Task<IActionResult> SaveXbjSsDy([FromBody] GyzxsXbjSsDy dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveXbjSsDyAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 煮茧 ----------
[HttpGet("zhujian")]
public async Task<IActionResult> GetZhuJian([FromQuery] long gyzkId)
=> Ok(ApiResult<GyzxsZhuJianDto>.Ok(await _svc.GetZhuJianAsync(gyzkId)));
[HttpPost("zhujian")]
public async Task<IActionResult> SaveZhuJian([FromBody] GyzxsZhuJianDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveZhuJianAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 自动缫 ----------
[HttpGet("zds")]
public async Task<IActionResult> GetZds([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsZdsDto>.Ok(await _svc.GetZdsAsync(gyzkId, guige)));
[HttpPost("zds")]
public async Task<IActionResult> SaveZds([FromBody] GyzxsZdsDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveZdsAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 后缫(工艺执行书) ----------
[HttpGet("hs")]
public async Task<IActionResult> GetHs([FromQuery] long gyzkId, [FromQuery] string guige = "")
=> Ok(ApiResult<GyzxsHs>.Ok(await _svc.GetHsAsync(gyzkId, guige)));
[HttpPost("hs")]
public async Task<IActionResult> SaveHs([FromBody] GyzxsHs dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveHsAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 报表数据 ----------
/// <summary>报表数据(type: fengmian / gyzxs / xbj / shichan / ssdy / zhujian / zds / zdsdy)</summary>
[HttpGet("print/{type}")]
public async Task<IActionResult> GetPrintData(string type, [FromQuery] long gyzkId, [FromQuery] string guige = "")
{
try { return Ok(ApiResult<GyzxsPrintDto>.Ok(await _svc.GetPrintDataAsync(type, gyzkId, guige))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
}
@@ -0,0 +1,139 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.QC;
using F9WebAiTest.Application.Services.QC;
using F9WebAiTest.Domain.Entities.QC.Yjss;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.QC;
/// <summary>
/// 质量模块 —— 样茧试缫(对应 F9 生产\质量\样茧试缫)。
/// type: 1=原料小样 2=综合样。
/// </summary>
[ApiController]
[Route("api/qc/yjss")]
[Authorize]
public class YjssController : ControllerBase
{
private readonly IYjssService _svc;
public YjssController(IYjssService svc) => _svc = svc;
// ---------- 主列表 / 主表 ----------
[HttpGet]
public async Task<IActionResult> GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20,
[FromQuery] int type = 1, [FromQuery] int year = 0, [FromQuery] string keyword = "")
=> Ok(ApiResult<PageResult<YjssListItemDto>>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, type, year, keyword)));
[HttpPost("ylxy")]
public async Task<IActionResult> SaveYlxy([FromBody] YjssMainYlxy dto)
{
try { return Ok(ApiResult<YjssListItemDto>.Ok(await _svc.SaveYlxyAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPost("zhy")]
public async Task<IActionResult> SaveZhy([FromBody] YjssMainZhy dto)
{
try { return Ok(ApiResult<YjssListItemDto>.Ok(await _svc.SaveZhyAsync(dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("{type}/{id}")]
public async Task<IActionResult> Delete(int type, long id)
{
try { await _svc.DeleteAsync(type, id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 选茧调查 ----------
[HttpGet("xjdc/{yjssId}")]
public async Task<IActionResult> GetXjdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssDcXjdc>.Ok(await _svc.GetXjdcAsync(type, yjssId)));
[HttpPost("xjdc")]
public async Task<IActionResult> SaveXjdc([FromQuery] int type, [FromBody] YjssDcXjdc dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveXjdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 解舒调查 ----------
[HttpGet("jsdc/{yjssId}")]
public async Task<IActionResult> GetJsdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<List<YjssDcJsdc>>.Ok(await _svc.GetJsdcAsync(type, yjssId)));
[HttpPost("jsdc")]
public async Task<IActionResult> SaveJsdc([FromQuery] int type, [FromBody] YjssDcJsdc dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveJsdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("jsdc/{type}/{id}")]
public async Task<IActionResult> DeleteJsdc(int type, long id)
{
try { await _svc.DeleteJsdcAsync(type, id); return Ok(ApiResult<object>.Ok(null, "删除成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 茧幅调查 ----------
[HttpGet("jfdc/{yjssId}")]
public async Task<IActionResult> GetJfdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssJfdcDto>.Ok(await _svc.GetJfdcAsync(type, yjssId)));
[HttpPost("jfdc")]
public async Task<IActionResult> SaveJfdc([FromQuery] int type, [FromBody] YjssJfdcDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveJfdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 切剖调查 ----------
[HttpGet("qpdc/{yjssId}")]
public async Task<IActionResult> GetQpdc(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssDcQpdc>.Ok(await _svc.GetQpdcAsync(type, yjssId)));
[HttpPost("qpdc")]
public async Task<IActionResult> SaveQpdc([FromQuery] int type, [FromBody] YjssDcQpdc dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveQpdcAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 清洁洁净 ----------
[HttpGet("qjjj/{yjssId}")]
public async Task<IActionResult> GetQjjj(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssDcQjjj>.Ok(await _svc.GetQjjjAsync(type, yjssId)));
[HttpPost("qjjj")]
public async Task<IActionResult> SaveQjjj([FromQuery] int type, [FromBody] YjssDcQjjj dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveQjjjAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 茧丝百回纤度 ----------
[HttpGet("jsbhxd/{yjssId}")]
public async Task<IActionResult> GetJsbhxd(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<YjssJsbhxdDto>.Ok(await _svc.GetJsbhxdAsync(type, yjssId)));
[HttpPost("jsbhxd")]
public async Task<IActionResult> SaveJsbhxd([FromQuery] int type, [FromBody] YjssJsbhxdDto dto)
{
try { return Ok(ApiResult<bool>.Ok(await _svc.SaveJsbhxdAsync(type, dto), "保存成功")); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 整页 / 报表 ----------
[HttpGet("full/{type}/{id}")]
public async Task<IActionResult> GetFull(int type, long id)
{
try { return Ok(ApiResult<YjssFullDto>.Ok(await _svc.GetFullAsync(type, id))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpGet("jsdc-summary/{yjssId}")]
public async Task<IActionResult> GetJsdcSummary(long yjssId, [FromQuery] int type = 1)
=> Ok(ApiResult<Dictionary<string, object?>>.Ok(await _svc.GetJsdcSummaryAsync(type, yjssId)));
}
@@ -0,0 +1,351 @@
using System.Security.Claims;
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Common.Exceptions;
using F9WebAiTest.Application.Dtos.SYS.Reports;
using F9WebAiTest.Application.Services.Auth;
using F9WebAiTest.Application.Services.Reports;
using F9WebAiTest.Domain.Entities.SYS.RBAC;
using F9WebAiTest.Domain.Enums.SYS.RBAC;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers;
/// <summary>报表中心:报表定义维护 + 模板下载 + 数据查询(Grid++Report 7 Web 嵌入)</summary>
[ApiController]
[Route("api/sys/report")]
[Authorize]
public class SysReportController : ControllerBase
{
private readonly IReportService _reportService;
private readonly IFreeSql _fsql;
private readonly IJwtService _jwt;
private readonly IConfiguration _config;
public SysReportController(IReportService reportService, IFreeSql fsql, IJwtService jwt, IConfiguration config)
{
_reportService = reportService;
_fsql = fsql;
_jwt = jwt;
_config = config;
}
// ==================== 报表管理 ====================
[HttpGet]
public async Task<IActionResult> GetPaged(
[FromQuery] string like = "",
[FromQuery] string category = "",
[FromQuery] bool? isActive = null,
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 50)
{
var data = await _reportService.GetPagedAsync(like ?? "", category ?? "", isActive, pageIndex, pageSize);
return Ok(ApiResult<object>.Ok(data));
}
[HttpGet("all")]
public async Task<IActionResult> GetAllActive()
=> Ok(ApiResult<object>.Ok(await _reportService.GetAllActiveAsync()));
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
try { return Ok(ApiResult<object>.Ok(await _reportService.GetByIdAsync(id))); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] ReportCreateDto dto)
{
try { return Ok(ApiResult<object>.Ok(new { id = await _reportService.CreateAsync(dto) }, "新增成功")); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(long id, [FromBody] ReportUpdateDto dto)
{
try { await _reportService.UpdateAsync(id, dto); return Ok(ApiResult<object>.Ok(new { id }, "修改成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
try { await _reportService.DeleteAsync(id); return Ok(ApiResult<object>.Ok(new { id }, "删除成功")); }
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
}
/// <summary>上传 .grf 模板文件。
/// 常规编辑流程已改为「模板随报表一起保存」(POST/PUT 携带 templateJson),
/// 本接口保留给 f9designer:// 本机设计器协议回传使用。</summary>
[HttpPost("{id}/template")]
[RequestSizeLimit(50 * 1024 * 1024)]
public async Task<IActionResult> UploadTemplate(long id, IFormFile file)
{
if (file == null || file.Length == 0) return BadRequest(ApiResult<object>.Fail("请选择要上传的 .grf 模板文件", 400));
if (!file.FileName.EndsWith(".grf", StringComparison.OrdinalIgnoreCase))
return BadRequest(ApiResult<object>.Fail("仅支持上传 .grf 模板文件", 400));
try
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
await _reportService.UploadTemplateAsync(id, ms.ToArray(), file.FileName);
return Ok(ApiResult<object>.Ok(new { size = file.Length }, "上传成功"));
}
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>
/// 直接保存模板文本(前端在线编辑「模板源码」后保存,免下载 / 上传 .grf)。
/// 与上传接口的区别:这里提交的是文本(JSON 或 "_WR" 压缩文本),不是文件流。
/// </summary>
[HttpPut("{id}/template/content")]
[RequestSizeLimit(50 * 1024 * 1024)]
public async Task<IActionResult> SaveTemplateContent(long id, [FromBody] ReportTemplateContentDto dto)
{
try
{
var size = await _reportService.SaveTemplateContentAsync(id, dto?.Content ?? "");
return Ok(ApiResult<object>.Ok(new { id, size, hasTemplate = true }, "模板已保存"));
}
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
/// <summary>下载 .grf 模板文件。保留给 f9designer:// 本机设计器协议(下载 → 编辑 → 自动回传)使用。</summary>
[HttpGet("{id}/template/download")]
public async Task<IActionResult> DownloadTemplate(long id)
{
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
var content = r == null ? null : await _reportService.GetTemplateContentAsync(r);
if (string.IsNullOrEmpty(content))
return NotFound(ApiResult<object>.Fail("模板尚未上传", 404));
return File(ToGrfBytes(content), "application/octet-stream", $"{r!.Code}.grf");
}
// ==================== 新增报表辅助:数据源 + 结构探测 ====================
/// <summary>数据源清单(IReportDataProvider 注册表 + 各数据源声明的查询参数)</summary>
[HttpGet("datasources")]
public async Task<IActionResult> GetDataSources()
=> Ok(ApiResult<object>.Ok(await _reportService.GetDataSourcesAsync()));
/// <summary>
/// 探测数据源结构:按 DsName(或 DataSql)执行一次查询,
/// 返回可自动配置的查询参数 + 明细字段(列)+ 少量样本行。
/// </summary>
[HttpPost("scaffold")]
public async Task<IActionResult> Scaffold([FromBody] ReportProbeRequestDto dto)
{
var result = await _reportService.ProbeAsync(dto ?? new ReportProbeRequestDto());
return Ok(ApiResult<object>.Ok(result));
}
// ==================== 模板与数据公开端点(供 HTML5 渲染 / 客户端调用) ====================
// grhtml5.insertReportViewer 的模板参数是普通 GET,浏览器无法附加 Authorization header,
// 故模板/数据接口接受 ?access_token=xxx 方式鉴权(仅内部网络使用,与 /hubs 相同的处理思路)。
/// <summary>下载报表 .grf 模板(带权限校验,支持 query token 鉴权)。
/// 额外暴露 template.json / template.grf 别名:Grid++ 的 JS 库与客户端对 URL 中不含
/// ".grf/.txt/.xml/.json" 的请求会用 POST(取模板会误入上传接口)。</summary>
[HttpGet("{id}/template")]
[HttpGet("{id}/template.json")]
[HttpGet("{id}/template.grf")]
[AllowAnonymous]
public async Task<IActionResult> GetTemplate(long id, [FromQuery(Name = "access_token")] string? accessToken = null)
{
var (userId, isAdmin) = await ResolveCallerAsync(accessToken);
if (userId <= 0) return Unauthorized(new { code = 401, message = "未授权" });
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
var content = r == null ? null : await _reportService.GetTemplateContentAsync(r);
if (string.IsNullOrEmpty(content))
return NotFound(new { code = 404, message = "模板尚未上传" });
if (!await CanAccessReportAsync(userId, isAdmin, r!)) return ForbidReport();
// 不输出 BOM:HTML5 渲染库按 { 或 _WR 前缀判断模板格式,BOM 会破坏该判断
return File(System.Text.Encoding.UTF8.GetBytes(content!), "application/json; charset=utf-8", $"{r!.Code}.grf");
}
/// <summary>执行报表数据查询(GET/POST 均可)。data.json 为 GET 别名(客户端 URL 方法判定约定)</summary>
[HttpGet("{id}/data")]
[HttpGet("{id}/data.json")]
[AllowAnonymous]
public async Task<IActionResult> GetData(long id, [FromQuery] string? access_token = null, [FromQuery] string? params_ = null)
=> await RunDataAsync(id, access_token, params_);
[HttpPost("{id}/data")]
[AllowAnonymous]
public async Task<IActionResult> PostData(long id, [FromQuery(Name = "access_token")] string? accessToken = null)
{
string? paramsJson = null;
try
{
Request.EnableBuffering();
Request.Body.Position = 0;
using var reader = new StreamReader(Request.Body, leaveOpen: true);
var body = await reader.ReadToEndAsync();
if (!string.IsNullOrEmpty(body)) paramsJson = body;
}
catch { /* 忽略读取错误 */ }
return await RunDataAsync(id, accessToken, paramsJson);
}
private async Task<IActionResult> RunDataAsync(long id, string? accessToken, string? paramsJson)
{
var (userId, isAdmin) = await ResolveCallerAsync(accessToken);
if (userId <= 0) return Unauthorized(new { code = 401, message = "未授权" });
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
if (r == null) return NotFound(new { code = 404, message = "报表不存在" });
if (!await CanAccessReportAsync(userId, isAdmin, r)) return ForbidReport();
var p = ParseParams(paramsJson);
try
{
var data = await _reportService.ExecuteDataAsync(r, p);
return Ok(data);
}
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
private static System.Collections.Generic.Dictionary<string, object?> ParseParams(string? json)
{
var result = new System.Collections.Generic.Dictionary<string, object?>(StringComparer.Ordinal);
if (string.IsNullOrWhiteSpace(json)) return result;
try
{
using var doc = System.Text.Json.JsonDocument.Parse(json);
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object)
{
foreach (var prop in doc.RootElement.EnumerateObject())
result[prop.Name] = System.Text.Json.JsonSerializer.Deserialize<object?>(prop.Value.GetRawText());
}
else if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array)
{
// 支持 [{name,value}] 形式(与 Grid++ WEB 客户端参数一致)
foreach (var el in doc.RootElement.EnumerateArray())
{
if (el.TryGetProperty("name", out var n) && el.TryGetProperty("value", out var v))
result[n.GetString() ?? ""] = System.Text.Json.JsonSerializer.Deserialize<object?>(v.GetRawText());
}
}
}
catch { /* 忽略解析错误,返回空 */ }
return result;
}
// ==================== 报表中心(用户视图) ====================
/// <summary>当前用户可见的报表(按权限过滤)</summary>
[HttpGet("visible")]
public async Task<IActionResult> GetVisible()
{
var userId = GetUserId();
var isAdmin = await IsAdminAsync(userId);
var perms = await GetPermissionsAsync(userId, isAdmin);
return Ok(ApiResult<object>.Ok(await _reportService.GetVisibleReportsAsync(userId, isAdmin, perms)));
}
/// <summary>当前用户预览某报表(带数据)</summary>
[HttpGet("{id}/preview")]
public async Task<IActionResult> Preview(long id, [FromQuery] string? params_ = null)
{
var userId = GetUserId();
var isAdmin = await IsAdminAsync(userId);
var perms = await GetPermissionsAsync(userId, isAdmin);
try
{
await _reportService.EnsureReportAccessAsync(userId, isAdmin, perms, id);
var r = await _fsql.Select<Domain.Entities.SYS.Reports.Report>().Where(x => x.Id == id && !x.IsDeleted).FirstAsync();
if (r == null) return NotFound(ApiResult<object>.Fail("报表不存在", 404));
if (r.ReportFileId == 0)
return BadRequest(ApiResult<object>.Fail("该报表尚未配置模板", 400));
var p = ParseParams(params_);
var data = await _reportService.ExecuteDataAsync(r, p);
return Ok(ApiResult<object>.Ok(new
{
report = new
{
r.Id,
r.Code,
r.Name,
r.Category,
r.Description,
config = r.Config,
hasTemplate = true
},
templateUrl = $"/api/sys/report/{r.Id}/template.json",
data
}));
}
catch (F9NotFoundException ex) { return NotFound(ApiResult<object>.Fail(ex.Message, 404)); }
catch (F9OperationException ex) { return BadRequest(ApiResult<object>.Fail(ex.Message, 400)); }
}
// ==================== 内部辅助 ====================
private long GetUserId() => long.Parse(User.FindFirst("id")!.Value);
private async Task<bool> IsAdminAsync(long userId)
{
var roleIds = await _fsql.Select<UserRole>().Where(ur => ur.UserId == userId).ToListAsync(x => x.RoleId);
if (roleIds.Count == 0) return false;
return await _fsql.Select<Role>().Where(r => roleIds.Contains(r.Id) && r.Code == "admin").AnyAsync();
}
private async Task<System.Collections.Generic.HashSet<string>> GetPermissionsAsync(long userId, bool isAdmin)
{
if (isAdmin)
return new System.Collections.Generic.HashSet<string>(
await _fsql.Select<Permission>().Where(p => !p.IsDeleted && !string.IsNullOrEmpty(p.Code)).ToListAsync(p => p.Code));
var roleIds = await _fsql.Select<UserRole>().Where(ur => ur.UserId == userId).ToListAsync(x => x.RoleId);
if (roleIds.Count == 0) return new();
var permIds = await _fsql.Select<RolePermission>().Where(rp => roleIds.Contains(rp.RoleId) && !rp.IsDeleted).ToListAsync(x => x.PermissionId);
if (permIds.Count == 0) return new();
var codes = await _fsql.Select<Permission>().Where(p => permIds.Contains(p.Id) && !p.IsDeleted && !string.IsNullOrEmpty(p.Code)).ToListAsync(p => p.Code);
return new System.Collections.Generic.HashSet<string>(codes);
}
/// <summary>用 query 中的 access_token 解析用户;优先尝试从 Authorization 头解析;解析失败返回 0。</summary>
private async Task<(long UserId, bool IsAdmin)> ResolveCallerAsync(string? accessToken)
{
// 优先用当前 HTTP 请求上下文(JWT 中间件已解析)
if (User?.Identity?.IsAuthenticated == true)
return (GetUserId(), await IsAdminAsync(GetUserId()));
if (string.IsNullOrEmpty(accessToken)) return (0, false);
try
{
var (uid, _) = _jwt.ValidateAccessToken(accessToken);
return (uid, await IsAdminAsync(uid));
}
catch { return (0, false); }
}
private async Task<bool> CanAccessReportAsync(long userId, bool isAdmin, Domain.Entities.SYS.Reports.Report r)
{
if (isAdmin) return true;
if (!r.IsActive) return false;
var perms = await GetPermissionsAsync(userId, isAdmin);
return perms.Contains($"report:{r.Code}:visble");
}
private IActionResult ForbidReport() => StatusCode(403, new { code = 403, message = "无权访问该报表" });
/// <summary>模板文本 → .grf 字节(带 UTF-8 BOM,与锐浪设计器保存格式一致)</summary>
private static byte[] ToGrfBytes(string content)
{
var text = content.Length > 0 && content[0] == '\uFEFF' ? content.Substring(1) : content;
var enc = new System.Text.UTF8Encoding(true);
var pre = enc.GetPreamble();
var body = enc.GetBytes(text);
var data = new byte[pre.Length + body.Length];
Buffer.BlockCopy(pre, 0, data, 0, pre.Length);
Buffer.BlockCopy(body, 0, data, pre.Length, body.Length);
return data;
}
}
+5 -2
View File
@@ -33,13 +33,16 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
// SignalR 通过 query string 传递 token,需从 access_token 读取
// SignalR 与报表模板/数据下载通过 query string 传递 token
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs"))
if (string.IsNullOrEmpty(accessToken)) return Task.CompletedTask;
var path = context.HttpContext.Request.Path;
if (path.StartsWithSegments("/hubs")
|| path.StartsWithSegments("/api/sys/report"))
{
context.Token = accessToken;
}
Binary file not shown.
@@ -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; }
}
}
@@ -0,0 +1,25 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 工艺执行书主控(对应 F9 biz_prod_qc_gyzxs_version):
/// 以「工艺庄口」为主键的一条汇总指针记录,记录该庄口下每张子单据是否已录入。
/// 列表上的 √ 标记全部来自本表各 *Id &gt; 0。
/// </summary>
[Table(Name = "qc_gyzxs_doc_version")]
public class GyzxsDocVersion : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口 Id
public long ZongheyangId { get; set; } // 综合样茧质 Id(本系统暂未建综合样茧质,保留指针)
public long ShichanId { get; set; } // 试产(试缫工艺单)Id
public long FengMianId { get; set; } // 封面 Id
public long XbjSsdyId { get; set; } // 试缫与大样 Id
public long XbjId { get; set; } // 混剥选茧 Id
public long ZhuJianId { get; set; } // 煮茧 Id
public long ZdsId { get; set; } // 自动缫 Id
public long ZdsDyId { get; set; } // 自动缫大样 Id
public long HsId { get; set; } // 后缫(工艺执行书)Id
public string Memo { get; set; } = ""; // 备注
}
@@ -0,0 +1,23 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 封面(对应 F9 biz_prod_qc_gyzxs_fengmian):
/// 报表「封面」= 桑蚕丝工艺设计书,含编号、庄口、规格、原料总量、生产丝量、缫制车间、生产时间。
/// </summary>
[Table(Name = "qc_gyzxs_fengmian")]
public class GyzxsFengMian : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口 Id
public string BianHao { get; set; } = ""; // 编号(如 SJZ-2025-武工艺-58)
public string Guige { get; set; } = ""; // 缫制规格
public decimal YuanLiaoZhongLiang { get; set; } // 原料总量(吨)
public decimal ShengSiZhongLiang { get; set; } // 生产丝量(吨)
public string ShengChanCheJian { get; set; } = "";// 缫制车间
public DateTime? ShengChanStartDate { get; set; } // 开始生产日期
public DateTime? ShengChanEndDate { get; set; } // 结束生产日期
public string FaSongBuMen { get; set; } = ""; // 发送部门
public string Memo { get; set; } = ""; // 备注
}
@@ -0,0 +1,57 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 后缫(编丝整理)工艺执行书(对应 F9 biz_prod_qc_gyzxs_hs)。
/// 报表「工艺执行书」即本单据:生产指标 9 项 + 工艺条件及控制指标 18 项 + 主要技术措施 + 设计/审核/审批。
/// 指标按 F9 约定全部为字符串,可存 "3100"、"99.8"、"20~38"、"185±5" 这类文本。
/// </summary>
[Table(Name = "qc_gyzxs_hs")]
public class GyzxsHs : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口
public string Guige { get; set; } = ""; // 生丝规格
public DateTime? ZhiXingDate { get; set; } // 执行时间
public string QianSaoCheJian { get; set; } = ""; // 前缫生产车间
public int YuSaoTianShu { get; set; } // 预缫天数
public string ZhengLiGuiGe { get; set; } = "GB1798-2008 白厂丝"; // 整理规格
// ---- 生产指标 ----
public string SczbDanChan { get; set; } = ""; // 单产(片/人、班)
public string SczbRiChan { get; set; } = ""; // 日产(片/班)
public string SczbBianSiLiang { get; set; } = ""; // 编丝量(片/人、班)
public string SczbZhengJianLiang { get; set; } = ""; // 整检量(片/日)
public string SczbShengChanZhengPinLv { get; set; } = ""; // 生产正品率(%)
public string SczbShangJianZhengPinLv { get; set; } = ""; // 商检正品率(%)
public string SczbQieDuan { get; set; } = ""; // 切断(次/批)
public string SczbXiaoLv { get; set; } = ""; // 效率(%)
public string SczbPinZhiCiDian { get; set; } = ""; // 编检品质疵点(%)
// ---- 工艺条件及控制指标 ----
public string GyzbCheSu { get; set; } = ""; // 车速(转/分)
public string GyzbXiaoYuDaiFanShiJian { get; set; } = ""; // 小䈅待返时间(分)
public string GyzbCheJianWenDu { get; set; } = ""; // 车间温度(℃)
public string GyzbCheJianXiangDuiShiDu { get; set; } = ""; // 车间相对湿度(%)
public string GyzbCheXiangWenDu { get; set; } = ""; // 车厢温度(℃)
public string GyzbCheXiangShiDu { get; set; } = ""; // 车厢湿度(%)
public string GyzbPingHengShiWenDu { get; set; } = ""; // 平衡室温度(℃)
public string GyzbPingHengShiXiangDuiShiDu { get; set; } = ""; // 平衡室相对湿度(%)
public string GyzbXiaoYuGeiShiWenDu { get; set; } = ""; // 小䈅给湿温度(℃)
public string GyzbXiaoYuSiPianPingHengShiJian { get; set; } = ""; // 小䈅丝片平衡时间(分)
public string GyzbZhenKongDu { get; set; } = ""; // 真空度(mp)
public string GyzbChouQiCiShu { get; set; } = ""; // 抽气次数(次)
public string GyzbDaYuSiPianPingHengShiJian { get; set; } = ""; // 大䈅丝片平衡时间(分)
public string GyzbDaYuSiPianHuiChaoLv { get; set; } = ""; // 大䈅丝片回潮率(%)
public string GyzbShengSiHuiChaoLv { get; set; } = ""; // 生丝回潮率(%)
public string GyzbDaYuSiPianKuanDu { get; set; } = ""; // 大䈅丝片宽度(cm)
public string GyzbXieSiDuiSiLv { get; set; } = ""; // 屑丝对丝率(%)
public string GyzbDaYuSiPianCiDian { get; set; } = ""; // 大䈅丝片疵点(%)
public string JiShuCuoShi { get; set; } = ""; // 主要技术措施
public string SheJiRen { get; set; } = ""; // 设计
public string ShenHeRen { get; set; } = ""; // 审核
public string PiFuRen { get; set; } = ""; // 审批
public string Memo { get; set; } = ""; // 备注
}
@@ -0,0 +1,65 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 试产(试缫工艺单,对应 F9 biz_prod_qc_gyzxs_shichan):
/// 两个分组——「自动缫试缫工艺」与「试缫煮茧工艺」。
/// 工艺指标大量是区间文本(如 190±5、9、10、11、≥99),因此车速/温度类用 string。
/// </summary>
[Table(Name = "qc_gyzxs_shichan")]
public class GyzxsShichan : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口
public string Guige { get; set; } = ""; // 规格
public string HuanZhuangFangShi { get; set; } = ""; // 换庄方式
public DateTime? ScDate { get; set; } // 试缫日期
public string ScCheJian { get; set; } = ""; // 生产车间
// ---- 茧质调查(来源:综合样茧质 / 手工填写)----
public decimal YiJianSiChang { get; set; } // 一茧丝长(m)
public decimal JieShuLv { get; set; } // 解舒率(%)
public decimal JieShuSiChang { get; set; } // 解舒丝长(m)
public decimal JianCengLv { get; set; } // 茧层率(%)
public decimal JianSiXianDu { get; set; } // 茧丝纤度(D)
public decimal JieShuGuangZhe { get; set; } // 解舒光折(kg)
public decimal WanDiao { get; set; } // 万吊(次)
// ---- 自动缫试缫工艺指标 ----
public string ZdsCheSu { get; set; } = ""; // 车速(转)
public string ZdsYunXuLiShu { get; set; } = ""; // 允许粒数(粒)
public decimal ZdsSaoZhe { get; set; } // 缫折(kg)
public decimal ZdsZhongXinXianDu { get; set; } // 中心纤度(D)
public string ZdsZhongXinLiFuLv { get; set; } = ""; // 中心粒符率(%)
public string ZdsSaoSiWenDu { get; set; } = ""; // 缫丝温度(℃)
public decimal ZdsJiSuanXianDu { get; set; } // 计算纤度(D)
public string ZdsYunXuLiFuLv { get; set; } = ""; // 允许粒符率(%)
public string ZdsSuoXuWenDu { get; set; } = ""; // 索绪温度(℃)
public decimal ZdsPingJunLiShu { get; set; } // 平均粒数(粒)
public decimal ZdsBaiJianYinXu { get; set; } // 白茧引绪(粒)
public string ZdsLiXuWenDu { get; set; } = ""; // 理绪温度(℃)
public decimal ZdsZhongXinLiShu { get; set; } // 中心粒数(粒)
public string ZdsGeiJianKouKuanDu { get; set; } = ""; // 给茧口宽度(mm)
public decimal ZdsLuoSiLongShu { get; set; } // 落丝笼数(笼)
public decimal ZdsLongJianLiShu { get; set; } // 笼茧粒数(粒)
// ---- 试缫煮茧工艺 ----
public string ZjCheSu { get; set; } = ""; // 煮茧车速(转)
public string ZjZhengZhuShi1 { get; set; } = ""; // 蒸煮室温度1
public string ZjZhengZhuShi2 { get; set; } = ""; // 蒸煮室温度2
public string ZjZhengZhuShi3 { get; set; } = ""; // 蒸煮室温度3
public decimal ZjLuoXuWai { get; set; } // 外层掉绪
public decimal ZjLuoXuZhong { get; set; } // 中层掉绪
public decimal ZjLuoXuNei { get; set; } // 内层掉绪
public string ZjTiaoZhengBuWenDu { get; set; } = ""; // 调整部温度
public string ZjChuKouWenDu { get; set; } = ""; // 出口温度
public string ZjFuZhuCheJian { get; set; } = ""; // 附煮车间
public string ZjFuZhuGyzkName { get; set; } = ""; // 附煮庄口
// ---- 签署 ----
public string SheJiRen { get; set; } = ""; // 设计人
public string ShenHeRen { get; set; } = ""; // 审核
public string PiFuRen { get; set; } = ""; // 审批
public string Memo { get; set; } = ""; // 备注
}
@@ -0,0 +1,61 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 混剥选茧工艺执行书(对应 F9 biz_prod_qc_gyzxs_xbj)。
/// 含生产指标 / 质量指标 / 质量控制指标 三组,指标多为百分数或区间文本。
/// </summary>
[Table(Name = "qc_gyzxs_xbj")]
public class GyzxsXbj : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口
public string Guige { get; set; } = ""; // 缫制规格
public string CheJian { get; set; } = ""; // 缫制车间
public DateTime? HuanZhuangDate { get; set; } // 换庄时间
// ---- 生产指标 ----
public decimal ScRiChanLiang { get; set; } // 日产量(包/人)
public decimal ScBanChanLiang { get; set; } // 班产量(包)
public string ScShangCheLv { get; set; } = ""; // 上车率
public string ScLianTongLv { get; set; } = ""; // 联统茧率
public string ScWuJiLv { get; set; } = ""; // 五级茧率
public string ScXiaJiaoLv { get; set; } = ""; // 下茧/茧衣率
public string ScJianYiLv { get; set; } = ""; // 茧衣率
public string ScJiShuCuoShi { get; set; } = ""; // 技术措施要求
// ---- 质量指标(统号茧 / 五级茧 / 下脚茧 交叉率)----
public string QcTongHaoWuJiLv { get; set; } = ""; // 统号茧-五级茧率
public string QcTongHaoXiaJianLv { get; set; } = ""; // 统号茧-下茧率
public string QcWuJiLianTongLv { get; set; } = ""; // 五级茧-联统茧率
public string QcWuJiXiaJiaoLv { get; set; } = ""; // 五级茧-下脚率
public string QcXiaJiaoLianTongLv { get; set; } = ""; // 下脚茧-联统率
public string QcXiaJiaoCiJianLv { get; set; } = ""; // 下脚茧-次茧率
// ---- 质量控制指标(其它级茧日产量 + 共性指标)----
public string QcZbHaoWuZuChanLiang { get; set; } = ""; // 好五茧(kg/组/3人)
public string QcZbLiuJiZuChanLiang { get; set; } = ""; // 六级茧(kg/组/3人)
public string QcZbMianJianZuChanLiang { get; set; } = ""; // 棉茧(kg/组/3人)
public string QcZbXiaJiaoZuChanLiang { get; set; } = ""; // 下脚茧(kg/组/3人)
public string QcZbShuangGongZuChanLiang { get; set; } = ""; // 双宫茧(kg/组/3人)
public string QcZbGongJinLiShu { get; set; } = ""; // 公斤粒数(粒)
public string QcZbBaoGuangLv { get; set; } = ""; // 剥光率(%)
public string QcZbJianYiLv { get; set; } = ""; // 茧衣率(%)
public string Memo { get; set; } = ""; // 备注
}
/// <summary>混茧比例明细(对应 F9 biz_prod_qc_gyzxs_xbj_hunjian)</summary>
[Table(Name = "qc_gyzxs_xbj_hunjian")]
public class GyzxsXbjHunJian : BaseEntity
{
public long XbjId { get; set; } // 选剥茧主表 Id
public long YlzkId { get; set; } // 原料庄口 Id
public string ZhuangBie { get; set; } = ""; // 庄别(冗余名称,便于打印)
public decimal ZhongLiang { get; set; } // 重量(kg)
public int BaoShu { get; set; } // 总包数
public decimal BiLi { get; set; } // 比例(%)
public int HunJianBaoShu { get; set; } // 混茧包数
public int Sort { get; set; } // 排序
}
@@ -0,0 +1,45 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 试缫与大样资料(对应 F9 biz_prod_qc_gyzxs_xbj_ssdy):
/// 「试缫」与「大样」两组同构指标,报表为两行对比表。
/// </summary>
[Table(Name = "qc_gyzxs_xbj_ssdy")]
public class GyzxsXbjSsDy : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口
public string Guige { get; set; } = ""; // 缫制规格
// ---- 试缫 ----
public string SsShangCheLv { get; set; } = ""; // 上车率(%)
public string SsLianTongLv { get; set; } = ""; // 联统率(%)
public string SsCiJianLv { get; set; } = ""; // 次茧率(%)
public string SsCiLiuJiLv { get; set; } = ""; // 次六级(%)
public string SsMianJianLv { get; set; } = ""; // 棉茧率(%)
public string SsXiaJiaoLv { get; set; } = ""; // 下脚率(%)
public string SsShuangGongLv { get; set; } = ""; // 双宫率(%)
public string SsJianYiLv { get; set; } = ""; // 茧衣率(%)
public decimal SsLiJianZhong { get; set; } // 400粒重(克)
public decimal SsGongJinLiShu { get; set; } // 公斤粒数
// ---- 大样 ----
public string DyShangCheLv { get; set; } = ""; // 上车率(%)
public string DyLianTongLv { get; set; } = ""; // 联统率(%)
public string DyCiJianLv { get; set; } = ""; // 次茧率(%)
public string DyCiLiuJiLv { get; set; } = ""; // 次六级(%)
public string DyMianJianLv { get; set; } = ""; // 棉茧率(%)
public string DyXiaJiaoLv { get; set; } = ""; // 下脚率(%)
public string DyShuangGongLv { get; set; } = ""; // 双宫率(%)
public string DyJianYiLv { get; set; } = ""; // 茧衣率(%)
public decimal DyLiJianZhong { get; set; } // 400粒重(克)
public decimal DyGongJinLiShu { get; set; } // 公斤粒数
public string JiShuCuoShi { get; set; } = ""; // 主要技术措施及要求
public string SheJiRen { get; set; } = ""; // 设计
public string ShenHeRen { get; set; } = ""; // 审核
public string PiFuRen { get; set; } = ""; // 审批
public string Memo { get; set; } = ""; // 备注
}
@@ -0,0 +1,112 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 自动缫车间生产工艺执行书(表一,对应 F9 biz_prod_qc_gyzxs_zds)。
/// 含「茧质概况」(冗余自综合样茧质)、「生产指标」、「工艺技术指标」三块。
/// </summary>
[Table(Name = "qc_gyzxs_zds")]
public class GyzxsZds : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口
public string Guige { get; set; } = ""; // 生丝规格
public DateTime? HuanZhuangDate { get; set; } // 接缫日期
public string HuanZhuangFangShi { get; set; } = ""; // 换庄方式
public string CheJian { get; set; } = ""; // 生产车间
public int YuSaoTianShu { get; set; } // 预缫天数(天)
public decimal YuSaoSiLiang { get; set; } // 预缫丝量(吨)
// ---- 茧质概况 ----
public decimal ZgYiJianSiChang { get; set; } // 一茧丝长(米)
public decimal ZgJieShuSiChang { get; set; } // 解舒丝长
public decimal ZgJieShuLv { get; set; } // 解舒率(%)
public decimal ZgJianSiXianDu { get; set; } // 茧丝纤度(D)
public decimal ZgJieShuGuangZhe { get; set; } // 解舒光折(kg)
public decimal ZgWanMiDiaoCao { get; set; } // 万米吊糙(次)
public decimal ZgQingJie { get; set; } // 清洁(分)
public decimal ZgJingDu { get; set; } // 洁净(分)
public decimal ZgJianCengLv { get; set; } // 茧层率(%)
public decimal ZgXianDuJunFangCha { get; set; } // 茧丝纤度均方差
public decimal ZgWaiSanBaiHui { get; set; } // 外三百回纤度
public decimal ZgZhongSanBaiHui { get; set; } // 中三百回纤度
public decimal ZgNeiSanBaiHui { get; set; } // 内三百回纤度
public decimal ZgZuiDaKaiCha { get; set; } // 最大开差
public decimal ZgNeiWaiKaiCha { get; set; } // 内外开差
public decimal ZgBaiSeJianDanXian { get; set; } // 白色茧单纤
public decimal ZgHuiSeJianDanXian { get; set; } // 灰色茧单纤
public decimal ZgHongSeJianDanXian { get; set; } // 红色茧单纤
public decimal ZgLuoXuWai { get; set; } // 落绪分布-外
public decimal ZgLuoXuZhong { get; set; } // 落绪分布-中
public decimal ZgLuoXuNei { get; set; } // 落绪分布-内
public decimal ZgPingJunJianFu { get; set; } // 平均茧幅(mm)
public decimal ZgJianFuZhengQiLv { get; set; } // 茧幅整齐率(%)
public string ZgJianSiBaiHuiXianDu { get; set; } = ""; // 茧丝百回纤度(明细串)
// ---- 生产指标 ----
public string SczbPinWei { get; set; } = ""; // 品味
public string SczbSaoZhe { get; set; } = ""; // 缫折(kg)
public string SczbTaiChan { get; set; } = ""; // 台产
public string SczbPianCha { get; set; } = ""; // 偏差
public string SczbZongCha { get; set; } = ""; // 总差
public string SczbQingJie { get; set; } = ""; // 清洁
public string SczbJingJie { get; set; } = ""; // 洁净
public string SczbYunEr { get; set; } = ""; // 匀Ⅱ
public string SczbZhengPinLv { get; set; } = ""; // 生产正品率
// ---- 工艺技术指标(第一行)----
public string GyzbCheSu { get; set; } = ""; // 车速(转/分)
public string GyzbTianXuCiShu { get; set; } = ""; // 添绪次数(次/分)
public string GyzbYunZhuanLv { get; set; } = ""; // 运转率(%)
public string GyzbZhongXinXianDu { get; set; } = ""; // 中心纤度(D)
public string GyzbSheJiDanXianDu { get; set; } = ""; // 设计单纤度(D)
public string GyzbPingJunLiShu { get; set; } = ""; // 平均粒数(粒)
public string GyzbZhongXinLiShu { get; set; } = ""; // 中心粒数(粒)
public string GyzbYunXuLiShu { get; set; } = ""; // 允许粒数(粒)
public string GyzbYinXu { get; set; } = ""; // 引绪(粒)
public string GyzbZhongXinLiFuLv { get; set; } = ""; // 中心粒符率
public string GyzbYunXuLiFuLv { get; set; } = ""; // 允许粒符率
public string GyzbGeiJianKouKuanDu { get; set; } = ""; // 给茧口宽度
public string GyzbTiaoTuLv { get; set; } = ""; // 条吐率(%)
public string GyzbSiQiaoChangDu { get; set; } = ""; // 丝鞘长度(cm)
// ---- 工艺技术指标(第二行)----
public string GyzbCheTouQiYa { get; set; } = ""; // 车头汽压(MP)
public string GyzbSuoXuWenDu { get; set; } = ""; // 索绪温度(℃)
public string GyzbLiXuWenDu { get; set; } = ""; // 理绪温度(℃)
public string GyzbPianXinPanJianLiang { get; set; } = ""; // 偏心盘茧量
public string GyzbSaoTangWenDu { get; set; } = ""; // 缫汤温度(℃)
public string GyzbJiaJianKouJianLiang { get; set; } = ""; // 加茧口茧量(粒)
public string GyzbWuXuLv { get; set; } = ""; // 无绪率(%)
public string GyzbPengCaoJianLv { get; set; } = ""; // 蓬糙茧率(%)
public string GyzbJiaJianJunYunLv { get; set; } = ""; // 加茧均匀率(%)
public string GyzbZhengQueTianXuLv { get; set; } = ""; // 正确添绪率(%)
public string GyzbTongJianLiang { get; set; } = ""; // 桶茧量(g)
public string GyzbLuoSiLongShu { get; set; } = ""; // 落丝笼数
public string SheJiRen { get; set; } = ""; // 设计
public string ShenHeRen { get; set; } = ""; // 审核
public string PiFuRen { get; set; } = ""; // 审批
public string Memo { get; set; } = ""; // 备注
}
/// <summary>自动缫大样资料(表二,对应 F9 biz_prod_qc_gyzxs_zds_dyzl)</summary>
[Table(Name = "qc_gyzxs_zds_dyzl")]
public class GyzxsZdsDyzl : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口
public long ZdsId { get; set; } // 自动缫主表 Id
public string Guige { get; set; } = ""; // 生丝规格
public string PianCha { get; set; } = ""; // 偏差
public string QingJie { get; set; } = ""; // 清洁
public string JingDu { get; set; } = ""; // 净度
public string JieShuLv { get; set; } = ""; // 解舒率
public string WanMiDiaoCao { get; set; } = ""; // 万米吊糙
public string SaoZhe { get; set; } = ""; // 缫折
public string JiShuCuoShi { get; set; } = ""; // 技术措施及要求
public string SheJiRen { get; set; } = ""; // 设计
public string ShenHeRen { get; set; } = ""; // 审核
public string PiFuRen { get; set; } = ""; // 审批
public string Memo { get; set; } = ""; // 备注
}
@@ -0,0 +1,98 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Gyzxs;
/// <summary>
/// 煮茧工艺执行书主表(对应 F9 biz_prod_qc_gyzxs_zhujian)。
/// 「庄口原料性能」区在本系统内冗余存储在表上,避免强依赖综合样茧质。
/// </summary>
[Table(Name = "qc_gyzxs_zhujian")]
public class GyzxsZhuJian : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口
public string Guige { get; set; } = ""; // 缫制规格
public DateTime? HuanZhuangDate { get; set; } // 换庄时间
public string HuanZhuangFangShi { get; set; } = ""; // 接缫方式
public string CheJian { get; set; } = ""; // 生产车间
public string ZhuJianJi { get; set; } = ""; // 煮茧机号
// ---- 庄口原料性能 ----
public decimal YlGongJinLiShu { get; set; } // 公斤粒数(粒)
public string YlSuoZhou { get; set; } = ""; // 缩绉
public decimal YlJianCengLv { get; set; } // 茧层率
public decimal YlQingJie { get; set; } // 清洁
public decimal YlJingDu { get; set; } // 净度
public decimal YlLiJianSiChang { get; set; } // 粒茧丝长
public decimal YlJieShuLv { get; set; } // 解舒率
public decimal YlJieShuSiChang { get; set; } // 解舒丝长
public decimal YlJianSiXianDu { get; set; } // 茧丝纤度
public decimal YlWanMiDiaoCao { get; set; } // 万米吊糙
public decimal YlJieShuGuangZhe { get; set; } // 解舒光折
// ---- 工艺技术指标 ----
public string GyzbJieShuLv { get; set; } = ""; // 实缫解舒
public string GyzbWanDiao { get; set; } = ""; // 实缫万吊
public string GyzbQingJie { get; set; } = ""; // 清洁
public string GyzbJingDu { get; set; } = ""; // 净度
public string GyzbSiJiaoRongShiLv { get; set; } = ""; // 丝胶溶失率
public string GyzbZuLuoSiLongShu { get; set; } = ""; // 组落丝笼数
public string GyzbSaoZhe { get; set; } = ""; // 缫折(kg)
public string GyzbLongJianLiShu { get; set; } = ""; // 笼茧粒数
public string GyzbMeiTongJianLiang { get; set; } = ""; // 每桶茧量(g)
public string GyzbMeiBaoTongShu { get; set; } = ""; // 每包桶数(笼)
public string GyzbMeiXiaoShiYongJianLong { get; set; } = ""; // 每小时用茧笼数
public string GyzbTiaoTuLv { get; set; } = ""; // 条吐率(%)
// ---- 真空渗透 ----
public string ZkstZhenKongDu { get; set; } = ""; // 真空度
public string ZkstXiShuiWenDu { get; set; } = ""; // 吸水温度
public string ZkstDaZhenKongShiChang { get; set; } = ""; // 没进水时达到最高真空度时长
public string ZkstZuiGaoWeiShiChang { get; set; } = ""; // 液面到最高位时长
public string ZkstGuanShuiZhenKongShiChang { get; set; } = ""; // 关水后继续真空时长
public string ZkstChuJianWenDu { get; set; } = ""; // 出茧温度
public string ZkstQuanJiHuiZhuanShiJian { get; set; } = ""; // 全机回转时间(分)
public string JiShuCuoShi { get; set; } = ""; // 主要技术措施
public string SheJiRen { get; set; } = ""; // 设计
public string ShenHeRen { get; set; } = ""; // 审核
public string PiFuRen { get; set; } = ""; // 审批
public string Memo { get; set; } = ""; // 备注
}
/// <summary>分车间煮茧指标(对应 F9 biz_prod_qc_gyzxs_zhujian_chejian_zhujianduan)</summary>
[Table(Name = "qc_gyzxs_zhujian_chejian")]
public class GyzxsZhuJianCheJian : BaseEntity
{
public long ZhuJianId { get; set; } // 煮茧主表 Id
public string CheJian { get; set; } = ""; // 车间
public string DiWenShenTou { get; set; } = ""; // 低温渗透
public string ZhengZhuShi1 { get; set; } = ""; // 蒸煮室1
public string ZhengZhuShi2 { get; set; } = ""; // 蒸煮室2
public string ZhengZhuShi3 { get; set; } = ""; // 蒸煮室3
public string TiaoZhengBu1 { get; set; } = ""; // 调整部1
public string TiaoZhengBu2 { get; set; } = ""; // 调整部2
public string TiaoZhengBu3 { get; set; } = ""; // 调整部3
public string TiaoZhengBu4 { get; set; } = ""; // 调整部4
public string TiaoZhengBu5 { get; set; } = ""; // 调整部5
public string ChuJianWenDu { get; set; } = ""; // 出茧温度
public string HuiZhuanShiJian { get; set; } = ""; // 全机回转时间
public int Sort { get; set; }
}
/// <summary>副煮车间结果(对应 F9 biz_prod_qc_gyzxs_zhujian_chejian_fuzhu)</summary>
[Table(Name = "qc_gyzxs_zhujian_fuzhu")]
public class GyzxsZhuJianFuZhu : BaseEntity
{
public long ZhuJianId { get; set; } // 煮茧主表 Id
public string CheJian { get; set; } = ""; // 附煮车间
public string JieShuLv { get; set; } = ""; // 解舒
public string WanDiao { get; set; } = ""; // 万吊
public string SaoZhe { get; set; } = ""; // 缫折(kg)
public string WenDu1 { get; set; } = ""; // 温度1
public string WenDu2 { get; set; } = ""; // 温度2
public string WenDu3 { get; set; } = ""; // 温度3
public string WenDu4 { get; set; } = ""; // 温度4
public string WenDu5 { get; set; } = ""; // 温度5
public int Sort { get; set; }
}
@@ -0,0 +1,28 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Yjss;
/// <summary>茧幅调查主表(对应 F9 biz_prod_qc_yjss_dc_jfdc_main)</summary>
[Table(Name = "qc_yjss_dc_jfdc_main")]
public class YjssDcJfdcMain : BaseEntity
{
public long YjssId { get; set; } // 主表 Id
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样
public decimal PingJunJianFu { get; set; } // 平均茧幅(mm)
public decimal ZuiDaJianFu { get; set; } // 最大茧幅(mm)
public decimal ZhengQiLv { get; set; } // 茧幅整齐率(%)
public decimal JunFangCha { get; set; } // 茧幅均方差
}
/// <summary>茧幅调查明细(对应 F9 biz_prod_qc_yjss_dc_jfdc_item)</summary>
[Table(Name = "qc_yjss_dc_jfdc_item")]
public class YjssDcJfdcItem : BaseEntity
{
public long JfdcId { get; set; } // 茧幅调查主表 Id
public long YjssId { get; set; } // 主表 Id(冗余,便于整表删除)
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样
public decimal JianFu { get; set; } // 茧幅(mm)
public decimal ShuLiang { get; set; } // 数量(粒)
public int Sort { get; set; }
}
@@ -0,0 +1,33 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Yjss;
/// <summary>茧丝百回纤度汇总(对应 F9 biz_prod_qc_yjss_dc_jsbhxdmain)</summary>
[Table(Name = "qc_yjss_dc_jsbhxd_main")]
public class YjssDcJsbhxdMain : BaseEntity
{
public long YjssId { get; set; } // 主表 Id
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样
public decimal XianDuJunFangCha { get; set; } // 茧丝纤度均方差
public decimal ZuiDaKaiCha { get; set; } // 最大开差
public decimal NeiWaiKaiCha { get; set; } // 内外开差
public decimal WaiSanBaiHui { get; set; } // 外三百回
public decimal ZhongSanBaiHui { get; set; } // 中三百回
public decimal NeiSanBaiHui { get; set; } // 内三百回
public decimal BaiSeJianDanXian { get; set; } // 白色茧单纤
public decimal HuiSeJianDanXian { get; set; } // 灰色茧单纤
public decimal HongSeJianDanXian { get; set; } // 红色茧单纤
}
/// <summary>茧丝百回纤度明细(对应 F9 biz_prod_qc_yjss_dc_jsbhxd)</summary>
[Table(Name = "qc_yjss_dc_jsbhxd")]
public class YjssDcJsbhxd : BaseEntity
{
public long JsbhxdMainId { get; set; } // 汇总表 Id
public long YjssId { get; set; } // 主表 Id(冗余)
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样
public int HuiShu { get; set; } // 回数
public decimal XianDu { get; set; } // 纤度(D)
public int Sort { get; set; }
}
@@ -0,0 +1,72 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Yjss;
/// <summary>
/// 解舒调查(试缫,对应 F9 biz_prod_qc_yjss_dc_jsdc):一条主表可有多条本记录。
/// </summary>
[Table(Name = "qc_yjss_dc_jsdc")]
public class YjssDcJsdc : BaseEntity
{
public long YjssId { get; set; } // 主表 Id
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样
public int XuHao { get; set; } // 序号
// ---- 缫丝工艺参数 ----
public int XuShu { get; set; } // 绪数
public int DingLi { get; set; } // 定粒
public int ZhuJianTangWen { get; set; } // 煮茧汤温(℃)
public int SaoSiTangWen { get; set; } // 缫丝汤温(℃)
public int CheSu { get; set; } // 车速(转/分)
public decimal XiaoSiGanLiang { get; set; } // 小丝干量(g)
public decimal XiaoSiGongLiang { get; set; } // 小丝公量(g)
// ---- 落绪分布 ----
public int LdfbWai { get; set; } // 外
public int LdfbZhong { get; set; } // 中
public int LdfbNei { get; set; } // 内
public int LdfbSum { get; set; } // 总粒数
// ---- 解舒核心 ----
public string XianJianYouXuLv { get; set; } = ""; // 鲜茧有绪率(区间文本)
public int JiaoShu { get; set; } // 绞数
public int LingHuiShu { get; set; } // 零回数
public decimal ShengSiZongChang { get; set; } // 生丝总长(回)
public decimal JianSiZongChang { get; set; } // 茧丝总长(回)
public decimal YiJianSiChang { get; set; } // 一茧丝长(m)
public decimal JieShuSiChang { get; set; } // 解舒丝长(m)
public decimal JieShuLv { get; set; } // 解舒率(%)
public decimal JieShuGuangZhe { get; set; } // 解舒光折(kg)
public decimal ShengSiXianDu { get; set; } // 生丝纤度(D)
public decimal JianSiXianDu { get; set; } // 茧丝纤度(D)
// ---- 蛹体粒数 / 比例 ----
public int YtLsNeiYin { get; set; } // 内印
public int YtLsMaoZu { get; set; } // 毛足
public int YtLsChuanTou { get; set; } // 穿头
public int YtLsShuangGong { get; set; } // 双宫
public decimal YtBlNeiYin { get; set; } // 内印比例
public decimal YtBlMaoZu { get; set; } // 毛足比例
public decimal YtBlChuanTou { get; set; } // 穿头比例
public decimal YtBlShuangGong { get; set; } // 双宫比例
// ---- 附煮 ----
public string FzCheJian { get; set; } = ""; // 附煮车间
public string FzGyzkName { get; set; } = ""; // 附煮工艺庄口
public string FzZhengZhuWenDu { get; set; } = ""; // 蒸煮室温度
public string FzTiaoZhengBuWenDu { get; set; } = ""; // 调整部温度
public string FzCheSu { get; set; } = ""; // 附煮车速
// ---- 屑物 ----
public decimal XwJianBaLiang { get; set; } // 总茧巴量
public decimal XwYongYiZhongLiang { get; set; } // 蛹衣重量
public decimal XwYongYiLv { get; set; } // 蛹衣率
public decimal XwLeiJie { get; set; } // 类节
public decimal XwWanMiDiaoCao { get; set; } // 万米吊糙
public decimal XwJianBaBiLi { get; set; } // 茧巴比例
// ---- 样茧 ----
public int JieShuLiShu { get; set; } // 解舒样茧粒数
public decimal JieShuZhongLiang { get; set; } // 解舒样茧重量(g)
}
@@ -0,0 +1,50 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Yjss;
/// <summary>
/// 清洁洁净(对应 F9 biz_prod_qc_yjss_dc_qjjj):
/// 匀度(一/二/三度) + 洁净档位片数(100~60) + 疵点粒数(次要/普通) + 计算出的清洁、洁净、片数。
/// </summary>
[Table(Name = "qc_yjss_dc_qjjj")]
public class YjssDcQjjj : BaseEntity
{
public long YjssId { get; set; } // 主表 Id
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样
// ---- 匀度 ----
public int YdYiDu { get; set; } // 一度
public int YdErDu { get; set; } // 二度
public int YdSanDu { get; set; } // 三度
// ---- 洁净档位(片数)----
public int Jj100 { get; set; }
public int Jj95 { get; set; }
public int Jj90 { get; set; }
public int Jj85 { get; set; }
public int Jj80 { get; set; }
public int Jj75 { get; set; }
public int Jj70 { get; set; }
public int Jj65 { get; set; }
public int Jj60 { get; set; }
// ---- 次要疵点 ----
public int QjTdc { get; set; } // 特大糙
public int QjFs { get; set; } // 废丝
public int QjDc { get; set; } // 大糙
public int QjNfc { get; set; } // 黏附糙
public int QjDcj { get; set; } // 大长结
public int QjZlx { get; set; } // 重螺旋
// ---- 普通疵点 ----
public int QjXc { get; set; } // 小糙
public int QjCj { get; set; } // 长结
public int QjLx { get; set; } // 螺旋
public int QjHjls { get; set; } // 环结及裂丝
// ---- 结果 ----
public int PianShu { get; set; } // 片数
public decimal QingJie { get; set; } // 清洁
public decimal JingJie { get; set; } // 洁净
}
@@ -0,0 +1,29 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Yjss;
/// <summary>切剖调查(对应 F9 biz_prod_qc_yjss_dc_qpdc)</summary>
[Table(Name = "qc_yjss_dc_qpdc")]
public class YjssDcQpdc : BaseEntity
{
public long YjssId { get; set; } // 主表 Id
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样
public decimal YlQuanJian { get; set; } // 原量-全茧
public decimal YlJianCeng { get; set; } // 原量-茧层
public decimal YlYongTi { get; set; } // 原量-蛹体
public decimal JianCengLv { get; set; } // 茧层率(%)
public decimal GlYongTi { get; set; } // 蛹体干量
public decimal GlJianCeng { get; set; } // 茧层干量
public decimal GlZhuJian { get; set; } // 煮茧后干量
public decimal JianCengLvGongLiang { get; set; } // 公量茧层率(%)
public decimal TuoJiaoLv { get; set; } // 脱胶率(%)
public int LsJiangCan { get; set; } // 僵蚕粒数
public int LsNeiRan { get; set; } // 内染粒数
public int LsMeiYong { get; set; } // 霉蛹粒数
public int LsMaoJiao { get; set; } // 毛脚粒数
public decimal BlJiangCan { get; set; } // 僵蚕比例(%)
public decimal BlNeiRan { get; set; } // 内染比例(%)
public decimal BlMeiYong { get; set; } // 霉蛹比例(%)
public decimal BlMaoJiao { get; set; } // 毛脚比例(%)
}
@@ -0,0 +1,45 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Yjss;
/// <summary>
/// 选茧调查(对应 F9 biz_prod_qc_yjss_dc_xjdc):
/// 11 个茧别重量 + 11 个比例 + 公斤茧粒数 + 解舒样茧粒数/重量。
/// </summary>
[Table(Name = "qc_yjss_dc_xjdc")]
public class YjssDcXjdc : BaseEntity
{
public long YjssId { get; set; } // 主表 Id(原料小样 / 综合样)
public int YjssType { get; set; } = 1; // 样品类型:1=原料小样 2=综合样(两张主表 Id 各自自增,需判别)
// ---- 茧重(g)----
public decimal ZlLtj { get; set; } // 联统茧
public decimal ZlHwj { get; set; } // 好五级茧
public decimal ZlWj { get; set; } // 五级茧
public decimal ZlMianJian { get; set; } // 绵茧
public decimal ZlHuangBan { get; set; } // 黄斑
public decimal ZlQuJian { get; set; } // 蛆茧
public decimal ZlShuangGong { get; set; } // 双宫
public decimal ZlXueJian { get; set; } // 血茧
public decimal ZlBaoPi { get; set; } // 薄皮
public decimal ZlJianYi { get; set; } // 茧衣
public decimal ZlMeiJian { get; set; } // 霉茧
// ---- 比例(%)----
public decimal BlLtj { get; set; }
public decimal BlHwj { get; set; }
public decimal BlWj { get; set; }
public decimal BlMianJian { get; set; }
public decimal BlHuangBan { get; set; }
public decimal BlQuJian { get; set; }
public decimal BlShuangGong { get; set; }
public decimal BlXueJian { get; set; }
public decimal BlBaoPi { get; set; }
public decimal BlJianYi { get; set; }
public decimal BlMeiJian { get; set; }
public int GongJinLiShu { get; set; } // 公斤茧粒数
public int JieShuLiShu { get; set; } // 解舒样茧粒数
public decimal JieShuZhongLiang { get; set; } // 解舒样茧重量(g)
}
@@ -0,0 +1,41 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
namespace F9WebAiTest.Domain.Entities.QC.Yjss;
/// <summary>
/// 样茧试缫——原料小样主表(对应 F9 biz_prod_qc_yjss_main_ylxy)。
/// 能力范围:选茧调查 / 解舒调查 / 清洁洁净。
/// </summary>
[Table(Name = "qc_yjss_main_ylxy")]
public class YjssMainYlxy : BaseEntity
{
public long YlzkId { get; set; } // 原料庄口 Id
public DateTime Date { get; set; } = DateTime.Today; // 日期
public int Type { get; set; } = 1; // 类型:1定价抽样 2入库抽样 3鲜茧抽样
public decimal ZhongLiang { get; set; } // 样茧重量(g)
public decimal HuiChaoLv { get; set; } // 回潮率(%)
public string Memo { get; set; } = ""; // 备注
public long XjdcId { get; set; } // 选茧调查 Id
public int JsdcCt { get; set; } // 解舒调查记录数
public long QjjjId { get; set; } // 清洁洁净 Id
}
/// <summary>
/// 样茧试缫——综合样主表(对应 F9 biz_prod_qc_yjss_main_zhy)。
/// 能力范围:选茧 / 解舒 / 茧幅 / 切剖 / 清洁洁净 / 茧丝百回纤度。
/// </summary>
[Table(Name = "qc_yjss_main_zhy")]
public class YjssMainZhy : BaseEntity
{
public long GyzkId { get; set; } // 工艺庄口 Id
public DateTime Date { get; set; } = DateTime.Today; // 日期
public decimal ZhongLiang { get; set; } // 样茧重量(g)
public string Memo { get; set; } = ""; // 备注
public long XjdcId { get; set; } // 选茧调查 Id
public int JsdcCt { get; set; } // 解舒调查记录数
public long JfdcId { get; set; } // 茧幅调查 Id
public long QpdcId { get; set; } // 切剖调查 Id
public long QjjjId { get; set; } // 清洁洁净 Id
public int JsbhxdCt { get; set; } // 茧丝百回纤度记录数
}
@@ -0,0 +1,130 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Events.SYS.Reports;
namespace F9WebAiTest.Domain.Entities.SYS.Reports;
/// <summary>
/// 报表定义(Grid++Report 模板)。每张报表对应一个 .grf 模板与一段数据查询 SQL。
/// 权限模型:创建/更新报表时自动同步一条 Permission(ResourceType=报表),
/// 角色在「角色管理 - 报表权限」中分配给用户后即可在报表中心查看。
/// </summary>
[Table(Name = "sys_reports")]
public class Report : AggregateRoot
{
[Column(Name = "code")]
public string Code { get; private set; } = ""; // 唯一标识,作为权限码的 resourceIdentity
[Column(Name = "name")]
public string Name { get; private set; } = ""; // 显示名称
[Column(Name = "category")]
public string Category { get; private set; } = "通用"; // 分类
[Column(Name = "description")]
public string Description { get; private set; } = ""; // 描述
/// <summary>数据集 SQL(必须以 SELECT 开头)。FreeSql ExecuteDataTable 执行。</summary>
[Column(Name = "data_sql")]
public string DataSql { get; private set; } = "";
/// <summary>数据集查询参数定义 JSON:[{"name":"deptId","label":"部门","type":"int","default":0}]</summary>
[Column(Name = "data_params")]
public string DataParams { get; private set; } = "[]";
/// <summary>
/// 查询配置 JSON(F9 ReportConfig 约定):
/// { "DsName":"查询名(后端 IReportDataProvider 解析)", "ViewerType":0,
/// "Parms":[{ "Name":"q_yearmonth","Label":"账期","Prompt":"","IsRequired":false,
/// "ControlType":"AntdUI.DatePickerYearMonth","Ds_Static":null,"Ds_Service":null,"DefaultValue":"" }] }
/// DsName 优先于 DataSql。
/// </summary>
[Column(Name = "config")]
public string Config { get; private set; } = "";
/// <summary>
/// 报表模板文件 Id(对齐 F9 Report.ReportFileId)。
/// 模板实例存储在 ReportFile(sys_report_files.DesignContent),与报表定义分离。
/// </summary>
[Column(Name = "report_file_id")]
public long ReportFileId { get; private set; }
// ---------- 以下三个字段已废弃(历史遗留列,仅供启动时把旧模板迁移到 ReportFile 读取) ----------
// 新代码一律通过 ReportFile.DesignContent 读写模板。
/// <summary>[已废弃] 旧版模板二进制,迁移到 ReportFile 后不再写入</summary>
[Column(Name = "template_data", MapType = typeof(byte[]))]
public byte[]? TemplateData { get; private set; }
[Column(Name = "template_name")]
public string TemplateName { get; private set; } = "";
[Column(Name = "template_size")]
public int TemplateSize { get; private set; }
[Column(Name = "sort_order")]
public int SortOrder { get; private set; }
[Column(Name = "is_active")]
public bool IsActive { get; private set; } = true;
public Report() { }
public Report(long id, string code, string name, string category, string description, string dataSql, string dataParams, int sortOrder, string config = "")
{
Id = id;
Code = code ?? "";
Name = name ?? "";
Category = string.IsNullOrWhiteSpace(category) ? "通用" : category.Trim();
Description = description ?? "";
DataSql = dataSql ?? "";
DataParams = string.IsNullOrWhiteSpace(dataParams) ? "[]" : dataParams;
Config = config ?? "";
SortOrder = sortOrder;
IsActive = true;
CreatedAt = DateTime.Now;
AddDomainEvent(new Report_ChangedDomainEvent(Id));
}
public void UpdateBasic(string code, string name, string category, string description, string dataSql, string dataParams, int sortOrder, string? config = null)
{
Code = code ?? Code;
Name = name ?? Name;
Category = string.IsNullOrWhiteSpace(category) ? Category : category.Trim();
Description = description ?? Description;
DataSql = dataSql ?? DataSql;
DataParams = string.IsNullOrWhiteSpace(dataParams) ? DataParams : dataParams;
Config = config ?? Config;
SortOrder = sortOrder;
UpdatedAt = DateTime.Now;
AddDomainEvent(new Report_ChangedDomainEvent(Id));
}
/// <summary>[已废弃] 旧版把模板二进制直接写在报表上;现模板存于 ReportFile.DesignContent</summary>
public void UploadTemplate(byte[] data, string fileName)
{
TemplateData = data ?? Array.Empty<byte>();
TemplateName = fileName ?? "";
TemplateSize = TemplateData.Length;
UpdatedAt = DateTime.Now;
AddDomainEvent(new Report_ChangedDomainEvent(Id));
}
/// <summary>绑定模板文件实例(对齐 F9:Report.ReportFileId → ReportFile)</summary>
public void BindReportFile(long reportFileId)
{
ReportFileId = reportFileId;
UpdatedAt = DateTime.Now;
AddDomainEvent(new Report_ChangedDomainEvent(Id));
}
/// <summary>迁移完成后清空废弃的旧模板字段</summary>
public void ClearLegacyTemplate()
{
TemplateData = null;
TemplateName = "";
TemplateSize = 0;
}
public void Activate() { IsActive = true; UpdatedAt = DateTime.Now; AddDomainEvent(new Report_ChangedDomainEvent(Id)); }
public void Deactivate() { IsActive = false; UpdatedAt = DateTime.Now; AddDomainEvent(new Report_ChangedDomainEvent(Id)); }
}
@@ -0,0 +1,35 @@
using FreeSql.DataAnnotations;
namespace F9WebAiTest.Domain.Entities.SYS.Reports;
/// <summary>
/// 报表模板文件(对齐 BBIT.F9.Domain.Entities.SYS.Reports.ReportFile)。
/// 报表模板作为独立实体与报表定义(Report)分离存储,Report 通过 ReportFileId 关联其模板实例。
/// .grf 本质是 UTF-8 JSON 文本,故以文本(DesignContent)保存。
/// </summary>
[Table(Name = "sys_report_files")]
public class ReportFile : BusinessEntity
{
[Column(Name = "report_id")]
public long ReportId { get; private set; }
/// <summary>模板设计内容(.grf 的 JSON 文本)</summary>
[Column(Name = "design_content", StringLength = -2)]
public string DesignContent { get; private set; } = "";
public ReportFile() { }
public ReportFile(long id, long reportId, string designContent)
{
Id = id;
ReportId = reportId;
DesignContent = designContent ?? "";
CreatedAt = DateTime.Now;
}
public void UpdateContent(string designContent)
{
DesignContent = designContent ?? "";
UpdatedAt = DateTime.Now;
}
}
@@ -0,0 +1,6 @@
using F9WebAiTest.Domain.Entities;
namespace F9WebAiTest.Domain.Events.SYS.Reports;
/// <summary>报表变更领域事件(用于缓存刷新等)</summary>
public record Report_ChangedDomainEvent(long ReportId) : DomainEvent;
@@ -0,0 +1,376 @@
{
"Version":"6.8.5.0",
"Title":"职工花名册",
"Author":"F9 报表中心",
"Description":"示例报表:列出职工的基本信息(数据源由 DsName=RepDt_Sys_Users 提供)。",
"Font":{
"Name":"宋体",
"Size":90000,
"Weight":400,
"Charset":134
},
"Printer":{
"TopMargin":1.5,
"BottomMargin":1.5
},
"DetailGrid":{
"CenterView":true,
"PrintAdaptMethod":"ResizeToFit",
"Recordset":{
"Field":[
{
"Name":"EmployeeCode"
},
{
"Name":"EmployeeName"
},
{
"Name":"Gender"
},
{
"Name":"DepName"
},
{
"Name":"WorktypeName"
},
{
"Name":"Phone"
},
{
"Name":"RuzhiDate"
}
]
},
"Column":[
{
"Name":"EmployeeCode",
"Width":2.5
},
{
"Name":"EmployeeName",
"Width":2.2
},
{
"Name":"Gender",
"Width":1.2
},
{
"Name":"DepName",
"Width":2.8
},
{
"Name":"WorktypeName",
"Width":2.4
},
{
"Name":"Phone",
"Width":2.6
},
{
"Name":"RuzhiDate",
"Width":2.4
}
],
"ColumnContent":{
"Height":0.79375,
"ColumnContentCell":[
{
"Column":"EmployeeCode",
"DataField":"EmployeeCode"
},
{
"Column":"EmployeeName",
"DataField":"EmployeeName"
},
{
"Column":"Gender",
"DataField":"Gender"
},
{
"Column":"DepName",
"DataField":"DepName"
},
{
"Column":"WorktypeName",
"DataField":"WorktypeName"
},
{
"Column":"Phone",
"DataField":"Phone"
},
{
"Column":"RuzhiDate",
"DataField":"RuzhiDate"
}
]
},
"ColumnTitle":{
"BackColor":"C0C0C0",
"Height":0.79375,
"Font":{
"Name":"宋体",
"Size":90000,
"Bold":true,
"Charset":134
},
"RepeatStyle":"OnPage",
"ColumnTitleCell":[
{
"GroupTitle":false,
"Column":"EmployeeCode",
"BackColor":"FFFFFF",
"Text":"工号"
},
{
"GroupTitle":false,
"Column":"EmployeeName",
"BackColor":"FFFFFF",
"Text":"姓名"
},
{
"GroupTitle":false,
"Column":"Gender",
"BackColor":"FFFFFF",
"Text":"性别"
},
{
"GroupTitle":false,
"Column":"DepName",
"BackColor":"FFFFFF",
"Text":"部门"
},
{
"GroupTitle":false,
"Column":"WorktypeName",
"BackColor":"FFFFFF",
"Text":"工种"
},
{
"GroupTitle":false,
"Column":"Phone",
"BackColor":"FFFFFF",
"Text":"电话"
},
{
"GroupTitle":false,
"Column":"RuzhiDate",
"BackColor":"FFFFFF",
"Text":"入职日期"
}
]
}
},
"PageHeader":{
"Height":1.32292,
"Control":[
{
"Type":"MemoBox",
"Name":"MemoBox1",
"Anchor":"[Top|Right]",
"Left":9.5,
"Top":0.396875,
"Width":6.3,
"Height":0.47625,
"Font":{
"Name":"楷体",
"Size":120000,
"Bold":true,
"Charset":134
},
"Text":"职工花名册 · 智慧缫丝系统"
},
{
"Type":"Line",
"Name":"Line1",
"Anchor":"[Left|Top|Right]",
"Top":1.00542,
"Width":16.0073,
"Pen":{
"Width":2
}
}
]
},
"PageFooter":{
"Height":1.32292,
"Control":[
{
"Type":"MemoBox",
"Name":"MemoBox2",
"Anchor":"[Top|Right]",
"Left":13.2027,
"Top":0.396875,
"Width":2.59292,
"Height":0.608542,
"Text":"第[#SystemVar(PageNumber)#]页 共[#SystemVar(PageCount)#]页"
},
{
"Type":"MemoBox",
"Name":"MemoBox3",
"Left":0.211667,
"Top":0.396875,
"Width":6.19125,
"Height":0.608542,
"Text":"打印时间:[#SystemVar(CurrentDateTime):yyyy年MM月dd日 HH:mm:ss#]"
}
]
},
"ReportHeader":[
{
"Height":0.978958,
"Control":[
{
"Type":"StaticBox",
"Name":"StaticBox1",
"Center":"Horizontal",
"Left":6.29708,
"Top":0.185208,
"Width":3.41313,
"Height":0.582083,
"Font":{
"Name":"宋体",
"Size":150000,
"Bold":true,
"Charset":134
},
"Text":"职工花名册"
}
]
},
{
"Height":0.6,
"Control":[
{
"Type":"StaticBox",
"Name":"LabelDep",
"Left":0.2,
"Top":0.05,
"Width":1.4,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Text":"部门:"
},
{
"Type":"StaticBox",
"Name":"ParamDep",
"Left":1.7,
"Top":0.05,
"Width":2.2,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Parameter":"部门"
},
{
"Type":"StaticBox",
"Name":"LabelKeyword",
"Left":4.0,
"Top":0.05,
"Width":1.6,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Text":"关键字:"
},
{
"Type":"StaticBox",
"Name":"ParamKeyword",
"Left":5.7,
"Top":0.05,
"Width":1.8,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Parameter":"关键字"
},
{
"Type":"StaticBox",
"Name":"LabelCount",
"Left":7.6,
"Top":0.05,
"Width":1.6,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Text":"记录数:"
},
{
"Type":"StaticBox",
"Name":"ParamCount",
"Left":9.3,
"Top":0.05,
"Width":1.2,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Parameter":"记录数"
},
{
"Type":"StaticBox",
"Name":"LabelQueryTime",
"Left":10.6,
"Top":0.05,
"Width":2.0,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Text":"查询时间:"
},
{
"Type":"StaticBox",
"Name":"ParamQueryTime",
"Left":12.7,
"Top":0.05,
"Width":3.3,
"Height":0.5,
"Font":{
"Name":"宋体",
"Size":90000,
"Charset":134
},
"Parameter":"查询时间"
}
]
}
],
"Parameter":[
{
"Name":"部门",
"DataType":"String"
},
{
"Name":"关键字",
"DataType":"String"
},
{
"Name":"记录数",
"DataType":"Integer"
},
{
"Name":"查询时间",
"DataType":"String"
}
]
}
@@ -0,0 +1,312 @@
{
"Version": "6.8.5.0",
"Title": "落丝记录",
"Author": "F9 报表中心",
"Description": "生产-前缫:落丝记录明细(数据源 DsName=RepDt_Prod_Lsjl)。",
"Font": {
"Name": "宋体",
"Size": 90000,
"Weight": 400,
"Charset": 134
},
"Printer": {
"TopMargin": 1.5,
"BottomMargin": 1.5
},
"DetailGrid": {
"CenterView": true,
"PrintAdaptMethod": "ResizeToFit",
"Recordset": {
"Field": [
{ "Name": "日期" },
{ "Name": "车间" },
{ "Name": "机组" },
{ "Name": "庄口" },
{ "Name": "回数" },
{ "Name": "颜色" },
{ "Name": "半车" },
{ "Name": "落丝时间" },
{ "Name": "缫丝桶数" }
]
},
"Column": [
{ "Name": "日期", "Width": 2.0 },
{ "Name": "车间", "Width": 3.0 },
{ "Name": "机组", "Width": 1.4 },
{ "Name": "庄口", "Width": 3.2 },
{ "Name": "回数", "Width": 1.4 },
{ "Name": "颜色", "Width": 1.6 },
{ "Name": "半车", "Width": 1.4 },
{ "Name": "落丝时间", "Width": 1.8 },
{ "Name": "缫丝桶数", "Width": 1.5 }
],
"ColumnContent": {
"Height": 0.79375,
"ColumnContentCell": [
{ "Column": "日期", "DataField": "日期" },
{ "Column": "车间", "DataField": "车间" },
{ "Column": "机组", "DataField": "机组" },
{ "Column": "庄口", "DataField": "庄口" },
{ "Column": "回数", "DataField": "回数" },
{ "Column": "颜色", "DataField": "颜色" },
{ "Column": "半车", "DataField": "半车" },
{ "Column": "落丝时间", "DataField": "落丝时间" },
{ "Column": "缫丝桶数", "DataField": "缫丝桶数" }
]
},
"ColumnTitle": {
"BackColor": "C0C0C0",
"Height": 0.79375,
"Font": {
"Name": "宋体",
"Size": 90000,
"Bold": true,
"Charset": 134
},
"RepeatStyle": "OnPage",
"ColumnTitleCell": [
{ "GroupTitle": false, "Column": "日期", "BackColor": "FFFFFF", "Text": "日期" },
{ "GroupTitle": false, "Column": "车间", "BackColor": "FFFFFF", "Text": "车间" },
{ "GroupTitle": false, "Column": "机组", "BackColor": "FFFFFF", "Text": "机组" },
{ "GroupTitle": false, "Column": "庄口", "BackColor": "FFFFFF", "Text": "庄口" },
{ "GroupTitle": false, "Column": "回数", "BackColor": "FFFFFF", "Text": "回数" },
{ "GroupTitle": false, "Column": "颜色", "BackColor": "FFFFFF", "Text": "颜色" },
{ "GroupTitle": false, "Column": "半车", "BackColor": "FFFFFF", "Text": "半车" },
{ "GroupTitle": false, "Column": "落丝时间", "BackColor": "FFFFFF", "Text": "落丝时间" },
{ "GroupTitle": false, "Column": "缫丝桶数", "BackColor": "FFFFFF", "Text": "缫丝桶数" }
]
}
},
"PageHeader": {
"Height": 1.32292,
"Control": [
{
"Type": "MemoBox",
"Name": "MemoBox1",
"Anchor": "[Top|Right]",
"Left": 9.5,
"Top": 0.396875,
"Width": 6.3,
"Height": 0.47625,
"Font": {
"Name": "楷体",
"Size": 120000,
"Bold": true,
"Charset": 134
},
"Text": "落丝记录 · 智慧缫丝系统"
},
{
"Type": "Line",
"Name": "Line1",
"Anchor": "[Left|Top|Right]",
"Top": 1.00542,
"Width": 16.0073,
"Pen": {
"Width": 2
}
}
]
},
"PageFooter": {
"Height": 1.32292,
"Control": [
{
"Type": "MemoBox",
"Name": "MemoBox2",
"Anchor": "[Top|Right]",
"Left": 13.2027,
"Top": 0.396875,
"Width": 2.59292,
"Height": 0.608542,
"Text": "第[#SystemVar(PageNumber)#]页 共[#SystemVar(PageCount)#]页"
},
{
"Type": "MemoBox",
"Name": "MemoBox3",
"Left": 0.211667,
"Top": 0.396875,
"Width": 6.19125,
"Height": 0.608542,
"Text": "打印时间:[#SystemVar(CurrentDateTime):yyyy年MM月dd日 HH:mm:ss#]"
}
]
},
"ReportHeader": [
{
"Height": 0.978958,
"Control": [
{
"Type": "StaticBox",
"Name": "StaticBoxTitle",
"Center": "Horizontal",
"Left": 6.29708,
"Top": 0.185208,
"Width": 3.41313,
"Height": 0.582083,
"Font": {
"Name": "宋体",
"Size": 150000,
"Bold": true,
"Charset": 134
},
"Text": "落丝记录"
}
]
},
{
"Height": 1.2,
"Control": [
{
"Type": "StaticBox",
"Name": "LabelDateStart",
"Left": 0.3,
"Top": 0.05,
"Width": 1.3,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "开始日期:"
},
{
"Type": "MemoBox",
"Name": "ParamDateStart",
"Left": 1.6,
"Top": 0.05,
"Width": 2.5,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "[#{开始日期}#]"
},
{
"Type": "StaticBox",
"Name": "LabelDateEnd",
"Left": 4.4,
"Top": 0.05,
"Width": 1.3,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "结束日期:"
},
{
"Type": "MemoBox",
"Name": "ParamDateEnd",
"Left": 5.7,
"Top": 0.05,
"Width": 2.5,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "[#{结束日期}#]"
},
{
"Type": "StaticBox",
"Name": "LabelDep",
"Left": 8.5,
"Top": 0.05,
"Width": 1.3,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "车间:"
},
{
"Type": "MemoBox",
"Name": "ParamDep",
"Left": 9.8,
"Top": 0.05,
"Width": 2.5,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "[#{车间}#]"
},
{
"Type": "StaticBox",
"Name": "LabelJizu",
"Left": 12.6,
"Top": 0.05,
"Width": 1.3,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "机组:"
},
{
"Type": "MemoBox",
"Name": "ParamJizu",
"Left": 13.9,
"Top": 0.05,
"Width": 2.5,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "[#{机组}#]"
},
{
"Type": "StaticBox",
"Name": "LabelZk",
"Left": 0.3,
"Top": 0.6,
"Width": 1.3,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "庄口:"
},
{
"Type": "MemoBox",
"Name": "ParamZk",
"Left": 1.6,
"Top": 0.6,
"Width": 2.5,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "[#{庄口}#]"
},
{
"Type": "StaticBox",
"Name": "LabelCount",
"Left": 4.4,
"Top": 0.6,
"Width": 1.3,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "记录数:"
},
{
"Type": "MemoBox",
"Name": "ParamCount",
"Left": 5.7,
"Top": 0.6,
"Width": 2.5,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "[#{记录数}#]"
},
{
"Type": "StaticBox",
"Name": "LabelQueryTime",
"Left": 8.5,
"Top": 0.6,
"Width": 1.3,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "查询时间:"
},
{
"Type": "MemoBox",
"Name": "ParamQueryTime",
"Left": 9.8,
"Top": 0.6,
"Width": 3.6,
"Height": 0.5,
"Font": { "Name": "宋体", "Size": 90000, "Charset": 134 },
"Text": "[#{查询时间}#]"
}
]
}
],
"Parameter": [
{ "Name": "开始日期", "DataType": "String" },
{ "Name": "结束日期", "DataType": "String" },
{ "Name": "车间", "DataType": "String" },
{ "Name": "机组", "DataType": "String" },
{ "Name": "庄口", "DataType": "String" },
{ "Name": "记录数", "DataType": "Integer" },
{ "Name": "查询时间", "DataType": "String" }
]
}
@@ -1,4 +1,5 @@
using System.Data;
using System.Text;
using FreeSql;
using F9WebAiTest.Application.Interfaces.SnowflakeID;
using F9WebAiTest.Domain.Common;
@@ -10,6 +11,8 @@ using SysMenu = F9WebAiTest.Domain.Entities.SYS.RBAC.Menu;
using SysRole = F9WebAiTest.Domain.Entities.SYS.RBAC.Role;
using SysPermission = F9WebAiTest.Domain.Entities.SYS.RBAC.Permission;
using SysRolePermission = F9WebAiTest.Domain.Entities.SYS.RBAC.RolePermission;
using SysReport = F9WebAiTest.Domain.Entities.SYS.Reports.Report;
using SysReportFile = F9WebAiTest.Domain.Entities.SYS.Reports.ReportFile;
namespace F9WebAiTest.Infrastructure.Data;
@@ -88,6 +91,20 @@ public static class DbSeeder
// 6.69 修正前缫部门的职责类型(车间班次菜单随"生产-前缫"一起处理)
FixQianxianDutyType(fsql);
// 6.695 质量模块(工艺执行书 / 样茧试缫)
SeedQcMenus(fsql, snowflake);
// 6.71 报表中心菜单(/report 目录 + 报表中心/报表管理两个页面)
SeedReportMenus(fsql, snowflake);
// 6.72 旧版内嵌模板(sys_reports.template_data)迁移为独立模板实例 ReportFile
MigrateReportTemplateToFile(fsql, snowflake);
// 6.72 报表中心示例报表(职工花名册)
SeedSampleReport(fsql, snowflake);
// 6.73 落丝记录报表(生产-前缫,DsName=RepDt_Prod_Lsjl)
SeedLsjlReport(fsql, snowflake);
// 6.7 为菜单创建权限单元(Permission),并修正旧菜单权限关联(统一权限模型)
FixMenuPermissions(fsql, snowflake);
@@ -156,6 +173,7 @@ public static class DbSeeder
EnsureColumn(fsql, "NotificationTypes", "RouteTemplate", "TEXT NOT NULL DEFAULT ''");
EnsureColumn(fsql, "NotificationMessages", "Category", "INTEGER NOT NULL DEFAULT 0");
EnsureColumn(fsql, "NotificationMessages", "RoutePath", "TEXT NOT NULL DEFAULT ''");
EnsureColumn(fsql, "sys_reports", "report_file_id", "INTEGER NOT NULL DEFAULT 0");
}
/// <summary>为已有表补加列(列已存在则忽略)</summary>
@@ -735,6 +753,42 @@ public static class DbSeeder
}
}
/// <summary>
/// 质量模块菜单:一级目录「质量」+ 工艺执行书 / 样茧试缫 两个页面。
/// 模块落在「生产-后缫」之后、消息中心之前。
/// </summary>
private static void SeedQcMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
EnsureTopMenuWithChildren(fsql, snowflake, "/qc", "质量", "Medal", 7,
new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[]
{
("工艺执行书", "/qc/gyzxs", "qc/gyzxs/index", "qc:gyzxs:list", "Document", 1),
("样茧试缫", "/qc/yjss", "qc/yjss/index", "qc:yjss:list", "DataAnalysis", 2)
});
foreach (var (path, sort) in new (string Path, int Sort)[]
{
("/attendance", 2), ("/base", 3), ("/employee", 4),
("/prod/qianxian", 5), ("/prod/houxian", 6), ("/qc", 7),
("/notification", 8), ("/sys", 9), ("/report", 10)
})
{
fsql.Update<SysMenu>()
.Set(x => x.SortOrder, sort)
.Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort)
.ExecuteAffrows();
}
Console.WriteLine("[DbSeeder] 质量模块菜单已就绪(工艺执行书 / 样茧试缫)");
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 质量模块菜单种子跳过:{ex.Message}");
}
}
/// <summary>
/// 确保一个一级目录菜单及其下属页面菜单存在(含父级、图标、启用状态校正),并给 admin 角色授权。
/// 注意:图标名必须是 Element Plus 图标组件名。
@@ -894,6 +948,22 @@ public static class DbSeeder
// 2. 修正旧菜单权限关联(permission_id 旧值 = Menu.Id,改为 Permission.Id)
fsql.Ado.ExecuteNonQuery($"UPDATE sys_rbac_role_permissions SET permission_id = {perm.Id} WHERE permission_id = {menu.Id};");
}
// 3. 清理失效的菜单权限单元:ResourceId 不对应任何菜单
// (前端曾用 Number() 传雪花 Id,超出 2^53 后精度丢失,写入的 resource_id 在菜单表里查不到,
// 表现为「勾选保存后重新打开仍是未勾选、该角色登录也看不到菜单」)
var menuIds = fsql.Select<SysMenu>().ToList().Select(m => m.Id).ToHashSet();
var orphanPerms = fsql.Select<SysPermission>()
.Where(p => p.ResourceType == Resource_Type_Enum.菜单).ToList()
.Where(p => !menuIds.Contains(p.ResourceId)).ToList();
foreach (var bad in orphanPerms)
{
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {bad.Id};");
fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_permissions WHERE id = {bad.Id};");
}
if (orphanPerms.Count > 0)
Console.WriteLine($"[DbSeeder] 已清理 {orphanPerms.Count} 条失效菜单权限单元(资源 Id 不存在)");
Console.WriteLine("[DbSeeder] 菜单权限单元同步完成");
}
catch (Exception ex)
@@ -901,4 +971,399 @@ public static class DbSeeder
Console.WriteLine($"[DbSeeder] 菜单权限单元同步跳过:{ex.Message}");
}
}
}
/// <summary>报表中心菜单(/report 目录 + 报表中心 / 报表管理 两个页面菜单,admin 授权)</summary>
private static void SeedReportMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
EnsureTopMenuWithChildren(fsql, snowflake, "/report", "报表中心", "DataAnalysis", 9,
new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[]
{
("报表中心", "/report/center", "report/center/index", "report:center:list", "PieChart", 1),
("报表管理", "/report/manage", "report/manage/index", "report:manage:list", "Document", 2)
});
Console.WriteLine("[DbSeeder] 报表中心菜单已就绪");
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 报表中心菜单种子跳过:{ex.Message}");
}
}
/// <summary>
/// 旧版把模板二进制直接存在 sys_reports.template_data,现模板是独立实体 ReportFile(DesignContent 文本),
/// Report 通过 ReportFileId 关联。启动时把旧数据迁移过去,迁移完成后清空旧字段。
/// </summary>
private static void MigrateReportTemplateToFile(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
var legacy = fsql.Select<SysReport>()
.Where(r => !r.IsDeleted && r.ReportFileId == 0 && r.TemplateData != null && r.TemplateData.Length > 0)
.ToList();
foreach (var report in legacy)
{
var text = Encoding.UTF8.GetString(report.TemplateData!);
if (text.Length > 0 && text[0] == '\uFEFF') text = text.Substring(1);
var file = new SysReportFile(snowflake.CreateId(), report.Id, text);
fsql.Insert(file).ExecuteAffrows();
report.BindReportFile(file.Id);
report.ClearLegacyTemplate();
fsql.Update<SysReport>().SetSource(report).ExecuteAffrows();
Console.WriteLine($"[DbSeeder] 报表「{report.Name}」模板已迁移到 ReportFile#{file.Id}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 报表模板迁移跳过:{ex.Message}");
}
}
/// <summary>报表中心示例报表:职工花名册(模板为独立 ReportFile 实例 + F9 ReportConfig 查询配置)</summary>
private static void SeedSampleReport(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
const string code = "employee_directory";
const string name = "职工花名册";
const string sql = "SELECT login_name AS LoginName, user_name AS UserName, phone_number AS PhoneNumber, email AS Email FROM sys_rbac_users WHERE is_deleted = 0 ORDER BY id LIMIT 1000";
// F9 ReportConfig:DsName 指向后端查询提供者;Parms 定义动态查询控件
const string config = "{\"DsName\":\"RepDt_Sys_Users\",\"ViewerType\":0,\"Parms\":[" +
"{\"Name\":\"q_dep\",\"Label\":\"部门\",\"Prompt\":\"含子部门,留空为全部\",\"IsRequired\":false," +
"\"ControlType\":\"AntdUI.Select\",\"Ds_Static\":null,\"Ds_Service\":\"/sys/department\",\"DefaultValue\":\"\"}," +
"{\"Name\":\"q_like\",\"Label\":\"关键字\",\"Prompt\":\"按工号 / 姓名模糊查询\",\"IsRequired\":false," +
"\"ControlType\":\"AntdUI.Input\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}]}";
var exist = fsql.Select<SysReport>().Where(r => r.Code == code && !r.IsDeleted).First();
var templateJson = LoadEmbeddedTemplateJson("employee_directory.grf");
// 老版本种子生成的是手写 6.6.8.1 模板(设计器 6.8 无法打开),这里统一替换为 6.8.5.0 官方结构模板
bool needReplaceTemplate = false;
if (exist != null)
{
var file = fsql.Select<SysReportFile>().Where(f => f.ReportId == exist.Id && !f.IsDeleted).First();
needReplaceTemplate = file == null || !file.DesignContent.Contains("6.8.5.0");
}
if (exist == null)
{
var report = new SysReport(snowflake.CreateId(), code, name, "职工管理", "示例报表:按部门 / 关键字查询职工基本信息。", sql, "[]", 1, config);
// 模板实例:ReportFile(对齐 F9 Report.ReportFileId)
if (templateJson != null)
{
var file = new SysReportFile(snowflake.CreateId(), report.Id, templateJson);
fsql.Insert(file).ExecuteAffrows();
report.BindReportFile(file.Id);
}
fsql.Insert(report).ExecuteAffrows();
// 创建对应权限单元
var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.报表, code, report.Id);
perm.SetCode($"report:{code}:visble");
fsql.Insert(perm).ExecuteAffrows();
// 授权给 admin 角色
var adminRole = fsql.Select<SysRole>().Where(r => r.Code == "admin").First();
if (adminRole != null)
{
fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, perm.Id)).ExecuteAffrows();
}
Console.WriteLine("[DbSeeder] 示例报表「职工花名册」已创建");
}
else
{
// 已存在:替换旧模板 / 补全缺失的模板与查询配置
bool changed = false;
if (needReplaceTemplate && templateJson != null)
{
var file = fsql.Select<SysReportFile>().Where(f => f.ReportId == exist.Id && !f.IsDeleted).First();
if (file != null)
{
file.UpdateContent(templateJson);
fsql.Update<SysReportFile>().SetSource(file).ExecuteAffrows();
}
else
{
file = new SysReportFile(snowflake.CreateId(), exist.Id, templateJson);
fsql.Insert(file).ExecuteAffrows();
exist.BindReportFile(file.Id);
}
changed = true;
}
if (string.IsNullOrEmpty(exist.Config) || !exist.Config.Contains("q_dep"))
{
exist.UpdateBasic(exist.Code, exist.Name, exist.Category, exist.Description, exist.DataSql, exist.DataParams, exist.SortOrder, config);
changed = true;
}
if (changed)
{
fsql.Update<SysReport>().SetSource(exist).ExecuteAffrows();
Console.WriteLine("[DbSeeder] 已更新示例报表模板/查询配置");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 示例报表种子跳过:{ex.Message}");
}
}
/// <summary>
/// 落丝记录报表(生产-前缫):DsName=RepDt_Prod_Lsjl,参数为日期范围 / 车间 / 机组 / 庄口 / 关键字。
/// 已存在的报表只补齐查询配置,不覆盖用户在设计器里维护的 .grf 模板。
/// </summary>
private static void SeedLsjlReport(IFreeSql fsql, ISnowflakeIdGenerator snowflake)
{
try
{
const string code = "PROD_QS_1";
const string name = "落丝记录";
const string category = "生产.前缫";
const string dsName = "RepDt_Prod_Lsjl";
// 日期范围参数默认值用「不限」:避免 paramInitialValue 把空值默认成今天,导致只查当天
const string config = "{\"DsName\":\"RepDt_Prod_Lsjl\",\"ViewerType\":0,\"Parms\":[" +
"{\"Name\":\"q_date_start\",\"Label\":\"开始日期\",\"Prompt\":\"留空则不限开始日期\",\"IsRequired\":false," +
"\"ControlType\":\"AntdUI.DatePicker\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"不限\"}," +
"{\"Name\":\"q_date_end\",\"Label\":\"结束日期\",\"Prompt\":\"留空则不限结束日期\",\"IsRequired\":false," +
"\"ControlType\":\"AntdUI.DatePicker\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"不限\"}," +
"{\"Name\":\"q_dep\",\"Label\":\"车间\",\"Prompt\":\"含子车间,留空为全部\",\"IsRequired\":false," +
"\"ControlType\":\"F9-DEP-QS-SELECT\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}," +
"{\"Name\":\"q_jizu\",\"Label\":\"机组\",\"Prompt\":\"留空为全部\",\"IsRequired\":false," +
"\"ControlType\":\"AntdUI.Input\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}," +
"{\"Name\":\"q_zk\",\"Label\":\"庄口\",\"Prompt\":\"点击选择庄口\",\"IsRequired\":false," +
"\"ControlType\":\"F9-GYZK-SELECT\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}," +
"{\"Name\":\"q_like\",\"Label\":\"关键字\",\"Prompt\":\"庄口 / 规格 / 颜色\",\"IsRequired\":false," +
"\"ControlType\":\"AntdUI.Input\",\"Ds_Static\":null,\"Ds_Service\":null,\"DefaultValue\":\"\"}]}";
const string description = "生产-前缫:按日期范围 / 车间 / 机组 / 庄口查询落丝记录明细。";
var exist = fsql.Select<SysReport>().Where(r => r.Code == code && !r.IsDeleted).First();
if (exist == null)
{
var report = new SysReport(snowflake.CreateId(), code, name, category, description, "", "[]", 2, config);
var newTemplate = LoadEmbeddedTemplateJson("lsjl.grf");
if (newTemplate != null)
{
var newFile = new SysReportFile(snowflake.CreateId(), report.Id, newTemplate);
fsql.Insert(newFile).ExecuteAffrows();
report.BindReportFile(newFile.Id);
}
fsql.Insert(report).ExecuteAffrows();
var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.报表, code, report.Id);
perm.SetCode($"report:{code}:visble");
fsql.Insert(perm).ExecuteAffrows();
var adminRole = fsql.Select<SysRole>().Where(r => r.Code == "admin").First();
if (adminRole != null)
fsql.Insert(new SysRolePermission(snowflake.CreateId(), adminRole.Id, perm.Id)).ExecuteAffrows();
Console.WriteLine("[DbSeeder] 落丝记录报表已创建(DsName=RepDt_Prod_Lsjl)");
return;
}
// 已存在:DsName 未指向该 Provider、或参数标签全为空(程序生成的占位配置)时补齐查询配置
// (数据 SQL 清空,避免与 DsName 混淆)
var changed = false;
if (string.IsNullOrEmpty(exist.Config) || !exist.Config.Contains(dsName)
|| ConfigParamsMissingLabel(exist.Config))
{
exist.UpdateBasic(exist.Code, exist.Name,
string.IsNullOrWhiteSpace(exist.Category) ? category : exist.Category,
string.IsNullOrWhiteSpace(exist.Description) ? description : exist.Description,
"", exist.DataParams, exist.SortOrder, config);
changed = true;
Console.WriteLine("[DbSeeder] 落丝记录报表查询配置已更新(DsName=RepDt_Prod_Lsjl)");
}
// 模板是占位模板(无标题 / 无参数声明,多为早期上传测试产物)时,替换为内置规范模板;
// 旧版内置模板有两类问题一并升级:
// 1) 用 Parameter 属性绑定报表参数(GR 推荐 [#{参数名}#] 文本标记)
// 2) 把「查询条件」(q_*) 误当成模板参数(那是查询面板的东西,不是报表参数)
var templateJson = LoadEmbeddedTemplateJson("lsjl.grf");
var file = fsql.Select<SysReportFile>().Where(f => f.ReportId == exist.Id && !f.IsDeleted).First();
var oursTemplate = file != null && file.DesignContent.Contains("\"Author\": \"F9 报表中心\"");
var legacyParamBinding = oursTemplate
&& (file!.DesignContent.Contains("\"Parameter\": \"开始日期\"")
|| TemplateHasQueryParams(file.DesignContent)
|| TemplateHasStaticBoxParamBinding(file.DesignContent));
if (templateJson != null && (file == null || legacyParamBinding || IsPlaceholderReportTemplate(file.DesignContent)))
{
if (file == null)
{
file = new SysReportFile(snowflake.CreateId(), exist.Id, templateJson);
fsql.Insert(file).ExecuteAffrows();
}
else
{
file.UpdateContent(templateJson);
fsql.Update<SysReportFile>().SetSource(file).ExecuteAffrows();
}
exist.BindReportFile(file.Id);
changed = true;
Console.WriteLine("[DbSeeder] 落丝记录报表模板已更新为内置模板(标题 + 查询参数区)");
}
if (changed) fsql.Update<SysReport>().SetSource(exist).ExecuteAffrows();
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 落丝记录报表种子跳过:{ex.Message}");
}
}
/// <summary>
/// 判断查询条件配置是否缺少标签(Parms 为空,或所有 Label 都为空 → 程序生成的占位配置)。
/// 用户手工配过标签的配置不会被种子覆盖。
/// </summary>
private static bool ConfigParamsMissingLabel(string config)
{
if (string.IsNullOrWhiteSpace(config)) return true;
try
{
using var doc = System.Text.Json.JsonDocument.Parse(config);
if (!doc.RootElement.TryGetProperty("Parms", out var ps)
|| ps.ValueKind != System.Text.Json.JsonValueKind.Array
|| ps.GetArrayLength() == 0)
return true;
foreach (var p in ps.EnumerateArray())
{
if (p.ValueKind != System.Text.Json.JsonValueKind.Object) continue;
var label = p.TryGetProperty("Label", out var l) && l.ValueKind == System.Text.Json.JsonValueKind.String
? l.GetString() ?? ""
: "";
if (!string.IsNullOrWhiteSpace(label)) return false;
}
return true;
}
catch
{
return true;
}
}
/// <summary>
/// 判断模板是否把报表参数绑在 StaticBox 上(实测 [#{参数}#] 只在 MemoBox 生效,StaticBox 原样输出)。
/// </summary>
private static bool TemplateHasStaticBoxParamBinding(string content)
{
if (string.IsNullOrWhiteSpace(content) || content.StartsWith("_WR", StringComparison.Ordinal)) return false;
try
{
using var doc = System.Text.Json.JsonDocument.Parse(content);
return ContainsStaticBoxParamBinding(doc.RootElement);
}
catch
{
return false;
}
}
private static bool ContainsStaticBoxParamBinding(System.Text.Json.JsonElement el)
{
if (el.ValueKind == System.Text.Json.JsonValueKind.Object)
{
var type = el.TryGetProperty("Type", out var t) && t.ValueKind == System.Text.Json.JsonValueKind.String
? t.GetString() ?? ""
: "";
var text = el.TryGetProperty("Text", out var x) && x.ValueKind == System.Text.Json.JsonValueKind.String
? x.GetString() ?? ""
: "";
if (type == "StaticBox" && text.Contains("[#{")) return true;
foreach (var prop in el.EnumerateObject())
if (ContainsStaticBoxParamBinding(prop.Value)) return true;
}
else if (el.ValueKind == System.Text.Json.JsonValueKind.Array)
{
foreach (var item in el.EnumerateArray())
if (ContainsStaticBoxParamBinding(item)) return true;
}
return false;
}
/// <summary>
/// 判断模板是否把「查询条件」(q_* 参数) 误写成了模板参数。
/// 模板参数应来自后端 Parameter_Data(如 车间 / 记录数 / 查询时间),不是查询面板的 q_*。
/// </summary>
private static bool TemplateHasQueryParams(string content)
{
if (string.IsNullOrWhiteSpace(content) || content.StartsWith("_WR", StringComparison.Ordinal)) return false;
try
{
using var doc = System.Text.Json.JsonDocument.Parse(content);
if (!doc.RootElement.TryGetProperty("Parameter", out var ps)
|| ps.ValueKind != System.Text.Json.JsonValueKind.Array)
return false;
foreach (var p in ps.EnumerateArray())
{
if (p.ValueKind != System.Text.Json.JsonValueKind.Object) continue;
if (!p.TryGetProperty("Name", out var n) || n.ValueKind != System.Text.Json.JsonValueKind.String) continue;
if ((n.GetString() ?? "").StartsWith("q_", StringComparison.Ordinal)) return true;
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// 判断模板是否为占位模板:JSON 格式、Title 为空、且没有声明任何 Parameter。
/// GR7 设计器保存的 _WR 压缩模板无法判断,一律视为正常模板(不覆盖)。
/// </summary>
private static bool IsPlaceholderReportTemplate(string content)
{
if (string.IsNullOrWhiteSpace(content) || content.StartsWith("_WR", StringComparison.Ordinal)) return false;
try
{
using var doc = System.Text.Json.JsonDocument.Parse(content);
var root = doc.RootElement;
var title = root.TryGetProperty("Title", out var t) && t.ValueKind == System.Text.Json.JsonValueKind.String
? t.GetString() ?? ""
: "";
var hasParamDecl = root.TryGetProperty("Parameter", out var p)
&& p.ValueKind == System.Text.Json.JsonValueKind.Array
&& p.GetArrayLength() > 0;
return string.IsNullOrWhiteSpace(title) && !hasParamDecl;
}
catch
{
return false;
}
}
/// <summary>从嵌入资源读取报表 .grf 模板 JSON 文本(.grf 为 UTF-8 JSON,GR7 向下兼容 GR6 格式)</summary>
private static string? LoadEmbeddedTemplateJson(string fileName)
{
try
{
var asm = typeof(DbSeeder).Assembly;
var resName = asm.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith("." + fileName, StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(resName)) return null;
using var stream = asm.GetManifestResourceStream(resName);
if (stream == null) return null;
using var reader = new StreamReader(stream, new UTF8Encoding(false));
var json = reader.ReadToEnd();
// 统一去 BOM 后保存(ReportFile.DesignContent 存纯文本,输出 .grf 时再补 BOM)
return json.Length > 0 && json[0] == '\uFEFF' ? json.Substring(1) : json;
}
catch (Exception ex)
{
Console.WriteLine($"[DbSeeder] 读取嵌入模板失败:{ex.Message}");
return null;
}
}
}
@@ -15,6 +15,8 @@
<ItemGroup>
<!-- 工种基础数据种子(来源于 F9 系统 biz_base_worktype 三张表导出) -->
<EmbeddedResource Include="Data\SeedData\*.json" />
<!-- 报表中心示例 .grf 模板(GR6.8.5.0 格式;GR7 设计器向下兼容,可直接打开编辑) -->
<EmbeddedResource Include="Assets\ReportTemplates\*.grf" />
</ItemGroup>
<ItemGroup>
@@ -20,7 +20,11 @@ internal class DepartmentWriteRepository : F9RepositoryBase, IDepartmentWriteRep
public async Task<bool> UpdateAsync(Department dep)
{
dep.UpdatedAt = DateTime.Now;
return await Orm.Update<Department>().SetSource(dep).ExecuteAffrowsAsync() > 0;
// 编辑只覆盖业务字段:启用/禁用有独立命令,不能被 SetSource 连带重置;创建信息与删除标记同理
return await Orm.Update<Department>()
.SetSource(dep)
.IgnoreColumns(x => new { x.IsActive, x.IsDeleted, x.CreatedAt, x.CreatedBy })
.ExecuteAffrowsAsync() > 0;
}
public async Task<bool> SoftDeleteAsync(long id)