| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- using System.ComponentModel;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Admin.NET.Plugin.ApprovalFlow;
- using Admin.NET.Plugin.ApprovalFlow.Service;
- namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
- /// <summary>
- /// S7 FQC 成品检验任务(报检)操作:认领 / 检验员调配 / 优先级调整 / 生成检验单(带认领门)。
- /// 对齐 S5 IQC 任务列表范式(IqcTaskEntryService),但 FQC 报检 qms_fqcbj 是头级单(无明细分录),
- /// 认领/优先级/负责人直接落 qms_fqcbj(jyfzr/yxj/FINSPECTSTATUS/jykssj)。DDL-free,字段均已存在。
- /// 只写 qms_fqcbj 的任务归属/优先级/进度;检验单生成复用 FqcApplyService.GenerateInspection(检规快照逻辑不重复)。
- /// </summary>
- [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;
- private readonly FlowEngineService _flowEngine;
- public FqcTaskEntryService(ISqlSugarClient db, UserManager userManager, FqcApplyService fqcApply, FlowEngineService flowEngine)
- {
- _db = db;
- _userManager = userManager;
- _fqcApply = fqcApply;
- _flowEngine = flowEngine;
- }
- private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager);
- /// <summary>待检任务列表:活源 qms_fqcbj + LEFT JOIN 检验单(qms_qcpp_inspbill by lydjbh+hid) 拿 inspBillId。</summary>
- [DisplayName("FQC检验任务列表(报检)")]
- [HttpGet("list")]
- public async Task<object> 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<string> { "q.tenant_id = @TenantId" };
- var pars = new List<SugarParameter> { 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<FqcTaskEntryRow>(
- $"""
- 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<FqcTaskEntryRow> 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<SysUser>()
- .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;
- }
- }
- /// <summary>认领:把选中报检的检验负责人 jyfzr 置为当前用户。</summary>
- [DisplayName("认领检验任务")]
- [HttpPost("claim")]
- public async Task<object> Claim([FromBody] FqcTaskEntryIdsInput input)
- {
- var ids = NormalizeIds(input?.Ids);
- var tenantId = ResolveTenantOrThrow();
- await EnsureNotLockedAsync(ids, tenantId); // G4:起流后冻结负责人
- 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 };
- }
- /// <summary>检验员调配:把选中报检的检验负责人 jyfzr 置为指定用户。</summary>
- [DisplayName("检验员调配")]
- [HttpPost("assign")]
- public async Task<object> Assign([FromBody] FqcTaskEntryAssignInput input)
- {
- var ids = NormalizeIds(input?.Ids);
- if (input == null || input.InspectorUserId <= 0) throw Oops.Oh("请指定检验负责人");
- var tenantId = ResolveTenantOrThrow();
- await EnsureNotLockedAsync(ids, tenantId); // G4:起流后冻结负责人
- 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 };
- }
- /// <summary>优先级调整:写 qms_fqcbj.yxj(与报检检验优先级同域:正常/紧急)。</summary>
- [DisplayName("优先级调整")]
- [HttpPost("priority")]
- public async Task<object> 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 };
- }
- /// <summary>
- /// 生成检验单(带认领门):校验当前用户为认领人 → 复用 FqcApplyService.GenerateInspection 生成(检规快照)
- /// → 置报检 FINSPECTSTATUS=检验中 + jykssj → 回填 inspBillId/inspBillNo。非认领人拒绝。
- /// </summary>
- [DisplayName("生成检验单(认领后)")]
- [HttpPost("generate-inspbill")]
- public async Task<FqcTaskGenerateOutput> 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<FqcTaskApplyRow>(
- "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<FqcTaskBillRow>(
- "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("检验单生成后未能回读,请刷新重试");
- // EnsureFlow:建单后幂等起流(无实例则起、有则复用)。检验单已 DB 幂等,Generate 可安全重调;
- // 即便"建单成功、起流失败",重调 Generate 会走到这里再次补流 → 无永久孤儿单。起流失败向上抛,暴露真实原因。
- await EnsureFlowAsync(bill.Id, bill.BillNo);
- return new FqcTaskGenerateOutput { InspBillId = bill.Id, InspBillNo = bill.BillNo ?? "" };
- }
- /// <summary>
- /// 幂等起流:BizType+BizId 已有实例则复用,无则 StartFlow。发起人=当前用户(认领门保证==jyfzr),
- /// N1(ApproverType=Initiator) 待办自然落发起人=认领人。幂等等级 B(代码级 check-then-start),
- /// 与 submit-result 的 inst?.Id ?? StartFlow 同范式;ApprovalFlow 核心(BizType,BizId)无 DB 唯一约束,
- /// 极端并发 race 仅登记风险(本批不改核心表)。
- /// </summary>
- private async Task EnsureFlowAsync(long inspBillId, string? billNo)
- {
- var hasInstance = await _db.Queryable<ApprovalFlowInstance>()
- .ClearFilter()
- .Where(x => x.BizType == FqcInspBillFlowConst.BizType && x.BizId == inspBillId)
- .AnyAsync();
- if (hasInstance) return; // 复用已有实例,不重复起流
- // StartFlow 引擎内非原子(Instance Insert 在前、N1 Task Insert 在后,无内部事务)→ 裸调可能留下
- // "实例有、N1 无"的半启动、永久卡死。用事务包裹(镜像 FqcInspBillFlowService.SubmitResult 的既有范式),
- // 保证"实例 + N1 任务"要么全成、要么全回滚:N1 建失败 → 实例回滚 → 退化为可恢复 Scenario B(Bill 有 Flow 无),
- // 由下一次 Generate 经本方法补起。失败明确上抛(不吞错、不在本次请求重试 StartFlow)。
- var tran = await _db.AsTenant().UseTranAsync(async () =>
- {
- await _flowEngine.StartFlow(new StartFlowInput
- {
- BizType = FqcInspBillFlowConst.BizType,
- BizId = inspBillId,
- BizNo = billNo,
- Title = $"FQC检验单 {billNo}",
- });
- });
- if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("FQC 检验单工作流启动失败");
- }
- private static List<long> NormalizeIds(List<long>? ids)
- {
- var list = (ids ?? new List<long>()).Where(x => x > 0).Distinct().ToList();
- if (list.Count == 0) throw Oops.Oh("请至少选择一条任务");
- if (list.Count > 200) throw Oops.Oh("单次最多操作 200 条");
- return list;
- }
- /// <summary>
- /// G4 负责人冻结:选中报检若有任一已生成检验单(=已起流),禁止再改 jyfzr(Claim/Assign),
- /// 防止 jyfzr 与 N1 待办 Assignee 分叉产生孤儿。以检验单存在性判定(生成即起流,二者等价)。
- /// </summary>
- private async Task EnsureNotLockedAsync(List<long> ids, long tenantId)
- {
- var locked = await _db.Ado.GetIntAsync(
- $@"SELECT COUNT(1) FROM qms_qcpp_inspbill b
- JOIN qms_fqcbj q ON q.FBILLNO=b.lydjbh AND q.id=b.hid AND q.tenant_id=b.tenant_id
- WHERE q.tenant_id=@t AND q.id IN ({string.Join(",", ids)})",
- new SugarParameter("@t", tenantId));
- if (locked > 0) throw Oops.Oh("检验单已生成,检验负责人已锁定,不允许重新认领或调配。");
- }
- 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<long>? Ids { get; set; }
- }
- public class FqcTaskEntryAssignInput
- {
- public List<long>? Ids { get; set; }
- public long InspectorUserId { get; set; }
- }
- public class FqcTaskEntryPriorityInput
- {
- public List<long>? Ids { get; set; }
- /// <summary>优先级:正常 / 紧急(与报检检验优先级同域)</summary>
- 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; } = "";
- }
|