SalesDeliveryNoticeService.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. using Admin.NET.Plugin.AiDOP.FinishedWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
  3. /// <summary>
  4. /// S7 销售发货通知(头列表)只读 list 服务。
  5. ///
  6. /// 数据源:aidopdev.ASNBOLShipperMaster a LEFT JOIN DepartmentMaster b,忠实复刻证据 sql.txt:
  7. /// 业务过滤 a.ShType='SH' AND a.Typed&lt;&gt;'S' AND a.IsActive=1;join b ON a.Department=b.Department AND a.Domain=b.Domain。
  8. /// 头列表粒度(一行一 ASNBOLShipperMaster)。口径:发货单号=Id;销售单号=OrdNbr;
  9. /// 部门=Department+' '+DepartmentMaster.Descr;发货日期=ShipDate;状态=Status(raw);备注=Remark。
  10. /// 排序默认 ShipDate DESC, RecID DESC。
  11. ///
  12. /// tenant 策略:ASNBOLShipperMaster 有 tenant_id,TenantId&gt;0 时追加 a.tenant_id=@TenantId,TenantId&lt;=0 不过滤;
  13. /// 不使用 Domain 占位过滤(Domain 仅用于 DepartmentMaster join)。
  14. ///
  15. /// 本服务仅 SELECT:无新增/编辑/删除/发货确认/出库/库存事务/状态流转/打印/导出/明细/详情。
  16. /// 明细表 ASNBOLShipperDetail 与 S1 AsnShipperService 均不在本服务范围。
  17. /// </summary>
  18. [ApiDescriptionSettings(Order = 320, Description = "销售发货通知")]
  19. [Route("api/S7SalesDeliveryNotice")]
  20. [NonUnify]
  21. public class SalesDeliveryNoticeService : IDynamicApiController, ITransient
  22. {
  23. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  24. private const long MainTenantId = 1300000000001L;
  25. private readonly ISqlSugarClient _db;
  26. private readonly UserManager _userManager;
  27. public SalesDeliveryNoticeService(ISqlSugarClient db, UserManager userManager)
  28. {
  29. _db = db;
  30. _userManager = userManager;
  31. }
  32. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  33. private long ResolveTenantOrThrow()
  34. {
  35. var tid = _userManager.TenantId;
  36. if (tid <= 0)
  37. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  38. if (_userManager.SuperAdmin && tid == MainTenantId)
  39. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  40. return tid;
  41. }
  42. /// <summary>排序字段白名单(前端 prop → 物理列),杜绝 orderBy 注入。</summary>
  43. private static readonly Dictionary<string, string> OrderByWhitelist = new(StringComparer.OrdinalIgnoreCase)
  44. {
  45. ["deliveryNo"] = "a.Id",
  46. ["salesOrderNo"] = "a.OrdNbr",
  47. ["dept"] = "a.Department",
  48. ["deliveryDate"] = "a.ShipDate",
  49. ["status"] = "a.Status",
  50. ["remark"] = "a.Remark",
  51. };
  52. /// <summary>
  53. /// 销售发货通知列表(只读分页查询)。
  54. /// </summary>
  55. [DisplayName("销售发货通知列表")]
  56. [HttpGet("list")]
  57. public async Task<object> GetList([FromQuery] SalesDeliveryNoticeListInput input)
  58. {
  59. var page = input.Page <= 0 ? 1 : input.Page;
  60. var pageSize = input.PageSize <= 0 ? 10 : (input.PageSize > 200 ? 200 : input.PageSize);
  61. var offset = (page - 1) * pageSize;
  62. // 强制租户过滤:租户只来自认证后的可信上下文(严格 JWT),忽略前端传入的 input.TenantId,杜绝跨租户可见。
  63. var tid = ResolveTenantOrThrow();
  64. var where = new List<string> { "a.ShType = 'SH'", "a.Typed <> 'S'", "a.IsActive = 1" };
  65. var pars = new List<SugarParameter>();
  66. where.Add("a.tenant_id = @TenantId");
  67. pars.Add(new SugarParameter("@TenantId", tid));
  68. if (!string.IsNullOrWhiteSpace(input.Id))
  69. {
  70. where.Add("a.Id LIKE @Id");
  71. pars.Add(new SugarParameter("@Id", $"%{input.Id.Trim()}%"));
  72. }
  73. if (!string.IsNullOrWhiteSpace(input.OrdNbr))
  74. {
  75. where.Add("a.OrdNbr LIKE @OrdNbr");
  76. pars.Add(new SugarParameter("@OrdNbr", $"%{input.OrdNbr.Trim()}%"));
  77. }
  78. if (!string.IsNullOrWhiteSpace(input.Department))
  79. {
  80. where.Add("(a.Department LIKE @Department OR b.Descr LIKE @Department)");
  81. pars.Add(new SugarParameter("@Department", $"%{input.Department.Trim()}%"));
  82. }
  83. if (!string.IsNullOrWhiteSpace(input.ShipDateFrom))
  84. {
  85. where.Add("a.ShipDate >= @ShipDateFrom");
  86. pars.Add(new SugarParameter("@ShipDateFrom", $"{input.ShipDateFrom.Trim()} 00:00:00"));
  87. }
  88. if (!string.IsNullOrWhiteSpace(input.ShipDateTo))
  89. {
  90. where.Add("a.ShipDate <= @ShipDateTo");
  91. pars.Add(new SugarParameter("@ShipDateTo", $"{input.ShipDateTo.Trim()} 23:59:59"));
  92. }
  93. var whereSql = string.Join(" AND ", where);
  94. // 排序:白名单回落,方向 asc/desc 回落 desc;默认 ShipDate DESC, RecID DESC。
  95. string orderSql;
  96. if (!string.IsNullOrWhiteSpace(input.OrderBy) && OrderByWhitelist.TryGetValue(input.OrderBy.Trim(), out var col))
  97. {
  98. var dir = string.Equals(input.OrderDir?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
  99. orderSql = $"{col} {dir}, a.RecID DESC";
  100. }
  101. else
  102. {
  103. orderSql = "a.ShipDate DESC, a.RecID DESC";
  104. }
  105. var total = await _db.Ado.GetIntAsync(
  106. $"SELECT COUNT(1) FROM ASNBOLShipperMaster a LEFT JOIN DepartmentMaster b ON a.Department = b.Department AND a.Domain = b.Domain WHERE {whereSql}",
  107. pars);
  108. var list = await _db.Ado.SqlQueryAsync<SalesDeliveryNoticeListRow>(
  109. $"""
  110. SELECT
  111. a.RecID AS RecId,
  112. a.Id AS DeliveryNo,
  113. a.OrdNbr AS SalesOrderNo,
  114. TRIM(CONCAT(IFNULL(a.Department, ''), ' ', IFNULL(b.Descr, ''))) AS Dept,
  115. a.ShipDate AS DeliveryDate,
  116. a.Status AS Status,
  117. a.Remark AS Remark,
  118. a.SoldTo AS SoldTo,
  119. a.Domain AS Domain,
  120. a.IsConfirm AS IsConfirm,
  121. a.tenant_id AS TenantId
  122. FROM ASNBOLShipperMaster a
  123. LEFT JOIN DepartmentMaster b ON a.Department = b.Department AND a.Domain = b.Domain
  124. WHERE {whereSql}
  125. ORDER BY {orderSql}
  126. LIMIT {pageSize} OFFSET {offset}
  127. """,
  128. pars);
  129. return new { total, page, pageSize, list };
  130. }
  131. }