IqcInspBillService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  3. /// <summary>
  4. /// S5 来料检验单(qms_qcp_inspbill)只读 list 服务 —— 来料检验流程主入口。
  5. ///
  6. /// 数据源:aidopdev.qms_qcp_inspbill 单表直读(不 join 申请主表/明细/SuppMaster/ApprovalFlow/SQE)。
  7. /// 口径:检验单号=FBILLNO;来源单号=lydjbh;物料=FMATERIALCFG/wlmc/ggxh;供应商=gysmc(内嵌);
  8. /// 批次=pch;数量=FRINSQTY/jysl/dhsl/bhgsl;判定=pd(raw);处理方式=clfs(raw);状态=FBILLSTATUS(raw);
  9. /// 检验员=FINSPECTORID;检验起止=FINSPESTARTDATE/FINSPEENDDATE;创建=FCREATETIME。默认排序 FCREATETIME DESC, id DESC。
  10. ///
  11. /// tenant/组织:qms_qcp_inspbill 已补 tenant_id 列,按严格可信租户过滤(需认证,租户仅取认证后 JWT,忽略前端 tenantId);FORGID 当前全 NULL,不做 FORGID 过滤。
  12. /// 本服务仅 SELECT:无新增/编辑/删除/详情/判定录入/主管审核/SQE 处置/仓储通知/
  13. /// 库存事务/流程实例;不写 qms_qcp_inspbill、不改 FBIZTYPE、不碰 ApprovalFlow。pd/clfs/FBILLSTATUS 字典未闭合,raw 展示。
  14. /// </summary>
  15. [ApiDescriptionSettings(Order = 309, Description = "来料检验单列表")]
  16. [Route("api/S5IqcInspBill")]
  17. [NonUnify]
  18. public class IqcInspBillService : IDynamicApiController, ITransient
  19. {
  20. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  21. private const long MainTenantId = 1300000000001L;
  22. private readonly ISqlSugarClient _db;
  23. private readonly UserManager _userManager;
  24. /// <summary>Batch 3:项目实例 + 逐样本 的只读读取器(与 save-detail 回执共用同一行形状与汇总口径)。</summary>
  25. private readonly IqcInspBillItemReader _itemReader;
  26. /// <summary>Batch 3:编辑授权唯一判据,供详情回传 canEdit(前端不得自行按节点推断)。</summary>
  27. private readonly IqcInspBillEditGate _editGate;
  28. public IqcInspBillService(
  29. ISqlSugarClient db,
  30. UserManager userManager,
  31. IqcInspBillItemReader itemReader,
  32. IqcInspBillEditGate editGate)
  33. {
  34. _db = db;
  35. _userManager = userManager;
  36. _itemReader = itemReader;
  37. _editGate = editGate;
  38. }
  39. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  40. private long ResolveTenantOrThrow()
  41. {
  42. var tid = _userManager.TenantId;
  43. if (tid <= 0)
  44. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  45. if (_userManager.SuperAdmin && tid == MainTenantId)
  46. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  47. return tid;
  48. }
  49. /// <summary>排序字段白名单(前端 prop → 物理列),杜绝 orderBy 注入。</summary>
  50. private static readonly Dictionary<string, string> OrderByWhitelist = new(StringComparer.OrdinalIgnoreCase)
  51. {
  52. ["billNo"] = "ib.FBILLNO",
  53. ["sourceBillNo"] = "ib.lydjbh",
  54. ["materialCode"] = "ib.FMATERIALCFG",
  55. ["materialName"] = "ib.wlmc",
  56. ["supplierName"] = "ib.gysmc",
  57. ["lotNo"] = "ib.pch",
  58. ["billStatus"] = "ib.FBILLSTATUS",
  59. ["judgement"] = "ib.pd",
  60. ["inspectDate"] = "ib.FINSPESTARTDATE",
  61. ["createTime"] = "ib.FCREATETIME",
  62. };
  63. /// <summary>
  64. /// 来料检验单列表(只读分页查询)。
  65. /// </summary>
  66. [DisplayName("来料检验单列表")]
  67. [HttpGet("list")]
  68. public async Task<object> GetList([FromQuery] IqcInspBillListInput input)
  69. {
  70. var page = input.Page <= 0 ? 1 : input.Page;
  71. var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize);
  72. var offset = (page - 1) * pageSize;
  73. // 同库分租户:qms_qcp_inspbill 已补 tenant_id 列,按严格可信租户(仅认证后 JWT,忽略前端 tenantId)过滤,杜绝匿名跨租户读。FORGID 全 NULL 不做组织过滤。
  74. var tenantId = ResolveTenantOrThrow();
  75. var where = new List<string> { "ib.tenant_id = @TenantId" };
  76. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  77. void AddLike(string? v, string col, string p)
  78. {
  79. if (string.IsNullOrWhiteSpace(v)) return;
  80. where.Add($"{col} LIKE @{p}");
  81. pars.Add(new SugarParameter($"@{p}", $"%{v.Trim()}%"));
  82. }
  83. AddLike(input.BillNo, "ib.FBILLNO", "BillNo");
  84. AddLike(input.SourceBillNo, "ib.lydjbh", "SourceBillNo");
  85. AddLike(input.MaterialCode, "ib.FMATERIALCFG", "MaterialCode");
  86. AddLike(input.MaterialName, "ib.wlmc", "MaterialName");
  87. AddLike(input.SupplierName, "ib.gysmc", "SupplierName");
  88. AddLike(input.LotNo, "ib.pch", "LotNo");
  89. AddLike(input.Inspector, "ib.FINSPECTORID", "Inspector");
  90. if (!string.IsNullOrWhiteSpace(input.BillStatus))
  91. {
  92. where.Add("ib.FBILLSTATUS = @BillStatus");
  93. pars.Add(new SugarParameter("@BillStatus", input.BillStatus.Trim()));
  94. }
  95. if (input.Judgement.HasValue)
  96. {
  97. where.Add("ib.pd = @Judgement");
  98. pars.Add(new SugarParameter("@Judgement", input.Judgement.Value));
  99. }
  100. if (input.ProcessMethod.HasValue)
  101. {
  102. where.Add("ib.clfs = @ProcessMethod");
  103. pars.Add(new SugarParameter("@ProcessMethod", input.ProcessMethod.Value));
  104. }
  105. if (!string.IsNullOrWhiteSpace(input.InspectDateFrom))
  106. {
  107. where.Add("ib.FINSPESTARTDATE >= @InspectDateFrom");
  108. pars.Add(new SugarParameter("@InspectDateFrom", $"{input.InspectDateFrom.Trim()} 00:00:00"));
  109. }
  110. if (!string.IsNullOrWhiteSpace(input.InspectDateTo))
  111. {
  112. where.Add("ib.FINSPESTARTDATE <= @InspectDateTo");
  113. pars.Add(new SugarParameter("@InspectDateTo", $"{input.InspectDateTo.Trim()} 23:59:59"));
  114. }
  115. if (!string.IsNullOrWhiteSpace(input.CreateDateFrom))
  116. {
  117. where.Add("ib.FCREATETIME >= @CreateDateFrom");
  118. pars.Add(new SugarParameter("@CreateDateFrom", $"{input.CreateDateFrom.Trim()} 00:00:00"));
  119. }
  120. if (!string.IsNullOrWhiteSpace(input.CreateDateTo))
  121. {
  122. where.Add("ib.FCREATETIME <= @CreateDateTo");
  123. pars.Add(new SugarParameter("@CreateDateTo", $"{input.CreateDateTo.Trim()} 23:59:59"));
  124. }
  125. var whereSql = string.Join(" AND ", where);
  126. // 排序:白名单回落,方向 asc/desc 回落 desc;默认 FCREATETIME DESC, id DESC。
  127. string orderSql;
  128. if (!string.IsNullOrWhiteSpace(input.OrderBy) && OrderByWhitelist.TryGetValue(input.OrderBy.Trim(), out var col))
  129. {
  130. var dir = string.Equals(input.OrderDir?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  131. orderSql = $"{col} {dir}, ib.id DESC";
  132. }
  133. else
  134. {
  135. orderSql = "ib.FCREATETIME DESC, ib.id DESC";
  136. }
  137. var total = await _db.Ado.GetIntAsync(
  138. $"SELECT COUNT(1) FROM qms_qcp_inspbill ib WHERE {whereSql}", pars);
  139. var list = await _db.Ado.SqlQueryAsync<IqcInspBillListRow>(
  140. $"""
  141. SELECT
  142. ib.id AS Id,
  143. ib.FBILLNO AS BillNo,
  144. ib.lydjbh AS SourceBillNo,
  145. ib.FMATERIALCFG AS MaterialCode,
  146. ib.wlmc AS MaterialName,
  147. ib.ggxh AS Spec,
  148. ib.gysmc AS SupplierName,
  149. ib.pch AS LotNo,
  150. ib.FRINSQTY AS ReportQty,
  151. ib.jysl AS InspectQty,
  152. ib.dhsl AS QualifiedQty,
  153. ib.bhgsl AS UnqualifiedQty,
  154. ib.pd AS Judgement,
  155. ib.clfs AS ProcessMethod,
  156. ib.FBILLSTATUS AS BillStatus,
  157. ib.FINSPECTORID AS Inspector,
  158. ib.FINSPESTARTDATE AS InspectStartDate,
  159. ib.FINSPEENDDATE AS InspectEndDate,
  160. ib.FCREATETIME AS CreateTime,
  161. ib.FORGID AS OrgId
  162. FROM qms_qcp_inspbill ib
  163. WHERE {whereSql}
  164. ORDER BY {orderSql}
  165. LIMIT {pageSize} OFFSET {offset}
  166. """,
  167. pars);
  168. return new { total, page, pageSize, list };
  169. }
  170. /// <summary>
  171. /// 来料检验单详情(只读,按 id 单表直读 qms_qcp_inspbill)。
  172. ///
  173. /// 仅 SELECT 单行:不 join 申请表/明细/SuppMaster/ApprovalFlow/SQE,不查字典,不写任何表。
  174. /// id&lt;=0 或不存在返回 null(HTTP 200)。字典字段 pd/clfs/FBILLSTATUS/pcsq/blwtflid/FBIZTYPE 全 raw。
  175. /// 流程占位 BizType/BizId/BizCode 由本方法拼装,**不查 ApprovalFlowInstance、不生成流程实例**。
  176. /// qms_qcp_inspbill 已补 tenant_id:详情按 id + 当前租户过滤(与 list 口径一致),FORGID 全 NULL 不做组织过滤。
  177. /// </summary>
  178. [DisplayName("来料检验单详情")]
  179. [HttpGet("detail")]
  180. public async Task<object?> GetDetail([FromQuery] long id)
  181. {
  182. if (id <= 0) return null;
  183. // 同库分租户:详情按 id + 严格可信租户(仅认证后 JWT,忽略前端 tenantId)过滤,杜绝匿名跨租户越权取单(最高优先越权点)。
  184. var tenantId = ResolveTenantOrThrow();
  185. var pars = new List<SugarParameter> { new("@id", id), new("@TenantId", tenantId) };
  186. var list = await _db.Ado.SqlQueryAsync<IqcInspBillDetailRow>(
  187. """
  188. SELECT
  189. ib.id AS Id,
  190. ib.FBILLNO AS BillNo,
  191. ib.lydjbh AS SourceBillNo,
  192. ib.FBILLSTATUS AS BillStatus,
  193. ib.FBIZTYPE AS BizTypeRaw,
  194. ib.FORGID AS OrgId,
  195. ib.FCREATETIME AS CreateTime,
  196. ib.FCREATORID AS CreateUserId,
  197. ib.FMODIFYTIME AS UpdateTime,
  198. ib.FMODIFIERID AS UpdateUserId,
  199. ib.FMATERIALCFG AS MaterialCode,
  200. ib.wlmc AS MaterialName,
  201. ib.ggxh AS Spec,
  202. ib.gysmc AS SupplierName,
  203. ib.pch AS LotNo,
  204. ib.jgbh AS InspectionSpecNo,
  205. ib.jgbb AS InspectionSpecVersion,
  206. ib.jgsxrq AS InspectionSpecEffectiveDate,
  207. ib.FRINSQTY AS ReportQty,
  208. ib.jysl AS InspectQty,
  209. ib.SampleQty AS SampleQty,
  210. ib.TestQty AS TestQty,
  211. ib.dhsl AS QualifiedQty,
  212. ib.bhgsl AS UnqualifiedQty,
  213. ib.phsl AS DestructiveQty,
  214. ib.lysl AS RetainSampleQty,
  215. ib.FINSPECTORID AS Inspector,
  216. ib.jyr AS InspectorName,
  217. ib.FINSPESTARTDATE AS InspectStartDate,
  218. ib.FINSPEENDDATE AS InspectEndDate,
  219. ib.pd AS Judgement,
  220. ib.clfs AS ProcessMethod,
  221. ib.UnqualifiedDescription AS UnqualifiedDescription,
  222. ib.thyy AS ReturnReason,
  223. ib.blwtflid AS DefectCategoryId,
  224. ib.blwtflmc AS DefectCategoryName,
  225. ib.pcsq AS DeviationApply,
  226. ib.pcsm AS DeviationDescription,
  227. ib.leader AS Leader,
  228. ib.FAUDITORID AS AuditorId,
  229. ib.FAUDITDATE AS AuditDate
  230. FROM qms_qcp_inspbill ib
  231. WHERE ib.id = @id AND ib.tenant_id = @TenantId
  232. LIMIT 1
  233. """,
  234. pars);
  235. var row = list.FirstOrDefault();
  236. if (row == null) return null;
  237. // 流程占位:固定 BizType、BizId=id、BizCode=FBILLNO,供后续 C'/D'/E' 批次接入 ApprovalFlow 用。本批不查/不建流程实例。
  238. row.BizType = "S5_IQC_INSPBILL";
  239. row.BizId = row.Id;
  240. row.BizCode = row.BillNo;
  241. row.FlowHint = "流程将在后续 C'/D'/E' 批次接入";
  242. // ── Batch 3 纯加法:项目级结果 ──
  243. // 与 Header 同一个已解析租户,谓词 tenant_id + inspection_bill_id(首列即租户,命中 uk/idx 最左前缀)。
  244. // 只读快照,绝不回查 S0 的 qms_jygfzb —— 否则 S0 改检规会篡改历史检验单的展示。
  245. // N2 / N3 / 终态同样返回(Backend 不因节点隐藏历史测量事实),是否允许编辑由 CanEdit 表达。
  246. row.Lines = await _itemReader.LoadLinesAsync(id, tenantId);
  247. row.LineSummary = IqcInspBillItemReader.Summarize(row.Lines);
  248. row.CanEdit = await _editGate.CanEditAsync(id, tenantId);
  249. return row;
  250. }
  251. }