OutsourceIssueService.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  3. /// <summary>
  4. /// S5 委外发料单 只读 list/detail 服务。
  5. ///
  6. /// 数据源:DOP 数据中台标准层 mdp_std_outsource_issue(头)/ mdp_std_outsource_issue_detail(明细)。
  7. /// 由 OutsourceIssueMdpSyncService 从 aidopdev.NbrMaster/NbrDetail Type='CA' 同步标准化而来。
  8. ///
  9. /// 本服务仅 SELECT:无新增/编辑/删除/写入;明细只暴露已确认 9 列(发料数量/已发数/批次号 3 候选列后置)。
  10. /// 当前上游未产 CA 单据,标准层为空,list 返回空数组、detail 返回 null(前端空态)。
  11. /// </summary>
  12. [ApiDescriptionSettings(Order = 304, Description = "委外发料单")]
  13. [Route("api/OutsourceIssue")]
  14. [NonUnify]
  15. public class OutsourceIssueService : IDynamicApiController, ITransient
  16. {
  17. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  18. private const long MainTenantId = 1300000000001L;
  19. private readonly ISqlSugarClient _db;
  20. private readonly UserManager _userManager;
  21. public OutsourceIssueService(ISqlSugarClient db, UserManager userManager)
  22. {
  23. _db = db;
  24. _userManager = userManager;
  25. }
  26. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  27. private long ResolveTenantOrThrow()
  28. {
  29. var tid = _userManager.TenantId;
  30. if (tid <= 0)
  31. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  32. if (_userManager.SuperAdmin && tid == MainTenantId)
  33. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  34. return tid;
  35. }
  36. /// <summary>
  37. /// 委外发料单列表(只读分页查询)。
  38. /// </summary>
  39. [DisplayName("委外发料单列表")]
  40. [HttpGet("list")]
  41. public async Task<object> GetList([FromQuery] OutsourceIssueListInput input)
  42. {
  43. var page = input.Page <= 0 ? 1 : input.Page;
  44. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  45. var offset = (page - 1) * pageSize;
  46. var where = new List<string> { "1=1" };
  47. var pars = new List<SugarParameter>();
  48. // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。
  49. var tid = ResolveTenantOrThrow();
  50. where.Add("m.tenant_id = @TenantId");
  51. pars.Add(new SugarParameter("@TenantId", tid));
  52. if (!string.IsNullOrWhiteSpace(input.BillNo))
  53. {
  54. where.Add("m.bill_no LIKE @BillNo");
  55. pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
  56. }
  57. if (!string.IsNullOrWhiteSpace(input.OutsourceNo))
  58. {
  59. where.Add("m.outsource_no LIKE @OutsourceNo");
  60. pars.Add(new SugarParameter("@OutsourceNo", $"%{input.OutsourceNo.Trim()}%"));
  61. }
  62. if (!string.IsNullOrWhiteSpace(input.IssueDateStart))
  63. {
  64. where.Add("m.issue_date >= @IssueDateStart");
  65. pars.Add(new SugarParameter("@IssueDateStart", $"{input.IssueDateStart.Trim()} 00:00:00"));
  66. }
  67. if (!string.IsNullOrWhiteSpace(input.IssueDateEnd))
  68. {
  69. where.Add("m.issue_date <= @IssueDateEnd");
  70. pars.Add(new SugarParameter("@IssueDateEnd", $"{input.IssueDateEnd.Trim()} 23:59:59"));
  71. }
  72. var whereSql = string.Join(" AND ", where);
  73. var total = await _db.Ado.GetIntAsync(
  74. $"SELECT COUNT(1) FROM mdp_std_outsource_issue m WHERE {whereSql}", pars);
  75. var list = await _db.Ado.SqlQueryAsync<OutsourceIssueListRow>(
  76. $"""
  77. SELECT
  78. m.id AS Id,
  79. m.bill_no AS Nbr,
  80. m.issue_date AS Date,
  81. m.status_desc AS StatusDescr,
  82. m.outsource_no AS Address,
  83. m.work_order AS WorkOrd,
  84. TRIM(CONCAT(IFNULL(m.department_code,''), ' ', IFNULL(m.department_name,''))) AS DepartmentDescr,
  85. m.issuer AS User1,
  86. m.remark AS Remark,
  87. m.create_user AS CreateUser,
  88. m.source_create_time AS CreateTime
  89. FROM mdp_std_outsource_issue m
  90. WHERE {whereSql}
  91. ORDER BY {BuildOrderBy(input.SortField, input.SortOrder)}
  92. LIMIT {pageSize} OFFSET {offset}
  93. """,
  94. pars);
  95. return new { total, page, pageSize, list };
  96. }
  97. /// <summary>
  98. /// 委外发料单详情(只读:头 + 明细)。查不到返回 null。
  99. /// </summary>
  100. [DisplayName("委外发料单详情")]
  101. [HttpGet("detail")]
  102. public async Task<OutsourceIssueDetailDto?> GetDetail([FromQuery] long id)
  103. {
  104. if (id <= 0) return null;
  105. // 强制租户过滤:头必须归属可信 JWT 租户,忽略前端 tenantId。
  106. var tid = ResolveTenantOrThrow();
  107. var headPars = new List<SugarParameter> { new("@Id", id), new("@TenantId", tid) };
  108. var headWhere = "m.id = @Id AND m.tenant_id = @TenantId";
  109. var head = await _db.Ado.SqlQuerySingleAsync<OutsourceIssueDetailDto>(
  110. $"""
  111. SELECT
  112. m.id AS Id,
  113. m.bill_no AS Nbr,
  114. m.issue_date AS Date,
  115. m.outsource_no AS Address,
  116. m.work_order AS WorkOrd,
  117. TRIM(CONCAT(IFNULL(m.department_code,''), ' ', IFNULL(m.department_name,''))) AS DepartmentDescr,
  118. m.issuer AS User1,
  119. m.status_desc AS StatusDescr,
  120. m.remark AS Remark,
  121. m.create_user AS CreateUser,
  122. m.source_create_time AS CreateTime
  123. FROM mdp_std_outsource_issue m
  124. WHERE {headWhere}
  125. LIMIT 1
  126. """,
  127. headPars);
  128. if (head == null || head.Id <= 0) return null;
  129. head.Lines = await _db.Ado.SqlQueryAsync<OutsourceIssueDetailLineDto>(
  130. """
  131. SELECT
  132. d.id AS Id,
  133. d.line AS Line,
  134. d.item_num AS ItemNum,
  135. d.item_name AS ItemName,
  136. d.um AS Um,
  137. d.qty_ord AS QtyOrd,
  138. d.location_from AS LocationFrom,
  139. d.location_to AS LocationTo,
  140. d.status AS Status,
  141. d.remark AS Remark
  142. FROM mdp_std_outsource_issue_detail d
  143. WHERE d.std_head_id = @HeadId AND d.tenant_id = @TenantId
  144. ORDER BY d.line ASC, d.id ASC
  145. """,
  146. new List<SugarParameter> { new("@HeadId", head.Id), new("@TenantId", tid) });
  147. return head;
  148. }
  149. /// <summary>
  150. /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入。
  151. /// </summary>
  152. private static string BuildOrderBy(string? sortField, string? sortOrder)
  153. {
  154. var column = sortField switch
  155. {
  156. "billNo" => "m.bill_no",
  157. "issueDate" => "m.issue_date",
  158. "sourceCreateTime" => "m.source_create_time",
  159. _ => "m.issue_date",
  160. };
  161. var direction = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  162. return $"{column} {direction}, m.id DESC";
  163. }
  164. }