工单类型差异化处理设计方案

一、现状分析

1.1 现有工单类型

系统当前支持5种工单类型(WorkOrderTypeEnum.java):

  • 维护保养 (maintenance) - 系统定时生成
  • 故障报修 (fault) - 客户上报
  • 软件升级 (software_upgrade) - 管理员/技术员创建
  • 硬件升级 (hardware_upgrade) - 管理员/技术员创建
  • 需求变更 (requirement_change) - 管理员创建

1.2 现有技术架构

前端: Vue 3 + Element Plus,采用统一页面 + 动态渲染的方式

  • 工单列表页: ui/src/views/business/workOrder/index.vue
  • 工单处理页: ui/src/views/business/workOrder/process.vue
  • 工单详情组件: ui/src/views/business/workOrder/components/

后端: Spring Boot + MyBatis-Plus,采用分层架构

  • 实体层: BzWorkOrder.java, BzWorkOrderSubtask.java
  • 服务层: BzWorkOrderServiceImpl.java, BzWorkOrderSubtaskServiceImpl.java
  • 控制层: BzWorkOrderController.java, BzWorkOrderSubtaskController.java
  • 枚举层: WorkOrderTypeEnum.java, WorkOrderStatusEnum.java

1.3 现有工单状态流转

待处理(pending)
    ↓
已分配(assigned) - 分配技术员
    ↓
已收到(accepted) - 技术员确认
    ↓
已联系(contacted) - 联系客户
    ↓
已预约(scheduled) - 预约时间
    ↓
执行中(in_progress) - 现场处理
    ↓
已完成(completed)
    ↓
已关闭(closed)

分支状态:

  • 已暂停(paused): 从 accepted/scheduled/in_progress 进入
  • 已取消(cancelled): 从任何状态都可以取消

二、五种工单类型的差异化设计

2.1 维护保养工单 (MAINTENANCE)

2.1.1 业务特点

  • 来源: 系统定时任务根据维护计划自动生成
  • 周期性: 日检/周检/月检/季检/年检
  • 计划性: 有明确的计划开始和结束时间
  • 关联性: 关联维护计划ID和检查模板
  • 可拆分: 需要按设备拆分子任务

2.1.2 数据库字段(BzWorkOrder表)

orderType: "maintenance"              // 工单类型
orderSource: "system_auto"            // 来源-系统自动
planId: Long                          // 关联的维护计划ID
templateId: Long                      // 关联的检查模板ID
maintenanceType: String               // 维护类型: daily/weekly/monthly/quarterly/yearly
maintenanceCycle: String              // 维护周期描述
hasSubtasks: 1                        // 有子任务
subtaskCount: Integer                 // 子任务总数
completedSubtasks: Integer            // 已完成子任务数
overallProgress: Integer              // 整体进度 0-100

2.1.3 前端差异化处理

工单创建页 (workorder-form.vue)

<!-- 维护保养专属字段 -->
<div v-if="form.orderType === 'maintenance'">
  <!-- 1. 维护计划选择 -->
  <el-form-item label="维护计划" prop="planId" required>
    <el-select v-model="form.planId" placeholder="请选择维护计划">
      <el-option v-for="plan in maintenancePlans"
                 :key="plan.id"
                 :label="plan.planName"
                 :value="plan.id" />
    </el-select>
  </el-form-item>

  <!-- 2. 检查模板选择 -->
  <el-form-item label="检查模板" prop="templateId" required>
    <el-select v-model="form.templateId" placeholder="请选择检查模板">
      <el-option v-for="template in checkTemplates"
                 :key="template.id"
                 :label="template.templateName"
                 :value="template.id" />
    </el-select>
  </el-form-item>

  <!-- 3. 维护类型 -->
  <el-form-item label="维护类型" prop="maintenanceType" required>
    <el-select v-model="form.maintenanceType">
      <el-option label="日检" value="daily" />
      <el-option label="周检" value="weekly" />
      <el-option label="月检" value="monthly" />
      <el-option label="季检" value="quarterly" />
      <el-option label="年检" value="yearly" />
    </el-select>
  </el-form-item>

  <!-- 4. 设备列表(多选) -->
  <el-form-item label="关联设备" prop="equipmentIds" required>
    <el-select v-model="form.equipmentIds" multiple placeholder="请选择设备">
      <el-option v-for="equipment in equipmentList"
                 :key="equipment.id"
                 :label="equipment.equipmentName"
                 :value="equipment.id" />
    </el-select>
  </el-form-item>
</div>

工单详情页 (workorder-detail.vue)

<!-- 维护保养专属信息 -->
<div v-if="workorder.orderType === 'maintenance'">
  <el-descriptions title="维护保养信息" :column="2" border>
    <el-descriptions-item label="维护计划">{{ workorder.planName }}</el-descriptions-item>
    <el-descriptions-item label="维护类型">{{ getMaintenanceTypeText(workorder.maintenanceType) }}</el-descriptions-item>
    <el-descriptions-item label="检查模板">{{ workorder.templateName }}</el-descriptions-item>
    <el-descriptions-item label="维护周期">{{ workorder.maintenanceCycle }}</el-descriptions-item>
  </el-descriptions>

  <!-- 子任务列表 -->
  <el-card title="设备子任务列表" class="mt-3">
    <el-table :data="subtaskList" border>
      <el-table-column label="设备编码" prop="equipmentCode" width="120" />
      <el-table-column label="设备名称" prop="equipmentName" width="150" />
      <el-table-column label="子任务状态" prop="status" width="100">
        <template #default="scope">
          <dict-tag :options="work_order_status" :value="scope.row.status" />
        </template>
      </el-table-column>
      <el-table-column label="指派技术员" prop="assignedTechnician" width="100" />
      <el-table-column label="完成进度" prop="progressPercentage" width="150">
        <template #default="scope">
          <el-progress :percentage="scope.row.progressPercentage || 0" />
        </template>
      </el-table-column>
      <el-table-column label="操作" width="200" fixed="right">
        <template #default="scope">
          <el-button link type="primary" @click="viewSubtaskDetail(scope.row)">详情</el-button>
          <el-button link type="success" @click="processSubtask(scope.row)"
                     v-if="canProcessSubtask(scope.row)">处理</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 整体进度统计 -->
    <div class="progress-summary mt-3">
      <el-statistic title="整体完成进度" :value="workorder.overallProgress" suffix="%" />
      <el-statistic title="已完成子任务" :value="workorder.completedSubtasks" :suffix="`/ ${workorder.subtaskCount}`" />
    </div>
  </el-card>
</div>

工单列表操作按钮

<!-- 拆分设备子任务按钮 - 仅维护保养工单显示 -->
<el-button v-if="row.orderType === 'maintenance' && row.status === 'pending'"
           link type="warning" icon="Operation"
           @click="handleSplitByEquipment(row)"
           v-hasPermi="['business:workOrder:edit']">
  拆分设备子任务
</el-button>

2.1.4 后端差异化处理

工单创建服务 (BzWorkOrderServiceImpl.java)

@Override
@Transactional
public int insertBzWorkOrder(BzWorkOrder workOrder) {
    // 1. 生成工单编码
    workOrder.setOrderCode(generateOrderCode());

    // 维护保养工单专属处理
    if (WorkOrderTypeEnum.isMaintenance(workOrder.getOrderType())) {
        // 1.1 验证维护计划是否存在
        BzMaintenancePlan plan = maintenancePlanService.selectById(workOrder.getPlanId());
        if (plan == null) {
            throw new ServiceException("维护计划不存在");
        }

        // 1.2 填充维护计划信息
        workOrder.setMaintenanceType(plan.getMaintenanceType());
        workOrder.setMaintenanceCycle(plan.getCycleDescription());
        workOrder.setTemplateId(plan.getTemplateId());

        // 1.3 设置默认值
        workOrder.setHasSubtasks(1);
        workOrder.setSubtaskCount(0);
        workOrder.setCompletedSubtasks(0);
        workOrder.setOverallProgress(0);

        // 1.4 保存工单
        int result = workOrderMapper.insertBzWorkOrder(workOrder);

        // 1.5 自动拆分子任务(按设备)
        if (workOrder.getEquipmentIds() != null && !workOrder.getEquipmentIds().isEmpty()) {
            List<BzWorkOrderSubtask> subtasks = decomposeByEquipment(workOrder);
            if (!subtasks.isEmpty()) {
                subtaskService.batchInsertBzWorkOrderSubtask(subtasks);
                // 更新子任务数量
                workOrder.setSubtaskCount(subtasks.size());
                workOrderMapper.updateBzWorkOrder(workOrder);
            }
        }

        return result;
    }

    // 其他类型工单的处理...
    return workOrderMapper.insertBzWorkOrder(workOrder);
}

/**
 * 按设备拆分子任务
 */
private List<BzWorkOrderSubtask> decomposeByEquipment(BzWorkOrder workOrder) {
    List<BzWorkOrderSubtask> subtasks = new ArrayList<>();
    List<Long> equipmentIds = workOrder.getEquipmentIds();

    for (int i = 0; i < equipmentIds.size(); i++) {
        Long equipmentId = equipmentIds.get(i);
        BzEquipment equipment = equipmentService.selectById(equipmentId);

        BzWorkOrderSubtask subtask = new BzWorkOrderSubtask();
        subtask.setOrderId(workOrder.getOrderId());
        subtask.setSubtaskCode(workOrder.getOrderCode() + "-ST" + String.format("%03d", i + 1));
        subtask.setSubtaskTitle(equipment.getEquipmentName() + " - " + workOrder.getMaintenanceType() + "维护");
        subtask.setSubtaskType("maintenance");
        subtask.setEquipmentId(equipmentId);
        subtask.setEquipmentName(equipment.getEquipmentName());
        subtask.setTemplateId(workOrder.getTemplateId());
        subtask.setStatus("pending");
        subtask.setPriority(workOrder.getPriority());
        subtask.setProgressPercentage(0);

        // 根据维护类型估算时长
        subtask.setEstimatedDuration(estimateDurationByMaintenanceType(workOrder.getMaintenanceType()));

        subtasks.add(subtask);
    }

    return subtasks;
}

/**
 * 根据维护类型估算时长(分钟)
 */
private Integer estimateDurationByMaintenanceType(String maintenanceType) {
    switch (maintenanceType) {
        case "daily": return 30;      // 30分钟
        case "weekly": return 60;     // 1小时
        case "monthly": return 120;   // 2小时
        case "quarterly": return 240; // 4小时
        case "yearly": return 480;    // 8小时
        default: return 60;
    }
}

工单完成验证

@Override
public int completeWorkOrder(Long orderId) {
    BzWorkOrder workOrder = workOrderMapper.selectBzWorkOrderByOrderId(orderId);

    // 维护保养工单专属验证
    if (WorkOrderTypeEnum.isMaintenance(workOrder.getOrderType())) {
        // 检查所有子任务是否都已完成
        List<BzWorkOrderSubtask> subtasks = subtaskService.selectBzWorkOrderSubtaskByOrderId(orderId);
        long uncompletedCount = subtasks.stream()
            .filter(s -> !s.getStatus().equals("completed"))
            .count();

        if (uncompletedCount > 0) {
            throw new ServiceException("存在未完成的设备子任务,无法完成工单");
        }
    }

    // 更新工单状态为已完成
    workOrder.setStatus("completed");
    workOrder.setCompletedTime(new Date());
    return workOrderMapper.updateBzWorkOrder(workOrder);
}

2.1.5 API接口

/**
 * 拆分维护保养工单为设备子任务
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:edit')")
@PostMapping("/splitByEquipment/{orderId}")
public AjaxResult splitByEquipment(@PathVariable Long orderId) {
    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);

    // 验证工单类型
    if (!WorkOrderTypeEnum.isMaintenance(workOrder.getOrderType())) {
        return error("仅维护保养工单支持按设备拆分");
    }

    // 验证工单状态
    if (!"pending".equals(workOrder.getStatus())) {
        return error("仅待处理状态的工单可以拆分");
    }

    // 执行拆分
    int count = subtaskService.splitByEquipment(orderId);
    return success("成功拆分 " + count + " 个设备子任务");
}

2.2 故障报修工单 (FAULT)

2.2.1 业务特点

  • 来源: 客户通过系统上报
  • 紧急性: 优先级默认为高或紧急
  • 响应要求: 24小时内确认收到
  • 现场照片: 支持故障现场照片上传
  • 快速分配: 需要快速分配给合适的技术员

2.2.2 数据库字段(BzWorkOrder表)

orderType: "fault"                    // 工单类型
orderSource: "customer_report"        // 来源-客户上报
priority: "urgent"/"high"             // 优先级(默认高)
reporter: String                      // 报告人
reporterPhone: String                 // 报告人电话
reportTime: Date                      // 报告时间
faultDescription: String              // 故障描述
faultType: String                     // 故障类型: mechanical/electrical/software/other
faultLevel: String                    // 故障等级: critical/major/minor
faultImages: String                   // 故障现场照片(JSON数组)
isEmergency: Integer                  // 是否紧急: 1-是 0-否
responseDeadline: Date                // 响应截止时间(24小时后)
diagnosisResult: String               // 故障诊断结果
repairMethod: String                  // 修复方法
replacedParts: String                 // 更换的备件

2.2.3 前端差异化处理

工单创建页 (workorder-form.vue)

<!-- 故障报修专属字段 -->
<div v-if="form.orderType === 'fault'">
  <!-- 1. 报告人信息 -->
  <el-form-item label="报告人" prop="reporter" required>
    <el-input v-model="form.reporter" placeholder="请输入报告人姓名" />
  </el-form-item>

  <el-form-item label="联系电话" prop="reporterPhone" required>
    <el-input v-model="form.reporterPhone" placeholder="请输入联系电话" />
  </el-form-item>

  <!-- 2. 故障类型 -->
  <el-form-item label="故障类型" prop="faultType" required>
    <el-select v-model="form.faultType">
      <el-option label="机械故障" value="mechanical" />
      <el-option label="电气故障" value="electrical" />
      <el-option label="软件故障" value="software" />
      <el-option label="其他故障" value="other" />
    </el-select>
  </el-form-item>

  <!-- 3. 故障等级 -->
  <el-form-item label="故障等级" prop="faultLevel" required>
    <el-radio-group v-model="form.faultLevel">
      <el-radio label="critical">
        <el-tag type="danger">严重故障</el-tag>
        <span class="ml-2">设备无法运行</span>
      </el-radio>
      <el-radio label="major">
        <el-tag type="warning">重要故障</el-tag>
        <span class="ml-2">设备功能受限</span>
      </el-radio>
      <el-radio label="minor">
        <el-tag type="info">一般故障</el-tag>
        <span class="ml-2">设备基本正常</span>
      </el-radio>
    </el-radio-group>
  </el-form-item>

  <!-- 4. 故障描述 -->
  <el-form-item label="故障描述" prop="faultDescription" required>
    <el-input v-model="form.faultDescription"
              type="textarea"
              :rows="4"
              placeholder="请详细描述故障现象、发生时间、影响范围等" />
  </el-form-item>

  <!-- 5. 故障现场照片 -->
  <el-form-item label="现场照片" prop="faultImages">
    <oss-image-upload
      v-model="form.faultImages"
      :limit="9"
      :file-size="10"
      accept="image/*"
      tips="支持上传最多9张现场照片,单张不超过10MB" />
  </el-form-item>

  <!-- 6. 是否紧急 -->
  <el-form-item label="是否紧急">
    <el-switch v-model="form.isEmergency"
               :active-value="1"
               :inactive-value="0"
               active-text="紧急故障"
               inactive-text="普通故障" />
    <div class="form-tip">
      紧急故障将立即通知技术员,并要求2小时内响应
    </div>
  </el-form-item>
</div>

工单详情页 (workorder-detail.vue)

<!-- 故障报修专属信息 -->
<div v-if="workorder.orderType === 'fault'">
  <!-- 故障信息 -->
  <el-descriptions title="故障信息" :column="2" border>
    <el-descriptions-item label="报告人">{{ workorder.reporter }}</el-descriptions-item>
    <el-descriptions-item label="联系电话">{{ workorder.reporterPhone }}</el-descriptions-item>
    <el-descriptions-item label="报告时间">{{ parseTime(workorder.reportTime) }}</el-descriptions-item>
    <el-descriptions-item label="响应截止">
      <span :class="isOverdue(workorder.responseDeadline) ? 'text-danger' : ''">
        {{ parseTime(workorder.responseDeadline) }}
      </span>
      <el-tag v-if="isOverdue(workorder.responseDeadline)" type="danger" size="small" class="ml-2">
        已超时
      </el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="故障类型">
      <dict-tag :options="fault_type" :value="workorder.faultType" />
    </el-descriptions-item>
    <el-descriptions-item label="故障等级">
      <el-tag :type="getFaultLevelType(workorder.faultLevel)">
        {{ getFaultLevelText(workorder.faultLevel) }}
      </el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="是否紧急">
      <el-tag :type="workorder.isEmergency ? 'danger' : 'info'">
        {{ workorder.isEmergency ? '紧急' : '普通' }}
      </el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="故障描述" :span="2">
      {{ workorder.faultDescription }}
    </el-descriptions-item>
  </el-descriptions>

  <!-- 故障现场照片 -->
  <el-card title="故障现场照片" class="mt-3" v-if="workorder.faultImages">
    <image-preview-gallery :images="parseImages(workorder.faultImages)" />
  </el-card>

  <!-- 故障诊断与修复 -->
  <el-card title="故障诊断与修复" class="mt-3" v-if="showDiagnosisInfo(workorder)">
    <el-descriptions :column="1" border>
      <el-descriptions-item label="诊断结果">
        {{ workorder.diagnosisResult || '待诊断' }}
      </el-descriptions-item>
      <el-descriptions-item label="修复方法">
        {{ workorder.repairMethod || '待修复' }}
      </el-descriptions-item>
      <el-descriptions-item label="更换备件">
        {{ workorder.replacedParts || '无' }}
      </el-descriptions-item>
    </el-descriptions>
  </el-card>
</div>

工单列表特殊标识

<!-- 故障报修工单的紧急标识 -->
<el-table-column label="工单标题" prop="orderTitle" min-width="200">
  <template #default="scope">
    <!-- 紧急图标 -->
    <el-icon v-if="scope.row.orderType === 'fault' && scope.row.isEmergency"
             color="#F56C6C" class="mr-1">
      <Warning />
    </el-icon>

    <!-- 超时标识 -->
    <el-tag v-if="scope.row.orderType === 'fault' && isResponseOverdue(scope.row)"
            type="danger" size="small" class="mr-1">
      超时
    </el-tag>

    {{ scope.row.orderTitle }}
  </template>
</el-table-column>

2.2.4 后端差异化处理

工单创建服务 (BzWorkOrderServiceImpl.java)

@Override
@Transactional
public int insertBzWorkOrder(BzWorkOrder workOrder) {
    // 故障报修工单专属处理
    if (WorkOrderTypeEnum.isFault(workOrder.getOrderType())) {
        // 1. 设置报告时间
        workOrder.setReportTime(new Date());

        // 2. 设置默认优先级(根据故障等级)
        if (workOrder.getPriority() == null) {
            if ("critical".equals(workOrder.getFaultLevel())) {
                workOrder.setPriority("urgent");
            } else if ("major".equals(workOrder.getFaultLevel())) {
                workOrder.setPriority("high");
            } else {
                workOrder.setPriority("medium");
            }
        }

        // 3. 设置响应截止时间
        if (workOrder.getIsEmergency() == 1) {
            // 紧急故障:2小时内响应
            workOrder.setResponseDeadline(DateUtils.addHours(new Date(), 2));
        } else {
            // 普通故障:24小时内响应
            workOrder.setResponseDeadline(DateUtils.addHours(new Date(), 24));
        }

        // 4. 保存工单
        int result = workOrderMapper.insertBzWorkOrder(workOrder);

        // 5. 自动分配技术员(根据故障类型和技术员专长匹配)
        assignTechnicianByFaultType(workOrder);

        // 6. 发送紧急通知
        if (workOrder.getIsEmergency() == 1) {
            sendEmergencyNotification(workOrder);
        }

        return result;
    }

    return workOrderMapper.insertBzWorkOrder(workOrder);
}

/**
 * 根据故障类型自动分配技术员
 */
private void assignTechnicianByFaultType(BzWorkOrder workOrder) {
    String faultType = workOrder.getFaultType();

    // 查询具备对应技能的技术员
    List<BzTechnician> technicians = technicianService.selectBySkill(faultType);

    if (technicians.isEmpty()) {
        log.warn("未找到具备{}技能的技术员", faultType);
        return;
    }

    // 选择当前任务最少的技术员
    BzTechnician selectedTechnician = technicians.stream()
        .min(Comparator.comparingInt(t ->
            workOrderMapper.countByTechnicianAndStatus(t.getTechnicianCode(), "in_progress")))
        .orElse(technicians.get(0));

    // 分配工单
    workOrder.setAssignedTechnicianId(selectedTechnician.getId());
    workOrder.setAssignedTechnician(selectedTechnician.getTechnicianCode());
    workOrder.setStatus("assigned");
    workOrderMapper.updateBzWorkOrder(workOrder);

    // 发送分配通知
    emailService.sendAssignNotification(workOrder, selectedTechnician);
}

/**
 * 发送紧急故障通知
 */
private void sendEmergencyNotification(BzWorkOrder workOrder) {
    // 通知项目经理
    List<SysUser> managers = userService.selectUsersByRole("projectManager");
    for (SysUser manager : managers) {
        emailService.sendEmergencyFaultNotification(manager.getEmail(), workOrder);
    }

    // 如果已分配技术员,同时通知技术员
    if (workOrder.getAssignedTechnicianId() != null) {
        BzTechnician technician = technicianService.selectById(workOrder.getAssignedTechnicianId());
        if (technician != null && technician.getEmail() != null) {
            emailService.sendEmergencyFaultNotification(technician.getEmail(), workOrder);
        }
    }
}

响应超时检查定时任务

/**
 * 检查故障报修工单响应超时
 * 每小时执行一次
 */
@Scheduled(cron = "0 0 * * * ?")
public void checkFaultResponseOverdue() {
    // 查询所有待处理或已分配的故障报修工单
    List<BzWorkOrder> faultOrders = workOrderMapper.selectList(
        new QueryWrapper<BzWorkOrder>()
            .eq("order_type", "fault")
            .in("status", Arrays.asList("pending", "assigned"))
            .lt("response_deadline", new Date())
    );

    for (BzWorkOrder order : faultOrders) {
        // 发送超时预警
        sendOverdueAlert(order);

        // 自动升级工单优先级
        if (!"urgent".equals(order.getPriority())) {
            order.setPriority("urgent");
            workOrderMapper.updateBzWorkOrder(order);
        }
    }
}

2.2.5 API接口

/**
 * 客户上报故障
 */
@PostMapping("/reportFault")
public AjaxResult reportFault(@RequestBody @Validated BzWorkOrder workOrder) {
    // 验证客户身份
    Long userId = SecurityUtils.getUserId();
    SysUser user = userService.selectUserById(userId);

    // 设置工单信息
    workOrder.setOrderType("fault");
    workOrder.setOrderSource("customer_report");
    workOrder.setReporter(user.getNickName());
    workOrder.setReporterPhone(user.getPhonenumber());
    workOrder.setCustomerId(user.getCustomerId());

    // 创建工单
    int result = workOrderService.insertBzWorkOrder(workOrder);

    return toAjax(result, "故障报修提交成功,我们将尽快安排技术员处理");
}

/**
 * 故障诊断
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:diagnose')")
@PutMapping("/diagnose")
public AjaxResult diagnoseFault(@RequestBody Map<String, Object> params) {
    Long orderId = Long.parseLong(params.get("orderId").toString());
    String diagnosisResult = params.get("diagnosisResult").toString();

    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);

    // 验证工单类型
    if (!WorkOrderTypeEnum.isFault(workOrder.getOrderType())) {
        return error("仅故障报修工单支持诊断");
    }

    // 保存诊断结果
    workOrder.setDiagnosisResult(diagnosisResult);
    workOrderService.updateBzWorkOrder(workOrder);

    return success("故障诊断完成");
}

/**
 * 修复完成
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:repair')")
@PutMapping("/completeRepair")
public AjaxResult completeRepair(@RequestBody Map<String, Object> params) {
    Long orderId = Long.parseLong(params.get("orderId").toString());
    String repairMethod = params.get("repairMethod").toString();
    String replacedParts = params.get("replacedParts").toString();

    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);

    // 保存修复信息
    workOrder.setRepairMethod(repairMethod);
    workOrder.setReplacedParts(replacedParts);
    workOrder.setStatus("completed");
    workOrder.setCompletedTime(new Date());
    workOrderService.updateBzWorkOrder(workOrder);

    // 扣减备件库存
    if (StringUtils.isNotEmpty(replacedParts)) {
        sparePartService.deductStock(replacedParts);
    }

    return success("故障修复完成");
}

2.3 软件升级工单 (SOFTWARE_UPGRADE)

2.3.1 业务特点

  • 来源: 管理员或技术员创建
  • 计划性: 需要详细的升级计划和测试方案
  • 版本管理: 关联软件版本库
  • 日志记录: 需要上传升级日志
  • 回滚机制: 支持升级失败后回滚

2.3.2 数据库字段(BzWorkOrder表)

orderType: "software_upgrade"         // 工单类型
orderSource: "admin_create"/"technician_create"
softwareVersion: String               // 目标软件版本
currentVersion: String                // 当前软件版本
upgradeType: String                   // 升级类型: major/minor/patch
upgradeMethod: String                 // 升级方式: online/offline
upgradePackageUrl: String             // 升级包下载地址
upgradeDocUrl: String                 // 升级文档地址
upgradeLogUrl: String                 // 升级日志文件地址
backupRequired: Integer               // 是否需要备份: 1-是 0-否
backupPath: String                    // 备份路径
testPlanUrl: String                   // 测试计划文档
testResult: String                    // 测试结果
rollbackPlan: String                  // 回滚方案
isRollback: Integer                   // 是否已回滚: 1-是 0-否
rollbackReason: String                // 回滚原因

2.3.3 前端差异化处理

工单创建页 (workorder-form.vue)

<!-- 软件升级专属字段 -->
<div v-if="form.orderType === 'software_upgrade'">
  <!-- 1. 版本信息 -->
  <el-form-item label="当前版本" prop="currentVersion" required>
    <el-input v-model="form.currentVersion" placeholder="如: v2.1.0" />
  </el-form-item>

  <el-form-item label="目标版本" prop="softwareVersion" required>
    <el-select v-model="form.softwareVersion"
               placeholder="请选择升级版本"
               @change="handleVersionChange">
      <el-option v-for="version in approvedVersions"
                 :key="version.id"
                 :label="version.versionName"
                 :value="version.versionNumber">
        <span>{{ version.versionName }}</span>
        <el-tag size="small" class="ml-2">{{ version.versionType }}</el-tag>
      </el-option>
    </el-select>
  </el-form-item>

  <!-- 2. 升级类型 -->
  <el-form-item label="升级类型" prop="upgradeType" required>
    <el-radio-group v-model="form.upgradeType">
      <el-radio label="major">
        <el-tag type="danger">重大版本</el-tag>
        <span class="ml-2">大版本升级,可能包含架构变更</span>
      </el-radio>
      <el-radio label="minor">
        <el-tag type="warning">次要版本</el-tag>
        <span class="ml-2">功能更新,向下兼容</span>
      </el-radio>
      <el-radio label="patch">
        <el-tag type="info">补丁版本</el-tag>
        <span class="ml-2">Bug修复,无功能变更</span>
      </el-radio>
    </el-radio-group>
  </el-form-item>

  <!-- 3. 升级方式 -->
  <el-form-item label="升级方式" prop="upgradeMethod" required>
    <el-radio-group v-model="form.upgradeMethod">
      <el-radio label="online">在线升级(不停机)</el-radio>
      <el-radio label="offline">离线升级(需停机)</el-radio>
    </el-radio-group>
  </el-form-item>

  <!-- 4. 升级包上传 -->
  <el-form-item label="升级包" prop="upgradePackageUrl" required>
    <file-upload
      v-model="form.upgradePackageUrl"
      :limit="1"
      accept=".zip,.tar.gz,.rar"
      :file-size="500"
      tips="支持zip、tar.gz格式,最大500MB" />
  </el-form-item>

  <!-- 5. 升级文档 -->
  <el-form-item label="升级文档" prop="upgradeDocUrl">
    <file-upload
      v-model="form.upgradeDocUrl"
      :limit="1"
      accept=".pdf,.doc,.docx,.md"
      tips="升级操作步骤文档" />
  </el-form-item>

  <!-- 6. 是否需要备份 -->
  <el-form-item label="数据备份">
    <el-switch v-model="form.backupRequired"
               :active-value="1"
               :inactive-value="0"
               active-text="升级前备份数据"
               inactive-text="无需备份" />
  </el-form-item>

  <!-- 7. 测试计划 -->
  <el-form-item label="测试计划" prop="testPlanUrl">
    <file-upload
      v-model="form.testPlanUrl"
      :limit="1"
      accept=".pdf,.doc,.docx,.xlsx"
      tips="升级后的测试计划文档" />
  </el-form-item>

  <!-- 8. 回滚方案 -->
  <el-form-item label="回滚方案" prop="rollbackPlan" required>
    <el-input v-model="form.rollbackPlan"
              type="textarea"
              :rows="4"
              placeholder="详细描述升级失败后的回滚步骤" />
  </el-form-item>
</div>

工单详情页 (workorder-detail.vue)

<!-- 软件升级专属信息 -->
<div v-if="workorder.orderType === 'software_upgrade'">
  <!-- 版本信息 -->
  <el-descriptions title="版本信息" :column="2" border>
    <el-descriptions-item label="当前版本">
      <el-tag type="info">{{ workorder.currentVersion }}</el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="目标版本">
      <el-tag type="success">{{ workorder.softwareVersion }}</el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="升级类型">
      <dict-tag :options="upgrade_type" :value="workorder.upgradeType" />
    </el-descriptions-item>
    <el-descriptions-item label="升级方式">
      {{ workorder.upgradeMethod === 'online' ? '在线升级' : '离线升级' }}
    </el-descriptions-item>
    <el-descriptions-item label="数据备份">
      <el-tag :type="workorder.backupRequired ? 'warning' : 'info'">
        {{ workorder.backupRequired ? '需要备份' : '无需备份' }}
      </el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="备份路径" v-if="workorder.backupPath">
      <el-link :href="workorder.backupPath" target="_blank">
        查看备份
      </el-link>
    </el-descriptions-item>
  </el-descriptions>

  <!-- 升级资源 -->
  <el-card title="升级资源" class="mt-3">
    <el-space direction="vertical" :size="15">
      <div v-if="workorder.upgradePackageUrl">
        <el-icon><Document /></el-icon>
        <el-link :href="workorder.upgradePackageUrl" target="_blank" class="ml-2">
          下载升级包
        </el-link>
      </div>
      <div v-if="workorder.upgradeDocUrl">
        <el-icon><Document /></el-icon>
        <el-link :href="workorder.upgradeDocUrl" target="_blank" class="ml-2">
          查看升级文档
        </el-link>
      </div>
      <div v-if="workorder.testPlanUrl">
        <el-icon><Document /></el-icon>
        <el-link :href="workorder.testPlanUrl" target="_blank" class="ml-2">
          查看测试计划
        </el-link>
      </div>
    </el-space>
  </el-card>

  <!-- 回滚方案 -->
  <el-card title="回滚方案" class="mt-3">
    <el-alert type="warning" :closable="false" class="mb-3">
      升级失败时请按以下步骤执行回滚
    </el-alert>
    <pre class="rollback-plan">{{ workorder.rollbackPlan }}</pre>
  </el-card>

  <!-- 升级日志 -->
  <el-card title="升级日志" class="mt-3" v-if="workorder.upgradeLogUrl">
    <el-link :href="workorder.upgradeLogUrl" target="_blank" type="primary">
      <el-icon><Document /></el-icon>
      查看升级日志
    </el-link>
  </el-card>

  <!-- 测试结果 -->
  <el-card title="测试结果" class="mt-3" v-if="workorder.testResult">
    <el-descriptions :column="1" border>
      <el-descriptions-item label="测试状态">
        <el-tag :type="getTestResultType(workorder.testResult)">
          {{ workorder.testResult }}
        </el-tag>
      </el-descriptions-item>
    </el-descriptions>
  </el-card>

  <!-- 回滚信息 -->
  <el-alert v-if="workorder.isRollback"
            type="error"
            title="该工单已回滚"
            class="mt-3"
            :closable="false">
    <div>回滚原因:{{ workorder.rollbackReason }}</div>
  </el-alert>
</div>

工单处理页特殊操作按钮

<!-- 软件升级工单的专属操作 -->
<div v-if="workorder.orderType === 'software_upgrade' && workorder.status === 'in_progress'">
  <!-- 上传升级日志 -->
  <el-button type="primary" @click="handleUpgradeLog">
    <el-icon><Upload /></el-icon>
    上传升级日志
  </el-button>

  <!-- 执行回滚 -->
  <el-button type="danger" @click="handleRollback">
    <el-icon><RefreshLeft /></el-icon>
    执行回滚
  </el-button>

  <!-- 完成测试 -->
  <el-button type="success" @click="handleCompleteTest">
    <el-icon><CircleCheck /></el-icon>
    完成测试
  </el-button>
</div>

2.3.4 后端差异化处理

工单创建服务 (BzWorkOrderServiceImpl.java)

@Override
@Transactional
public int insertBzWorkOrder(BzWorkOrder workOrder) {
    // 软件升级工单专属处理
    if ("software_upgrade".equals(workOrder.getOrderType())) {
        // 1. 验证目标版本是否已审核通过
        BzSoftwareVersion targetVersion = softwareVersionService
            .selectByVersionNumber(workOrder.getSoftwareVersion());

        if (targetVersion == null || !"approved".equals(targetVersion.getStatus())) {
            throw new ServiceException("目标版本未审核通过,无法创建升级工单");
        }

        // 2. 验证版本升级路径是否合法
        if (!validateUpgradePath(workOrder.getCurrentVersion(), workOrder.getSoftwareVersion())) {
            throw new ServiceException("不支持从当前版本直接升级到目标版本");
        }

        // 3. 设置升级类型(根据版本号判断)
        if (workOrder.getUpgradeType() == null) {
            workOrder.setUpgradeType(determineUpgradeType(
                workOrder.getCurrentVersion(),
                workOrder.getSoftwareVersion()
            ));
        }

        // 4. 设置默认优先级
        if ("major".equals(workOrder.getUpgradeType())) {
            workOrder.setPriority("high");  // 重大升级默认高优先级
        } else {
            workOrder.setPriority("medium");
        }

        // 5. 保存工单
        int result = workOrderMapper.insertBzWorkOrder(workOrder);

        // 6. 创建升级检查清单子任务
        createUpgradeChecklistSubtasks(workOrder);

        return result;
    }

    return workOrderMapper.insertBzWorkOrder(workOrder);
}

/**
 * 验证升级路径
 */
private boolean validateUpgradePath(String currentVersion, String targetVersion) {
    // 解析版本号 (如: v2.1.0 -> [2, 1, 0])
    int[] current = parseVersion(currentVersion);
    int[] target = parseVersion(targetVersion);

    // 不允许降级
    if (compareVersion(current, target) >= 0) {
        return false;
    }

    // 跨主版本升级需要经过中间版本
    if (target[0] - current[0] > 1) {
        return false;
    }

    return true;
}

/**
 * 确定升级类型
 */
private String determineUpgradeType(String currentVersion, String targetVersion) {
    int[] current = parseVersion(currentVersion);
    int[] target = parseVersion(targetVersion);

    if (target[0] > current[0]) {
        return "major";   // 主版本升级
    } else if (target[1] > current[1]) {
        return "minor";   // 次版本升级
    } else {
        return "patch";   // 补丁升级
    }
}

/**
 * 创建升级检查清单子任务
 */
private void createUpgradeChecklistSubtasks(BzWorkOrder workOrder) {
    List<BzWorkOrderSubtask> subtasks = new ArrayList<>();

    // 1. 升级前检查
    subtasks.add(createSubtask(workOrder, "升级前检查",
        "检查系统状态、备份数据、确认升级包完整性", 1));

    // 2. 执行升级
    subtasks.add(createSubtask(workOrder, "执行升级",
        "按照升级文档执行升级操作", 2));

    // 3. 功能测试
    subtasks.add(createSubtask(workOrder, "功能测试",
        "按照测试计划验证系统功能", 3));

    // 4. 性能测试(仅重大升级)
    if ("major".equals(workOrder.getUpgradeType())) {
        subtasks.add(createSubtask(workOrder, "性能测试",
            "验证系统性能指标是否符合要求", 4));
    }

    // 5. 验收确认
    subtasks.add(createSubtask(workOrder, "验收确认",
        "客户验收确认升级成功", 5));

    // 批量插入子任务
    subtaskService.batchInsertBzWorkOrderSubtask(subtasks);
}

升级回滚处理

/**
 * 执行软件升级回滚
 */
@Transactional
public int rollbackUpgrade(Long orderId, String rollbackReason) {
    BzWorkOrder workOrder = workOrderMapper.selectBzWorkOrderByOrderId(orderId);

    // 验证工单类型
    if (!"software_upgrade".equals(workOrder.getOrderType())) {
        throw new ServiceException("仅软件升级工单支持回滚");
    }

    // 验证工单状态
    if (!"in_progress".equals(workOrder.getStatus())) {
        throw new ServiceException("仅执行中的工单可以回滚");
    }

    // 1. 标记工单已回滚
    workOrder.setIsRollback(1);
    workOrder.setRollbackReason(rollbackReason);
    workOrder.setStatus("cancelled");
    workOrder.setCancelledTime(new Date());

    // 2. 记录回滚日志
    BzWorkOrderStatusLog log = new BzWorkOrderStatusLog();
    log.setOrderId(orderId);
    log.setFromStatus("in_progress");
    log.setToStatus("cancelled");
    log.setActionType("rollback");
    log.setRemark("升级失败,执行回滚:" + rollbackReason);
    log.setOperatorId(SecurityUtils.getUserId());
    log.setOperatorName(SecurityUtils.getUsername());
    log.setOperationTime(new Date());
    statusLogService.insert(log);

    // 3. 更新工单
    int result = workOrderMapper.updateBzWorkOrder(workOrder);

    // 4. 发送回滚通知
    emailService.sendRollbackNotification(workOrder);

    return result;
}

2.3.5 API接口

/**
 * 上传升级日志
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:edit')")
@PostMapping("/uploadUpgradeLog/{orderId}")
public AjaxResult uploadUpgradeLog(@PathVariable Long orderId,
                                   @RequestParam("file") MultipartFile file) {
    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);

    // 验证工单类型
    if (!"software_upgrade".equals(workOrder.getOrderType())) {
        return error("仅软件升级工单支持上传升级日志");
    }

    // 上传文件到OSS
    String fileUrl = ossService.uploadFile(file);

    // 保存文件URL
    workOrder.setUpgradeLogUrl(fileUrl);
    workOrderService.updateBzWorkOrder(workOrder);

    return success("升级日志上传成功", fileUrl);
}

/**
 * 执行回滚
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:rollback')")
@PostMapping("/rollback/{orderId}")
public AjaxResult rollback(@PathVariable Long orderId,
                          @RequestBody Map<String, String> params) {
    String rollbackReason = params.get("rollbackReason");

    if (StringUtils.isEmpty(rollbackReason)) {
        return error("请填写回滚原因");
    }

    int result = workOrderService.rollbackUpgrade(orderId, rollbackReason);
    return toAjax(result, "升级回滚成功");
}

/**
 * 完成升级测试
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:test')")
@PutMapping("/completeTest/{orderId}")
public AjaxResult completeTest(@PathVariable Long orderId,
                               @RequestBody Map<String, String> params) {
    String testResult = params.get("testResult");

    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);
    workOrder.setTestResult(testResult);

    // 如果测试通过,可以完成工单
    if ("passed".equals(testResult)) {
        workOrder.setStatus("completed");
        workOrder.setCompletedTime(new Date());
    }

    workOrderService.updateBzWorkOrder(workOrder);
    return success("测试结果已保存");
}

2.4 硬件升级工单 (HARDWARE_UPGRADE)

2.4.1 业务特点

  • 来源: 管理员或技术员创建
  • 备件管理: 需要先检查备件库存
  • 库存扣减: 执行后需登记备件使用情况
  • 采购流程: 库存不足时可发起紧急采购
  • 验收流程: 需要硬件验收确认

2.4.2 数据库字段(BzWorkOrder表)

orderType: "hardware_upgrade"         // 工单类型
orderSource: "admin_create"/"technician_create"
upgradeType: String                   // 升级类型: replacement/addition/optimization
hardwareType: String                  // 硬件类型: sensor/controller/motor/other
requiredParts: String                 // 所需备件清单(JSON数组)
partsCheckStatus: String              // 备件检查状态: pending/sufficient/insufficient
insufficientParts: String             // 不足的备件(JSON数组)
purchaseRequestId: Long               // 关联的采购申请ID
installedParts: String                // 已安装备件(JSON数组)
replacedParts: String                 // 更换下来的旧件(JSON数组)
installationPhoto: String             // 安装照片
acceptanceStatus: String              // 验收状态: pending/passed/failed
acceptanceReport: String              // 验收报告

2.4.3 前端差异化处理

工单创建页 (workorder-form.vue)

<!-- 硬件升级专属字段 -->
<div v-if="form.orderType === 'hardware_upgrade'">
  <!-- 1. 升级类型 -->
  <el-form-item label="升级类型" prop="upgradeType" required>
    <el-radio-group v-model="form.upgradeType">
      <el-radio label="replacement">硬件替换</el-radio>
      <el-radio label="addition">硬件新增</el-radio>
      <el-radio label="optimization">硬件优化</el-radio>
    </el-radio-group>
  </el-form-item>

  <!-- 2. 硬件类型 -->
  <el-form-item label="硬件类型" prop="hardwareType" required>
    <el-select v-model="form.hardwareType">
      <el-option label="传感器" value="sensor" />
      <el-option label="控制器" value="controller" />
      <el-option label="电机" value="motor" />
      <el-option label="执行器" value="actuator" />
      <el-option label="电源模块" value="power_module" />
      <el-option label="其他" value="other" />
    </el-select>
  </el-form-item>

  <!-- 3. 所需备件清单 -->
  <el-form-item label="备件清单" required>
    <el-button type="primary" @click="handleAddPart">
      <el-icon><Plus /></el-icon>
      添加备件
    </el-button>

    <el-table :data="form.requiredParts" class="mt-3" border>
      <el-table-column label="备件编码" prop="partCode" width="150" />
      <el-table-column label="备件名称" prop="partName" width="200" />
      <el-table-column label="规格型号" prop="specification" width="150" />
      <el-table-column label="所需数量" prop="quantity" width="100" />
      <el-table-column label="当前库存" prop="stock" width="100">
        <template #default="scope">
          <span :class="scope.row.stock < scope.row.quantity ? 'text-danger' : 'text-success'">
            {{ scope.row.stock }}
          </span>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="100">
        <template #default="scope">
          <el-button link type="danger" @click="handleRemovePart(scope.$index)">
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-form-item>

  <!-- 4. 库存不足提示 -->
  <el-alert v-if="hasInsufficientParts"
            type="warning"
            title="部分备件库存不足"
            :closable="false"
            class="mb-3">
    <div v-for="part in insufficientParts" :key="part.partCode">
      {{ part.partName }}:需要 {{ part.quantity }} 个,库存 {{ part.stock }} 个,
      缺少 {{ part.quantity - part.stock }} 个
    </div>
    <el-button type="primary" size="small" class="mt-2" @click="handleEmergencyPurchase">
      发起紧急采购
    </el-button>
  </el-alert>

  <!-- 5. 升级方案 -->
  <el-form-item label="升级方案" prop="description" required>
    <el-input v-model="form.description"
              type="textarea"
              :rows="6"
              placeholder="请详细描述硬件升级方案、安装步骤、注意事项等" />
  </el-form-item>
</div>

工单详情页 (workorder-detail.vue)

<!-- 硬件升级专属信息 -->
<div v-if="workorder.orderType === 'hardware_upgrade'">
  <!-- 升级信息 -->
  <el-descriptions title="硬件升级信息" :column="2" border>
    <el-descriptions-item label="升级类型">
      <dict-tag :options="hardware_upgrade_type" :value="workorder.upgradeType" />
    </el-descriptions-item>
    <el-descriptions-item label="硬件类型">
      <dict-tag :options="hardware_type" :value="workorder.hardwareType" />
    </el-descriptions-item>
    <el-descriptions-item label="备件检查">
      <el-tag :type="getPartsCheckStatusType(workorder.partsCheckStatus)">
        {{ getPartsCheckStatusText(workorder.partsCheckStatus) }}
      </el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="采购申请" v-if="workorder.purchaseRequestId">
      <el-link :href="`/business/purchase/${workorder.purchaseRequestId}`" type="primary">
        查看采购申请
      </el-link>
    </el-descriptions-item>
  </el-descriptions>

  <!-- 备件清单 -->
  <el-card title="所需备件清单" class="mt-3">
    <el-table :data="parseRequiredParts(workorder.requiredParts)" border>
      <el-table-column label="备件编码" prop="partCode" width="150" />
      <el-table-column label="备件名称" prop="partName" width="200" />
      <el-table-column label="规格型号" prop="specification" width="150" />
      <el-table-column label="所需数量" prop="quantity" width="100" />
      <el-table-column label="当前库存" prop="stock" width="100">
        <template #default="scope">
          <span :class="scope.row.stock < scope.row.quantity ? 'text-danger' : 'text-success'">
            {{ scope.row.stock }}
          </span>
        </template>
      </el-table-column>
      <el-table-column label="状态" width="100">
        <template #default="scope">
          <el-tag :type="scope.row.stock >= scope.row.quantity ? 'success' : 'danger'">
            {{ scope.row.stock >= scope.row.quantity ? '充足' : '不足' }}
          </el-tag>
        </template>
      </el-table-column>
    </el-table>
  </el-card>

  <!-- 已安装备件 -->
  <el-card title="已安装备件" class="mt-3" v-if="workorder.installedParts">
    <el-table :data="parseInstalledParts(workorder.installedParts)" border>
      <el-table-column label="备件编码" prop="partCode" width="150" />
      <el-table-column label="备件名称" prop="partName" width="200" />
      <el-table-column label="序列号" prop="serialNumber" width="150" />
      <el-table-column label="安装时间" prop="installTime" width="180" />
      <el-table-column label="安装人员" prop="installer" width="100" />
    </el-table>
  </el-card>

  <!-- 更换下的旧件 -->
  <el-card title="更换下的旧件" class="mt-3" v-if="workorder.replacedParts">
    <el-table :data="parseReplacedParts(workorder.replacedParts)" border>
      <el-table-column label="备件编码" prop="partCode" width="150" />
      <el-table-column label="备件名称" prop="partName" width="200" />
      <el-table-column label="序列号" prop="serialNumber" width="150" />
      <el-table-column label="使用时长" prop="usageDuration" width="150" />
      <el-table-column label="处理方式" prop="disposal" width="100">
        <template #default="scope">
          <el-tag :type="getDisposalType(scope.row.disposal)">
            {{ scope.row.disposal }}
          </el-tag>
        </template>
      </el-table-column>
    </el-table>
  </el-card>

  <!-- 安装照片 -->
  <el-card title="安装照片" class="mt-3" v-if="workorder.installationPhoto">
    <image-preview-gallery :images="parseImages(workorder.installationPhoto)" />
  </el-card>

  <!-- 验收信息 -->
  <el-card title="验收信息" class="mt-3" v-if="workorder.acceptanceStatus">
    <el-descriptions :column="2" border>
      <el-descriptions-item label="验收状态">
        <el-tag :type="getAcceptanceStatusType(workorder.acceptanceStatus)">
          {{ getAcceptanceStatusText(workorder.acceptanceStatus) }}
        </el-tag>
      </el-descriptions-item>
      <el-descriptions-item label="验收报告" v-if="workorder.acceptanceReport">
        <el-link :href="workorder.acceptanceReport" target="_blank">
          查看验收报告
        </el-link>
      </el-descriptions-item>
    </el-descriptions>
  </el-card>
</div>

工单处理页特殊操作

<!-- 硬件升级工单的专属操作 -->
<div v-if="workorder.orderType === 'hardware_upgrade'">
  <!-- 检查备件库存 -->
  <el-button v-if="workorder.status === 'pending'"
             type="primary"
             @click="handleCheckStock">
    <el-icon><Box /></el-icon>
    检查备件库存
  </el-button>

  <!-- 登记备件使用 -->
  <el-button v-if="workorder.status === 'in_progress'"
             type="success"
             @click="handleRecordPartUsage">
    <el-icon><Edit /></el-icon>
    登记备件使用
  </el-button>

  <!-- 上传安装照片 -->
  <el-button v-if="workorder.status === 'in_progress'"
             type="primary"
             @click="handleUploadPhoto">
    <el-icon><Camera /></el-icon>
    上传安装照片
  </el-button>

  <!-- 发起验收 -->
  <el-button v-if="workorder.status === 'in_progress'"
             type="warning"
             @click="handleInitiateAcceptance">
    <el-icon><DocumentChecked /></el-icon>
    发起验收
  </el-button>
</div>

2.4.4 后端差异化处理

工单创建服务 (BzWorkOrderServiceImpl.java)

@Override
@Transactional
public int insertBzWorkOrder(BzWorkOrder workOrder) {
    // 硬件升级工单专属处理
    if ("hardware_upgrade".equals(workOrder.getOrderType())) {
        // 1. 自动检查备件库存
        List<SparePartRequirement> requiredParts = parseRequiredParts(workOrder.getRequiredParts());
        SparePartsCheckResult checkResult = sparePartService.checkStock(requiredParts);

        // 2. 设置备件检查状态
        if (checkResult.isAllSufficient()) {
            workOrder.setPartsCheckStatus("sufficient");
        } else {
            workOrder.setPartsCheckStatus("insufficient");
            workOrder.setInsufficientParts(JSON.toJSONString(checkResult.getInsufficientParts()));

            // 如果库存不足,工单状态设置为暂停
            workOrder.setStatus("paused");
        }

        // 3. 设置默认优先级
        workOrder.setPriority("high");  // 硬件升级默认高优先级

        // 4. 保存工单
        int result = workOrderMapper.insertBzWorkOrder(workOrder);

        // 5. 如果库存充足,预留备件
        if (checkResult.isAllSufficient()) {
            sparePartService.reserveParts(workOrder.getOrderId(), requiredParts);
        }

        return result;
    }

    return workOrderMapper.insertBzWorkOrder(workOrder);
}

/**
 * 检查备件库存
 */
public SparePartsCheckResult checkStock(Long orderId) {
    BzWorkOrder workOrder = workOrderMapper.selectBzWorkOrderByOrderId(orderId);

    if (!"hardware_upgrade".equals(workOrder.getOrderType())) {
        throw new ServiceException("仅硬件升级工单需要检查备件库存");
    }

    List<SparePartRequirement> requiredParts = parseRequiredParts(workOrder.getRequiredParts());
    SparePartsCheckResult checkResult = sparePartService.checkStock(requiredParts);

    // 更新工单备件检查状态
    if (checkResult.isAllSufficient()) {
        workOrder.setPartsCheckStatus("sufficient");
        workOrder.setInsufficientParts(null);

        // 如果之前是暂停状态,恢复为待处理
        if ("paused".equals(workOrder.getStatus())) {
            workOrder.setStatus("pending");
        }

        // 预留备件
        sparePartService.reserveParts(workOrder.getOrderId(), requiredParts);
    } else {
        workOrder.setPartsCheckStatus("insufficient");
        workOrder.setInsufficientParts(JSON.toJSONString(checkResult.getInsufficientParts()));
    }

    workOrderMapper.updateBzWorkOrder(workOrder);
    return checkResult;
}

/**
 * 登记备件使用
 */
@Transactional
public int recordPartUsage(Long orderId, List<InstalledPart> installedParts,
                          List<ReplacedPart> replacedParts) {
    BzWorkOrder workOrder = workOrderMapper.selectBzWorkOrderByOrderId(orderId);

    // 1. 保存已安装备件信息
    workOrder.setInstalledParts(JSON.toJSONString(installedParts));

    // 2. 保存更换下的旧件信息
    if (replacedParts != null && !replacedParts.isEmpty()) {
        workOrder.setReplacedParts(JSON.toJSONString(replacedParts));
    }

    // 3. 扣减备件库存
    for (InstalledPart part : installedParts) {
        sparePartService.deductStock(part.getPartCode(), part.getQuantity(),
            "工单使用:" + workOrder.getOrderCode());
    }

    // 4. 旧件入库(如果可维修或回收)
    for (ReplacedPart part : replacedParts) {
        if ("repair".equals(part.getDisposal()) || "recycle".equals(part.getDisposal())) {
            sparePartService.addReplacedPart(part);
        }
    }

    // 5. 更新工单
    return workOrderMapper.updateBzWorkOrder(workOrder);
}

紧急采购流程

/**
 * 发起紧急采购
 */
@Transactional
public Long createEmergencyPurchase(Long orderId) {
    BzWorkOrder workOrder = workOrderMapper.selectBzWorkOrderByOrderId(orderId);

    // 获取不足的备件
    List<InsufficientPart> insufficientParts = parseInsufficientParts(workOrder.getInsufficientParts());

    // 创建采购申请
    BzPurchaseRequest purchaseRequest = new BzPurchaseRequest();
    purchaseRequest.setRequestType("emergency");
    purchaseRequest.setRelatedOrderId(orderId);
    purchaseRequest.setRelatedOrderCode(workOrder.getOrderCode());
    purchaseRequest.setRequestReason("工单备件库存不足");
    purchaseRequest.setUrgencyLevel("high");
    purchaseRequest.setStatus("pending");

    // 添加采购项目
    List<BzPurchaseItem> items = new ArrayList<>();
    for (InsufficientPart part : insufficientParts) {
        BzPurchaseItem item = new BzPurchaseItem();
        item.setPartCode(part.getPartCode());
        item.setPartName(part.getPartName());
        item.setSpecification(part.getSpecification());
        item.setQuantity(part.getShortage());  // 短缺数量
        items.add(item);
    }
    purchaseRequest.setItems(JSON.toJSONString(items));

    // 保存采购申请
    purchaseRequestService.insert(purchaseRequest);

    // 关联工单
    workOrder.setPurchaseRequestId(purchaseRequest.getId());
    workOrderMapper.updateBzWorkOrder(workOrder);

    // 发送采购通知
    emailService.sendEmergencyPurchaseNotification(purchaseRequest);

    return purchaseRequest.getId();
}

2.4.5 API接口

/**
 * 检查备件库存
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:checkStock')")
@GetMapping("/checkStock/{orderId}")
public AjaxResult checkStock(@PathVariable Long orderId) {
    SparePartsCheckResult result = workOrderService.checkStock(orderId);
    return success(result);
}

/**
 * 发起紧急采购
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:purchase')")
@PostMapping("/emergencyPurchase/{orderId}")
public AjaxResult emergencyPurchase(@PathVariable Long orderId) {
    Long purchaseRequestId = workOrderService.createEmergencyPurchase(orderId);
    return success("紧急采购申请已创建", purchaseRequestId);
}

/**
 * 登记备件使用
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:recordParts')")
@PostMapping("/recordPartUsage/{orderId}")
public AjaxResult recordPartUsage(@PathVariable Long orderId,
                                 @RequestBody PartUsageRequest request) {
    int result = workOrderService.recordPartUsage(
        orderId,
        request.getInstalledParts(),
        request.getReplacedParts()
    );
    return toAjax(result, "备件使用登记成功");
}

/**
 * 发起验收
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:acceptance')")
@PostMapping("/initiateAcceptance/{orderId}")
public AjaxResult initiateAcceptance(@PathVariable Long orderId) {
    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);

    // 验证是否已登记备件使用
    if (StringUtils.isEmpty(workOrder.getInstalledParts())) {
        return error("请先登记备件使用情况");
    }

    // 设置验收状态
    workOrder.setAcceptanceStatus("pending");
    workOrderService.updateBzWorkOrder(workOrder);

    // 发送验收通知
    emailService.sendAcceptanceNotification(workOrder);

    return success("验收流程已启动");
}

2.5 需求变更工单 (REQUIREMENT_CHANGE)

2.5.1 业务特点

  • 来源: 管理员创建
  • 审批流程: 需要多角色评审(项目经理 + 研发负责人)
  • 影响评估: 需要评估变更影响范围
  • 角色区分: 分配时可选择研发/实施团队
  • 变更记录: 详细记录变更内容和原因

2.5.2 数据库字段(BzWorkOrder表)

orderType: "requirement_change"       // 工单类型
orderSource: "admin_create"           // 来源-管理员创建
changeType: String                    // 变更类型: new_feature/modification/optimization/bug_fix
changeReason: String                  // 变更原因
changeContent: String                 // 变更内容(详细描述)
impactScope: String                   // 影响范围: minor/moderate/major/critical
impactAnalysis: String                // 影响分析
reviewStatus: String                  // 评审状态: pending/in_review/approved/rejected
reviewers: String                     // 评审人列表(JSON数组)
reviewRecords: String                 // 评审记录(JSON数组)
approvalTime: Date                    // 批准时间
rejectionReason: String               // 拒绝原因
implementationTeam: String            // 实施团队: development/implementation/both
estimatedWorkload: Integer            // 预估工作量(人天)
actualWorkload: Integer               // 实际工作量(人天)
testRequirement: String               // 测试要求
acceptanceCriteria: String            // 验收标准

2.5.3 前端差异化处理

工单创建页 (workorder-form.vue)

<!-- 需求变更专属字段 -->
<div v-if="form.orderType === 'requirement_change'">
  <!-- 1. 变更类型 -->
  <el-form-item label="变更类型" prop="changeType" required>
    <el-select v-model="form.changeType">
      <el-option label="新功能开发" value="new_feature" />
      <el-option label="功能修改" value="modification" />
      <el-option label="功能优化" value="optimization" />
      <el-option label="缺陷修复" value="bug_fix" />
    </el-select>
  </el-form-item>

  <!-- 2. 变更原因 -->
  <el-form-item label="变更原因" prop="changeReason" required>
    <el-input v-model="form.changeReason"
              type="textarea"
              :rows="3"
              placeholder="请说明为什么需要进行此次变更" />
  </el-form-item>

  <!-- 3. 变更内容 -->
  <el-form-item label="变更内容" prop="changeContent" required>
    <rich-text-editor v-model="form.changeContent"
                      placeholder="请详细描述变更的具体内容、功能点、业务流程等" />
  </el-form-item>

  <!-- 4. 影响范围 -->
  <el-form-item label="影响范围" prop="impactScope" required>
    <el-radio-group v-model="form.impactScope">
      <el-radio label="minor">
        <el-tag type="info">轻微</el-tag>
        <span class="ml-2">仅影响单个模块</span>
      </el-radio>
      <el-radio label="moderate">
        <el-tag type="warning">中等</el-tag>
        <span class="ml-2">影响多个相关模块</span>
      </el-radio>
      <el-radio label="major">
        <el-tag type="danger">重大</el-tag>
        <span class="ml-2">影响核心业务流程</span>
      </el-radio>
      <el-radio label="critical">
        <el-tag type="danger">严重</el-tag>
        <span class="ml-2">影响整个系统架构</span>
      </el-radio>
    </el-radio-group>
  </el-form-item>

  <!-- 5. 影响分析 -->
  <el-form-item label="影响分析" prop="impactAnalysis" required>
    <el-input v-model="form.impactAnalysis"
              type="textarea"
              :rows="4"
              placeholder="分析此次变更对现有功能、数据、用户、性能等方面的影响" />
  </el-form-item>

  <!-- 6. 预估工作量 -->
  <el-form-item label="预估工作量" prop="estimatedWorkload" required>
    <el-input-number v-model="form.estimatedWorkload"
                     :min="1"
                     :max="100"
                     controls-position="right" />
    <span class="ml-2">人天</span>
  </el-form-item>

  <!-- 7. 实施团队 -->
  <el-form-item label="实施团队" prop="implementationTeam" required>
    <el-radio-group v-model="form.implementationTeam">
      <el-radio label="development">研发团队</el-radio>
      <el-radio label="implementation">实施团队</el-radio>
      <el-radio label="both">研发+实施团队</el-radio>
    </el-radio-group>
  </el-form-item>

  <!-- 8. 测试要求 -->
  <el-form-item label="测试要求" prop="testRequirement">
    <el-input v-model="form.testRequirement"
              type="textarea"
              :rows="3"
              placeholder="描述需要进行的测试类型和测试场景" />
  </el-form-item>

  <!-- 9. 验收标准 -->
  <el-form-item label="验收标准" prop="acceptanceCriteria" required>
    <el-input v-model="form.acceptanceCriteria"
              type="textarea"
              :rows="3"
              placeholder="定义清晰的验收标准,以便后续验收" />
  </el-form-item>

  <!-- 10. 评审人选择 -->
  <el-form-item label="评审人" prop="reviewers" required>
    <el-select v-model="form.reviewers"
               multiple
               placeholder="请选择评审人">
      <el-option-group label="项目经理">
        <el-option v-for="pm in projectManagers"
                   :key="pm.userId"
                   :label="pm.nickName"
                   :value="pm.userId" />
      </el-option-group>
      <el-option-group label="研发负责人">
        <el-option v-for="dev in devLeaders"
                   :key="dev.userId"
                   :label="dev.nickName"
                   :value="dev.userId" />
      </el-option-group>
    </el-select>
  </el-form-item>
</div>

工单详情页 (workorder-detail.vue)

<!-- 需求变更专属信息 -->
<div v-if="workorder.orderType === 'requirement_change'">
  <!-- 变更信息 -->
  <el-descriptions title="变更信息" :column="2" border>
    <el-descriptions-item label="变更类型">
      <dict-tag :options="change_type" :value="workorder.changeType" />
    </el-descriptions-item>
    <el-descriptions-item label="影响范围">
      <el-tag :type="getImpactScopeType(workorder.impactScope)">
        {{ getImpactScopeText(workorder.impactScope) }}
      </el-tag>
    </el-descriptions-item>
    <el-descriptions-item label="实施团队">
      <dict-tag :options="implementation_team" :value="workorder.implementationTeam" />
    </el-descriptions-item>
    <el-descriptions-item label="预估工作量">
      {{ workorder.estimatedWorkload }} 人天
    </el-descriptions-item>
    <el-descriptions-item label="实际工作量" v-if="workorder.actualWorkload">
      {{ workorder.actualWorkload }} 人天
    </el-descriptions-item>
    <el-descriptions-item label="变更原因" :span="2">
      {{ workorder.changeReason }}
    </el-descriptions-item>
  </el-descriptions>

  <!-- 变更内容 -->
  <el-card title="变更内容" class="mt-3">
    <div class="rich-text-content" v-html="workorder.changeContent"></div>
  </el-card>

  <!-- 影响分析 -->
  <el-card title="影响分析" class="mt-3">
    <pre class="analysis-content">{{ workorder.impactAnalysis }}</pre>
  </el-card>

  <!-- 评审流程 -->
  <el-card title="评审流程" class="mt-3">
    <el-descriptions :column="2" border class="mb-3">
      <el-descriptions-item label="评审状态">
        <el-tag :type="getReviewStatusType(workorder.reviewStatus)">
          {{ getReviewStatusText(workorder.reviewStatus) }}
        </el-tag>
      </el-descriptions-item>
      <el-descriptions-item label="批准时间" v-if="workorder.approvalTime">
        {{ parseTime(workorder.approvalTime) }}
      </el-descriptions-item>
      <el-descriptions-item label="拒绝原因" :span="2" v-if="workorder.rejectionReason">
        <el-alert type="error" :closable="false">
          {{ workorder.rejectionReason }}
        </el-alert>
      </el-descriptions-item>
    </el-descriptions>

    <!-- 评审记录时间轴 -->
    <el-timeline v-if="workorder.reviewRecords">
      <el-timeline-item
        v-for="(record, index) in parseReviewRecords(workorder.reviewRecords)"
        :key="index"
        :timestamp="parseTime(record.reviewTime)"
        :type="getReviewResultType(record.result)">
        <div class="review-record">
          <div class="reviewer-info">
            <el-tag>{{ record.reviewerName }}</el-tag>
            <el-tag :type="getReviewResultType(record.result)" class="ml-2">
              {{ record.result }}
            </el-tag>
          </div>
          <div class="review-comment mt-2" v-if="record.comment">
            {{ record.comment }}
          </div>
        </div>
      </el-timeline-item>
    </el-timeline>
  </el-card>

  <!-- 测试要求 -->
  <el-card title="测试要求" class="mt-3" v-if="workorder.testRequirement">
    <pre class="test-requirement">{{ workorder.testRequirement }}</pre>
  </el-card>

  <!-- 验收标准 -->
  <el-card title="验收标准" class="mt-3">
    <pre class="acceptance-criteria">{{ workorder.acceptanceCriteria }}</pre>
  </el-card>
</div>

评审操作界面

<!-- 需求变更评审对话框 -->
<el-dialog title="需求变更评审" v-model="reviewDialogOpen" width="700px">
  <el-form :model="reviewForm" label-width="100px">
    <el-form-item label="评审结果" required>
      <el-radio-group v-model="reviewForm.result">
        <el-radio label="approved">
          <el-tag type="success">通过</el-tag>
        </el-radio>
        <el-radio label="rejected">
          <el-tag type="danger">拒绝</el-tag>
        </el-radio>
        <el-radio label="need_modification">
          <el-tag type="warning">需要修改</el-tag>
        </el-radio>
      </el-radio-group>
    </el-form-item>

    <el-form-item label="评审意见" required>
      <el-input v-model="reviewForm.comment"
                type="textarea"
                :rows="5"
                placeholder="请输入您的评审意见..." />
    </el-form-item>

    <!-- 拒绝原因(仅拒绝时显示) -->
    <el-form-item label="拒绝原因" v-if="reviewForm.result === 'rejected'" required>
      <el-select v-model="reviewForm.rejectionReason" placeholder="请选择拒绝原因">
        <el-option label="需求不合理" value="unreasonable" />
        <el-option label="技术不可行" value="infeasible" />
        <el-option label="影响范围太大" value="too_large_impact" />
        <el-option label="优先级不够" value="low_priority" />
        <el-option label="其他原因" value="other" />
      </el-select>
    </el-form-item>

    <!-- 修改建议(需要修改时显示) -->
    <el-form-item label="修改建议" v-if="reviewForm.result === 'need_modification'" required>
      <el-input v-model="reviewForm.modificationSuggestion"
                type="textarea"
                :rows="4"
                placeholder="请详细说明需要修改的内容..." />
    </el-form-item>
  </el-form>

  <template #footer>
    <el-button @click="reviewDialogOpen = false">取消</el-button>
    <el-button type="primary" @click="submitReview" :loading="reviewLoading">
      提交评审
    </el-button>
  </template>
</el-dialog>

2.5.4 后端差异化处理

工单创建服务 (BzWorkOrderServiceImpl.java)

@Override
@Transactional
public int insertBzWorkOrder(BzWorkOrder workOrder) {
    // 需求变更工单专属处理
    if ("requirement_change".equals(workOrder.getOrderType())) {
        // 1. 设置评审状态为待评审
        workOrder.setReviewStatus("pending");

        // 2. 设置优先级(根据影响范围)
        if ("critical".equals(workOrder.getImpactScope())) {
            workOrder.setPriority("urgent");
        } else if ("major".equals(workOrder.getImpactScope())) {
            workOrder.setPriority("high");
        } else {
            workOrder.setPriority("medium");
        }

        // 3. 保存工单
        int result = workOrderMapper.insertBzWorkOrder(workOrder);

        // 4. 初始化评审记录
        List<ReviewRecord> reviewRecords = new ArrayList<>();
        List<Long> reviewerIds = parseReviewers(workOrder.getReviewers());
        for (Long reviewerId : reviewerIds) {
            ReviewRecord record = new ReviewRecord();
            record.setReviewerId(reviewerId);
            record.setResult("pending");
            reviewRecords.add(record);
        }
        workOrder.setReviewRecords(JSON.toJSONString(reviewRecords));
        workOrderMapper.updateBzWorkOrder(workOrder);

        // 5. 发送评审通知
        sendReviewNotification(workOrder, reviewerIds);

        return result;
    }

    return workOrderMapper.insertBzWorkOrder(workOrder);
}

/**
 * 发送评审通知
 */
private void sendReviewNotification(BzWorkOrder workOrder, List<Long> reviewerIds) {
    for (Long reviewerId : reviewerIds) {
        SysUser reviewer = userService.selectUserById(reviewerId);
        if (reviewer != null && reviewer.getEmail() != null) {
            emailService.sendReviewNotification(reviewer.getEmail(), workOrder);
        }
    }
}

评审流程处理

/**
 * 提交评审意见
 */
@Transactional
public int submitReview(Long orderId, Long reviewerId, ReviewRequest request) {
    BzWorkOrder workOrder = workOrderMapper.selectBzWorkOrderByOrderId(orderId);

    // 验证工单类型
    if (!"requirement_change".equals(workOrder.getOrderType())) {
        throw new ServiceException("仅需求变更工单支持评审");
    }

    // 验证评审状态
    if (!"pending".equals(workOrder.getReviewStatus()) &&
        !"in_review".equals(workOrder.getReviewStatus())) {
        throw new ServiceException("工单当前状态不支持评审");
    }

    // 验证评审人权限
    List<Long> reviewerIds = parseReviewers(workOrder.getReviewers());
    if (!reviewerIds.contains(reviewerId)) {
        throw new ServiceException("您不是该工单的评审人");
    }

    // 更新评审记录
    List<ReviewRecord> reviewRecords = parseReviewRecords(workOrder.getReviewRecords());
    boolean found = false;
    for (ReviewRecord record : reviewRecords) {
        if (record.getReviewerId().equals(reviewerId)) {
            record.setResult(request.getResult());
            record.setComment(request.getComment());
            record.setReviewTime(new Date());
            record.setReviewerName(SecurityUtils.getUsername());
            found = true;
            break;
        }
    }

    if (!found) {
        throw new ServiceException("未找到您的评审记录");
    }

    // 保存评审记录
    workOrder.setReviewRecords(JSON.toJSONString(reviewRecords));
    workOrder.setReviewStatus("in_review");

    // 检查所有评审人是否都已评审
    boolean allReviewed = reviewRecords.stream()
        .allMatch(r -> !"pending".equals(r.getResult()));

    if (allReviewed) {
        // 所有人都已评审,判断最终结果
        long approvedCount = reviewRecords.stream()
            .filter(r -> "approved".equals(r.getResult()))
            .count();

        long rejectedCount = reviewRecords.stream()
            .filter(r -> "rejected".equals(r.getResult()))
            .count();

        if (rejectedCount > 0) {
            // 有人拒绝,则评审不通过
            workOrder.setReviewStatus("rejected");
            workOrder.setRejectionReason(request.getRejectionReason());
            workOrder.setStatus("cancelled");
        } else if (approvedCount == reviewRecords.size()) {
            // 所有人都通过,则评审通过
            workOrder.setReviewStatus("approved");
            workOrder.setApprovalTime(new Date());
            workOrder.setStatus("pending");  // 可以分配执行
        } else {
            // 有人要求修改
            workOrder.setReviewStatus("need_modification");
        }

        // 发送评审结果通知
        emailService.sendReviewResultNotification(workOrder);
    }

    return workOrderMapper.updateBzWorkOrder(workOrder);
}

工单分配验证

@Override
public int assignWorkOrderToTechnician(Long orderId, String technicianCode, String technicianName) {
    BzWorkOrder workOrder = workOrderMapper.selectBzWorkOrderByOrderId(orderId);

    // 需求变更工单专属验证
    if ("requirement_change".equals(workOrder.getOrderType())) {
        // 必须评审通过才能分配
        if (!"approved".equals(workOrder.getReviewStatus())) {
            throw new ServiceException("需求变更工单必须评审通过后才能分配执行");
        }
    }

    // 执行分配
    workOrder.setAssignedTechnicianId(technicianService.getIdByCode(technicianCode));
    workOrder.setAssignedTechnician(technicianCode);
    workOrder.setStatus("assigned");

    return workOrderMapper.updateBzWorkOrder(workOrder);
}

2.5.5 API接口

/**
 * 提交评审意见
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:review')")
@PostMapping("/submitReview/{orderId}")
public AjaxResult submitReview(@PathVariable Long orderId,
                               @RequestBody @Validated ReviewRequest request) {
    Long reviewerId = SecurityUtils.getUserId();
    int result = workOrderService.submitReview(orderId, reviewerId, request);
    return toAjax(result, "评审意见提交成功");
}

/**
 * 获取我的待评审工单列表
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:review')")
@GetMapping("/myPendingReviews")
public TableDataInfo myPendingReviews() {
    Long userId = SecurityUtils.getUserId();
    startPage();
    List<BzWorkOrder> list = workOrderService.selectPendingReviewsByReviewer(userId);
    return getDataTable(list);
}

/**
 * 催办评审
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:urgeReview')")
@PostMapping("/urgeReview/{orderId}")
public AjaxResult urgeReview(@PathVariable Long orderId) {
    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);

    // 获取未评审的评审人
    List<ReviewRecord> reviewRecords = parseReviewRecords(workOrder.getReviewRecords());
    List<Long> pendingReviewerIds = reviewRecords.stream()
        .filter(r -> "pending".equals(r.getResult()))
        .map(ReviewRecord::getReviewerId)
        .collect(Collectors.toList());

    if (pendingReviewerIds.isEmpty()) {
        return error("所有评审人都已完成评审");
    }

    // 发送催办通知
    for (Long reviewerId : pendingReviewerIds) {
        SysUser reviewer = userService.selectUserById(reviewerId);
        if (reviewer != null && reviewer.getEmail() != null) {
            emailService.sendUrgeReviewNotification(reviewer.getEmail(), workOrder);
        }
    }

    return success("催办通知已发送");
}

/**
 * 重新提交评审(修改后)
 */
@PreAuthorize("@ss.hasPermi('business:workOrder:edit')")
@PutMapping("/resubmitReview/{orderId}")
public AjaxResult resubmitReview(@PathVariable Long orderId) {
    BzWorkOrder workOrder = workOrderService.selectBzWorkOrderByOrderId(orderId);

    // 重置评审状态
    workOrder.setReviewStatus("pending");

    // 重置所有评审记录
    List<ReviewRecord> reviewRecords = parseReviewRecords(workOrder.getReviewRecords());
    for (ReviewRecord record : reviewRecords) {
        record.setResult("pending");
        record.setComment(null);
        record.setReviewTime(null);
    }
    workOrder.setReviewRecords(JSON.toJSONString(reviewRecords));

    workOrderService.updateBzWorkOrder(workOrder);

    // 重新发送评审通知
    List<Long> reviewerIds = reviewRecords.stream()
        .map(ReviewRecord::getReviewerId)
        .collect(Collectors.toList());
    sendReviewNotification(workOrder, reviewerIds);

    return success("已重新提交评审");
}

三、统一处理逻辑(通用部分)

3.1 通用状态流转

所有工单类型共享相同的状态流转机制,由 WorkOrderStatusEnum.java 统一管理。

3.2 通用工单操作

以下操作对所有工单类型都适用:

  • 查询工单列表/详情
  • 修改工单基础信息(标题、描述、优先级等)
  • 分配技术员
  • 收到工单(技术员确认)
  • 联系客户
  • 到达现场
  • 暂停工单
  • 恢复工单
  • 取消工单
  • 关闭工单

3.3 差异化处理策略

采用if-else分支的方式在统一接口中处理差异化逻辑:

public int processWorkOrder(Long orderId, String action) {
    BzWorkOrder workOrder = selectBzWorkOrderByOrderId(orderId);

    // 通用处理逻辑
    // ...

    // 类型差异化处理
    if (WorkOrderTypeEnum.isMaintenance(workOrder.getOrderType())) {
        // 维护保养专属逻辑
        handleMaintenanceWorkOrder(workOrder, action);
    } else if (WorkOrderTypeEnum.isFault(workOrder.getOrderType())) {
        // 故障报修专属逻辑
        handleFaultWorkOrder(workOrder, action);
    } else if ("software_upgrade".equals(workOrder.getOrderType())) {
        // 软件升级专属逻辑
        handleSoftwareUpgradeWorkOrder(workOrder, action);
    } else if ("hardware_upgrade".equals(workOrder.getOrderType())) {
        // 硬件升级专属逻辑
        handleHardwareUpgradeWorkOrder(workOrder, action);
    } else if ("requirement_change".equals(workOrder.getOrderType())) {
        // 需求变更专属逻辑
        handleRequirementChangeWorkOrder(workOrder, action);
    }

    // 继续通用处理
    // ...
}

四、数据字典配置

需要在系统数据字典中配置以下字典项:

4.1 工单类型 (work_order_type)

maintenance - 维护保养
fault - 故障报修
software_upgrade - 软件升级
hardware_upgrade - 硬件升级
requirement_change - 需求变更

4.2 故障类型 (fault_type)

mechanical - 机械故障
electrical - 电气故障
software - 软件故障
network - 网络故障
other - 其他故障

4.3 故障等级 (fault_level)

critical - 严重故障
major - 重要故障
minor - 一般故障

4.4 硬件升级类型 (hardware_upgrade_type)

replacement - 硬件替换
addition - 硬件新增
optimization - 硬件优化

4.5 硬件类型 (hardware_type)

sensor - 传感器
controller - 控制器
motor - 电机
actuator - 执行器
power_module - 电源模块
other - 其他

4.6 变更类型 (change_type)

new_feature - 新功能开发
modification - 功能修改
optimization - 功能优化
bug_fix - 缺陷修复

4.7 影响范围 (impact_scope)

minor - 轻微
moderate - 中等
major - 重大
critical - 严重

4.8 实施团队 (implementation_team)

development - 研发团队
implementation - 实施团队
both - 研发+实施

五、实现优先级建议

阶段一:核心功能(1-2周)

  1. 维护保养工单 - 最常用,优先实现

    • 按设备拆分子任务
    • 关联检查模板
    • 进度统计
  2. 故障报修工单 - 客户最关心

    • 客户上报入口
    • 紧急通知机制
    • 响应超时检查

阶段二:高级功能(2-3周)

  1. 硬件升级工单

    • 备件库存检查
    • 备件使用登记
    • 紧急采购流程
  2. 软件升级工单

    • 版本管理
    • 升级日志上传
    • 回滚机制

阶段三:扩展功能(1-2周)

  1. 需求变更工单
    • 评审流程
    • 多角色审批
    • 评审记录时间轴

六、总结

6.1 设计优势

  1. 统一架构: 5种工单类型共享核心数据结构和状态流转机制
  2. 差异化处理: 通过字段扩展和分支逻辑实现各类型专属功能
  3. 代码复用: 最大化复用现有代码,避免重复开发
  4. 易于维护: 集中管理工单逻辑,便于后期扩展新类型
  5. 用户体验: 统一的前端页面,动态展示不同类型的专属信息

6.2 技术特点

  • 前端: Vue 3组件化 + v-if动态渲染 + Element Plus
  • 后端: Spring Boot分层架构 + 策略模式 + 模板方法
  • 数据库: 单表存储 + JSON扩展字段
  • 状态管理: 枚举定义 + 状态机验证
  • 权限控制: Spring Security + 按钮级权限

6.3 扩展性

  • 新增工单类型只需:
    1. 在枚举中添加类型定义
    2. 在前端添加v-if分支渲染
    3. 在服务层添加差异化处理分支
    4. 无需修改核心架构
作者:聂盼盼  创建时间:2025-10-15 22:42
最后编辑:聂盼盼  更新时间:2025-10-28 19:53