Переглянути джерело

feat(risk): 监督抽检/平台客诉模块(3.1.4)

- SQL: risk_supervision.sql(5表+6字典) + risk_supervision_menu.sql(1菜单+10按钮)
- 后端 Inspection 模块: 实体/Bo/Vo/Mapper/Service/ServiceImpl/Controller + auditInspection
- 后端 Case 模块(5节点流程): 主表+节点表 实体/Bo/Vo/Mapper/Service/ServiceImpl/Controller + NodeVo + processCaseNode
- 后端 Complaint 模块(7节点流程): 主表+节点表 实体/Bo/Vo/Mapper/Service/ServiceImpl/Controller + NodeVo + processComplaintNode
- Stats Vo + /risk/supervision/case/stats 接口(4 张数据看板聚合)
yangjingjing 1 місяць тому
батько
коміт
29de68ac14
30 змінених файлів з 2848 додано та 0 видалено
  1. 147 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskSupervisionCaseController.java
  2. 112 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskSupervisionComplaintController.java
  3. 112 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskSupervisionInspectionController.java
  4. 107 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionCase.java
  5. 72 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionCaseNode.java
  6. 102 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionComplaint.java
  7. 72 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionComplaintNode.java
  8. 93 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionInspection.java
  9. 86 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskSupervisionCaseBo.java
  10. 83 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskSupervisionComplaintBo.java
  11. 85 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskSupervisionInspectionBo.java
  12. 89 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskSupervisionCaseVo.java
  13. 87 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskSupervisionComplaintVo.java
  14. 72 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskSupervisionInspectionVo.java
  15. 55 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/SupervisionCaseNodeVo.java
  16. 55 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/SupervisionComplaintNodeVo.java
  17. 49 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/SupervisionStatsVo.java
  18. 66 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionCaseMapper.java
  19. 14 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionCaseNodeMapper.java
  20. 66 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionComplaintMapper.java
  21. 14 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionComplaintNodeMapper.java
  22. 66 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionInspectionMapper.java
  23. 56 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskSupervisionCaseService.java
  24. 56 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskSupervisionComplaintService.java
  25. 64 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskSupervisionInspectionService.java
  26. 238 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskSupervisionCaseServiceImpl.java
  27. 217 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskSupervisionComplaintServiceImpl.java
  28. 185 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskSupervisionInspectionServiceImpl.java
  29. 245 0
      sql/risk_supervision.sql
  30. 83 0
      sql/risk_supervision_menu.sql

+ 147 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskSupervisionCaseController.java

@@ -0,0 +1,147 @@
+package org.dromara.risk.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.risk.domain.bo.RiskSupervisionCaseBo;
+import org.dromara.risk.domain.vo.RiskSupervisionCaseVo;
+import org.dromara.risk.domain.vo.SupervisionStatsVo;
+import org.dromara.risk.service.IRiskSupervisionCaseService;
+import org.dromara.risk.service.IRiskSupervisionInspectionService;
+import org.dromara.risk.service.IRiskSupervisionComplaintService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 监督抽检 - 不合格案件
+ *
+ * @author ruoyi
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/risk/supervision/case")
+public class RiskSupervisionCaseController extends BaseController {
+
+    private final IRiskSupervisionCaseService caseService;
+    private final IRiskSupervisionInspectionService inspectionService;
+    private final IRiskSupervisionComplaintService complaintService;
+
+    /**
+     * 查询案件列表
+     */
+    @SaCheckPermission("risk:supervision:case:list")
+    @GetMapping("/list")
+    public TableDataInfo<RiskSupervisionCaseVo> list(RiskSupervisionCaseBo bo, PageQuery pageQuery) {
+        return caseService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 获取案件详细信息(含历史节点)
+     */
+    @SaCheckPermission("risk:supervision:case:list")
+    @GetMapping("/{id}")
+    public R<RiskSupervisionCaseVo> getInfo(@NotNull(message = "案件ID不能为空") @PathVariable("id") Long id) {
+        return R.ok(caseService.queryById(id));
+    }
+
+    /**
+     * 新增案件(从不合格抽检单"案件处理"按钮触发)
+     */
+    @SaCheckPermission("risk:supervision:case:process")
+    @Log(title = "不合格案件处理-新增", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody RiskSupervisionCaseBo bo) {
+        return toAjax(caseService.insertByBo(bo));
+    }
+
+    /**
+     * 修改案件基础信息
+     */
+    @SaCheckPermission("risk:supervision:case:process")
+    @Log(title = "不合格案件处理-修改", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody RiskSupervisionCaseBo bo) {
+        return toAjax(caseService.updateByBo(bo));
+    }
+
+    /**
+     * 删除案件
+     */
+    @SaCheckPermission("risk:supervision:case:process")
+    @Log(title = "不合格案件处理-删除", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "案件ID不能为空") @PathVariable Long[] ids) {
+        return toAjax(caseService.deleteWithValidByIds(Arrays.asList(ids)));
+    }
+
+    /**
+     * 导出案件列表
+     */
+    @SaCheckPermission("risk:supervision:case:list")
+    @Log(title = "不合格案件处理-导出", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(RiskSupervisionCaseBo bo, HttpServletResponse response) {
+        List<RiskSupervisionCaseVo> list = caseService.queryList(bo);
+        ExcelUtil.exportExcel(list, "不合格案件处理", RiskSupervisionCaseVo.class, response);
+    }
+
+    /**
+     * 处理案件节点(提交当前节点处理, 写节点表 + current_node + 1)
+     */
+    @SaCheckPermission("risk:supervision:case:process")
+    @Log(title = "不合格案件处理-节点处理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/process")
+    public R<Void> process(@Validated(EditGroup.class) @RequestBody RiskSupervisionCaseBo bo) {
+        boolean ok = caseService.processCaseNode(bo);
+        return ok ? R.ok() : R.fail("案件节点处理失败");
+    }
+
+    /**
+     * 顶部 4 张数据看板聚合接口
+     */
+    @SaCheckPermission("risk:supervision:case:list")
+    @GetMapping("/stats")
+    public R<SupervisionStatsVo> stats() {
+        SupervisionStatsVo vo = new SupervisionStatsVo();
+
+        // 1) 今日待审核抽检(单据总量, 不限定结果)
+        long todayPending = inspectionService.countTodayPending();
+        vo.setTodayPending(todayPending);
+
+        // 2) 不合格待处理案件(状态=处置中)
+        long pendingCases = caseService.countProcessing();
+        vo.setPendingCases(pendingCases);
+
+        // 3) 待处理客诉移交(状态=处置中)
+        long pendingComplaints = complaintService.countProcessing();
+        vo.setPendingComplaints(pendingComplaints);
+
+        // 4) 本月抽检合格率
+        String ym = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
+        SupervisionStatsVo.MonthPassRate rate = inspectionService.calcMonthPassRate(ym);
+        vo.setMonthPassRate(rate);
+
+        return R.ok(vo);
+    }
+}

+ 112 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskSupervisionComplaintController.java

@@ -0,0 +1,112 @@
+package org.dromara.risk.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.risk.domain.bo.RiskSupervisionComplaintBo;
+import org.dromara.risk.domain.vo.RiskSupervisionComplaintVo;
+import org.dromara.risk.service.IRiskSupervisionComplaintService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 监督抽检 - 移交监管部门平台客诉处理
+ *
+ * @author ruoyi
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/risk/supervision/complaint")
+public class RiskSupervisionComplaintController extends BaseController {
+
+    private final IRiskSupervisionComplaintService complaintService;
+
+    /**
+     * 查询客诉列表
+     */
+    @SaCheckPermission("risk:supervision:complaint:list")
+    @GetMapping("/list")
+    public TableDataInfo<RiskSupervisionComplaintVo> list(RiskSupervisionComplaintBo bo, PageQuery pageQuery) {
+        return complaintService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 获取客诉详细信息(含历史节点)
+     */
+    @SaCheckPermission("risk:supervision:complaint:list")
+    @GetMapping("/{id}")
+    public R<RiskSupervisionComplaintVo> getInfo(@NotNull(message = "客诉ID不能为空") @PathVariable("id") Long id) {
+        return R.ok(complaintService.queryById(id));
+    }
+
+    /**
+     * 新增客诉(从"新增客诉"按钮触发)
+     */
+    @SaCheckPermission("risk:supervision:complaint:add")
+    @Log(title = "平台客诉处理-新增", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody RiskSupervisionComplaintBo bo) {
+        return toAjax(complaintService.insertByBo(bo));
+    }
+
+    /**
+     * 修改客诉基础信息
+     */
+    @SaCheckPermission("risk:supervision:complaint:process")
+    @Log(title = "平台客诉处理-修改", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody RiskSupervisionComplaintBo bo) {
+        return toAjax(complaintService.updateByBo(bo));
+    }
+
+    /**
+     * 删除客诉
+     */
+    @SaCheckPermission("risk:supervision:complaint:process")
+    @Log(title = "平台客诉处理-删除", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "客诉ID不能为空") @PathVariable Long[] ids) {
+        return toAjax(complaintService.deleteWithValidByIds(Arrays.asList(ids)));
+    }
+
+    /**
+     * 导出客诉列表
+     */
+    @SaCheckPermission("risk:supervision:complaint:list")
+    @Log(title = "平台客诉处理-导出", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(RiskSupervisionComplaintBo bo, HttpServletResponse response) {
+        List<RiskSupervisionComplaintVo> list = complaintService.queryList(bo);
+        ExcelUtil.exportExcel(list, "平台客诉处理", RiskSupervisionComplaintVo.class, response);
+    }
+
+    /**
+     * 处理客诉节点(提交当前环节处理, 写节点表 + current_node + 1)
+     */
+    @SaCheckPermission("risk:supervision:complaint:process")
+    @Log(title = "平台客诉处理-节点处理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/process")
+    public R<Void> process(@Validated(EditGroup.class) @RequestBody RiskSupervisionComplaintBo bo) {
+        boolean ok = complaintService.processComplaintNode(bo);
+        return ok ? R.ok() : R.fail("客诉节点处理失败");
+    }
+}

+ 112 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskSupervisionInspectionController.java

@@ -0,0 +1,112 @@
+package org.dromara.risk.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.risk.domain.bo.RiskSupervisionInspectionBo;
+import org.dromara.risk.domain.vo.RiskSupervisionInspectionVo;
+import org.dromara.risk.service.IRiskSupervisionInspectionService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 监督抽检 - 门店抽检单
+ *
+ * @author ruoyi
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/risk/supervision/inspection")
+public class RiskSupervisionInspectionController extends BaseController {
+
+    private final IRiskSupervisionInspectionService inspectionService;
+
+    /**
+     * 查询抽检单列表
+     */
+    @SaCheckPermission("risk:supervision:inspection:list")
+    @GetMapping("/list")
+    public TableDataInfo<RiskSupervisionInspectionVo> list(RiskSupervisionInspectionBo bo, PageQuery pageQuery) {
+        return inspectionService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 获取抽检单详细信息
+     */
+    @SaCheckPermission("risk:supervision:inspection:list")
+    @GetMapping("/{id}")
+    public R<RiskSupervisionInspectionVo> getInfo(@NotNull(message = "抽检单ID不能为空") @PathVariable("id") Long id) {
+        return R.ok(inspectionService.queryById(id));
+    }
+
+    /**
+     * 新增抽检单
+     */
+    @SaCheckPermission("risk:supervision:inspection:add")
+    @Log(title = "门店抽检登记", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody RiskSupervisionInspectionBo bo) {
+        return toAjax(inspectionService.insertByBo(bo));
+    }
+
+    /**
+     * 修改抽检单
+     */
+    @SaCheckPermission("risk:supervision:inspection:edit")
+    @Log(title = "门店抽检登记", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody RiskSupervisionInspectionBo bo) {
+        return toAjax(inspectionService.updateByBo(bo));
+    }
+
+    /**
+     * 删除抽检单
+     */
+    @SaCheckPermission("risk:supervision:inspection:remove")
+    @Log(title = "门店抽检登记", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "抽检单ID不能为空") @PathVariable Long[] ids) {
+        return toAjax(inspectionService.deleteWithValidByIds(Arrays.asList(ids)));
+    }
+
+    /**
+     * 导出抽检单列表
+     */
+    @SaCheckPermission("risk:supervision:inspection:export")
+    @Log(title = "门店抽检登记", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(RiskSupervisionInspectionBo bo, HttpServletResponse response) {
+        List<RiskSupervisionInspectionVo> list = inspectionService.queryList(bo);
+        ExcelUtil.exportExcel(list, "门店抽检登记", RiskSupervisionInspectionVo.class, response);
+    }
+
+    /**
+     * 审核抽检单(待审核 → 已审核)
+     */
+    @SaCheckPermission("risk:supervision:inspection:edit")
+    @Log(title = "门店抽检登记-审核", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/audit/{id}")
+    public R<Void> audit(@NotNull(message = "抽检单ID不能为空") @PathVariable("id") Long id) {
+        boolean ok = inspectionService.auditInspection(id);
+        return ok ? R.ok() : R.fail("审核失败");
+    }
+}

+ 107 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionCase.java

@@ -0,0 +1,107 @@
+package org.dromara.risk.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 不合格案件主表对象 risk_supervision_case
+ *
+ * @author ruoyi
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@TableName("risk_supervision_case")
+public class RiskSupervisionCase extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 案件编号(AJ+yyyyMMdd+4位序号)
+     */
+    private String caseCode;
+
+    /**
+     * 关联抽检单ID
+     */
+    private Long inspectionId;
+
+    /**
+     * 冗余抽检单号
+     */
+    private String inspectionCode;
+
+    /**
+     * 涉事门店ID
+     */
+    private Long storeId;
+
+    /**
+     * 涉事门店
+     */
+    private String storeName;
+
+    /**
+     * 不合格产品
+     */
+    private String productName;
+
+    /**
+     * 当前节点(1-5)
+     */
+    private Integer currentNode;
+
+    /**
+     * 当前节点名称(冗余展示)
+     */
+    private String nodeName;
+
+    /**
+     * 风险等级(字典:高/中/低风险)
+     */
+    private String riskLevel;
+
+    /**
+     * 风险描述
+     */
+    private String riskDesc;
+
+    /**
+     * 控制措施
+     */
+    private String controlMeasures;
+
+    /**
+     * 处理人ID
+     */
+    private Long handlerId;
+
+    /**
+     * 处理人
+     */
+    private String handlerName;
+
+    /**
+     * 状态(字典:处置中/已闭环)
+     */
+    private String status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 72 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionCaseNode.java

@@ -0,0 +1,72 @@
+package org.dromara.risk.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 案件节点处理记录对象 risk_supervision_case_node
+ *
+ * @author ruoyi
+ */
+@Data
+@NoArgsConstructor
+@TableName("risk_supervision_case_node")
+public class RiskSupervisionCaseNode extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 案件ID
+     */
+    private Long caseId;
+
+    /**
+     * 节点编号 1-5
+     */
+    private Integer nodeCode;
+
+    /**
+     * 节点名称
+     */
+    private String nodeName;
+
+    /**
+     * 当前节点处理意见
+     */
+    private String opinion;
+
+    /**
+     * 凭证附件URL(MinIO)
+     */
+    private String attachmentUrl;
+
+    /**
+     * 凭证附件原始文件名
+     */
+    private String attachmentName;
+
+    /**
+     * 处理人ID
+     */
+    private Long handlerId;
+
+    /**
+     * 处理人
+     */
+    private String handlerName;
+
+    /**
+     * 处理时间
+     */
+    private Date handleTime;
+}

+ 102 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionComplaint.java

@@ -0,0 +1,102 @@
+package org.dromara.risk.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 平台客诉主表对象 risk_supervision_complaint
+ *
+ * @author ruoyi
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@TableName("risk_supervision_complaint")
+public class RiskSupervisionComplaint extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 客诉编号(TS+yyyyMMdd+4位序号)
+     */
+    private String complaintCode;
+
+    /**
+     * 客诉来源(字典)
+     */
+    private String source;
+
+    /**
+     * 投诉人姓名
+     */
+    private String complainantName;
+
+    /**
+     * 投诉人电话
+     */
+    private String complainantPhone;
+
+    /**
+     * 涉事门店ID
+     */
+    private Long storeId;
+
+    /**
+     * 涉事门店
+     */
+    private String storeName;
+
+    /**
+     * 移交时间
+     */
+    private Date transferTime;
+
+    /**
+     * 客诉内容
+     */
+    private String complaintContent;
+
+    /**
+     * 附件URL
+     */
+    private String attachmentUrl;
+
+    /**
+     * 附件原始文件名
+     */
+    private String attachmentName;
+
+    /**
+     * 当前节点(1-7)
+     */
+    private Integer currentNode;
+
+    /**
+     * 当前节点名称
+     */
+    private String nodeName;
+
+    /**
+     * 状态(字典:处置中/已闭环)
+     */
+    private String status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 72 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionComplaintNode.java

@@ -0,0 +1,72 @@
+package org.dromara.risk.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 平台客诉节点处理记录对象 risk_supervision_complaint_node
+ *
+ * @author ruoyi
+ */
+@Data
+@NoArgsConstructor
+@TableName("risk_supervision_complaint_node")
+public class RiskSupervisionComplaintNode extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 客诉ID
+     */
+    private Long complaintId;
+
+    /**
+     * 节点编号 1-7
+     */
+    private Integer nodeCode;
+
+    /**
+     * 节点名称
+     */
+    private String nodeName;
+
+    /**
+     * 当前环节处理意见
+     */
+    private String opinion;
+
+    /**
+     * 凭证附件URL
+     */
+    private String attachmentUrl;
+
+    /**
+     * 凭证附件原始文件名
+     */
+    private String attachmentName;
+
+    /**
+     * 处理人ID
+     */
+    private Long handlerId;
+
+    /**
+     * 处理人
+     */
+    private String handlerName;
+
+    /**
+     * 处理时间
+     */
+    private Date handleTime;
+}

+ 93 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskSupervisionInspection.java

@@ -0,0 +1,93 @@
+package org.dromara.risk.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 门店抽检单对象 risk_supervision_inspection
+ *
+ * @author ruoyi
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@TableName("risk_supervision_inspection")
+public class RiskSupervisionInspection extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 抽检单号(JC+yyyyMMdd+4位序号)
+     */
+    private String inspectionCode;
+
+    /**
+     * 门店ID
+     */
+    private Long storeId;
+
+    /**
+     * 门店名称
+     */
+    private String storeName;
+
+    /**
+     * 抽检时间
+     */
+    private Date inspectionTime;
+
+    /**
+     * 抽检商品
+     */
+    private String productName;
+
+    /**
+     * 抽检结果(字典:抽检合格/抽检不合格)
+     */
+    private String inspectionResult;
+
+    /**
+     * 不合格原因
+     */
+    private String failReason;
+
+    /**
+     * 状态(字典:待审核/已审核)
+     */
+    private String status;
+
+    /**
+     * 抽检人ID
+     */
+    private Long inspectorId;
+
+    /**
+     * 抽检人姓名
+     */
+    private String inspectorName;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 删除标志
+     */
+    @TableLogic
+    private String delFlag;
+}

+ 86 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskSupervisionCaseBo.java

@@ -0,0 +1,86 @@
+package org.dromara.risk.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.risk.domain.RiskSupervisionCase;
+
+/**
+ * 监督抽检 - 不合格案件业务对象 risk_supervision_case
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = RiskSupervisionCase.class, reverseConvertGenerate = false)
+public class RiskSupervisionCaseBo extends BaseEntity {
+
+    private Long id;
+
+    /** 案件编号 */
+    private String caseCode;
+
+    /** 关联抽检单ID(新增案件时必填,从不合格抽检单发起) */
+    @NotNull(message = "关联抽检单不能为空")
+    private Long inspectionId;
+
+    /** 冗余抽检单号 */
+    private String inspectionCode;
+
+    /** 涉事门店ID */
+    private Long storeId;
+
+    /** 涉事门店 */
+    private String storeName;
+
+    /** 不合格产品 */
+    private String productName;
+
+    /** 当前节点(1-5) */
+    private Integer currentNode;
+
+    /** 当前节点名称 */
+    private String nodeName;
+
+    /** 风险等级(字典:高/中/低风险) */
+    @NotBlank(message = "风险等级不能为空")
+    private String riskLevel;
+
+    /** 风险描述 */
+    @NotBlank(message = "风险描述不能为空")
+    private String riskDesc;
+
+    /** 控制措施 */
+    @NotBlank(message = "控制措施不能为空")
+    private String controlMeasures;
+
+    /** 处理人ID */
+    private Long handlerId;
+
+    /** 处理人 */
+    @NotBlank(message = "处理人不能为空")
+    private String handlerName;
+
+    /** 状态(字典:处置中/已闭环) */
+    private String status;
+
+    /** 备注 */
+    private String remark;
+
+    /** 关键词(搜索:案件编号/抽检单号/门店/产品) */
+    private String keyword;
+
+    /** ============== 节点处理专用字段(PUT /process) ============== */
+
+    /** 当前节点处理意见(处理时必填) */
+    private String opinion;
+
+    /** 凭证附件URL(MinIO) */
+    private String attachmentUrl;
+
+    /** 凭证附件原始文件名 */
+    private String attachmentName;
+}

+ 83 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskSupervisionComplaintBo.java

@@ -0,0 +1,83 @@
+package org.dromara.risk.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.risk.domain.RiskSupervisionComplaint;
+
+import java.util.Date;
+
+/**
+ * 监督抽检 - 平台客诉业务对象 risk_supervision_complaint
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = RiskSupervisionComplaint.class, reverseConvertGenerate = false)
+public class RiskSupervisionComplaintBo extends BaseEntity {
+
+    private Long id;
+
+    /** 客诉编号 */
+    private String complaintCode;
+
+    /** 客诉来源(字典) */
+    @NotBlank(message = "客诉来源不能为空")
+    private String source;
+
+    /** 投诉人姓名 */
+    @NotBlank(message = "投诉人姓名不能为空")
+    private String complainantName;
+
+    /** 投诉人电话 */
+    private String complainantPhone;
+
+    /** 涉事门店ID */
+    private Long storeId;
+
+    /** 涉事门店 */
+    @NotBlank(message = "涉事门店不能为空")
+    private String storeName;
+
+    /** 移交时间 */
+    private Date transferTime;
+
+    /** 客诉内容 */
+    @NotBlank(message = "客诉内容不能为空")
+    private String complaintContent;
+
+    /** 客诉附件URL */
+    private String attachmentUrl;
+
+    /** 客诉附件原始文件名 */
+    private String attachmentName;
+
+    /** 当前节点(1-7) */
+    private Integer currentNode;
+
+    /** 当前节点名称 */
+    private String nodeName;
+
+    /** 状态(字典:处置中/已闭环) */
+    private String status;
+
+    /** 备注 */
+    private String remark;
+
+    /** 关键词(搜索:客诉编号/投诉人/门店/内容) */
+    private String keyword;
+
+    /** ============== 节点处理专用字段(PUT /process) ============== */
+
+    /** 当前节点处理意见(处理时必填) */
+    private String opinion;
+
+    /** 凭证附件URL(处理环节上传) */
+    private String processAttachmentUrl;
+
+    /** 凭证附件原始文件名(处理环节上传) */
+    private String processAttachmentName;
+}

+ 85 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskSupervisionInspectionBo.java

@@ -0,0 +1,85 @@
+package org.dromara.risk.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.risk.domain.RiskSupervisionInspection;
+
+import java.util.Date;
+
+/**
+ * 监督抽检 - 门店抽检单业务对象 risk_supervision_inspection
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = RiskSupervisionInspection.class, reverseConvertGenerate = false)
+public class RiskSupervisionInspectionBo extends BaseEntity {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 抽检单号(JC+yyyyMMdd+4位序号)
+     */
+    private String inspectionCode;
+
+    /**
+     * 门店ID
+     */
+    private Long storeId;
+
+    /**
+     * 门店名称
+     */
+    private String storeName;
+
+    /**
+     * 抽检时间
+     */
+    private Date inspectionTime;
+
+    /**
+     * 抽检商品
+     */
+    private String productName;
+
+    /**
+     * 抽检结果(字典:抽检合格/抽检不合格)
+     */
+    private String inspectionResult;
+
+    /**
+     * 不合格原因
+     */
+    private String failReason;
+
+    /**
+     * 状态(字典:待审核/已审核)
+     */
+    private String status;
+
+    /**
+     * 抽检人ID
+     */
+    private Long inspectorId;
+
+    /**
+     * 抽检人姓名
+     */
+    private String inspectorName;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 关键词(搜索:抽检单号/门店名称/抽检商品)
+     */
+    private String keyword;
+}

+ 89 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskSupervisionCaseVo.java

@@ -0,0 +1,89 @@
+package org.dromara.risk.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.risk.domain.RiskSupervisionCase;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 监督抽检 - 不合格案件视图对象 risk_supervision_case
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = RiskSupervisionCase.class)
+public class RiskSupervisionCaseVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "主键ID")
+    private Long id;
+
+    @ExcelProperty(value = "案件编号")
+    private String caseCode;
+
+    @ExcelProperty(value = "关联抽检单ID")
+    private Long inspectionId;
+
+    @ExcelProperty(value = "关联抽检单号")
+    private String inspectionCode;
+
+    @ExcelProperty(value = "涉事门店ID")
+    private Long storeId;
+
+    @ExcelProperty(value = "涉事门店")
+    private String storeName;
+
+    @ExcelProperty(value = "不合格产品")
+    private String productName;
+
+    @ExcelProperty(value = "当前节点")
+    private Integer currentNode;
+
+    @ExcelProperty(value = "当前节点名称")
+    private String nodeName;
+
+    @ExcelProperty(value = "风险等级")
+    private String riskLevel;
+
+    @ExcelProperty(value = "风险描述")
+    private String riskDesc;
+
+    @ExcelProperty(value = "控制措施")
+    private String controlMeasures;
+
+    @ExcelProperty(value = "处理人ID")
+    private Long handlerId;
+
+    @ExcelProperty(value = "处理人")
+    private String handlerName;
+
+    @ExcelProperty(value = "状态")
+    private String status;
+
+    @ExcelProperty(value = "备注")
+    private String remark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "创建时间")
+    private Date createTime;
+
+    @ExcelProperty(value = "更新人")
+    private Long updateBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "更新时间")
+    private Date updateTime;
+
+    /**
+     * 历史节点处理记录(仅详情接口填充)
+     */
+    private List<SupervisionCaseNodeVo> nodeRecords;
+}

+ 87 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskSupervisionComplaintVo.java

@@ -0,0 +1,87 @@
+package org.dromara.risk.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.risk.domain.RiskSupervisionComplaint;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 监督抽检 - 平台客诉视图对象 risk_supervision_complaint
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = RiskSupervisionComplaint.class)
+public class RiskSupervisionComplaintVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "主键ID")
+    private Long id;
+
+    @ExcelProperty(value = "客诉编号")
+    private String complaintCode;
+
+    @ExcelProperty(value = "客诉来源")
+    private String source;
+
+    @ExcelProperty(value = "投诉人姓名")
+    private String complainantName;
+
+    @ExcelProperty(value = "投诉人电话")
+    private String complainantPhone;
+
+    @ExcelProperty(value = "涉事门店ID")
+    private Long storeId;
+
+    @ExcelProperty(value = "涉事门店")
+    private String storeName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "移交时间")
+    private Date transferTime;
+
+    @ExcelProperty(value = "客诉内容")
+    private String complaintContent;
+
+    @ExcelProperty(value = "附件URL")
+    private String attachmentUrl;
+
+    @ExcelProperty(value = "附件文件名")
+    private String attachmentName;
+
+    @ExcelProperty(value = "当前节点")
+    private Integer currentNode;
+
+    @ExcelProperty(value = "当前节点名称")
+    private String nodeName;
+
+    @ExcelProperty(value = "状态")
+    private String status;
+
+    @ExcelProperty(value = "备注")
+    private String remark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "创建时间")
+    private Date createTime;
+
+    @ExcelProperty(value = "更新人")
+    private Long updateBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "更新时间")
+    private Date updateTime;
+
+    /**
+     * 历史节点处理记录(仅详情接口填充)
+     */
+    private List<SupervisionComplaintNodeVo> nodeRecords;
+}

+ 72 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskSupervisionInspectionVo.java

@@ -0,0 +1,72 @@
+package org.dromara.risk.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.risk.domain.RiskSupervisionInspection;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 门店抽检单视图对象 risk_supervision_inspection
+ *
+ * @author ruoyi
+ */
+@Data
+@AutoMapper(target = RiskSupervisionInspection.class)
+public class RiskSupervisionInspectionVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @ExcelProperty(value = "主键ID")
+    private Long id;
+
+    @ExcelProperty(value = "抽检单号")
+    private String inspectionCode;
+
+    @ExcelProperty(value = "门店ID")
+    private Long storeId;
+
+    @ExcelProperty(value = "门店名称")
+    private String storeName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "抽检时间")
+    private Date inspectionTime;
+
+    @ExcelProperty(value = "抽检商品")
+    private String productName;
+
+    @ExcelProperty(value = "抽检结果")
+    private String inspectionResult;
+
+    @ExcelProperty(value = "不合格原因")
+    private String failReason;
+
+    @ExcelProperty(value = "审核状态")
+    private String status;
+
+    @ExcelProperty(value = "抽检人ID")
+    private Long inspectorId;
+
+    @ExcelProperty(value = "抽检人")
+    private String inspectorName;
+
+    @ExcelProperty(value = "备注")
+    private String remark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "创建时间")
+    private Date createTime;
+
+    @ExcelProperty(value = "更新人")
+    private Long updateBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ExcelProperty(value = "更新时间")
+    private Date updateTime;
+}

+ 55 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/SupervisionCaseNodeVo.java

@@ -0,0 +1,55 @@
+package org.dromara.risk.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 案件节点处理记录视图对象
+ *
+ * @author ruoyi
+ */
+@Data
+public class SupervisionCaseNodeVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 案件ID */
+    private Long caseId;
+
+    /** 节点编号 1-5 */
+    private Integer nodeCode;
+
+    /** 节点名称 */
+    private String nodeName;
+
+    /** 当前节点处理意见 */
+    private String opinion;
+
+    /** 凭证附件URL(MinIO) */
+    private String attachmentUrl;
+
+    /** 凭证附件原始文件名 */
+    private String attachmentName;
+
+    /** 处理人ID */
+    private Long handlerId;
+
+    /** 处理人 */
+    private String handlerName;
+
+    /** 处理时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date handleTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 55 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/SupervisionComplaintNodeVo.java

@@ -0,0 +1,55 @@
+package org.dromara.risk.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 监督抽检 - 客诉节点处理记录视图对象
+ *
+ * @author ruoyi
+ */
+@Data
+public class SupervisionComplaintNodeVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 客诉ID */
+    private Long complaintId;
+
+    /** 节点编号 1-7 */
+    private Integer nodeCode;
+
+    /** 节点名称 */
+    private String nodeName;
+
+    /** 当前节点处理意见 */
+    private String opinion;
+
+    /** 凭证附件URL */
+    private String attachmentUrl;
+
+    /** 凭证附件原始文件名 */
+    private String attachmentName;
+
+    /** 处理人ID */
+    private Long handlerId;
+
+    /** 处理人 */
+    private String handlerName;
+
+    /** 处理时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date handleTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 49 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/SupervisionStatsVo.java

@@ -0,0 +1,49 @@
+package org.dromara.risk.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 监督抽检 - 顶部数据看板聚合 VO
+ *
+ * <p>4 张卡片:今日待审核抽检 / 不合格待处理案件 / 待处理客诉移交 / 本月抽检合格率</p>
+ *
+ * @author ruoyi
+ */
+@Data
+public class SupervisionStatsVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 今日待审核抽检单据总量 */
+    private Long todayPending;
+
+    /** 不合格待处理案件数(状态=处置中) */
+    private Long pendingCases;
+
+    /** 待处理客诉移交数(状态=处置中) */
+    private Long pendingComplaints;
+
+    /** 本月抽检合格率(分子分母 + 百分比) */
+    private MonthPassRate monthPassRate;
+
+    /**
+     * 本月抽检合格率明细
+     */
+    @Data
+    public static class MonthPassRate implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+        /** 统计月份 yyyy-MM */
+        private String month;
+        /** 本月合格单数 */
+        private Long passCount;
+        /** 本月总单数 */
+        private Long totalCount;
+        /** 合格率(0-100, 保留 2 位小数) */
+        private Double passRate;
+    }
+}

+ 66 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionCaseMapper.java

@@ -0,0 +1,66 @@
+package org.dromara.risk.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.common.mybatis.annotation.DataColumn;
+import org.dromara.common.mybatis.annotation.DataPermission;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.risk.domain.RiskSupervisionCase;
+import org.dromara.risk.domain.vo.RiskSupervisionCaseVo;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 监督抽检 - 不合格案件主表 数据层
+ *
+ * @author ruoyi
+ */
+public interface RiskSupervisionCaseMapper extends BaseMapperPlus<RiskSupervisionCase, RiskSupervisionCaseVo> {
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default <P extends IPage<RiskSupervisionCaseVo>> P selectVoPage(IPage<RiskSupervisionCase> page, Wrapper<RiskSupervisionCase> wrapper) {
+        return selectVoPage(page, wrapper, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default List<RiskSupervisionCaseVo> selectVoList(Wrapper<RiskSupervisionCase> wrapper) {
+        return selectVoList(wrapper, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default RiskSupervisionCaseVo selectVoById(Serializable id) {
+        return selectVoById(id, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default List<RiskSupervisionCaseVo> selectVoByIds(Collection<? extends Serializable> idList) {
+        return selectVoByIds(idList, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    int updateById(@Param(Constants.ENTITY) RiskSupervisionCase entity);
+}

+ 14 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionCaseNodeMapper.java

@@ -0,0 +1,14 @@
+package org.dromara.risk.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.risk.domain.RiskSupervisionCaseNode;
+import org.dromara.risk.domain.vo.SupervisionCaseNodeVo;
+
+/**
+ * 监督抽检 - 案件节点处理记录 数据层
+ *
+ * @author ruoyi
+ */
+public interface RiskSupervisionCaseNodeMapper extends BaseMapperPlus<RiskSupervisionCaseNode, SupervisionCaseNodeVo> {
+
+}

+ 66 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionComplaintMapper.java

@@ -0,0 +1,66 @@
+package org.dromara.risk.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.common.mybatis.annotation.DataColumn;
+import org.dromara.common.mybatis.annotation.DataPermission;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.risk.domain.RiskSupervisionComplaint;
+import org.dromara.risk.domain.vo.RiskSupervisionComplaintVo;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 监督抽检 - 平台客诉主表 数据层
+ *
+ * @author ruoyi
+ */
+public interface RiskSupervisionComplaintMapper extends BaseMapperPlus<RiskSupervisionComplaint, RiskSupervisionComplaintVo> {
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default <P extends IPage<RiskSupervisionComplaintVo>> P selectVoPage(IPage<RiskSupervisionComplaint> page, Wrapper<RiskSupervisionComplaint> wrapper) {
+        return selectVoPage(page, wrapper, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default List<RiskSupervisionComplaintVo> selectVoList(Wrapper<RiskSupervisionComplaint> wrapper) {
+        return selectVoList(wrapper, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default RiskSupervisionComplaintVo selectVoById(Serializable id) {
+        return selectVoById(id, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default List<RiskSupervisionComplaintVo> selectVoByIds(Collection<? extends Serializable> idList) {
+        return selectVoByIds(idList, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    int updateById(@Param(Constants.ENTITY) RiskSupervisionComplaint entity);
+}

+ 14 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionComplaintNodeMapper.java

@@ -0,0 +1,14 @@
+package org.dromara.risk.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.risk.domain.RiskSupervisionComplaintNode;
+import org.dromara.risk.domain.vo.SupervisionComplaintNodeVo;
+
+/**
+ * 监督抽检 - 客诉节点处理记录 数据层
+ *
+ * @author ruoyi
+ */
+public interface RiskSupervisionComplaintNodeMapper extends BaseMapperPlus<RiskSupervisionComplaintNode, SupervisionComplaintNodeVo> {
+
+}

+ 66 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskSupervisionInspectionMapper.java

@@ -0,0 +1,66 @@
+package org.dromara.risk.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.common.mybatis.annotation.DataColumn;
+import org.dromara.common.mybatis.annotation.DataPermission;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.risk.domain.RiskSupervisionInspection;
+import org.dromara.risk.domain.vo.RiskSupervisionInspectionVo;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 监督抽检 - 门店抽检单 数据层
+ *
+ * @author ruoyi
+ */
+public interface RiskSupervisionInspectionMapper extends BaseMapperPlus<RiskSupervisionInspection, RiskSupervisionInspectionVo> {
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default <P extends IPage<RiskSupervisionInspectionVo>> P selectVoPage(IPage<RiskSupervisionInspection> page, Wrapper<RiskSupervisionInspection> wrapper) {
+        return selectVoPage(page, wrapper, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default List<RiskSupervisionInspectionVo> selectVoList(Wrapper<RiskSupervisionInspection> wrapper) {
+        return selectVoList(wrapper, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default RiskSupervisionInspectionVo selectVoById(Serializable id) {
+        return selectVoById(id, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default List<RiskSupervisionInspectionVo> selectVoByIds(Collection<? extends Serializable> idList) {
+        return selectVoByIds(idList, this.currentVoClass());
+    }
+
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    int updateById(@Param(Constants.ENTITY) RiskSupervisionInspection entity);
+}

+ 56 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskSupervisionCaseService.java

@@ -0,0 +1,56 @@
+package org.dromara.risk.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.risk.domain.bo.RiskSupervisionCaseBo;
+import org.dromara.risk.domain.vo.RiskSupervisionCaseVo;
+
+import java.util.List;
+
+/**
+ * 监督抽检 - 不合格案件 业务层
+ *
+ * @author ruoyi
+ */
+public interface IRiskSupervisionCaseService {
+
+    /**
+     * 查询案件详情(含历史节点)
+     */
+    RiskSupervisionCaseVo queryById(Long id);
+
+    /**
+     * 查询案件列表(分页)
+     */
+    TableDataInfo<RiskSupervisionCaseVo> queryPageList(RiskSupervisionCaseBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询案件列表(不分页,用于导出)
+     */
+    List<RiskSupervisionCaseVo> queryList(RiskSupervisionCaseBo bo);
+
+    /**
+     * 新增案件(从不合格抽检单发起)
+     */
+    int insertByBo(RiskSupervisionCaseBo bo);
+
+    /**
+     * 修改案件基础信息
+     */
+    int updateByBo(RiskSupervisionCaseBo bo);
+
+    /**
+     * 批量删除案件
+     */
+    int deleteWithValidByIds(List<Long> ids);
+
+    /**
+     * 处理案件节点(写节点表 + 主表 current_node + 1;达上限则 status='已闭环')
+     */
+    boolean processCaseNode(RiskSupervisionCaseBo bo);
+
+    /**
+     * 统计处置中案件数(用于顶部看板)
+     */
+    long countProcessing();
+}

+ 56 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskSupervisionComplaintService.java

@@ -0,0 +1,56 @@
+package org.dromara.risk.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.risk.domain.bo.RiskSupervisionComplaintBo;
+import org.dromara.risk.domain.vo.RiskSupervisionComplaintVo;
+
+import java.util.List;
+
+/**
+ * 监督抽检 - 平台客诉 业务层
+ *
+ * @author ruoyi
+ */
+public interface IRiskSupervisionComplaintService {
+
+    /**
+     * 查询客诉详情(含历史节点)
+     */
+    RiskSupervisionComplaintVo queryById(Long id);
+
+    /**
+     * 查询客诉列表(分页)
+     */
+    TableDataInfo<RiskSupervisionComplaintVo> queryPageList(RiskSupervisionComplaintBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询客诉列表(不分页,用于导出)
+     */
+    List<RiskSupervisionComplaintVo> queryList(RiskSupervisionComplaintBo bo);
+
+    /**
+     * 新增客诉(从"新增客诉"按钮触发)
+     */
+    int insertByBo(RiskSupervisionComplaintBo bo);
+
+    /**
+     * 修改客诉基础信息
+     */
+    int updateByBo(RiskSupervisionComplaintBo bo);
+
+    /**
+     * 批量删除客诉
+     */
+    int deleteWithValidByIds(List<Long> ids);
+
+    /**
+     * 处理客诉节点(写节点表 + 主表 current_node + 1;达上限则 status='已闭环')
+     */
+    boolean processComplaintNode(RiskSupervisionComplaintBo bo);
+
+    /**
+     * 统计处置中客诉数(用于顶部看板)
+     */
+    long countProcessing();
+}

+ 64 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskSupervisionInspectionService.java

@@ -0,0 +1,64 @@
+package org.dromara.risk.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.risk.domain.bo.RiskSupervisionInspectionBo;
+import org.dromara.risk.domain.vo.RiskSupervisionInspectionVo;
+import org.dromara.risk.domain.vo.SupervisionStatsVo;
+
+import java.util.List;
+
+/**
+ * 监督抽检 - 门店抽检单 业务层
+ *
+ * @author ruoyi
+ */
+public interface IRiskSupervisionInspectionService {
+
+    /**
+     * 查询抽检单详情
+     */
+    RiskSupervisionInspectionVo queryById(Long id);
+
+    /**
+     * 查询抽检单列表(分页)
+     */
+    TableDataInfo<RiskSupervisionInspectionVo> queryPageList(RiskSupervisionInspectionBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询抽检单列表(不分页,用于导出)
+     */
+    List<RiskSupervisionInspectionVo> queryList(RiskSupervisionInspectionBo bo);
+
+    /**
+     * 新增抽检单
+     */
+    int insertByBo(RiskSupervisionInspectionBo bo);
+
+    /**
+     * 修改抽检单
+     */
+    int updateByBo(RiskSupervisionInspectionBo bo);
+
+    /**
+     * 批量删除抽检单
+     */
+    int deleteWithValidByIds(List<Long> ids);
+
+    /**
+     * 审核抽检单(待审核 → 已审核)
+     */
+    boolean auditInspection(Long id);
+
+    /**
+     * 统计今日抽检单据总量(用于顶部看板)
+     */
+    long countTodayPending();
+
+    /**
+     * 计算指定月份的抽检合格率(用于顶部看板)
+     *
+     * @param ym 月份 yyyy-MM
+     */
+    SupervisionStatsVo.MonthPassRate calcMonthPassRate(String ym);
+}

+ 238 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskSupervisionCaseServiceImpl.java

@@ -0,0 +1,238 @@
+package org.dromara.risk.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.risk.domain.RiskSupervisionCase;
+import org.dromara.risk.domain.RiskSupervisionCaseNode;
+import org.dromara.risk.domain.RiskSupervisionInspection;
+import org.dromara.risk.domain.bo.RiskSupervisionCaseBo;
+import org.dromara.risk.domain.vo.RiskSupervisionCaseVo;
+import org.dromara.risk.domain.vo.SupervisionCaseNodeVo;
+import org.dromara.risk.mapper.RiskSupervisionCaseMapper;
+import org.dromara.risk.mapper.RiskSupervisionCaseNodeMapper;
+import org.dromara.risk.mapper.RiskSupervisionInspectionMapper;
+import org.dromara.risk.service.IRiskSupervisionCaseService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 监督抽检 - 不合格案件 业务层实现
+ *
+ * <p>5 节点流程:风险控制 → 异议复检 → 调查取证 → 调查报告 → 闭环管理</p>
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class RiskSupervisionCaseServiceImpl implements IRiskSupervisionCaseService {
+
+    /** 案件最大节点编号 */
+    private static final int MAX_NODE = 5;
+
+    /** 节点名称常量(索引 1-5) */
+    private static final String[] NODE_NAMES = {"", "风险控制", "异议复检", "调查取证", "调查报告", "闭环管理"};
+
+    /** 状态:处置中 */
+    private static final String STATUS_PROCESSING = "处置中";
+    /** 状态:已闭环 */
+    private static final String STATUS_CLOSED = "已闭环";
+
+    private final RiskSupervisionCaseMapper baseMapper;
+    private final RiskSupervisionCaseNodeMapper nodeMapper;
+    private final RiskSupervisionInspectionMapper inspectionMapper;
+
+    @Override
+    public RiskSupervisionCaseVo queryById(Long id) {
+        RiskSupervisionCaseVo vo = baseMapper.selectVoById(id);
+        if (vo == null) {
+            return null;
+        }
+        // 加载历史节点(按节点编号升序)
+        LambdaQueryWrapper<RiskSupervisionCaseNode> lqw = Wrappers.lambdaQuery();
+        lqw.eq(RiskSupervisionCaseNode::getCaseId, id);
+        lqw.orderByAsc(RiskSupervisionCaseNode::getNodeCode);
+        List<SupervisionCaseNodeVo> nodes = nodeMapper.selectVoList(lqw);
+        vo.setNodeRecords(nodes == null ? Collections.emptyList() : nodes);
+        return vo;
+    }
+
+    @Override
+    public TableDataInfo<RiskSupervisionCaseVo> queryPageList(RiskSupervisionCaseBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<RiskSupervisionCase> lqw = buildQueryWrapper(bo);
+        Page<RiskSupervisionCaseVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(page);
+    }
+
+    @Override
+    public List<RiskSupervisionCaseVo> queryList(RiskSupervisionCaseBo bo) {
+        LambdaQueryWrapper<RiskSupervisionCase> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<RiskSupervisionCase> buildQueryWrapper(RiskSupervisionCaseBo bo) {
+        LambdaQueryWrapper<RiskSupervisionCase> lqw = Wrappers.lambdaQuery();
+        lqw.eq(StringUtils.isNotBlank(bo.getCaseCode()), RiskSupervisionCase::getCaseCode, bo.getCaseCode());
+        lqw.eq(bo.getCurrentNode() != null, RiskSupervisionCase::getCurrentNode, bo.getCurrentNode());
+        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), RiskSupervisionCase::getStatus, bo.getStatus());
+        lqw.eq(StringUtils.isNotBlank(bo.getRiskLevel()), RiskSupervisionCase::getRiskLevel, bo.getRiskLevel());
+        lqw.like(StringUtils.isNotBlank(bo.getStoreName()), RiskSupervisionCase::getStoreName, bo.getStoreName());
+        lqw.like(StringUtils.isNotBlank(bo.getProductName()), RiskSupervisionCase::getProductName, bo.getProductName());
+        if (StringUtils.isNotBlank(bo.getKeyword())) {
+            lqw.and(w -> w.like(RiskSupervisionCase::getCaseCode, bo.getKeyword())
+                .or().like(RiskSupervisionCase::getInspectionCode, bo.getKeyword())
+                .or().like(RiskSupervisionCase::getStoreName, bo.getKeyword())
+                .or().like(RiskSupervisionCase::getProductName, bo.getKeyword()));
+        }
+        lqw.orderByDesc(RiskSupervisionCase::getCreateTime);
+        return lqw;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertByBo(RiskSupervisionCaseBo bo) {
+        // 必须有关联抽检单
+        Assert.notNull(bo.getInspectionId(), "关联抽检单ID不能为空");
+        RiskSupervisionInspection insp = inspectionMapper.selectById(bo.getInspectionId());
+        Assert.notNull(insp, "关联抽检单不存在: " + bo.getInspectionId());
+        if (!"抽检不合格".equals(insp.getInspectionResult())) {
+            throw new ServiceException("只有抽检不合格的单据才能发起案件处理");
+        }
+        // 冗余抽检单号/门店/产品
+        bo.setInspectionCode(insp.getInspectionCode());
+        if (StrUtil.isBlank(bo.getStoreName())) {
+            bo.setStoreName(insp.getStoreName());
+        }
+        if (bo.getStoreId() == null) {
+            bo.setStoreId(insp.getStoreId());
+        }
+        if (StrUtil.isBlank(bo.getProductName())) {
+            bo.setProductName(insp.getProductName());
+        }
+        RiskSupervisionCase add = MapstructUtils.convert(bo, RiskSupervisionCase.class);
+        add.setCaseCode(generateCaseCode());
+        // 初始节点 1:风险控制
+        add.setCurrentNode(1);
+        add.setNodeName(NODE_NAMES[1]);
+        add.setStatus(STATUS_PROCESSING);
+        // 处理人默认取当前登录用户
+        if (add.getHandlerId() == null) {
+            try {
+                add.setHandlerId(LoginHelper.getUserId());
+                add.setHandlerName(LoginHelper.getUsername());
+            } catch (Exception ignore) {
+                // 允许匿名
+            }
+        }
+        int row = baseMapper.insert(add);
+        bo.setId(add.getId());
+        return row;
+    }
+
+    @Override
+    public int updateByBo(RiskSupervisionCaseBo bo) {
+        RiskSupervisionCase update = MapstructUtils.convert(bo, RiskSupervisionCase.class);
+        return baseMapper.updateById(update);
+    }
+
+    @Override
+    public int deleteWithValidByIds(List<Long> ids) {
+        if (ObjectUtil.isEmpty(ids)) {
+            return 0;
+        }
+        // 删除前先删节点记录
+        for (Long id : ids) {
+            LambdaQueryWrapper<RiskSupervisionCaseNode> lqw = Wrappers.lambdaQuery();
+            lqw.eq(RiskSupervisionCaseNode::getCaseId, id);
+            nodeMapper.delete(lqw);
+        }
+        return baseMapper.deleteBatchIds(ids);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean processCaseNode(RiskSupervisionCaseBo bo) {
+        Assert.notNull(bo.getId(), "案件ID不能为空");
+        RiskSupervisionCase caze = baseMapper.selectById(bo.getId());
+        Assert.notNull(caze, "案件不存在: " + bo.getId());
+        if (STATUS_CLOSED.equals(caze.getStatus())) {
+            throw new ServiceException("案件已闭环,不允许再处理");
+        }
+        Assert.notNull(caze.getCurrentNode(), "案件当前节点为空");
+        int cur = caze.getCurrentNode();
+        if (cur < 1 || cur > MAX_NODE) {
+            throw new ServiceException("案件当前节点编号非法: " + cur);
+        }
+        // 处理意见必填
+        if (StrUtil.isBlank(bo.getOpinion())) {
+            throw new ServiceException("处理意见不能为空");
+        }
+        // 写节点记录
+        RiskSupervisionCaseNode node = new RiskSupervisionCaseNode();
+        node.setCaseId(caze.getId());
+        node.setNodeCode(cur);
+        node.setNodeName(NODE_NAMES[cur]);
+        node.setOpinion(bo.getOpinion());
+        node.setAttachmentUrl(bo.getAttachmentUrl());
+        node.setAttachmentName(bo.getAttachmentName());
+        try {
+            node.setHandlerId(LoginHelper.getUserId());
+            node.setHandlerName(LoginHelper.getUsername());
+        } catch (Exception ignore) {
+            // 允许匿名
+        }
+        node.setHandleTime(new Date());
+        nodeMapper.insert(node);
+
+        // 主表 current_node + 1, 达上限则闭环
+        RiskSupervisionCase update = new RiskSupervisionCase();
+        update.setId(caze.getId());
+        if (cur >= MAX_NODE) {
+            update.setCurrentNode(MAX_NODE);
+            update.setNodeName(NODE_NAMES[MAX_NODE]);
+            update.setStatus(STATUS_CLOSED);
+        } else {
+            int next = cur + 1;
+            update.setCurrentNode(next);
+            update.setNodeName(NODE_NAMES[next]);
+        }
+        int row = baseMapper.updateById(update);
+        log.info("案件节点处理完成 caseId={} from={} to={} status={}", caze.getId(), cur, update.getCurrentNode(), update.getStatus());
+        return row > 0;
+    }
+
+    @Override
+    public long countProcessing() {
+        LambdaQueryWrapper<RiskSupervisionCase> lqw = Wrappers.lambdaQuery();
+        lqw.eq(RiskSupervisionCase::getStatus, STATUS_PROCESSING);
+        return baseMapper.selectCount(lqw);
+    }
+
+    /**
+     * 生成案件编号:AJ+yyyyMMdd+4位随机序号
+     */
+    private String generateCaseCode() {
+        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        int seq = ThreadLocalRandom.current().nextInt(0, 10000);
+        return String.format("AJ%s%04d", date, seq);
+    }
+}

+ 217 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskSupervisionComplaintServiceImpl.java

@@ -0,0 +1,217 @@
+package org.dromara.risk.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.risk.domain.RiskSupervisionComplaint;
+import org.dromara.risk.domain.RiskSupervisionComplaintNode;
+import org.dromara.risk.domain.bo.RiskSupervisionComplaintBo;
+import org.dromara.risk.domain.vo.RiskSupervisionComplaintVo;
+import org.dromara.risk.domain.vo.SupervisionComplaintNodeVo;
+import org.dromara.risk.mapper.RiskSupervisionComplaintMapper;
+import org.dromara.risk.mapper.RiskSupervisionComplaintNodeMapper;
+import org.dromara.risk.service.IRiskSupervisionComplaintService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 监督抽检 - 平台客诉 业务层实现
+ *
+ * <p>7 节点流程:风险控制 → 调查取证 → 调查判断 → 部门协同 → 依规处理 → 溯源整改 → 闭环管理</p>
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class RiskSupervisionComplaintServiceImpl implements IRiskSupervisionComplaintService {
+
+    /** 客诉最大节点编号 */
+    private static final int MAX_NODE = 7;
+
+    /** 节点名称常量(索引 1-7) */
+    private static final String[] NODE_NAMES = {
+        "",
+        "风险控制",
+        "调查取证",
+        "调查判断",
+        "部门协同",
+        "依规处理",
+        "溯源整改",
+        "闭环管理"
+    };
+
+    /** 状态:处置中 */
+    private static final String STATUS_PROCESSING = "处置中";
+    /** 状态:已闭环 */
+    private static final String STATUS_CLOSED = "已闭环";
+
+    private final RiskSupervisionComplaintMapper baseMapper;
+    private final RiskSupervisionComplaintNodeMapper nodeMapper;
+
+    @Override
+    public RiskSupervisionComplaintVo queryById(Long id) {
+        RiskSupervisionComplaintVo vo = baseMapper.selectVoById(id);
+        if (vo == null) {
+            return null;
+        }
+        LambdaQueryWrapper<RiskSupervisionComplaintNode> lqw = Wrappers.lambdaQuery();
+        lqw.eq(RiskSupervisionComplaintNode::getComplaintId, id);
+        lqw.orderByAsc(RiskSupervisionComplaintNode::getNodeCode);
+        List<SupervisionComplaintNodeVo> nodes = nodeMapper.selectVoList(lqw);
+        vo.setNodeRecords(nodes == null ? Collections.emptyList() : nodes);
+        return vo;
+    }
+
+    @Override
+    public TableDataInfo<RiskSupervisionComplaintVo> queryPageList(RiskSupervisionComplaintBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<RiskSupervisionComplaint> lqw = buildQueryWrapper(bo);
+        Page<RiskSupervisionComplaintVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(page);
+    }
+
+    @Override
+    public List<RiskSupervisionComplaintVo> queryList(RiskSupervisionComplaintBo bo) {
+        LambdaQueryWrapper<RiskSupervisionComplaint> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<RiskSupervisionComplaint> buildQueryWrapper(RiskSupervisionComplaintBo bo) {
+        LambdaQueryWrapper<RiskSupervisionComplaint> lqw = Wrappers.lambdaQuery();
+        lqw.eq(StringUtils.isNotBlank(bo.getComplaintCode()), RiskSupervisionComplaint::getComplaintCode, bo.getComplaintCode());
+        lqw.eq(StringUtils.isNotBlank(bo.getSource()), RiskSupervisionComplaint::getSource, bo.getSource());
+        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), RiskSupervisionComplaint::getStatus, bo.getStatus());
+        lqw.eq(bo.getCurrentNode() != null, RiskSupervisionComplaint::getCurrentNode, bo.getCurrentNode());
+        lqw.like(StringUtils.isNotBlank(bo.getStoreName()), RiskSupervisionComplaint::getStoreName, bo.getStoreName());
+        lqw.like(StringUtils.isNotBlank(bo.getComplainantName()), RiskSupervisionComplaint::getComplainantName, bo.getComplainantName());
+        if (StringUtils.isNotBlank(bo.getKeyword())) {
+            lqw.and(w -> w.like(RiskSupervisionComplaint::getComplaintCode, bo.getKeyword())
+                .or().like(RiskSupervisionComplaint::getStoreName, bo.getKeyword())
+                .or().like(RiskSupervisionComplaint::getComplainantName, bo.getKeyword())
+                .or().like(RiskSupervisionComplaint::getComplaintContent, bo.getKeyword()));
+        }
+        lqw.orderByDesc(RiskSupervisionComplaint::getTransferTime);
+        return lqw;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertByBo(RiskSupervisionComplaintBo bo) {
+        RiskSupervisionComplaint add = MapstructUtils.convert(bo, RiskSupervisionComplaint.class);
+        add.setComplaintCode(generateComplaintCode());
+        // 初始节点 1:风险控制
+        add.setCurrentNode(1);
+        add.setNodeName(NODE_NAMES[1]);
+        add.setStatus(STATUS_PROCESSING);
+        if (add.getTransferTime() == null) {
+            add.setTransferTime(new Date());
+        }
+        int row = baseMapper.insert(add);
+        bo.setId(add.getId());
+        return row;
+    }
+
+    @Override
+    public int updateByBo(RiskSupervisionComplaintBo bo) {
+        RiskSupervisionComplaint update = MapstructUtils.convert(bo, RiskSupervisionComplaint.class);
+        return baseMapper.updateById(update);
+    }
+
+    @Override
+    public int deleteWithValidByIds(List<Long> ids) {
+        if (ObjectUtil.isEmpty(ids)) {
+            return 0;
+        }
+        for (Long id : ids) {
+            LambdaQueryWrapper<RiskSupervisionComplaintNode> lqw = Wrappers.lambdaQuery();
+            lqw.eq(RiskSupervisionComplaintNode::getComplaintId, id);
+            nodeMapper.delete(lqw);
+        }
+        return baseMapper.deleteBatchIds(ids);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean processComplaintNode(RiskSupervisionComplaintBo bo) {
+        Assert.notNull(bo.getId(), "客诉ID不能为空");
+        RiskSupervisionComplaint cpt = baseMapper.selectById(bo.getId());
+        Assert.notNull(cpt, "客诉不存在: " + bo.getId());
+        if (STATUS_CLOSED.equals(cpt.getStatus())) {
+            throw new ServiceException("客诉已闭环,不允许再处理");
+        }
+        Assert.notNull(cpt.getCurrentNode(), "客诉当前节点为空");
+        int cur = cpt.getCurrentNode();
+        if (cur < 1 || cur > MAX_NODE) {
+            throw new ServiceException("客诉当前节点编号非法: " + cur);
+        }
+        if (StrUtil.isBlank(bo.getOpinion())) {
+            throw new ServiceException("处理意见不能为空");
+        }
+        // 写节点记录
+        RiskSupervisionComplaintNode node = new RiskSupervisionComplaintNode();
+        node.setComplaintId(cpt.getId());
+        node.setNodeCode(cur);
+        node.setNodeName(NODE_NAMES[cur]);
+        node.setOpinion(bo.getOpinion());
+        node.setAttachmentUrl(bo.getProcessAttachmentUrl());
+        node.setAttachmentName(bo.getProcessAttachmentName());
+        try {
+            node.setHandlerId(LoginHelper.getUserId());
+            node.setHandlerName(LoginHelper.getUsername());
+        } catch (Exception ignore) {
+            // 允许匿名
+        }
+        node.setHandleTime(new Date());
+        nodeMapper.insert(node);
+
+        // 主表 current_node + 1, 达上限则闭环
+        RiskSupervisionComplaint update = new RiskSupervisionComplaint();
+        update.setId(cpt.getId());
+        if (cur >= MAX_NODE) {
+            update.setCurrentNode(MAX_NODE);
+            update.setNodeName(NODE_NAMES[MAX_NODE]);
+            update.setStatus(STATUS_CLOSED);
+        } else {
+            int next = cur + 1;
+            update.setCurrentNode(next);
+            update.setNodeName(NODE_NAMES[next]);
+        }
+        int row = baseMapper.updateById(update);
+        log.info("客诉节点处理完成 complaintId={} from={} to={} status={}", cpt.getId(), cur, update.getCurrentNode(), update.getStatus());
+        return row > 0;
+    }
+
+    @Override
+    public long countProcessing() {
+        LambdaQueryWrapper<RiskSupervisionComplaint> lqw = Wrappers.lambdaQuery();
+        lqw.eq(RiskSupervisionComplaint::getStatus, STATUS_PROCESSING);
+        return baseMapper.selectCount(lqw);
+    }
+
+    /**
+     * 生成客诉编号:TS+yyyyMMdd+4位随机序号
+     */
+    private String generateComplaintCode() {
+        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        int seq = ThreadLocalRandom.current().nextInt(0, 10000);
+        return String.format("TS%s%04d", date, seq);
+    }
+}

+ 185 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskSupervisionInspectionServiceImpl.java

@@ -0,0 +1,185 @@
+package org.dromara.risk.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.risk.domain.RiskSupervisionInspection;
+import org.dromara.risk.domain.bo.RiskSupervisionInspectionBo;
+import org.dromara.risk.domain.vo.RiskSupervisionInspectionVo;
+import org.dromara.risk.domain.vo.SupervisionStatsVo;
+import org.dromara.risk.mapper.RiskSupervisionInspectionMapper;
+import org.dromara.risk.service.IRiskSupervisionInspectionService;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 监督抽检 - 门店抽检单 业务层实现
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class RiskSupervisionInspectionServiceImpl implements IRiskSupervisionInspectionService {
+
+    /** 抽检结果:合格 */
+    private static final String RESULT_PASS = "抽检合格";
+    /** 抽检结果:不合格 */
+    private static final String RESULT_FAIL = "抽检不合格";
+
+    /** 状态:待审核 */
+    private static final String STATUS_PENDING = "待审核";
+    /** 状态:已审核 */
+    private static final String STATUS_AUDITED = "已审核";
+
+    private final RiskSupervisionInspectionMapper baseMapper;
+
+    @Override
+    public RiskSupervisionInspectionVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    @Override
+    public TableDataInfo<RiskSupervisionInspectionVo> queryPageList(RiskSupervisionInspectionBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<RiskSupervisionInspection> lqw = buildQueryWrapper(bo);
+        Page<RiskSupervisionInspectionVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(page);
+    }
+
+    @Override
+    public List<RiskSupervisionInspectionVo> queryList(RiskSupervisionInspectionBo bo) {
+        LambdaQueryWrapper<RiskSupervisionInspection> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<RiskSupervisionInspection> buildQueryWrapper(RiskSupervisionInspectionBo bo) {
+        LambdaQueryWrapper<RiskSupervisionInspection> lqw = Wrappers.lambdaQuery();
+        lqw.eq(StringUtils.isNotBlank(bo.getInspectionCode()), RiskSupervisionInspection::getInspectionCode, bo.getInspectionCode());
+        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), RiskSupervisionInspection::getStatus, bo.getStatus());
+        lqw.eq(StringUtils.isNotBlank(bo.getInspectionResult()), RiskSupervisionInspection::getInspectionResult, bo.getInspectionResult());
+        lqw.like(StringUtils.isNotBlank(bo.getStoreName()), RiskSupervisionInspection::getStoreName, bo.getStoreName());
+        lqw.like(StringUtils.isNotBlank(bo.getProductName()), RiskSupervisionInspection::getProductName, bo.getProductName());
+        // 关键词搜索:抽检单号/门店/商品
+        if (StringUtils.isNotBlank(bo.getKeyword())) {
+            lqw.and(w -> w.like(RiskSupervisionInspection::getInspectionCode, bo.getKeyword())
+                .or().like(RiskSupervisionInspection::getStoreName, bo.getKeyword())
+                .or().like(RiskSupervisionInspection::getProductName, bo.getKeyword()));
+        }
+        // 默认按抽检时间倒序
+        lqw.orderByDesc(RiskSupervisionInspection::getInspectionTime);
+        return lqw;
+    }
+
+    @Override
+    public int insertByBo(RiskSupervisionInspectionBo bo) {
+        RiskSupervisionInspection add = MapstructUtils.convert(bo, RiskSupervisionInspection.class);
+        if (StrUtil.isBlank(add.getInspectionCode())) {
+            add.setInspectionCode(generateInspectionCode());
+        }
+        if (StrUtil.isBlank(add.getStatus())) {
+            add.setStatus(STATUS_PENDING);
+        }
+        if (StrUtil.isBlank(add.getInspectionResult())) {
+            add.setInspectionResult(RESULT_PASS);
+        }
+        if (add.getInspectorId() == null) {
+            try {
+                add.setInspectorId(LoginHelper.getUserId());
+                add.setInspectorName(LoginHelper.getUsername());
+            } catch (Exception ignore) {
+                // 允许匿名创建
+            }
+        }
+        int row = baseMapper.insert(add);
+        bo.setId(add.getId());
+        return row;
+    }
+
+    @Override
+    public int updateByBo(RiskSupervisionInspectionBo bo) {
+        RiskSupervisionInspection update = MapstructUtils.convert(bo, RiskSupervisionInspection.class);
+        return baseMapper.updateById(update);
+    }
+
+    @Override
+    public int deleteWithValidByIds(List<Long> ids) {
+        if (ObjectUtil.isEmpty(ids)) {
+            return 0;
+        }
+        return baseMapper.deleteBatchIds(ids);
+    }
+
+    @Override
+    public boolean auditInspection(Long id) {
+        Assert.notNull(id, "抽检单ID不能为空");
+        RiskSupervisionInspection insp = baseMapper.selectById(id);
+        Assert.notNull(insp, "抽检单不存在: " + id);
+        if (!STATUS_PENDING.equals(insp.getStatus())) {
+            throw new ServiceException("当前状态(" + insp.getStatus() + ")不允许审核");
+        }
+        RiskSupervisionInspection update = new RiskSupervisionInspection();
+        update.setId(id);
+        update.setStatus(STATUS_AUDITED);
+        int row = baseMapper.updateById(update);
+        log.info("抽检单审核完成 id={} inspectionCode={}", id, insp.getInspectionCode());
+        return row > 0;
+    }
+
+    /**
+     * 生成抽检单号:JC+yyyyMMdd+4位随机序号
+     */
+    private String generateInspectionCode() {
+        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        int seq = ThreadLocalRandom.current().nextInt(0, 10000);
+        return String.format("JC%s%04d", date, seq);
+    }
+
+    @Override
+    public long countTodayPending() {
+        LambdaQueryWrapper<RiskSupervisionInspection> lqw = Wrappers.lambdaQuery();
+        lqw.apply("DATE(inspection_time) = CURDATE()");
+        return baseMapper.selectCount(lqw);
+    }
+
+    @Override
+    public SupervisionStatsVo.MonthPassRate calcMonthPassRate(String ym) {
+        SupervisionStatsVo.MonthPassRate rate = new SupervisionStatsVo.MonthPassRate();
+        rate.setMonth(ym);
+        // 本月总单数
+        LambdaQueryWrapper<RiskSupervisionInspection> totalQw = Wrappers.lambdaQuery();
+        totalQw.apply("DATE_FORMAT(inspection_time, '%Y-%m') = {0}", ym);
+        long total = baseMapper.selectCount(totalQw);
+        rate.setTotalCount(total);
+        // 本月合格单数
+        LambdaQueryWrapper<RiskSupervisionInspection> passQw = Wrappers.lambdaQuery();
+        passQw.apply("DATE_FORMAT(inspection_time, '%Y-%m') = {0}", ym);
+        passQw.eq(RiskSupervisionInspection::getInspectionResult, RESULT_PASS);
+        long pass = baseMapper.selectCount(passQw);
+        rate.setPassCount(pass);
+        // 计算百分比
+        if (total <= 0) {
+            rate.setPassRate(0.0);
+        } else {
+            double r = (double) pass * 100.0 / (double) total;
+            rate.setPassRate(BigDecimal.valueOf(r).setScale(2, RoundingMode.HALF_UP).doubleValue());
+        }
+        return rate;
+    }
+}

+ 245 - 0
sql/risk_supervision.sql

@@ -0,0 +1,245 @@
+-- ============================================================
+-- 监督抽检 / 平台客诉 模块 - 数据库升级脚本(幂等)
+-- 适用: MySQL 5.7+ / 8.0
+-- 表: 5 张(抽检单 / 案件主表 / 案件节点 / 客诉主表 / 客诉节点)
+-- 字典: 6 个(inspection_result / inspection_status / risk_level /
+--              complaint_source / case_status / complaint_status)
+-- 说明: 使用 INFORMATION_SCHEMA 兼容低版本 MySQL,所有 IF NOT EXISTS
+--       风格的 DDL,支持重复执行不报错
+-- ============================================================
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ============================================================
+-- 1. 字典(6 个)
+-- ============================================================
+
+-- 1.1 抽检结果
+INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, create_time, update_by, update_time)
+SELECT '抽检结果', 'risk_supervision_inspection_result', '0', '门店抽检结果(合格/不合格)', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'risk_supervision_inspection_result');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 1, '抽检合格', '抽检合格', 'risk_supervision_inspection_result', '', 'success', 'Y', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_inspection_result' AND dict_value = '抽检合格');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 2, '抽检不合格', '抽检不合格', 'risk_supervision_inspection_result', '', 'danger', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_inspection_result' AND dict_value = '抽检不合格');
+
+-- 1.2 抽检审核状态
+INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, create_time, update_by, update_time)
+SELECT '抽检审核状态', 'risk_supervision_inspection_status', '0', '门店抽检单审核状态', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'risk_supervision_inspection_status');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 1, '待审核', '待审核', 'risk_supervision_inspection_status', '', 'warning', 'Y', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_inspection_status' AND dict_value = '待审核');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 2, '已审核', '已审核', 'risk_supervision_inspection_status', '', 'success', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_inspection_status' AND dict_value = '已审核');
+
+-- 1.3 风险等级
+INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, create_time, update_by, update_time)
+SELECT '风险等级', 'risk_supervision_risk_level', '0', '案件风险等级', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'risk_supervision_risk_level');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 1, '高风险', '高风险', 'risk_supervision_risk_level', '', 'danger', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_risk_level' AND dict_value = '高风险');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 2, '中风险', '中风险', 'risk_supervision_risk_level', '', 'warning', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_risk_level' AND dict_value = '中风险');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 3, '低风险', '低风险', 'risk_supervision_risk_level', '', 'info', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_risk_level' AND dict_value = '低风险');
+
+-- 1.4 客诉来源
+INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, create_time, update_by, update_time)
+SELECT '客诉来源', 'risk_supervision_complaint_source', '0', '平台客诉来源', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'risk_supervision_complaint_source');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 1, '消费者投诉', '消费者投诉', 'risk_supervision_complaint_source', '', 'primary', 'Y', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_complaint_source' AND dict_value = '消费者投诉');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 2, '媒体舆情', '媒体舆情', 'risk_supervision_complaint_source', '', 'warning', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_complaint_source' AND dict_value = '媒体舆情');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 3, '12315平台', '12315平台', 'risk_supervision_complaint_source', '', 'danger', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_complaint_source' AND dict_value = '12315平台');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 4, '其他来源', '其他来源', 'risk_supervision_complaint_source', '', 'info', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_complaint_source' AND dict_value = '其他来源');
+
+-- 1.5 案件状态
+INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, create_time, update_by, update_time)
+SELECT '案件状态', 'risk_supervision_case_status', '0', '不合格案件处理状态', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'risk_supervision_case_status');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 1, '处置中', '处置中', 'risk_supervision_case_status', '', 'warning', 'Y', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_case_status' AND dict_value = '处置中');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 2, '已闭环', '已闭环', 'risk_supervision_case_status', '', 'success', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_case_status' AND dict_value = '已闭环');
+
+-- 1.6 客诉状态
+INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, create_time, update_by, update_time)
+SELECT '客诉状态', 'risk_supervision_complaint_status', '0', '平台客诉处理状态', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'risk_supervision_complaint_status');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 1, '处置中', '处置中', 'risk_supervision_complaint_status', '', 'warning', 'Y', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_complaint_status' AND dict_value = '处置中');
+
+INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, remark, create_by, create_time, update_by, update_time)
+SELECT 2, '已闭环', '已闭环', 'risk_supervision_complaint_status', '', 'success', 'N', '0', '', 'admin', NOW(), 'admin', NOW()
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'risk_supervision_complaint_status' AND dict_value = '已闭环');
+
+-- ============================================================
+-- 2. 业务表(5 张)
+-- ============================================================
+
+-- 2.1 门店抽检单
+CREATE TABLE IF NOT EXISTS `risk_supervision_inspection` (
+    `id`               BIGINT          NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `inspection_code`  VARCHAR(50)     NOT NULL COMMENT '抽检单号 JC+yyyyMMdd+4位序号',
+    `store_id`         BIGINT          DEFAULT NULL COMMENT '门店ID',
+    `store_name`       VARCHAR(200)    DEFAULT NULL COMMENT '门店名称',
+    `inspection_time`  DATETIME        DEFAULT NULL COMMENT '抽检时间',
+    `product_name`     VARCHAR(200)    DEFAULT NULL COMMENT '抽检商品',
+    `inspection_result` VARCHAR(20)    DEFAULT NULL COMMENT '抽检结果(字典:抽检合格/抽检不合格)',
+    `fail_reason`      VARCHAR(1000)   DEFAULT NULL COMMENT '不合格原因',
+    `status`           VARCHAR(20)     DEFAULT '待审核' COMMENT '状态(字典:待审核/已审核)',
+    `inspector_id`     BIGINT          DEFAULT NULL COMMENT '抽检人ID',
+    `inspector_name`   VARCHAR(50)     DEFAULT NULL COMMENT '抽检人姓名',
+    `remark`           VARCHAR(500)    DEFAULT NULL COMMENT '备注',
+    `tenant_id`        VARCHAR(20)     DEFAULT '000000' COMMENT '租户ID',
+    `create_dept`      BIGINT          DEFAULT NULL COMMENT '创建部门',
+    `create_by`        BIGINT          DEFAULT NULL COMMENT '创建人',
+    `create_time`      DATETIME        DEFAULT NULL COMMENT '创建时间',
+    `update_by`        BIGINT          DEFAULT NULL COMMENT '更新人',
+    `update_time`      DATETIME        DEFAULT NULL COMMENT '更新时间',
+    `del_flag`         CHAR(1)         DEFAULT '0' COMMENT '删除标志(0正常 1删除)',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_inspection_code` (`inspection_code`),
+    KEY `idx_inspection_time` (`inspection_time`),
+    KEY `idx_status_result` (`status`, `inspection_result`),
+    KEY `idx_store_id` (`store_id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='监督抽检 - 门店抽检单';
+
+-- 2.2 不合格案件主表
+CREATE TABLE IF NOT EXISTS `risk_supervision_case` (
+    `id`               BIGINT          NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `case_code`        VARCHAR(50)     NOT NULL COMMENT '案件编号 AJ+yyyyMMdd+4位序号',
+    `inspection_id`    BIGINT          DEFAULT NULL COMMENT '关联抽检单ID',
+    `inspection_code`  VARCHAR(50)     DEFAULT NULL COMMENT '冗余抽检单号',
+    `store_id`         BIGINT          DEFAULT NULL COMMENT '涉事门店ID',
+    `store_name`       VARCHAR(200)    DEFAULT NULL COMMENT '涉事门店',
+    `product_name`     VARCHAR(200)    DEFAULT NULL COMMENT '不合格产品',
+    `current_node`     INT             DEFAULT 1 COMMENT '当前节点(1-5)',
+    `node_name`        VARCHAR(50)     DEFAULT NULL COMMENT '当前节点名称(冗余)',
+    `risk_level`       VARCHAR(20)     DEFAULT NULL COMMENT '风险等级(字典:高/中/低风险)',
+    `risk_desc`        VARCHAR(2000)   DEFAULT NULL COMMENT '风险描述',
+    `control_measures` VARCHAR(2000)   DEFAULT NULL COMMENT '控制措施',
+    `handler_id`       BIGINT          DEFAULT NULL COMMENT '处理人ID',
+    `handler_name`     VARCHAR(50)     DEFAULT NULL COMMENT '处理人',
+    `status`           VARCHAR(20)     DEFAULT '处置中' COMMENT '状态(字典:处置中/已闭环)',
+    `remark`           VARCHAR(500)    DEFAULT NULL COMMENT '备注',
+    `tenant_id`        VARCHAR(20)     DEFAULT '000000' COMMENT '租户ID',
+    `create_dept`      BIGINT          DEFAULT NULL COMMENT '创建部门',
+    `create_by`        BIGINT          DEFAULT NULL COMMENT '创建人',
+    `create_time`      DATETIME        DEFAULT NULL COMMENT '创建时间',
+    `update_by`        BIGINT          DEFAULT NULL COMMENT '更新人',
+    `update_time`      DATETIME        DEFAULT NULL COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_case_code` (`case_code`),
+    KEY `idx_inspection_id` (`inspection_id`),
+    KEY `idx_status_node` (`status`, `current_node`),
+    KEY `idx_store_id` (`store_id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='监督抽检 - 不合格案件主表';
+
+-- 2.3 案件节点处理记录
+CREATE TABLE IF NOT EXISTS `risk_supervision_case_node` (
+    `id`               BIGINT          NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `case_id`          BIGINT          NOT NULL COMMENT '案件ID',
+    `node_code`        INT             NOT NULL COMMENT '节点编号 1-5',
+    `node_name`        VARCHAR(50)     DEFAULT NULL COMMENT '节点名称',
+    `opinion`          VARCHAR(2000)   DEFAULT NULL COMMENT '当前节点处理意见',
+    `attachment_url`   VARCHAR(500)    DEFAULT NULL COMMENT '凭证附件URL(MinIO)',
+    `attachment_name`  VARCHAR(200)    DEFAULT NULL COMMENT '凭证附件原始文件名',
+    `handler_id`       BIGINT          DEFAULT NULL COMMENT '处理人ID',
+    `handler_name`     VARCHAR(50)     DEFAULT NULL COMMENT '处理人',
+    `handle_time`      DATETIME        DEFAULT NULL COMMENT '处理时间',
+    `tenant_id`        VARCHAR(20)     DEFAULT '000000' COMMENT '租户ID',
+    `create_dept`      BIGINT          DEFAULT NULL COMMENT '创建部门',
+    `create_by`        BIGINT          DEFAULT NULL COMMENT '创建人',
+    `create_time`      DATETIME        DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_case_id_node` (`case_id`, `node_code`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='监督抽检 - 案件节点处理记录';
+
+-- 2.4 平台客诉主表
+CREATE TABLE IF NOT EXISTS `risk_supervision_complaint` (
+    `id`                BIGINT          NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `complaint_code`    VARCHAR(50)     NOT NULL COMMENT '客诉编号 TS+yyyyMMdd+4位序号',
+    `source`            VARCHAR(50)     DEFAULT NULL COMMENT '客诉来源(字典)',
+    `complainant_name`  VARCHAR(50)     DEFAULT NULL COMMENT '投诉人姓名',
+    `complainant_phone` VARCHAR(20)     DEFAULT NULL COMMENT '投诉人电话',
+    `store_id`          BIGINT          DEFAULT NULL COMMENT '涉事门店ID',
+    `store_name`        VARCHAR(200)    DEFAULT NULL COMMENT '涉事门店',
+    `transfer_time`     DATETIME        DEFAULT NULL COMMENT '移交时间',
+    `complaint_content` TEXT            COMMENT '客诉内容',
+    `attachment_url`    VARCHAR(500)    DEFAULT NULL COMMENT '附件URL(MinIO)',
+    `attachment_name`   VARCHAR(200)    DEFAULT NULL COMMENT '附件原始文件名',
+    `current_node`      INT             DEFAULT 1 COMMENT '当前节点(1-7)',
+    `node_name`         VARCHAR(50)     DEFAULT NULL COMMENT '当前节点名称',
+    `status`            VARCHAR(20)     DEFAULT '处置中' COMMENT '状态(字典:处置中/已闭环)',
+    `remark`            VARCHAR(500)    DEFAULT NULL COMMENT '备注',
+    `tenant_id`         VARCHAR(20)     DEFAULT '000000' COMMENT '租户ID',
+    `create_dept`       BIGINT          DEFAULT NULL COMMENT '创建部门',
+    `create_by`         BIGINT          DEFAULT NULL COMMENT '创建人',
+    `create_time`       DATETIME        DEFAULT NULL COMMENT '创建时间',
+    `update_by`         BIGINT          DEFAULT NULL COMMENT '更新人',
+    `update_time`       DATETIME        DEFAULT NULL COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_complaint_code` (`complaint_code`),
+    KEY `idx_status_node` (`status`, `current_node`),
+    KEY `idx_transfer_time` (`transfer_time`),
+    KEY `idx_store_id` (`store_id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='监督抽检 - 平台客诉主表';
+
+-- 2.5 客诉节点处理记录
+CREATE TABLE IF NOT EXISTS `risk_supervision_complaint_node` (
+    `id`               BIGINT          NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `complaint_id`     BIGINT          NOT NULL COMMENT '客诉ID',
+    `node_code`        INT             NOT NULL COMMENT '节点编号 1-7',
+    `node_name`        VARCHAR(50)     DEFAULT NULL COMMENT '节点名称',
+    `opinion`          VARCHAR(2000)   DEFAULT NULL COMMENT '当前环节处理意见',
+    `attachment_url`   VARCHAR(500)    DEFAULT NULL COMMENT '凭证附件URL(MinIO)',
+    `attachment_name`  VARCHAR(200)    DEFAULT NULL COMMENT '凭证附件原始文件名',
+    `handler_id`       BIGINT          DEFAULT NULL COMMENT '处理人ID',
+    `handler_name`     VARCHAR(50)     DEFAULT NULL COMMENT '处理人',
+    `handle_time`      DATETIME        DEFAULT NULL COMMENT '处理时间',
+    `tenant_id`        VARCHAR(20)     DEFAULT '000000' COMMENT '租户ID',
+    `create_dept`      BIGINT          DEFAULT NULL COMMENT '创建部门',
+    `create_by`        BIGINT          DEFAULT NULL COMMENT '创建人',
+    `create_time`      DATETIME        DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_complaint_id_node` (`complaint_id`, `node_code`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='监督抽检 - 客诉节点处理记录';
+
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- ============================================================
+-- 完成 - 监督抽检 / 平台客诉 模块升级
+-- ============================================================

+ 83 - 0
sql/risk_supervision_menu.sql

@@ -0,0 +1,83 @@
+-- ============================================================
+-- 监督抽检 / 平台客诉 - 菜单权限 SQL(幂等)
+-- ============================================================
+-- 菜单路径: 风控平台 -> 监督抽检/平台客诉
+-- 权限标识:
+--   risk:supervision:inspection:list / add / edit / remove / export
+--   risk:supervision:case:list / process
+--   risk:supervision:complaint:list / add / process
+-- ============================================================
+
+SET NAMES utf8mb4;
+
+-- ----------------------------------------------------------
+-- 1. 父菜单(插入到 风控平台 菜单下)
+-- ----------------------------------------------------------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '监督抽检/平台客诉', (
+        SELECT menu_id FROM sys_menu WHERE menu_name = '风控平台' AND menu_type = 'M' LIMIT 1
+    ), 12, 'supervision', 'risk/supervision/index', 1, 0, 'C', '0', '0', 'risk:supervision:list', 'monitor', 'admin', NOW(), 'admin', NOW(), '监督抽检与平台客诉目录'
+WHERE NOT EXISTS (
+    SELECT 1 FROM sys_menu WHERE menu_name = '监督抽检/平台客诉' AND menu_type = 'C'
+);
+
+SET @supervisionMenuId = LAST_INSERT_ID();
+
+-- 若已存在则取已有的 ID
+SET @supervisionMenuId = COALESCE(
+    (SELECT menu_id FROM sys_menu WHERE menu_name = '监督抽检/平台客诉' AND menu_type = 'C' LIMIT 1),
+    @supervisionMenuId
+);
+
+-- ----------------------------------------------------------
+-- 2. 抽检单按钮权限(5 个)
+-- ----------------------------------------------------------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '抽检登记查询', @supervisionMenuId, 1, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:inspection:list', '#', 'admin', NOW(), 'admin', NOW(), '门店抽检当日审核登记查询'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:inspection:list');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '抽检登记新增', @supervisionMenuId, 2, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:inspection:add', '#', 'admin', NOW(), 'admin', NOW(), '新增门店抽检登记'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:inspection:add');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '抽检登记编辑', @supervisionMenuId, 3, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:inspection:edit', '#', 'admin', NOW(), 'admin', NOW(), '编辑门店抽检登记'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:inspection:edit');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '抽检登记删除', @supervisionMenuId, 4, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:inspection:remove', '#', 'admin', NOW(), 'admin', NOW(), '删除门店抽检登记'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:inspection:remove');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '抽检登记导出', @supervisionMenuId, 5, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:inspection:export', '#', 'admin', NOW(), 'admin', NOW(), '导出抽检登记'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:inspection:export');
+
+-- ----------------------------------------------------------
+-- 3. 案件按钮权限(2 个)
+-- ----------------------------------------------------------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '案件查询', @supervisionMenuId, 6, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:case:list', '#', 'admin', NOW(), 'admin', NOW(), '不合格案件查询'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:case:list');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '案件处理', @supervisionMenuId, 7, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:case:process', '#', 'admin', NOW(), 'admin', NOW(), '处理不合格案件各节点'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:case:process');
+
+-- ----------------------------------------------------------
+-- 4. 客诉按钮权限(3 个)
+-- ----------------------------------------------------------
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '客诉查询', @supervisionMenuId, 8, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:complaint:list', '#', 'admin', NOW(), 'admin', NOW(), '平台客诉查询'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:complaint:list');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '客诉新增', @supervisionMenuId, 9, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:complaint:add', '#', 'admin', NOW(), 'admin', NOW(), '新增平台客诉登记'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:complaint:add');
+
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+SELECT '客诉处理', @supervisionMenuId, 10, '#', '', 1, 0, 'F', '0', '0', 'risk:supervision:complaint:process', '#', 'admin', NOW(), 'admin', NOW(), '处理平台客诉各环节'
+WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'risk:supervision:complaint:process');
+
+-- ============================================================
+-- 完成
+-- ============================================================