Skip to content

审计报告任务配置与执行流程说明文档 ​

文档说明 ​

本文档详细说明了审计报告任务从配置、定时触发、AI智能分析到结果生成的完整业务流程。该业务涉及 pigx-audit 和 pigx-ai 两个微服务之间的复杂交互,包括多次服务调用和数据回写。


一、业务概述 ​

1.1 功能简介 ​

审计报告任务配置功能允许用户创建定时或单次执行的审计任务。任务启动后,系统会在指定时间自动触发,通过 AI 服务对企业上传的各类文档资料进行合规性审查,识别潜在风险,并生成结构化的审计报告。

1.2 核心特性 ​

  • 定时任务调度:支持单次执行和周期性执行(日/周/月/季/年)
  • AI 智能分析:基于 LangChain4j 和 LangGraph 的状态机工作流,自动分析文档合规性
  • 风险识别:根据外部法规、内部规范和公司制度,识别文档中的风险点
  • 多部门协同:自动识别风险归属部门,支持跨部门审计
  • 结果追溯:完整记录任务执行过程、风险发现过程和整改建议

1.3 涉及的核心服务 ​

  • pigx-audit:审计业务服务,负责任务配置、定时调度、数据存储
  • pigx-ai:AI 分析服务,负责文档分析、风险识别、智能评估

二、技术架构 ​

2.1 架构图 ​

┌─────────────────────────────────────────────────────────────────┐
│                         用户/前端系统                              │
└─────────────────────────────────────────────────────────────────┘
                                ↓
┌─────────────────────────────────────────────────────────────────┐
│                    pigx-audit (审计服务)                          │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  AuditReportTaskConfigController (任务配置接口)             │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                ↓                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  AuditReportTaskConfigService (任务配置服务)                │  │
│  │    - saveTaskConfig()      : 保存任务配置                   │
│  │    - startTask()           : 启动定时任务                   │
│  │    - creatReportLinkData() : 创建报告关联数据               │
│  └───────────────────────────────────────────────────────────┘  │
│                                ↓                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  DynamicTaskScheduler (动态任务调度器)                      │  │
│  │    - 按 Cron 表达式触发任务                                 │
│  │    - 管理任务生命周期                                       │
│  └───────────────────────────────────────────────────────────┘  │
│                                ↓                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  AuditReportTaskRunnable (任务执行器)                       │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                ↓                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  AuditReportService (报告服务)                              │  │
│  │    - agentMcpSaveReport() : 调用 AI 服务生成报告           │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                                ↓ Feign 调用
┌─────────────────────────────────────────────────────────────────┐
│                      pigx-ai (AI 分析服务)                        │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  AuditReportAgentController (AI 任务入口)                   │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                ↓                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  AuditReportProcessAgent (AI 流程编排)                      │  │
│  │    基于 LangGraph4j 的状态机工作流                           │
│  └───────────────────────────────────────────────────────────┘  │
│                                ↓                                 │
│  ┌────────────────────┬──────────────────┬───────────────────┐  │
│  │  FeignNodeAction   │ ReportRiskNode   │ ReportNodeAction  │  │
│  │  (数据获取)        │ Action           │ (报告生成)        │  │
│  │                    │ (风险分析)       │                   │  │
│  └────────────────────┴──────────────────┴───────────────────┘  │
│                                ↓                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  ReportAgent (AI 智能体接口)                                │  │
│  │    - auditReportRiskByContent() : 基于文档内容识别风险      │  │
│  │    - 调用 LLM 进行合规性分析                                │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                                ↓ Feign 回调
┌─────────────────────────────────────────────────────────────────┐
│                    pigx-audit (数据回写)                          │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  AuditTaskMiddleController (任务过程接口)                   │  │
│  │    - saveAuditRiskProcess() : 保存风险过程数据              │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                ↓                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  数据库持久化                                               │  │
│  │    - AuditTaskMiddle       : 任务执行过程                   │  │
│  │    - AuditTaskMiddleContent: 过程内容详情                   │  │
│  │    - AuditRisk             : 风险数据                       │  │
│  │    - AuditRiskProcess      : 风险处理过程                   │  │
│  │    - AuditRiskResult       : 风险结果                       │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

2.2 关键技术栈 ​

  • Spring Cloud:微服务框架,服务间通信使用 Feign
  • Spring Scheduler:动态定时任务调度
  • LangChain4j:Java 版本的 LangChain,用于构建 AI 应用
  • LangGraph4j:基于状态图的工作流引擎
  • MyBatis-Plus:数据持久化
  • Nacos:服务注册与配置中心

三、完整业务流程详解 ​

3.1 流程概览 ​

整个业务流程可以分为以下几个阶段:

  1. 任务配置阶段:用户创建任务配置
  2. 定时触发阶段:调度器在指定时间触发任务
  3. AI 分析阶段:调用 AI 服务进行文档分析
  4. 数据回写阶段:AI 分析结果回写到数据库
  5. 任务完成阶段:更新任务状态和统计信息

3.2 阶段一:任务配置阶段 ​

3.2.1 入口接口 ​

接口位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/controller/AuditReportTaskConfigController.java:77

java
@PostMapping
public R save(@RequestBody AuditReportTaskConfigEntity auditReportTaskConfig) {
    return R.ok(auditReportTaskConfigService.saveTaskConfig(auditReportTaskConfig));
}

3.2.2 核心逻辑 ​

服务实现位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditReportTaskConfigServiceImpl.java:68

核心方法:saveTaskConfig()

处理步骤:

  1. 验证任务名称唯一性

    java
    long nameCount = count(Wrappers.<AuditReportTaskConfigEntity>lambdaQuery()
        .eq(AuditReportTaskConfigEntity::getTaskName, taskName));
    if (nameCount > 0) {
        throw new InternalAuthenticationServiceException("任务名称已存在");
    }
  2. 保存任务配置到数据库

    java
    save(taskConfig);
  3. 启动定时任务

    java
    startTask(taskConfig);

3.2.3 启动定时任务详解 ​

方法位置:AuditReportTaskConfigServiceImpl.java:458

核心方法:startTask()

处理步骤:

  1. 构建任务配置

    java
    String taskName = TASK_NAME_KEY + taskConfig.getTaskName();
    String executeType = taskConfig.getExecuteType();  // 1-单次执行,2-周期执行
    LocalDateTime executeDate = taskConfig.getExecuteDate();
    String periodType = taskConfig.getPeriodType();    // day/week/month/quarter/year
    String periodVal = taskConfig.getPeriodVal();
    TimingTaskBaseDTO task = new TimingTaskBaseDTO(taskName, executeType, executeDate, periodType, periodVal);
  2. 注册定时任务

    java
    // 创建任务执行器(Runnable)
    AuditReportTaskRunnable runnable = new AuditReportTaskRunnable(auditReportService, taskConfig.getId());
    
    // 向调度器注册任务
    LocalDateTime nextExecuteTime = dynamicTaskScheduler.scheduleTask(task, runnable);
    taskConfig.setNextTaskDate(nextExecuteTime);
  3. 生成关联的空数据

    java
    creatReportLinkData(taskConfig, null);

3.2.4 创建报告关联数据 ​

方法位置:AuditReportTaskConfigServiceImpl.java:309

核心方法:creatReportLinkData()

处理步骤:

  1. 创建空的审计报告

    java
    AuditReportEntity auditReport = new AuditReportEntity();
    auditReport.setTaskId(taskConfig.getId());
    auditReport.setTenantId(taskConfig.getTenantId());
    auditReportService.save(auditReport);
  2. 获取控点详情

    java
    String controlPointDetailId = taskConfig.getControlPointDetailId();
    List<Long> controlPointDetailIdList = Func.toLongList(controlPointDetailId);
  3. 查询子级控点

    java
    List<AuditControlPointDetailEntity> childDetails = auditControlPointDetailService.list(
        Wrappers.<AuditControlPointDetailEntity>lambdaQuery()
            .in(AuditControlPointDetailEntity::getParentId, controlPointDetailIdList)
    );
  4. 提取资料清单

    java
    // 从 examineData 字段中拆分资料名称
    Set<String> materialNameSet = new HashSet<>();
    for (AuditControlPointDetailEntity child : childDetails) {
        if (StrUtil.isNotBlank(child.getExamineData())) {
            String[] materialNames = child.getExamineData().split("[,,、]");
            for (String materialName : materialNames) {
                materialNameSet.add(materialName.trim());
            }
        }
    }
  5. 创建任务执行资料记录

    java
    List<AuditTaskExecutionMaterialEntity> taskExecutionMaterials = new ArrayList<>();
    controlPointMaterials.forEach(material -> {
        AuditTaskExecutionMaterialEntity entity = new AuditTaskExecutionMaterialEntity();
        entity.setControlPointMaterialId(material.getId());
        entity.setTaskId(taskConfig.getId());
        entity.setTaskExecutionId(auditReport.getId());
        entity.setBeginTime(LocalDateTime.now());
        entity.setEndTime(taskConfig.getNextTaskDate());
        entity.setTenantId(taskConfig.getTenantId());
        taskExecutionMaterials.add(entity);
    });
    auditTaskExecutionMaterialService.saveBatch(taskExecutionMaterials);
  6. 启动提醒任务(如果开启)

    java
    if ("1".equals(taskConfig.getIsRemind()) && taskConfig.getNextTaskDate() != null) {
        startRemindTask(taskConfig);
    }

3.3 阶段二:定时触发阶段 ​

3.3.1 任务触发机制 ​

触发时机:由 DynamicTaskScheduler 根据 Cron 表达式在指定时间自动触发

执行器位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/infrastructure/scheduler/tasks/AuditReportTaskRunnable.java:21

java
@Override
public void run() {
    auditReportService.agentMcpSaveReport(taskIdConfigId);
}

3.3.2 报告生成入口 ​

方法位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditReportServiceImpl.java:126

核心方法:agentMcpSaveReport()

处理步骤:

  1. 验证任务配置

    java
    AuditReportTaskConfigEntity taskConfig = taskConfigService.getById(taskConfigId);
    if (taskConfig == null) {
        return false;
    }
  2. 获取待处理的报告

    java
    // 获取报告编号为空(未生成)的报告
    List<AuditReportEntity> reportList = list(
        Wrappers.<AuditReportEntity>lambdaQuery()
            .eq(AuditReportEntity::getTaskId, taskConfigId)
            .isNull(AuditReportEntity::getReportNo)
            .last("limit 1")
    );
    AuditReportEntity report = reportList.get(0);
  3. 准备调用参数

    java
    List<Long> controlPointDetailIdList = Func.toLongList(taskConfig.getControlPointDetailId());
    String controlPointDetailIdStr = JSON.toJSONString(controlPointDetailIdList);
    Long reportId = report.getId();
  4. 调用 AI 服务

    java
    // 通过 Feign 调用 pigx-ai 服务
    auditReportProcessAgentFeign.start(new String[]{reportId + "", controlPointDetailIdStr});
  5. 更新任务配置和报告

    java
    // 更新任务状态和关联报告
    taskConfigService.updateTaskConfigStatusAndLinkReport(taskConfig, reportId);
    
    // 生成报告编号
    String reportNo = "REPORT-" + (lastReportNum + 1);
    report.setReportNo(reportNo);
    
    // 更新检查项统计
    report.setHighNum(taskConfig.getHighNum());
    report.setLowNum(taskConfig.getLowNum());
    report.setMiddleNum(taskConfig.getMiddleNum());
    report.setExamineNum(taskConfig.getExamineNum());
    updateById(report);

3.4 阶段三:AI 分析阶段 ​

3.4.1 AI 服务入口 ​

Feign 接口位置:pigx-ai/pigx-ai-api/src/main/java/com/pig4cloud/pigx/ai/api/feign/AuditReportProcessAgentFeign.java:16

java
@PostMapping("/audit-report-agent/start")
R start(@RequestBody String[] args);

Controller 位置:pigx-ai/pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/controller/AuditReportAgentController.java:29

java
@PostMapping("/start")
public R start(@RequestBody String[] args) {
    auditReportProcessAgent.start(args);
    return R.ok();
}

3.4.2 AI 流程编排 ​

Agent 位置:pigx-ai/pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/process/AuditReportProcessAgent.java:36

核心方法:start()

工作流定义:

使用 LangGraph4j 构建状态机工作流,包含以下节点:

  1. getAuditReportData:获取审计数据(FeignNodeAction)
  2. reportRiskNodeActioin:风险分析节点(ReportRiskNodeActioin)

流程图:

START → getAuditReportData → reportRiskNodeActioin → END

代码实现:

java
// 定义流转过程
StateGraph<AuditReportState> stateGraph = new StateGraph<>(AuditReportState::new)
    .addNode("getAuditReportData", node_async(feignNodeAction))
    .addNode("reportRiskNodeActioin", node_async(reportRiskNodeActioin))
    .addEdge(StateGraph.START, "getAuditReportData")
    .addEdge("getAuditReportData", "reportRiskNodeActioin")
    .addEdge("reportRiskNodeActioin", StateGraph.END);

// 编译并执行
CompiledGraph<AuditReportState> compiledGraph = stateGraph.compile();
Map<String, Object> initialState = Map.of(AuditReportState.REPORT_ID_KEY, args[0]);
compiledGraph.invoke(initialState);

3.4.3 节点一:数据获取(FeignNodeAction) ​

节点位置:pigx-ai/pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/nodeAction/FeignNodeAction.java:43

核心方法:apply()

处理步骤:

  1. 准备 Feign 调用

    java
    // 从注册表获取 Feign 实例
    String feignClass = "com.pig4cloud.pigx.audit.feign.service.AuditTaskExecutionMaterialFeignService";
    String feignMethod = "getDocumentLibrariesByTaskExecutionId";
    FeignClassInfoDTO feignClassInfoDTO = FeignRegistryInitializer.registry.get(feignClass);
    FeignMethodInfoDTO methodInfoDTO = feignClassInfoDTO.getMethodInfos().get(feignMethod);
  2. 构建调用参数

    java
    // 参数:reportId, controlPointDetailIds
    Object[] requestParams = new Object[args.length];
    for (int i = 0; i < parameters.size(); i++) {
        requestParams[i] = memberFieldConstructor.getConvertedValue(args[i]);
    }
  3. 执行远程调用

    java
    MethodInvoker methodInvoker = MethodInvoker.builder()
        .instance(feignClassInfoDTO.getInstance())
        .method(methodInfoDTO.getMethod())
        .args(requestParams)
        .build();
    R<?> result = methodInvoker.invokeService();
  4. 返回结果

    java
    // 将结果放入状态中
    return Map.of(AuditReportState.RAW_FEIGN_DATA_KEY, result.getData());

回调的 Feign 接口:pigx-audit/pigx-audit-api/src/main/java/com/pig4cloud/pigx/audit/feign/service/AuditTaskExecutionMaterialFeignService.java:28

java
@PostMapping("/v1/task/execution/data/control-point/{reportId}")
R<List<AuditControlPointDetailEntity>> getDocumentLibrariesByTaskExecutionId(
    @PathVariable("reportId") Long reportId,
    @RequestBody List<Long> controlPointDetailIds
);

返回的数据结构:

json
[
  {
    "examineItem": "检查事项名称",
    "externalRegulation": "外部法律法规内容",
    "internalSpecification": "内部规范内容",
    "companyRegulation": "公司制度内容",
    "auditProcedures": "审计程序",
    "riskDescription": "风险描述",
    "managementSuggestion": "管理建议",
    "problemType": "问题类型",
    "controlPointAction": "控点行为",
    "deptIdToNameMap": {
      "部门名称1": "部门ID1",
      "部门名称2": "部门ID2"
    },
    "documentLibraries": [
      {
        "fileName": "文档文件名",
        "mdContent": "文档内容(Markdown格式)"
      }
    ]
  }
]

3.4.4 节点二:风险分析(ReportRiskNodeActioin) ​

节点位置:pigx-ai/pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/nodeAction/ReportRiskNodeActioin.java:48

核心方法:apply()

处理步骤:

  1. 获取上一节点的数据

    java
    JSONArray dataArray = state.getMessageArrayByKey(AuditReportState.RAW_FEIGN_DATA_KEY);
    log.info("获取到审计数据,共 {} 条记录", dataArray.size());
  2. 初始化 AI 模型

    java
    ChatModel chatModel = getChatLanguageModel();
    ReportAgent reportAgent = AiServices.builder(ReportAgent.class)
        .chatModel(chatModel)
        .build();
  3. 遍历所有控点数据

    java
    int sumCount = 0;  // 总检查项数
    int nowCount = 0;  // 当前进度
    
    // 统计总文档数
    for (int i = 0; i < dataArray.size(); i++) {
        JSONObject dataObj = dataArray.getJSONObject(i);
        JSONArray documentLibraries = dataObj.getJSONArray("documentLibraries");
        sumCount += documentLibraries.size();
    }
  4. 对每个文档进行 AI 分析

    java
    for (int i = 0; i < dataArray.size(); i++) {
        JSONObject dataObj = dataArray.getJSONObject(i);
        JSONObject deptIdToNameMap = dataObj.getJSONObject("deptIdToNameMap");
        JSONArray documentLibraries = dataObj.getJSONArray("documentLibraries");
        Set<String> deptNameSet = deptIdToNameMap.getInnerMap().keySet();
    
        for (int o = 0; o < documentLibraries.size(); o++) {
            nowCount++;
            Date examineTime = new Date();
    
            // 获取文档内容
            JSONObject jsonObject = documentLibraries.getJSONObject(o);
            String mdContent = jsonObject.getString("mdContent");
            String examineItem = dataObj.getString("examineItem");
            String externalRegulation = dataObj.getString("externalRegulation");
            String internalSpecification = dataObj.getString("internalSpecification");
            String auditProcedures = dataObj.getString("auditProcedures");
            String companyRegulation = dataObj.getString("companyRegulation");
            String riskDescription = dataObj.getString("riskDescription");
            String managementSuggestion = dataObj.getString("managementSuggestion");
    
            // 调用 AI 进行风险分析
            String text = reportAgent.auditReportRiskByContent(
                mdContent, examineItem, externalRegulation,
                internalSpecification, auditProcedures, companyRegulation,
                riskDescription, managementSuggestion, deptNameSet
            );
    
            // 解析 AI 返回的 JSON
            JSONObject data = parseAIResponse(text);
    
            // 判断是否存在风险
            String isRisk = data.getString("isRisk");
            if ("1".equals(isRisk)) {
                // 组装风险数据
                data.put("startTime", examineTime);
                data.put("endTime", new Date());
                data.put("examineTime", examineTime);
                data.put("auditProcedures", auditProcedures);
                data.put("dependOn", jsonObject.getString("fileName"));
                data.put("examineItem", examineItem);
                data.put("reportId", state.getResultRaw(AuditReportState.REPORT_ID_KEY));
                data.put("problemType", dataObj.getString("problemType"));
                data.put("externalRegulation", externalRegulation);
                data.put("internalSpecification", internalSpecification);
                data.put("deptId", deptIdToNameMap.get(data.getString("deptName")));
                data.put("examineNum", sumCount);
                data.put("awaitNum", nowCount);
                data.put("controlPointAction", dataObj.getString("controlPointAction"));
    
                // 异步保存风险数据
                executor.execute(() -> materialFeignService.saveAuditRiskProcess(data));
            }
        }
    }
  5. JSON 解析与修复

    java
    private JSONObject parseAIResponse(String text) {
        try {
            return JSONObject.parseObject(text);
        } catch (JSONException e) {
            // 使用 JSONRepair 修复格式错误
            JSONRepairConfig config = new JSONRepairConfig();
            config.enableExtractJSON();
            JSONRepair repair = new JSONRepair(config);
            String correctJSON = repair.handle(text);
            return JSONObject.parseObject(correctJSON);
        }
    }

3.4.5 AI 智能体接口详解 ​

接口位置:pigx-ai/pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/agent/ReportAgent.java:318

核心方法:auditReportRiskByContent()

方法签名:

java
String auditReportRiskByContent(
    @V("mdContent") String mdContent,                          // 文档内容(Markdown)
    @V("examineItem") String examineItem,                      // 检查事项
    @V("externalRegulation") String externalRegulation,        // 外部法规
    @V("internalSpecification") String internalSpecification,  // 内部规范
    @V("companyRegulation") String companyRegulation,          // 公司制度
    @V("auditProcedures") String auditProcedures,              // 审计程序
    @V("riskDescription") String riskDescription,              // 风险描述
    @V("managementSuggestion") String managementSuggestion,    // 管理建议
    @V("departmentList") Set<String> deptList                  // 部门列表
);

系统提示词(SystemMessage):

你是一个专业的审计风险数据生成智能体(Risk Data Generation Agent)。
你的任务是根据用户提供的检查规则(examineRule)和用户依据文件(mdContent)内容,
生成一个符合指定JSON结构的风险数据对象。

【重要要求】:
1. 所有输出的JSON数据必须基于用户输入的内容生成,禁止虚构、推断或添加不存在的信息。
2. 输出必须为纯JSON格式,符合标准语法规范。
3. JSON中的字段值必须严格匹配数据类型和格式约束。
4. 当isRisk为1时,必须要输出至少一组风险数据,并且风险数据的所有字段都必须有值。
5. 责任部门名称必须在用户输入的部门列表中选择一个最匹配的部门名称。

用户提示词(UserMessage):

你作为审计风险评估专家,需要根据提供的审计材料进行精准分析。

步骤1:解析输入材料
- 仔细审阅被审计文件正文(mdContent)
- 明确控点清单事项(examineItem)的具体内容
- 核对依赖的外部法律法规、内部规范及公司内部控制要求

步骤2:合规性比对分析
- 逐项对比被审计事项与法律法规、内部规范的条款
- 重点验证行为主体、时间节点、操作流程是否匹配
- 引用法规时需注明全称、条、款、项

步骤3:基于审查程序的风险判定
- 依据审查程序逐步核查风险点
- 重点关注时效性、关联性、责任主体

步骤4:总结风险描述与整改建议
- 根据预设的风险描述与整改建议输出
- 可以在原本的内容上进行简单的润色或补充

步骤5:输出结构化结论
- 包含事实摘要与对应法规条款的精准引用
- 风险等级评估及依据
- 针对性整改建议

步骤6:风险内容的简述

步骤7:责任部门归属
- 在部门列表中选择最符合风险责任的部门名称

AI 返回的数据结构:

json
{
  "deptName": "财务部门",
  "isRisk": "1",
  "riskSimple": [
    {
      "content": "采购制度不完善、缺失重要业务流程",
      "level": "1"
    },
    {
      "content": "采购审批流程缺失部门负责人",
      "level": "2"
    }
  ],
  "riskData": [
    {
      "level": "1",
      "riskInfo": {
        "riskInfluence": "1、造成直接资金损失。2、影响公司正常经营。",
        "changeSuggest": "1、暂停合同执行。2、签订补充协议。",
        "attributeTo": "1、采购部门对内控制度执行不严格。2、审批系统设置缺陷。",
        "resultList": [
          {
            "riskLevel": "2",
            "context": "关键审批流程缺失:采购合同金额达到120万元,但未经法务部门审核。"
          },
          {
            "riskLevel": "3",
            "context": "采购合同签署完毕后,供应商信息已过期。"
          }
        ],
        "processList": [
          {
            "date": "2025-11-13T18:33:28",
            "context": "采购部门张三发起关于设备的采购申请,共计200w元。"
          },
          {
            "date": "2025-11-13T19:32:01",
            "context": "经部门经理直接审批通过,财务总监未审批。"
          }
        ]
      }
    }
  ]
}

3.5 阶段四:数据回写阶段 ​

3.5.1 回写接口 ​

Feign 接口位置:pigx-audit/pigx-audit-api/src/main/java/com/pig4cloud/pigx/audit/feign/service/AuditTaskExecutionMaterialFeignService.java:43

java
@PostMapping("/auditTaskMiddle/process/json")
R saveAuditRiskProcess(@RequestBody JSONObject data);

Controller 位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/controller/AuditTaskMiddleController.java:131

java
@PostMapping("/process/json")
R saveAuditRiskProcess(@RequestBody JSONObject data) {
    return R.ok(auditTaskMiddleService.saveAuditRiskProcess(data));
}

3.5.2 数据保存逻辑 ​

服务位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditTaskMiddleServiceImpl.java:61

核心方法:saveAuditRiskProcess()

处理步骤:

  1. 解析风险数据

    java
    AgentReportRiskDTO agentReportRisk = JSONObject.toJavaObject(data, AgentReportRiskDTO.class);
    Long reportId = agentReportRisk.getReportId();
    AuditReportEntity report = auditReportService.getById(reportId);
  2. 更新任务配置进度

    java
    long examineNum = agentReportRisk.getExamineNum();  // 总检查项数
    long awaitNum = agentReportRisk.getAwaitNum();      // 已检查数
    BigDecimal rate = awaitDecimal.divide(examineDecimal, 8, RoundingMode.HALF_UP)
        .multiply(BigDecimal.valueOf(100));
    
    AuditReportTaskConfigEntity auditReportTaskConfig = taskConfigService.getById(taskId);
    auditReportTaskConfig.setExamineNum(examineNum);
    auditReportTaskConfig.setAwaitNum(awaitNum);
    auditReportTaskConfig.setFinshRate(rate);
    taskConfigService.updateById(auditReportTaskConfig);
  3. 保存任务执行过程

    java
    AuditTaskMiddleEntity auditTaskMiddle = new AuditTaskMiddleEntity();
    auditTaskMiddle.setTaskId(taskId);
    auditTaskMiddle.setExamineTime(agentReportRisk.getExamineTime());
    auditTaskMiddle.setStartTime(agentReportRisk.getStartTime());
    auditTaskMiddle.setEndTime(agentReportRisk.getEndTime());
    auditTaskMiddle.setTitle(agentReportRisk.getExamineItem());
    auditTaskMiddle.setDependOn(agentReportRisk.getDependOn());
    auditTaskMiddle.setContent(agentReportRisk.getAuditProcedures());
    auditTaskMiddle.setRiskNum((long) riskDataList.size());
    auditTaskMiddle.setHighNum(highNum);
    auditTaskMiddle.setRiskStatus(level);
    auditTaskMiddle.setTenantId(agentReportRisk.getTenantId());
    save(auditTaskMiddle);
  4. 保存过程内容详情

    java
    List<AuditTaskMiddleContentEntity> riskSimpleList = agentReportRisk.getRiskSimple();
    riskSimpleList.forEach(v -> {
        v.setTaskMiddleId(auditTaskMiddle.getId());
        v.setTenantId(agentReportRisk.getTenantId());
    });
    auditTaskMiddleContentService.saveBatch(riskSimpleList);
  5. 保存风险数据

    java
    List<AuditRiskEntity> auditRiskList = new ArrayList<>();
    for (AgentReportRiskDTO.RiskData riskData : riskDataList) {
        AgentReportRiskDTO.RiskInfoDTO riskInfoDTO = riskData.getRiskInfo();
        AuditRiskEntity auditRisk = new AuditRiskEntity();
        auditRisk.setAttributeTo(riskInfoDTO.getAttributeTo());        // 风险归因
        auditRisk.setInfluence(riskInfoDTO.getRiskInfluence());        // 风险影响
        auditRisk.setSuggest(riskInfoDTO.getChangeSuggest());          // 整改建议
        auditRisk.setResultList(riskInfoDTO.getResultList());          // 风险结果列表
        auditRisk.setProcessList(riskInfoDTO.getProcessList());        // 风险过程列表
        auditRisk.setSimpleTitle(agentReportRisk.getProblemType());    // 问题类型
        auditRisk.setSimpleSonTitle(agentReportRisk.getExamineItem()); // 检查事项
        auditRisk.setDeptId(agentReportRisk.getDeptId());              // 部门ID
        auditRisk.setDeptName(agentReportRisk.getDeptName());          // 部门名称
        auditRisk.setDate(LocalDateTime.now());
        auditRisk.setReportId(reportId);
        auditRisk.setFilePath(filePath);
        auditRisk.setTenantId(agentReportRisk.getTenantId());
        auditRisk.setLinkRuleInterior(agentReportRisk.getInternalSpecification());
        auditRisk.setLinkRuleLaw(agentReportRisk.getExternalRegulation());
        auditRisk.setRiskLeve(level);
        auditRisk.setRiskNum(++riskNum + "");
        auditRiskList.add(auditRisk);
    }
    auditRiskService.saveBatchRisk(auditRiskList);

3.6 阶段五:报告统计数据计算与填充 ​

3.6.1 统计功能概述 ​

在 AI 分析完成后,系统会自动计算并填充报告的所有统计字段。统计数据在 AI 分析过程中收集,并通过 Feign 回调的方式异步更新到报告中。

统计字段清单:

字段名计算逻辑说明
reportName任务名称-YYYY年MM月审计报告报告名称
reportTimeLocalDateTime.now()生成报告的时间
riskNumriskList.size()总风险数量
highNumlevel = 2 或 3 的数量高风险数量
middleNumlevel = 1 的数量中风险数量
lowNumlevel = 0 的数量低风险数量
involveDeptNumdeptCount涉及部门数量
involveScopeNumbusinessDomainCount涉及业务域数量
groupRisk按业务域分组的风险分布 JSON分组统计风险分布
datumUpFullRateuploadedMaterialCount / totalMaterialCount资料上传完善度
datumExFullRateparsedMaterialCount / uploadedMaterialCount资料检测完善度
fileTotalSizetotalFileSize (KB)总文件大小
fileNumtotalFileCount总文件数量
examineNumtaskConfig.examineNum总检查项数量
correctRate(examineNum - riskNum) / examineNum合格率
examineScope上次执行时间 ~ 本次执行时间检测范围
examineDurationXh Ym检查时长

3.6.2 统计数据收集(ReportRiskNodeActioin) ​

扩展位置:pigx-ai/pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/nodeAction/ReportRiskNodeActioin.java:63-211

在风险分析节点的文档处理循环中,同时收集统计数据:

收集的统计指标:

java
// 统计变量初始化
int totalMaterialCount = 0;      // 总资料清单数量(控点数量)
int uploadedMaterialCount = 0;   // 已上传资料数量(有 documentLibraries 的控点)
int parsedMaterialCount = 0;     // 解析成功数量(mdContent 不为空)
double totalFileSize = 0.0;      // 总文件大小(KB)
int totalFileCount = 0;          // 总文件数量
Set<String> businessDomainSet = new HashSet<>();  // 业务域集合(去重)
Set<String> deptNameSet = new HashSet<>();        // 部门名称集合(去重)

// 在文档处理循环中收集
for (int i = 0; i < dataArray.size(); i++) {
    JSONObject dataObj = dataArray.getJSONObject(i);
    JSONArray documentLibraries = dataObj.getJSONArray("documentLibraries");

    // 统计业务域
    String businessDomainName = dataObj.getString("businessDomainNames");
    if (businessDomainName != null && !businessDomainName.isEmpty()) {
        businessDomainSet.add(businessDomainName);
    }

    // 统计部门
    JSONObject deptIdToNameMap = dataObj.getJSONObject("deptIdToNameMap");
    if (deptIdToNameMap != null) {
        deptNameSet.addAll(deptIdToNameMap.getInnerMap().keySet());
    }

    // 统计控点数量(每个控点算一个资料清单)
    totalMaterialCount++;

    // 统计已上传资料(有文档的控点)
    if (documentLibraries != null && documentLibraries.size() > 0) {
        uploadedMaterialCount++;
    }

    // 处理每个文档
    for (int o = 0; o < documentLibraries.size(); o++) {
        JSONObject jsonObject = documentLibraries.getJSONObject(o);
        String mdContent = jsonObject.getString("mdContent");

        // 统计文件信息
        totalFileCount++;
        Double fileSize = jsonObject.getDouble("fileSize");
        if (fileSize != null) {
            totalFileSize += fileSize;
        }

        // 统计解析成功数量(mdContent 不为空)
        if (mdContent != null && !mdContent.isEmpty()) {
            parsedMaterialCount++;
        }

        // ... AI 分析逻辑 ...
    }
}

// 构建统计数据对象
JSONObject statistics = new JSONObject();
statistics.put("totalMaterialCount", totalMaterialCount);
statistics.put("uploadedMaterialCount", uploadedMaterialCount);
statistics.put("parsedMaterialCount", parsedMaterialCount);
statistics.put("totalFileSize", totalFileSize);
statistics.put("totalFileCount", totalFileCount);
statistics.put("businessDomainCount", businessDomainSet.size());
statistics.put("deptCount", deptNameSet.size());

// 返回结果和统计数据
Map<String, Object> returnMap = new HashMap<>();
returnMap.put(AuditReportState.REPORT_MIDDLE_CONTENT_KEY, result);
returnMap.put("REPORT_STATISTICS", statistics);
return returnMap;

统计数据示例:

json
{
  "totalMaterialCount": 10,       // 总控点数量
  "uploadedMaterialCount": 8,     // 有文档的控点数量
  "parsedMaterialCount": 7,       // mdContent不为空的文档数量
  "totalFileSize": 1024.5,        // 总文件大小(KB)
  "totalFileCount": 15,           // 总文件数量
  "businessDomainCount": 3,       // 业务域数量(去重后)
  "deptCount": 5                  // 部门数量(去重后)
}

3.6.3 流程完成回调(AuditReportProcessAgent) ​

扩展位置:pigx-ai/pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/process/AuditReportProcessAgent.java:86-110

当 LangGraph 流程完成时,从 finalState 中提取统计数据并发送到审计服务:

java
// 执行编译图,处理整个流程
compiledGraph.invoke(initialState)
    .ifPresent(finalState -> {
        System.out.println("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=这是最终的返回结果-=-=-=-=-=-=-=-=-=-=-=-=-=-=");

        // 提取统计数据
        Object statisticsObj = finalState.getResultRaw("REPORT_STATISTICS");
        if (statisticsObj != null) {
            try {
                // 将统计数据发送回审计服务
                JSONObject statistics = JSONObject.parseObject(JSONObject.toJSONString(statisticsObj));
                statistics.put("reportId", args[0]); // 添加报告ID

                // 调用审计服务的Feign接口保存统计数据
                auditReportFeignService.updateReportStatistics(statistics);

                System.out.println("统计数据已发送到审计服务: " + statistics.toJSONString());
            } catch (Exception e) {
                System.err.println("发送统计数据失败: " + e.getMessage());
                e.printStackTrace();
            }
        } else {
            System.out.println("警告: 未找到统计数据");
        }
    });

3.6.4 统计数据更新接口 ​

Feign 接口位置:pigx-audit/pigx-audit-api/src/main/java/com/pig4cloud/pigx/audit/feign/service/AuditReportFeignService.java

java
@FeignClient(contextId = "auditReportFeignService",
             value = ServiceNameConstants.AUDIT_SERVER,
             configuration = AuditFeignConfig.class)
public interface AuditReportFeignService {

    @NoToken
    @Operation(summary = "更新报告统计数据", description = "更新报告统计数据")
    @PostMapping("/auditReport/update/statistics")
    R updateReportStatistics(@RequestBody JSONObject statistics);
}

Controller 位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/controller/AuditReportController.java:211-223

java
@Inner
@Operation(summary = "更新报告统计数据" , description = "更新报告统计数据" )
@SysLog("更新报告统计数据" )
@PostMapping("/update/statistics")
public R updateReportStatistics(@RequestBody JSONObject statistics) {
    return R.ok(auditReportService.updateReportStatistics(statistics));
}

3.6.5 统计数据处理逻辑 ​

服务位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditReportServiceImpl.java:231-306

核心方法:updateReportStatistics()

处理步骤:

  1. 提取报告ID和验证

    java
    Long reportId = statistics.getLong("reportId");
    if (reportId == null) {
        log.error("报告ID为空,无法更新统计数据");
        return false;
    }
  2. 获取报告和任务配置

    java
    AuditReportEntity report = getById(reportId);
    AuditReportTaskConfigEntity taskConfig = taskConfigService.getById(report.getTaskId());
  3. 获取任务开始时间

    java
    // 从任务历史表查询最新一条记录
    List<AuditReportTaskHisEntity> hisList = auditReportTaskHisService.list(
        Wrappers.<AuditReportTaskHisEntity>lambdaQuery()
            .eq(AuditReportTaskHisEntity::getTaskConfigId, taskId)
            .orderByDesc(AuditReportTaskHisEntity::getCreateTime)
            .last("limit 1")
    );
    LocalDateTime taskStartTime = hisList.isEmpty() ? LocalDateTime.now() : hisList.get(0).getCreateTime();
  4. 提取统计数据

    java
    int totalMaterialCount = statistics.getIntValue("totalMaterialCount");
    int uploadedMaterialCount = statistics.getIntValue("uploadedMaterialCount");
    int parsedMaterialCount = statistics.getIntValue("parsedMaterialCount");
    double totalFileSize = statistics.getDoubleValue("totalFileSize");
    int totalFileCount = statistics.getIntValue("totalFileCount");
    int businessDomainCount = statistics.getIntValue("businessDomainCount");
    int deptCount = statistics.getIntValue("deptCount");
  5. 调用统计服务计算并填充报告字段

    java
    auditReportStatisticsService.calculateAndFillReportStatistics(
        report, taskConfig, taskStartTime,
        totalMaterialCount, uploadedMaterialCount, parsedMaterialCount,
        totalFileSize, totalFileCount, businessDomainCount, deptCount
    );
  6. 更新报告

    java
    updateById(report);
    log.info("报告统计数据更新成功,报告ID: {}", reportId);

3.6.6 统计计算服务(AuditReportStatisticsService) ​

服务位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditReportStatisticsService.java

核心方法:calculateAndFillReportStatistics()

这是一个集中计算所有报告统计字段的服务,方法签名:

java
public void calculateAndFillReportStatistics(
    AuditReportEntity report,
    AuditReportTaskConfigEntity taskConfig,
    LocalDateTime taskStartTime,
    int totalMaterialCount,
    int uploadedMaterialCount,
    int parsedMaterialCount,
    double totalFileSize,
    int totalFileCount,
    int businessDomainCount,
    int deptCount)

计算步骤:

  1. 生成报告名称 (35-39行)

    java
    String taskName = taskConfig.getTaskName();
    String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月"));
    report.setReportName(taskName + "-" + dateStr + "审计报告");
  2. 设置报告时间 (72-73行)

    java
    report.setReportTime(LocalDateTime.now());
  3. 获取所有风险数据 (75-79行)

    java
    List<AuditRiskEntity> riskList = auditRiskService.list(
        Wrappers.<AuditRiskEntity>lambdaQuery()
            .eq(AuditRiskEntity::getReportId, reportId)
    );
  4. 计算风险数量统计 (82行,详细实现 148-174行)

    java
    calculateRiskStatistics(report, riskList);
    
    // 具体实现
    private void calculateRiskStatistics(AuditReportEntity report, List<AuditRiskEntity> riskList) {
        long totalRisk = riskList.size();
        long highRisk = 0L, middleRisk = 0L, lowRisk = 0L;
    
        for (AuditRiskEntity risk : riskList) {
            String riskLevel = risk.getRiskLeve();
            if ("2".equals(riskLevel) || "3".equals(riskLevel)) {
                highRisk++;  // 高风险:等级 2 或 3
            } else if ("1".equals(riskLevel)) {
                middleRisk++;  // 中风险:等级 1
            } else if ("0".equals(riskLevel)) {
                lowRisk++;  // 低风险:等级 0
            }
        }
    
        report.setRiskNum(totalRisk);
        report.setHighNum(highRisk);
        report.setMiddleNum(middleRisk);
        report.setLowNum(lowRisk);
    }
  5. 设置涉及部门和业务域数量 (85-88行)

    java
    report.setInvolveDeptNum((long) deptCount);
    report.setInvolveScopeNum((long) businessDomainCount);
  6. 计算分组统计风险分布 (91行,详细实现 180-228行)

    java
    report.setGroupRisk(calculateGroupRiskByBusinessDomain(riskList));
    
    // 具体实现:按业务域分组统计
    private String calculateGroupRiskByBusinessDomain(List<AuditRiskEntity> riskList) {
        // 按业务域(simpleTitle)分组统计
        Map<String, Long> groupCount = riskList.stream()
            .filter(risk -> risk.getSimpleTitle() != null && !risk.getSimpleTitle().isEmpty())
            .collect(Collectors.groupingBy(
                AuditRiskEntity::getSimpleTitle,
                Collectors.counting()
            ));
    
        long totalCount = groupCount.values().stream().mapToLong(Long::longValue).sum();
    
        // 构建 rate 和 num 对象
        Map<String, String> rateMap = new LinkedHashMap<>();
        Map<String, Long> numMap = new LinkedHashMap<>();
    
        // 按数量降序排序
        groupCount.entrySet().stream()
            .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
            .forEach(entry -> {
                String businessDomain = entry.getKey();
                Long count = entry.getValue();
    
                // 计算占比
                BigDecimal rate = BigDecimal.valueOf(count)
                    .divide(BigDecimal.valueOf(totalCount), 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100));
    
                rateMap.put(businessDomain, rate.setScale(2, RoundingMode.HALF_UP) + "%");
                numMap.put(businessDomain, count);
            });
    
        // 构建 JSON 对象
        JSONObject result = new JSONObject();
        result.put("rate", rateMap);
        result.put("num", numMap);
    
        return result.toJSONString();
        // 输出示例: {"rate":{"费用管理":"60%","销售管理":"20%"},"num":{"费用管理":6,"销售管理":2}}
    }
  7. 计算资料上传完善度 (94-98行)

    java
    // 资料上传完善度 = 已上传资料数量 / 总资料清单数量
    BigDecimal uploadRate = totalMaterialCount > 0
        ? BigDecimal.valueOf(uploadedMaterialCount)
            .divide(BigDecimal.valueOf(totalMaterialCount), 4, RoundingMode.HALF_UP)
        : BigDecimal.ZERO;
    report.setDatumUpFullRate(uploadRate);
  8. 计算资料检测完善度 (101-105行)

    java
    // 资料检测完善度 = 解析成功数量 / 已上传资料数量
    BigDecimal parseRate = uploadedMaterialCount > 0
        ? BigDecimal.valueOf(parsedMaterialCount)
            .divide(BigDecimal.valueOf(uploadedMaterialCount), 4, RoundingMode.HALF_UP)
        : BigDecimal.ZERO;
    report.setDatumExFullRate(parseRate);
  9. 设置文件统计信息 (108-111行)

    java
    report.setFileTotalSize(BigDecimal.valueOf(totalFileSize));  // 总文件大小(KB)
    report.setFileNum((long) totalFileCount);                     // 总文件数量
  10. 设置总检查项数量 (114行)

    java
    report.setExamineNum(taskConfig.getExamineNum());
  11. 计算合格率 (117行,详细实现 235-254行)

    java
    report.setCorrectRate(calculateCorrectRate(taskConfig.getExamineNum(), riskList.size()));
    
    // 具体实现:合格率 = (总检查项 - 风险数量) / 总检查项
    private BigDecimal calculateCorrectRate(Long totalExamineNum, long riskCount) {
        if (totalExamineNum == null || totalExamineNum == 0) {
            return BigDecimal.ZERO;
        }
    
        // 符合的控点数量 = 总检查项 - 风险数量
        long compliantCount = totalExamineNum - riskCount;
        if (compliantCount < 0) {
            compliantCount = 0;
        }
    
        // 计算合格率
        return BigDecimal.valueOf(compliantCount)
            .divide(BigDecimal.valueOf(totalExamineNum), 4, RoundingMode.HALF_UP);
    }
  12. 计算检测范围 (120行,详细实现 261-289行)

    java
    report.setExamineScope(calculateExamineScope(taskConfig));
    
    // 具体实现
    private String calculateExamineScope(AuditReportTaskConfigEntity taskConfig) {
        // 查询任务历史记录,按创建时间降序
        List<AuditReportTaskHisEntity> hisList = auditReportTaskHisService.list(
            Wrappers.<AuditReportTaskHisEntity>lambdaQuery()
                .eq(AuditReportTaskHisEntity::getTaskConfigId, taskId)
                .orderByDesc(AuditReportTaskHisEntity::getCreateTime)
        );
    
        LocalDateTime startTime;
        if (hisList.size() > 1) {
            // 有历史记录(排除当前这次),取上一次执行的时间
            startTime = hisList.get(1).getCreateTime();
        } else {
            // 没有历史记录,使用任务创建时间
            startTime = taskConfig.getCreateTime();
        }
    
        String startTimeStr = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM"));
        String endTimeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
    
        return startTimeStr + " ~ " + endTimeStr;
    }
  13. 计算检查时长 (123行,详细实现 295-306行)

    java
    report.setExamineDuration(calculateExamineDuration(taskStartTime, LocalDateTime.now()));
    
    // 具体实现:格式 Xh Ym
    private String calculateExamineDuration(LocalDateTime startTime, LocalDateTime endTime) {
        if (startTime == null || endTime == null) {
            return "0h 0m";
        }
    
        Duration duration = Duration.between(startTime, endTime);
        long hours = duration.toHours();
        long minutes = duration.toMinutes() % 60;
    
        return hours + "h " + minutes + "m";
    }

3.6.7 业务域字段修改 ​

为了支持按业务域分组统计风险,对相关实体和服务进行了以下修改:

1. AgentReportRiskDTO 新增字段

位置:pigx-audit/pigx-audit-api/src/main/java/com/pig4cloud/pigx/audit/dto/AgentReportRiskDTO.java:84

java
// 业务域名称(用于分组统计)
private String businessDomainName;

2. 风险保存逻辑修改

位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditTaskMiddleServiceImpl.java:184-188

java
// 使用业务域名称作为 simpleTitle,如果没有则使用 problemType
String businessDomainName = agentReportRisk.getBusinessDomainName();
auditRisk.setSimpleTitle(businessDomainName != null && !businessDomainName.isEmpty()
        ? businessDomainName
        : agentReportRisk.getProblemType());

说明:风险的 simpleTitle 字段现在存储业务域名称,用于分组统计。

3.6.8 统计数据流时序图 ​

┌─────────────┐        ┌──────────────┐        ┌─────────────┐        ┌──────────────────┐
│ LangGraph   │        │ AI Service   │        │ Audit       │        │ Statistics       │
│ 流程        │        │              │        │ Service     │        │ Service          │
└─────────────┘        └──────────────┘        └─────────────┘        └──────────────────┘
      │                       │                       │                         │
      │ FeignNode 获取数据     │                       │                         │
      │─────────────────────→│                       │                         │
      │                       │                       │                         │
      │ ReportRiskNode 分析   │                       │                         │
      │ 收集统计数据:         │                       │                         │
      │ - totalMaterialCount  │                       │                         │
      │ - uploadedMaterialCount│                      │                         │
      │ - parsedMaterialCount │                       │                         │
      │ - totalFileSize       │                       │                         │
      │ - totalFileCount      │                       │                         │
      │ - businessDomainCount │                       │                         │
      │ - deptCount           │                       │                         │
      │                       │                       │                         │
      │ 流程完成              │                       │                         │
      │──────────────────────→│                       │                         │
      │ (finalState包含       │                       │                         │
      │  REPORT_STATISTICS)   │                       │                         │
      │                       │                       │                         │
      │                       │ Feign 回调:           │                         │
      │                       │ updateReportStatistics│                         │
      │                       │──────────────────────→│                         │
      │                       │ (statistics + reportId)│                        │
      │                       │                       │                         │
      │                       │                       │ 获取报告和任务配置       │
      │                       │                       │─────────────────────┐  │
      │                       │                       │                     │  │
      │                       │                       │←────────────────────┘  │
      │                       │                       │                         │
      │                       │                       │ 调用统计服务             │
      │                       │                       │────────────────────────→│
      │                       │                       │ calculateAndFillReportStatistics()
      │                       │                       │                         │
      │                       │                       │                         │ 计算所有字段:
      │                       │                       │                         │ - reportName
      │                       │                       │                         │ - correctRate
      │                       │                       │                         │ - examineNum
      │                       │                       │                         │ - highNum/middleNum/lowNum
      │                       │                       │                         │ - groupRisk
      │                       │                       │                         │ - datumUpFullRate
      │                       │                       │                         │ - datumExFullRate
      │                       │                       │                         │ - fileTotalSize/fileNum
      │                       │                       │                         │ - reportTime
      │                       │                       │                         │ - examineScope
      │                       │                       │                         │ - examineDuration
      │                       │                       │                         │
      │                       │                       │         完成             │
      │                       │                       │←────────────────────────│
      │                       │                       │                         │
      │                       │                       │ 更新报告到数据库         │
      │                       │                       │─────────────────────┐  │
      │                       │                       │                     │  │
      │                       │                       │←────────────────────┘  │
      │                       │                       │                         │
      │                       │         成功          │                         │
      │                       │←──────────────────────│                         │

3.6.9 最终报告数据示例 ​

所有字段填充完成后的报告示例:

json
{
  "id": 123,
  "reportName": "2025年度内控审计-2025年01月审计报告",
  "reportTime": "2025-01-24T10:30:00",
  "reportNo": "REPORT-001",

  "riskNum": 10,
  "highNum": 3,
  "middleNum": 4,
  "lowNum": 3,

  "involveDeptNum": 5,
  "involveScopeNum": 3,

  "groupRisk": "{\"rate\":{\"费用管理\":\"60%\",\"销售管理\":\"20%\",\"生产管理\":\"20%\"},\"num\":{\"费用管理\":6,\"销售管理\":2,\"生产管理\":2}}",

  "datumUpFullRate": 0.8000,
  "datumExFullRate": 0.8750,

  "fileTotalSize": 1024.5,
  "fileNum": 15,

  "examineNum": 50,
  "correctRate": 0.8000,

  "examineScope": "2024-12 ~ 2025-01",
  "examineDuration": "2h 30m"
}

3.7 阶段六:任务完成阶段 ​

3.7.1 任务状态更新 ​

方法位置:pigx-audit/pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditReportTaskConfigServiceImpl.java:260

核心方法:updateTaskConfigStatusAndLinkReport()

处理步骤:

  1. 判断任务类型

    java
    String executeType = taskConfig.getExecuteType();
    if ("1".equals(executeType)) {
        // 单次执行,执行完成后关闭任务
        taskConfig.setTaskStatus("0");
        upTaskStatus(taskConfig);
    }
  2. 增加执行次数

    java
    Long executeNum = taskConfig.getExecuteNum();
    taskConfig.setExecuteNum(++executeNum);
    updateById(taskConfig);
  3. 生成任务历史记录

    java
    AuditReportTaskHisEntity auditReportTaskHisEntity = new AuditReportTaskHisEntity();
    auditReportTaskHisEntity.setTaskConfigId(taskConfigId);
    auditReportTaskHisEntity.setResult("执行成功");
    auditReportTaskHisEntity.setCause("无");
    auditReportTaskHisEntity.setTenantId(taskConfig.getTenantId());
    auditReportTaskHisService.save(auditReportTaskHisEntity);
  4. 更新报告链接数据

    java
    uploadReportLinkData(taskConfigId, reportId);
  5. 计算下次执行时间(周期任务)

    java
    LocalDateTime nextExecuteDate = getNextExecuteDate(taskConfig);
    taskConfig.setNextTaskDate(nextExecuteDate);
  6. 启动下次提醒任务

    java
    if ("1".equals(taskConfig.getIsRemind()) && taskConfig.getNextTaskDate() != null) {
        startRemindTask(taskConfig);
    }

四、核心数据结构说明 ​

4.1 任务配置表(audit_report_task_config) ​

字段名类型说明
idBIGINT主键
task_nameVARCHAR任务名称(唯一)
task_statusVARCHAR任务状态:0-关闭,1-启动
execute_typeVARCHAR执行类型:1-单次,2-周期
execute_dateDATETIME执行时间
period_typeVARCHAR周期类型:day/week/month/quarter/year
period_valVARCHAR周期值
next_task_dateDATETIME下次执行时间
execute_numBIGINT已执行次数
control_point_detail_idVARCHAR控点详情ID列表(逗号分隔)
examine_numBIGINT总检查项数
await_numBIGINT已检查数
finsh_rateDECIMAL完成率
high_numBIGINT高风险数量
middle_numBIGINT中风险数量
low_numBIGINT低风险数量
is_remindVARCHAR是否开启提醒:0-否,1-是
examine_statusVARCHAR审查状态:0-进行中,1-需要干预,2-完成
tenant_idBIGINT租户ID

4.2 审计报告表(audit_report) ​

字段名类型说明
idBIGINT主键
task_idBIGINT关联的任务配置ID
report_noVARCHAR报告编号(如:REPORT-1)
report_nameVARCHAR报告名称
report_timeDATETIME报告生成时间
correct_rateDECIMAL合规率
full_rateDECIMAL完整率
examine_numBIGINT总检查项数
examine_scopeVARCHAR检查范围
examine_durationVARCHAR检查时长
involve_scope_numBIGINT涉及业务域数量
involve_dept_numBIGINT涉及部门数量
high_numBIGINT高风险数量
middle_numBIGINT中风险数量
low_numBIGINT低风险数量
cost_rateDECIMAL费用合规率
sale_rateDECIMAL销售合规率
purchase_rateDECIMAL采购合规率
personnel_rateDECIMAL人事合规率
datum_up_full_rateDECIMAL资料上传完整度
datum_ex_full_rateDECIMAL资料检测完善度
tenant_idBIGINT租户ID

4.3 任务执行过程表(audit_task_middle) ​

字段名类型说明
idBIGINT主键
task_idBIGINT关联的任务配置ID
titleVARCHAR过程标题
contentTEXT过程内容
examine_timeDATETIME检查时间
start_timeDATETIME开始时间
end_timeDATETIME结束时间
depend_onVARCHAR依据文件
risk_numBIGINT风险数量
high_numBIGINT高风险数量
risk_statusVARCHAR风险状态:0-低,1-中,2-高
tenant_idBIGINT租户ID

4.4 过程内容详情表(audit_task_middle_content) ​

字段名类型说明
idBIGINT主键
task_middle_idBIGINT关联的任务过程ID
dateDATETIME记录时间
contentTEXT内容描述
file_iconVARCHAR文件图标
remind_infoVARCHAR提醒信息
remind_statusVARCHAR提醒状态
levelVARCHAR风险等级
tenant_idBIGINT租户ID

4.5 风险数据表(audit_risk) ​

字段名类型说明
idBIGINT主键
report_idBIGINT关联的报告ID
risk_numVARCHAR风险编号
risk_leveVARCHAR风险等级:0-低,1-中,2-高
simple_titleVARCHAR问题类型
simple_son_titleVARCHAR检查事项
dept_idBIGINT责任部门ID
dept_nameVARCHAR责任部门名称
attribute_toTEXT风险归因(JSON数组)
influenceTEXT风险影响(JSON数组)
suggestTEXT整改建议(JSON数组)
file_pathVARCHAR依据文件路径
link_rule_lawTEXT关联外部法规(JSON数组)
link_rule_interiorTEXT关联内部规范(JSON数组)
result_listJSON风险结果列表
process_listJSON风险过程列表
dateDATETIME发现时间
tenant_idBIGINT租户ID

result_list 结构:

json
[
  {
    "riskLevel": "2",
    "context": "风险描述内容"
  }
]

process_list 结构:

json
[
  {
    "date": "2025-11-13T18:33:28",
    "context": "过程描述内容"
  }
]

4.6 任务执行资料关联表(audit_task_execution_material) ​

字段名类型说明
idBIGINT主键
task_idBIGINT关联的任务配置ID
task_execution_idBIGINT关联的报告ID
control_point_material_idBIGINT控点资料ID
begin_timeDATETIME开始时间
end_timeDATETIME结束时间
tenant_idBIGINT租户ID

4.7 任务执行历史表(audit_report_task_his) ​

字段名类型说明
idBIGINT主键
task_config_idBIGINT关联的任务配置ID
resultVARCHAR执行结果
causeVARCHAR原因
tenant_idBIGINT租户ID
create_timeDATETIME创建时间

五、关键配置说明 ​

5.1 定时任务配置 ​

执行类型(execute_type):

  • 1:单次执行
  • 2:周期执行

周期类型(period_type):

  • day:每日
  • week:每周
  • month:每月
  • quarter:每季度
  • year:每年

周期值(period_val):

  • 对于 day:天数(如:1 表示每天,7 表示每 7 天)
  • 对于 week:星期几(如:1 表示周一,7 表示周日)
  • 对于 month:日期(如:1 表示每月 1 号)
  • 对于 quarter:月份(如:1 表示每季度第 1 个月)
  • 对于 year:月份(如:1 表示每年 1 月)

5.2 提醒任务配置 ​

提醒时机:

  • 距离下次执行 >= 180 天:提前 30 天提醒
  • 距离下次执行 >= 30 天 且 < 180 天:提前 7 天提醒
  • 距离下次执行 < 30 天:提前 2 天提醒

提醒时间:每天下午 15:00

5.3 风险等级定义 ​

  • 0:无风险
  • 1:低风险
  • 2:中风险
  • 3:高风险

六、服务间交互详解 ​

6.1 交互流程图 ​

┌─────────────────────┐
│   pigx-audit        │
│   (审计服务)         │
└─────────────────────┘
         │
         │ 1. 调用 AI 服务
         │    Feign: auditReportProcessAgentFeign.start()
         │    接口: POST /audit-report-agent/start
         │    参数: [reportId, controlPointDetailIds]
         ↓
┌─────────────────────┐
│   pigx-ai           │
│   (AI 分析服务)      │
└─────────────────────┘
         │
         │ 2. 回调获取数据
         │    Feign: AuditTaskExecutionMaterialFeignService.getDocumentLibrariesByTaskExecutionId()
         │    接口: POST /v1/task/execution/data/control-point/{reportId}
         │    参数: reportId, controlPointDetailIds
         │    返回: 控点数据 + 文档资料列表
         ↓
┌─────────────────────┐
│   pigx-audit        │
│   (审计服务)         │
└─────────────────────┘
         │
         │ 3. AI 分析(内部处理)
         │    - 遍历所有文档
         │    - 调用 LLM 进行合规性分析
         │    - 识别风险点
         │
         │ 4. 回写风险数据(循环调用)
         │    Feign: materialFeignService.saveAuditRiskProcess()
         │    接口: POST /auditTaskMiddle/process/json
         │    参数: 风险数据 JSON
         ↓
┌─────────────────────┐
│   pigx-audit        │
│   (审计服务)         │
│   - 保存任务过程    │
│   - 保存风险数据    │
└─────────────────────┘

6.2 数据流转说明 ​

第一次调用:pigx-audit → pigx-ai ​

请求数据:

json
["报告ID", "[控点详情ID列表]"]
// 例如:["123", "[1,2,3]"]

第二次调用:pigx-ai → pigx-audit(获取数据) ​

请求数据:

json
{
  "reportId": 123,
  "controlPointDetailIds": [1, 2, 3]
}

响应数据:

json
[
  {
    "examineItem": "检查事项:采购业务审批流程",
    "externalRegulation": "《企业内部控制应用指引第7号--采购业务》第四条...",
    "internalSpecification": "公司采购管理制度...",
    "companyRegulation": "采购审批权限规定...",
    "auditProcedures": "1. 检查采购申请是否完整...",
    "riskDescription": "采购审批流程不规范...",
    "managementSuggestion": "建议完善采购审批制度...",
    "problemType": "采购管理",
    "controlPointAction": "审批流程控制",
    "deptIdToNameMap": {
      "采购部": 1001,
      "财务部": 1002
    },
    "documentLibraries": [
      {
        "fileName": "2025年10月采购合同.pdf",
        "mdContent": "# 采购合同\n\n**合同编号**:PC-2025-001..."
      },
      {
        "fileName": "采购审批单.pdf",
        "mdContent": "# 采购审批单\n\n**申请部门**:采购部..."
      }
    ]
  }
]

第三次调用(多次):pigx-ai → pigx-audit(回写风险) ​

请求数据(每发现一个风险点都会回写一次):

json
{
  "reportId": 123,
  "deptName": "采购部",
  "deptId": 1001,
  "isRisk": "1",
  "examineItem": "检查事项:采购业务审批流程",
  "examineTime": "2025-11-24T10:30:00",
  "startTime": "2025-11-24T10:30:00",
  "endTime": "2025-11-24T10:35:00",
  "dependOn": "2025年10月采购合同.pdf",
  "auditProcedures": "1. 检查采购申请是否完整...",
  "problemType": "采购管理",
  "controlPointAction": "审批流程控制",
  "externalRegulation": "《企业内部控制应用指引第7号--采购业务》...",
  "internalSpecification": "公司采购管理制度...",
  "examineNum": 50,
  "awaitNum": 10,
  "riskSimple": [
    {
      "content": "采购审批流程缺失部门负责人审批环节",
      "level": "2"
    },
    {
      "content": "供应商资质审核不完整",
      "level": "1"
    }
  ],
  "riskData": [
    {
      "level": "2",
      "riskInfo": {
        "riskInfluence": "1、造成直接资金损失。2、影响公司正常经营。",
        "changeSuggest": "1、暂停合同执行。2、签订补充协议。3、补全审批流程。",
        "attributeTo": "1、采购部门对内控制度执行不严格。2、审批系统设置缺陷。",
        "resultList": [
          {
            "riskLevel": "2",
            "context": "关键审批流程缺失:采购合同金额达到120万元,但未经法务部门审核。"
          },
          {
            "riskLevel": "3",
            "context": "采购合同签署完毕后,供应商信息已过期。"
          }
        ],
        "processList": [
          {
            "date": "2025-11-13T18:33:28",
            "context": "采购部门张三发起关于设备的采购申请,共计200w元。"
          },
          {
            "date": "2025-11-13T19:32:01",
            "context": "经部门经理直接审批通过,财务总监未审批。"
          },
          {
            "date": "2025-11-15T12:36:01",
            "context": "采购合同签署完毕,供应商信息已过期。"
          }
        ]
      }
    }
  ]
}

七、核心代码位置索引 ​

7.1 pigx-audit 服务 ​

功能模块文件位置核心方法
任务配置 Controllerpigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/controller/AuditReportTaskConfigController.javasave() (77行)
任务配置 Servicepigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditReportTaskConfigServiceImpl.javasaveTaskConfig() (68行)<br>startTask() (458行)<br>creatReportLinkData() (309行)
定时任务执行器pigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/infrastructure/scheduler/tasks/AuditReportTaskRunnable.javarun() (21行)
报告 Servicepigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditReportServiceImpl.javaagentMcpSaveReport() (126行)
AI 服务 Feign 接口pigx-ai-api/src/main/java/com/pig4cloud/pigx/ai/api/feign/AuditReportProcessAgentFeign.javastart() (16行)
任务过程 Controllerpigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/controller/AuditTaskMiddleController.javasaveAuditRiskProcess() (131行)
任务过程 Servicepigx-audit-biz/src/main/java/com/pig4cloud/pigx/audit/service/impl/AuditTaskMiddleServiceImpl.javasaveAuditRiskProcess() (61行)
审计 Feign 接口pigx-audit-api/src/main/java/com/pig4cloud/pigx/audit/feign/service/AuditTaskExecutionMaterialFeignService.javagetDocumentLibrariesByTaskExecutionId() (28行)<br>saveAuditRiskProcess() (43行)

7.2 pigx-ai 服务 ​

功能模块文件位置核心方法
AI 任务 Controllerpigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/controller/AuditReportAgentController.javastart() (29行)
AI 流程编排pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/process/AuditReportProcessAgent.javastart() (36行)
数据获取节点pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/nodeAction/FeignNodeAction.javaapply() (43行)
风险分析节点pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/nodeAction/ReportRiskNodeActioin.javaapply() (48行)
AI 智能体接口pigx-ai-biz/src/main/java/com/pig4cloud/pigx/ai/agent/langgraph/agent/ReportAgent.javaauditReportRiskByContent() (318行)

八、异常处理与容错 ​

8.1 任务执行失败处理 ​

  1. 任务启动失败

    • 检查任务配置是否正确
    • 检查 Cron 表达式是否有效
    • 检查控点详情 ID 是否存在
  2. AI 服务调用失败

    • 重试机制:Feign 内置重试(根据配置)
    • 失败记录:记录到任务历史表
    • 任务状态:不影响任务配置状态,下次定时触发继续执行
  3. 数据回写失败

    • 异步回写:使用线程池,单个失败不影响整体
    • 事务保证:每次回写独立事务
    • 日志记录:完整记录错误堆栈

8.2 AI 响应解析失败处理 ​

问题:AI 返回的 JSON 格式不正确

解决方案:

  1. 第一步:尝试直接解析

    java
    JSONObject data = JSONObject.parseObject(text);
  2. 第二步:使用 JSONRepair 修复

    java
    JSONRepairConfig config = new JSONRepairConfig();
    config.enableExtractJSON();
    JSONRepair repair = new JSONRepair(config);
    String correctJSON = repair.handle(text);
    data = JSONObject.parseObject(correctJSON);
  3. 第三步:记录错误并跳过

    java
    log.error("AI 响应解析失败,原始文本:{}", text);
    continue;  // 跳过该文档,继续处理下一个

8.3 并发控制 ​

  1. 定时任务并发

    • 任务名称作为唯一标识
    • 同一任务不会并发执行
    • 使用 DynamicTaskScheduler 管理任务生命周期
  2. 数据回写并发

    • 使用线程池异步回写
    • 每个风险点独立事务
    • 数据库乐观锁保证数据一致性

九、性能优化建议 ​

9.1 批量处理 ​

当前实现:每发现一个风险点就回写一次

优化建议:

  1. 批量回写:收集所有风险点后,批量回写

    java
    List<JSONObject> riskDataList = new ArrayList<>();
    // 收集所有风险
    for (...) {
        if ("1".equals(isRisk)) {
            riskDataList.add(data);
        }
    }
    // 批量回写
    materialFeignService.batchSaveAuditRiskProcess(riskDataList);
  2. 减少 Feign 调用次数:从 N 次减少到 1 次

9.2 并行处理 ​

当前实现:串行处理所有文档

优化建议:

  1. 文档并行分析:使用线程池并行调用 AI

    java
    List<CompletableFuture<JSONObject>> futures = new ArrayList<>();
    for (JSONObject document : documentLibraries) {
        CompletableFuture<JSONObject> future = CompletableFuture.supplyAsync(() -> {
            String text = reportAgent.auditReportRiskByContent(...);
            return parseAIResponse(text);
        }, executor);
        futures.add(future);
    }
    // 等待所有完成
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
  2. 提升处理速度:处理时间从 O(n) 降到 O(1)

9.3 缓存优化 ​

当前实现:每次都查询数据库

优化建议:

  1. 部门信息缓存:使用 Redis 缓存部门信息
  2. 法规规范缓存:缓存常用的法规和规范
  3. 控点信息缓存:缓存控点详情

十、常见问题与解答 ​

10.1 任务未触发 ​

问题:任务配置保存成功,但到时间未触发

排查步骤:

  1. 检查任务状态是否为"启动"(task_status = 1)
  2. 检查执行类型和时间配置是否正确
  3. 检查 DynamicTaskScheduler 是否正常运行
  4. 查看日志是否有异常信息

10.2 AI 分析无结果 ​

问题:任务执行完成,但没有风险数据

排查步骤:

  1. 检查文档内容是否为空
  2. 检查 AI 服务是否正常
  3. 检查 AI 返回的 isRisk 字段是否为 "1"
  4. 查看 AI 返回的原始内容

10.3 数据回写失败 ​

问题:AI 分析完成,但数据库中没有记录

排查步骤:

  1. 检查 Feign 调用是否成功
  2. 检查数据库连接是否正常
  3. 检查事务是否回滚
  4. 查看异常日志

十一、未来扩展方向 ​

11.1 功能扩展 ​

  1. 多模型支持

    • 支持不同的 LLM 模型
    • 支持模型切换和配置
  2. 报告生成

    • 自动生成 Word/PDF 报告
    • 支持自定义报告模板
  3. 通知提醒

    • 邮件通知
    • 企业微信/钉钉通知
    • 短信通知

11.2 技术优化 ​

  1. 流式处理

    • 使用 WebFlux 实现流式处理
    • 实时推送处理进度
  2. 分布式任务

    • 使用 XXL-Job 或 ElasticJob
    • 支持任务分片和负载均衡
  3. 监控告警

    • 任务执行监控
    • 性能指标监控
    • 异常告警

十二、总结 ​

本文档详细说明了审计报告任务配置与执行的完整流程,包括:

  1. 业务流程:从任务配置、定时触发、AI 分析、数据回写到报告统计的完整链路
  2. 技术实现:基于 Spring Cloud、LangChain4j、LangGraph4j 的微服务架构
  3. 服务交互:pigx-audit 和 pigx-ai 之间的多次 Feign 调用
  4. 数据流转:详细的数据结构和存储设计
  5. 统计功能:完整的报告统计字段计算与填充流程
  6. 代码位置:所有核心代码的文件路径和行号

该功能实现了智能化的审计流程,通过 AI 技术大幅提升了审计效率和准确性。统计功能的加入使报告数据更加完整和规范,为企业的合规管理提供了有力支持。


文档版本:v2.0 更新日期:2025-01-24 编写人:Claude Code 审核人:待审核 更新内容:新增第 3.6 节「报告统计数据计算与填充」功能说明