Przeglądaj źródła

feat(architecture): 删除企业售后Agent项目设计文档

- 移除完整的售后Agent项目设计说明文档
- 文档涵盖目标、架构、模块划分、流程、数据库设计及测试策略
- 包括Java与Python服务、LangGraph编排及审批流程细节
- 删除后清理相关设计依赖,准备后续设计或开发工作
wzq 1 miesiąc temu
rodzic
commit
23802f1183
47 zmienionych plików z 2602 dodań i 49 usunięć
  1. 0 3
      .vscode/settings.json
  2. 0 0
      doc/s.md
  3. 10 10
      doc/第三方接入API文档.md
  4. 164 0
      sql/mysql/agent_aftersales_m1.sql
  5. 55 0
      sql/postgresql/aftersales_policy_pgvector.sql
  6. 12 0
      src/main/java/com/zsElectric/boot/business/controller/ThirdPartyInfoController.java
  7. 3 0
      src/main/java/com/zsElectric/boot/business/model/query/ChargeOrderInfoQuery.java
  8. 2 0
      src/main/java/com/zsElectric/boot/business/model/vo/ChargeOrderInfoVO.java
  9. 1 1
      src/main/java/com/zsElectric/boot/business/quartz/CompensateOrderJob.java
  10. 1 1
      src/main/java/com/zsElectric/boot/business/quartz/FailOrderDisposeJob.java
  11. 3 0
      src/main/java/com/zsElectric/boot/business/service/ThirdPartyInfoService.java
  12. 26 26
      src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java
  13. 13 0
      src/main/java/com/zsElectric/boot/business/service/impl/ThirdPartyInfoServiceImpl.java
  14. 2 0
      src/main/java/com/zsElectric/boot/business/service/impl/UserInfoServiceImpl.java
  15. 2 2
      src/main/java/com/zsElectric/boot/charging/quartz/ChargingJob.java
  16. 51 0
      src/main/java/com/zsElectric/boot/platform/agent/config/AgentToolConfig.java
  17. 107 0
      src/main/java/com/zsElectric/boot/platform/agent/controller/AgentApprovalController.java
  18. 68 0
      src/main/java/com/zsElectric/boot/platform/agent/controller/AgentMcpController.java
  19. 12 0
      src/main/java/com/zsElectric/boot/platform/agent/mapper/AftersalesWorkOrderMapper.java
  20. 12 0
      src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentApprovalMapper.java
  21. 12 0
      src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentAuditLogMapper.java
  22. 12 0
      src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentPendingActionMapper.java
  23. 15 0
      src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentToolRegistryMapper.java
  24. 28 0
      src/main/java/com/zsElectric/boot/platform/agent/model/dto/AgentApprovalDecisionRequest.java
  25. 46 0
      src/main/java/com/zsElectric/boot/platform/agent/model/dto/AgentToolCallRequest.java
  26. 107 0
      src/main/java/com/zsElectric/boot/platform/agent/model/dto/AgentToolCallResponse.java
  27. 99 0
      src/main/java/com/zsElectric/boot/platform/agent/model/entity/AftersalesWorkOrder.java
  28. 107 0
      src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentApproval.java
  29. 104 0
      src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentAuditLog.java
  30. 94 0
      src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentPendingAction.java
  31. 90 0
      src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentToolRegistry.java
  32. 45 0
      src/main/java/com/zsElectric/boot/platform/agent/service/AgentApprovalStore.java
  33. 17 0
      src/main/java/com/zsElectric/boot/platform/agent/service/AgentAuditWriter.java
  34. 24 0
      src/main/java/com/zsElectric/boot/platform/agent/service/AgentBusinessToolExecutor.java
  35. 491 0
      src/main/java/com/zsElectric/boot/platform/agent/service/AgentToolOrchestrator.java
  36. 285 0
      src/main/java/com/zsElectric/boot/platform/agent/service/DefaultAgentBusinessToolExecutor.java
  37. 83 0
      src/main/java/com/zsElectric/boot/platform/agent/service/MyBatisAgentApprovalStore.java
  38. 30 0
      src/main/java/com/zsElectric/boot/platform/agent/service/MyBatisAgentAuditWriter.java
  39. 23 0
      src/main/java/com/zsElectric/boot/platform/agent/tool/AgentRiskLevel.java
  40. 89 0
      src/main/java/com/zsElectric/boot/platform/agent/tool/AgentToolCatalog.java
  41. 25 0
      src/main/java/com/zsElectric/boot/platform/agent/tool/AgentToolDefinition.java
  42. 6 0
      src/main/resources/application-dev.yml
  43. 4 0
      src/main/resources/application-prod.yml
  44. 10 4
      src/main/resources/application-test.yml
  45. 1 1
      src/main/resources/application.yml
  46. 6 1
      src/main/resources/mapper/business/ChargeOrderInfoMapper.xml
  47. 205 0
      src/test/java/com/zsElectric/boot/platform/agent/service/AgentToolOrchestratorTest.java

+ 0 - 3
.vscode/settings.json

@@ -1,3 +0,0 @@
-{
-  "java.compile.nullAnalysis.mode": "automatic"
-}

+ 0 - 0
docs/superpowers/specs/2026-07-08-enterprise-aftersales-agent-design.md → doc/s.md


+ 10 - 10
doc/第三方接入API文档.md

@@ -10,7 +10,7 @@
 - [2. 获取Token](#2-获取token)
 - [3. 充值档位信息分页列表](#3-充值档位信息分页列表)
 - [4. 获取用户信息](#4-获取用户信息)
-- [5. 充点券购买](#5-充点券购买)
+- [5. 充电券购买](#5-充电券购买)
 - [6. 获取充电站列表](#6-获取充电站列表)
 - [7. 获取充电站详情与充电终端列表](#7-获取充电站详情与充电终端列表)
 - [8. 获取充电终端详情](#8-获取充电终端详情)
@@ -659,15 +659,15 @@
 
 ### 9.2 输入参数(data解密后)
 
-| 参数名称             | 参数定义 | 参数类型 | 描述 | 是否必填 |
-|------------------|----------|----------|------|------|
-| equipmentId      | 充电桩编号 | String | 充电桩编号 | 是    |
-| stationId        | 第三方充电站ID | String | 充电站ID | 是    |
-| connectorId      | 充电设备接口编码 | String | 充电设备接口编码 | 是    |
-| channelOrderNo   | 渠道方订单编号 | String | 第三方平台订单编号 | 是    |
-| channelUserPhone | 渠道方用户手机号 | String | 用户手机号 | 是    |
-| channelPreAmt    | 渠道方预支付金额 | BigDecimal | 预支付金额 | 是    |
-| plateNum         | 车牌号 | String | 车牌号 | 否    |
+| 参数名称             | 参数定义 | 参数类型 | 描述           | 是否必填 |
+|------------------|----------|----------|--------------|------|
+| equipmentId      | 充电桩编号 | String | 充电桩编号        | 是    |
+| stationId        | 第三方充电站ID | String | 充电站ID        | 是    |
+| connectorId      | 充电设备接口编码 | String | 充电设备接口编码     | 是    |
+| channelOrderNo   | 渠道方订单编号 | String | 第三方平台订单编号    | 是    |
+| channelUserPhone | 渠道方用户手机号 | String | 用户手机号        | 是    |
+| channelPreAmt    | 渠道方预支付金额 | BigDecimal | 预支付金额        | 是    |
+| plateNum         | 车牌号 | String | 车牌号(用于停车减免)) | 否    |
 
 ### 9.3 请求示例
 

+ 164 - 0
sql/mysql/agent_aftersales_m1.sql

@@ -0,0 +1,164 @@
+CREATE TABLE IF NOT EXISTS agent_tool_registry (
+  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
+  tool_name VARCHAR(128) NOT NULL COMMENT 'MCP工具名',
+  tool_version VARCHAR(32) NOT NULL DEFAULT 'v1' COMMENT '工具版本',
+  description VARCHAR(512) NOT NULL COMMENT '工具说明',
+  transport VARCHAR(32) NOT NULL DEFAULT 'mcp' COMMENT '工具传输协议',
+  handler_bean VARCHAR(192) NOT NULL COMMENT 'Java处理器Bean或路由标识',
+  risk_level VARCHAR(16) NOT NULL COMMENT '风险等级: LOW, MEDIUM, HIGH',
+  enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
+  require_approval TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否需要审批',
+  idempotent TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否支持幂等',
+  input_schema_json JSON NOT NULL COMMENT '输入参数JSON Schema',
+  output_schema_json JSON NULL COMMENT '输出结果JSON Schema',
+  allowed_roles JSON NULL COMMENT '允许调用的角色列表',
+  sensitive_fields JSON NULL COMMENT '需要脱敏的字段列表',
+  rate_limit_per_minute INT NOT NULL DEFAULT 60 COMMENT '单工具分钟限流',
+  timeout_ms INT NOT NULL DEFAULT 10000 COMMENT '工具超时时间',
+  remark VARCHAR(512) NULL COMMENT '备注',
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
+  UNIQUE KEY uk_agent_tool_name_version (tool_name, tool_version),
+  KEY idx_agent_tool_enabled_risk (enabled, risk_level),
+  KEY idx_agent_tool_handler (handler_bean)
+) COMMENT='Agent MCP工具注册表';
+
+CREATE TABLE IF NOT EXISTS agent_approval (
+  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
+  approval_id VARCHAR(64) NOT NULL COMMENT '审批ID',
+  trace_id VARCHAR(64) NOT NULL COMMENT 'Agent运行链路ID',
+  pending_action_id BIGINT NULL COMMENT '关联冻结动作ID',
+  biz_type VARCHAR(64) NOT NULL COMMENT '业务类型',
+  biz_no VARCHAR(128) NULL COMMENT '业务编号',
+  risk_level VARCHAR(16) NOT NULL COMMENT '风险等级',
+  status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '状态',
+  request_user_id BIGINT NULL COMMENT '发起用户ID',
+  request_username VARCHAR(128) NULL COMMENT '发起用户名',
+  request_reason VARCHAR(1024) NULL COMMENT '申请原因',
+  risk_reason VARCHAR(1024) NULL COMMENT '命中高风险的原因',
+  policy_evidence_json JSON NULL COMMENT '政策依据和RAG命中证据',
+  requested_amount DECIMAL(10,2) NULL COMMENT '申请金额',
+  approver_id BIGINT NULL COMMENT '审批人ID',
+  approver_username VARCHAR(128) NULL COMMENT '审批人名称',
+  decision_comment VARCHAR(1024) NULL COMMENT '审批意见',
+  decision_time DATETIME NULL COMMENT '审批时间',
+  expires_at DATETIME NOT NULL COMMENT '审批过期时间',
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
+  deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
+  UNIQUE KEY uk_agent_approval_id (approval_id),
+  KEY idx_agent_approval_trace (trace_id),
+  KEY idx_agent_approval_status_time (status, create_time),
+  KEY idx_agent_approval_request_user (request_user_id, status),
+  KEY idx_agent_approval_biz (biz_type, biz_no)
+) COMMENT='Agent高风险动作审批表';
+
+CREATE TABLE IF NOT EXISTS agent_pending_action (
+  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
+  action_id VARCHAR(64) NOT NULL COMMENT '冻结动作ID',
+  approval_id VARCHAR(64) NOT NULL COMMENT '关联审批ID',
+  trace_id VARCHAR(64) NOT NULL COMMENT 'Agent运行链路ID',
+  tool_name VARCHAR(128) NOT NULL COMMENT '待执行MCP工具名',
+  tool_version VARCHAR(32) NOT NULL DEFAULT 'v1' COMMENT '工具版本',
+  risk_level VARCHAR(16) NOT NULL COMMENT '风险等级',
+  params_json JSON NOT NULL COMMENT '冻结后的工具参数',
+  params_hash CHAR(64) NOT NULL COMMENT '参数SHA-256摘要',
+  idempotency_key VARCHAR(128) NOT NULL COMMENT '幂等键',
+  execute_status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT '执行状态',
+  execute_result_json JSON NULL COMMENT '执行结果',
+  error_message TEXT NULL COMMENT '失败原因',
+  retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
+  executed_at DATETIME NULL COMMENT '执行完成时间',
+  expires_at DATETIME NOT NULL COMMENT '冻结动作过期时间',
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
+  deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
+  UNIQUE KEY uk_agent_action_id (action_id),
+  UNIQUE KEY uk_agent_action_idempotency (idempotency_key),
+  KEY idx_agent_action_approval (approval_id),
+  KEY idx_agent_action_trace (trace_id),
+  KEY idx_agent_action_status_time (execute_status, create_time),
+  KEY idx_agent_action_tool (tool_name, tool_version)
+) COMMENT='Agent审批后待恢复执行动作表';
+
+CREATE TABLE IF NOT EXISTS agent_audit_log (
+  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
+  trace_id VARCHAR(64) NOT NULL COMMENT 'Agent运行链路ID',
+  span_id VARCHAR(64) NULL COMMENT '步骤Span ID',
+  parent_span_id VARCHAR(64) NULL COMMENT '父级Span ID',
+  approval_id VARCHAR(64) NULL COMMENT '关联审批ID',
+  pending_action_id VARCHAR(64) NULL COMMENT '关联冻结动作ID',
+  actor_type VARCHAR(32) NOT NULL COMMENT '操作者类型',
+  actor_id VARCHAR(64) NULL COMMENT '操作者ID',
+  actor_name VARCHAR(128) NULL COMMENT '操作者名称',
+  step VARCHAR(64) NOT NULL COMMENT '步骤',
+  event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
+  tool_name VARCHAR(128) NULL COMMENT '工具名',
+  risk_level VARCHAR(16) NULL COMMENT '风险等级',
+  input_json JSON NULL COMMENT '输入快照',
+  output_json JSON NULL COMMENT '输出快照',
+  status VARCHAR(32) NOT NULL COMMENT '状态',
+  error_message TEXT NULL COMMENT '错误信息',
+  duration_ms BIGINT NULL COMMENT '步骤耗时毫秒',
+  ip_address VARCHAR(64) NULL COMMENT '客户端IP',
+  user_agent VARCHAR(512) NULL COMMENT '客户端User-Agent',
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  KEY idx_agent_audit_trace_time (trace_id, create_time),
+  KEY idx_agent_audit_approval (approval_id),
+  KEY idx_agent_audit_action (pending_action_id),
+  KEY idx_agent_audit_tool_time (tool_name, create_time),
+  KEY idx_agent_audit_status_time (status, create_time)
+) COMMENT='Agent全链路审计日志表';
+
+CREATE TABLE IF NOT EXISTS aftersales_work_order (
+  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
+  work_order_no VARCHAR(64) NOT NULL COMMENT '售后工单号',
+  trace_id VARCHAR(64) NOT NULL COMMENT 'Agent运行链路ID',
+  order_no VARCHAR(128) NOT NULL COMMENT '充电订单号',
+  charge_record_id BIGINT NULL COMMENT '充电记录ID',
+  user_id BIGINT NULL COMMENT '用户ID',
+  phone_masked VARCHAR(64) NULL COMMENT '脱敏手机号',
+  issue_type VARCHAR(64) NOT NULL COMMENT '问题类型',
+  issue_level VARCHAR(16) NOT NULL DEFAULT 'normal' COMMENT '问题等级',
+  source VARCHAR(32) NOT NULL DEFAULT 'agent' COMMENT '来源',
+  suggestion TEXT NOT NULL COMMENT 'Agent处理建议',
+  policy_evidence_json JSON NULL COMMENT '政策依据和RAG命中证据',
+  order_snapshot_json JSON NULL COMMENT '订单和充电记录快照',
+  status VARCHAR(32) NOT NULL DEFAULT 'created' COMMENT '状态',
+  created_by_agent TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否Agent创建',
+  created_by VARCHAR(64) NULL COMMENT '创建人',
+  assigned_to VARCHAR(64) NULL COMMENT '处理人',
+  resolve_result TEXT NULL COMMENT '处理结果',
+  closed_at DATETIME NULL COMMENT '关闭时间',
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
+  UNIQUE KEY uk_aftersales_work_order_no (work_order_no),
+  KEY idx_aftersales_order_no (order_no),
+  KEY idx_aftersales_trace (trace_id),
+  KEY idx_aftersales_status_time (status, create_time),
+  KEY idx_aftersales_issue_type (issue_type, create_time)
+) COMMENT='售后工单表';
+
+INSERT INTO agent_tool_registry
+  (tool_name, tool_version, description, transport, handler_bean, risk_level, enabled, require_approval, idempotent, input_schema_json, output_schema_json)
+VALUES
+  ('query_charge_order', 'v1', '查询充电订单', 'mcp', 'defaultAgentBusinessToolExecutor', 'LOW', 1, 0, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('query_charge_session', 'v1', '查询充电记录', 'mcp', 'defaultAgentBusinessToolExecutor', 'LOW', 1, 0, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('retrieve_aftersales_policy', 'v1', '检索售后政策', 'mcp', 'defaultAgentBusinessToolExecutor', 'LOW', 1, 0, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('create_aftersales_work_order', 'v1', '创建售后工单', 'mcp', 'defaultAgentBusinessToolExecutor', 'LOW', 1, 0, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('request_refund', 'v1', '发起退款审批', 'mcp', 'defaultAgentBusinessToolExecutor', 'HIGH', 1, 1, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('request_compensation', 'v1', '发起补偿审批', 'mcp', 'defaultAgentBusinessToolExecutor', 'HIGH', 1, 1, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('resume_after_approval', 'v1', '审批后恢复冻结动作', 'mcp', 'agentToolOrchestrator', 'HIGH', 1, 0, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('get_approval_status', 'v1', '查询审批状态', 'mcp', 'agentToolOrchestrator', 'LOW', 1, 0, 1, JSON_OBJECT(), JSON_OBJECT()),
+  ('write_agent_audit', 'v1', '写入Agent审计日志', 'mcp', 'defaultAgentBusinessToolExecutor', 'LOW', 1, 0, 1, JSON_OBJECT(), JSON_OBJECT())
+ON DUPLICATE KEY UPDATE
+  description = VALUES(description),
+  handler_bean = VALUES(handler_bean),
+  risk_level = VALUES(risk_level),
+  enabled = VALUES(enabled),
+  require_approval = VALUES(require_approval),
+  update_time = CURRENT_TIMESTAMP;

+ 55 - 0
sql/postgresql/aftersales_policy_pgvector.sql

@@ -0,0 +1,55 @@
+CREATE EXTENSION IF NOT EXISTS vector;
+
+CREATE TABLE IF NOT EXISTS aftersales_policy_doc (
+  id BIGSERIAL PRIMARY KEY,
+  doc_code VARCHAR(64) NOT NULL,
+  title VARCHAR(256) NOT NULL,
+  category VARCHAR(64) NOT NULL,
+  issue_type VARCHAR(64) NOT NULL,
+  version VARCHAR(32) NOT NULL,
+  content TEXT NOT NULL,
+  content_hash CHAR(64) NOT NULL,
+  source_type VARCHAR(32) NOT NULL DEFAULT 'manual',
+  source_uri VARCHAR(512),
+  effective_at TIMESTAMPTZ,
+  expired_at TIMESTAMPTZ,
+  enabled BOOLEAN NOT NULL DEFAULT TRUE,
+  metadata JSONB,
+  create_time TIMESTAMPTZ NOT NULL DEFAULT now(),
+  update_time TIMESTAMPTZ NOT NULL DEFAULT now(),
+  UNIQUE (doc_code, version)
+);
+
+CREATE INDEX IF NOT EXISTS idx_policy_doc_issue_enabled
+  ON aftersales_policy_doc (issue_type, enabled);
+
+CREATE INDEX IF NOT EXISTS idx_policy_doc_category
+  ON aftersales_policy_doc (category);
+
+CREATE TABLE IF NOT EXISTS aftersales_policy_chunk (
+  id BIGSERIAL PRIMARY KEY,
+  doc_id BIGINT NOT NULL REFERENCES aftersales_policy_doc(id),
+  chunk_no INT NOT NULL,
+  chunk_text TEXT NOT NULL,
+  token_count INT,
+  embedding VECTOR(1536) NOT NULL,
+  content_hash CHAR(64) NOT NULL,
+  issue_type VARCHAR(64) NOT NULL,
+  metadata JSONB,
+  enabled BOOLEAN NOT NULL DEFAULT TRUE,
+  create_time TIMESTAMPTZ NOT NULL DEFAULT now(),
+  update_time TIMESTAMPTZ NOT NULL DEFAULT now(),
+  UNIQUE (doc_id, chunk_no)
+);
+
+CREATE INDEX IF NOT EXISTS idx_policy_chunk_doc
+  ON aftersales_policy_chunk (doc_id);
+
+CREATE INDEX IF NOT EXISTS idx_policy_chunk_issue_enabled
+  ON aftersales_policy_chunk (issue_type, enabled);
+
+CREATE INDEX IF NOT EXISTS idx_policy_chunk_metadata
+  ON aftersales_policy_chunk USING GIN (metadata);
+
+CREATE INDEX IF NOT EXISTS idx_policy_chunk_embedding
+  ON aftersales_policy_chunk USING hnsw (embedding vector_cosine_ops);

+ 12 - 0
src/main/java/com/zsElectric/boot/business/controller/ThirdPartyInfoController.java

@@ -1,8 +1,10 @@
 package com.zsElectric.boot.business.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zsElectric.boot.business.model.entity.ThirdPartyInfo;
 import com.zsElectric.boot.business.model.form.ThirdPartyInfoForm;
 import com.zsElectric.boot.business.model.query.ThirdPartyInfoQuery;
+import com.zsElectric.boot.business.model.vo.PartyStationInfoVO;
 import com.zsElectric.boot.business.model.vo.ThirdPartyInfoVO;
 import com.zsElectric.boot.business.service.ThirdPartyInfoService;
 import com.zsElectric.boot.common.annotation.Log;
@@ -16,6 +18,8 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 /**
  * 第三方对接信息控制器
  *
@@ -38,6 +42,14 @@ public class ThirdPartyInfoController {
         return PageResult.success(result);
     }
 
+    @Operation(summary = "获取第三方对接信息列表")
+    @GetMapping("/list")
+    @Log(value = "获取第三方对接信息列表", module = LogModuleEnum.OTHER)
+    public Result<List<ThirdPartyInfoVO>> getThirdPartyInfoList() {
+        List<ThirdPartyInfoVO> result = thirdPartyInfoService.getThirdPartyInfoList();
+        return Result.success(result);
+    }
+
     @Operation(summary = "根据ID获取渠道方对接信息详情")
     @GetMapping("/{id}")
     @Log(value = "获取渠道方对接信息详情", module = LogModuleEnum.OTHER)

+ 3 - 0
src/main/java/com/zsElectric/boot/business/model/query/ChargeOrderInfoQuery.java

@@ -56,4 +56,7 @@ public class ChargeOrderInfoQuery extends BasePageQuery {
     @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime endTime;
+
+    @Schema(description = "运营商ID")
+    private String operatorId;
 }

+ 2 - 0
src/main/java/com/zsElectric/boot/business/model/vo/ChargeOrderInfoVO.java

@@ -124,4 +124,6 @@ public class ChargeOrderInfoVO implements Serializable {
     private String phone;
     @Schema(description = "电池soc%")
     private String soc;
+    @Schema(description = "运营商")
+    private String ecName;
 }

+ 1 - 1
src/main/java/com/zsElectric/boot/business/quartz/CompensateOrderJob.java

@@ -29,7 +29,7 @@ public class CompensateOrderJob {
      * 查找状态为3(已完成)或5(未成功充电)且充电数据为0的订单
      * 优先通过third_party_api_log表获取推送数据,备选通过third_party_charge_status表查询
      */
-    @Scheduled(cron = "${job.compensate-order.cron:0 0/10 * * * ?}")
+//    @Scheduled(cron = "${job.compensate-order.cron:0 0/10 * * * ?}")
     public void compensateChargeOrder() {
         if (!running.compareAndSet(false, true)) {
             log.warn("充电订单补偿定时任务正在执行中,跳过本次调度");

+ 1 - 1
src/main/java/com/zsElectric/boot/business/quartz/FailOrderDisposeJob.java

@@ -18,7 +18,7 @@ public class FailOrderDisposeJob {
     /**
      * 每3分钟执行一次
      */
-    @Scheduled(cron = "0 */3 * * * ?")
+//    @Scheduled(cron = "0 */3 * * * ?")
     public void failOrderDispose(){
         log.info("开始执行失败订单处理定时任务");
 

+ 3 - 0
src/main/java/com/zsElectric/boot/business/service/ThirdPartyInfoService.java

@@ -60,4 +60,7 @@ public interface ThirdPartyInfoService {
      * 获取渠道方集合
      * */
     List<PartyStationInfoVO> getPartyStationInfoList();
+
+    List<ThirdPartyInfoVO> getThirdPartyInfoList();
+
 }

+ 26 - 26
src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java

@@ -386,9 +386,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
      * @return
      */
     public AppChargeVO channelInvokeCharge(AppInvokeChargeForm formData) throws JsonProcessingException {
-        if (StrUtil.isBlank(formData.getOperatorId())) {
-            throw new BusinessException("运营商ID不能为空");
-        }
+
         if (StrUtil.isBlank(formData.getChannelOrderNo())) {
             throw new BusinessException("渠道订单号不能为空");
         }
@@ -399,28 +397,6 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
             throw new BusinessException("渠道预支付金额必须大于0");
         }
 
-        ThirdPartyInfo thirdPartyInfo = thirdPartyInfoMapper.selectOne(
-                Wrappers.lambdaQuery(ThirdPartyInfo.class)
-                        .eq(ThirdPartyInfo::getOperatorId, formData.getOperatorId())
-                        .last("limit 1"));
-        if (thirdPartyInfo == null) {
-            throw new BusinessException("渠道运营商不存在");
-        }
-
-        UserInfo channelUser = userInfoService.getUserInfoByPhoneAndOperatorId(
-                formData.getChannelUserPhone(),
-                thirdPartyInfo.getId()
-        );
-        if (channelUser == null) {
-            channelUser = userInfoService.registerThirdPartyUserByPhone(
-                    formData.getChannelUserPhone(),
-                    thirdPartyInfo.getId()
-            );
-        }
-        if (channelUser == null) {
-            throw new BusinessException("渠道用户不存在且自动注册失败");
-        }
-
         String seq = ConnectivityConstants.OPERATOR_ID + formData.getChannelOrderNo();
 
         //请求设备认证
@@ -436,13 +412,37 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
         //创建订单
         ChargeOrderInfo chargeOrderInfo = new ChargeOrderInfo();
         Long currentUserId = SecurityUtils.getUserId();
-        chargeOrderInfo.setUserId(channelUser.getId());
         User user = currentUserId == null ? null : userMapper.selectById(currentUserId);
         if(ObjectUtil.isNotEmpty(user)){
             FirmInfo firmInfo = firmInfoMapper.selectOne(Wrappers.lambdaQuery(FirmInfo.class).eq(FirmInfo::getDeptId, user.getDeptId()).last("limit 1"));
             if(firmInfo != null) {
                 chargeOrderInfo.setFirmId(firmInfo.getId());
             }
+            chargeOrderInfo.setUserId(user.getId());
+        }
+
+        if (StrUtil.isNotBlank(formData.getOperatorId())) {
+            ThirdPartyInfo thirdPartyInfo = thirdPartyInfoMapper.selectOne(
+                    Wrappers.lambdaQuery(ThirdPartyInfo.class)
+                            .eq(ThirdPartyInfo::getOperatorId, formData.getOperatorId())
+                            .last("limit 1"));
+            if (thirdPartyInfo == null) {
+                throw new BusinessException("渠道运营商不存在");
+            }
+            UserInfo channelUser = userInfoService.getUserInfoByPhoneAndOperatorId(
+                    formData.getChannelUserPhone(),
+                    thirdPartyInfo.getId()
+            );
+            if (channelUser == null) {
+                channelUser = userInfoService.registerThirdPartyUserByPhone(
+                        formData.getChannelUserPhone(),
+                        thirdPartyInfo.getId()
+                );
+            }
+            if (channelUser == null) {
+                throw new BusinessException("渠道用户不存在且自动注册失败");
+            }
+            chargeOrderInfo.setUserId(channelUser.getId());
         }
 
         chargeOrderInfo.setOrderType(SystemConstants.CHARGE_ORDER_TYPE_CHANNEL);

+ 13 - 0
src/main/java/com/zsElectric/boot/business/service/impl/ThirdPartyInfoServiceImpl.java

@@ -89,4 +89,17 @@ public class ThirdPartyInfoServiceImpl implements ThirdPartyInfoService {
                 })
                 .collect(Collectors.toList());
     }
+
+    @Override
+    public List<ThirdPartyInfoVO> getThirdPartyInfoList() {
+        List<ThirdPartyInfo> thirdPartyInfos = thirdPartyInfoMapper.selectList(null);
+        // 转换为VO列表
+        return thirdPartyInfos.stream()
+                .map(info -> {
+                    ThirdPartyInfoVO vo = new ThirdPartyInfoVO();
+                    BeanUtils.copyProperties(info, vo);
+                    return vo;
+                })
+                .collect(Collectors.toList());
+    }
 }

+ 2 - 0
src/main/java/com/zsElectric/boot/business/service/impl/UserInfoServiceImpl.java

@@ -175,6 +175,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
         return this.getOne(
                 new LambdaQueryWrapper<UserInfo>()
                         .eq(UserInfo::getOpenid, openid)
+                        .isNull(UserInfo::getThirdPartId)
         );
     }
 
@@ -192,6 +193,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
         return this.getOne(
                 new LambdaQueryWrapper<UserInfo>()
                         .eq(UserInfo::getPhone, phone)
+                        .isNull(UserInfo::getThirdPartId)
         );
     }
 

+ 2 - 2
src/main/java/com/zsElectric/boot/charging/quartz/ChargingJob.java

@@ -39,7 +39,7 @@ public class ChargingJob {
      * 同步充电站信息
      * 每5分钟执行一次,从第三方接口获取充电站信息并存储到数据库
      */
-    @Scheduled(cron = "0 0/15 * * * ?")
+//    @Scheduled(cron = "0 0/15 * * * ?")
     public void syncStationsInfo() {
         log.info("开始执行充电站信息同步定时任务");
 
@@ -76,7 +76,7 @@ public class ChargingJob {
      * 每10分钟执行一次,查询所有充电桩的价格策略并存储到数据库
      * cron表达式: 0 10 * * * ? 表示每10分钟执行
      */
-    @Scheduled(cron = "0 */10 * * * ?")
+//    @Scheduled(cron = "0 */10 * * * ?")
     public void syncEquipmentPricePolicy() {
         // 检查任务是否正在执行,防止并发
         if (isPricePolicySyncRunning) {

+ 51 - 0
src/main/java/com/zsElectric/boot/platform/agent/config/AgentToolConfig.java

@@ -0,0 +1,51 @@
+package com.zsElectric.boot.platform.agent.config;
+
+import com.zsElectric.boot.platform.agent.service.AgentApprovalStore;
+import com.zsElectric.boot.platform.agent.service.AgentAuditWriter;
+import com.zsElectric.boot.platform.agent.service.AgentBusinessToolExecutor;
+import com.zsElectric.boot.platform.agent.service.AgentToolOrchestrator;
+import com.zsElectric.boot.platform.agent.tool.AgentToolCatalog;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Clock;
+
+/**
+ * Agent 工具控制面配置。
+ *
+ * <p>将白名单目录、编排器和时钟注册为 Spring Bean。这样测试可以直接 new 编排器,
+ * 生产环境则由 Spring 注入 MyBatis 持久化端口和默认业务工具执行器。</p>
+ */
+@Configuration
+public class AgentToolConfig {
+
+    /**
+     * 注册默认工具白名单。
+     */
+    @Bean
+    public AgentToolCatalog agentToolCatalog() {
+        return AgentToolCatalog.defaultCatalog();
+    }
+
+    /**
+     * 注册统一时钟。时间相关逻辑集中从 Clock 获取,便于后续测试和排查。
+     */
+    @Bean
+    public Clock agentClock() {
+        return Clock.systemDefaultZone();
+    }
+
+    /**
+     * 注册 Agent 工具编排器。
+     */
+    @Bean
+    public AgentToolOrchestrator agentToolOrchestrator(
+            AgentToolCatalog agentToolCatalog,
+            AgentApprovalStore approvalStore,
+            AgentBusinessToolExecutor toolExecutor,
+            AgentAuditWriter auditWriter,
+            Clock agentClock
+    ) {
+        return new AgentToolOrchestrator(agentToolCatalog, approvalStore, toolExecutor, auditWriter, agentClock);
+    }
+}

+ 107 - 0
src/main/java/com/zsElectric/boot/platform/agent/controller/AgentApprovalController.java

@@ -0,0 +1,107 @@
+package com.zsElectric.boot.platform.agent.controller;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.zsElectric.boot.core.web.Result;
+import com.zsElectric.boot.platform.agent.mapper.AgentApprovalMapper;
+import com.zsElectric.boot.platform.agent.model.dto.AgentApprovalDecisionRequest;
+import com.zsElectric.boot.platform.agent.model.entity.AgentApproval;
+import com.zsElectric.boot.platform.agent.service.AgentToolOrchestrator;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * Agent 高风险动作审批演示接口。
+ *
+ * <p>首版不做完整审批后台,使用 Swagger/Postman 调这些接口完成审批演示。
+ * 审批通过或拒绝只改变审批状态;真正的退款/补偿恢复执行仍由 Python 调用
+ * resume_after_approval 工具触发。</p>
+ */
+@Tag(name = "Agent审批接口")
+@RestController
+@RequestMapping("/api/v1/agent/approvals")
+@RequiredArgsConstructor
+public class AgentApprovalController {
+
+    /**
+     * 审批单 Mapper,用于列表和详情查询。
+     */
+    private final AgentApprovalMapper agentApprovalMapper;
+    /**
+     * 审批决策委托给编排器,保证状态流转和审计逻辑集中。
+     */
+    private final AgentToolOrchestrator agentToolOrchestrator;
+
+    /**
+     * 查询审批列表。
+     *
+     * <p>limit 被限制在 1 到 100,避免演示接口一次拉取过多数据。</p>
+     */
+    @Operation(summary = "获取Agent审批列表")
+    @GetMapping
+    public Result<List<AgentApproval>> listApprovals(
+            @RequestParam(required = false) String status,
+            @RequestParam(defaultValue = "20") Integer limit
+    ) {
+        List<AgentApproval> approvals = agentApprovalMapper.selectList(Wrappers.lambdaQuery(AgentApproval.class)
+                .eq(status != null && !status.isBlank(), AgentApproval::getStatus, status)
+                .orderByDesc(AgentApproval::getCreateTime)
+                .last("limit " + Math.max(1, Math.min(limit, 100))));
+        return Result.success(approvals);
+    }
+
+    /**
+     * 查询单个审批详情。
+     */
+    @Operation(summary = "获取Agent审批详情")
+    @GetMapping("/{approvalId}")
+    public Result<AgentApproval> getApproval(@PathVariable String approvalId) {
+        AgentApproval approval = agentApprovalMapper.selectOne(Wrappers.lambdaQuery(AgentApproval.class)
+                .eq(AgentApproval::getApprovalId, approvalId)
+                .last("limit 1"));
+        return Result.success(approval);
+    }
+
+    /**
+     * 审批通过。
+     *
+     * <p>该接口不会直接执行业务动作,只把审批状态置为 approved。</p>
+     */
+    @Operation(summary = "通过Agent审批")
+    @PostMapping("/{approvalId}/approve")
+    public Result<Void> approve(
+            @PathVariable String approvalId,
+            @RequestBody AgentApprovalDecisionRequest request
+    ) {
+        agentToolOrchestrator.approve(
+                approvalId,
+                request.getApproverId(),
+                request.getApproverUsername(),
+                request.getDecisionComment()
+        );
+        return Result.success();
+    }
+
+    /**
+     * 审批拒绝。
+     *
+     * <p>拒绝后冻结动作会同步标记 rejected,resume 时返回确定结果。</p>
+     */
+    @Operation(summary = "拒绝Agent审批")
+    @PostMapping("/{approvalId}/reject")
+    public Result<Void> reject(
+            @PathVariable String approvalId,
+            @RequestBody AgentApprovalDecisionRequest request
+    ) {
+        agentToolOrchestrator.reject(
+                approvalId,
+                request.getApproverId(),
+                request.getApproverUsername(),
+                request.getDecisionComment()
+        );
+        return Result.success();
+    }
+}

+ 68 - 0
src/main/java/com/zsElectric/boot/platform/agent/controller/AgentMcpController.java

@@ -0,0 +1,68 @@
+package com.zsElectric.boot.platform.agent.controller;
+
+import com.zsElectric.boot.core.web.Result;
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallRequest;
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallResponse;
+import com.zsElectric.boot.platform.agent.service.AgentToolOrchestrator;
+import com.zsElectric.boot.platform.agent.tool.AgentToolCatalog;
+import com.zsElectric.boot.platform.agent.tool.AgentToolDefinition;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * Agent MCP 工具入口。
+ *
+ * <p>Python Agent 只通过该控制器调用 Java 业务能力。控制器不会把所有 REST 接口
+ * 自动暴露给模型,而是把请求交给 AgentToolOrchestrator,由编排器执行白名单、
+ * 风险分级、审批、冻结动作和审计。</p>
+ */
+@Tag(name = "Agent MCP工具接口")
+@RestController
+@RequestMapping("/api/v1/agent/mcp")
+@RequiredArgsConstructor
+public class AgentMcpController {
+
+    /**
+     * 当前可暴露给模型的工具目录。
+     */
+    private final AgentToolCatalog agentToolCatalog;
+    /**
+     * 工具调用控制面,负责安全治理和业务执行分流。
+     */
+    private final AgentToolOrchestrator agentToolOrchestrator;
+
+    /**
+     * 获取白名单工具列表。
+     *
+     * <p>该接口给 Python Agent、Swagger 或 Postman 演示使用,便于确认当前哪些工具
+     * 可被调用以及对应风险等级。</p>
+     */
+    @Operation(summary = "获取Agent白名单工具")
+    @GetMapping("/tools")
+    public Result<List<AgentToolDefinition>> listTools() {
+        return Result.success(agentToolCatalog.list());
+    }
+
+    /**
+     * 调用指定 MCP 工具。
+     *
+     * <p>toolName 以路径参数为准,避免请求体伪造不同工具名。工具版本缺省为 v1。
+     * 后续如果需要按角色校验、IP 限流、签名校验,可以在进入编排器前统一补充。</p>
+     */
+    @Operation(summary = "调用Agent工具")
+    @PostMapping("/tools/{toolName}/call")
+    public Result<AgentToolCallResponse> callTool(
+            @PathVariable String toolName,
+            @RequestBody AgentToolCallRequest request
+    ) {
+        request.setToolName(toolName);
+        if (request.getToolVersion() == null || request.getToolVersion().isBlank()) {
+            request.setToolVersion("v1");
+        }
+        return Result.success(agentToolOrchestrator.call(request));
+    }
+}

+ 12 - 0
src/main/java/com/zsElectric/boot/platform/agent/mapper/AftersalesWorkOrderMapper.java

@@ -0,0 +1,12 @@
+package com.zsElectric.boot.platform.agent.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zsElectric.boot.platform.agent.model.entity.AftersalesWorkOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 售后工单 Mapper。
+ */
+@Mapper
+public interface AftersalesWorkOrderMapper extends BaseMapper<AftersalesWorkOrder> {
+}

+ 12 - 0
src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentApprovalMapper.java

@@ -0,0 +1,12 @@
+package com.zsElectric.boot.platform.agent.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zsElectric.boot.platform.agent.model.entity.AgentApproval;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * Agent 审批单 Mapper。
+ */
+@Mapper
+public interface AgentApprovalMapper extends BaseMapper<AgentApproval> {
+}

+ 12 - 0
src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentAuditLogMapper.java

@@ -0,0 +1,12 @@
+package com.zsElectric.boot.platform.agent.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zsElectric.boot.platform.agent.model.entity.AgentAuditLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * Agent 审计日志 Mapper。
+ */
+@Mapper
+public interface AgentAuditLogMapper extends BaseMapper<AgentAuditLog> {
+}

+ 12 - 0
src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentPendingActionMapper.java

@@ -0,0 +1,12 @@
+package com.zsElectric.boot.platform.agent.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zsElectric.boot.platform.agent.model.entity.AgentPendingAction;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * Agent 冻结动作 Mapper。
+ */
+@Mapper
+public interface AgentPendingActionMapper extends BaseMapper<AgentPendingAction> {
+}

+ 15 - 0
src/main/java/com/zsElectric/boot/platform/agent/mapper/AgentToolRegistryMapper.java

@@ -0,0 +1,15 @@
+package com.zsElectric.boot.platform.agent.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zsElectric.boot.platform.agent.model.entity.AgentToolRegistry;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * Agent 工具注册表 Mapper。
+ *
+ * <p>当前运行时白名单由 AgentToolCatalog 提供,该 Mapper 用于后续动态工具治理、
+ * 管理后台展示和 SQL 初始化验证。</p>
+ */
+@Mapper
+public interface AgentToolRegistryMapper extends BaseMapper<AgentToolRegistry> {
+}

+ 28 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/dto/AgentApprovalDecisionRequest.java

@@ -0,0 +1,28 @@
+package com.zsElectric.boot.platform.agent.model.dto;
+
+import lombok.Data;
+
+/**
+ * Agent 审批决策请求。
+ *
+ * <p>审批接口只修改审批状态,不直接执行业务动作。审批通过后仍需要调用
+ * resume_after_approval,按冻结动作恢复执行。</p>
+ */
+@Data
+public class AgentApprovalDecisionRequest {
+
+    /**
+     * 审批人 ID,用于审计和审批记录展示。
+     */
+    private String approverId;
+
+    /**
+     * 审批人名称。
+     */
+    private String approverUsername;
+
+    /**
+     * 审批意见,例如“同意退款”或“证据不足,拒绝补偿”。
+     */
+    private String decisionComment;
+}

+ 46 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/dto/AgentToolCallRequest.java

@@ -0,0 +1,46 @@
+package com.zsElectric.boot.platform.agent.model.dto;
+
+import lombok.Data;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Agent 工具调用请求。
+ *
+ * <p>Python Agent 调 Java MCP 工具时使用该结构。它不承载具体业务 DTO,
+ * 而是使用 params 保存工具参数,方便不同工具共享同一个入口。</p>
+ */
+@Data
+public class AgentToolCallRequest {
+
+    /**
+     * Agent 单次运行链路 ID。所有审批、冻结动作、审计、工单都应携带同一个 traceId。
+     */
+    private String traceId;
+
+    /**
+     * 工具名。Controller 会用路径中的 toolName 覆盖该字段,避免请求体和 URL 不一致。
+     */
+    private String toolName;
+
+    /**
+     * 工具版本。默认 v1,后续参数 schema 变化时可并存多个版本。
+     */
+    private String toolVersion = "v1";
+
+    /**
+     * 调用操作者 ID,通常来自 Python Agent 传入的后台用户或系统账号。
+     */
+    private String operatorId;
+
+    /**
+     * 调用操作者名称,用于审批和审计展示。
+     */
+    private String operatorName;
+
+    /**
+     * 工具参数。高风险工具会把该 Map 稳定序列化后写入 agent_pending_action.params_json。
+     */
+    private Map<String, Object> params = new LinkedHashMap<>();
+}

+ 107 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/dto/AgentToolCallResponse.java

@@ -0,0 +1,107 @@
+package com.zsElectric.boot.platform.agent.model.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * Agent 工具调用响应。
+ *
+ * <p>该结构统一承载低风险工具直接执行结果、高风险工具审批等待结果,以及失败原因。
+ * Python Agent 只需要根据 status 和 approvalRequired 判断下一步是回复用户、等待审批,
+ * 还是提示人工介入。</p>
+ */
+@Data
+public class AgentToolCallResponse {
+
+    /**
+     * 工具调用在控制面是否成功。审批等待也算成功,因为系统已经正确创建审批单。
+     */
+    private boolean success;
+
+    /**
+     * 是否需要人工审批。true 时 approvalId 必须有值。
+     */
+    private boolean approvalRequired;
+
+    /**
+     * 状态:executed、pending_approval、rejected、expired、failed 等。
+     */
+    private String status;
+
+    /**
+     * 本次 Agent 运行链路 ID。
+     */
+    private String traceId;
+
+    /**
+     * 实际调用的工具名。
+     */
+    private String toolName;
+
+    /**
+     * 审批 ID,高风险工具创建审批或恢复执行时返回。
+     */
+    private String approvalId;
+
+    /**
+     * 冻结动作 ID,用于定位 agent_pending_action。
+     */
+    private String pendingActionId;
+
+    /**
+     * 工具业务输出。低风险工具是业务结果;审批等待时包含 approval_id 等提示信息。
+     */
+    private Map<String, Object> output;
+
+    /**
+     * 失败原因。MCP 工具失败时不要让 Agent 猜测结果,应把明确错误返回给调用方。
+     */
+    private String errorMessage;
+
+    /**
+     * 构造已执行成功的工具响应。
+     */
+    public static AgentToolCallResponse executed(AgentToolCallRequest request, Map<String, Object> output) {
+        AgentToolCallResponse response = new AgentToolCallResponse();
+        response.setSuccess(true);
+        response.setStatus("executed");
+        response.setTraceId(request.getTraceId());
+        response.setToolName(request.getToolName());
+        response.setOutput(output);
+        return response;
+    }
+
+    /**
+     * 构造需要人工审批的工具响应。
+     */
+    public static AgentToolCallResponse pendingApproval(AgentToolCallRequest request, String approvalId, String actionId) {
+        AgentToolCallResponse response = new AgentToolCallResponse();
+        response.setSuccess(true);
+        response.setApprovalRequired(true);
+        response.setStatus("pending_approval");
+        response.setTraceId(request.getTraceId());
+        response.setToolName(request.getToolName());
+        response.setApprovalId(approvalId);
+        response.setPendingActionId(actionId);
+        response.setOutput(Map.of(
+                "approval_required", true,
+                "approval_id", approvalId,
+                "pending_action_id", actionId
+        ));
+        return response;
+    }
+
+    /**
+     * 构造工具失败响应。
+     */
+    public static AgentToolCallResponse failed(AgentToolCallRequest request, String message) {
+        AgentToolCallResponse response = new AgentToolCallResponse();
+        response.setSuccess(false);
+        response.setStatus("failed");
+        response.setTraceId(request.getTraceId());
+        response.setToolName(request.getToolName());
+        response.setErrorMessage(message);
+        return response;
+    }
+}

+ 99 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/entity/AftersalesWorkOrder.java

@@ -0,0 +1,99 @@
+package com.zsElectric.boot.platform.agent.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.zsElectric.boot.common.base.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * 售后工单实体。
+ *
+ * <p>低风险售后问题由 Agent 自动创建工单;高风险退款/补偿在审批恢复后也可以
+ * 写入工单,作为客服处理记录和审计入口。该表面向售后处理,不替代真实退款单或账户流水。</p>
+ */
+@Getter
+@Setter
+@TableName("aftersales_work_order")
+public class AftersalesWorkOrder extends BaseEntity {
+
+    /**
+     * 售后工单号,对外展示和客服检索使用。
+     */
+    private String workOrderNo;
+    /**
+     * Agent 运行链路 ID。
+     */
+    private String traceId;
+    /**
+     * 充电订单号。
+     */
+    private String orderNo;
+    /**
+     * 充电记录 ID,首版可为空,后续接入更细的充电会话表时回填。
+     */
+    private Long chargeRecordId;
+    /**
+     * 用户 ID。
+     */
+    private Long userId;
+    /**
+     * 脱敏手机号,避免工具结果和工单直接保存完整手机号。
+     */
+    private String phoneMasked;
+    /**
+     * 问题类型:charge_failed、payment_issue、refund_request、compensation_request。
+     */
+    private String issueType;
+    /**
+     * 问题等级:normal、urgent。
+     */
+    private String issueLevel;
+    /**
+     * 工单来源:agent、manual、third_party。
+     */
+    private String source;
+    /**
+     * Agent 生成的处理建议。
+     */
+    private String suggestion;
+    /**
+     * 政策依据和 RAG 命中证据 JSON。
+     */
+    private String policyEvidenceJson;
+    /**
+     * 订单和充电记录快照 JSON,方便后续复盘,不依赖实时订单状态。
+     */
+    private String orderSnapshotJson;
+    /**
+     * 工单状态:created、processing、resolved、closed、canceled 等。
+     */
+    private String status;
+    /**
+     * 是否由 Agent 创建。
+     */
+    private Integer createdByAgent;
+    /**
+     * 创建人,通常是 Agent 调用上下文中的 operatorName。
+     */
+    private String createdBy;
+    /**
+     * 分配处理人。
+     */
+    private String assignedTo;
+    /**
+     * 售后处理结果。
+     */
+    private String resolveResult;
+    /**
+     * 工单关闭时间。
+     */
+    private LocalDateTime closedAt;
+    /**
+     * 逻辑删除标记。
+     */
+    @TableLogic
+    private Integer deleted;
+}

+ 107 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentApproval.java

@@ -0,0 +1,107 @@
+package com.zsElectric.boot.platform.agent.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.Version;
+import com.zsElectric.boot.common.base.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * Agent 高风险动作审批单。
+ *
+ * <p>审批单只表达“是否允许执行某个高风险动作”,完整执行参数不放在这里,
+ * 而是放在 agent_pending_action。这样审批列表可以展示业务摘要和决策信息,
+ * 但真正恢复执行时仍以冻结动作为准。</p>
+ */
+@Getter
+@Setter
+@TableName("agent_approval")
+public class AgentApproval extends BaseEntity {
+
+    /**
+     * 外部展示和恢复执行使用的审批 ID。
+     */
+    private String approvalId;
+    /**
+     * Agent 运行链路 ID,贯穿审批、冻结动作、审计和工单。
+     */
+    private String traceId;
+    /**
+     * 关联冻结动作数据库主键。保存后回填,用于后台详情页快速定位。
+     */
+    private Long pendingActionId;
+    /**
+     * 业务类型,例如 refund、compensation。
+     */
+    private String bizType;
+    /**
+     * 业务编号,通常是充电订单号。
+     */
+    private String bizNo;
+    /**
+     * 风险等级,首版高风险审批固定为 HIGH。
+     */
+    private String riskLevel;
+    /**
+     * 审批状态:pending、approved、rejected、expired、executed。
+     */
+    private String status;
+    /**
+     * 发起审批的用户 ID,可能来自后台登录用户或 Agent 系统账号。
+     */
+    private Long requestUserId;
+    /**
+     * 发起审批的用户名。
+     */
+    private String requestUsername;
+    /**
+     * 申请原因,通常来自用户售后诉求或 Agent 生成的处理理由。
+     */
+    private String requestReason;
+    /**
+     * 命中高风险的原因,用于审批人理解为什么不能自动执行。
+     */
+    private String riskReason;
+    /**
+     * 政策依据和 RAG 命中证据 JSON,供审批人判断。
+     */
+    private String policyEvidenceJson;
+    /**
+     * 申请退款或补偿金额。
+     */
+    private BigDecimal requestedAmount;
+    /**
+     * 审批人 ID。
+     */
+    private Long approverId;
+    /**
+     * 审批人名称。
+     */
+    private String approverUsername;
+    /**
+     * 审批意见。
+     */
+    private String decisionComment;
+    /**
+     * 审批决策时间。
+     */
+    private LocalDateTime decisionTime;
+    /**
+     * 审批过期时间。过期后需要重新发起高风险申请。
+     */
+    private LocalDateTime expiresAt;
+    /**
+     * 乐观锁版本,防止多人同时审批覆盖状态。
+     */
+    @Version
+    private Integer version;
+    /**
+     * 逻辑删除标记。
+     */
+    @TableLogic
+    private Integer deleted;
+}

+ 104 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentAuditLog.java

@@ -0,0 +1,104 @@
+package com.zsElectric.boot.platform.agent.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * Agent 全链路审计日志。
+ *
+ * <p>审计表用于还原一次 Agent 运行的关键步骤,包括用户输入、意图判断、RAG 命中、
+ * 工具调用、审批创建、审批决策、恢复执行和最终回复。该表只追加,不用于驱动业务状态。</p>
+ */
+@Getter
+@Setter
+@TableName("agent_audit_log")
+public class AgentAuditLog {
+
+    /**
+     * 主键 ID。
+     */
+    private Long id;
+    /**
+     * Agent 运行链路 ID。
+     */
+    private String traceId;
+    /**
+     * 当前步骤 span ID,预留给更细粒度链路追踪。
+     */
+    private String spanId;
+    /**
+     * 父级 span ID,预留给树形调用链。
+     */
+    private String parentSpanId;
+    /**
+     * 关联审批 ID。
+     */
+    private String approvalId;
+    /**
+     * 关联冻结动作 ID。
+     */
+    private String pendingActionId;
+    /**
+     * 操作者类型:user、agent、system、approver。
+     */
+    private String actorType;
+    /**
+     * 操作者 ID。
+     */
+    private String actorId;
+    /**
+     * 操作者名称。
+     */
+    private String actorName;
+    /**
+     * 步骤名,例如 tool_called、approval_created、action_resumed。
+     */
+    private String step;
+    /**
+     * 事件类型,首版通常与 step 保持一致,后续可进一步细分。
+     */
+    private String eventType;
+    /**
+     * 关联工具名。
+     */
+    private String toolName;
+    /**
+     * 工具风险等级。
+     */
+    private String riskLevel;
+    /**
+     * 输入快照 JSON。后续可在写入前统一脱敏。
+     */
+    private String inputJson;
+    /**
+     * 输出快照 JSON。用于排查工具执行和 Agent 回复依据。
+     */
+    private String outputJson;
+    /**
+     * 步骤状态:success、failed、pending、skipped。
+     */
+    private String status;
+    /**
+     * 失败原因。
+     */
+    private String errorMessage;
+    /**
+     * 步骤耗时毫秒。
+     */
+    private Long durationMs;
+    /**
+     * 客户端 IP。
+     */
+    private String ipAddress;
+    /**
+     * 客户端 User-Agent。
+     */
+    private String userAgent;
+    /**
+     * 审计创建时间。
+     */
+    private LocalDateTime createTime;
+}

+ 94 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentPendingAction.java

@@ -0,0 +1,94 @@
+package com.zsElectric.boot.platform.agent.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.Version;
+import com.zsElectric.boot.common.base.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * Agent 待恢复执行的冻结动作。
+ *
+ * <p>当模型请求退款、补偿等高风险工具时,系统不会立即执行业务动作,而是把工具名、
+ * 工具版本、参数 JSON、参数哈希和幂等键冻结到这张表。审批通过后,
+ * resume_after_approval 只能按这张表保存的参数恢复执行,不能让模型重新生成参数。</p>
+ */
+@Getter
+@Setter
+@TableName("agent_pending_action")
+public class AgentPendingAction extends BaseEntity {
+
+    /**
+     * 外部展示用冻结动作 ID。
+     */
+    private String actionId;
+    /**
+     * 关联审批 ID。
+     */
+    private String approvalId;
+    /**
+     * Agent 运行链路 ID。
+     */
+    private String traceId;
+    /**
+     * 审批通过后要执行的工具名。
+     */
+    private String toolName;
+    /**
+     * 工具版本。
+     */
+    private String toolVersion;
+    /**
+     * 风险等级。
+     */
+    private String riskLevel;
+    /**
+     * 冻结后的工具参数 JSON。
+     */
+    private String paramsJson;
+    /**
+     * params_json 的 SHA-256 摘要,用于恢复执行前防篡改校验。
+     */
+    private String paramsHash;
+    /**
+     * 幂等键,用于防止重复恢复导致重复退款或重复补偿。
+     */
+    private String idempotencyKey;
+    /**
+     * 执行状态:pending、executing、executed、failed、expired、rejected。
+     */
+    private String executeStatus;
+    /**
+     * 执行成功后的结果 JSON。重复 resume 时直接返回该历史结果。
+     */
+    private String executeResultJson;
+    /**
+     * 执行失败或校验失败原因。
+     */
+    private String errorMessage;
+    /**
+     * 重试次数,预留给后续失败重试策略。
+     */
+    private Integer retryCount;
+    /**
+     * 实际执行完成时间。
+     */
+    private LocalDateTime executedAt;
+    /**
+     * 冻结动作过期时间。
+     */
+    private LocalDateTime expiresAt;
+    /**
+     * 乐观锁版本。
+     */
+    @Version
+    private Integer version;
+    /**
+     * 逻辑删除标记。
+     */
+    @TableLogic
+    private Integer deleted;
+}

+ 90 - 0
src/main/java/com/zsElectric/boot/platform/agent/model/entity/AgentToolRegistry.java

@@ -0,0 +1,90 @@
+package com.zsElectric.boot.platform.agent.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.zsElectric.boot.common.base.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Agent 工具注册表实体。
+ *
+ * <p>该表用于治理 MCP 工具:记录工具名、版本、处理器、风险等级、审批要求、输入输出 schema
+ * 和限流策略。当前运行时代码先使用 AgentToolCatalog 内置白名单,数据库表用于后续动态治理
+ * 和运营审计。</p>
+ */
+@Getter
+@Setter
+@TableName("agent_tool_registry")
+public class AgentToolRegistry extends BaseEntity {
+
+    /**
+     * MCP 工具名。
+     */
+    private String toolName;
+    /**
+     * 工具版本。
+     */
+    private String toolVersion;
+    /**
+     * 工具说明。
+     */
+    private String description;
+    /**
+     * 工具传输协议,首版固定 mcp。
+     */
+    private String transport;
+    /**
+     * Java 处理器 Bean 或路由标识。
+     */
+    private String handlerBean;
+    /**
+     * 风险等级:LOW、MEDIUM、HIGH。
+     */
+    private String riskLevel;
+    /**
+     * 是否启用。
+     */
+    private Integer enabled;
+    /**
+     * 是否需要审批。
+     */
+    private Integer requireApproval;
+    /**
+     * 是否支持幂等。
+     */
+    private Integer idempotent;
+    /**
+     * 输入参数 JSON Schema。
+     */
+    private String inputSchemaJson;
+    /**
+     * 输出结果 JSON Schema。
+     */
+    private String outputSchemaJson;
+    /**
+     * 允许调用的角色列表 JSON。
+     */
+    private String allowedRoles;
+    /**
+     * 需要脱敏的敏感字段列表 JSON。
+     */
+    private String sensitiveFields;
+    /**
+     * 单工具每分钟限流阈值。
+     */
+    private Integer rateLimitPerMinute;
+    /**
+     * 工具超时时间,单位毫秒。
+     */
+    private Integer timeoutMs;
+    /**
+     * 备注。
+     */
+    private String remark;
+    /**
+     * 逻辑删除标记。
+     */
+    @TableLogic
+    private Integer deleted;
+}

+ 45 - 0
src/main/java/com/zsElectric/boot/platform/agent/service/AgentApprovalStore.java

@@ -0,0 +1,45 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.zsElectric.boot.platform.agent.model.entity.AgentApproval;
+import com.zsElectric.boot.platform.agent.model.entity.AgentPendingAction;
+
+import java.util.Optional;
+
+/**
+ * Agent 审批和冻结动作存储端口。
+ *
+ * <p>编排器依赖该接口而不是直接依赖 MyBatis,便于单元测试使用内存实现。
+ * 生产环境由 MyBatisAgentApprovalStore 负责落库。</p>
+ */
+public interface AgentApprovalStore {
+
+    /**
+     * 保存审批单。
+     */
+    void saveApproval(AgentApproval approval);
+
+    /**
+     * 保存冻结动作。
+     */
+    void savePendingAction(AgentPendingAction pendingAction);
+
+    /**
+     * 按审批 ID 查询审批单。
+     */
+    Optional<AgentApproval> findApproval(String approvalId);
+
+    /**
+     * 按审批 ID 查询关联冻结动作。
+     */
+    Optional<AgentPendingAction> findPendingAction(String approvalId);
+
+    /**
+     * 更新审批单状态或决策信息。
+     */
+    void updateApproval(AgentApproval approval);
+
+    /**
+     * 更新冻结动作执行状态或结果。
+     */
+    void updatePendingAction(AgentPendingAction pendingAction);
+}

+ 17 - 0
src/main/java/com/zsElectric/boot/platform/agent/service/AgentAuditWriter.java

@@ -0,0 +1,17 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.zsElectric.boot.platform.agent.model.entity.AgentAuditLog;
+
+/**
+ * Agent 审计写入端口。
+ *
+ * <p>所有关键控制面事件都通过该接口写入 agent_audit_log。后续如需统一敏感字段脱敏、
+ * 异步写入或日志降级,可以替换实现而不改编排器。</p>
+ */
+public interface AgentAuditWriter {
+
+    /**
+     * 追加一条审计日志。
+     */
+    void write(AgentAuditLog auditLog);
+}

+ 24 - 0
src/main/java/com/zsElectric/boot/platform/agent/service/AgentBusinessToolExecutor.java

@@ -0,0 +1,24 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallRequest;
+
+import java.util.Map;
+
+/**
+ * Agent 业务工具执行端口。
+ *
+ * <p>编排器只负责安全治理,真正的订单查询、政策检索、售后建单、审批后业务动作
+ * 都通过该接口执行。这样可以保持“控制面”和“业务能力”边界清楚。</p>
+ */
+public interface AgentBusinessToolExecutor {
+
+    /**
+     * 执行指定白名单工具。
+     *
+     * @param toolName 工具名
+     * @param params 工具参数
+     * @param request 原始调用上下文
+     * @return 工具输出,返回给 Python Agent
+     */
+    Map<String, Object> execute(String toolName, Map<String, Object> params, AgentToolCallRequest request);
+}

+ 491 - 0
src/main/java/com/zsElectric/boot/platform/agent/service/AgentToolOrchestrator.java

@@ -0,0 +1,491 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallRequest;
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallResponse;
+import com.zsElectric.boot.platform.agent.model.entity.AgentApproval;
+import com.zsElectric.boot.platform.agent.model.entity.AgentAuditLog;
+import com.zsElectric.boot.platform.agent.model.entity.AgentPendingAction;
+import com.zsElectric.boot.platform.agent.tool.AgentToolCatalog;
+import com.zsElectric.boot.platform.agent.tool.AgentToolDefinition;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.HexFormat;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Agent 工具编排器。
+ *
+ * <p>这是 Java 侧 Agent 控制面的核心类,不直接承载具体业务逻辑,而是负责把一次
+ * MCP tool 调用放进受控流程中:先查白名单和风险等级,再决定是直接执行低风险工具,
+ * 还是为高风险工具创建审批单和冻结动作。这样可以避免模型绕过审批直接触发退款、
+ * 补偿等有资金或业务副作用的动作。</p>
+ *
+ * <p>高风险动作的关键安全点是“冻结参数”:审批前把 tool_name、params_json、
+ * params_hash 和 idempotency_key 固化到 agent_pending_action。审批通过后恢复执行时,
+ * 只允许执行这份冻结参数,不再让模型重新生成参数。</p>
+ */
+public class AgentToolOrchestrator {
+
+    /**
+     * Jackson 反序列化 Map 的类型引用,专门用于读取冻结动作里的 params_json
+     * 和执行结果 JSON。
+     */
+    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
+    };
+
+    /**
+     * 工具白名单与风险定义。所有模型可调用工具必须先出现在这里。
+     */
+    private final AgentToolCatalog toolCatalog;
+    /**
+     * 审批单和冻结动作的持久化端口。测试中可以替换成内存实现,生产使用 MyBatis 实现。
+     */
+    private final AgentApprovalStore approvalStore;
+    /**
+     * 真实业务工具执行端口。编排器只关心“是否允许执行”,不关心具体业务怎么落库。
+     */
+    private final AgentBusinessToolExecutor toolExecutor;
+    /**
+     * Agent 全链路审计写入端口。关键节点都会追加审计,便于按 trace_id 回溯。
+     */
+    private final AgentAuditWriter auditWriter;
+    /**
+     * 可注入时钟,便于测试审批过期、冻结动作过期等时间相关逻辑。
+     */
+    private final Clock clock;
+    /**
+     * 用于生成稳定 JSON。参数哈希必须基于稳定序列化结果,否则同一参数 Map 的字段顺序
+     * 变化会导致哈希不一致。
+     */
+    private final ObjectMapper objectMapper;
+
+    public AgentToolOrchestrator(
+            AgentToolCatalog toolCatalog,
+            AgentApprovalStore approvalStore,
+            AgentBusinessToolExecutor toolExecutor,
+            AgentAuditWriter auditWriter,
+            Clock clock
+    ) {
+        this.toolCatalog = toolCatalog;
+        this.approvalStore = approvalStore;
+        this.toolExecutor = toolExecutor;
+        this.auditWriter = auditWriter;
+        this.clock = clock;
+        this.objectMapper = new ObjectMapper()
+                .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
+    }
+
+    /**
+     * MCP tool 调用入口。
+     *
+     * <p>这里统一执行三类分流:</p>
+     * <ul>
+     *     <li>恢复类工具:resume_after_approval 只能走冻结动作恢复流程。</li>
+     *     <li>查询类工具:get_approval_status 只读返回审批状态。</li>
+     *     <li>普通业务工具:按白名单风险等级决定直接执行或创建审批。</li>
+     * </ul>
+     *
+     * @param request Python Agent 传来的工具调用请求
+     * @return 工具执行结果,或审批等待结果,或明确失败原因
+     */
+    public AgentToolCallResponse call(AgentToolCallRequest request) {
+        AgentToolDefinition definition = toolCatalog.find(request.getToolName(), request.getToolVersion())
+                .orElse(null);
+        if (definition == null) {
+            return AgentToolCallResponse.failed(request, "tool is not in agent whitelist: " + request.getToolName());
+        }
+
+        if ("resume_after_approval".equals(definition.name())) {
+            return resumeAfterApproval(request);
+        }
+
+        if ("get_approval_status".equals(definition.name())) {
+            return getApprovalStatus(request);
+        }
+
+        if (definition.approvalRequired()) {
+            return createApproval(request, definition);
+        }
+
+        return executeLowRiskTool(request, definition);
+    }
+
+    /**
+     * 审批通过。
+     *
+     * <p>注意:审批通过只把 agent_approval.status 从 pending 改成 approved,
+     * 不直接执行退款或补偿。真实执行必须由 resume_after_approval 再触发,
+     * 这样审批动作和执行恢复边界清楚,审计也更容易解释。</p>
+     */
+    public void approve(String approvalId, String approverId, String approverName, String decisionComment) {
+        AgentApproval approval = approvalStore.findApproval(approvalId)
+                .orElseThrow(() -> new IllegalArgumentException("approval not found: " + approvalId));
+        if (!"pending".equals(approval.getStatus())) {
+            throw new IllegalStateException("only pending approval can be approved");
+        }
+        approval.setStatus("approved");
+        approval.setApproverId(parseLong(approverId));
+        approval.setApproverUsername(approverName);
+        approval.setDecisionComment(decisionComment);
+        approval.setDecisionTime(now());
+        approvalStore.updateApproval(approval);
+        writeAudit(approval.getTraceId(), approvalId, null, "approval_decided", null, "success", null, null);
+    }
+
+    /**
+     * 审批拒绝。
+     *
+     * <p>拒绝后同步把冻结动作标记为 rejected,后续 resume_after_approval 会返回
+     * 确定的 rejected 结果,而不是再次尝试执行业务动作。</p>
+     */
+    public void reject(String approvalId, String approverId, String approverName, String decisionComment) {
+        AgentApproval approval = approvalStore.findApproval(approvalId)
+                .orElseThrow(() -> new IllegalArgumentException("approval not found: " + approvalId));
+        if (!"pending".equals(approval.getStatus())) {
+            throw new IllegalStateException("only pending approval can be rejected");
+        }
+        approval.setStatus("rejected");
+        approval.setApproverId(parseLong(approverId));
+        approval.setApproverUsername(approverName);
+        approval.setDecisionComment(decisionComment);
+        approval.setDecisionTime(now());
+        approvalStore.updateApproval(approval);
+
+        approvalStore.findPendingAction(approvalId).ifPresent(action -> {
+            action.setExecuteStatus("rejected");
+            approvalStore.updatePendingAction(action);
+        });
+        writeAudit(approval.getTraceId(), approvalId, null, "approval_decided", null, "success", null, null);
+    }
+
+    /**
+     * 为高风险工具创建审批单和冻结动作。
+     *
+     * <p>冻结动作保存的是审批后真正要执行的工具名和参数快照。params_hash 用来防止
+     * 审批期间参数被人为或程序篡改;idempotency_key 用于后续业务执行幂等保护。</p>
+     */
+    private AgentToolCallResponse createApproval(AgentToolCallRequest request, AgentToolDefinition definition) {
+        String approvalId = "APR" + token();
+        String actionId = "ACT" + token();
+        String paramsJson = canonicalJson(request.getParams());
+        LocalDateTime now = now();
+        LocalDateTime expiresAt = now.plusHours(24);
+
+        AgentApproval approval = new AgentApproval();
+        approval.setApprovalId(approvalId);
+        approval.setTraceId(request.getTraceId());
+        approval.setBizType(bizType(definition.name()));
+        approval.setBizNo(stringParam(request.getParams(), "order_no"));
+        approval.setRiskLevel(definition.riskLevel().name());
+        approval.setStatus("pending");
+        approval.setRequestUsername(request.getOperatorName());
+        approval.setRequestReason(stringParam(request.getParams(), "reason"));
+        approval.setRiskReason("HIGH risk tool requires human approval: " + definition.name());
+        approval.setPolicyEvidenceJson(toJsonOrNull(request.getParams().get("evidence")));
+        approval.setRequestedAmount(decimalParam(request.getParams(), "amount"));
+        approval.setExpiresAt(expiresAt);
+        approval.setVersion(0);
+        approval.setDeleted(0);
+
+        AgentPendingAction pendingAction = new AgentPendingAction();
+        pendingAction.setActionId(actionId);
+        pendingAction.setApprovalId(approvalId);
+        pendingAction.setTraceId(request.getTraceId());
+        pendingAction.setToolName(definition.name());
+        pendingAction.setToolVersion(definition.version());
+        pendingAction.setRiskLevel(definition.riskLevel().name());
+        pendingAction.setParamsJson(paramsJson);
+        pendingAction.setParamsHash(sha256(paramsJson));
+        pendingAction.setIdempotencyKey(definition.name() + ":" + approvalId);
+        pendingAction.setExecuteStatus("pending");
+        pendingAction.setRetryCount(0);
+        pendingAction.setExpiresAt(expiresAt);
+        pendingAction.setVersion(0);
+        pendingAction.setDeleted(0);
+
+        approvalStore.saveApproval(approval);
+        approvalStore.savePendingAction(pendingAction);
+        approval.setPendingActionId(pendingAction.getId());
+        approvalStore.updateApproval(approval);
+        writeAudit(request.getTraceId(), approvalId, actionId, "approval_created", definition.name(), "pending", request.getParams(), null);
+        return AgentToolCallResponse.pendingApproval(request, approvalId, actionId);
+    }
+
+    /**
+     * 执行低风险工具。
+     *
+     * <p>低风险并不代表不审计;每次成功或失败都会写 agent_audit_log,确保
+     * Python Agent 的一次回答能通过 trace_id 还原工具链路。</p>
+     */
+    private AgentToolCallResponse executeLowRiskTool(AgentToolCallRequest request, AgentToolDefinition definition) {
+        try {
+            Map<String, Object> output = toolExecutor.execute(definition.name(), request.getParams(), request);
+            writeAudit(request.getTraceId(), null, null, "tool_called", definition.name(), "success", request.getParams(), output);
+            return AgentToolCallResponse.executed(request, output);
+        } catch (Exception ex) {
+            writeAudit(request.getTraceId(), null, null, "tool_called", definition.name(), "failed", request.getParams(), Map.of("error", ex.getMessage()));
+            return AgentToolCallResponse.failed(request, ex.getMessage());
+        }
+    }
+
+    /**
+     * 审批通过后的恢复执行入口。
+     *
+     * <p>恢复执行会按固定顺序校验:审批存在、冻结动作存在、未重复执行、审批已通过、
+     * 未过期、工具仍在白名单、params_hash 与 params_json 一致。只有这些条件全部满足,
+     * 才会调用真实业务工具。</p>
+     */
+    private AgentToolCallResponse resumeAfterApproval(AgentToolCallRequest request) {
+        String approvalId = stringParam(request.getParams(), "approval_id");
+        if (approvalId == null || approvalId.isBlank()) {
+            return AgentToolCallResponse.failed(request, "approval_id is required");
+        }
+
+        AgentApproval approval = approvalStore.findApproval(approvalId).orElse(null);
+        AgentPendingAction pendingAction = approvalStore.findPendingAction(approvalId).orElse(null);
+        if (approval == null || pendingAction == null) {
+            return AgentToolCallResponse.failed(request, "approval or pending_action not found");
+        }
+
+        if ("executed".equals(pendingAction.getExecuteStatus())) {
+            return executedFromHistory(request, pendingAction);
+        }
+        if ("rejected".equals(approval.getStatus()) || "rejected".equals(pendingAction.getExecuteStatus())) {
+            return terminalResponse(request, "rejected", Map.of("approval_id", approvalId, "status", "rejected"));
+        }
+        if (!"approved".equals(approval.getStatus())) {
+            return terminalResponse(request, "pending_approval", Map.of("approval_id", approvalId, "status", approval.getStatus()));
+        }
+        if (pendingAction.getExpiresAt() != null && pendingAction.getExpiresAt().isBefore(now())) {
+            approval.setStatus("expired");
+            pendingAction.setExecuteStatus("expired");
+            approvalStore.updateApproval(approval);
+            approvalStore.updatePendingAction(pendingAction);
+            return terminalResponse(request, "expired", Map.of("approval_id", approvalId, "status", "expired"));
+        }
+
+        AgentToolDefinition frozenDefinition = toolCatalog.find(pendingAction.getToolName(), pendingAction.getToolVersion())
+                .orElse(null);
+        if (frozenDefinition == null) {
+            return failPendingAction(request, pendingAction, "frozen tool is no longer enabled");
+        }
+
+        Map<String, Object> frozenParams = parseJsonMap(pendingAction.getParamsJson());
+        String recalculatedHash = sha256(canonicalJson(frozenParams));
+        if (!recalculatedHash.equals(pendingAction.getParamsHash())) {
+            return failPendingAction(request, pendingAction, "params_hash validation failed");
+        }
+
+        try {
+            pendingAction.setExecuteStatus("executing");
+            approvalStore.updatePendingAction(pendingAction);
+            Map<String, Object> output = toolExecutor.execute(pendingAction.getToolName(), frozenParams, request);
+            pendingAction.setExecuteStatus("executed");
+            pendingAction.setExecuteResultJson(canonicalJson(output));
+            pendingAction.setExecutedAt(now());
+            approval.setStatus("executed");
+            approvalStore.updatePendingAction(pendingAction);
+            approvalStore.updateApproval(approval);
+            writeAudit(request.getTraceId(), approvalId, pendingAction.getActionId(), "action_resumed", pendingAction.getToolName(), "success", frozenParams, output);
+            return AgentToolCallResponse.executed(request, output);
+        } catch (Exception ex) {
+            return failPendingAction(request, pendingAction, ex.getMessage());
+        }
+    }
+
+    /**
+     * 查询审批状态,供 Python Agent 或演示脚本轮询审批结果。
+     */
+    private AgentToolCallResponse getApprovalStatus(AgentToolCallRequest request) {
+        String approvalId = stringParam(request.getParams(), "approval_id");
+        return approvalStore.findApproval(approvalId)
+                .map(approval -> terminalResponse(request, "success", Map.of(
+                        "approval_id", approval.getApprovalId(),
+                        "status", approval.getStatus(),
+                        "trace_id", approval.getTraceId()
+                )))
+                .orElseGet(() -> AgentToolCallResponse.failed(request, "approval not found: " + approvalId));
+    }
+
+    /**
+     * 重复恢复时返回历史执行结果。
+     *
+     * <p>这一步保证 resume_after_approval 具备幂等语义:同一个 approval_id 已经执行成功后,
+     * 再次调用不会重复退款或补偿,只返回第一次执行的结果快照。</p>
+     */
+    private AgentToolCallResponse executedFromHistory(AgentToolCallRequest request, AgentPendingAction pendingAction) {
+        AgentToolCallResponse response = AgentToolCallResponse.executed(request, parseJsonMap(pendingAction.getExecuteResultJson()));
+        response.setApprovalId(pendingAction.getApprovalId());
+        response.setPendingActionId(pendingAction.getActionId());
+        return response;
+    }
+
+    /**
+     * 构造不会继续执行业务动作的终态响应,例如 rejected、expired、pending_approval。
+     */
+    private AgentToolCallResponse terminalResponse(AgentToolCallRequest request, String status, Map<String, Object> output) {
+        AgentToolCallResponse response = new AgentToolCallResponse();
+        response.setSuccess(true);
+        response.setStatus(status);
+        response.setTraceId(request.getTraceId());
+        response.setToolName(request.getToolName());
+        response.setOutput(output);
+        return response;
+    }
+
+    /**
+     * 冻结动作恢复失败时统一落失败状态并写审计。
+     */
+    private AgentToolCallResponse failPendingAction(AgentToolCallRequest request, AgentPendingAction pendingAction, String message) {
+        pendingAction.setExecuteStatus("failed");
+        pendingAction.setErrorMessage(message);
+        approvalStore.updatePendingAction(pendingAction);
+        writeAudit(request.getTraceId(), pendingAction.getApprovalId(), pendingAction.getActionId(), "action_resumed", pendingAction.getToolName(), "failed", null, Map.of("error", message));
+        return AgentToolCallResponse.failed(request, message);
+    }
+
+    /**
+     * 写 Agent 审计日志。
+     *
+     * <p>审计日志只追加,不参与业务状态判断。这里保存输入输出快照时仍然走 JSON,
+     * 后续如果有敏感字段脱敏策略,可以在这个入口或 AgentAuditWriter 实现里统一处理。</p>
+     */
+    private void writeAudit(
+            String traceId,
+            String approvalId,
+            String pendingActionId,
+            String step,
+            String toolName,
+            String status,
+            Object input,
+            Object output
+    ) {
+        AgentAuditLog auditLog = new AgentAuditLog();
+        auditLog.setTraceId(traceId);
+        auditLog.setApprovalId(approvalId);
+        auditLog.setPendingActionId(pendingActionId);
+        auditLog.setActorType("agent");
+        auditLog.setStep(step);
+        auditLog.setEventType(step);
+        auditLog.setToolName(toolName);
+        auditLog.setStatus(status);
+        auditLog.setInputJson(toJsonOrNull(input));
+        auditLog.setOutputJson(toJsonOrNull(output));
+        auditLog.setCreateTime(now());
+        auditWriter.write(auditLog);
+    }
+
+    /**
+     * 生成稳定 JSON 字符串,用于冻结参数和哈希计算。
+     */
+    private String canonicalJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value == null ? Map.of() : value);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException("failed to serialize json", e);
+        }
+    }
+
+    /**
+     * 将冻结参数或历史执行结果反序列化为 Map,供恢复执行和历史返回使用。
+     */
+    private Map<String, Object> parseJsonMap(String json) {
+        if (json == null || json.isBlank()) {
+            return new LinkedHashMap<>();
+        }
+        try {
+            return objectMapper.readValue(json, MAP_TYPE);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException("failed to parse json", e);
+        }
+    }
+
+    /**
+     * 计算 SHA-256 摘要。params_hash 使用该方法防止冻结参数被篡改。
+     */
+    private String sha256(String value) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            return HexFormat.of().formatHex(digest.digest(value.getBytes(StandardCharsets.UTF_8)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new IllegalStateException("SHA-256 is unavailable", e);
+        }
+    }
+
+    /**
+     * 当前时间统一从注入时钟获取,避免测试中依赖真实系统时间。
+     */
+    private LocalDateTime now() {
+        return LocalDateTime.now(clock);
+    }
+
+    /**
+     * 生成外部展示用的短 ID 片段,调用方再加 APR/ACT 前缀区分审批和冻结动作。
+     */
+    private String token() {
+        return UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
+    }
+
+    /**
+     * 将工具名转换成审批业务类型,便于审批列表按业务维度筛选。
+     */
+    private String bizType(String toolName) {
+        if ("request_compensation".equals(toolName)) {
+            return "compensation";
+        }
+        if ("request_refund".equals(toolName)) {
+            return "refund";
+        }
+        return toolName;
+    }
+
+    /**
+     * 从通用参数 Map 中读取字符串参数。
+     */
+    private String stringParam(Map<String, Object> params, String name) {
+        Object value = params == null ? null : params.get(name);
+        return value == null ? null : String.valueOf(value);
+    }
+
+    /**
+     * 从通用参数 Map 中读取金额参数。
+     */
+    private BigDecimal decimalParam(Map<String, Object> params, String name) {
+        String value = stringParam(params, name);
+        if (value == null || value.isBlank()) {
+            return null;
+        }
+        return new BigDecimal(value);
+    }
+
+    /**
+     * 将审批人 ID 等字符串字段转换成长整型,允许为空。
+     */
+    private Long parseLong(String value) {
+        if (value == null || value.isBlank()) {
+            return null;
+        }
+        return Long.parseLong(value);
+    }
+
+    /**
+     * 可空对象转 JSON;空值保持 null,避免审计表里出现无意义的 "{}"。
+     */
+    private String toJsonOrNull(Object value) {
+        if (value == null) {
+            return null;
+        }
+        return canonicalJson(value);
+    }
+}

+ 285 - 0
src/main/java/com/zsElectric/boot/platform/agent/service/DefaultAgentBusinessToolExecutor.java

@@ -0,0 +1,285 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zsElectric.boot.business.model.entity.ChargeOrderInfo;
+import com.zsElectric.boot.business.model.vo.ChargeOrderInfoVO;
+import com.zsElectric.boot.business.service.ChargeOrderInfoService;
+import com.zsElectric.boot.platform.agent.mapper.AftersalesWorkOrderMapper;
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallRequest;
+import com.zsElectric.boot.platform.agent.model.entity.AftersalesWorkOrder;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Agent 默认业务工具执行器。
+ *
+ * <p>该类只处理已经被 AgentToolOrchestrator 判定为“允许执行”的工具调用。
+ * 也就是说,高风险退款/补偿在到达这里之前已经完成审批和冻结参数校验。
+ * 这里不暴露通用 Controller,也不让模型直接访问 Mapper,而是把白名单工具映射到
+ * 明确的 Service/Mapper 调用。</p>
+ *
+ * <p>首版重点是跑通售后闭环,所以政策检索使用默认政策文本,退款/补偿审批通过后
+ * 先落售后工单记录,后续可以在对应分支接入真实支付退款或补偿账户变更逻辑。</p>
+ */
+@Component
+@RequiredArgsConstructor
+public class DefaultAgentBusinessToolExecutor implements AgentBusinessToolExecutor {
+
+    /**
+     * 复用现有充电订单 Service,保证订单查询仍走业务层而不是直接让 Agent 查库。
+     */
+    private final ChargeOrderInfoService chargeOrderInfoService;
+    /**
+     * 售后工单 Mapper,用于低风险自动建单和高风险审批后的处理记录。
+     */
+    private final AftersalesWorkOrderMapper aftersalesWorkOrderMapper;
+    /**
+     * 允许 write_agent_audit 工具追加审计日志。
+     */
+    private final AgentAuditWriter auditWriter;
+    /**
+     * 用于把 VO 转成 Map、把 evidence/order_snapshot 等结构化参数写入 JSON 字符串。
+     */
+    private final ObjectMapper objectMapper;
+
+    /**
+     * 根据白名单工具名分发到具体业务实现。
+     *
+     * <p>这里故意使用显式 switch,不做反射调用 Bean 方法,避免未来把未治理的业务方法
+     * 意外暴露给模型。</p>
+     */
+    @Override
+    public Map<String, Object> execute(String toolName, Map<String, Object> params, AgentToolCallRequest request) {
+        return switch (toolName) {
+            case "query_charge_order" -> queryChargeOrder(params);
+            case "query_charge_session" -> queryChargeSession(params);
+            case "retrieve_aftersales_policy" -> retrieveAftersalesPolicy(params);
+            case "create_aftersales_work_order" -> createWorkOrder(params, request, "created");
+            case "request_refund" -> createApprovedRiskWorkOrder(params, request, "refund_submitted");
+            case "request_compensation" -> createApprovedRiskWorkOrder(params, request, "compensation_submitted");
+            case "write_agent_audit" -> writeAgentAudit(params, request);
+            default -> throw new IllegalArgumentException("unsupported agent tool: " + toolName);
+        };
+    }
+
+    /**
+     * 查询充电订单。
+     *
+     * <p>优先使用 order_no 精确查询;如果没有订单号,再按 user_id 或 phone 找最近一条
+     * 充电订单。找不到时抛出明确异常,让 Agent 停止工具链路并返回人工核查。</p>
+     */
+    private Map<String, Object> queryChargeOrder(Map<String, Object> params) {
+        ChargeOrderInfoVO vo = null;
+        String orderNo = string(params, "order_no");
+        if (notBlank(orderNo)) {
+            vo = chargeOrderInfoService.queryOrder(orderNo);
+        } else {
+            ChargeOrderInfo entity = findChargeOrderEntity(params);
+            if (entity != null) {
+                vo = chargeOrderInfoService.queryOrder(entity.getChargeOrderNo());
+            }
+        }
+        if (vo == null) {
+            throw new IllegalArgumentException("charge order not found");
+        }
+        return objectMapper.convertValue(vo, Map.class);
+    }
+
+    /**
+     * 查询充电会话摘要。
+     *
+     * <p>首版没有单独的“充电会话表”抽象,因此从充电订单详情中提取开始时间、结束时间、
+     * 充电时长、费用、电量和停止原因,作为 Agent 生成售后建议的上下文。</p>
+     */
+    private Map<String, Object> queryChargeSession(Map<String, Object> params) {
+        Map<String, Object> order = queryChargeOrder(params);
+        Map<String, Object> session = new LinkedHashMap<>();
+        session.put("order_no", order.get("chargeOrderNo"));
+        session.put("status", order.get("status"));
+        session.put("start_time", order.get("startTime"));
+        session.put("end_time", order.get("endTime"));
+        session.put("charge_time", order.get("chargeTime"));
+        session.put("total_charge", order.get("totalCharge"));
+        session.put("real_cost", order.get("realCost"));
+        session.put("stop_reason", order.get("stopReason"));
+        session.put("charge_details", order.get("chargeDetails"));
+        return session;
+    }
+
+    /**
+     * 在缺少订单号时,按用户或手机号定位最近订单。
+     */
+    private ChargeOrderInfo findChargeOrderEntity(Map<String, Object> params) {
+        String phone = string(params, "phone");
+        Long userId = longValue(params, "user_id");
+        return chargeOrderInfoService.getOne(Wrappers.lambdaQuery(ChargeOrderInfo.class)
+                .eq(userId != null, ChargeOrderInfo::getUserId, userId)
+                .eq(notBlank(phone), ChargeOrderInfo::getPhoneNum, phone)
+                .orderByDesc(ChargeOrderInfo::getCreateTime)
+                .last("limit 1"));
+    }
+
+    /**
+     * 检索售后政策。
+     *
+     * <p>当前 Java 侧先提供兜底政策能力,保证 Agent 闭环可以运行。真正的 pgvector RAG
+     * 接入后,可以把该方法替换为调用政策检索服务,接口返回结构保持不变。</p>
+     */
+    private Map<String, Object> retrieveAftersalesPolicy(Map<String, Object> params) {
+        String issueType = string(params, "issue_type");
+        String keywords = string(params, "keywords");
+        return Map.of(
+                "issue_type", issueType == null ? "unknown" : issueType,
+                "keywords", keywords == null ? "" : keywords,
+                "policy_hit", true,
+                "source", "java-default-policy",
+                "content", "首版默认政策:普通售后可建工单;退款或补偿必须进入审批并按冻结动作恢复执行。"
+        );
+    }
+
+    /**
+     * 审批通过后的高风险售后记录。
+     *
+     * <p>该方法只会在 resume_after_approval 完成审批状态和参数哈希校验后被调用。
+     * 首版用售后工单承接“已审批的退款/补偿动作”,为后续接真实退款能力保留审计依据。</p>
+     */
+    private Map<String, Object> createApprovedRiskWorkOrder(Map<String, Object> params, AgentToolCallRequest request, String status) {
+        String issueType = "refund_submitted".equals(status) ? "refund_request" : "compensation_request";
+        Map<String, Object> enriched = new LinkedHashMap<>(params);
+        enriched.putIfAbsent("issue_type", issueType);
+        enriched.putIfAbsent("suggestion", "审批已通过,记录高风险售后处理动作。");
+        Map<String, Object> result = createWorkOrder(enriched, request, status);
+        result.put("execution_mode", "approved_after_sales_record");
+        return result;
+    }
+
+    /**
+     * 创建售后工单。
+     *
+     * <p>低风险场景直接创建状态为 created 的工单;高风险审批恢复场景会传入
+     * refund_submitted 或 compensation_submitted,用来区分工单来源。</p>
+     */
+    private Map<String, Object> createWorkOrder(Map<String, Object> params, AgentToolCallRequest request, String status) {
+        AftersalesWorkOrder workOrder = new AftersalesWorkOrder();
+        workOrder.setWorkOrderNo("WO" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase());
+        workOrder.setTraceId(request.getTraceId());
+        workOrder.setOrderNo(required(params, "order_no"));
+        workOrder.setUserId(longValue(params, "user_id"));
+        workOrder.setPhoneMasked(maskPhone(string(params, "phone")));
+        workOrder.setIssueType(defaultString(string(params, "issue_type"), "charge_failed"));
+        workOrder.setIssueLevel(defaultString(string(params, "issue_level"), "normal"));
+        workOrder.setSource("agent");
+        workOrder.setSuggestion(defaultString(string(params, "suggestion"), string(params, "reason")));
+        workOrder.setPolicyEvidenceJson(toJson(params.get("evidence")));
+        workOrder.setOrderSnapshotJson(toJson(params.get("order_snapshot")));
+        workOrder.setStatus(defaultString(status, "created"));
+        workOrder.setCreatedByAgent(1);
+        workOrder.setCreatedBy(request.getOperatorName());
+        workOrder.setDeleted(0);
+        aftersalesWorkOrderMapper.insert(workOrder);
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("work_order_no", workOrder.getWorkOrderNo());
+        result.put("status", workOrder.getStatus());
+        result.put("order_no", workOrder.getOrderNo());
+        result.put("issue_type", workOrder.getIssueType());
+        return result;
+    }
+
+    /**
+     * 外部显式写审计的工具实现。
+     *
+     * <p>通常编排器会自动写关键审计;该工具用于 Python Agent 想补充记录意图判断、
+     * RAG 命中、最终回复等 Java 无法感知的步骤。</p>
+     */
+    private Map<String, Object> writeAgentAudit(Map<String, Object> params, AgentToolCallRequest request) {
+        com.zsElectric.boot.platform.agent.model.entity.AgentAuditLog auditLog =
+                new com.zsElectric.boot.platform.agent.model.entity.AgentAuditLog();
+        auditLog.setTraceId(defaultString(string(params, "trace_id"), request.getTraceId()));
+        auditLog.setActorType("agent");
+        auditLog.setStep(defaultString(string(params, "step"), "tool_called"));
+        auditLog.setEventType(defaultString(string(params, "event_type"), auditLog.getStep()));
+        auditLog.setToolName(string(params, "tool_name"));
+        auditLog.setStatus(defaultString(string(params, "status"), "success"));
+        auditLog.setInputJson(toJson(params.get("input")));
+        auditLog.setOutputJson(toJson(params.get("output")));
+        auditLog.setCreateTime(LocalDateTime.now());
+        auditWriter.write(auditLog);
+        return Map.of("audit_written", true, "trace_id", auditLog.getTraceId());
+    }
+
+    /**
+     * 必填参数读取,不满足时直接终止工具执行。
+     */
+    private String required(Map<String, Object> params, String name) {
+        String value = string(params, name);
+        if (!notBlank(value)) {
+            throw new IllegalArgumentException(name + " is required");
+        }
+        return value;
+    }
+
+    /**
+     * 从通用参数 Map 中读取字符串。
+     */
+    private String string(Map<String, Object> params, String name) {
+        Object value = params == null ? null : params.get(name);
+        return value == null ? null : String.valueOf(value);
+    }
+
+    /**
+     * 从通用参数 Map 中读取 Long 类型参数。
+     */
+    private Long longValue(Map<String, Object> params, String name) {
+        String value = string(params, name);
+        if (!notBlank(value)) {
+            return null;
+        }
+        return Long.parseLong(value);
+    }
+
+    /**
+     * 判断字符串是否有实际内容。
+     */
+    private boolean notBlank(String value) {
+        return value != null && !value.isBlank();
+    }
+
+    /**
+     * 字符串默认值处理。
+     */
+    private String defaultString(String value, String defaultValue) {
+        return notBlank(value) ? value : defaultValue;
+    }
+
+    /**
+     * 手机号脱敏后再写入售后工单,避免 Agent 工具结果泄露完整手机号。
+     */
+    private String maskPhone(String phone) {
+        if (!notBlank(phone) || phone.length() < 7) {
+            return phone;
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
+
+    /**
+     * 将结构化参数转成 JSON 字符串用于落库。
+     */
+    private String toJson(Object value) {
+        if (value == null) {
+            return null;
+        }
+        try {
+            return objectMapper.writeValueAsString(value);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException("json serialization failed", e);
+        }
+    }
+}

+ 83 - 0
src/main/java/com/zsElectric/boot/platform/agent/service/MyBatisAgentApprovalStore.java

@@ -0,0 +1,83 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.zsElectric.boot.platform.agent.mapper.AgentApprovalMapper;
+import com.zsElectric.boot.platform.agent.mapper.AgentPendingActionMapper;
+import com.zsElectric.boot.platform.agent.model.entity.AgentApproval;
+import com.zsElectric.boot.platform.agent.model.entity.AgentPendingAction;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+/**
+ * 基于 MyBatis-Plus 的审批和冻结动作存储实现。
+ *
+ * <p>这里保持简单的按 approval_id 查询和按主键更新。复杂并发控制依赖实体上的
+ * @Version 乐观锁字段,后续如果审批并发更复杂,可以在该实现中补充状态条件更新。</p>
+ */
+@Component
+@RequiredArgsConstructor
+public class MyBatisAgentApprovalStore implements AgentApprovalStore {
+
+    /**
+     * 审批单 Mapper。
+     */
+    private final AgentApprovalMapper approvalMapper;
+    /**
+     * 冻结动作 Mapper。
+     */
+    private final AgentPendingActionMapper pendingActionMapper;
+
+    /**
+     * 新增审批单。
+     */
+    @Override
+    public void saveApproval(AgentApproval approval) {
+        approvalMapper.insert(approval);
+    }
+
+    /**
+     * 新增冻结动作。
+     */
+    @Override
+    public void savePendingAction(AgentPendingAction pendingAction) {
+        pendingActionMapper.insert(pendingAction);
+    }
+
+    /**
+     * 按 approval_id 查询审批单,limit 1 防止异常重复数据影响调用方。
+     */
+    @Override
+    public Optional<AgentApproval> findApproval(String approvalId) {
+        return Optional.ofNullable(approvalMapper.selectOne(Wrappers.lambdaQuery(AgentApproval.class)
+                .eq(AgentApproval::getApprovalId, approvalId)
+                .last("limit 1")));
+    }
+
+    /**
+     * 按 approval_id 查询冻结动作。
+     */
+    @Override
+    public Optional<AgentPendingAction> findPendingAction(String approvalId) {
+        return Optional.ofNullable(pendingActionMapper.selectOne(Wrappers.lambdaQuery(AgentPendingAction.class)
+                .eq(AgentPendingAction::getApprovalId, approvalId)
+                .last("limit 1")));
+    }
+
+    /**
+     * 更新审批单。
+     */
+    @Override
+    public void updateApproval(AgentApproval approval) {
+        approvalMapper.updateById(approval);
+    }
+
+    /**
+     * 更新冻结动作。
+     */
+    @Override
+    public void updatePendingAction(AgentPendingAction pendingAction) {
+        pendingActionMapper.updateById(pendingAction);
+    }
+}

+ 30 - 0
src/main/java/com/zsElectric/boot/platform/agent/service/MyBatisAgentAuditWriter.java

@@ -0,0 +1,30 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.zsElectric.boot.platform.agent.mapper.AgentAuditLogMapper;
+import com.zsElectric.boot.platform.agent.model.entity.AgentAuditLog;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+/**
+ * MyBatis-Plus 审计写入实现。
+ *
+ * <p>审计日志采用追加写,不在这里更新业务状态。即便未来改成异步队列,
+ * 也应保持“业务状态由审批/冻结动作表承载,审计表只记录过程”的边界。</p>
+ */
+@Component
+@RequiredArgsConstructor
+public class MyBatisAgentAuditWriter implements AgentAuditWriter {
+
+    /**
+     * 审计日志 Mapper。
+     */
+    private final AgentAuditLogMapper agentAuditLogMapper;
+
+    /**
+     * 追加审计日志。
+     */
+    @Override
+    public void write(AgentAuditLog auditLog) {
+        agentAuditLogMapper.insert(auditLog);
+    }
+}

+ 23 - 0
src/main/java/com/zsElectric/boot/platform/agent/tool/AgentRiskLevel.java

@@ -0,0 +1,23 @@
+package com.zsElectric.boot.platform.agent.tool;
+
+/**
+ * Agent 工具风险等级。
+ *
+ * <p>风险等级用于把“模型建议调用工具”和“系统真正执行业务动作”隔开。
+ * LOW 可以直接执行并审计;MEDIUM 预留给后续需要二次确认但不一定审批的动作;
+ * HIGH 必须经过审批或冻结恢复流程。</p>
+ */
+public enum AgentRiskLevel {
+    /**
+     * 低风险:只读查询、政策检索、普通售后建单、审计写入。
+     */
+    LOW,
+    /**
+     * 中风险:首版暂未使用,预留给修改非资金类状态等场景。
+     */
+    MEDIUM,
+    /**
+     * 高风险:退款、补偿、审批后恢复执行等有资金或强业务副作用的动作。
+     */
+    HIGH
+}

+ 89 - 0
src/main/java/com/zsElectric/boot/platform/agent/tool/AgentToolCatalog.java

@@ -0,0 +1,89 @@
+package com.zsElectric.boot.platform.agent.tool;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Agent 工具白名单目录。
+ *
+ * <p>这里定义“模型可以看到并调用哪些工具”。首版先用代码内置目录,避免把现有
+ * Controller 或 Service 自动暴露给模型;后续如果要改成数据库动态目录,也应该保持
+ * 同样的约束:工具必须有固定名称、版本、风险等级、审批策略和参数 schema。</p>
+ */
+public class AgentToolCatalog {
+
+    /**
+     * key 格式为 toolName:version,用版本号避免后续参数 schema 变更时出现兼容问题。
+     */
+    private final Map<String, AgentToolDefinition> tools;
+
+    /**
+     * 构造白名单索引。重复工具名和版本会被后写入定义覆盖,调用方应保证定义唯一。
+     */
+    public AgentToolCatalog(List<AgentToolDefinition> definitions) {
+        Map<String, AgentToolDefinition> indexed = new LinkedHashMap<>();
+        for (AgentToolDefinition definition : definitions) {
+            indexed.put(key(definition.name(), definition.version()), definition);
+        }
+        this.tools = Map.copyOf(indexed);
+    }
+
+    /**
+     * 首版售后 Agent 工具清单。
+     *
+     * <p>LOW 工具可以直接执行;HIGH 工具要么创建审批和冻结动作,要么是恢复执行工具。
+     * 这里刻意没有收录启动充电、停止充电、清余额等高副作用接口,避免模型直接操作。</p>
+     */
+    public static AgentToolCatalog defaultCatalog() {
+        return new AgentToolCatalog(List.of(
+                low("query_charge_order", "查询充电订单"),
+                low("query_charge_session", "查询充电记录"),
+                low("retrieve_aftersales_policy", "检索售后政策"),
+                low("create_aftersales_work_order", "创建售后工单"),
+                high("request_refund", "发起退款审批", true),
+                high("request_compensation", "发起补偿审批", true),
+                high("resume_after_approval", "审批后恢复冻结动作", false),
+                low("get_approval_status", "查询审批状态"),
+                low("write_agent_audit", "写入Agent审计日志")
+        ));
+    }
+
+    /**
+     * 按工具名和版本查找白名单定义。
+     */
+    public Optional<AgentToolDefinition> find(String name, String version) {
+        return Optional.ofNullable(tools.get(key(name, version == null ? "v1" : version)));
+    }
+
+    /**
+     * 返回给 Python Agent 或 Swagger 演示查看的工具列表。
+     */
+    public List<AgentToolDefinition> list() {
+        return tools.values().stream()
+                .sorted((left, right) -> left.name().compareTo(right.name()))
+                .toList();
+    }
+
+    /**
+     * 创建低风险工具定义。
+     */
+    private static AgentToolDefinition low(String name, String description) {
+        return new AgentToolDefinition(name, "v1", description, AgentRiskLevel.LOW, false, Map.of(), Map.of());
+    }
+
+    /**
+     * 创建高风险工具定义。
+     */
+    private static AgentToolDefinition high(String name, String description, boolean approvalRequired) {
+        return new AgentToolDefinition(name, "v1", description, AgentRiskLevel.HIGH, approvalRequired, Map.of(), Map.of());
+    }
+
+    /**
+     * 工具索引键,名称和版本共同确定一个稳定工具契约。
+     */
+    private static String key(String name, String version) {
+        return name + ":" + version;
+    }
+}

+ 25 - 0
src/main/java/com/zsElectric/boot/platform/agent/tool/AgentToolDefinition.java

@@ -0,0 +1,25 @@
+package com.zsElectric.boot.platform.agent.tool;
+
+import java.util.Map;
+
+/**
+ * Agent 工具定义。
+ *
+ * @param name 工具名,作为 Python Agent 调用 Java MCP 工具的稳定标识
+ * @param version 工具版本,参数结构变化时通过版本隔离
+ * @param description 工具说明,用于工具列表展示和后续模型提示词组装
+ * @param riskLevel 风险等级,决定是否需要进入审批和冻结动作
+ * @param approvalRequired 是否需要人工审批;高风险恢复工具本身可以是 HIGH 但不再创建新审批
+ * @param inputSchema 输入参数 JSON Schema,首版预留,后续可用于运行时校验和提示词生成
+ * @param outputSchema 输出结果 JSON Schema,首版预留,后续可用于工具结果标准化
+ */
+public record AgentToolDefinition(
+        String name,
+        String version,
+        String description,
+        AgentRiskLevel riskLevel,
+        boolean approvalRequired,
+        Map<String, Object> inputSchema,
+        Map<String, Object> outputSchema
+) {
+}

+ 6 - 0
src/main/resources/application-dev.yml

@@ -51,6 +51,10 @@ spring:
     # 邮件发送者
     from: youlaitech@163.com
 
+# 充电站经纬度同步黑名单(黑名单中的站点不同步经纬度,保留数据库原有值)
+black-charging-stations:
+  station-list: "202510211552,202511201459,202512181925"
+
 # 第三方认证配置
 third-party:
   jwt:
@@ -125,6 +129,8 @@ security:
     - /charge-business/v1/linkData/**
     - /applet/v1/homePage/** # 用户端分页查询站点信息
     - /third_party/v1/** # 第三方接入接口
+    - /api/v1/agent/mcp/**
+    - /api/v1/agent/approvals/**
   # 只走第三方过滤器、不走其他安全链
   third-party-urls:
     - /charge-business/v1/linkData/notification_start_charge_result

+ 4 - 0
src/main/resources/application-prod.yml

@@ -51,6 +51,10 @@ spring:
     # 邮件发送者
     from: youlaitech@163.com
 
+# 充电站经纬度同步黑名单(黑名单中的站点不同步经纬度,保留数据库原有值)
+black-charging-stations:
+  station-list: "202510211552,202511201459,202512181925"
+
 # 第三方认证配置
 third-party:
   jwt:

+ 10 - 4
src/main/resources/application-test.yml

@@ -51,6 +51,10 @@ spring:
     # 邮件发送者
     from: youlaitech@163.com
 
+# 充电站经纬度同步黑名单(黑名单中的站点不同步经纬度,保留数据库原有值)
+black-charging-stations:
+  station-list: ""
+
 # 第三方认证配置
 third-party:
   jwt:
@@ -125,6 +129,8 @@ security:
     - /charge-business/v1/linkData/**
     - /applet/v1/homePage/** # 用户端分页查询站点信息
     - /third_party/v1/** # 第三方接入接口
+    - /api/v1/agent/mcp/**
+    - /api/v1/agent/approvals/**
   # 只走第三方过滤器、不走其他安全链
   third-party-urls:
     - /charge-business/v1/linkData/notification_start_charge_result
@@ -162,13 +168,13 @@ oss:
   # 阿里云OSS对象存储服务
   aliyun:
     # 服务Endpoint
-    endpoint: oss-cn-beijing.aliyuncs.com
+    endpoint: oss-cn-chengdu.aliyuncs.com
     # 访问凭据`
-    access-key-id: LTAI5tJscqbev7wSugGCrEtt
+    access-key-id: LTAI5tBd18obT6zfRBZqGWiC
     # 凭据密钥
-    access-key-secret: xJkoJR1ILpXNSF2ERnxNq71UZTQNcB
+    access-key-secret: LHsruEf7Y9lLFfdLRTOkQrd2nzZTWx
     # 存储桶名称
-    bucket-name: national-motion
+    bucket-name: zsdd-electric
   # 本地存储
   local:
     # 文件存储路径 请注意下,mac用户请使用 /Users/your-username/your-path/,否则会有权限问题,windows用户请使用 D:/your-path/

+ 1 - 1
src/main/resources/application.yml

@@ -2,7 +2,7 @@ spring:
   application:
     name: zsElectric-boot
   profiles:
-    active: prod
+    active: dev
   config:
     import: classpath:codegen.yml
   servlet:

+ 6 - 1
src/main/resources/mapper/business/ChargeOrderInfoMapper.xml

@@ -54,13 +54,15 @@
         coi.is_deleted,
         pci.connector_id,
         pci.connector_name,
-        CONCAT(tcs.soc,'%') AS soc
+        CONCAT(tcs.soc,'%') AS soc,
+        ctp.ec_name
         FROM
         c_charge_order_info coi
         LEFT JOIN c_user_info ui ON coi.user_id = ui.id
         left JOIN third_party_connector_info pci ON coi.connector_id = pci.connector_id
         LEFT JOIN third_party_station_info psi ON coi.third_party_station_id = psi.id
         LEFT JOIN third_party_charge_status tcs ON coi.start_charge_seq = tcs.start_charge_seq
+        LEFT JOIN c_third_party_info ctp ON coi.operator_id = ctp.operator_id
         <where>
             coi.is_deleted = 0
             <if test="queryParams.equipmentId != null">
@@ -100,6 +102,9 @@
             <if test="queryParams.connectorId != null and queryParams.connectorId != ''">
                 AND coi.connector_id = #{queryParams.connectorId}
             </if>
+            <if test="queryParams.operatorId != null">
+                AND coi.operator_id = #{queryParams.operatorId}
+            </if>
         </where>
         ORDER BY coi.create_time DESC
     </select>

+ 205 - 0
src/test/java/com/zsElectric/boot/platform/agent/service/AgentToolOrchestratorTest.java

@@ -0,0 +1,205 @@
+package com.zsElectric.boot.platform.agent.service;
+
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallRequest;
+import com.zsElectric.boot.platform.agent.model.dto.AgentToolCallResponse;
+import com.zsElectric.boot.platform.agent.model.entity.AgentApproval;
+import com.zsElectric.boot.platform.agent.model.entity.AgentPendingAction;
+import com.zsElectric.boot.platform.agent.tool.AgentToolCatalog;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AgentToolOrchestratorTest {
+
+    private InMemoryAgentApprovalStore store;
+    private RecordingBusinessToolExecutor executor;
+    private AgentToolOrchestrator orchestrator;
+
+    @BeforeEach
+    void setUp() {
+        store = new InMemoryAgentApprovalStore();
+        executor = new RecordingBusinessToolExecutor();
+        orchestrator = new AgentToolOrchestrator(
+                AgentToolCatalog.defaultCatalog(),
+                store,
+                executor,
+                audit -> {
+                },
+                Clock.fixed(Instant.parse("2026-07-08T10:15:30Z"), ZoneId.of("UTC"))
+        );
+    }
+
+    @Test
+    void highRiskRefundCreatesApprovalInsteadOfExecutingImmediately() {
+        AgentToolCallRequest request = request(
+                "trace-001",
+                "request_refund",
+                Map.of("order_no", "CD001", "amount", "12.50", "reason", "充电失败")
+        );
+
+        AgentToolCallResponse response = orchestrator.call(request);
+
+        assertThat(response.isApprovalRequired()).isTrue();
+        assertThat(response.getApprovalId()).startsWith("APR");
+        assertThat(response.getStatus()).isEqualTo("pending_approval");
+        assertThat(executor.calls).isEmpty();
+        assertThat(store.approvals).hasSize(1);
+        assertThat(store.pendingActions).hasSize(1);
+        assertThat(store.pendingActions.get(0).getToolName()).isEqualTo("request_refund");
+        assertThat(store.pendingActions.get(0).getParamsHash()).hasSize(64);
+    }
+
+    @Test
+    void lowRiskWorkOrderExecutesImmediately() {
+        executor.nextResult = Map.of("work_order_no", "WO001", "status", "created");
+        AgentToolCallRequest request = request(
+                "trace-002",
+                "create_aftersales_work_order",
+                Map.of("order_no", "CD002", "issue_type", "charge_failed", "suggestion", "安排客服核查")
+        );
+
+        AgentToolCallResponse response = orchestrator.call(request);
+
+        assertThat(response.isApprovalRequired()).isFalse();
+        assertThat(response.getStatus()).isEqualTo("executed");
+        assertThat(response.getOutput()).containsEntry("work_order_no", "WO001");
+        assertThat(executor.calls).containsExactly("create_aftersales_work_order");
+        assertThat(store.approvals).isEmpty();
+    }
+
+    @Test
+    void approvedActionResumesTheFrozenToolAndMarksRecordsExecuted() {
+        AgentToolCallResponse approval = orchestrator.call(request(
+                "trace-003",
+                "request_refund",
+                Map.of("order_no", "CD003", "amount", "8.00", "reason", "重复扣费")
+        ));
+        orchestrator.approve(approval.getApprovalId(), "1001", "张三", "同意退款");
+        executor.nextResult = Map.of("refund_status", "submitted", "refund_no", "RF001");
+
+        AgentToolCallResponse response = orchestrator.call(request(
+                "trace-003",
+                "resume_after_approval",
+                Map.of("approval_id", approval.getApprovalId())
+        ));
+
+        assertThat(response.getStatus()).isEqualTo("executed");
+        assertThat(response.getOutput()).containsEntry("refund_no", "RF001");
+        assertThat(executor.calls).containsExactly("request_refund");
+        assertThat(store.approvals.get(0).getStatus()).isEqualTo("executed");
+        assertThat(store.pendingActions.get(0).getExecuteStatus()).isEqualTo("executed");
+    }
+
+    @Test
+    void repeatedResumeReturnsHistoricalResultWithoutExecutingAgain() {
+        AgentToolCallResponse approval = orchestrator.call(request(
+                "trace-004",
+                "request_compensation",
+                Map.of("order_no", "CD004", "amount", "5.00", "reason", "服务异常")
+        ));
+        orchestrator.approve(approval.getApprovalId(), "1001", "张三", "同意补偿");
+        executor.nextResult = Map.of("compensation_status", "submitted", "compensation_no", "CP001");
+
+        orchestrator.call(request("trace-004", "resume_after_approval", Map.of("approval_id", approval.getApprovalId())));
+        AgentToolCallResponse repeated = orchestrator.call(request(
+                "trace-004",
+                "resume_after_approval",
+                Map.of("approval_id", approval.getApprovalId())
+        ));
+
+        assertThat(repeated.getStatus()).isEqualTo("executed");
+        assertThat(repeated.getOutput()).containsEntry("compensation_no", "CP001");
+        assertThat(executor.calls).containsExactly("request_compensation");
+    }
+
+    @Test
+    void resumeRejectsTamperedFrozenParams() {
+        AgentToolCallResponse approval = orchestrator.call(request(
+                "trace-005",
+                "request_refund",
+                new LinkedHashMap<>(Map.of("order_no", "CD005", "amount", "3.00", "reason", "异常扣费"))
+        ));
+        orchestrator.approve(approval.getApprovalId(), "1001", "张三", "同意退款");
+        store.pendingActions.get(0).setParamsJson("{\"order_no\":\"CD005\",\"amount\":\"300.00\",\"reason\":\"异常扣费\"}");
+
+        AgentToolCallResponse response = orchestrator.call(request(
+                "trace-005",
+                "resume_after_approval",
+                Map.of("approval_id", approval.getApprovalId())
+        ));
+
+        assertThat(response.getStatus()).isEqualTo("failed");
+        assertThat(response.getErrorMessage()).contains("params_hash");
+        assertThat(executor.calls).isEmpty();
+        assertThat(store.pendingActions.get(0).getExecuteStatus()).isEqualTo("failed");
+    }
+
+    private static AgentToolCallRequest request(String traceId, String toolName, Map<String, Object> params) {
+        AgentToolCallRequest request = new AgentToolCallRequest();
+        request.setTraceId(traceId);
+        request.setToolName(toolName);
+        request.setToolVersion("v1");
+        request.setOperatorId("agent-test");
+        request.setOperatorName("Agent Test");
+        request.setParams(params);
+        return request;
+    }
+
+    private static class InMemoryAgentApprovalStore implements AgentApprovalStore {
+        private final List<AgentApproval> approvals = new ArrayList<>();
+        private final List<AgentPendingAction> pendingActions = new ArrayList<>();
+
+        @Override
+        public void saveApproval(AgentApproval approval) {
+            approvals.add(approval);
+        }
+
+        @Override
+        public void savePendingAction(AgentPendingAction pendingAction) {
+            pendingActions.add(pendingAction);
+        }
+
+        @Override
+        public Optional<AgentApproval> findApproval(String approvalId) {
+            return approvals.stream()
+                    .filter(approval -> approvalId.equals(approval.getApprovalId()))
+                    .findFirst();
+        }
+
+        @Override
+        public Optional<AgentPendingAction> findPendingAction(String approvalId) {
+            return pendingActions.stream()
+                    .filter(action -> approvalId.equals(action.getApprovalId()))
+                    .findFirst();
+        }
+
+        @Override
+        public void updateApproval(AgentApproval approval) {
+        }
+
+        @Override
+        public void updatePendingAction(AgentPendingAction pendingAction) {
+        }
+    }
+
+    private static class RecordingBusinessToolExecutor implements AgentBusinessToolExecutor {
+        private final List<String> calls = new ArrayList<>();
+        private Map<String, Object> nextResult = Map.of("status", "ok");
+
+        @Override
+        public Map<String, Object> execute(String toolName, Map<String, Object> params, AgentToolCallRequest request) {
+            calls.add(toolName);
+            return nextResult;
+        }
+    }
+}