using System.ComponentModel; using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse; /// /// S7 FQC 成品检验任务(报检)操作:认领 / 检验员调配 / 优先级调整 / 生成检验单(带认领门)。 /// 对齐 S5 IQC 任务列表范式(IqcTaskEntryService),但 FQC 报检 qms_fqcbj 是头级单(无明细分录), /// 认领/优先级/负责人直接落 qms_fqcbj(jyfzr/yxj/FINSPECTSTATUS/jykssj)。DDL-free,字段均已存在。 /// 只写 qms_fqcbj 的任务归属/优先级/进度;检验单生成复用 FqcApplyService.GenerateInspection(检规快照逻辑不重复)。 /// [ApiDescriptionSettings(Order = 344, Description = "FQC检验任务操作")] [Route("api/S7FqcTaskEntry")] [NonUnify] public class FqcTaskEntryService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly UserManager _userManager; private readonly FqcApplyService _fqcApply; public FqcTaskEntryService(ISqlSugarClient db, UserManager userManager, FqcApplyService fqcApply) { _db = db; _userManager = userManager; _fqcApply = fqcApply; } private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager); /// 待检任务列表:活源 qms_fqcbj + LEFT JOIN 检验单(qms_qcpp_inspbill by lydjbh+hid) 拿 inspBillId。 [DisplayName("FQC检验任务列表(报检)")] [HttpGet("list")] public async Task GetList([FromQuery] FqcTaskEntryListInput input) { var page = input.Page <= 0 ? 1 : input.Page; var pageSize = input.PageSize <= 0 ? 20 : (input.PageSize > 200 ? 200 : input.PageSize); var offset = (page - 1) * pageSize; var tenantId = ResolveTenantOrThrow(); var where = new List { "q.tenant_id = @TenantId" }; var pars = new List { new("@TenantId", tenantId) }; if (!string.IsNullOrWhiteSpace(input.ProductionOrderNo)) { where.Add("q.sczld LIKE @ProductionOrderNo"); pars.Add(new SugarParameter("@ProductionOrderNo", $"%{input.ProductionOrderNo.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.ProductionBatchNo)) { where.Add("q.scph LIKE @ProductionBatchNo"); pars.Add(new SugarParameter("@ProductionBatchNo", $"%{input.ProductionBatchNo.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.MaterialCode)) { where.Add("q.wlbm LIKE @MaterialCode"); pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.InspectProgress)) { where.Add("q.FINSPECTSTATUS = @InspectProgress"); pars.Add(new SugarParameter("@InspectProgress", input.InspectProgress.Trim())); } if (!string.IsNullOrWhiteSpace(input.Priority)) { where.Add("q.yxj LIKE @Priority"); pars.Add(new SugarParameter("@Priority", $"%{input.Priority.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Inspector)) { where.Add("q.jyfzr LIKE @Inspector"); pars.Add(new SugarParameter("@Inspector", $"%{input.Inspector.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.ApplyTimeStart)) { where.Add("q.FAPPLYTIME >= @ApplyTimeStart"); pars.Add(new SugarParameter("@ApplyTimeStart", input.ApplyTimeStart.Trim() + " 00:00:00")); } if (!string.IsNullOrWhiteSpace(input.ApplyTimeEnd)) { where.Add("q.FAPPLYTIME <= @ApplyTimeEnd"); pars.Add(new SugarParameter("@ApplyTimeEnd", input.ApplyTimeEnd.Trim() + " 23:59:59")); } var whereSql = string.Join(" AND ", where); var total = await _db.Ado.GetIntAsync( $"SELECT COUNT(1) FROM qms_fqcbj q WHERE {whereSql}", pars); // yxj 优先级排序:数值型转 SIGNED 升序(空/非数值排最后),再按 id 倒序 var list = await _db.Ado.SqlQueryAsync( $""" SELECT q.id AS Id, q.FBILLNO AS ApplyBillNo, q.sczld AS ProductionOrderNo, q.FAPPLYTIME AS ApplyTime, q.wlbm AS MaterialCode, q.cpmc AS ProductName, q.cpxh AS ProductModel, q.scph AS ProductionBatchNo, q.sczldsl AS OrderQty, q.sl AS ApplyQty, q.FAPPLYUSER AS Applicant, q.FCOMMENT AS Remark, q.yxj AS Priority, q.jyfzr AS OwnerId, CAST(NULL AS CHAR) AS OwnerName, q.jykssj AS InspectStartTime, q.jywcsj AS InspectFinishTime, q.FINSPECTSTATUS AS InspectProgress, b.id AS InspBillId, b.FBILLNO AS InspBillNo FROM qms_fqcbj q LEFT JOIN qms_qcpp_inspbill b ON b.lydjbh = q.FBILLNO AND b.hid = q.id AND b.tenant_id = q.tenant_id WHERE {whereSql} ORDER BY (q.yxj = '紧急') DESC, (q.yxj = '正常') DESC, q.id DESC LIMIT {pageSize} OFFSET {offset} """, pars); await FillOwnerNamesAsync(list); return new { total, page, pageSize, list }; } private async Task FillOwnerNamesAsync(List list) { var ids = list .Select(x => x.OwnerId) .Where(x => !string.IsNullOrWhiteSpace(x) && long.TryParse(x, out _)) .Select(x => long.Parse(x!)) .Distinct() .ToList(); if (ids.Count == 0) return; var users = await _db.Queryable() .ClearFilter() .Where(u => ids.Contains(u.Id)) .Select(u => new { u.Id, u.RealName, u.Account }) .ToListAsync(); var map = users.ToDictionary(u => u.Id.ToString(), u => u.RealName ?? u.Account ?? u.Id.ToString()); foreach (var row in list) { if (!string.IsNullOrWhiteSpace(row.OwnerId) && map.TryGetValue(row.OwnerId!, out var name)) row.OwnerName = name; } } /// 认领:把选中报检的检验负责人 jyfzr 置为当前用户。 [DisplayName("认领检验任务")] [HttpPost("claim")] public async Task Claim([FromBody] FqcTaskEntryIdsInput input) { var ids = NormalizeIds(input?.Ids); var tenantId = ResolveTenantOrThrow(); var owner = _userManager.UserId.ToString(); var n = await _db.Ado.ExecuteCommandAsync( $"UPDATE qms_fqcbj SET jyfzr = @Owner WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})", new SugarParameter("@Owner", owner), new SugarParameter("@TenantId", tenantId)); return new { updated = n, ownerId = owner }; } /// 检验员调配:把选中报检的检验负责人 jyfzr 置为指定用户。 [DisplayName("检验员调配")] [HttpPost("assign")] public async Task Assign([FromBody] FqcTaskEntryAssignInput input) { var ids = NormalizeIds(input?.Ids); if (input == null || input.InspectorUserId <= 0) throw Oops.Oh("请指定检验负责人"); var tenantId = ResolveTenantOrThrow(); var owner = input.InspectorUserId.ToString(); var n = await _db.Ado.ExecuteCommandAsync( $"UPDATE qms_fqcbj SET jyfzr = @Owner WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})", new SugarParameter("@Owner", owner), new SugarParameter("@TenantId", tenantId)); return new { updated = n, ownerId = owner }; } /// 优先级调整:写 qms_fqcbj.yxj(与报检检验优先级同域:正常/紧急)。 [DisplayName("优先级调整")] [HttpPost("priority")] public async Task AdjustPriority([FromBody] FqcTaskEntryPriorityInput input) { var ids = NormalizeIds(input?.Ids); var priority = input?.Priority?.Trim(); if (priority != "正常" && priority != "紧急") throw Oops.Oh("优先级仅支持 正常/紧急"); var tenantId = ResolveTenantOrThrow(); var n = await _db.Ado.ExecuteCommandAsync( $"UPDATE qms_fqcbj SET yxj = @Priority WHERE tenant_id = @TenantId AND id IN ({string.Join(",", ids)})", new SugarParameter("@Priority", priority), new SugarParameter("@TenantId", tenantId)); return new { updated = n }; } /// /// 生成检验单(带认领门):校验当前用户为认领人 → 复用 FqcApplyService.GenerateInspection 生成(检规快照) /// → 置报检 FINSPECTSTATUS=检验中 + jykssj → 回填 inspBillId/inspBillNo。非认领人拒绝。 /// [DisplayName("生成检验单(认领后)")] [HttpPost("generate-inspbill")] public async Task GenerateInspBill([FromBody] FqcTaskGenerateInput input) { if (input == null || input.EntryId <= 0) throw Oops.Oh("报检 id 非法"); var tenantId = ResolveTenantOrThrow(); var userId = _userManager.UserId.ToString(); var apply = await _db.Ado.SqlQuerySingleAsync( "SELECT id AS Id, FBILLNO AS BillNo, jyfzr AS OwnerId FROM qms_fqcbj WHERE id=@Id AND tenant_id=@t LIMIT 1", new SugarParameter("@Id", input.EntryId), new SugarParameter("@t", tenantId)); if (apply == null) throw Oops.Oh("报检不存在或不属于当前租户"); if (string.IsNullOrWhiteSpace(apply.OwnerId) || !string.Equals(apply.OwnerId!.Trim(), userId, StringComparison.Ordinal)) throw Oops.Oh("尚未认领或非认领人操作!"); // 复用现有生成逻辑(检规解析 + 12 项快照 + 幂等 UNIQUE),不重复实现 await _fqcApply.GenerateInspection(new FqcGenerateInput { ApplyId = input.EntryId }); // 生成后置报检为检验中 + 开始时间(IFNULL 保留既有) await _db.Ado.ExecuteCommandAsync( "UPDATE qms_fqcbj SET FINSPECTSTATUS='检验中', jykssj=IFNULL(jykssj,@now) WHERE id=@Id AND tenant_id=@t", new SugarParameter("@now", DateTime.Now), new SugarParameter("@Id", input.EntryId), new SugarParameter("@t", tenantId)); // 回读检验单 id/号(幂等:已存在也返回) var bill = await _db.Ado.SqlQuerySingleAsync( "SELECT id AS Id, FBILLNO AS BillNo FROM qms_qcpp_inspbill WHERE lydjbh=@no AND hid=@Id AND tenant_id=@t LIMIT 1", new SugarParameter("@no", apply.BillNo), new SugarParameter("@Id", input.EntryId), new SugarParameter("@t", tenantId)); if (bill == null) throw Oops.Oh("检验单生成后未能回读,请刷新重试"); return new FqcTaskGenerateOutput { InspBillId = bill.Id, InspBillNo = bill.BillNo ?? "" }; } private static List NormalizeIds(List? ids) { var list = (ids ?? new List()).Where(x => x > 0).Distinct().ToList(); if (list.Count == 0) throw Oops.Oh("请至少选择一条任务"); if (list.Count > 200) throw Oops.Oh("单次最多操作 200 条"); return list; } private sealed class FqcTaskApplyRow { public long Id { get; set; } public string? BillNo { get; set; } public string? OwnerId { get; set; } } private sealed class FqcTaskBillRow { public long Id { get; set; } public string? BillNo { get; set; } } } public class FqcTaskEntryListInput { public string? ProductionOrderNo { get; set; } public string? ProductionBatchNo { get; set; } public string? MaterialCode { get; set; } public string? InspectProgress { get; set; } public string? Priority { get; set; } public string? Inspector { get; set; } public string? ApplyTimeStart { get; set; } public string? ApplyTimeEnd { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 20; } public class FqcTaskEntryRow { public long Id { get; set; } public string? ApplyBillNo { get; set; } public string? ProductionOrderNo { get; set; } public string? ApplyTime { get; set; } public string? MaterialCode { get; set; } public string? ProductName { get; set; } public string? ProductModel { get; set; } public string? ProductionBatchNo { get; set; } public decimal? OrderQty { get; set; } public decimal? ApplyQty { get; set; } public string? Applicant { get; set; } public string? Remark { get; set; } public string? Priority { get; set; } public string? OwnerId { get; set; } public string? OwnerName { get; set; } public string? InspectStartTime { get; set; } public string? InspectFinishTime { get; set; } public string? InspectProgress { get; set; } public long? InspBillId { get; set; } public string? InspBillNo { get; set; } } public class FqcTaskEntryIdsInput { public List? Ids { get; set; } } public class FqcTaskEntryAssignInput { public List? Ids { get; set; } public long InspectorUserId { get; set; } } public class FqcTaskEntryPriorityInput { public List? Ids { get; set; } /// 优先级:正常 / 紧急(与报检检验优先级同域) public string? Priority { get; set; } } public class FqcTaskGenerateInput { public long EntryId { get; set; } } public class FqcTaskGenerateOutput { public long InspBillId { get; set; } public string InspBillNo { get; set; } = ""; }