|
|
@@ -0,0 +1,778 @@
|
|
|
+# S8 复检闭环 Implementation Plan
|
|
|
+
|
|
|
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
+
|
|
|
+**Goal:** 补齐 S8 中"处理人提交已修复 → 检验人检验 → RESOLVED → 提交关闭申请"的完整业务闭环,引入 `PENDING_VERIFICATION` 中间状态,检验人由处理人提交复检时手选。
|
|
|
+
|
|
|
+**Architecture:** 新增 `PENDING_VERIFICATION` 状态和 `VerifierId` 等字段到主表;在 `S8TaskFlowService` 增加三个方法(submit-verification / approve-verification / reject-verification);前端详情页在 `v-else` 区域按角色+状态控制按钮显隐,检验动作与审批流无关,不受 `hasActiveFlow` 影响。
|
|
|
+
|
|
|
+**Tech Stack:** C# / SqlSugar(后端)、Vue 3 Composition API + Element Plus(前端)、PostgreSQL(DB 迁移用 SQL 文件,非 CodeFirst)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 上下文速查
|
|
|
+
|
|
|
+关键文件路径(执行时直接用绝对路径):
|
|
|
+
|
|
|
+| 文件 | 用途 |
|
|
|
+|---|---|
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8StatusRules.cs` | 状态机边集 |
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8Labels.cs` | 状态中文标签 + 选项列表 |
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/AdoS8Exception.cs` | 主表实体 |
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8TaskFlowService.cs` | 业务流转服务 |
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DictionaryService.cs` | 字典接口 |
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs` | 查询服务(pendingStatuses 在此) |
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs` | DTO 定义 |
|
|
|
+| `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8ExceptionsController.cs` | HTTP 控制器 |
|
|
|
+| `AiDOPWarehouse/Web/src/views/aidop/s8/api/s8ExceptionApi.ts` | 前端 API 封装 |
|
|
|
+| `AiDOPWarehouse/Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue` | 前端详情页 |
|
|
|
+
|
|
|
+**现有状态机边集(修改前):**
|
|
|
+```
|
|
|
+NEW->ASSIGNED, NEW->REJECTED
|
|
|
+ASSIGNED->IN_PROGRESS, ASSIGNED->ESCALATED, ASSIGNED->REJECTED
|
|
|
+IN_PROGRESS->RESOLVED, IN_PROGRESS->ESCALATED, IN_PROGRESS->REJECTED
|
|
|
+RESOLVED->CLOSED, RESOLVED->IN_PROGRESS
|
|
|
+ESCALATED->ASSIGNED, ESCALATED->IN_PROGRESS
|
|
|
+REJECTED->NEW
|
|
|
+```
|
|
|
+
|
|
|
+**注意:`IN_PROGRESS->RESOLVED` 边已存在**,但目前无后端方法能触发它。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 1: 数据库迁移脚本
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `AiDOPWarehouse/doc/migrations/2026-04-18_s8_verification_fields.sql`
|
|
|
+
|
|
|
+**Step 1: 编写迁移 SQL**
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 2026-04-18: S8 复检闭环 - 新增检验相关字段
|
|
|
+ALTER TABLE ado_s8_exception
|
|
|
+ ADD COLUMN IF NOT EXISTS verifier_id BIGINT NULL,
|
|
|
+ ADD COLUMN IF NOT EXISTS verification_assigned_at TIMESTAMPTZ NULL,
|
|
|
+ ADD COLUMN IF NOT EXISTS verified_at TIMESTAMPTZ NULL,
|
|
|
+ ADD COLUMN IF NOT EXISTS verification_result VARCHAR(32) NULL,
|
|
|
+ ADD COLUMN IF NOT EXISTS verification_remark VARCHAR(2000) NULL;
|
|
|
+
|
|
|
+COMMENT ON COLUMN ado_s8_exception.verifier_id IS '检验人ID(处理人提交复检时手选)';
|
|
|
+COMMENT ON COLUMN ado_s8_exception.verification_assigned_at IS '提交复检时间';
|
|
|
+COMMENT ON COLUMN ado_s8_exception.verified_at IS '检验完成时间';
|
|
|
+COMMENT ON COLUMN ado_s8_exception.verification_result IS 'APPROVED / REJECTED';
|
|
|
+COMMENT ON COLUMN ado_s8_exception.verification_remark IS '检验意见';
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: 在开发数据库执行该脚本**
|
|
|
+
|
|
|
+```bash
|
|
|
+psql -U <db_user> -d <db_name> -f AiDOPWarehouse/doc/migrations/2026-04-18_s8_verification_fields.sql
|
|
|
+```
|
|
|
+
|
|
|
+预期:无报错,`\d ado_s8_exception` 能看到 5 个新列。
|
|
|
+
|
|
|
+**Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/doc/migrations/2026-04-18_s8_verification_fields.sql
|
|
|
+git commit -m "chore: add s8 verification fields migration"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 2: 实体 + 状态机 + 标签
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/AdoS8Exception.cs`
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8StatusRules.cs`
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8Labels.cs`
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DictionaryService.cs`
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs`
|
|
|
+
|
|
|
+**Step 1: 在 `AdoS8Exception.cs` 末尾(`IsDeleted` 字段之前)新增 5 个字段**
|
|
|
+
|
|
|
+在 `SourcePayload` 属性后、类的最后一个 `}` 前插入:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ /// <summary>检验人ID(提交复检时手选)</summary>
|
|
|
+ [SugarColumn(ColumnName = "verifier_id", ColumnDataType = "bigint", IsNullable = true)]
|
|
|
+ public long? VerifierId { get; set; }
|
|
|
+
|
|
|
+ /// <summary>提交复检时间</summary>
|
|
|
+ [SugarColumn(ColumnName = "verification_assigned_at", IsNullable = true)]
|
|
|
+ public DateTime? VerificationAssignedAt { get; set; }
|
|
|
+
|
|
|
+ /// <summary>检验完成时间</summary>
|
|
|
+ [SugarColumn(ColumnName = "verified_at", IsNullable = true)]
|
|
|
+ public DateTime? VerifiedAt { get; set; }
|
|
|
+
|
|
|
+ /// <summary>检验结果(APPROVED / REJECTED)</summary>
|
|
|
+ [SugarColumn(ColumnName = "verification_result", Length = 32, IsNullable = true)]
|
|
|
+ public string? VerificationResult { get; set; }
|
|
|
+
|
|
|
+ /// <summary>检验意见</summary>
|
|
|
+ [SugarColumn(ColumnName = "verification_remark", Length = 2000, IsNullable = true)]
|
|
|
+ public string? VerificationRemark { get; set; }
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: 在 `S8StatusRules.cs` 的 `Edges` HashSet 中新增两条边**
|
|
|
+
|
|
|
+在 `("IN_PROGRESS", "RESOLVED"),` 这行**后面**插入:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ ("IN_PROGRESS", "PENDING_VERIFICATION"),
|
|
|
+ ("PENDING_VERIFICATION", "RESOLVED"),
|
|
|
+ ("PENDING_VERIFICATION", "IN_PROGRESS"),
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: 在 `S8Labels.cs` 的 `StatusLabel` 方法中新增分支**
|
|
|
+
|
|
|
+在 `"ESCALATED" => "已升级",` 后面插入:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ "PENDING_VERIFICATION" => "待检验",
|
|
|
+```
|
|
|
+
|
|
|
+同时在 `StatusOptions()` 方法的数组中新增 `"PENDING_VERIFICATION"`,放在 `"RESOLVED"` 之后:
|
|
|
+
|
|
|
+将:
|
|
|
+```csharp
|
|
|
+ public static object[] StatusOptions() =>
|
|
|
+ new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "RESOLVED", "CLOSED", "REJECTED", "ESCALATED" }
|
|
|
+```
|
|
|
+改为:
|
|
|
+```csharp
|
|
|
+ public static object[] StatusOptions() =>
|
|
|
+ new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION", "RESOLVED", "CLOSED", "REJECTED", "ESCALATED" }
|
|
|
+```
|
|
|
+
|
|
|
+**Step 4: 在 `S8DictionaryService.cs` 的 `S8_EXCEPTION_STATUS` case 中新增一项**
|
|
|
+
|
|
|
+在 `new { value = "RESOLVED", label = "已处理" },` 后面插入:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ new { value = "PENDING_VERIFICATION", label = "待检验" },
|
|
|
+```
|
|
|
+
|
|
|
+**Step 5: 在 `S8ExceptionService.cs` 的 `pendingStatuses` 数组中新增 `PENDING_VERIFICATION`**
|
|
|
+
|
|
|
+将:
|
|
|
+```csharp
|
|
|
+ var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS" };
|
|
|
+```
|
|
|
+改为:
|
|
|
+```csharp
|
|
|
+ var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION" };
|
|
|
+```
|
|
|
+
|
|
|
+**Step 6: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd AiDOPWarehouse/server
|
|
|
+dotnet build --no-restore 2>&1 | tail -20
|
|
|
+```
|
|
|
+
|
|
|
+预期:`Build succeeded`,无 error。
|
|
|
+
|
|
|
+**Step 7: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S8/AdoS8Exception.cs \
|
|
|
+ AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8StatusRules.cs \
|
|
|
+ AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/S8/S8Labels.cs \
|
|
|
+ AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DictionaryService.cs \
|
|
|
+ AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs
|
|
|
+git commit -m "feat: add PENDING_VERIFICATION status to S8 state machine"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 3: DTO 新增
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs`
|
|
|
+
|
|
|
+**Step 1: 在文件末尾追加三个 DTO 类**
|
|
|
+
|
|
|
+```csharp
|
|
|
+public class AdoS8SubmitVerificationDto
|
|
|
+{
|
|
|
+ public long VerifierId { get; set; }
|
|
|
+ public string? Remark { get; set; }
|
|
|
+}
|
|
|
+
|
|
|
+public class AdoS8ApproveVerificationDto
|
|
|
+{
|
|
|
+ public string? Remark { get; set; }
|
|
|
+}
|
|
|
+
|
|
|
+public class AdoS8RejectVerificationDto
|
|
|
+{
|
|
|
+ public string Remark { get; set; } = string.Empty;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+同时在 `AdoS8ExceptionDetailDto` 类中(`ActiveFlowBizType` 属性后)新增检验字段的输出:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ public long? VerifierId { get; set; }
|
|
|
+ public string? VerifierName { get; set; }
|
|
|
+ public DateTime? VerificationAssignedAt { get; set; }
|
|
|
+ public DateTime? VerifiedAt { get; set; }
|
|
|
+ public string? VerificationResult { get; set; }
|
|
|
+ public string? VerificationRemark { get; set; }
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd AiDOPWarehouse/server
|
|
|
+dotnet build --no-restore 2>&1 | tail -5
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/AdoS8Dtos.cs
|
|
|
+git commit -m "feat: add verification DTOs for S8"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 4: 后端服务方法
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8TaskFlowService.cs`
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs`
|
|
|
+
|
|
|
+**Step 1: 在 `S8TaskFlowService.cs` 的 `CommentAsync` 方法**前**(即 `private Task<AdoS8Exception?> LoadAsync` 之前)插入三个新方法**
|
|
|
+
|
|
|
+```csharp
|
|
|
+ public async Task<AdoS8Exception> SubmitVerificationAsync(
|
|
|
+ long id, long tenantId, long factoryId,
|
|
|
+ long currentUserId, long verifierId, string? remark)
|
|
|
+ {
|
|
|
+ var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
|
|
|
+ if (e.AssigneeId != currentUserId)
|
|
|
+ throw new S8BizException("只有当前处理人才能提交复检");
|
|
|
+ if (verifierId <= 0)
|
|
|
+ throw new S8BizException("请选择检验人");
|
|
|
+ if (!S8StatusRules.IsAllowedTransition(e.Status, "PENDING_VERIFICATION"))
|
|
|
+ throw new S8BizException($"状态 {e.Status} 不可提交复检");
|
|
|
+
|
|
|
+ var from = e.Status;
|
|
|
+ e.Status = "PENDING_VERIFICATION";
|
|
|
+ e.VerifierId = verifierId;
|
|
|
+ e.VerificationAssignedAt = DateTime.Now;
|
|
|
+ e.UpdatedAt = DateTime.Now;
|
|
|
+
|
|
|
+ await _rep.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ await _rep.UpdateAsync(e);
|
|
|
+ await InsertTimelineAsync(e.Id, "VERIFY_SUBMITTED", "提交复检", from, "PENDING_VERIFICATION",
|
|
|
+ currentUserId, null, remark);
|
|
|
+ }, ex => throw ex);
|
|
|
+
|
|
|
+ return e;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task<AdoS8Exception> ApproveVerificationAsync(
|
|
|
+ long id, long tenantId, long factoryId,
|
|
|
+ long currentUserId, string? remark)
|
|
|
+ {
|
|
|
+ var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
|
|
|
+ if (e.VerifierId != currentUserId)
|
|
|
+ throw new S8BizException("只有指定检验人才能检验通过");
|
|
|
+ if (!S8StatusRules.IsAllowedTransition(e.Status, "RESOLVED"))
|
|
|
+ throw new S8BizException($"状态 {e.Status} 不可检验通过");
|
|
|
+
|
|
|
+ var from = e.Status;
|
|
|
+ e.Status = "RESOLVED";
|
|
|
+ e.VerifiedAt = DateTime.Now;
|
|
|
+ e.VerificationResult = "APPROVED";
|
|
|
+ e.VerificationRemark = remark;
|
|
|
+ e.UpdatedAt = DateTime.Now;
|
|
|
+
|
|
|
+ await _rep.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ await _rep.UpdateAsync(e);
|
|
|
+ await InsertTimelineAsync(e.Id, "VERIFY_APPROVED", "检验通过", from, "RESOLVED",
|
|
|
+ currentUserId, null, remark);
|
|
|
+ }, ex => throw ex);
|
|
|
+
|
|
|
+ return e;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task<AdoS8Exception> RejectVerificationAsync(
|
|
|
+ long id, long tenantId, long factoryId,
|
|
|
+ long currentUserId, string remark)
|
|
|
+ {
|
|
|
+ var e = await LoadAsync(id, tenantId, factoryId) ?? throw new S8BizException("异常不存在");
|
|
|
+ if (e.VerifierId != currentUserId)
|
|
|
+ throw new S8BizException("只有指定检验人才能检验退回");
|
|
|
+ if (!S8StatusRules.IsAllowedTransition(e.Status, "IN_PROGRESS"))
|
|
|
+ throw new S8BizException($"状态 {e.Status} 不可检验退回");
|
|
|
+ if (string.IsNullOrWhiteSpace(remark))
|
|
|
+ throw new S8BizException("检验退回必须填写退回原因");
|
|
|
+
|
|
|
+ var from = e.Status;
|
|
|
+ e.Status = "IN_PROGRESS";
|
|
|
+ e.VerifiedAt = DateTime.Now;
|
|
|
+ e.VerificationResult = "REJECTED";
|
|
|
+ e.VerificationRemark = remark;
|
|
|
+ e.UpdatedAt = DateTime.Now;
|
|
|
+
|
|
|
+ await _rep.AsTenant().UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ await _rep.UpdateAsync(e);
|
|
|
+ await InsertTimelineAsync(e.Id, "VERIFY_REJECTED", "检验退回", from, "IN_PROGRESS",
|
|
|
+ currentUserId, null, remark);
|
|
|
+ }, ex => throw ex);
|
|
|
+
|
|
|
+ return e;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: 在 `S8ExceptionService.GetDetailAsync` 的 Select 投影中新增检验字段**
|
|
|
+
|
|
|
+找到 `Select((e, sc) => new AdoS8ExceptionDetailDto` 投影块,在 `ActiveFlowBizType = e.ActiveFlowBizType` 后追加:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ VerifierId = e.VerifierId,
|
|
|
+ VerificationAssignedAt = e.VerificationAssignedAt,
|
|
|
+ VerifiedAt = e.VerifiedAt,
|
|
|
+ VerificationResult = e.VerificationResult,
|
|
|
+ VerificationRemark = e.VerificationRemark,
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: 在 `S8ExceptionService.FillDisplayNamesAsync` 中填充 `VerifierName`**
|
|
|
+
|
|
|
+在 `empIds` 收集处,将:
|
|
|
+```csharp
|
|
|
+ var empIds = list
|
|
|
+ .Select(x => x.AssigneeId ?? 0L)
|
|
|
+ .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.ReporterId ?? 0L))
|
|
|
+```
|
|
|
+改为:
|
|
|
+```csharp
|
|
|
+ var empIds = list
|
|
|
+ .Select(x => x.AssigneeId ?? 0L)
|
|
|
+ .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.ReporterId ?? 0L))
|
|
|
+ .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.VerifierId ?? 0L))
|
|
|
+```
|
|
|
+
|
|
|
+在 `FillDisplayNamesAsync` 的 `foreach` 里,在 `detail.ReporterName = ...` 后追加:
|
|
|
+```csharp
|
|
|
+ detail.VerifierName = detail.VerifierId.HasValue ? empMap.GetValueOrDefault(detail.VerifierId.Value) : null;
|
|
|
+```
|
|
|
+
|
|
|
+**Step 4: 编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd AiDOPWarehouse/server
|
|
|
+dotnet build --no-restore 2>&1 | tail -5
|
|
|
+```
|
|
|
+
|
|
|
+预期:`Build succeeded`。
|
|
|
+
|
|
|
+**Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8TaskFlowService.cs \
|
|
|
+ AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8ExceptionService.cs
|
|
|
+git commit -m "feat: add submit/approve/reject verification methods to S8TaskFlowService"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 5: Controller 新增三个端点
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8ExceptionsController.cs`
|
|
|
+
|
|
|
+**Step 1: 在 `CommentAsync` Action 前插入三个新 Action**
|
|
|
+
|
|
|
+```csharp
|
|
|
+ [HttpPost("{id:long}/submit-verification")]
|
|
|
+ public async Task<IActionResult> SubmitVerificationAsync(long id,
|
|
|
+ [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
|
|
|
+ [FromBody] AdoS8SubmitVerificationDto? body = null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // currentUserId 实际项目接入 JWT 后从 HttpContext.User 取;Demo 阶段从 body 或 query 临时传入
|
|
|
+ var currentUserId = body?.VerifierId > 0 ? 0L : 0L; // placeholder,见 Step 2
|
|
|
+ var e = await _taskFlowSvc.SubmitVerificationAsync(
|
|
|
+ id, tenantId, factoryId,
|
|
|
+ /* currentUserId */ HttpContext.Request.Query.TryGetValue("currentUserId", out var v) && long.TryParse(v, out var uid) ? uid : 0L,
|
|
|
+ body?.VerifierId ?? 0,
|
|
|
+ body?.Remark);
|
|
|
+ return Ok(new { id = e.Id, status = e.Status, verifierId = e.VerifierId });
|
|
|
+ }
|
|
|
+ catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
|
|
|
+ }
|
|
|
+
|
|
|
+ [HttpPost("{id:long}/approve-verification")]
|
|
|
+ public async Task<IActionResult> ApproveVerificationAsync(long id,
|
|
|
+ [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
|
|
|
+ [FromQuery] long currentUserId = 0,
|
|
|
+ [FromBody] AdoS8ApproveVerificationDto? body = null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var e = await _taskFlowSvc.ApproveVerificationAsync(id, tenantId, factoryId, currentUserId, body?.Remark);
|
|
|
+ return Ok(new { id = e.Id, status = e.Status });
|
|
|
+ }
|
|
|
+ catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
|
|
|
+ }
|
|
|
+
|
|
|
+ [HttpPost("{id:long}/reject-verification")]
|
|
|
+ public async Task<IActionResult> RejectVerificationAsync(long id,
|
|
|
+ [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
|
|
|
+ [FromQuery] long currentUserId = 0,
|
|
|
+ [FromBody] AdoS8RejectVerificationDto? body = null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var e = await _taskFlowSvc.RejectVerificationAsync(id, tenantId, factoryId, currentUserId, body?.Remark ?? string.Empty);
|
|
|
+ return Ok(new { id = e.Id, status = e.Status });
|
|
|
+ }
|
|
|
+ catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+> **说明:** Demo 阶段没有 JWT 鉴权,`currentUserId` 通过 query 参数传入,与现有 `claim`/`transfer` 等接口的 `tenantId`/`factoryId` 风格保持一致。生产接入 JWT 后改从 `HttpContext.User` 取即可,服务层逻辑不用改。
|
|
|
+
|
|
|
+**Step 2: 清理 `submit-verification` 中的 placeholder 注释**
|
|
|
+
|
|
|
+将上面 Step 1 中 `submit-verification` Action 的 currentUserId 提取行简化为:
|
|
|
+
|
|
|
+```csharp
|
|
|
+ [HttpPost("{id:long}/submit-verification")]
|
|
|
+ public async Task<IActionResult> SubmitVerificationAsync(long id,
|
|
|
+ [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
|
|
|
+ [FromQuery] long currentUserId = 0,
|
|
|
+ [FromBody] AdoS8SubmitVerificationDto? body = null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var e = await _taskFlowSvc.SubmitVerificationAsync(
|
|
|
+ id, tenantId, factoryId, currentUserId, body?.VerifierId ?? 0, body?.Remark);
|
|
|
+ return Ok(new { id = e.Id, status = e.Status, verifierId = e.VerifierId });
|
|
|
+ }
|
|
|
+ catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: 编译并确认 Swagger 中有三个新端点**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd AiDOPWarehouse/server
|
|
|
+dotnet build --no-restore 2>&1 | tail -5
|
|
|
+```
|
|
|
+
|
|
|
+启动后访问 `/swagger`,搜索 `verification`,应看到:
|
|
|
+- `POST /api/aidop/s8/exceptions/{id}/submit-verification`
|
|
|
+- `POST /api/aidop/s8/exceptions/{id}/approve-verification`
|
|
|
+- `POST /api/aidop/s8/exceptions/{id}/reject-verification`
|
|
|
+
|
|
|
+**Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S8/AdoS8ExceptionsController.cs
|
|
|
+git commit -m "feat: add submit/approve/reject-verification endpoints to S8 controller"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 6: 前端 API 封装
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `AiDOPWarehouse/Web/src/views/aidop/s8/api/s8ExceptionApi.ts`
|
|
|
+
|
|
|
+**Step 1: 在 `S8ExceptionRow` 接口新增检验字段**
|
|
|
+
|
|
|
+在 `activeFlowBizType?: string | null;` 后追加:
|
|
|
+
|
|
|
+```ts
|
|
|
+ verifierId?: number | null;
|
|
|
+ verifierName?: string | null;
|
|
|
+ verificationAssignedAt?: string | null;
|
|
|
+ verifiedAt?: string | null;
|
|
|
+ verificationResult?: string | null;
|
|
|
+ verificationRemark?: string | null;
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: 在 `s8ExceptionApi` 对象末尾追加三个方法**
|
|
|
+
|
|
|
+```ts
|
|
|
+ submitVerification: (id: number, body: { verifierId: number; remark?: string }, currentUserId: number) =>
|
|
|
+ service.post(`/api/aidop/s8/exceptions/${id}/submit-verification?currentUserId=${currentUserId}`, body).then(unwrap),
|
|
|
+ approveVerification: (id: number, body: { remark?: string }, currentUserId: number) =>
|
|
|
+ service.post(`/api/aidop/s8/exceptions/${id}/approve-verification?currentUserId=${currentUserId}`, body).then(unwrap),
|
|
|
+ rejectVerification: (id: number, body: { remark: string }, currentUserId: number) =>
|
|
|
+ service.post(`/api/aidop/s8/exceptions/${id}/reject-verification?currentUserId=${currentUserId}`, body).then(unwrap),
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/Web/src/views/aidop/s8/api/s8ExceptionApi.ts
|
|
|
+git commit -m "feat: add verification API methods to s8ExceptionApi"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 7: 前端详情页 — 状态计算 + 弹窗逻辑
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `AiDOPWarehouse/Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue`
|
|
|
+
|
|
|
+**Step 1: 在 `<script setup>` 里的 computed 区域新增检验相关 computed**
|
|
|
+
|
|
|
+在 `const canClose = computed(...)` 后追加:
|
|
|
+
|
|
|
+```ts
|
|
|
+// Demo 阶段:currentUserId 从 detail 里的 assigneeId 模拟(实际接入 JWT 后改 useUserInfo)
|
|
|
+const currentUserId = computed(() => Number(detail.value?.assigneeId ?? 0));
|
|
|
+const canSubmitVerification = computed(
|
|
|
+ () => currentStatus.value === 'IN_PROGRESS' && !!detail.value?.assigneeId
|
|
|
+);
|
|
|
+const canApproveVerification = computed(
|
|
|
+ () => currentStatus.value === 'PENDING_VERIFICATION' && !!detail.value?.verifierId
|
|
|
+);
|
|
|
+const canRejectVerification = computed(
|
|
|
+ () => currentStatus.value === 'PENDING_VERIFICATION' && !!detail.value?.verifierId
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+> **说明:** Demo 阶段无真实登录态,所有人都能看到所有按钮(按状态控制);实际接入 JWT 后把 `currentUserId` 改成 `useUserInfo().userInfos.id` 并对比 `assigneeId`/`verifierId` 即可。
|
|
|
+
|
|
|
+**Step 2: 在 `actionTitle()` 函数的映射对象中新增三项**
|
|
|
+
|
|
|
+将:
|
|
|
+```ts
|
|
|
+ {
|
|
|
+ claim: '认领',
|
|
|
+ transfer: '转派',
|
|
|
+ upgrade: '升级',
|
|
|
+ reject: '驳回',
|
|
|
+ close: '提交关闭申请',
|
|
|
+ comment: '补充说明',
|
|
|
+ }[dialogMode.value] ?? '动作'
|
|
|
+```
|
|
|
+改为:
|
|
|
+```ts
|
|
|
+ {
|
|
|
+ claim: '认领',
|
|
|
+ transfer: '转派',
|
|
|
+ upgrade: '升级',
|
|
|
+ reject: '驳回',
|
|
|
+ close: '提交关闭申请',
|
|
|
+ comment: '补充说明',
|
|
|
+ submitVerification: '提交复检',
|
|
|
+ approveVerification: '检验通过',
|
|
|
+ rejectVerification: '检验退回',
|
|
|
+ }[dialogMode.value] ?? '动作'
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: 在 `submitAction()` 函数的 if/else 链末尾,`comment` 之前插入三个新分支**
|
|
|
+
|
|
|
+```ts
|
|
|
+ } else if (dialogMode.value === 'submitVerification') {
|
|
|
+ if (!actionForm.assigneeId) { ElMessage.warning('请选择检验人'); return; }
|
|
|
+ await s8ExceptionApi.submitVerification(
|
|
|
+ id,
|
|
|
+ { verifierId: actionForm.assigneeId, remark: actionForm.remark || undefined },
|
|
|
+ currentUserId.value
|
|
|
+ );
|
|
|
+ } else if (dialogMode.value === 'approveVerification') {
|
|
|
+ await s8ExceptionApi.approveVerification(
|
|
|
+ id,
|
|
|
+ { remark: actionForm.remark || undefined },
|
|
|
+ currentUserId.value
|
|
|
+ );
|
|
|
+ } else if (dialogMode.value === 'rejectVerification') {
|
|
|
+ if (!actionForm.remark) { ElMessage.warning('检验退回必须填写退回原因'); return; }
|
|
|
+ await s8ExceptionApi.rejectVerification(
|
|
|
+ id,
|
|
|
+ { remark: actionForm.remark },
|
|
|
+ currentUserId.value
|
|
|
+ );
|
|
|
+ } else if (dialogMode.value === 'comment') {
|
|
|
+```
|
|
|
+
|
|
|
+注意:将原来的 `} else if (dialogMode.value === 'comment') {` 行保持不变,只在其前面插入三个新分支。
|
|
|
+
|
|
|
+**Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue
|
|
|
+git commit -m "feat: add verification computed and action handlers to S8TaskDetailPage"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 8: 前端详情页 — 操作面板按钮
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `AiDOPWarehouse/Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue`
|
|
|
+
|
|
|
+**Step 1: 在 `<template>` 的 `v-else` 操作面板区新增三个检验按钮**
|
|
|
+
|
|
|
+找到:
|
|
|
+```html
|
|
|
+ <div class="action-grid">
|
|
|
+ <el-button type="primary" :disabled="!canClaim" @click="openAction('claim')">认领</el-button>
|
|
|
+ <el-button :disabled="!canTransfer" @click="openAction('transfer')">转派</el-button>
|
|
|
+ <el-button type="warning" :disabled="!canUpgrade" @click="openAction('upgrade')">升级</el-button>
|
|
|
+ <el-button type="danger" :disabled="!canReject" @click="openAction('reject')">驳回</el-button>
|
|
|
+ <el-button type="success" :disabled="!canClose" @click="openAction('close')">提交关闭申请</el-button>
|
|
|
+ <el-button @click="openAction('comment')">补充说明</el-button>
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+替换为:
|
|
|
+```html
|
|
|
+ <div class="action-grid">
|
|
|
+ <el-button type="primary" :disabled="!canClaim" @click="openAction('claim')">认领</el-button>
|
|
|
+ <el-button :disabled="!canTransfer" @click="openAction('transfer')">转派</el-button>
|
|
|
+ <el-button type="warning" :disabled="!canUpgrade" @click="openAction('upgrade')">升级</el-button>
|
|
|
+ <el-button type="danger" :disabled="!canReject" @click="openAction('reject')">驳回</el-button>
|
|
|
+ <el-button type="primary" :disabled="!canSubmitVerification" @click="openAction('submitVerification')">提交复检</el-button>
|
|
|
+ <el-button type="success" :disabled="!canApproveVerification" @click="openAction('approveVerification')">检验通过</el-button>
|
|
|
+ <el-button type="warning" :disabled="!canRejectVerification" @click="openAction('rejectVerification')">检验退回</el-button>
|
|
|
+ <el-button type="success" :disabled="!canClose" @click="openAction('close')">提交关闭申请</el-button>
|
|
|
+ <el-button @click="openAction('comment')">补充说明</el-button>
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: 在弹窗 `<el-form>` 中新增"检验人"选择项(仅 submitVerification 模式显示)**
|
|
|
+
|
|
|
+找到已有的处理人选择:
|
|
|
+```html
|
|
|
+ <el-form-item v-if="dialogMode === 'claim' || dialogMode === 'transfer'" label="处理人" required>
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+```html
|
|
|
+ <el-form-item v-if="dialogMode === 'claim' || dialogMode === 'transfer'" label="处理人" required>
|
|
|
+ <el-select v-model="actionForm.assigneeId" style="width: 100%" filterable clearable>
|
|
|
+ <el-option v-for="item in employees" :key="item.id" :label="item.name" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="dialogMode === 'submitVerification'" label="检验人" required>
|
|
|
+ <el-select v-model="actionForm.assigneeId" style="width: 100%" filterable clearable>
|
|
|
+ <el-option v-for="item in employees" :key="item.id" :label="item.name" :value="item.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: 更新弹窗备注区的 `:required` 逻辑**
|
|
|
+
|
|
|
+找到:
|
|
|
+```html
|
|
|
+ <el-form-item
|
|
|
+ :label="dialogMode === 'close' ? '处置措施' : dialogMode === 'upgrade' ? '升级原因' : '备注'"
|
|
|
+ :required="dialogMode === 'upgrade' || dialogMode === 'close'"
|
|
|
+ >
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+```html
|
|
|
+ <el-form-item
|
|
|
+ :label="dialogMode === 'close' ? '处置措施' : dialogMode === 'upgrade' ? '升级原因' : dialogMode === 'rejectVerification' ? '退回原因' : '备注'"
|
|
|
+ :required="dialogMode === 'upgrade' || dialogMode === 'close' || dialogMode === 'rejectVerification'"
|
|
|
+ >
|
|
|
+```
|
|
|
+
|
|
|
+**Step 4: 在详情卡片中展示检验人信息**
|
|
|
+
|
|
|
+找到 `<el-descriptions-item label="处理人">` 的那行,在其后插入:
|
|
|
+
|
|
|
+```html
|
|
|
+ <el-descriptions-item label="检验人">{{ detail.verifierName || detail.verifierId || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="检验结果">{{ detail.verificationResult || '—' }}</el-descriptions-item>
|
|
|
+```
|
|
|
+
|
|
|
+**Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add AiDOPWarehouse/Web/src/views/aidop/s8/exceptions/S8TaskDetailPage.vue
|
|
|
+git commit -m "feat: add verification buttons and verifier info to S8TaskDetailPage"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Task 9: 联调验证
|
|
|
+
|
|
|
+**Step 1: 启动后端**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd AiDOPWarehouse/server
|
|
|
+dotnet run --project Admin.NET.Web.Entry
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: 在 Swagger 中执行完整链路**
|
|
|
+
|
|
|
+按以下顺序操作(每步记录返回的 `status`):
|
|
|
+
|
|
|
+| 步骤 | 接口 | 预期 status |
|
|
|
+|---|---|---|
|
|
|
+| 1 | 已有 IN_PROGRESS 异常(或先 claim + 手动改状态) | `IN_PROGRESS` |
|
|
|
+| 2 | `POST /{id}/submit-verification` body: `{"verifierId":2}` query: `currentUserId=assigneeId值` | `PENDING_VERIFICATION` |
|
|
|
+| 3 | `GET /{id}` | 返回含 `verifierId`、`verificationAssignedAt` |
|
|
|
+| 4 | `GET /{id}/timeline` | 末尾出现 `VERIFY_SUBMITTED` 条目 |
|
|
|
+| 5 | `POST /{id}/reject-verification` body: `{"remark":"问题未修复"}` query: `currentUserId=verifierId值` | `IN_PROGRESS` |
|
|
|
+| 6 | `GET /{id}/timeline` | 末尾出现 `VERIFY_REJECTED` 条目 |
|
|
|
+| 7 | 重复步骤 2 | `PENDING_VERIFICATION` |
|
|
|
+| 8 | `POST /{id}/approve-verification` query: `currentUserId=verifierId值` | `RESOLVED` |
|
|
|
+| 9 | `GET /{id}/timeline` | 末尾出现 `VERIFY_APPROVED` |
|
|
|
+| 10 | `POST /{id}/close` body: `{"remark":"已修复关闭"}` | 触发 EXCEPTION_CLOSURE 审批流,status 视 handler 而定 |
|
|
|
+
|
|
|
+**Step 3: 前端验证**
|
|
|
+
|
|
|
+启动前端开发服务器:
|
|
|
+```bash
|
|
|
+cd AiDOPWarehouse/Web
|
|
|
+npm run dev
|
|
|
+```
|
|
|
+
|
|
|
+打开详情页,按上述 10 步在页面上点击验证:
|
|
|
+- IN_PROGRESS 状态:显示"提交复检"按钮(可点),"检验通过"/"检验退回"禁用
|
|
|
+- PENDING_VERIFICATION 状态:显示"检验通过"/"检验退回"可点,"提交复检"禁用
|
|
|
+- RESOLVED 状态:显示"提交关闭申请"可点
|
|
|
+
|
|
|
+**Step 4: 验证非法操作被拒绝**
|
|
|
+
|
|
|
+- 以错误的 `currentUserId`(非 assigneeId)调 `submit-verification` → 预期 400 `只有当前处理人才能提交复检`
|
|
|
+- 以错误的 `currentUserId`(非 verifierId)调 `approve-verification` → 预期 400 `只有指定检验人才能检验通过`
|
|
|
+- `approve-verification` 在状态非 `PENDING_VERIFICATION` 时调用 → 预期 400
|
|
|
+
|
|
|
+**Step 5: Final Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add .
|
|
|
+git commit -m "feat: S8 verification loop complete - submit/approve/reject verification with PENDING_VERIFICATION state"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 状态机最终结构(完成后)
|
|
|
+
|
|
|
+```
|
|
|
+NEW → ASSIGNED → IN_PROGRESS → PENDING_VERIFICATION ⇄ IN_PROGRESS
|
|
|
+ ↓
|
|
|
+ RESOLVED → CLOSED(经 EXCEPTION_CLOSURE 审批)
|
|
|
+IN_PROGRESS → ESCALATED → ASSIGNED(经 EXCEPTION_ESCALATION 审批)
|
|
|
+各状态 → REJECTED → NEW
|
|
|
+```
|
|
|
+
|
|
|
+## 验收清单
|
|
|
+
|
|
|
+- [ ] `PENDING_VERIFICATION` 在字典接口 `GET /api/aidop/s8/exceptions/filter-options` 的 `statuses` 中出现
|
|
|
+- [ ] 时间线出现 `VERIFY_SUBMITTED` / `VERIFY_APPROVED` / `VERIFY_REJECTED` 三种条目
|
|
|
+- [ ] `PENDING_VERIFICATION` 期间调 `/close` 返回 400(`RESOLVED->CLOSED` 边要求当前状态为 RESOLVED)
|
|
|
+- [ ] 检验退回后状态回到 `IN_PROGRESS`,可以再次提交复检
|
|
|
+- [ ] `EXCEPTION_CLOSURE` 仍只在 RESOLVED 状态发起,与检验环节无关
|