2026-04-18-s8-verification-loop.md 29 KB

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

-- 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: 在开发数据库执行该脚本

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

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 属性后、类的最后一个 } 前插入:

    /// <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.csEdges HashSet 中新增两条边

("IN_PROGRESS", "RESOLVED"), 这行后面插入:

        ("IN_PROGRESS", "PENDING_VERIFICATION"),
        ("PENDING_VERIFICATION", "RESOLVED"),
        ("PENDING_VERIFICATION", "IN_PROGRESS"),

Step 3: 在 S8Labels.csStatusLabel 方法中新增分支

"ESCALATED" => "已升级", 后面插入:

        "PENDING_VERIFICATION" => "待检验",

同时在 StatusOptions() 方法的数组中新增 "PENDING_VERIFICATION",放在 "RESOLVED" 之后:

将:

    public static object[] StatusOptions() =>
        new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "RESOLVED", "CLOSED", "REJECTED", "ESCALATED" }

改为:

    public static object[] StatusOptions() =>
        new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION", "RESOLVED", "CLOSED", "REJECTED", "ESCALATED" }

Step 4: 在 S8DictionaryService.csS8_EXCEPTION_STATUS case 中新增一项

new { value = "RESOLVED", label = "已处理" }, 后面插入:

                new { value = "PENDING_VERIFICATION", label = "待检验" },

Step 5: 在 S8ExceptionService.cspendingStatuses 数组中新增 PENDING_VERIFICATION

将:

        var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS" };

改为:

        var pendingStatuses = new[] { "NEW", "ASSIGNED", "IN_PROGRESS", "PENDING_VERIFICATION" };

Step 6: 编译验证

cd AiDOPWarehouse/server
dotnet build --no-restore 2>&1 | tail -20

预期:Build succeeded,无 error。

Step 7: Commit

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 类

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 属性后)新增检验字段的输出:

    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: 编译验证

cd AiDOPWarehouse/server
dotnet build --no-restore 2>&1 | tail -5

Step 3: Commit

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.csCommentAsync 方法(即 private Task<AdoS8Exception?> LoadAsync 之前)插入三个新方法

    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 后追加:

                VerifierId = e.VerifierId,
                VerificationAssignedAt = e.VerificationAssignedAt,
                VerifiedAt = e.VerifiedAt,
                VerificationResult = e.VerificationResult,
                VerificationRemark = e.VerificationRemark,

Step 3: 在 S8ExceptionService.FillDisplayNamesAsync 中填充 VerifierName

empIds 收集处,将:

        var empIds = list
            .Select(x => x.AssigneeId ?? 0L)
            .Concat(list.OfType<AdoS8ExceptionDetailDto>().Select(x => x.ReporterId ?? 0L))

改为:

        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))

FillDisplayNamesAsyncforeach 里,在 detail.ReporterName = ... 后追加:

                detail.VerifierName = detail.VerifierId.HasValue ? empMap.GetValueOrDefault(detail.VerifierId.Value) : null;

Step 4: 编译验证

cd AiDOPWarehouse/server
dotnet build --no-restore 2>&1 | tail -5

预期:Build succeeded

Step 5: Commit

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

    [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 提取行简化为:

    [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 中有三个新端点

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

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; 后追加:

	verifierId?: number | null;
	verifierName?: string | null;
	verificationAssignedAt?: string | null;
	verifiedAt?: string | null;
	verificationResult?: string | null;
	verificationRemark?: string | null;

Step 2: 在 s8ExceptionApi 对象末尾追加三个方法

	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

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(...) 后追加:

// 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() 函数的映射对象中新增三项

将:

		{
			claim: '认领',
			transfer: '转派',
			upgrade: '升级',
			reject: '驳回',
			close: '提交关闭申请',
			comment: '补充说明',
		}[dialogMode.value] ?? '动作'

改为:

		{
			claim: '认领',
			transfer: '转派',
			upgrade: '升级',
			reject: '驳回',
			close: '提交关闭申请',
			comment: '补充说明',
			submitVerification: '提交复检',
			approveVerification: '检验通过',
			rejectVerification: '检验退回',
		}[dialogMode.value] ?? '动作'

Step 3: 在 submitAction() 函数的 if/else 链末尾,comment 之前插入三个新分支

		} 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

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 操作面板区新增三个检验按钮

找到:

					<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>

替换为:

					<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 模式显示)

找到已有的处理人选择:

				<el-form-item v-if="dialogMode === 'claim' || dialogMode === 'transfer'" label="处理人" required>

改为:

				<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 逻辑

找到:

				<el-form-item
					:label="dialogMode === 'close' ? '处置措施' : dialogMode === 'upgrade' ? '升级原因' : '备注'"
					:required="dialogMode === 'upgrade' || dialogMode === 'close'"
				>

改为:

				<el-form-item
					:label="dialogMode === 'close' ? '处置措施' : dialogMode === 'upgrade' ? '升级原因' : dialogMode === 'rejectVerification' ? '退回原因' : '备注'"
					:required="dialogMode === 'upgrade' || dialogMode === 'close' || dialogMode === 'rejectVerification'"
				>

Step 4: 在详情卡片中展示检验人信息

找到 <el-descriptions-item label="处理人"> 的那行,在其后插入:

						<el-descriptions-item label="检验人">{{ detail.verifierName || detail.verifierId || '—' }}</el-descriptions-item>
						<el-descriptions-item label="检验结果">{{ detail.verificationResult || '—' }}</el-descriptions-item>

Step 5: Commit

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: 启动后端

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} 返回含 verifierIdverificationAssignedAt
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: 前端验证

启动前端开发服务器:

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

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-optionsstatuses 中出现
  • 时间线出现 VERIFY_SUBMITTED / VERIFY_APPROVED / VERIFY_REJECTED 三种条目
  • PENDING_VERIFICATION 期间调 /close 返回 400(RESOLVED->CLOSED 边要求当前状态为 RESOLVED)
  • 检验退回后状态回到 IN_PROGRESS,可以再次提交复检
  • EXCEPTION_CLOSURE 仍只在 RESOLVED 状态发起,与检验环节无关