using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse; /// /// S5 生产入库单 只读 list/detail 服务。 /// /// 数据源:DOP 数据中台标准层 mdp_std_production_receipt(扁平,一行=一条 NbrDetail 明细)。 /// 由 ProductionReceiptMdpSyncService 从 aidopdev.NbrMaster/NbrDetail Type='WOI' 同步标准化而来。 /// /// Route 用 api/S5ProductionReceipt(S5 前缀),规避 S7 成品仓储未来同名 ProductionReceipt 服务撞名。 /// 本服务仅 SELECT:无新增/编辑/删除/入库确认/库存事务/状态流转。 /// 当前上游未产 WOI 单据,标准层为空,list 返回空数组、detail 返回空数组(前端空态)。 /// [ApiDescriptionSettings(Order = 307, Description = "生产入库单")] [Route("api/S5ProductionReceipt")] [NonUnify] public class ProductionReceiptService : IDynamicApiController, ITransient { /// 主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。 private const long MainTenantId = 1300000000001L; private readonly ISqlSugarClient _db; private readonly UserManager _userManager; public ProductionReceiptService(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] ProductionReceiptListInput 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.Nbr)) { where.Add("m.nbr LIKE @Nbr"); pars.Add(new SugarParameter("@Nbr", $"%{input.Nbr.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.WorkOrd)) { where.Add("m.work_ord LIKE @WorkOrd"); pars.Add(new SugarParameter("@WorkOrd", $"%{input.WorkOrd.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.ErpWorkOrd)) { where.Add("m.erp_work_ord LIKE @ErpWorkOrd"); pars.Add(new SugarParameter("@ErpWorkOrd", $"%{input.ErpWorkOrd.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.LotSerial)) { where.Add("m.lot_serial LIKE @LotSerial"); pars.Add(new SugarParameter("@LotSerial", $"%{input.LotSerial.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.DateFrom)) { where.Add("m.receipt_date >= @DateFrom"); pars.Add(new SugarParameter("@DateFrom", $"{input.DateFrom.Trim()} 00:00:00")); } if (!string.IsNullOrWhiteSpace(input.DateTo)) { where.Add("m.receipt_date <= @DateTo"); pars.Add(new SugarParameter("@DateTo", $"{input.DateTo.Trim()} 23:59:59")); } var whereSql = string.Join(" AND ", where); var total = await _db.Ado.GetIntAsync( $"SELECT COUNT(1) FROM mdp_std_production_receipt m WHERE {whereSql}", pars); var list = await _db.Ado.SqlQueryAsync( $""" {SelectColumns()} FROM mdp_std_production_receipt m WHERE {whereSql} ORDER BY {BuildOrderBy(input.OrderBy, input.OrderDir)} LIMIT {pageSize} OFFSET {offset} """, pars); return new { total, page, pageSize, list }; } /// /// 生产入库单详情(只读:同一入库单号的扁平明细行,按 line 排序)。查不到返回空数组。 /// [DisplayName("生产入库单详情")] [HttpGet("detail")] public async Task> GetDetail([FromQuery] string nbr) { if (string.IsNullOrWhiteSpace(nbr)) return new List(); // 强制租户过滤:明细行必须归属可信 JWT 租户,忽略前端 tenantId。 var tid = ResolveTenantOrThrow(); var pars = new List { new("@Nbr", nbr.Trim()), new("@TenantId", tid) }; var where = "m.nbr = @Nbr AND m.tenant_id = @TenantId"; return await _db.Ado.SqlQueryAsync( $""" {SelectColumns()} FROM mdp_std_production_receipt m WHERE {where} ORDER BY m.line ASC, m.id ASC """, pars); } /// 统一 SELECT 列(list/detail 共用;扁平行)。 private static string SelectColumns() => """ SELECT m.id AS Id, m.nbr AS Nbr, m.line AS Line, m.receipt_date AS ReceiptDate, m.status AS Status, m.status_desc AS StatusDesc, m.remark AS Remark, m.prod_line AS ProdLine, m.work_ord AS WorkOrd, m.department_desc AS DepartmentDesc, m.erp_work_ord AS ErpWorkOrd, m.item_num AS ItemNum, m.item_name AS ItemName, m.item_spec AS ItemSpec, m.location_to AS LocationTo, m.location_to_desc AS LocationToDesc, m.lot_serial AS LotSerial, m.qty_rec AS QtyRec, m.um AS Um, m.ord_nbr AS OrdNbr, m.applicant_name AS ApplicantName """; /// /// 排序白名单:仅允许按已展示列排序,杜绝 SQL 注入;非法字段回落默认。 /// private static string BuildOrderBy(string? orderBy, string? orderDir) { var column = orderBy switch { "nbr" => "m.nbr", "receiptDate" => "m.receipt_date", "status" => "m.status", "workOrd" => "m.work_ord", "erpWorkOrd" => "m.erp_work_ord", "lotSerial" => "m.lot_serial", "itemNum" => "m.item_num", "createTime" => "m.create_time", _ => "m.receipt_date", }; var direction = string.Equals(orderDir, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC"; return $"{column} {direction}, m.id DESC"; } }