瀏覽代碼

feat(charging): 添加充电设备状态校验防止非法启动

- 新增接口 checkEquipmentOccupancyStatus 校验充电终端状态
- 启动充电前校验设备状态,防止离网、故障、占用等异常启动
- 充电订单渠道方逻辑支持设置运营商ID及用户匹配第三方信息
- 调整渠道方启动充电返回类型为包含订单详细信息的VO
- 优化渠道方推送订单信息时 FirmInfo 判断,避免空指针
- 移除恶意HTTP请求异常相关类及配置,改由异常处理统一安全事件记录
- 全局异常处理增强请求参数和请求体校验异常安全事件日志记录
- 添加恶意输入检测,拦截非法请求并记录安全日志
- 配置文件调整,移除XSS和SQL注入配置,新增恶意请求拦截开关
- 过滤器配置调整,禁用 ThirdPartyJwtAuthFilter 全局注册
- 第三方接口数据模型新增充电站经纬度信息字段
- SDK示例中修正baseUrl路径及查询充电终端ID类型为字符串
wzq 3 周之前
父節點
當前提交
1395bce139
共有 43 個文件被更改,包括 1324 次插入1856 次删除
  1. 89 6
      doc/第三方接入API文档.md
  2. 49 0
      sql/mysql/security_event_log.sql
  3. 0 59
      src/main/java/com/zsElectric/boot/auth/controller/SecurityTestController.java
  4. 3 0
      src/main/java/com/zsElectric/boot/business/model/form/applet/AppStopChargeForm.java
  5. 42 13
      src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java
  6. 3 0
      src/main/java/com/zsElectric/boot/charging/service/ChargingBusinessService.java
  7. 44 0
      src/main/java/com/zsElectric/boot/charging/service/impl/ChargingBusinessServiceImpl.java
  8. 27 25
      src/main/java/com/zsElectric/boot/charging/service/impl/ChargingReceptionServiceImpl.java
  9. 0 380
      src/main/java/com/zsElectric/boot/common/util/SecurityUtils.java
  10. 6 6
      src/main/java/com/zsElectric/boot/common/util/electric/queryToken/ThirdPartyJwtAuthFilter.java
  11. 0 27
      src/main/java/com/zsElectric/boot/common/xss/Xss.java
  12. 0 58
      src/main/java/com/zsElectric/boot/common/xss/XssFilter.java
  13. 0 134
      src/main/java/com/zsElectric/boot/common/xss/XssHttpServletRequestWrapper.java
  14. 0 28
      src/main/java/com/zsElectric/boot/common/xss/XssProperties.java
  15. 0 20
      src/main/java/com/zsElectric/boot/common/xss/XssValidator.java
  16. 6 19
      src/main/java/com/zsElectric/boot/config/FilterConfig.java
  17. 0 31
      src/main/java/com/zsElectric/boot/config/SecurityUtilsConfig.java
  18. 0 48
      src/main/java/com/zsElectric/boot/config/property/SecurityFilterProperties.java
  19. 18 0
      src/main/java/com/zsElectric/boot/config/property/SecurityProperties.java
  20. 0 22
      src/main/java/com/zsElectric/boot/core/exception/BadHttpRequestException.java
  21. 238 18
      src/main/java/com/zsElectric/boot/core/exception/GlobalExceptionHandler.java
  22. 0 261
      src/main/java/com/zsElectric/boot/core/filter/XssAndSqlInjectionFilter.java
  23. 13 5
      src/main/java/com/zsElectric/boot/sdk/SdkExample.java
  24. 123 36
      src/main/java/com/zsElectric/boot/sdk/ZsElectricClient.java
  25. 15 0
      src/main/java/com/zsElectric/boot/system/mapper/SecurityEventLogMapper.java
  26. 93 0
      src/main/java/com/zsElectric/boot/system/model/entity/SecurityEventLog.java
  27. 8 0
      src/main/java/com/zsElectric/boot/system/model/query/UserPageQuery.java
  28. 33 0
      src/main/java/com/zsElectric/boot/system/service/SecurityEventLogService.java
  29. 149 0
      src/main/java/com/zsElectric/boot/system/service/impl/SecurityEventLogServiceImpl.java
  30. 23 0
      src/main/java/com/zsElectric/boot/system/service/impl/UserServiceImpl.java
  31. 9 0
      src/main/java/com/zsElectric/boot/thirdParty/controller/ThirdPartyController.java
  32. 6 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationListResponseData.java
  33. 21 0
      src/main/java/com/zsElectric/boot/thirdParty/model/StationPriceListRequestData.java
  34. 67 0
      src/main/java/com/zsElectric/boot/thirdParty/model/StationPriceListResponseData.java
  35. 9 0
      src/main/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenService.java
  36. 205 93
      src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java
  37. 3 27
      src/main/resources/application-dev.yml
  38. 2 27
      src/main/resources/application-prod.yml
  39. 3 27
      src/main/resources/application-test.yml
  40. 6 4
      src/main/resources/mapper/business/ThirdPartyStationInfoMapper.xml
  41. 11 2
      src/main/resources/mapper/system/UserMapper.xml
  42. 0 60
      src/test/java/com/zsElectric/boot/common/util/SecurityUtilsTest.java
  43. 0 420
      src/test/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenServiceTest.java

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

@@ -19,7 +19,8 @@
 - [11. 查询充电订单实时费用](#11-查询充电订单实时费用)
 - [12. 查询充电订单分页列表](#12-查询充电订单分页列表)
 - [13. 查询充电订单详情](#13-查询充电订单详情)
-- [14. 清除用户余额](#14-清除用户余额)
+- [14. 获取电站价格列表](#14-获取电站价格列表)
+- [15. 清除用户余额](#15-清除用户余额)
 - [附录:配置模板](#附录配置模板)
 
 ---
@@ -1036,10 +1037,92 @@
 
 ---
 
-## 14. 清除用户余额
+## 14. 获取电站价格列表
 
 ### 14.1 接口描述
 
+- **接口名称:** query_station_price_list
+- **接口说明:** 第三方获取电站价格列表(各时段电费、服务费、合计充电价)
+- **请求格式:** JSON
+- **请求方式:** POST
+- **接口路径:** `/third_party/v1/query_station_price_list`
+- **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
+
+### 14.2 输入参数(data解密后)
+
+| 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
+|----------|----------|----------|------|----------|
+| stationId | 充电站ID | Long | 充电站ID | 是 |
+
+### 14.3 请求示例
+
+**data加密前:**
+
+```json
+{
+  "stationId": 1
+}
+```
+
+### 14.4 返回参数(data解密后)
+
+| 参数名称 | 参数定义 | 参数类型 | 描述 |
+|----------|----------|----------|------|
+| stationId | 站点ID | Long | 充电站ID |
+| stationName | 站点名称 | String | 充电站名称 |
+| tips | 提示语 | String | 标签提示信息(如:充电减免2小时停车费) |
+| priceList | 价格列表 | Array | 各时段价格列表 |
+
+**priceList 数组元素:**
+
+| 参数名称 | 参数定义 | 参数类型 | 描述 |
+|----------|----------|----------|------|
+| timePeriod | 时段 | String | 时段范围(如:00:00-08:00) |
+| periodFlag | 时段标志 | Integer | 1-尖,2-峰,3-平,4-谷 |
+| periodFlagName | 时段标志名称 | String | 尖/峰/平/谷 |
+| elecPrice | 电费 | BigDecimal | 电费(元/度) |
+| servicePrice | 服务费 | BigDecimal | 服务费(元/度) |
+| totalPrice | 合计充电价 | BigDecimal | 合计充电价(元/度) |
+| currentPeriod | 是否当前时段 | Boolean | true-是,false-否 |
+
+### 14.5 返回示例
+
+**data解密后:**
+
+```json
+{
+  "stationId": 1,
+  "stationName": "XX充电站",
+  "tips": "充电减免2小时停车费,超出部分按每小时3元计费",
+  "priceList": [
+    {
+      "timePeriod": "00:00-08:00",
+      "periodFlag": 4,
+      "periodFlagName": "谷",
+      "elecPrice": 0.35,
+      "servicePrice": 0.20,
+      "totalPrice": 0.55,
+      "currentPeriod": false
+    },
+    {
+      "timePeriod": "08:00-12:00",
+      "periodFlag": 2,
+      "periodFlagName": "峰",
+      "elecPrice": 0.85,
+      "servicePrice": 0.35,
+      "totalPrice": 1.20,
+      "currentPeriod": true
+    }
+  ]
+}
+```
+
+---
+
+## 15. 清除用户余额
+
+### 15.1 接口描述
+
 - **接口名称:** clear_user_balance
 - **接口说明:** 第三方清除用户账户余额,将用户可用抵用券余额清零并记录动账日志
 - **请求格式:** JSON
@@ -1047,13 +1130,13 @@
 - **接口路径:** `/third_party/v1/clear_user_balance`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
-### 14.2 输入参数(data解密后)
+### 15.2 输入参数(data解密后)
 
 | 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
 |----------|----------|----------|------|----------|
 | userId | 用户ID | Long | 用户ID | 是 |
 
-### 14.3 请求示例
+### 15.3 请求示例
 
 **data加密前:**
 
@@ -1063,7 +1146,7 @@
 }
 ```
 
-### 14.4 返回参数(data解密后)
+### 15.4 返回参数(data解密后)
 
 | 参数名称 | 参数定义 | 参数类型 | 描述 |
 |----------|----------|----------|------|
@@ -1073,7 +1156,7 @@
 | balanceBefore | 清除前余额 | BigDecimal | 清除前账户余额 |
 | balanceAfter | 清除后余额 | BigDecimal | 清除后账户余额 |
 
-### 14.5 返回示例
+### 15.5 返回示例
 
 **data解密后:**
 

+ 49 - 0
sql/mysql/security_event_log.sql

@@ -0,0 +1,49 @@
+-- ----------------------------
+-- Table structure for security_event_log
+-- ----------------------------
+-- 用途:
+--   记录应用层、网关或 WAF 识别到的安全风险事件,用于追踪恶意请求、误报复盘、统计分析和后续封禁策略。
+-- 设计原则:
+--   1. 不保存完整请求体,仅保存截断摘要和哈希,降低敏感数据落库风险。
+--   2. 事件识别与事件记录解耦,应用校验、限流、网关/WAF 等检测源都可以写入本表。
+--   3. 枚举值使用 varchar,便于后续扩展检测类型、处置动作和风险等级。
+
+CREATE TABLE IF NOT EXISTS `security_event_log` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID,自增唯一标识一条安全事件记录',
+  `event_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '安全事件类型,例如 SQL_INJECTION、XSS、INVALID_JSON、INVALID_PARAMETER、WAF_BLOCK、RATE_LIMIT',
+  `risk_level` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'LOW' COMMENT '风险等级:LOW-低风险,MEDIUM-中风险,HIGH-高风险,CRITICAL-严重风险',
+  `detector` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '检测来源:APP_VALIDATION-应用参数校验,APP_RULE-应用安全规则,WAF-WAF网关,GATEWAY-网关,RATE_LIMIT-限流组件,MANUAL-人工标记',
+  `rule_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '命中的检测规则编号或规则名称,例如 mapper-order-field-whitelist、waf-942100;无明确规则时可为空',
+  `event_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '安全事件简要描述,用于列表展示和人工快速判断事件原因',
+  `request_uri` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求路径,不包含协议、域名和查询字符串,例如 /applet/v1/homePage/getStationInfoPage',
+  `request_method` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'HTTP请求方法,例如 GET、POST、PUT、DELETE',
+  `query_string` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'URL查询字符串摘要,超过字段长度应在写入前截断;用于定位GET参数风险',
+  `client_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP地址,支持IPv4和IPv6;通常取系统解析后的真实访问IP',
+  `x_forwarded_for` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'X-Forwarded-For请求头原始摘要,用于排查代理链路;超过字段长度应在写入前截断',
+  `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'User-Agent请求头摘要,用于识别浏览器、脚本工具或扫描器',
+  `referer` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Referer请求头摘要,用于分析请求来源页面;可能为空',
+  `user_id` bigint(20) NULL DEFAULT NULL COMMENT '平台用户ID;未登录、无法解析或第三方请求无平台用户时为空',
+  `operator_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第三方运营商ID或渠道方标识;非第三方请求可为空',
+  `request_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求追踪ID,例如网关TraceId、日志MDC TraceId或业务生成的请求序列号,用于关联应用日志',
+  `matched_field` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '命中风险的字段名或参数名,例如 latitude、sortType、orderBy;无法定位字段时为空',
+  `payload_excerpt` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '命中风险的请求内容摘要,仅保存截断片段,不保存完整请求体,避免敏感数据长期落库',
+  `payload_hash` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '命中内容的SHA-256哈希值,便于去重、关联同类攻击和追踪重复扫描行为',
+  `action` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'OBSERVE' COMMENT '处置动作:OBSERVE-仅记录,REJECT-拒绝请求,BLOCK-阻断请求,RATE_LIMIT-触发限流,ALLOW-确认放行',
+  `http_status` int(11) NULL DEFAULT NULL COMMENT '本次请求最终返回的HTTP状态码,例如 400、403、429;未进入响应阶段时可为空',
+  `handle_result` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '处理结果说明,例如 rejected_by_validation、blocked_by_waf、record_only、false_positive',
+  `remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注信息,用于人工复核、误报说明或补充上下文',
+  `create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建人ID;系统自动记录通常为空或为系统账号ID',
+  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,即安全事件写入数据库的时间',
+  `update_by` bigint(20) NULL DEFAULT NULL COMMENT '更新人ID;人工复核或修改事件状态时记录操作人ID',
+  `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间,事件被人工复核、修改备注或更新处置结果时刷新',
+  `is_deleted` tinyint(4) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识:0-未删除,1-已删除;用于后台列表隐藏而非物理删除',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_security_event_create_time` (`create_time`) USING BTREE,
+  INDEX `idx_security_event_type_time` (`event_type`, `create_time`) USING BTREE,
+  INDEX `idx_security_event_risk_time` (`risk_level`, `create_time`) USING BTREE,
+  INDEX `idx_security_event_ip_time` (`client_ip`, `create_time`) USING BTREE,
+  INDEX `idx_security_event_request_id` (`request_id`) USING BTREE,
+  INDEX `idx_security_event_payload_hash` (`payload_hash`) USING BTREE,
+  INDEX `idx_security_event_action_time` (`action`, `create_time`) USING BTREE
+) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '安全事件日志表' ROW_FORMAT = DYNAMIC;
+

+ 0 - 59
src/main/java/com/zsElectric/boot/auth/controller/SecurityTestController.java

@@ -1,59 +0,0 @@
-package com.zsElectric.boot.auth.controller;
-
-import com.zsElectric.boot.core.web.Result;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-
-/**
- * 安全测试控制器
- * <p>
- * 用于测试 XSS 和 SQL 注入防护功能
- *
- * @author zsElectric
- */
-@Tag(name = "安全测试接口", description = "用于测试 XSS 和 SQL 注入防护功能")
-@RestController
-@RequestMapping("/api/v1/security/test")
-@RequiredArgsConstructor
-@Slf4j
-public class SecurityTestController {
-
-    @Operation(summary = "测试 XSS 防护", description = "测试 XSS 攻击防护功能")
-    @PostMapping("/xss")
-    public Result<String> testXss(@RequestBody TestRequest request) {
-        log.info("收到 XSS 测试请求: {}", request.getContent());
-        return Result.success("XSS 测试成功,内容: " + request.getContent());
-    }
-
-    @Operation(summary = "测试 SQL 注入防护", description = "测试 SQL 注入防护功能")
-    @PostMapping("/sql-injection")
-    public Result<String> testSqlInjection(@RequestBody TestRequest request) {
-        log.info("收到 SQL 注入测试请求: {}", request.getContent());
-        return Result.success("SQL 注入测试成功,内容: " + request.getContent());
-    }
-
-    @Operation(summary = "测试查询参数", description = "测试查询参数的 XSS 和 SQL 注入防护")
-    @GetMapping("/query")
-    public Result<String> testQueryParams(@RequestParam String param) {
-        log.info("收到查询参数测试请求: {}", param);
-        return Result.success("查询参数测试成功,内容: " + param);
-    }
-
-    /**
-     * 测试请求对象
-     */
-    public static class TestRequest {
-        private String content;
-
-        public String getContent() {
-            return content;
-        }
-
-        public void setContent(String content) {
-            this.content = content;
-        }
-    }
-}

+ 3 - 0
src/main/java/com/zsElectric/boot/business/model/form/applet/AppStopChargeForm.java

@@ -19,4 +19,7 @@ public class AppStopChargeForm implements Serializable {
     @Schema(description = "充电订单编号")
     @NotBlank(message = "充电订单编号不能为空")
     private String chargeOrderNo;
+
+    @Schema(description = "运营商ID")
+    private String operatorId;
 }

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

@@ -25,6 +25,7 @@ import com.zsElectric.boot.business.model.vo.applet.AppUserInfoVO;
 import com.zsElectric.boot.business.service.AppletHomeService;
 import com.zsElectric.boot.business.service.ChargeOrderInfoService;
 import com.zsElectric.boot.business.service.UserAccountService;
+import com.zsElectric.boot.business.service.UserInfoService;
 import com.zsElectric.boot.charging.dto.StartChargingRequestDTO;
 import com.zsElectric.boot.charging.dto.StartChargingResponseVO;
 import com.zsElectric.boot.charging.service.ChargingBusinessService;
@@ -89,6 +90,8 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
 
     private final ChargingBusinessService chargingBusinessService;
 
+    private final ThirdPartyInfoMapper thirdPartyInfoMapper;
+
     private final UserInfoMapper userInfoMapper;
 
     private final FirmInfoMapper firmInfoMapper;
@@ -208,14 +211,14 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
 
         log.info("启动充电开始,用户ID:{},设备认证流水号:{},充电桩编号:{}", SecurityUtils.getUserId(), formData.getEquipAuthSeq(), formData.getEquipmentId());
 
+        //校验设备占用状态
+        chargingBusinessService.checkEquipmentOccupancyStatus(formData.getConnectorId());
+
         // 渠道方启动充电不需要用户资金锁
         if (Objects.equals(formData.getOrderType(), SystemConstants.CHARGE_ORDER_TYPE_CHANNEL)) {
             log.info("渠道方启动充电开始,用户ID:{},设备认证流水号:{},充电桩编号:{}", SecurityUtils.getUserId(), formData.getEquipAuthSeq(), formData.getEquipmentId());
             try {
-                AppChargeVO appInvokeChargeVO = new AppChargeVO();
-                String orderNo = channelInvokeCharge(formData);
-                appInvokeChargeVO.setChargeOrderNo(orderNo);
-                return appInvokeChargeVO;
+                return channelInvokeCharge(formData);
             } catch (Exception e) {
                 log.error("渠道方启动充电失败", e);
                 throw new BusinessException("启动充电失败 !" + e.getMessage());
@@ -380,7 +383,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
      * @param formData
      * @return
      */
-    public String channelInvokeCharge(AppInvokeChargeForm formData) throws JsonProcessingException {
+    public AppChargeVO channelInvokeCharge(AppInvokeChargeForm formData) throws JsonProcessingException {
 
         String seq = ConnectivityConstants.OPERATOR_ID + formData.getChannelOrderNo();
 
@@ -394,14 +397,34 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
             log.info("设备认证成功,设备认证流水号:{}", equipmentAuthResponseVO.getEquipAuthSeq());
         }
 
+        //创建订单
+        ChargeOrderInfo chargeOrderInfo = new ChargeOrderInfo();
         Long userId = SecurityUtils.getUserId();
         User user = userMapper.selectById(userId);
-        FirmInfo firmInfo = firmInfoMapper.selectOne(Wrappers.lambdaQuery(FirmInfo.class).eq(FirmInfo::getDeptId, user.getDeptId()).last("limit 1"));
+        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());
+            }
+        }
+
+        if (ObjectUtil.isNotEmpty(formData.getOperatorId())){
+            ThirdPartyInfo thirdPartyInfo = thirdPartyInfoMapper.selectOne(
+                    Wrappers.lambdaQuery(ThirdPartyInfo.class)
+                            .eq(ThirdPartyInfo::getOperatorId, formData.getOperatorId())
+                            .last("limit 1"));
+            if (thirdPartyInfo != null) {
+                UserInfo userInfo = userInfoMapper.selectOne(
+                        Wrappers.lambdaQuery(UserInfo.class)
+                                .eq(UserInfo::getPhone, formData.getChannelUserPhone())
+                                .eq(UserInfo::getThirdPartId, thirdPartyInfo.getId())
+                                .last("limit 1"));
+                if (userInfo != null) {
+                    chargeOrderInfo.setUserId(userInfo.getId());
+                }
+            }
+        }
 
-        //创建订单
-        ChargeOrderInfo chargeOrderInfo = new ChargeOrderInfo();
-        chargeOrderInfo.setUserId(userId);
-        chargeOrderInfo.setFirmId(firmInfo.getId());
         chargeOrderInfo.setOrderType(SystemConstants.CHARGE_ORDER_TYPE_CHANNEL);
         chargeOrderInfo.setConnectorId(formData.getConnectorId());
         chargeOrderInfo.setEquipmentId(formData.getEquipmentId());
@@ -440,7 +463,11 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
         //保存订单
         this.save(chargeOrderInfo);
 
-        return chargeOrderInfo.getChargeOrderNo();
+        AppChargeVO appInvokeChargeVO = new AppChargeVO();
+        appInvokeChargeVO.setChargeOrderId(chargeOrderInfo.getId());
+        appInvokeChargeVO.setChargeOrderNo(chargeOrderInfo.getChargeOrderNo());
+        appInvokeChargeVO.setStatus(SystemConstants.STATUS_ZERO);
+        return appInvokeChargeVO;
     }
 
     @Override
@@ -449,8 +476,10 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
             AppChargeVO appInvokeChargeVO = new AppChargeVO();
 
             //订单
-            ChargeOrderInfo chargeOrderInfo = this.getOne(Wrappers.lambdaQuery(ChargeOrderInfo.class).eq(ChargeOrderInfo::getChargeOrderNo,
-                    formData.getChargeOrderNo()).last("limit 1"));
+            ChargeOrderInfo chargeOrderInfo = this.getOne(Wrappers.lambdaQuery(ChargeOrderInfo.class)
+                    .eq(ChargeOrderInfo::getChargeOrderNo, formData.getChargeOrderNo())
+                    .eq(StrUtil.isNotBlank(formData.getOperatorId()), ChargeOrderInfo::getOperatorId, formData.getOperatorId())
+                    .last("limit 1"));
             if(ObjectUtil.isEmpty(chargeOrderInfo)){
                 throw new BusinessException("订单不存在");
             }

+ 3 - 0
src/main/java/com/zsElectric/boot/charging/service/ChargingBusinessService.java

@@ -5,6 +5,7 @@ import com.zsElectric.boot.charging.dto.StartChargingRequestDTO;
 import com.zsElectric.boot.charging.dto.StartChargingResponseVO;
 import com.zsElectric.boot.charging.vo.*;
 import com.zsElectric.boot.common.util.electric.ApiToken;
+import jakarta.validation.constraints.NotBlank;
 
 import java.util.List;
 
@@ -70,4 +71,6 @@ public interface ChargingBusinessService {
      * @author SheepHy
      */
     StopChargingOperationResponseVO stopCharging(String StartChargeSeq, String ConnectorID) throws JsonProcessingException;
+
+    void checkEquipmentOccupancyStatus(@NotBlank(message = "充电设备编号不能为空") String connectorId);
 }

+ 44 - 0
src/main/java/com/zsElectric/boot/charging/service/impl/ChargingBusinessServiceImpl.java

@@ -4,8 +4,11 @@ import cn.hutool.core.bean.BeanUtil;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zsElectric.boot.charging.dto.StartChargingRequestDTO;
 import com.zsElectric.boot.charging.dto.StartChargingResponseVO;
+import com.zsElectric.boot.charging.entity.ThirdPartyConnectorInfo;
+import com.zsElectric.boot.charging.mapper.ThirdPartyConnectorInfoMapper;
 import com.zsElectric.boot.charging.service.ChargingBusinessService;
 import com.zsElectric.boot.business.service.ThirdPartyChargingService;
 import com.zsElectric.boot.charging.vo.*;
@@ -13,6 +16,7 @@ import com.zsElectric.boot.common.constant.ConnectivityConstants;
 import com.zsElectric.boot.common.util.AESCryptoUtils;
 import com.zsElectric.boot.common.util.HmacMD5Util;
 import com.zsElectric.boot.common.util.electric.*;
+import com.zsElectric.boot.core.exception.BusinessException;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -30,6 +34,7 @@ public class ChargingBusinessServiceImpl implements ChargingBusinessService {
 
     private final ChargingUtil chargingUtil;
     private final ThirdPartyChargingService thirdPartyChargingService;
+    private final ThirdPartyConnectorInfoMapper connectorInfoMapper;
 
     private final ObjectMapper objectMapper = new ObjectMapper();
 
@@ -203,4 +208,43 @@ public class ChargingBusinessServiceImpl implements ChargingBusinessService {
         log.info("停止充电返回结果解密后:{}", responseDecode);
         return objectMapper.readValue(responseDecode.toString(), StopChargingOperationResponseVO.class);
     }
+
+    @Override
+    public void checkEquipmentOccupancyStatus(String connectorId) {
+        ThirdPartyConnectorInfo connectorInfo = connectorInfoMapper.selectOne(
+                new LambdaQueryWrapper<ThirdPartyConnectorInfo>()
+                        .eq(ThirdPartyConnectorInfo::getConnectorId, connectorId)
+                        .eq(ThirdPartyConnectorInfo::getIsDeleted, 0)
+                        .last("LIMIT 1")
+        );
+        if (connectorInfo == null) {
+            throw new BusinessException("充电终端不存在,connectorId: {}", connectorId);
+        }
+
+        Integer status = connectorInfo.getStatus();
+        if (status == null) {
+            throw new BusinessException("充电终端状态异常,无法获取设备状态");
+        }
+
+        switch (status) {
+            case 0:
+                throw new BusinessException("充电终端已离网,无法启动充电");
+            case 2:
+                // 占用(未充电)状态,允许充电
+                log.info("充电终端状态校验通过,connectorId: {},状态:占用(未充电)", connectorId);
+                break;
+            case 3:
+                throw new BusinessException("充电终端正在充电中,请稍后再试");
+            case 4:
+                throw new BusinessException("充电终端已被预约锁定,请稍后再试");
+            case 255:
+                throw new BusinessException("充电终端故障,无法启动充电");
+            case 1:
+                // 空闲状态,允许充电
+                log.info("充电终端状态校验通过,connectorId: {},状态:空闲", connectorId);
+                break;
+            default:
+                throw new BusinessException("充电终端状态未知({}), 无法启动充电", status);
+        }
+    }
 }

+ 27 - 25
src/main/java/com/zsElectric/boot/charging/service/impl/ChargingReceptionServiceImpl.java

@@ -184,35 +184,37 @@ public class ChargingReceptionServiceImpl implements ChargingReceptionService {
                             //订单信息渠道方推送
                             Map<String, Object> map = objectMapper.convertValue(jsonNode, Map.class);
                             map.put("chargeOrderNo", chargeOrderInfo.getChargeOrderNo());
-                            FirmInfo firmInfo = firmInfoMapper.selectById(chargeOrderInfo.getFirmId());
-                            String requestBody = com.alibaba.fastjson2.JSONObject.toJSONString(map);
-                            if (ObjectUtil.isNotNull(firmInfo)) {
-                                String url = firmInfo.getChannelUrl() + "/notification_charge_order_info";
-                                int maxRetries = 3;
-                                int retryIntervalMs = 5000;
-                                for (int attempt = 1; attempt <= maxRetries; attempt++) {
-                                    try {
-                                        JsonNode response = okHttpUtil.doPostJson(url, requestBody, null);
-                                        log.info("渠道方推送充电订单信息成功 - chargeOrderNo: {}, firmId: {}, response: {}", 
-                                                chargeOrderInfo.getChargeOrderNo(), chargeOrderInfo.getFirmId(), response);
-                                        break;
-                                    } catch (Exception e) {
-                                        log.error("渠道方推送充电订单信息失败(第{}次) - chargeOrderNo: {}, firmId: {}, channelUrl: {}, 错误信息: {}", 
-                                                attempt, chargeOrderInfo.getChargeOrderNo(), chargeOrderInfo.getFirmId(), url, e.getMessage(), e);
-                                        if (attempt < maxRetries) {
-                                            try {
-                                                Thread.sleep(retryIntervalMs);
-                                            } catch (InterruptedException ie) {
-                                                Thread.currentThread().interrupt();
-                                                log.warn("重试等待被中断 - chargeOrderNo: {}", chargeOrderInfo.getChargeOrderNo());
-                                                break;
+                            if(ObjectUtil.isNotEmpty(chargeOrderInfo.getFirmId())){
+                                FirmInfo firmInfo = firmInfoMapper.selectById(chargeOrderInfo.getFirmId());
+                                String requestBody = com.alibaba.fastjson2.JSONObject.toJSONString(map);
+                                if (ObjectUtil.isNotNull(firmInfo)) {
+                                    String url = firmInfo.getChannelUrl() + "/notification_charge_order_info";
+                                    int maxRetries = 3;
+                                    int retryIntervalMs = 5000;
+                                    for (int attempt = 1; attempt <= maxRetries; attempt++) {
+                                        try {
+                                            JsonNode response = okHttpUtil.doPostJson(url, requestBody, null);
+                                            log.info("渠道方推送充电订单信息成功 - chargeOrderNo: {}, firmId: {}, response: {}",
+                                                    chargeOrderInfo.getChargeOrderNo(), chargeOrderInfo.getFirmId(), response);
+                                            break;
+                                        } catch (Exception e) {
+                                            log.error("渠道方推送充电订单信息失败(第{}次) - chargeOrderNo: {}, firmId: {}, channelUrl: {}, 错误信息: {}",
+                                                    attempt, chargeOrderInfo.getChargeOrderNo(), chargeOrderInfo.getFirmId(), url, e.getMessage(), e);
+                                            if (attempt < maxRetries) {
+                                                try {
+                                                    Thread.sleep(retryIntervalMs);
+                                                } catch (InterruptedException ie) {
+                                                    Thread.currentThread().interrupt();
+                                                    log.warn("重试等待被中断 - chargeOrderNo: {}", chargeOrderInfo.getChargeOrderNo());
+                                                    break;
+                                                }
                                             }
                                         }
                                     }
+                                } else {
+                                    log.warn("渠道方推送充电订单信息失败 - firmInfo为空, chargeOrderNo: {}, firmId: {}",
+                                            chargeOrderInfo.getChargeOrderNo(), chargeOrderInfo.getFirmId());
                                 }
-                            } else {
-                                log.warn("渠道方推送充电订单信息失败 - firmInfo为空, chargeOrderNo: {}, firmId: {}", 
-                                        chargeOrderInfo.getChargeOrderNo(), chargeOrderInfo.getFirmId());
                             }
                         }
                         //推送订单明细

+ 0 - 380
src/main/java/com/zsElectric/boot/common/util/SecurityUtils.java

@@ -1,380 +0,0 @@
-package com.zsElectric.boot.common.util;
-
-import cn.hutool.core.util.StrUtil;
-import lombok.extern.slf4j.Slf4j;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-/**
- * 安全防护工具类
- * <p>
- * 提供 XSS 攻击和 SQL 注入防护的核心检测方法
- *
- * @author zsElectric
- */
-@Slf4j
-public class SecurityUtils {
-
-    /**
-     * SQL 注入检测是否使用严格模式
-     * 默认为宽松模式以减少误判
-     */
-    private static volatile boolean sqlStrictMode = false;
-
-    /**
-     * 设置 SQL 注入检测模式
-     * 
-     * @param strictMode true 为严格模式,false 为宽松模式
-     */
-    public static void setSqlStrictMode(boolean strictMode) {
-        sqlStrictMode = strictMode;
-    }
-
-    /**
-     * XSS 攻击检测正则表达式
-     */
-    private static final Pattern[] XSS_PATTERNS = {
-            // Script 标签
-            Pattern.compile("<script[^>]*?>.*?</script>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
-            Pattern.compile("<script[^>]*?>", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
-            // JavaScript 事件
-            Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onerror\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onload\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onclick\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onmouseover\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onfocus\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onblur\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onsubmit\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onreset\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onselect\\s*=", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("onchange\\s*=", Pattern.CASE_INSENSITIVE),
-            // iframe 标签
-            Pattern.compile("<iframe[^>]*?>.*?</iframe>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
-            Pattern.compile("<iframe[^>]*?>", Pattern.CASE_INSENSITIVE),
-            // embed、object 标签
-            Pattern.compile("<embed[^>]*?>", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("<object[^>]*?>", Pattern.CASE_INSENSITIVE),
-            // eval、expression
-            Pattern.compile("eval\\s*\\(", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("expression\\s*\\(", Pattern.CASE_INSENSITIVE),
-            // vbscript
-            Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
-            // img 标签的 src
-            Pattern.compile("<img[^>]+src[\\s]*=[\\s]*['\"]?javascript:", Pattern.CASE_INSENSITIVE),
-            // style 中的 expression
-            Pattern.compile("style\\s*=.*expression", Pattern.CASE_INSENSITIVE),
-            // base64 编码的脚本
-            Pattern.compile("data:text/html;base64", Pattern.CASE_INSENSITIVE),
-            // SVG
-            Pattern.compile("<svg[^>]*?>.*?</svg>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
-            // Meta 标签
-            Pattern.compile("<meta[^>]*?>", Pattern.CASE_INSENSITIVE),
-            // Link 标签
-            Pattern.compile("<link[^>]*?>", Pattern.CASE_INSENSITIVE)
-    };
-
-    /**
-     * SQL 注入危险关键词
-     */
-    private static final Set<String> SQL_KEYWORDS = new HashSet<>(Arrays.asList(
-            // DML 语句
-            "select", "insert", "update", "delete",
-            // DDL 语句
-            "drop", "create", "alter", "truncate",
-            // DCL 语句
-            "grant", "revoke",
-            // 联合查询
-            "union", "join",
-            // 系统函数和存储过程
-            "exec", "execute", "xp_cmdshell", "sp_executesql",
-            // 信息获取
-            "information_schema", "mysql.user", "sys.",
-            // 条件判断
-            "case", "when", "then", "else", "end",
-            // 其他危险操作
-            "declare", "cast", "convert", "char", "chr",
-            "concat", "load_file", "into outfile", "into dumpfile",
-            "benchmark", "sleep", "waitfor", "delay",
-            // 子查询
-            "exists", "any", "all", "some"
-    ));
-
-    /**
-     * SQL 注入检测正则表达式
-     */
-    private static final Pattern[] SQL_INJECTION_PATTERNS = {
-            // SQL 注释
-            Pattern.compile("('.+--)|(--)|(;)|(\\|{2})"),
-            // SQL 函数调用
-            Pattern.compile("\\bexec(ute)?\\s*\\(", Pattern.CASE_INSENSITIVE),
-            // union 查询
-            Pattern.compile("\\bunion\\b.*\\bselect\\b", Pattern.CASE_INSENSITIVE),
-            // 多语句
-            Pattern.compile(";.*?(select|insert|update|delete|drop|create|alter)", Pattern.CASE_INSENSITIVE),
-            // 16 进制编码
-            Pattern.compile("0x[0-9a-f]+", Pattern.CASE_INSENSITIVE),
-            // 字符串拼接
-            Pattern.compile("\\bconcat\\s*\\(", Pattern.CASE_INSENSITIVE),
-            // sleep 函数
-            Pattern.compile("\\bsleep\\s*\\(", Pattern.CASE_INSENSITIVE),
-            // benchmark 函数
-            Pattern.compile("\\bbenckmark\\s*\\(", Pattern.CASE_INSENSITIVE),
-            // waitfor delay
-            Pattern.compile("\\bwaitfor\\s+\\bdelay\\b", Pattern.CASE_INSENSITIVE),
-            // 子查询
-            Pattern.compile("\\bsubstr\\s*\\(", Pattern.CASE_INSENSITIVE),
-            Pattern.compile("\\bsubstring\\s*\\(", Pattern.CASE_INSENSITIVE)
-    };
-
-    /**
-     * 检测 XSS 攻击
-     *
-     * @param value 待检测的字符串
-     * @return 如果检测到 XSS 攻击返回 true,否则返回 false
-     */
-    public static boolean containsXss(String value) {
-        if (StrUtil.isBlank(value)) {
-            return false;
-        }
-
-        // 解码 URL 编码
-        String decodedValue = urlDecode(value);
-
-        // 使用正则表达式检测
-        for (Pattern pattern : XSS_PATTERNS) {
-            if (pattern.matcher(decodedValue).find()) {
-                log.warn("检测到 XSS 攻击,匹配模式: {}, 内容: {}", pattern.pattern(), value);
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * 检测 SQL 注入
-     *
-     * @param value 待检测的字符串
-     * @return 如果检测到 SQL 注入返回 true,否则返回 false
-     */
-    public static boolean containsSqlInjection(String value) {
-        if (StrUtil.isBlank(value)) {
-            return false;
-        }
-
-        String lowerValue = value.toLowerCase();
-
-        // 检查注释符号(更严格的检测)
-        if (lowerValue.contains("--") || lowerValue.contains("/*") || lowerValue.contains("*/") || lowerValue.contains("#")) {
-            // 检查是否是真正的注释而不是普通文本
-            if (lowerValue.matches(".*\\s(--|#).*") || lowerValue.contains("/*") || lowerValue.contains("*/")) {
-                log.warn("检测到 SQL 注入注释符号: {}, 内容: {}", "--/#/*", value);
-                return true;
-            }
-        }
-
-        // 检查危险关键词(使用更精确的匹配规则)
-        for (String keyword : SQL_KEYWORDS) {
-            // 使用单词边界进行匹配,避免误判(例如:"selection" 不应匹配 "select")
-            // 同时确保关键词前后不是字母数字字符
-            String pattern = "([^a-zA-Z0-9]|^)" + keyword + "([^a-zA-Z0-9]|$)";
-            if (Pattern.compile(pattern, Pattern.CASE_INSENSITIVE).matcher(lowerValue).find()) {
-                // 进一步检查是否为真实攻击而非正常文本
-                // 例如:"select" 在 "selected" 中是正常文本,但在 "select * from" 中可能是攻击
-                if (isRealSqlInjection(keyword, lowerValue)) {
-                    log.warn("检测到 SQL 注入关键词: {}, 内容: {}", keyword, value);
-                    return true;
-                }
-            }
-        }
-
-        // 使用正则表达式检测
-        for (Pattern pattern : SQL_INJECTION_PATTERNS) {
-            if (pattern.matcher(value).find()) {
-                log.warn("检测到 SQL 注入,匹配模式: {}, 内容: {}", pattern.pattern(), value);
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * 清理 XSS 攻击内容(转义特殊字符)
-     *
-     * @param value 待清理的字符串
-     * @return 清理后的字符串
-     */
-    public static String cleanXss(String value) {
-        if (StrUtil.isBlank(value)) {
-            return value;
-        }
-
-        // HTML 实体编码
-        value = value.replace("&", "&amp;")
-                .replace("<", "&lt;")
-                .replace(">", "&gt;")
-                .replace("\"", "&quot;")
-                .replace("'", "&#x27;")
-                .replace("/", "&#x2F;");
-
-        return value;
-    }
-
-    /**
-     * URL 解码(支持多次编码)
-     *
-     * @param value 待解码的字符串
-     * @return 解码后的字符串
-     */
-    private static String urlDecode(String value) {
-        String decoded = value;
-        try {
-            // 最多解码 3 次,防止多重编码绕过
-            for (int i = 0; i < 3; i++) {
-                String temp = java.net.URLDecoder.decode(decoded, "UTF-8");
-                if (temp.equals(decoded)) {
-                    break;
-                }
-                decoded = temp;
-            }
-        } catch (Exception e) {
-            log.warn("URL 解码失败: {}", value, e);
-        }
-        return decoded;
-    }
-
-    /**
-     * 验证输入是否安全(综合检查 XSS 和 SQL 注入)
-     *
-     * @param value 待验证的字符串
-     * @return 如果输入安全返回 true,否则返回 false
-     */
-    public static boolean isSafeInput(String value) {
-        return !containsXss(value) && !containsSqlInjection(value);
-    }
-
-    /**
-     * 判断是否为真实的SQL注入攻击
-     * 
-     * @param keyword 检测到的关键词
-     * @param value   待检测的字符串(小写)
-     * @return 如果是真实攻击返回 true,否则返回 false
-     */
-    private static boolean isRealSqlInjection(String keyword, String value) {
-        if (!sqlStrictMode) {
-            // 在宽松模式下,只对明显的攻击模式进行拦截
-            switch (keyword) {
-                case "select":
-                    // 只有当 select 后面跟着典型的 SQL 结构时才认为是攻击
-                    boolean isSelectAttack = value.matches(".*select\\s+[a-z0-9_*]+\\s+from\\s+[a-z0-9_]+.*") || 
-                           value.matches(".*select\\s+\\*\\s+from\\s+[a-z0-9_]+.*");
-                    if (isSelectAttack) {
-                        log.debug("检测到可能的 SELECT 攻击: {}", value);
-                    }
-                    return isSelectAttack;
-                case "insert":
-                    boolean isInsertAttack = value.contains("insert into");
-                    if (isInsertAttack) {
-                        log.debug("检测到可能的 INSERT 攻击: {}", value);
-                    }
-                    return isInsertAttack;
-                case "update":
-                    boolean isUpdateAttack = value.contains("update ") && value.contains(" set ");
-                    if (isUpdateAttack) {
-                        log.debug("检测到可能的 UPDATE 攻击: {}", value);
-                    }
-                    return isUpdateAttack;
-                case "delete":
-                    boolean isDeleteAttack = value.contains("delete from");
-                    if (isDeleteAttack) {
-                        log.debug("检测到可能的 DELETE 攻击: {}", value);
-                    }
-                    return isDeleteAttack;
-                case "drop":
-                case "create":
-                case "alter":
-                case "truncate":
-                    boolean isDdlAttack = value.contains(keyword + " ");
-                    if (isDdlAttack) {
-                        log.debug("检测到可能的 DDL 攻击 ({}): {}", keyword, value);
-                    }
-                    return isDdlAttack;
-                case "union":
-                    boolean isUnionAttack = value.contains("union select");
-                    if (isUnionAttack) {
-                        log.debug("检测到可能的 UNION 攻击: {}", value);
-                    }
-                    return isUnionAttack;
-                case "exec":
-                case "execute":
-                    boolean isExecAttack = value.contains(keyword + "(");
-                    if (isExecAttack) {
-                        log.debug("检测到可能的 EXEC 攻击 ({}): {}", keyword, value);
-                    }
-                    return isExecAttack;
-                default:
-                    // 对于其他关键词,采用宽松策略,避免误判正常用户名等
-                    return false;
-            }
-        } else {
-            // 严格模式下保持原来的逻辑
-            switch (keyword) {
-                case "select":
-                    // select 通常是攻击的一部分,后面跟着列名和 from
-                    boolean isSelectAttack = value.matches(".*select\\s+[a-z0-9_*]+\\s+from\\s+[a-z0-9_]+.*") || 
-                           value.matches(".*select\\s+\\*\\s+from\\s+[a-z0-9_]+.*");
-                    if (isSelectAttack) {
-                        log.debug("[严格模式] 检测到可能的 SELECT 攻击: {}", value);
-                    }
-                    return isSelectAttack;
-                case "insert":
-                    // insert 通常是攻击的一部分,后面跟着 into
-                    boolean isInsertAttack = value.contains("insert into");
-                    if (isInsertAttack) {
-                        log.debug("[严格模式] 检测到可能的 INSERT 攻击: {}", value);
-                    }
-                    return isInsertAttack;
-                case "update":
-                    // update 通常是攻击的一部分,后面跟着 set
-                    boolean isUpdateAttack = value.contains("update ") && value.contains(" set ");
-                    if (isUpdateAttack) {
-                        log.debug("[严格模式] 检测到可能的 UPDATE 攻击: {}", value);
-                    }
-                    return isUpdateAttack;
-                case "delete":
-                    // delete 通常是攻击的一部分,后面跟着 from
-                    boolean isDeleteAttack = value.contains("delete from");
-                    if (isDeleteAttack) {
-                        log.debug("[严格模式] 检测到可能的 DELETE 攻击: {}", value);
-                    }
-                    return isDeleteAttack;
-                case "drop":
-                case "create":
-                case "alter":
-                case "truncate":
-                    // DDL 语句通常是攻击的一部分
-                    boolean isDdlAttack = value.contains(keyword + " ");
-                    if (isDdlAttack) {
-                        log.debug("[严格模式] 检测到可能的 DDL 攻击 ({}): {}", keyword, value);
-                    }
-                    return isDdlAttack;
-                case "union":
-                    // union 通常是攻击的一部分,后面跟着 select
-                    boolean isUnionAttack = value.contains("union select");
-                    if (isUnionAttack) {
-                        log.debug("[严格模式] 检测到可能的 UNION 攻击: {}", value);
-                    }
-                    return isUnionAttack;
-                default:
-                    // 对于其他关键词,采用宽松策略,避免误判正常用户名等
-                    return false;
-            }
-        }
-    }
-}

+ 6 - 6
src/main/java/com/zsElectric/boot/common/util/electric/queryToken/ThirdPartyJwtAuthFilter.java

@@ -44,21 +44,21 @@ public class ThirdPartyJwtAuthFilter extends OncePerRequestFilter {
     private final AntPathMatcher pathMatcher = new AntPathMatcher();
     
     public ThirdPartyJwtAuthFilter() {
-        log.warn("========== ThirdPartyJwtAuthFilter 已初始化 ==========");
+        log.info("========== ThirdPartyJwtAuthFilter 已初始化 ==========");
     }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         
         String requestUri = request.getRequestURI();
-        log.warn("第三方JWT过滤器处理请求: {}", requestUri);
+        log.debug("第三方JWT过滤器处理请求: {}", requestUri);
 
         boolean isThirdPartyRequest = thirdPartyApiPaths.stream()
                 .anyMatch(pattern -> pathMatcher.match(pattern, requestUri));
 
         // 检查当前请求是否是需要第三方Token验证的接口
         if (isThirdPartyRequest) {
-            log.warn("检测到第三方接口请求: {}", requestUri);
+            log.info("检测到第三方接口请求: {}", requestUri);
             String token = extractToken(request);
             
             if (token == null) {
@@ -67,14 +67,14 @@ public class ThirdPartyJwtAuthFilter extends OncePerRequestFilter {
                 return; // 重要:直接返回,不再执行过滤链后续操作
             }
             
-            log.warn("提取到Token: {}...", token.substring(0, Math.min(20, token.length())));
+            log.debug("提取到Token: {}...", token.substring(0, Math.min(20, token.length())));
             
             try {
                 // 验证Token的有效性(例如是否过期、签名是否正确)
                 if (jwtTokenUtil.validateToken(token)) {
                     // 从Token中解析用户标识
                     String principal = jwtTokenUtil.getOperatorIdFromToken(token);
-                    log.warn("Token验证成功,OperatorID: {}", principal);
+                    log.debug("Token验证成功,OperatorID: {}", principal);
                     // 构建Authentication对象,细节见下文
                     UsernamePasswordAuthenticationToken authentication =
                         new UsernamePasswordAuthenticationToken(principal, null, new ArrayList<>());
@@ -123,4 +123,4 @@ public class ThirdPartyJwtAuthFilter extends OncePerRequestFilter {
         response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
         response.getWriter().flush();
     }
-}
+}

+ 0 - 27
src/main/java/com/zsElectric/boot/common/xss/Xss.java

@@ -1,27 +0,0 @@
-package com.zsElectric.boot.common.xss;
-
-import jakarta.validation.Constraint;
-import jakarta.validation.Payload;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * 自定义xss校验注解
- *
- * @author Lion Li
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
-@Constraint(validatedBy = {XssValidator.class})
-public @interface Xss {
-
-    String message() default "不允许任何脚本运行";
-
-    Class<?>[] groups() default {};
-
-    Class<? extends Payload>[] payload() default {};
-
-}

+ 0 - 58
src/main/java/com/zsElectric/boot/common/xss/XssFilter.java

@@ -1,58 +0,0 @@
-package com.zsElectric.boot.common.xss;
-
-import com.zsElectric.boot.common.util.SpringUtils;
-import com.zsElectric.boot.common.util.StringUtils;
-import jakarta.servlet.*;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.springframework.http.HttpMethod;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * 防止XSS攻击的过滤器
- *
- * @author ruoyi
- */
-public class XssFilter implements Filter {
-    /**
-     * 排除链接
-     */
-    public List<String> excludes = new ArrayList<>();
-
-    @Override
-    public void init(FilterConfig filterConfig) throws ServletException {
-        XssProperties properties = SpringUtils.getBean(XssProperties.class);
-        excludes.addAll(properties.getExcludeUrls());
-    }
-
-    @Override
-    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
-        throws IOException, ServletException {
-        HttpServletRequest req = (HttpServletRequest) request;
-        HttpServletResponse resp = (HttpServletResponse) response;
-        if (handleExcludeURL(req, resp)) {
-            chain.doFilter(request, response);
-            return;
-        }
-        XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
-        chain.doFilter(xssRequest, response);
-    }
-
-    private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
-        String url = request.getServletPath();
-        String method = request.getMethod();
-        // GET DELETE 不过滤
-        if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) {
-            return true;
-        }
-        return StringUtils.matches(url, excludes);
-    }
-
-    @Override
-    public void destroy() {
-
-    }
-}

+ 0 - 134
src/main/java/com/zsElectric/boot/common/xss/XssHttpServletRequestWrapper.java

@@ -1,134 +0,0 @@
-package com.zsElectric.boot.common.xss;
-
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.map.MapUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.http.HtmlUtil;
-import com.zsElectric.boot.common.util.StringUtils;
-import jakarta.servlet.ReadListener;
-import jakarta.servlet.ServletInputStream;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletRequestWrapper;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * XSS过滤处理
- *
- * @author ruoyi
- */
-public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
-    /**
-     * @param request
-     */
-    public XssHttpServletRequestWrapper(HttpServletRequest request) {
-        super(request);
-    }
-
-    @Override
-    public String getParameter(String name) {
-        String value = super.getParameter(name);
-        if (value == null) {
-            return null;
-        }
-        return HtmlUtil.cleanHtmlTag(value).trim();
-    }
-
-    @Override
-    public Map<String, String[]> getParameterMap() {
-        Map<String, String[]> valueMap = super.getParameterMap();
-        if (MapUtil.isEmpty(valueMap)) {
-            return valueMap;
-        }
-        // 避免某些容器不允许改参数的情况 copy一份重新改
-        Map<String, String[]> map = new HashMap<>(valueMap.size());
-        map.putAll(valueMap);
-        for (Map.Entry<String, String[]> entry : map.entrySet()) {
-            String[] values = entry.getValue();
-            if (values != null) {
-                int length = values.length;
-                String[] escapseValues = new String[length];
-                for (int i = 0; i < length; i++) {
-                    // 防xss攻击和过滤前后空格
-                    escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
-                }
-                map.put(entry.getKey(), escapseValues);
-            }
-        }
-        return map;
-    }
-
-    @Override
-    public String[] getParameterValues(String name) {
-        String[] values = super.getParameterValues(name);
-        if (ArrayUtil.isEmpty(values)) {
-            return values;
-        }
-        int length = values.length;
-        String[] escapseValues = new String[length];
-        for (int i = 0; i < length; i++) {
-            // 防xss攻击和过滤前后空格
-            escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
-        }
-        return escapseValues;
-    }
-
-    @Override
-    public ServletInputStream getInputStream() throws IOException {
-        // 非json类型,直接返回
-        if (!isJsonRequest()) {
-            return super.getInputStream();
-        }
-
-        // 为空,直接返回
-        String json = StrUtil.str(IoUtil.readBytes(super.getInputStream(), false), StandardCharsets.UTF_8);
-        if (StringUtils.isEmpty(json)) {
-            return super.getInputStream();
-        }
-
-        // xss过滤
-        json = HtmlUtil.cleanHtmlTag(json).trim();
-        byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
-        final ByteArrayInputStream bis = IoUtil.toStream(jsonBytes);
-        return new ServletInputStream() {
-            @Override
-            public boolean isFinished() {
-                return true;
-            }
-
-            @Override
-            public boolean isReady() {
-                return true;
-            }
-
-            @Override
-            public int available() throws IOException {
-                return jsonBytes.length;
-            }
-
-            @Override
-            public void setReadListener(ReadListener readListener) {
-            }
-
-            @Override
-            public int read() throws IOException {
-                return bis.read();
-            }
-        };
-    }
-
-    /**
-     * 是否是Json请求
-     */
-    public boolean isJsonRequest() {
-        String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
-        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
-    }
-}

+ 0 - 28
src/main/java/com/zsElectric/boot/common/xss/XssProperties.java

@@ -1,28 +0,0 @@
-package com.zsElectric.boot.common.xss;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * xss过滤 配置属性
- *
- * @author Lion Li
- */
-@Data
-@ConfigurationProperties(prefix = "xss")
-public class XssProperties {
-
-    /**
-     * Xss开关
-     */
-    private Boolean enabled;
-
-    /**
-     * 排除路径
-     */
-    private List<String> excludeUrls = new ArrayList<>();
-
-}

+ 0 - 20
src/main/java/com/zsElectric/boot/common/xss/XssValidator.java

@@ -1,20 +0,0 @@
-package com.zsElectric.boot.common.xss;
-
-import cn.hutool.core.util.ReUtil;
-import cn.hutool.http.HtmlUtil;
-import jakarta.validation.ConstraintValidator;
-import jakarta.validation.ConstraintValidatorContext;
-
-/**
- * 自定义xss校验注解实现
- *
- * @author Lion Li
- */
-public class XssValidator implements ConstraintValidator<Xss, String> {
-
-    @Override
-    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
-        return !ReUtil.contains(HtmlUtil.RE_HTML_MARK, value);
-    }
-
-}

+ 6 - 19
src/main/java/com/zsElectric/boot/config/FilterConfig.java

@@ -1,42 +1,29 @@
 package com.zsElectric.boot.config;
 
-import com.zsElectric.boot.config.property.SecurityFilterProperties;
-import com.zsElectric.boot.core.filter.XssAndSqlInjectionFilter;
+import com.zsElectric.boot.common.util.electric.queryToken.ThirdPartyJwtAuthFilter;
 import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
 /**
  * 过滤器配置类
- * <p>
- * 配置 XSS 和 SQL 注入防护过滤器
  *
  * @author zsElectric
  */
-@Slf4j
 @Configuration
 @RequiredArgsConstructor
 public class FilterConfig {
     
-    private final SecurityFilterProperties securityFilterProperties;
+    private final ThirdPartyJwtAuthFilter thirdPartyJwtAuthFilter;
 
     /**
-     * 注册 XSS 和 SQL 注入防护过滤器
+     * ThirdPartyJwtAuthFilter 只应挂在 SecurityFilterChain 中,避免被 Spring Boot 自动注册为全局 Servlet Filter。
      */
     @Bean
-    public FilterRegistrationBean<XssAndSqlInjectionFilter> xssAndSqlInjectionFilter() {
-        log.info("注册 XSS 和 SQL 注入防护过滤器");
-        
-        FilterRegistrationBean<XssAndSqlInjectionFilter> registration = new FilterRegistrationBean<>();
-        registration.setFilter(new XssAndSqlInjectionFilter(securityFilterProperties));
-        registration.addUrlPatterns("/*");
-        registration.setName("xssAndSqlInjectionFilter");
-        // 设置过滤器优先级(数字越小优先级越高)
-        // 应该在 Spring Security 过滤器之前执行
-        registration.setOrder(1);
-        
+    public FilterRegistrationBean<ThirdPartyJwtAuthFilter> thirdPartyJwtAuthFilterRegistration() {
+        FilterRegistrationBean<ThirdPartyJwtAuthFilter> registration = new FilterRegistrationBean<>(thirdPartyJwtAuthFilter);
+        registration.setEnabled(false);
         return registration;
     }
 }

+ 0 - 31
src/main/java/com/zsElectric/boot/config/SecurityUtilsConfig.java

@@ -1,31 +0,0 @@
-package com.zsElectric.boot.config;
-
-import com.zsElectric.boot.common.util.SecurityUtils;
-import com.zsElectric.boot.config.property.SecurityFilterProperties;
-import jakarta.annotation.PostConstruct;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * 安全工具类配置
- * <p>
- * 用于初始化安全工具类的相关配置
- *
- * @author zsElectric
- */
-@Slf4j
-@Configuration
-@RequiredArgsConstructor
-public class SecurityUtilsConfig {
-
-    private final SecurityFilterProperties securityFilterProperties;
-
-    @PostConstruct
-    public void init() {
-        // 设置 SQL 注入检测模式
-        SecurityUtils.setSqlStrictMode(securityFilterProperties.getSqlStrictMode());
-        log.info("安全工具类初始化完成,SQL 注入检测模式: {}", 
-                securityFilterProperties.getSqlStrictMode() ? "严格模式" : "宽松模式");
-    }
-}

+ 0 - 48
src/main/java/com/zsElectric/boot/config/property/SecurityFilterProperties.java

@@ -1,48 +0,0 @@
-package com.zsElectric.boot.config.property;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * 安全过滤器配置属性
- * <p>
- * 用于配置 XSS 和 SQL 注入防护过滤器的行为
- *
- * @author zsElectric
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "security.filter")
-public class SecurityFilterProperties {
-
-    /**
-     * 是否启用 XSS 防护
-     */
-    private Boolean xssEnabled = true;
-
-    /**
-     * 是否启用 SQL 注入防护
-     */
-    private Boolean sqlInjectionEnabled = true;
-
-    /**
-     * SQL 注入检测严格模式
-     * true: 严格模式,可能误判一些正常输入
-     * false: 宽松模式,减少误判但可能漏掉一些攻击
-     */
-    private Boolean sqlStrictMode = false;
-
-    /**
-     * 排除的 URL 路径(不进行安全检查)
-     */
-    private List<String> excludeUrls = new ArrayList<>();
-
-    /**
-     * 需要检查的请求头
-     */
-    private List<String> checkHeaders = new ArrayList<>();
-}

+ 18 - 0
src/main/java/com/zsElectric/boot/config/property/SecurityProperties.java

@@ -46,6 +46,11 @@ public class SecurityProperties {
      */
     private String[] thirdPartyUrls;
 
+    /**
+     * 恶意请求拦截配置
+     */
+    private MaliciousRequestBlockConfig maliciousRequestBlock = new MaliciousRequestBlockConfig();
+
     /**
      * 会话配置嵌套类
      */
@@ -114,4 +119,17 @@ public class SecurityProperties {
          */
         private Boolean allowMultiLogin = true;
     }
+
+    /**
+     * 恶意请求拦截配置嵌套类
+     */
+    @Data
+    public static class MaliciousRequestBlockConfig {
+        /**
+         * 是否开启恶意请求拦截
+         * <p>true - 拦截高置信恶意请求(默认)</p>
+         * <p>false - 关闭过滤器拦截,用于灰度回退</p>
+         */
+        private Boolean enabled = true;
+    }
 }

+ 0 - 22
src/main/java/com/zsElectric/boot/core/exception/BadHttpRequestException.java

@@ -1,22 +0,0 @@
-package com.zsElectric.boot.core.exception;
-
-import lombok.Getter;
-
-/**
- * 恶意HTTP请求异常
- * <p>
- * 用于标识XSS攻击、SQL注入等恶意请求
- * 
- * @author zsElectric
- */
-@Getter
-public class BadHttpRequestException extends RuntimeException {
-
-    public BadHttpRequestException(String message) {
-        super(message);
-    }
-
-    public BadHttpRequestException(String message, Throwable cause) {
-        super(message, cause);
-    }
-}

+ 238 - 18
src/main/java/com/zsElectric/boot/core/exception/GlobalExceptionHandler.java

@@ -2,12 +2,21 @@ package com.zsElectric.boot.core.exception;
 
 import cn.hutool.core.util.StrUtil;
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.zsElectric.boot.common.util.IPUtils;
+import com.zsElectric.boot.core.security.SecurityThreatDetector;
 import com.zsElectric.boot.core.web.Result;
 import com.zsElectric.boot.core.web.ResultCode;
+import com.zsElectric.boot.security.util.SecurityUtils;
+import com.zsElectric.boot.system.model.entity.SecurityEventLog;
+import com.zsElectric.boot.system.service.SecurityEventLogService;
 import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.ConstraintViolationException;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
 import org.springframework.beans.TypeMismatchException;
 import org.springframework.context.support.DefaultMessageSourceResolvable;
 import org.springframework.http.HttpStatus;
@@ -16,6 +25,7 @@ import org.springframework.jdbc.BadSqlGrammarException;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -23,10 +33,16 @@ import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
 import org.springframework.web.servlet.NoHandlerFoundException;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.sql.SQLIntegrityConstraintViolationException;
 import java.sql.SQLSyntaxErrorException;
+import java.util.Arrays;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -38,8 +54,15 @@ import java.util.stream.Collectors;
  */
 @RestControllerAdvice
 @Slf4j
+@RequiredArgsConstructor
 public class GlobalExceptionHandler {
 
+    private static final int SHORT_TEXT_LIMIT = 128;
+    private static final int MIDDLE_TEXT_LIMIT = 512;
+    private static final int LONG_TEXT_LIMIT = 2048;
+
+    private final SecurityEventLogService securityEventLogService;
+
     /**
      * 处理绑定异常
      * <p>
@@ -50,20 +73,10 @@ public class GlobalExceptionHandler {
     public <T> Result<T> processException(BindException e) {
         log.error("BindException:{}", e.getMessage());
         String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
+        recordBindExceptionSecurityEvent(e);
         return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg);
     }
 
-    /**
-     * 处理恶意HTTP请求异常(XSS攻击、SQL注入等)
-     * 当检测到恶意请求时,会抛出 BadHttpRequestException 异常。
-     */
-    @ExceptionHandler(BadHttpRequestException.class)
-    @ResponseStatus(HttpStatus.BAD_REQUEST)
-    public <T> Result<T> handleBadHttpRequestException(BadHttpRequestException e) {
-        log.error("检测到恶意请求,异常原因:{}", e.getMessage(), e);
-        return Result.failed(ResultCode.USER_INPUT_CONTENT_ILLEGAL, ResultCode.USER_INPUT_CONTENT_ILLEGAL.getMsg());
-    }
-
     /**
      * 处理 @RequestParam 参数校验异常
      * <p>
@@ -141,8 +154,20 @@ public class GlobalExceptionHandler {
     @ExceptionHandler(MethodArgumentTypeMismatchException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
-        log.error(e.getMessage(), e);
-        return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误");
+        String fieldName = e.getName();
+        String errorMessage = StrUtil.isNotBlank(fieldName) ? "%s字段类型错误".formatted(fieldName) : "类型错误";
+        String payloadExcerpt = "%s=%s".formatted(fieldName, e.getValue());
+        boolean malicious = SecurityThreatDetector.containsMaliciousFeature(payloadExcerpt);
+        recordValidationSecurityEvent("INVALID_PARAMETER", "spring-method-argument-type",
+                "请求参数类型错误", fieldName, payloadExcerpt, HttpStatus.BAD_REQUEST.value(),
+                malicious ? "REJECT" : "OBSERVE",
+                malicious ? "rejected_by_malicious_parameter" : "rejected_by_validation");
+        log.warn("请求参数类型错误: field={}, value={}, requiredType={}",
+                fieldName, e.getValue(), e.getRequiredType() == null ? null : e.getRequiredType().getName());
+        if (malicious) {
+            return Result.failed(ResultCode.USER_INPUT_CONTENT_ILLEGAL, ResultCode.USER_INPUT_CONTENT_ILLEGAL.getMsg());
+        }
+        return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, errorMessage);
     }
 
     /**
@@ -189,13 +214,17 @@ public class GlobalExceptionHandler {
     @ExceptionHandler(HttpMessageNotReadableException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public <T> Result<T> processException(HttpMessageNotReadableException e) {
-        log.error(e.getMessage(), e);
-        String errorMessage = "请求体不可为空";
+        String errorMessage = "请求体不可为空或格式错误";
         Throwable cause = e.getCause();
         if (cause != null) {
             errorMessage = convertMessage(cause);
         }
-        return Result.failed(errorMessage);
+        recordHttpMessageNotReadableEvent(e, errorMessage);
+        log.warn("请求体解析失败: {}", errorMessage);
+        if (isMaliciousHttpMessage(e)) {
+            return Result.failed(ResultCode.USER_INPUT_CONTENT_ILLEGAL, ResultCode.USER_INPUT_CONTENT_ILLEGAL.getMsg());
+        }
+        return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, errorMessage);
     }
 
     /**
@@ -291,6 +320,10 @@ public class GlobalExceptionHandler {
      * @return 错误信息
      */
     private String convertMessage(Throwable throwable) {
+        String fieldName = extractJsonFieldName(throwable);
+        if (StrUtil.isNotBlank(fieldName)) {
+            return "%s字段类型错误".formatted(fieldName);
+        }
         String error = throwable.toString();
         String regulation = "\\[\"(.*?)\"]+";
         Pattern pattern = Pattern.compile(regulation);
@@ -302,6 +335,193 @@ public class GlobalExceptionHandler {
             matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", ""));
             group += matchString;
         }
-        return group;
+        return StrUtil.isNotBlank(group) ? group : "请求参数格式错误";
+    }
+
+    private void recordHttpMessageNotReadableEvent(HttpMessageNotReadableException e, String errorMessage) {
+        Throwable cause = e.getCause();
+        String payloadExcerpt = getHttpMessagePayloadExcerpt(e);
+        String matchedField = extractJsonFieldName(cause);
+        String defaultEventType = cause instanceof JsonMappingException ? "INVALID_PARAMETER" : "INVALID_JSON";
+        boolean malicious = SecurityThreatDetector.containsMaliciousFeature(payloadExcerpt);
+        recordValidationSecurityEvent(defaultEventType, "spring-http-message-converter",
+                errorMessage, matchedField, payloadExcerpt, HttpStatus.BAD_REQUEST.value(),
+                malicious ? "REJECT" : "OBSERVE",
+                malicious ? "rejected_by_malicious_payload" : "rejected_by_validation");
+    }
+
+    private boolean isMaliciousHttpMessage(HttpMessageNotReadableException e) {
+        return SecurityThreatDetector.containsMaliciousFeature(getHttpMessagePayloadExcerpt(e));
+    }
+
+    private String getHttpMessagePayloadExcerpt(HttpMessageNotReadableException e) {
+        Throwable cause = e.getCause();
+        return cause == null ? e.getMessage() : cause.getMessage();
+    }
+
+    private void recordBindExceptionSecurityEvent(BindException e) {
+        for (FieldError fieldError : e.getFieldErrors()) {
+            String fieldName = fieldError.getField();
+            String rejectedValue = fieldError.getRejectedValue() == null ? null : String.valueOf(fieldError.getRejectedValue());
+            String payloadExcerpt = "%s=%s".formatted(fieldName, rejectedValue);
+            boolean whitelistError = isFieldWhitelistError(fieldError);
+            boolean suspiciousValue = SecurityThreatDetector.containsMaliciousFeature(payloadExcerpt);
+            if (!whitelistError && !suspiciousValue) {
+                continue;
+            }
+
+            recordValidationSecurityEvent("INVALID_PARAMETER",
+                    whitelistError ? "field-whitelist" : "spring-bind",
+                    whitelistError ? "请求参数字段不在白名单" : "请求参数绑定异常包含风险内容",
+                    fieldName,
+                    payloadExcerpt,
+                    HttpStatus.BAD_REQUEST.value(),
+                    "REJECT",
+                    whitelistError ? "rejected_by_field_whitelist" : "rejected_by_validation");
+        }
+    }
+
+    private boolean isFieldWhitelistError(FieldError fieldError) {
+        String[] codes = fieldError.getCodes();
+        return (codes != null && Arrays.stream(codes).anyMatch(code -> StrUtil.equals(code, "ValidField")
+                || StrUtil.startWith(code, "ValidField.")))
+                || StrUtil.equals(fieldError.getDefaultMessage(), "非法字段");
+    }
+
+    private void recordValidationSecurityEvent(String defaultEventType, String ruleId, String eventDesc,
+                                               String matchedField, String payloadExcerpt, int httpStatus) {
+        recordValidationSecurityEvent(defaultEventType, ruleId, eventDesc, matchedField, payloadExcerpt,
+                httpStatus, "OBSERVE", "rejected_by_validation");
+    }
+
+    private void recordValidationSecurityEvent(String defaultEventType, String ruleId, String eventDesc,
+                                               String matchedField, String payloadExcerpt, int httpStatus,
+                                               String action, String handleResult) {
+        try {
+            HttpServletRequest request = getCurrentRequest();
+            String excerpt = truncate(payloadExcerpt, LONG_TEXT_LIMIT);
+            SecurityEventLog eventLog = new SecurityEventLog();
+            eventLog.setEventType(resolveEventType(defaultEventType, excerpt));
+            eventLog.setRiskLevel(resolveRiskLevel(excerpt));
+            eventLog.setDetector("APP_VALIDATION");
+            eventLog.setRuleId(ruleId);
+            eventLog.setEventDesc(truncate(eventDesc, 255));
+            eventLog.setMatchedField(truncate(matchedField, SHORT_TEXT_LIMIT));
+            eventLog.setPayloadExcerpt(excerpt);
+            eventLog.setPayloadHash(sha256(excerpt));
+            eventLog.setAction(action);
+            eventLog.setHttpStatus(httpStatus);
+            eventLog.setHandleResult(handleResult);
+            eventLog.setIsDeleted(0);
+
+            if (request != null) {
+                eventLog.setRequestUri(truncate(request.getRequestURI(), MIDDLE_TEXT_LIMIT));
+                eventLog.setRequestMethod(truncate(request.getMethod(), 16));
+                eventLog.setQueryString(truncate(request.getQueryString(), LONG_TEXT_LIMIT));
+                eventLog.setClientIp(truncate(IPUtils.getIpAddr(request), 45));
+                eventLog.setXForwardedFor(truncate(request.getHeader("X-Forwarded-For"), MIDDLE_TEXT_LIMIT));
+                eventLog.setUserAgent(truncate(request.getHeader("User-Agent"), MIDDLE_TEXT_LIMIT));
+                eventLog.setReferer(truncate(request.getHeader("Referer"), MIDDLE_TEXT_LIMIT));
+                eventLog.setOperatorId(truncate(resolveOperatorId(request), 64));
+                eventLog.setRequestId(truncate(resolveRequestId(request), SHORT_TEXT_LIMIT));
+            }
+
+            eventLog.setUserId(getCurrentUserId());
+            securityEventLogService.record(eventLog);
+        } catch (Exception ex) {
+            log.warn("安全事件日志构造失败: {}", ex.getMessage(), ex);
+        }
+    }
+
+    private HttpServletRequest getCurrentRequest() {
+        if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) {
+            return attributes.getRequest();
+        }
+        return null;
+    }
+
+    private Long getCurrentUserId() {
+        try {
+            return SecurityUtils.getUserId();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private String extractJsonFieldName(Throwable throwable) {
+        if (throwable instanceof JsonMappingException jsonMappingException) {
+            return jsonMappingException.getPath().stream()
+                    .map(JsonMappingException.Reference::getFieldName)
+                    .filter(StrUtil::isNotBlank)
+                    .collect(Collectors.joining("."));
+        }
+        return null;
+    }
+
+    private String resolveEventType(String defaultEventType, String content) {
+        return SecurityThreatDetector.resolveEventType(defaultEventType, content);
+    }
+
+    private String resolveRiskLevel(String content) {
+        return SecurityThreatDetector.resolveRiskLevel(content);
+    }
+
+    private String resolveRequestId(HttpServletRequest request) {
+        return firstNotBlank(
+                request.getHeader("X-Request-Id"),
+                request.getHeader("X-Request-ID"),
+                request.getHeader("Request-ID"),
+                request.getHeader("Trace-Id"),
+                request.getHeader("traceId"),
+                MDC.get("traceId"),
+                MDC.get("requestId")
+        );
+    }
+
+    private String resolveOperatorId(HttpServletRequest request) {
+        return firstNotBlank(
+                request.getHeader("OperatorID"),
+                request.getHeader("OperatorId"),
+                request.getHeader("operatorID"),
+                request.getHeader("operatorId"),
+                request.getHeader("Operator-Id")
+        );
+    }
+
+    private String firstNotBlank(String... values) {
+        if (values == null) {
+            return null;
+        }
+        for (String value : values) {
+            if (StrUtil.isNotBlank(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private String truncate(String value, int maxLength) {
+        if (value == null || value.length() <= maxLength) {
+            return value;
+        }
+        return value.substring(0, maxLength);
+    }
+
+    private String sha256(String value) {
+        if (StrUtil.isBlank(value)) {
+            return null;
+        }
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
+            StringBuilder builder = new StringBuilder(bytes.length * 2);
+            for (byte b : bytes) {
+                builder.append(String.format("%02x", b));
+            }
+            return builder.toString();
+        } catch (NoSuchAlgorithmException e) {
+            log.warn("SHA-256算法不可用: {}", e.getMessage());
+            return null;
+        }
     }
-}
+}

+ 0 - 261
src/main/java/com/zsElectric/boot/core/filter/XssAndSqlInjectionFilter.java

@@ -1,261 +0,0 @@
-package com.zsElectric.boot.core.filter;
-
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.StrUtil;
-import com.zsElectric.boot.common.util.SecurityUtils;
-import com.zsElectric.boot.config.property.SecurityFilterProperties;
-import com.zsElectric.boot.core.exception.BadHttpRequestException;
-import jakarta.servlet.*;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletRequestWrapper;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-
-/**
- * XSS 和 SQL 注入防护过滤器
- * <p>
- * 对所有 HTTP 请求进行安全检查,包括:
- * <ul>
- *     <li>请求参数(Query String 和 Form Data)</li>
- *     <li>请求体(JSON、XML 等)</li>
- *     <li>请求头</li>
- * </ul>
- *
- * @author zsElectric
- */
-@Slf4j
-public class XssAndSqlInjectionFilter implements Filter {
-
-    private final SecurityFilterProperties properties;
-
-    /**
-     * 默认排除的 URL 路径(不进行安全检查)
-     */
-    private static final Set<String> DEFAULT_EXCLUDE_URLS = new HashSet<>(Arrays.asList(
-            "/api/v1/auth/captcha",
-            "/doc.html",
-            "/swagger-ui",
-            "/v3/api-docs",
-            "/webjars",
-            "/charge-business/v1/linkData"  // 第三方充电平台接口(加密Base64数据会误判)
-    ));
-
-    /**
-     * 默认需要检查的请求头
-     */
-    private static final Set<String> DEFAULT_CHECK_HEADERS = new HashSet<>(Arrays.asList(
-//            "Referer",
-            "X-Forwarded-For"
-    ));
-
-    public XssAndSqlInjectionFilter(SecurityFilterProperties properties) {
-        this.properties = properties;
-    }
-
-    @Override
-    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
-            throws IOException, ServletException {
-
-        HttpServletRequest httpRequest = (HttpServletRequest) request;
-
-        // 检查是否为排除的 URL
-        if (isExcludedUrl(httpRequest)) {
-            chain.doFilter(request, response);
-            return;
-        }
-
-        try {
-            // 包装请求,进行安全检查
-            SecurityRequestWrapper wrappedRequest = new SecurityRequestWrapper(httpRequest);
-            chain.doFilter(wrappedRequest, response);
-        } catch (BadHttpRequestException e) {
-            log.error("检测到恶意请求,URL: {}, 错误信息: {}", httpRequest.getRequestURI(), e.getMessage());
-            throw e;
-        }
-    }
-
-    /**
-     * 检查 URL 是否在排除列表中
-     */
-    private boolean isExcludedUrl(HttpServletRequest request) {
-        String uri = request.getRequestURI();
-        
-        // 检查默认排除列表
-        if (DEFAULT_EXCLUDE_URLS.stream().anyMatch(uri::startsWith)) {
-            return true;
-        }
-        
-        // 检查配置的排除列表
-        if (properties.getExcludeUrls() != null) {
-            return properties.getExcludeUrls().stream().anyMatch(uri::startsWith);
-        }
-        
-        return false;
-    }
-
-    /**
-     * 安全请求包装类
-     */
-    private class SecurityRequestWrapper extends HttpServletRequestWrapper {
-
-        private byte[] body;
-
-        public SecurityRequestWrapper(HttpServletRequest request) throws IOException {
-            super(request);
-            // 缓存请求体
-            if (isJsonOrXmlRequest(request)) {
-                body = IoUtil.readBytes(request.getInputStream());
-                // 检查请求体
-                String bodyContent = new String(body, StandardCharsets.UTF_8);
-                if (StrUtil.isNotBlank(bodyContent)) {
-                    checkContent(bodyContent, "请求体");
-                }
-            }
-            // 检查请求头
-            checkHeaders();
-        }
-
-        @Override
-        public String getParameter(String name) {
-            String value = super.getParameter(name);
-            if (value != null) {
-                checkContent(value, "请求参数[" + name + "]");
-            }
-            return value;
-        }
-
-        @Override
-        public String[] getParameterValues(String name) {
-            String[] values = super.getParameterValues(name);
-            if (values != null) {
-                for (int i = 0; i < values.length; i++) {
-                    if (values[i] != null) {
-                        checkContent(values[i], "请求参数[" + name + "][" + i + "]");
-                    }
-                }
-            }
-            return values;
-        }
-
-        @Override
-        public Map<String, String[]> getParameterMap() {
-            Map<String, String[]> parameterMap = super.getParameterMap();
-            if (parameterMap != null) {
-                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
-                    String[] values = entry.getValue();
-                    if (values != null) {
-                        for (int i = 0; i < values.length; i++) {
-                            if (values[i] != null) {
-                                checkContent(values[i], "请求参数[" + entry.getKey() + "][" + i + "]");
-                            }
-                        }
-                    }
-                }
-            }
-            return parameterMap;
-        }
-
-        @Override
-        public ServletInputStream getInputStream() throws IOException {
-            if (body == null) {
-                return super.getInputStream();
-            }
-
-            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
-
-            return new ServletInputStream() {
-                @Override
-                public boolean isFinished() {
-                    return byteArrayInputStream.available() == 0;
-                }
-
-                @Override
-                public boolean isReady() {
-                    return true;
-                }
-
-                @Override
-                public void setReadListener(ReadListener readListener) {
-                    // Not implemented
-                }
-
-                @Override
-                public int read() throws IOException {
-                    return byteArrayInputStream.read();
-                }
-            };
-        }
-
-        @Override
-        public BufferedReader getReader() throws IOException {
-            return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
-        }
-
-        /**
-         * 检查请求头
-         */
-        private void checkHeaders() {
-            Set<String> headersToCheck = new HashSet<>(DEFAULT_CHECK_HEADERS);
-            if (properties.getCheckHeaders() != null && !properties.getCheckHeaders().isEmpty()) {
-                headersToCheck.addAll(properties.getCheckHeaders());
-            }
-            
-            for (String headerName : headersToCheck) {
-                String headerValue = super.getHeader(headerName);
-                if (headerValue != null) {
-                    checkContent(headerValue, "请求头[" + headerName + "]");
-                }
-            }
-        }
-
-        /**
-         * 检查内容是否包含恶意代码
-         *
-         * @param content 待检查的内容
-         * @param location 内容来源位置(用于日志记录)
-         */
-        private void checkContent(String content, String location) {
-            // XSS 检测
-            if (properties.getXssEnabled() && SecurityUtils.containsXss(content)) {
-                throw new BadHttpRequestException("检测到 XSS 攻击尝试,位置: " + location);
-            }
-
-            // SQL 注入检测
-            if (properties.getSqlInjectionEnabled() && SecurityUtils.containsSqlInjection(content)) {
-                throw new BadHttpRequestException("检测到 SQL 注入尝试,位置: " + location);
-            }
-        }
-
-        /**
-         * 判断是否为 JSON 或 XML 请求
-         */
-        private boolean isJsonOrXmlRequest(HttpServletRequest request) {
-            String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE);
-            if (contentType == null) {
-                return false;
-            }
-            return contentType.contains(MediaType.APPLICATION_JSON_VALUE)
-                    || contentType.contains(MediaType.APPLICATION_XML_VALUE)
-                    || contentType.contains(MediaType.TEXT_XML_VALUE);
-        }
-    }
-
-    @Override
-    public void init(FilterConfig filterConfig) throws ServletException {
-        log.info("XSS 和 SQL 注入防护过滤器已启动");
-    }
-
-    @Override
-    public void destroy() {
-        log.info("XSS 和 SQL 注入防护过滤器已销毁");
-    }
-}

+ 13 - 5
src/main/java/com/zsElectric/boot/sdk/SdkExample.java

@@ -2,6 +2,7 @@ package com.zsElectric.boot.sdk;
 
 import com.zsElectric.boot.sdk.model.SdkResult;
 
+import java.math.BigDecimal;
 import java.util.Map;
 
 /**
@@ -13,7 +14,7 @@ public class SdkExample {
 
     public static void main(String[] args) {
         // 1. 创建SDK配置
-        // baseUrl只填写域名和端口,SDK内部会自动拼接 /third-party/v1
+        // baseUrl只填写域名和端口,SDK内部会自动拼接 /third_party/v1
         ZsElectricConfig config = ZsElectricConfig.builder()
                 .baseUrl("http://127.0.0.1:8989/third_party/v1")    // 服务端地址
                 .operatorId("12345qwer")               // 运营商ID
@@ -76,7 +77,7 @@ public class SdkExample {
 
         // 8. 查询充电终端详情
         System.out.println("\n========== 查询充电终端详情 ==========");
-        SdkResult<Map<String, Object>> deviceResult = client.queryChargeDeviceDetail(1L);
+        SdkResult<Map<String, Object>> deviceResult = client.queryChargeDeviceDetail("CONNECTOR_CODE_001");
         if (deviceResult.isSuccess()) {
             System.out.println("充电终端详情: " + deviceResult.getData());
         } else {
@@ -85,7 +86,7 @@ public class SdkExample {
 
         // 9. 充点券购买
         System.out.println("\n========== 充点券购买 ==========");
-        SdkResult<Map<String, Object>> payResult = client.chargeOrderPay("13800138000", 1L, "OUT_ORDER_001");
+        SdkResult<Map<String, Object>> payResult = client.chargeOrderPay(1L, "13800138000", 1L, "OUT_ORDER_001");
         if (payResult.isSuccess()) {
             System.out.println("购买成功: " + payResult.getData());
         } else {
@@ -94,7 +95,14 @@ public class SdkExample {
 
         // 10. 启动充电
         System.out.println("\n========== 启动充电 ==========");
-        SdkResult<Map<String, Object>> invokeResult = client.invokeCharge("13800138000", 1L, "CHARGE_ORDER_001");
+        SdkResult<Map<String, Object>> invokeResult = client.invokeCharge(
+                "EQUIPMENT_ID_001",
+                "STATION_ID_001",
+                "CONNECTOR_ID_001",
+                "CHARGE_ORDER_001",
+                "13800138000",
+                new BigDecimal("10.00")
+        );
         if (invokeResult.isSuccess()) {
             System.out.println("启动充电成功: " + invokeResult.getData());
         } else {
@@ -112,7 +120,7 @@ public class SdkExample {
 
         // 12. 查询充电订单列表
         System.out.println("\n========== 查询充电订单列表 ==========");
-        SdkResult<Map<String, Object>> orderListResult = client.queryChargeOrderList("13800138000", 1, 10);
+        SdkResult<Map<String, Object>> orderListResult = client.queryChargeOrderList(1L, null, 1, 10);
         if (orderListResult.isSuccess()) {
             System.out.println("订单列表: " + orderListResult.getData());
         } else {

+ 123 - 36
src/main/java/com/zsElectric/boot/sdk/ZsElectricClient.java

@@ -1,6 +1,5 @@
 package com.zsElectric.boot.sdk;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.zsElectric.boot.sdk.model.SdkRequest;
@@ -11,6 +10,7 @@ import com.zsElectric.boot.sdk.util.SdkCryptoUtil;
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.math.BigDecimal;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
@@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
  */
 public class ZsElectricClient {
 
-    private static final String API_PREFIX = "/third-party/v1";
+    private static final String API_PREFIX = "/third_party/v1";
     private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
 
     private final ZsElectricConfig config;
@@ -49,9 +49,10 @@ public class ZsElectricClient {
         
         // 智能处理baseUrl:如果已包含API路径前缀,则不再拼接
         String base = config.getBaseUrl();
-        if (base.contains("/third_party/") || base.contains("/third_party/")) {
-            // 统一路径分隔符为连字符格式
-            this.apiBaseUrl = base.replace("/third_party/", "/third_party/");
+        if (base.contains(API_PREFIX)) {
+            this.apiBaseUrl = base;
+        } else if (base.contains("/third-party/v1")) {
+            this.apiBaseUrl = base.replace("/third-party/v1", API_PREFIX);
         } else {
             this.apiBaseUrl = base + API_PREFIX;
         }
@@ -105,31 +106,48 @@ public class ZsElectricClient {
     /**
      * 根据手机号获取用户信息
      *
-     * @param mobile 手机号
+     * @param phone 手机号
      * @return 用户信息
      */
-    public SdkResult<Map<String, Object>> queryUserInfo(String mobile) {
+    public SdkResult<Map<String, Object>> queryUserInfo(String phone) {
         Map<String, Object> requestData = new HashMap<>();
-        requestData.put("mobile", mobile);
+        requestData.put("phone", phone);
         return executeRequest("/query_user_info", requestData, true);
     }
 
     /**
      * 充点券购买
      *
-     * @param mobile      用户手机号
-     * @param levelId     档位ID
-     * @param outOrderNo  外部订单号
+     * @param userId  用户ID
+     * @param phone   用户手机号
+     * @param levelId 档位ID
+     * @param orderNo 渠道订单号
      * @return 购买结果
      */
-    public SdkResult<Map<String, Object>> chargeOrderPay(String mobile, Long levelId, String outOrderNo) {
+    public SdkResult<Map<String, Object>> chargeOrderPay(Long userId, String phone, Long levelId, String orderNo) {
+        return chargeOrderPay(userId, phone, levelId, orderNo, null, null);
+    }
+
+    public SdkResult<Map<String, Object>> chargeOrderPay(Long userId, String phone, Long levelId, String orderNo, String payTime, BigDecimal totalMoney) {
         Map<String, Object> requestData = new HashMap<>();
-        requestData.put("mobile", mobile);
+        requestData.put("userId", userId);
+        requestData.put("phone", phone);
         requestData.put("levelId", levelId);
-        requestData.put("outOrderNo", outOrderNo);
+        requestData.put("orderNo", orderNo);
+        if (payTime != null && !payTime.isEmpty()) {
+            requestData.put("payTime", payTime);
+        }
+        if (totalMoney != null) {
+            requestData.put("totalMoney", totalMoney.toPlainString());
+        }
         return executeRequest("/charge_order_pay", requestData, true);
     }
 
+    @Deprecated
+    public SdkResult<Map<String, Object>> chargeOrderPay(String phone, Long levelId, String orderNo) {
+        return chargeOrderPay(null, phone, levelId, orderNo);
+    }
+
     /**
      * 获取充电站列表
      *
@@ -140,9 +158,16 @@ public class ZsElectricClient {
      * @return 充电站列表
      */
     public SdkResult<Map<String, Object>> queryChargeStationList(int pageNum, int pageSize, Double latitude, Double longitude) {
+        return queryChargeStationList(pageNum, pageSize, latitude, longitude, null);
+    }
+
+    public SdkResult<Map<String, Object>> queryChargeStationList(int pageNum, int pageSize, Double latitude, Double longitude, Integer sortType) {
         Map<String, Object> requestData = new HashMap<>();
         requestData.put("pageNum", pageNum);
         requestData.put("pageSize", pageSize);
+        if (sortType != null) {
+            requestData.put("sortType", sortType);
+        }
         if (latitude != null) {
             requestData.put("latitude", latitude);
         }
@@ -159,81 +184,143 @@ public class ZsElectricClient {
      * @return 充电站详情
      */
     public SdkResult<Map<String, Object>> queryChargeStationDetail(Long stationId) {
+        return queryChargeStationDetail(stationId, null, null);
+    }
+
+    public SdkResult<Map<String, Object>> queryChargeStationDetail(Long stationId, Double latitude, Double longitude) {
         Map<String, Object> requestData = new HashMap<>();
         requestData.put("stationId", stationId);
+        if (latitude != null) {
+            requestData.put("latitude", latitude);
+        }
+        if (longitude != null) {
+            requestData.put("longitude", longitude);
+        }
         return executeRequest("/query_charge_station_detail", requestData, true);
     }
 
     /**
      * 获取充电终端详情
      *
-     * @param deviceId 设备ID
+     * @param connectorCode 充电设备接口编码
      * @return 充电终端详情
      */
-    public SdkResult<Map<String, Object>> queryChargeDeviceDetail(Long deviceId) {
+    public SdkResult<Map<String, Object>> queryChargeDeviceDetail(String connectorCode) {
         Map<String, Object> requestData = new HashMap<>();
-        requestData.put("deviceId", deviceId);
+        requestData.put("connectorCode", connectorCode);
         return executeRequest("/query_charge_device_detail", requestData, true);
     }
 
+    @Deprecated
+    public SdkResult<Map<String, Object>> queryChargeDeviceDetail(Long connectorCode) {
+        return queryChargeDeviceDetail(connectorCode == null ? null : String.valueOf(connectorCode));
+    }
+
     /**
      * 启动充电
      *
-     * @param mobile      用户手机号
-     * @param connectorId 充电枪ID
-     * @param outOrderNo  外部订单号
+     * @param equipmentId      充电桩编号
+     * @param stationId        第三方充电站ID
+     * @param connectorId      充电设备接口编码
+     * @param channelOrderNo   渠道方订单编号
+     * @param channelUserPhone 渠道方用户手机号
+     * @param channelPreAmt    渠道方预支付金额
      * @return 启动结果
      */
-    public SdkResult<Map<String, Object>> invokeCharge(String mobile, Long connectorId, String outOrderNo) {
+    public SdkResult<Map<String, Object>> invokeCharge(String equipmentId, String stationId, String connectorId,
+                                                       String channelOrderNo, String channelUserPhone, BigDecimal channelPreAmt) {
         Map<String, Object> requestData = new HashMap<>();
-        requestData.put("mobile", mobile);
+        requestData.put("equipmentId", equipmentId);
+        requestData.put("stationId", stationId);
         requestData.put("connectorId", connectorId);
-        requestData.put("outOrderNo", outOrderNo);
+        requestData.put("channelOrderNo", channelOrderNo);
+        requestData.put("channelUserPhone", channelUserPhone);
+        requestData.put("channelPreAmt", channelPreAmt);
         return executeRequest("/invoke_charge", requestData, true);
     }
 
+    @Deprecated
+    public SdkResult<Map<String, Object>> invokeCharge(String channelUserPhone, Long connectorId, String channelOrderNo) {
+        return invokeCharge(null, null, connectorId == null ? null : String.valueOf(connectorId),
+                channelOrderNo, channelUserPhone, null);
+    }
+
     /**
      * 停止充电
      *
-     * @param orderNo 充电订单号
+     * @param chargeOrderNo 充电订单号
      * @return 停止结果
      */
-    public SdkResult<Map<String, Object>> stopCharge(String orderNo) {
+    public SdkResult<Map<String, Object>> stopCharge(String chargeOrderNo) {
         Map<String, Object> requestData = new HashMap<>();
-        requestData.put("orderNo", orderNo);
+        requestData.put("chargeOrderNo", chargeOrderNo);
         return executeRequest("/stop_charge", requestData, true);
     }
 
     /**
      * 查询充电订单列表
      *
-     * @param mobile    用户手机号 (可选)
-     * @param pageNum   页码
-     * @param pageSize  每页数量
+     * @param userId        用户ID (可选)
+     * @param chargeOrderNo 充电订单号 (可选)
+     * @param pageNo        页码
+     * @param pageSize      每页数量
      * @return 订单列表
      */
-    public SdkResult<Map<String, Object>> queryChargeOrderList(String mobile, int pageNum, int pageSize) {
+    public SdkResult<Map<String, Object>> queryChargeOrderList(Long userId, String chargeOrderNo, long pageNo, long pageSize) {
         Map<String, Object> requestData = new HashMap<>();
-        if (mobile != null && !mobile.isEmpty()) {
-            requestData.put("mobile", mobile);
+        requestData.put("operatorId", config.getOperatorId());
+        if (userId != null) {
+            requestData.put("userId", userId);
         }
-        requestData.put("pageNum", pageNum);
+        if (chargeOrderNo != null && !chargeOrderNo.isEmpty()) {
+            requestData.put("chargeOrderNo", chargeOrderNo);
+        }
+        requestData.put("pageNo", pageNo);
         requestData.put("pageSize", pageSize);
         return executeRequest("/query_charge_order_list", requestData, true);
     }
 
+    @Deprecated
+    public SdkResult<Map<String, Object>> queryChargeOrderList(String chargeOrderNo, int pageNo, int pageSize) {
+        return queryChargeOrderList(null, chargeOrderNo, pageNo, pageSize);
+    }
+
     /**
      * 查询充电订单详情
      *
-     * @param orderNo 充电订单号
+     * @param chargeOrderNo 充电订单号
      * @return 订单详情
      */
-    public SdkResult<Map<String, Object>> queryChargeOrderInfo(String orderNo) {
+    public SdkResult<Map<String, Object>> queryChargeOrderInfo(String chargeOrderNo) {
         Map<String, Object> requestData = new HashMap<>();
-        requestData.put("orderNo", orderNo);
+        requestData.put("chargeOrderNo", chargeOrderNo);
         return executeRequest("/query_charge_order_info", requestData, true);
     }
 
+    /**
+     * 查询充电订单实时费用
+     *
+     * @param chargeOrderNo 充电订单号
+     * @return 实时费用
+     */
+    public SdkResult<Map<String, Object>> queryChargingCost(String chargeOrderNo) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("chargeOrderNo", chargeOrderNo);
+        return executeRequest("/query_charging_cost", requestData, true);
+    }
+
+    /**
+     * 清除用户账户余额
+     *
+     * @param userId 用户ID
+     * @return 清除结果
+     */
+    public SdkResult<Map<String, Object>> clearAccountBalance(Long userId) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("userId", userId);
+        return executeRequest("/clear_user_balance", requestData, true);
+    }
+
     // ==================== 内部方法 ====================
 
     /**

+ 15 - 0
src/main/java/com/zsElectric/boot/system/mapper/SecurityEventLogMapper.java

@@ -0,0 +1,15 @@
+package com.zsElectric.boot.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zsElectric.boot.system.model.entity.SecurityEventLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 安全事件日志数据访问层。
+ *
+ * @author zsElectric
+ */
+@Mapper
+public interface SecurityEventLogMapper extends BaseMapper<SecurityEventLog> {
+}
+

+ 93 - 0
src/main/java/com/zsElectric/boot/system/model/entity/SecurityEventLog.java

@@ -0,0 +1,93 @@
+package com.zsElectric.boot.system.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.zsElectric.boot.common.base.BaseEntity;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 安全事件日志实体。
+ *
+ * <p>用于记录应用校验、网关、WAF、限流等检测源发现的安全风险事件。</p>
+ *
+ * @author zsElectric
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Schema(description = "安全事件日志")
+@TableName("security_event_log")
+public class SecurityEventLog extends BaseEntity {
+
+    @Schema(description = "安全事件类型,例如 SQL_INJECTION、XSS、INVALID_JSON、WAF_BLOCK")
+    private String eventType;
+
+    @Schema(description = "风险等级:LOW、MEDIUM、HIGH、CRITICAL")
+    private String riskLevel;
+
+    @Schema(description = "检测来源:APP_VALIDATION、APP_RULE、WAF、GATEWAY、RATE_LIMIT、MANUAL")
+    private String detector;
+
+    @Schema(description = "命中的检测规则编号或规则名称")
+    private String ruleId;
+
+    @Schema(description = "安全事件简要描述")
+    private String eventDesc;
+
+    @Schema(description = "请求路径")
+    private String requestUri;
+
+    @Schema(description = "HTTP请求方法")
+    private String requestMethod;
+
+    @Schema(description = "URL查询字符串摘要")
+    private String queryString;
+
+    @Schema(description = "客户端IP地址")
+    private String clientIp;
+
+    @Schema(description = "X-Forwarded-For请求头原始摘要")
+    private String xForwardedFor;
+
+    @Schema(description = "User-Agent请求头摘要")
+    private String userAgent;
+
+    @Schema(description = "Referer请求头摘要")
+    private String referer;
+
+    @Schema(description = "平台用户ID")
+    private Long userId;
+
+    @Schema(description = "第三方运营商ID或渠道方标识")
+    private String operatorId;
+
+    @Schema(description = "请求追踪ID")
+    private String requestId;
+
+    @Schema(description = "命中风险的字段名或参数名")
+    private String matchedField;
+
+    @Schema(description = "命中风险的请求内容摘要")
+    private String payloadExcerpt;
+
+    @Schema(description = "命中内容的SHA-256哈希值")
+    private String payloadHash;
+
+    @Schema(description = "处置动作:OBSERVE、REJECT、BLOCK、RATE_LIMIT、ALLOW")
+    private String action;
+
+    @Schema(description = "本次请求最终返回的HTTP状态码")
+    private Integer httpStatus;
+
+    @Schema(description = "处理结果说明")
+    private String handleResult;
+
+    @Schema(description = "备注信息")
+    private String remark;
+
+    @Schema(description = "逻辑删除标识:0-未删除,1-已删除")
+    @TableLogic
+    private Integer isDeleted;
+}
+

+ 8 - 0
src/main/java/com/zsElectric/boot/system/model/query/UserPageQuery.java

@@ -43,6 +43,14 @@ public class UserPageQuery extends BasePageQuery {
     @Schema(description = "排序方式(正序:ASC;反序:DESC)")
     private Direction direction;
 
+    @JsonIgnore
+    @Schema(hidden = true)
+    private String orderByColumn;
+
+    @JsonIgnore
+    @Schema(hidden = true)
+    private String orderByDirection;
+
     /**
      * 是否超级管理员
      */

+ 33 - 0
src/main/java/com/zsElectric/boot/system/service/SecurityEventLogService.java

@@ -0,0 +1,33 @@
+package com.zsElectric.boot.system.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zsElectric.boot.system.model.entity.SecurityEventLog;
+import com.zsElectric.boot.system.model.query.SecurityEventLogPageQuery;
+import com.zsElectric.boot.system.model.vo.SecurityEventLogPageVO;
+
+/**
+ * 安全事件日志服务。
+ *
+ * @author zsElectric
+ */
+public interface SecurityEventLogService extends IService<SecurityEventLog> {
+
+    /**
+     * 记录安全事件。
+     *
+     * <p>调用方负责判断事件是否需要记录;本方法只负责统一落库并保证记录失败不影响主业务。</p>
+     *
+     * @param eventLog 安全事件日志
+     * @return 是否记录成功
+     */
+    boolean record(SecurityEventLog eventLog);
+
+    /**
+     * Page security event logs.
+     *
+     * @param queryParams query params
+     * @return security event log page
+     */
+    Page<SecurityEventLogPageVO> getSecurityEventLogPage(SecurityEventLogPageQuery queryParams);
+}

+ 149 - 0
src/main/java/com/zsElectric/boot/system/service/impl/SecurityEventLogServiceImpl.java

@@ -0,0 +1,149 @@
+package com.zsElectric.boot.system.service.impl;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zsElectric.boot.system.mapper.SecurityEventLogMapper;
+import com.zsElectric.boot.system.model.entity.SecurityEventLog;
+import com.zsElectric.boot.system.model.query.SecurityEventLogPageQuery;
+import com.zsElectric.boot.system.model.vo.SecurityEventLogPageVO;
+import com.zsElectric.boot.system.service.SecurityEventLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 安全事件日志服务实现。
+ *
+ * @author zsElectric
+ */
+@Slf4j
+@Service
+public class SecurityEventLogServiceImpl extends ServiceImpl<SecurityEventLogMapper, SecurityEventLog>
+        implements SecurityEventLogService {
+
+    @Override
+    public boolean record(SecurityEventLog eventLog) {
+        if (eventLog == null) {
+            return false;
+        }
+        try {
+            return this.save(eventLog);
+        } catch (Exception e) {
+            log.warn("安全事件日志写入失败: eventType={}, detector={}, requestUri={}",
+                    eventLog.getEventType(), eventLog.getDetector(), eventLog.getRequestUri(), e);
+            return false;
+        }
+    }
+
+    @Override
+    public Page<SecurityEventLogPageVO> getSecurityEventLogPage(SecurityEventLogPageQuery queryParams) {
+        LambdaQueryWrapper<SecurityEventLog> queryWrapper = buildQueryWrapper(queryParams);
+        Page<SecurityEventLog> entityPage = this.page(
+                new Page<>(queryParams.getPageNum(), queryParams.getPageSize()),
+                queryWrapper
+        );
+
+        Page<SecurityEventLogPageVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
+        voPage.setRecords(entityPage.getRecords().stream().map(this::toPageVO).toList());
+        return voPage;
+    }
+
+    private LambdaQueryWrapper<SecurityEventLog> buildQueryWrapper(SecurityEventLogPageQuery queryParams) {
+        LambdaQueryWrapper<SecurityEventLog> queryWrapper = new LambdaQueryWrapper<>();
+
+        queryWrapper.eq(StrUtil.isNotBlank(queryParams.getEventType()), SecurityEventLog::getEventType, queryParams.getEventType())
+                .eq(StrUtil.isNotBlank(queryParams.getRiskLevel()), SecurityEventLog::getRiskLevel, queryParams.getRiskLevel())
+                .eq(StrUtil.isNotBlank(queryParams.getDetector()), SecurityEventLog::getDetector, queryParams.getDetector())
+                .eq(StrUtil.isNotBlank(queryParams.getAction()), SecurityEventLog::getAction, queryParams.getAction())
+                .eq(StrUtil.isNotBlank(queryParams.getClientIp()), SecurityEventLog::getClientIp, queryParams.getClientIp())
+                .like(StrUtil.isNotBlank(queryParams.getRequestUri()), SecurityEventLog::getRequestUri, queryParams.getRequestUri())
+                .eq(StrUtil.isNotBlank(queryParams.getMatchedField()), SecurityEventLog::getMatchedField, queryParams.getMatchedField())
+                .eq(StrUtil.isNotBlank(queryParams.getRequestId()), SecurityEventLog::getRequestId, queryParams.getRequestId())
+                .eq(StrUtil.isNotBlank(queryParams.getPayloadHash()), SecurityEventLog::getPayloadHash, queryParams.getPayloadHash())
+                .eq(queryParams.getHttpStatus() != null, SecurityEventLog::getHttpStatus, queryParams.getHttpStatus())
+                .eq(queryParams.getUserId() != null, SecurityEventLog::getUserId, queryParams.getUserId())
+                .eq(StrUtil.isNotBlank(queryParams.getOperatorId()), SecurityEventLog::getOperatorId, queryParams.getOperatorId());
+
+        if (StrUtil.isNotBlank(queryParams.getKeywords())) {
+            String keywords = queryParams.getKeywords();
+            queryWrapper.and(wrapper -> wrapper
+                    .like(SecurityEventLog::getEventDesc, keywords)
+                    .or().like(SecurityEventLog::getRequestUri, keywords)
+                    .or().like(SecurityEventLog::getClientIp, keywords)
+                    .or().like(SecurityEventLog::getUserAgent, keywords)
+                    .or().like(SecurityEventLog::getMatchedField, keywords)
+                    .or().like(SecurityEventLog::getRequestId, keywords)
+                    .or().like(SecurityEventLog::getOperatorId, keywords)
+                    .or().like(SecurityEventLog::getPayloadExcerpt, keywords)
+            );
+        }
+
+        fillCreateTimeCondition(queryWrapper, queryParams.getCreateTime());
+        queryWrapper.orderByDesc(SecurityEventLog::getCreateTime);
+        return queryWrapper;
+    }
+
+    private void fillCreateTimeCondition(LambdaQueryWrapper<SecurityEventLog> queryWrapper, List<String> createTime) {
+        if (CollectionUtil.isEmpty(createTime)) {
+            return;
+        }
+
+        String start = createTime.get(0);
+        if (StrUtil.isNotBlank(start)) {
+            queryWrapper.ge(SecurityEventLog::getCreateTime, normalizeStartTime(start));
+        }
+
+        if (createTime.size() < 2) {
+            return;
+        }
+
+        String end = createTime.get(1);
+        if (StrUtil.isNotBlank(end)) {
+            queryWrapper.le(SecurityEventLog::getCreateTime, normalizeEndTime(end));
+        }
+    }
+
+    private String normalizeStartTime(String value) {
+        return value.length() == 10 ? value + " 00:00:00" : value;
+    }
+
+    private String normalizeEndTime(String value) {
+        return value.length() == 10 ? value + " 23:59:59" : value;
+    }
+
+    private SecurityEventLogPageVO toPageVO(SecurityEventLog entity) {
+        SecurityEventLogPageVO vo = new SecurityEventLogPageVO();
+        vo.setId(entity.getId());
+        vo.setEventType(entity.getEventType());
+        vo.setRiskLevel(entity.getRiskLevel());
+        vo.setDetector(entity.getDetector());
+        vo.setRuleId(entity.getRuleId());
+        vo.setEventDesc(entity.getEventDesc());
+        vo.setRequestUri(entity.getRequestUri());
+        vo.setRequestMethod(entity.getRequestMethod());
+        vo.setQueryString(entity.getQueryString());
+        vo.setClientIp(entity.getClientIp());
+        vo.setXForwardedFor(entity.getXForwardedFor());
+        vo.setUserAgent(entity.getUserAgent());
+        vo.setReferer(entity.getReferer());
+        vo.setUserId(entity.getUserId());
+        vo.setOperatorId(entity.getOperatorId());
+        vo.setRequestId(entity.getRequestId());
+        vo.setMatchedField(entity.getMatchedField());
+        vo.setPayloadExcerpt(entity.getPayloadExcerpt());
+        vo.setPayloadHash(entity.getPayloadHash());
+        vo.setAction(entity.getAction());
+        vo.setHttpStatus(entity.getHttpStatus());
+        vo.setHandleResult(entity.getHandleResult());
+        vo.setRemark(entity.getRemark());
+        vo.setCreateBy(entity.getCreateBy());
+        vo.setCreateTime(entity.getCreateTime());
+        vo.setUpdateBy(entity.getUpdateBy());
+        vo.setUpdateTime(entity.getUpdateTime());
+        return vo;
+    }
+}

+ 23 - 0
src/main/java/com/zsElectric/boot/system/service/impl/UserServiceImpl.java

@@ -62,6 +62,11 @@ import java.util.stream.Collectors;
 @Slf4j
 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
 
+    private static final Map<String, String> USER_PAGE_SORT_FIELD_MAP = Map.of(
+            "create_time", "u.create_time",
+            "update_time", "u.update_time"
+    );
+
     private final PasswordEncoder passwordEncoder;
 
     private final UserRoleService userRoleService;
@@ -100,6 +105,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
 
         boolean isRoot = SecurityUtils.isRoot();
         queryParams.setIsRoot(isRoot);
+        fillSafeUserPageSort(queryParams);
 
         // 查询数据
         Page<UserBO> userPage = this.baseMapper.getUserPage(page, queryParams);
@@ -108,6 +114,23 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
         return userConverter.toPageVo(userPage);
     }
 
+    private void fillSafeUserPageSort(UserPageQuery queryParams) {
+        queryParams.setOrderByColumn(null);
+        queryParams.setOrderByDirection(null);
+
+        if (StrUtil.isBlank(queryParams.getField()) || queryParams.getDirection() == null) {
+            return;
+        }
+
+        String orderByColumn = USER_PAGE_SORT_FIELD_MAP.get(queryParams.getField());
+        if (StrUtil.isBlank(orderByColumn)) {
+            return;
+        }
+
+        queryParams.setOrderByColumn(orderByColumn);
+        queryParams.setOrderByDirection(queryParams.getDirection().name());
+    }
+
     /**
      * 获取用户表单数据
      *

+ 9 - 0
src/main/java/com/zsElectric/boot/thirdParty/controller/ThirdPartyController.java

@@ -165,4 +165,13 @@ public class ThirdPartyController {
             @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
         return thirdPartyTokenService.clearAccountBalance(request, authorization);
     }
+
+    @Operation(summary = "获取电站价格列表", description = "第三方获取电站价格列表,需要在Header中携带Authorization")
+    @PostMapping("/query_station_price_list")
+    @Log(value = "第三方获取电站价格列表", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryStationPriceList(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.queryStationPriceList(request, authorization);
+    }
 }

+ 6 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationListResponseData.java

@@ -61,6 +61,12 @@ public class ChargeStationListResponseData implements Serializable {
         @Schema(description = "距离(km)")
         private BigDecimal distance;
 
+        @Schema(description = "经度")
+        private BigDecimal longitude;
+
+        @Schema(description = "纬度")
+        private BigDecimal latitude;
+
         @Schema(description = "快充(格式:空闲/总数)")
         private String fastCharging;
 

+ 21 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/StationPriceListRequestData.java

@@ -0,0 +1,21 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 电站价格列表请求数据
+ *
+ * @author wzq
+ */
+@Data
+public class StationPriceListRequestData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 充电站ID
+     */
+    private Long stationId;
+}

+ 67 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/StationPriceListResponseData.java

@@ -0,0 +1,67 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 电站价格列表响应数据
+ *
+ * @author wzq
+ */
+@Data
+public class StationPriceListResponseData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "站点ID")
+    private Long stationId;
+
+    @Schema(description = "站点名称")
+    private String stationName;
+
+    @Schema(description = "提示语/标签(如:充电减免2小时停车费,超出部分按每小时3元计费)")
+    private String tips;
+
+    @Schema(description = "价格列表")
+    private List<PriceItem> priceList;
+
+    /**
+     * 时段价格信息
+     */
+    @Getter
+    @Setter
+    @Schema(description = "时段价格信息")
+    public static class PriceItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "时段(如:00:00-08:00)")
+        private String timePeriod;
+
+        @Schema(description = "时段标志(1-尖,2-峰,3-平,4-谷)")
+        private Integer periodFlag;
+
+        @Schema(description = "时段标志名称(尖/峰/平/谷)")
+        private String periodFlagName;
+
+        @Schema(description = "电费(元/度)")
+        private BigDecimal elecPrice;
+
+        @Schema(description = "服务费(元/度)")
+        private BigDecimal servicePrice;
+
+        @Schema(description = "合计充电价(元/度)")
+        private BigDecimal totalPrice;
+
+        @Schema(description = "是否当前时段")
+        private Boolean currentPeriod;
+    }
+}

+ 9 - 0
src/main/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenService.java

@@ -111,4 +111,13 @@ public interface ThirdPartyTokenService {
      * @return 响应结果
      */
     ThirdPartyResponse clearAccountBalance(ThirdPartyRequest request, String authorization);
+
+    /**
+     * 获取电站价格列表
+     *
+     * @param request       第三方请求参数
+     * @param authorization 请求头中的Authorization
+     * @return 响应结果
+     */
+    ThirdPartyResponse queryStationPriceList(ThirdPartyRequest request, String authorization);
 }

+ 205 - 93
src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java

@@ -16,6 +16,7 @@ import com.zsElectric.boot.business.model.vo.applet.AppletConnectorDetailVO;
 import com.zsElectric.boot.charging.mapper.ThirdPartyConnectorInfoMapper;
 import com.zsElectric.boot.business.mapper.UserAccountMapper;
 import com.zsElectric.boot.business.mapper.UserOrderInfoMapper;
+import com.zsElectric.boot.business.model.entity.ChargeOrderInfo;
 import com.zsElectric.boot.business.model.entity.RechargeLevel;
 import com.zsElectric.boot.business.model.entity.ThirdPartyInfo;
 import com.zsElectric.boot.business.model.entity.UserAccount;
@@ -50,6 +51,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
@@ -99,6 +101,12 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
      */
     private static final int DEFAULT_TOKEN_EXPIRE_SECONDS = 7200;
 
+    private static final long REQUEST_TIMESTAMP_WINDOW_SECONDS = 300;
+
+    private static final String REPLAY_KEY_PREFIX = "third_party_access:replay:";
+
+    private static final DateTimeFormatter REQUEST_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
     @Override
     public ThirdPartyResponse queryToken(ThirdPartyRequest request) {
         try {
@@ -120,6 +128,10 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
                 log.warn("签名验证失败, operatorId: {}", request.getOperatorId());
                 return buildErrorResponse(4001, "签名错误", thirdPartyInfo);
             }
+            ThirdPartyResponse replayValidationResponse = validateTimestampAndReplay(request, thirdPartyInfo);
+            if (replayValidationResponse != null) {
+                return replayValidationResponse;
+            }
 
             // 4. 解密并解析业务参数
             String decryptedData = AESCryptoUtils.decrypt(
@@ -187,13 +199,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -258,13 +266,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -364,13 +368,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -519,6 +519,57 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
         return operatorId.equals(storedOperatorId);
     }
 
+    private ThirdPartyResponse validateAccessTokenAndReplay(String accessToken, ThirdPartyRequest request, ThirdPartyInfo thirdPartyInfo) {
+        if (StrUtil.isBlank(accessToken)) {
+            log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
+            return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+        }
+        if (!validateAccessToken(accessToken, request.getOperatorId())) {
+            log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
+            return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+        }
+        return validateTimestampAndReplay(request, thirdPartyInfo);
+    }
+
+    private ThirdPartyResponse validateTimestampAndReplay(ThirdPartyRequest request, ThirdPartyInfo thirdPartyInfo) {
+        LocalDateTime requestTime;
+        try {
+            requestTime = LocalDateTime.parse(request.getTimeStamp(), REQUEST_TIMESTAMP_FORMATTER);
+        } catch (Exception e) {
+            log.warn("timeStamp格式错误, operatorId: {}, timeStamp: {}", request.getOperatorId(), request.getTimeStamp());
+            return buildErrorResponse(4003, "timeStamp格式错误", thirdPartyInfo);
+        }
+
+        long diffSeconds = Math.abs(Duration.between(requestTime, LocalDateTime.now()).getSeconds());
+        if (diffSeconds > REQUEST_TIMESTAMP_WINDOW_SECONDS) {
+            log.warn("timeStamp已过期, operatorId: {}, timeStamp: {}, diffSeconds: {}",
+                    request.getOperatorId(), request.getTimeStamp(), diffSeconds);
+            return buildErrorResponse(4003, "timeStamp已过期", thirdPartyInfo);
+        }
+
+        String replayKey = REPLAY_KEY_PREFIX + request.getOperatorId() + ":" + request.getTimeStamp() + ":" + request.getSeq();
+        Boolean acquired = redisTemplate.opsForValue()
+                .setIfAbsent(replayKey, "1", REQUEST_TIMESTAMP_WINDOW_SECONDS * 2, TimeUnit.SECONDS);
+        if (!Boolean.TRUE.equals(acquired)) {
+            log.warn("重复请求, operatorId: {}, timeStamp: {}, seq: {}",
+                    request.getOperatorId(), request.getTimeStamp(), request.getSeq());
+            return buildErrorResponse(4003, "重复请求", thirdPartyInfo);
+        }
+
+        return null;
+    }
+
+    private ChargeOrderInfo getCurrentOperatorChargeOrder(String chargeOrderNo, String operatorId) {
+        return chargeOrderInfoService.getOne(
+                Wrappers.<ChargeOrderInfo>lambdaQuery()
+                        .eq(ChargeOrderInfo::getChargeOrderNo, chargeOrderNo)
+                        .eq(ChargeOrderInfo::getOrderType, SystemConstants.CHARGE_ORDER_TYPE_CHANNEL)
+                        .eq(ChargeOrderInfo::getOperatorId, operatorId)
+                        .eq(ChargeOrderInfo::getIsDeleted, 0)
+                        .last("LIMIT 1")
+        );
+    }
+
     /**
      * 从数据库查询充值档位分页数据
      */
@@ -674,13 +725,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -755,13 +802,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -942,13 +985,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -1044,6 +1083,8 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
         item.setStationName(stationInfoVO.getStationName());
         item.setTips(stationInfoVO.getTips());
         item.setDistance(stationInfoVO.getDistance());
+        item.setLongitude(stationInfoVO.getLongitude());
+        item.setLatitude(stationInfoVO.getLatitude());
         item.setFastCharging(stationInfoVO.getFastCharging());
         item.setSlowCharging(stationInfoVO.getSlowCharging());
         item.setPeakValue(stationInfoVO.getPeakValue());
@@ -1111,13 +1152,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -1137,6 +1174,7 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             // 6. 构建启动充电表单
             AppInvokeChargeForm formData = new AppInvokeChargeForm();
             formData.setOrderType(2); // 渠道方订单
+            formData.setOperatorId(request.getOperatorId());
             formData.setEquipmentId(invokeRequest.getEquipmentId());
             formData.setStationId(invokeRequest.getStationId());
             formData.setConnectorId(invokeRequest.getConnectorId());
@@ -1187,13 +1225,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -1208,10 +1242,16 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             if (stopRequest == null || StrUtil.isBlank(stopRequest.getChargeOrderNo())) {
                 return buildErrorResponse(4003, "请求的业务参数不合法", thirdPartyInfo);
             }
+            ChargeOrderInfo chargeOrderInfo = getCurrentOperatorChargeOrder(stopRequest.getChargeOrderNo(), request.getOperatorId());
+            if (chargeOrderInfo == null) {
+                StopChargeResponseData failData = StopChargeResponseData.fail("充电订单不存在或不属于当前运营商");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
 
             // 6. 构建停止充电表单
             AppStopChargeForm formData = new AppStopChargeForm();
             formData.setChargeOrderNo(stopRequest.getChargeOrderNo());
+            formData.setOperatorId(request.getOperatorId());
 
             // 7. 调用停止充电服务
             AppChargeVO chargeVO = chargeOrderInfoService.stopCharge(formData);
@@ -1256,13 +1296,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -1281,7 +1317,7 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             // 6. 构建查询条件(只查询渠道类型订单 order_type=2)
             LambdaQueryWrapper<com.zsElectric.boot.business.model.entity.ChargeOrderInfo> queryWrapper = Wrappers.lambdaQuery();
             queryWrapper.eq(com.zsElectric.boot.business.model.entity.ChargeOrderInfo::getOrderType, SystemConstants.CHARGE_ORDER_TYPE_CHANNEL);
-            queryWrapper.eq(com.zsElectric.boot.business.model.entity.ChargeOrderInfo::getOperatorId, listRequest.getOperatorId());
+            queryWrapper.eq(com.zsElectric.boot.business.model.entity.ChargeOrderInfo::getOperatorId, request.getOperatorId());
             queryWrapper.eq(com.zsElectric.boot.business.model.entity.ChargeOrderInfo::getIsDeleted, 0);
 
             // 可选条件:userId
@@ -1394,13 +1430,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -1417,13 +1449,7 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             }
 
             // 6. 查询订单(只查询渠道类型订单 order_type=2)
-            com.zsElectric.boot.business.model.entity.ChargeOrderInfo order = chargeOrderInfoService.getOne(
-                    Wrappers.<com.zsElectric.boot.business.model.entity.ChargeOrderInfo>lambdaQuery()
-                            .eq(com.zsElectric.boot.business.model.entity.ChargeOrderInfo::getChargeOrderNo, detailRequest.getChargeOrderNo())
-                            .eq(com.zsElectric.boot.business.model.entity.ChargeOrderInfo::getOrderType, SystemConstants.CHARGE_ORDER_TYPE_CHANNEL)
-                            .eq(com.zsElectric.boot.business.model.entity.ChargeOrderInfo::getIsDeleted, 0)
-                            .last("LIMIT 1")
-            );
+            ChargeOrderInfo order = getCurrentOperatorChargeOrder(detailRequest.getChargeOrderNo(), request.getOperatorId());
 
             if (order == null) {
                 log.warn("充电订单不存在或不是渠道订单, chargeOrderNo: {}", detailRequest.getChargeOrderNo());
@@ -1504,13 +1530,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -1525,6 +1547,11 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             if (costRequest == null || StrUtil.isBlank(costRequest.getChargeOrderNo())) {
                 return buildErrorResponse(4003, "请求的业务参数不合法,chargeOrderNo不能为空", thirdPartyInfo);
             }
+            ChargeOrderInfo chargeOrderInfo = getCurrentOperatorChargeOrder(costRequest.getChargeOrderNo(), request.getOperatorId());
+            if (chargeOrderInfo == null) {
+                QueryChargingCostResponseData failData = QueryChargingCostResponseData.fail("充电订单不存在或不属于当前运营商");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
 
             // 6. 调用AppletHomeService查询充电订单实时费用
             AppChargingCostVO chargingCost = appletHomeService.getChargingCost(costRequest.getChargeOrderNo());
@@ -1588,13 +1615,9 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
 
             // 4. 从Header中解析并验证Token
             String accessToken = parseAccessToken(authorization);
-            if (StrUtil.isBlank(accessToken)) {
-                log.warn("Token为空或格式错误, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
-            }
-            if (!validateAccessToken(accessToken, request.getOperatorId())) {
-                log.warn("Token验证失败, operatorId: {}", request.getOperatorId());
-                return buildErrorResponse(4002, "token错误", thirdPartyInfo);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
             }
 
             // 5. 解密并解析业务参数
@@ -1662,6 +1685,95 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             return buildErrorResponse(500, "系统错误: " + e.getMessage(), null);
         }
     }
-}
 
+    @Override
+    public ThirdPartyResponse queryStationPriceList(ThirdPartyRequest request, String authorization) {
+        try {
+            // 1. 参数校验
+            if (!validateRequestParams(request)) {
+                return buildErrorResponse(4003, "参数不合法,缺少必需的参数", null);
+            }
+
+            // 2. 通过operatorId查询第三方配置信息
+            ThirdPartyInfo thirdPartyInfo = getThirdPartyInfoByOperatorId(request.getOperatorId());
+            if (thirdPartyInfo == null) {
+                log.warn("运营商不存在, operatorId: {}", request.getOperatorId());
+                return buildErrorResponse(4004, "运营商不存在", null);
+            }
+
+            // 3. 验证签名
+            String signContent = request.getOperatorId() + request.getData() + request.getTimeStamp() + request.getSeq();
+            if (!HmacMD5Util.verify(signContent, thirdPartyInfo.getSigSecret(), request.getSig())) {
+                log.warn("签名验证失败, operatorId: {}", request.getOperatorId());
+                return buildErrorResponse(4001, "签名错误", thirdPartyInfo);
+            }
+
+            // 4. 从Header中解析并验证Token
+            String accessToken = parseAccessToken(authorization);
+            ThirdPartyResponse accessTokenValidationResponse = validateAccessTokenAndReplay(accessToken, request, thirdPartyInfo);
+            if (accessTokenValidationResponse != null) {
+                return accessTokenValidationResponse;
+            }
 
+            // 5. 解密并解析业务参数
+            String decryptedData = AESCryptoUtils.decrypt(
+                    request.getData(),
+                    thirdPartyInfo.getDataSecret(),
+                    thirdPartyInfo.getDataSecretIV()
+            );
+            log.info("解密后的请求数据: {}", decryptedData);
+
+            StationPriceListRequestData priceRequest = objectMapper.readValue(decryptedData, StationPriceListRequestData.class);
+            if (priceRequest == null || priceRequest.getStationId() == null) {
+                return buildErrorResponse(4003, "请求的业务参数不合法", thirdPartyInfo);
+            }
+
+            // 6. 调用已有服务获取电站价格列表
+            com.zsElectric.boot.business.model.vo.AppletStationPriceListVO priceListVO = appletHomeService.getStationPriceList(priceRequest.getStationId());
+            if (priceListVO == null) {
+                return buildErrorResponse(4004, "充电站不存在", thirdPartyInfo);
+            }
+
+            // 7. 转换为第三方响应数据
+            StationPriceListResponseData responseData = convertToStationPriceList(priceListVO);
+
+            log.info("查询电站价格列表成功, operatorId: {}, stationId: {}", request.getOperatorId(), priceRequest.getStationId());
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_station_price_list请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    /**
+     * 转换电站价格列表为第三方响应数据
+     */
+    private StationPriceListResponseData convertToStationPriceList(com.zsElectric.boot.business.model.vo.AppletStationPriceListVO priceListVO) {
+        StationPriceListResponseData responseData = new StationPriceListResponseData();
+        responseData.setStationId(priceListVO.getStationId());
+        responseData.setStationName(priceListVO.getStationName());
+        responseData.setTips(priceListVO.getTips());
+
+        if (priceListVO.getPriceList() != null) {
+            List<StationPriceListResponseData.PriceItem> priceItems = priceListVO.getPriceList().stream()
+                    .map(item -> {
+                        StationPriceListResponseData.PriceItem priceItem = new StationPriceListResponseData.PriceItem();
+                        priceItem.setTimePeriod(item.getTimePeriod());
+                        priceItem.setPeriodFlag(item.getPeriodFlag());
+                        priceItem.setPeriodFlagName(item.getPeriodFlagName());
+                        priceItem.setElecPrice(item.getElecPrice());
+                        priceItem.setServicePrice(item.getServicePrice());
+                        priceItem.setTotalPrice(item.getTotalPrice());
+                        priceItem.setCurrentPeriod(item.getCurrentPeriod());
+                        return priceItem;
+                    })
+                    .collect(Collectors.toList());
+            responseData.setPriceList(priceItems);
+        } else {
+            responseData.setPriceList(new ArrayList<>());
+        }
+
+        return responseData;
+    }
+}

+ 3 - 27
src/main/resources/application-dev.yml

@@ -96,6 +96,8 @@ security:
       secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
     redis-token:
       allow-multi-login: true # 是否允许多设备登录
+  malicious-request-block:
+    enabled: true # 是否开启恶意请求拦截,灰度回退时可改为 false
   # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等)
   ignore-urls:
     - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
@@ -131,32 +133,6 @@ security:
     - /charge-business/v1/linkData/notification_charge_order_info
     - /charge-business/v1/linkData/notification_stationStatus
 
-  # XSS 和 SQL 注入防护过滤器配置
-  filter:
-    # 是否启用 XSS 防护
-    xss-enabled: false
-    # 是否启用 SQL 注入防护
-    sql-injection-enabled: false
-    # SQL 注入检测严格模式
-    # true: 严格模式,可能误判一些正常输入
-    # false: 宽松模式,减少误判但可能漏掉一些攻击
-    sql-strict-mode: false
-    # 排除的 URL 路径(不进行安全检查,注意:默认已经排除了 /doc.html、/swagger-ui 等接口文档相关路径)
-    exclude-urls:
-      - /api/v1/auth/captcha  # 验证码接口
-      - /charge-business/v1/linkData/  # 第三方充电平台接口(数据为加密的Base64,会误判)
-      - /charge-business/v1/linkData/notification_charge_order_info
-      - /charge-business/v1/linkData/notification_start_charge_result
-      - /charge-business/v1/linkData/notification_equip_charge_status
-      - /charge-business/v1/linkData/notification_stop_charge_result
-      - /charge-business/v1/linkData/notification_stationStatus
-      - /charge-business/v1/linkData/query_token
-      - /applet/v1/homePage/test  # 首页测试接口(包含加密数据)
-      - /third-party/v1/** # 第三方接入接口(数据为加密的Base64,会误判)
-    # 额外需要检查的请求头(默认已检查 User-Agent、Referer、X-Forwarded-For)
-    check-headers:
-#      - X-Forwarded-For
-
 okhttp:
   connect-timeout: 30s
   read-timeout: 120s
@@ -381,4 +357,4 @@ ai:
   # 限流配置
   rate-limit:
     max-executions-per-minute: 10
-    max-executions-per-day: 100
+    max-executions-per-day: 100

+ 2 - 27
src/main/resources/application-prod.yml

@@ -96,6 +96,8 @@ security:
       secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
     redis-token:
       allow-multi-login: true # 是否允许多设备登录
+  malicious-request-block:
+    enabled: true # 是否开启恶意请求拦截,灰度回退时可改为 false
   # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等)
   ignore-urls:
     - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
@@ -132,33 +134,6 @@ security:
     - /charge-business/v1/linkData/notification_charge_order_info
     - /charge-business/v1/linkData/notification_stationStatus
 
-  # XSS 和 SQL 注入防护过滤器配置
-  filter:
-    # 是否启用 XSS 防护
-    xss-enabled: false
-    # 是否启用 SQL 注入防护
-    sql-injection-enabled: false
-    # SQL 注入检测严格模式
-    # true: 严格模式,可能误判一些正常输入
-    # false: 宽松模式,减少误判但可能漏掉一些攻击
-    sql-strict-mode: false
-    # 排除的 URL 路径(不进行安全检查,注意:默认已经排除了 /doc.html、/swagger-ui 等接口文档相关路径)
-    exclude-urls:
-      - /api/v1/auth/captcha  # 验证码接口
-      - /charge-business/v1/linkData/  # 第三方充电平台接口(数据为加密的Base64,会误判)
-      - /charge-business/v1/linkData/notification_charge_order_info
-      - /charge-business/v1/linkData/notification_start_charge_result
-      - /charge-business/v1/linkData/notification_equip_charge_status
-      - /charge-business/v1/linkData/notification_stop_charge_result
-      - /charge-business/v1/linkData/notification_stationStatus
-      - /charge-business/v1/linkData/query_token
-      - /applet/v1/homePage/test  # 首页测试接口(包含加密数据)
-      - /third-party/v1/** # 第三方接入接口(数据为加密的Base64,会误判)
-    # 额外需要检查的请求头(默认已检查 User-Agent、Referer、X-Forwarded-For)
-    check-headers:
-#      - Referer
-#      - X-Forwarded-For
-
 okhttp:
   connect-timeout: 30s
   read-timeout: 120s

+ 3 - 27
src/main/resources/application-test.yml

@@ -96,6 +96,8 @@ security:
       secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
     redis-token:
       allow-multi-login: true # 是否允许多设备登录
+  malicious-request-block:
+    enabled: true # 是否开启恶意请求拦截,灰度回退时可改为 false
   # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等)
   ignore-urls:
     - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
@@ -131,32 +133,6 @@ security:
     - /charge-business/v1/linkData/notification_charge_order_info
     - /charge-business/v1/linkData/notification_stationStatus
 
-  # XSS 和 SQL 注入防护过滤器配置
-  filter:
-    # 是否启用 XSS 防护
-    xss-enabled: false
-    # 是否启用 SQL 注入防护
-    sql-injection-enabled: false
-    # SQL 注入检测严格模式
-    # true: 严格模式,可能误判一些正常输入
-    # false: 宽松模式,减少误判但可能漏掉一些攻击
-    sql-strict-mode: false
-    # 排除的 URL 路径(不进行安全检查,注意:默认已经排除了 /doc.html、/swagger-ui 等接口文档相关路径)
-    exclude-urls:
-      - /api/v1/auth/captcha  # 验证码接口
-      - /charge-business/v1/linkData/  # 第三方充电平台接口(数据为加密的Base64,会误判)
-      - /charge-business/v1/linkData/notification_charge_order_info
-      - /charge-business/v1/linkData/notification_start_charge_result
-      - /charge-business/v1/linkData/notification_equip_charge_status
-      - /charge-business/v1/linkData/notification_stop_charge_result
-      - /charge-business/v1/linkData/notification_stationStatus
-      - /charge-business/v1/linkData/query_token
-      - /applet/v1/homePage/test  # 首页测试接口(包含加密数据)
-      - /third-party/v1/** # 第三方接入接口(数据为加密的Base64,会误判)
-    # 额外需要检查的请求头(默认已检查 User-Agent、Referer、X-Forwarded-For)
-    check-headers:
-#      - X-Forwarded-For
-
 okhttp:
   connect-timeout: 30s
   read-timeout: 120s
@@ -361,4 +337,4 @@ ai:
   # 限流配置
   rate-limit:
     max-executions-per-minute: 10
-    max-executions-per-day: 100
+    max-executions-per-day: 100

+ 6 - 4
src/main/resources/mapper/business/ThirdPartyStationInfoMapper.xml

@@ -183,7 +183,7 @@
             </if>
             <!-- 快充统计(空闲/总数) -->
             CONCAT(
-                COUNT(tpci.id),
+                IFNULL(SUM(CASE WHEN tpci.status = 1 THEN 1 ELSE 0 END), 0),
                 '/',
                 COUNT(tpci.id)
             ) AS fast_charging,
@@ -301,7 +301,8 @@
             NULL AS enterprise_price
             </if>
         FROM third_party_station_info tpsi
-        LEFT JOIN third_party_connector_info tpci ON tpsi.station_id = tpci.station_id AND tpci.is_deleted = 0
+        LEFT JOIN third_party_equipment_info tpei ON tpsi.station_id = tpei.station_id AND tpei.is_deleted = 0
+        LEFT JOIN third_party_connector_info tpci ON tpei.equipment_id = tpci.equipment_id AND tpci.is_deleted = 0
         WHERE tpsi.is_deleted = 0
             <!-- 确保存在平台价配置 -->
             AND EXISTS (
@@ -363,7 +364,7 @@
             ) AS distance,
             <!-- 快充统计(空闲/总数) -->
             CONCAT(
-                COUNT(tpci.id),
+                IFNULL(SUM(CASE WHEN tpci.status = 1 THEN 1 ELSE 0 END), 0),
                 '/',
                 COUNT(tpci.id)
             ) AS fast_charging,
@@ -435,7 +436,8 @@
             <!-- 企业价(地图模式不需要) -->
             NULL AS enterprise_price
         FROM third_party_station_info tpsi
-        LEFT JOIN third_party_connector_info tpci ON tpsi.station_id = tpci.station_id AND tpci.is_deleted = 0
+        LEFT JOIN third_party_equipment_info tpei ON tpsi.station_id = tpei.station_id AND tpei.is_deleted = 0
+        LEFT JOIN third_party_connector_info tpci ON tpei.equipment_id = tpci.equipment_id AND tpci.is_deleted = 0
         WHERE tpsi.is_deleted = 0
             <!-- 确保存在平台价配置 -->
             AND EXISTS (

+ 11 - 2
src/main/resources/mapper/system/UserMapper.xml

@@ -71,8 +71,17 @@
             u.id
         <choose>
             <!-- 如果排序参数都传入 -->
-            <when test="queryParams.field != null and queryParams.field != '' and queryParams.direction != null">
-                ORDER BY u.${queryParams.field} ${queryParams.direction}
+            <when test="queryParams.orderByColumn == 'u.create_time' and queryParams.orderByDirection == 'ASC'">
+                ORDER BY u.create_time ASC
+            </when>
+            <when test="queryParams.orderByColumn == 'u.create_time' and queryParams.orderByDirection == 'DESC'">
+                ORDER BY u.create_time DESC
+            </when>
+            <when test="queryParams.orderByColumn == 'u.update_time' and queryParams.orderByDirection == 'ASC'">
+                ORDER BY u.update_time ASC
+            </when>
+            <when test="queryParams.orderByColumn == 'u.update_time' and queryParams.orderByDirection == 'DESC'">
+                ORDER BY u.update_time DESC
             </when>
             <!-- 默认排序 -->
             <otherwise>

+ 0 - 60
src/test/java/com/zsElectric/boot/common/util/SecurityUtilsTest.java

@@ -1,60 +0,0 @@
-package com.zsElectric.boot.common.util;
-
-import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * SecurityUtils 测试类
- */
-class SecurityUtilsTest {
-
-    @Test
-    void testContainsXss() {
-        // 测试明显的 XSS 攻击
-        assertTrue(SecurityUtils.containsXss("<script>alert('XSS')</script>"));
-        assertTrue(SecurityUtils.containsXss("javascript:alert('XSS')"));
-        assertTrue(SecurityUtils.containsXss("onerror=alert('XSS')"));
-        
-        // 测试正常输入不应该被误判
-        assertFalse(SecurityUtils.containsXss("username"));
-        assertFalse(SecurityUtils.containsXss("select-user"));
-        assertFalse(SecurityUtils.containsXss("user selection"));
-    }
-
-    @Test
-    void testContainsSqlInjection() {
-        // 测试宽松模式下的 SQL 注入检测
-        SecurityUtils.setSqlStrictMode(false);
-        
-        // 明显的 SQL 注入应该被检测到
-        assertTrue(SecurityUtils.containsSqlInjection("select * from users"));
-        assertTrue(SecurityUtils.containsSqlInjection("insert into users"));
-        assertTrue(SecurityUtils.containsSqlInjection("delete from users"));
-        assertTrue(SecurityUtils.containsSqlInjection("union select"));
-        
-        // 包含 SQL 关键词但不是攻击的正常输入不应该被误判
-        assertFalse(SecurityUtils.containsSqlInjection("username")); // 包含 "select" 的用户名
-        assertFalse(SecurityUtils.containsSqlInjection("user selection")); // 包含 "select" 的正常文本
-        assertFalse(SecurityUtils.containsSqlInjection("inserted value")); // 包含 "insert" 的正常文本
-        assertFalse(SecurityUtils.containsSqlInjection("deleted item")); // 包含 "delete" 的正常文本
-        
-        // 测试严格模式下的 SQL 注入检测
-        SecurityUtils.setSqlStrictMode(true);
-        
-        // 在严格模式下,即使是正常输入也可能被误判
-        // 但我们主要关注的是默认的宽松模式能正常工作
-        SecurityUtils.setSqlStrictMode(false);
-    }
-
-    @Test
-    void testIsSafeInput() {
-        // 安全的输入
-        assertTrue(SecurityUtils.isSafeInput("normal_username"));
-        assertTrue(SecurityUtils.isSafeInput("user123"));
-        assertTrue(SecurityUtils.isSafeInput("test@example.com"));
-        
-        // 不安全的输入
-        assertFalse(SecurityUtils.isSafeInput("<script>alert('XSS')</script>"));
-        assertFalse(SecurityUtils.isSafeInput("select * from users"));
-    }
-}

+ 0 - 420
src/test/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenServiceTest.java

@@ -1,420 +0,0 @@
-package com.zsElectric.boot.thirdParty.service;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.zsElectric.boot.business.mapper.ThirdPartyInfoMapper;
-import com.zsElectric.boot.business.model.entity.ThirdPartyInfo;
-import com.zsElectric.boot.common.util.AESCryptoUtils;
-import com.zsElectric.boot.common.util.HmacMD5Util;
-import com.zsElectric.boot.thirdParty.model.*;
-import com.zsElectric.boot.thirdParty.service.impl.ThirdPartyTokenServiceImpl;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.ValueOperations;
-
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
-
-/**
- * ThirdPartyTokenService 测试类
- * 测试 query_token 接口的各种场景
- *
- * @author wzq
- */
-@ExtendWith(MockitoExtension.class)
-@DisplayName("第三方Token服务测试")
-class ThirdPartyTokenServiceTest {
-
-    @Mock
-    private ThirdPartyInfoMapper thirdPartyInfoMapper;
-
-    @Mock
-    private RedisTemplate<String, Object> redisTemplate;
-
-    @Mock
-    private ValueOperations<String, Object> valueOperations;
-
-    @InjectMocks
-    private ThirdPartyTokenServiceImpl thirdPartyTokenService;
-
-    private final ObjectMapper objectMapper = new ObjectMapper();
-
-    // 测试用的固定配置(16位字符)
-    private static final String TEST_OPERATOR_ID = "12345qwer";
-    private static final String TEST_OPERATOR_SECRET = "vfkh4k740lfg88kq";
-    private static final String TEST_DATA_SECRET = "bbkwy062pzyjhqmg";  // 16位
-    private static final String TEST_DATA_SECRET_IV = "xgbzfgwz6ki2gm5j";  // 16位
-    private static final String TEST_SIG_SECRET = "8h9sf4zd5cbtlu8x";
-
-    private ThirdPartyInfo mockThirdPartyInfo;
-
-    @BeforeEach
-    void setUp() {
-        // 初始化模拟的第三方配置信息
-        mockThirdPartyInfo = new ThirdPartyInfo();
-        mockThirdPartyInfo.setId(1L);
-        mockThirdPartyInfo.setOperatorId(TEST_OPERATOR_ID);
-        mockThirdPartyInfo.setOperatorSecret(TEST_OPERATOR_SECRET);
-        mockThirdPartyInfo.setDataSecret(TEST_DATA_SECRET);
-        mockThirdPartyInfo.setDataSecretIV(TEST_DATA_SECRET_IV);
-        mockThirdPartyInfo.setSigSecret(TEST_SIG_SECRET);
-        mockThirdPartyInfo.setStatus(0);
-    }
-
-    // ==================== 参数校验测试 ====================
-
-    @Test
-    @DisplayName("请求参数为null应返回4003错误")
-    void testQueryToken_NullRequest() {
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(null);
-
-        assertEquals(4003, response.getRet());
-        assertEquals("参数不合法,缺少必需的参数", response.getMsg());
-    }
-
-    @Test
-    @DisplayName("缺少operatorId应返回4003错误")
-    void testQueryToken_MissingOperatorId() {
-        ThirdPartyRequest request = new ThirdPartyRequest();
-        request.setData("testData");
-        request.setTimeStamp("20260312120000");
-        request.setSeq("0001");
-        request.setSig("testSig");
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4003, response.getRet());
-        assertEquals("参数不合法,缺少必需的参数", response.getMsg());
-    }
-
-    @Test
-    @DisplayName("缺少data应返回4003错误")
-    void testQueryToken_MissingData() {
-        ThirdPartyRequest request = new ThirdPartyRequest();
-        request.setOperatorId(TEST_OPERATOR_ID);
-        request.setTimeStamp("20260312120000");
-        request.setSeq("0001");
-        request.setSig("testSig");
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4003, response.getRet());
-        assertEquals("参数不合法,缺少必需的参数", response.getMsg());
-    }
-
-    @Test
-    @DisplayName("缺少timeStamp应返回4003错误")
-    void testQueryToken_MissingTimeStamp() {
-        ThirdPartyRequest request = new ThirdPartyRequest();
-        request.setOperatorId(TEST_OPERATOR_ID);
-        request.setData("testData");
-        request.setSeq("0001");
-        request.setSig("testSig");
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4003, response.getRet());
-        assertEquals("参数不合法,缺少必需的参数", response.getMsg());
-    }
-
-    @Test
-    @DisplayName("缺少seq应返回4003错误")
-    void testQueryToken_MissingSeq() {
-        ThirdPartyRequest request = new ThirdPartyRequest();
-        request.setOperatorId(TEST_OPERATOR_ID);
-        request.setData("testData");
-        request.setTimeStamp("20260312120000");
-        request.setSig("testSig");
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4003, response.getRet());
-        assertEquals("参数不合法,缺少必需的参数", response.getMsg());
-    }
-
-    @Test
-    @DisplayName("缺少sig应返回4003错误")
-    void testQueryToken_MissingSig() {
-        ThirdPartyRequest request = new ThirdPartyRequest();
-        request.setOperatorId(TEST_OPERATOR_ID);
-        request.setData("testData");
-        request.setTimeStamp("20260312120000");
-        request.setSeq("0001");
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4003, response.getRet());
-        assertEquals("参数不合法,缺少必需的参数", response.getMsg());
-    }
-
-    // ==================== 运营商校验测试 ====================
-
-    @Test
-    @DisplayName("运营商不存在应返回4004错误")
-    void testQueryToken_OperatorNotFound() {
-        ThirdPartyRequest request = buildCompleteRequest("NOTEXIST1", TEST_OPERATOR_SECRET);
-
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4004, response.getRet());
-        assertEquals("运营商不存在", response.getMsg());
-    }
-
-    // ==================== 签名校验测试 ====================
-
-    @Test
-    @DisplayName("签名错误应返回4001错误")
-    void testQueryToken_InvalidSignature() throws Exception {
-        // 构建请求但使用错误的签名
-        QueryTokenRequestData tokenData = new QueryTokenRequestData();
-        tokenData.setOperatorId(TEST_OPERATOR_ID);
-        tokenData.setOperatorSecret(TEST_OPERATOR_SECRET);
-        String jsonData = objectMapper.writeValueAsString(tokenData);
-        String encryptedData = AESCryptoUtils.encrypt(jsonData, TEST_DATA_SECRET, TEST_DATA_SECRET_IV);
-
-        ThirdPartyRequest request = new ThirdPartyRequest();
-        request.setOperatorId(TEST_OPERATOR_ID);
-        request.setData(encryptedData);
-        request.setTimeStamp("20260312120000");
-        request.setSeq("0001");
-        request.setSig("WRONG_SIGNATURE_12345678901234567890");  // 错误的签名
-
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4001, response.getRet());
-        assertEquals("签名错误", response.getMsg());
-        assertNotNull(response.getSig());  // 应该有签名
-    }
-
-    // ==================== 业务参数校验测试 ====================
-
-    @Test
-    @DisplayName("业务参数中operatorId为空应返回4004错误")
-    void testQueryToken_EmptyOperatorIdInData() throws Exception {
-        // 构建请求,业务参数中operatorId为空
-        QueryTokenRequestData tokenData = new QueryTokenRequestData();
-        tokenData.setOperatorId("");  // 空的operatorId
-        tokenData.setOperatorSecret(TEST_OPERATOR_SECRET);
-
-        ThirdPartyRequest request = buildRequestWithTokenData(tokenData);
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4004, response.getRet());
-        assertEquals("请求的业务参数不合法", response.getMsg());
-    }
-
-    @Test
-    @DisplayName("业务参数中operatorSecret为空应返回4004错误")
-    void testQueryToken_EmptyOperatorSecretInData() throws Exception {
-        // 构建请求,业务参数中operatorSecret为空
-        QueryTokenRequestData tokenData = new QueryTokenRequestData();
-        tokenData.setOperatorId(TEST_OPERATOR_ID);
-        tokenData.setOperatorSecret("");  // 空的operatorSecret
-
-        ThirdPartyRequest request = buildRequestWithTokenData(tokenData);
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        assertEquals(4004, response.getRet());
-        assertEquals("请求的业务参数不合法", response.getMsg());
-    }
-
-    // ==================== 凭证验证测试 ====================
-
-    @Test
-    @DisplayName("operatorId不匹配应返回业务失败(resultStatus=1)")
-    void testQueryToken_OperatorIdMismatch() throws Exception {
-        // 构建请求,业务参数中operatorId不匹配
-        QueryTokenRequestData tokenData = new QueryTokenRequestData();
-        tokenData.setOperatorId("DIFFERENT1");  // 与数据库中不同的operatorId
-        tokenData.setOperatorSecret(TEST_OPERATOR_SECRET);
-
-        ThirdPartyRequest request = buildRequestWithTokenData(tokenData);
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        // 返回成功响应,但业务数据中resultStatus=1
-        assertEquals(0, response.getRet());
-        assertNotNull(response.getData());
-        assertNotNull(response.getSig());
-
-        // 解密响应数据验证resultStatus
-        String decryptedData = AESCryptoUtils.decrypt(response.getData(), TEST_DATA_SECRET, TEST_DATA_SECRET_IV);
-        QueryTokenResponseData responseData = objectMapper.readValue(decryptedData, QueryTokenResponseData.class);
-        assertEquals(1, responseData.getResultStatus());
-        assertEquals(1, responseData.getFailReason());
-    }
-
-    @Test
-    @DisplayName("operatorSecret不匹配应返回业务失败(resultStatus=1)")
-    void testQueryToken_OperatorSecretMismatch() throws Exception {
-        // 构建请求,业务参数中operatorSecret不匹配
-        QueryTokenRequestData tokenData = new QueryTokenRequestData();
-        tokenData.setOperatorId(TEST_OPERATOR_ID);
-        tokenData.setOperatorSecret("WrongSecret12345");  // 错误的密钥
-
-        ThirdPartyRequest request = buildRequestWithTokenData(tokenData);
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        // 返回成功响应,但业务数据中resultStatus=1
-        assertEquals(0, response.getRet());
-
-        // 解密响应数据验证resultStatus
-        String decryptedData = AESCryptoUtils.decrypt(response.getData(), TEST_DATA_SECRET, TEST_DATA_SECRET_IV);
-        QueryTokenResponseData responseData = objectMapper.readValue(decryptedData, QueryTokenResponseData.class);
-        assertEquals(1, responseData.getResultStatus());
-        assertEquals(1, responseData.getFailReason());
-    }
-
-    // ==================== 成功场景测试 ====================
-
-    @Test
-    @DisplayName("正常请求应成功获取Token")
-    void testQueryToken_Success() throws Exception {
-        ThirdPartyRequest request = buildCompleteRequest(TEST_OPERATOR_ID, TEST_OPERATOR_SECRET);
-
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
-        when(valueOperations.get(anyString())).thenReturn(null);  // 无已有Token
-        when(redisTemplate.getExpire(anyString(), eq(TimeUnit.SECONDS))).thenReturn(7200L);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        // 验证响应
-        assertEquals(0, response.getRet());
-        assertEquals("", response.getMsg());
-        assertNotNull(response.getData());
-        assertNotNull(response.getSig());
-
-        // 解密响应数据
-        String decryptedData = AESCryptoUtils.decrypt(response.getData(), TEST_DATA_SECRET, TEST_DATA_SECRET_IV);
-        QueryTokenResponseData responseData = objectMapper.readValue(decryptedData, QueryTokenResponseData.class);
-
-        assertEquals(TEST_OPERATOR_ID, responseData.getOperatorId());
-        assertEquals(0, responseData.getResultStatus());
-        assertNotNull(responseData.getAccessToken());
-        assertTrue(responseData.getAccessToken().length() > 0);
-        assertEquals(0, responseData.getFailReason());
-
-        // 验证Redis操作
-        verify(valueOperations, times(2)).set(anyString(), any(), eq(7200L), eq(TimeUnit.SECONDS));
-    }
-
-    @Test
-    @DisplayName("已存在有效Token应复用")
-    void testQueryToken_ReuseExistingToken() throws Exception {
-        String existingToken = "existingToken12345678901234567890123456789012345678901234567890";
-
-        ThirdPartyRequest request = buildCompleteRequest(TEST_OPERATOR_ID, TEST_OPERATOR_SECRET);
-
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
-        when(valueOperations.get("third_party_access:operator:" + TEST_OPERATOR_ID)).thenReturn(existingToken);
-        when(redisTemplate.hasKey("third_party_access:token:" + existingToken)).thenReturn(true);
-        when(redisTemplate.getExpire("third_party_access:token:" + existingToken, TimeUnit.SECONDS)).thenReturn(3600L);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        // 验证响应
-        assertEquals(0, response.getRet());
-
-        // 解密响应数据
-        String decryptedData = AESCryptoUtils.decrypt(response.getData(), TEST_DATA_SECRET, TEST_DATA_SECRET_IV);
-        QueryTokenResponseData responseData = objectMapper.readValue(decryptedData, QueryTokenResponseData.class);
-
-        assertEquals(existingToken, responseData.getAccessToken());
-        assertEquals(3600, responseData.getTokenExpirationTime());
-
-        // 验证没有创建新Token
-        verify(valueOperations, never()).set(anyString(), any(), anyLong(), any(TimeUnit.class));
-    }
-
-    @Test
-    @DisplayName("已存在Token但已过期应生成新Token")
-    void testQueryToken_ExpiredTokenGenerateNew() throws Exception {
-        String expiredToken = "expiredToken12345678901234567890123456789012345678901234567890";
-
-        ThirdPartyRequest request = buildCompleteRequest(TEST_OPERATOR_ID, TEST_OPERATOR_SECRET);
-
-        when(thirdPartyInfoMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(mockThirdPartyInfo);
-        when(redisTemplate.opsForValue()).thenReturn(valueOperations);
-        when(valueOperations.get("third_party_access:operator:" + TEST_OPERATOR_ID)).thenReturn(expiredToken);
-        when(redisTemplate.hasKey("third_party_access:token:" + expiredToken)).thenReturn(false);  // Token已过期
-        when(redisTemplate.getExpire(anyString(), eq(TimeUnit.SECONDS))).thenReturn(7200L);
-
-        ThirdPartyResponse response = thirdPartyTokenService.queryToken(request);
-
-        // 验证响应
-        assertEquals(0, response.getRet());
-
-        // 解密响应数据
-        String decryptedData = AESCryptoUtils.decrypt(response.getData(), TEST_DATA_SECRET, TEST_DATA_SECRET_IV);
-        QueryTokenResponseData responseData = objectMapper.readValue(decryptedData, QueryTokenResponseData.class);
-
-        // 应该是新生成的Token,不是过期的Token
-        assertNotEquals(expiredToken, responseData.getAccessToken());
-
-        // 验证创建了新Token
-        verify(valueOperations, times(2)).set(anyString(), any(), eq(7200L), eq(TimeUnit.SECONDS));
-    }
-
-    // ==================== 辅助方法 ====================
-
-    /**
-     * 构建完整的请求(包含正确的签名)
-     */
-    private ThirdPartyRequest buildCompleteRequest(String operatorId, String operatorSecret) {
-        try {
-            // 构建业务数据
-            QueryTokenRequestData tokenData = new QueryTokenRequestData();
-            tokenData.setOperatorId(operatorId);
-            tokenData.setOperatorSecret(operatorSecret);
-
-            return buildRequestWithTokenData(tokenData);
-        } catch (Exception e) {
-            throw new RuntimeException("构建请求失败", e);
-        }
-    }
-
-    /**
-     * 使用指定的TokenData构建请求
-     */
-    private ThirdPartyRequest buildRequestWithTokenData(QueryTokenRequestData tokenData) throws Exception {
-        String jsonData = objectMapper.writeValueAsString(tokenData);
-        String encryptedData = AESCryptoUtils.encrypt(jsonData, TEST_DATA_SECRET, TEST_DATA_SECRET_IV);
-        String timeStamp = "20260312120000";
-        String seq = "0001";
-
-        // 生成正确的签名
-        String signContent = TEST_OPERATOR_ID + encryptedData + timeStamp + seq;
-        String sig = HmacMD5Util.hmacMD5Hex(signContent, TEST_SIG_SECRET);
-
-        ThirdPartyRequest request = new ThirdPartyRequest();
-        request.setOperatorId(TEST_OPERATOR_ID);
-        request.setData(encryptedData);
-        request.setTimeStamp(timeStamp);
-        request.setSeq(seq);
-        request.setSig(sig);
-
-        return request;
-    }
-}