using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse; /// /// S5 采购收货单 只读 list 服务。 /// /// 数据源:DOP 数据中台标准层 mdp_std_purchase_receipt(明细粒度 domain#receiver#line)。 /// 由 PurchaseReceiptMdpSyncService 从 aidopdev.PurOrdRctDetail/PurOrdRctMaster RctType='rc' 同步标准化而来。 /// /// 本服务仅 SELECT:无新增/编辑/删除/收货确认/入库写入/库存事务/状态流转;无 detail(扁平列表)。 /// [ApiDescriptionSettings(Order = 305, Description = "采购收货单")] [Route("api/PurchaseReceipt")] [NonUnify] public class PurchaseReceiptService : IDynamicApiController, ITransient { /// 主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。 private const long MainTenantId = 1300000000001L; private readonly ISqlSugarClient _db; private readonly UserManager _userManager; public PurchaseReceiptService(ISqlSugarClient db, UserManager userManager) { _db = db; _userManager = userManager; } /// 严格可信租户解析:只来自认证后 JWT;无 Token(<=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。 private long ResolveTenantOrThrow() { var tid = _userManager.TenantId; if (tid <= 0) throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户"); if (_userManager.SuperAdmin && tid == MainTenantId) throw Oops.Oh("超级管理员操作前必须选择目标租户"); return tid; } /// /// 采购收货单列表(只读分页查询)。 /// [DisplayName("采购收货单列表")] [HttpGet("list")] public async Task GetList([FromQuery] PurchaseReceiptListInput input) { var page = input.Page <= 0 ? 1 : input.Page; var pageSize = input.PageSize <= 0 ? 10 : input.PageSize; var offset = (page - 1) * pageSize; var where = new List { "1=1" }; var pars = new List(); // 强制租户过滤:租户只来自认证后可信 JWT,忽略前端 input.TenantId,杜绝跨租户可见。 var tid = ResolveTenantOrThrow(); where.Add("m.tenant_id = @TenantId"); pars.Add(new SugarParameter("@TenantId", tid)); if (!string.IsNullOrWhiteSpace(input.Supp)) { where.Add("(m.supp LIKE @Supp OR m.sort_name LIKE @Supp)"); pars.Add(new SugarParameter("@Supp", $"%{input.Supp.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.ItemNum)) { where.Add("m.item_num LIKE @ItemNum"); pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.RctDateFrom)) { where.Add("m.rct_date >= @RctDateFrom"); pars.Add(new SugarParameter("@RctDateFrom", $"{input.RctDateFrom.Trim()} 00:00:00")); } if (!string.IsNullOrWhiteSpace(input.RctDateTo)) { where.Add("m.rct_date <= @RctDateTo"); pars.Add(new SugarParameter("@RctDateTo", $"{input.RctDateTo.Trim()} 23:59:59")); } if (!string.IsNullOrWhiteSpace(input.PurOrd)) { where.Add("m.pur_ord LIKE @PurOrd"); pars.Add(new SugarParameter("@PurOrd", $"%{input.PurOrd.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Receiver)) { where.Add("m.receiver LIKE @Receiver"); pars.Add(new SugarParameter("@Receiver", $"%{input.Receiver.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.SalesJob)) { where.Add("m.sales_job LIKE @SalesJob"); pars.Add(new SugarParameter("@SalesJob", $"%{input.SalesJob.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Req)) { where.Add("m.req LIKE @Req"); pars.Add(new SugarParameter("@Req", $"%{input.Req.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.DopReq)) { where.Add("m.dop_req LIKE @DopReq"); pars.Add(new SugarParameter("@DopReq", $"%{input.DopReq.Trim()}%")); } var whereSql = string.Join(" AND ", where); var total = await _db.Ado.GetIntAsync( $"SELECT COUNT(1) FROM mdp_std_purchase_receipt m WHERE {whereSql}", pars); var list = await _db.Ado.SqlQueryAsync( $""" SELECT m.id AS Id, m.receiver AS Receiver, m.line AS Line, m.rct_date AS RctDate, m.supp AS Supp, m.sort_name AS SortName, m.item_num AS ItemNum, m.item_name AS ItemName, m.item_spec AS ItemSpec, m.um AS Um, m.qty_ordered AS QtyOrdered, m.qty_received AS QtyReceived, m.lot_serial AS LotSerial, m.location AS Location, m.pur_ord AS PurOrd, m.sales_job AS SalesJob, m.address1 AS Address1, m.req AS Req, m.dop_req AS DopReq, m.ord_nbr AS OrdNbr FROM mdp_std_purchase_receipt m WHERE {whereSql} ORDER BY {BuildOrderBy(input.OrderBy, input.OrderDir)} LIMIT {pageSize} OFFSET {offset} """, pars); return new { total, page, pageSize, list }; } /// /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入。 /// private static string BuildOrderBy(string? orderBy, string? orderDir) { var column = orderBy switch { "receiver" => "m.receiver", "rctDate" => "m.rct_date", "itemNum" => "m.item_num", "sortName" => "m.sort_name", "qtyOrdered" => "m.qty_ordered", "qtyReceived" => "m.qty_received", "purOrd" => "m.pur_ord", "salesJob" => "m.sales_job", _ => "m.rct_date", }; var direction = string.Equals(orderDir, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC"; return $"{column} {direction}, m.id DESC"; } }