using Admin.NET.Plugin.AiDOP.Manufacturing.Dto; namespace Admin.NET.Plugin.AiDOP.Manufacturing; /// /// S6 生产指令单 只读消费服务(Phase 1A)。 /// /// 数据源:数据中台标准层 mdp_std_work_order_schedule(S2/Production 从工单主表贴源 /// → mdp_stg_schedule → 标准化而来;specification 由 S2 转换层从物料主数据 enrich)。 /// /// 只读铁律:本服务只 SELECT mdp_std_*,绝不直连源库业务表(工单主表 / 物料主表 / MES 工单表),绝不回退源库、绝不写库。 /// /// 租户隔离(P0):租户只来自认证后 JWT(AidopTenantScope.ResolveOrThrow),忽略前端传入的任何 tenantId; /// COUNT / PAGE / DETAIL 三处 SQL 都显式带 tenant_id = @TenantId(mdp_std 实体非 ITenantIdFilter,不依赖框架自动过滤)。 /// 工厂隔离:当前 S6 体系只可靠解析租户、无稳定工厂作用域 → FACTORY_SCOPE = PARTIAL(tenant-only,不硬编码 factory_id)。 /// [ApiDescriptionSettings(Order = 305, Description = "生产指令单")] [Route("api/S6ProductionInstruction")] [NonUnify] public class S6ProductionInstructionService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly UserManager _userManager; public S6ProductionInstructionService(ISqlSugarClient db, UserManager userManager) { _db = db; _userManager = userManager; } /// 严格可信租户解析,见 private long ResolveTenantOrThrow() => Infrastructure.AidopTenantScope.ResolveOrThrow(_userManager); private const string SelectColumns = """ m.id AS Id, m.work_order AS WorkOrderNo, m.lot_serial AS LotSerial, m.item_code AS ItemCode, m.item_name AS ItemName, m.specification AS Specification, m.drawing_no AS DrawingNo, m.project AS Project, m.qty_ordered AS OrderQty, m.qty_completed AS CompletedQty, m.order_date AS OrderDate, m.due_date AS DueDate, m.status AS Status, m.work_order_type AS WorkOrderType """; /// /// 生产指令单列表(只读真分页)。 /// [DisplayName("生产指令单列表")] [HttpGet("page")] public async Task GetPage([FromQuery] S6ProductionInstructionInput 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(); // 强制租户过滤:mdp_std_work_order_schedule 非 ITenantIdFilter,隔离全靠显式 SQL 谓词。 var tid = ResolveTenantOrThrow(); where.Add("m.tenant_id = @TenantId"); pars.Add(new SugarParameter("@TenantId", tid)); if (!string.IsNullOrWhiteSpace(input.WorkOrderNo)) { where.Add("m.work_order LIKE @WorkOrderNo"); pars.Add(new SugarParameter("@WorkOrderNo", $"%{input.WorkOrderNo.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.ItemCode)) { where.Add("m.item_code LIKE @ItemCode"); pars.Add(new SugarParameter("@ItemCode", $"%{input.ItemCode.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Specification)) { where.Add("m.specification LIKE @Specification"); pars.Add(new SugarParameter("@Specification", $"%{input.Specification.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Status)) { where.Add("m.status = @Status"); pars.Add(new SugarParameter("@Status", input.Status.Trim())); } if (!string.IsNullOrWhiteSpace(input.OrderDateStart)) { where.Add("m.order_date >= @OrderDateStart"); pars.Add(new SugarParameter("@OrderDateStart", input.OrderDateStart.Trim())); } if (!string.IsNullOrWhiteSpace(input.OrderDateEnd)) { // 含止日当天:order_date < 止日 + 1 天 where.Add("m.order_date < DATE_ADD(@OrderDateEnd, INTERVAL 1 DAY)"); pars.Add(new SugarParameter("@OrderDateEnd", input.OrderDateEnd.Trim())); } var whereSql = string.Join(" AND ", where); var total = await _db.Ado.GetIntAsync( $"SELECT COUNT(1) FROM mdp_std_work_order_schedule m WHERE {whereSql}", pars); var list = total == 0 ? new List() : await _db.Ado.SqlQueryAsync( $""" SELECT {SelectColumns} FROM mdp_std_work_order_schedule m WHERE {whereSql} ORDER BY m.order_date DESC, m.work_order DESC LIMIT {pageSize} OFFSET {offset} """, pars); return new { total, page, pageSize, list }; } /// /// 生产指令单详情(只读,按工单编号)。查不到 / 跨租户 → 返回 null(不泄漏“无权限”信息)。 /// [DisplayName("生产指令单详情")] [HttpGet("detail")] public async Task GetDetail([FromQuery] string workOrderNo) { if (string.IsNullOrWhiteSpace(workOrderNo)) return null; var tid = ResolveTenantOrThrow(); var pars = new List { new("@TenantId", tid), new("@WorkOrderNo", workOrderNo.Trim()) }; return await _db.Ado.SqlQuerySingleAsync( $""" SELECT {SelectColumns} FROM mdp_std_work_order_schedule m WHERE m.tenant_id = @TenantId AND m.work_order = @WorkOrderNo LIMIT 1 """, pars); } }