浏览代码

feat(charging): 优化充电启动及订单处理逻辑

- 修改AESCryptoUtils主函数注释,更新测试加解密数据
- AppChargeVO订单号改为订单id描述,AppInvokeChargeForm新增车牌号字段
- AppletWFTOrderController退款接口新增防重提交及用户登录校验
- application-dev.yml及application-prod.yml中增加微信小程序配置及调整第三方接口过滤路径
- 调整ChargeDeviceDetailRequestData设备编码字段名称,保持字段语义准确
- ChargeOrderInfoServiceImpl新增用户余额校验防止起充价不足导致异常
- 充电订单中实现车牌号优先使用表单数据,不存在时使用用户默认车牌
- ChargingBusinessServiceImpl优化启动充电日志输出,准确反映请求及响应信息
- 修改DataBoardMapper中退款金额统计SQL,去除重复状态过滤条件,保持数据准确
- 新增充电相关工具测试类:DecryptDataMain与GenerateTokenRequestMain,辅助数据加解密测试
wzq 2 天之前
父节点
当前提交
e9d36b2f99
共有 47 个文件被更改,包括 2830 次插入246 次删除
  1. 118 32
      doc/第三方接入API文档.md
  2. 22 0
      src/main/java/com/zsElectric/boot/business/controller/UserAccountController.java
  3. 3 3
      src/main/java/com/zsElectric/boot/business/controller/UserRefundsOrderInfoController.java
  4. 5 0
      src/main/java/com/zsElectric/boot/business/controller/applet/AppletWFTOrderController.java
  5. 22 0
      src/main/java/com/zsElectric/boot/business/mapper/UserAccountMapper.java
  6. 0 20
      src/main/java/com/zsElectric/boot/business/model/entity/UserVehicle.java
  7. 0 12
      src/main/java/com/zsElectric/boot/business/model/form/UserVehicleForm.java
  8. 3 0
      src/main/java/com/zsElectric/boot/business/model/form/applet/AppInvokeChargeForm.java
  9. 0 6
      src/main/java/com/zsElectric/boot/business/model/query/UserVehicleQuery.java
  10. 45 0
      src/main/java/com/zsElectric/boot/business/model/vo/RefundCompensationVO.java
  11. 0 12
      src/main/java/com/zsElectric/boot/business/model/vo/UserVehicleVO.java
  12. 1 1
      src/main/java/com/zsElectric/boot/business/model/vo/applet/AppChargeVO.java
  13. 19 0
      src/main/java/com/zsElectric/boot/business/service/UserAccountService.java
  14. 174 61
      src/main/java/com/zsElectric/boot/business/service/WFTOrderService.java
  15. 35 0
      src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java
  16. 68 0
      src/main/java/com/zsElectric/boot/business/service/impl/UserAccountServiceImpl.java
  17. 14 4
      src/main/java/com/zsElectric/boot/business/service/impl/UserVehicleServiceImpl.java
  18. 1 1
      src/main/java/com/zsElectric/boot/charging/controller/LinkDataController.java
  19. 3 3
      src/main/java/com/zsElectric/boot/charging/service/impl/ChargingBusinessServiceImpl.java
  20. 61 57
      src/main/java/com/zsElectric/boot/common/util/AESCryptoUtils.java
  21. 1 1
      src/main/java/com/zsElectric/boot/common/util/HmacMD5Util.java
  22. 2 2
      src/main/java/com/zsElectric/boot/common/util/electric/queryToken/JwtTokenUtil.java
  23. 22 10
      src/main/java/com/zsElectric/boot/common/util/electric/queryToken/ThirdPartyJwtAuthFilter.java
  24. 133 0
      src/main/java/com/zsElectric/boot/sdk/SdkExample.java
  25. 369 0
      src/main/java/com/zsElectric/boot/sdk/ZsElectricClient.java
  26. 156 0
      src/main/java/com/zsElectric/boot/sdk/ZsElectricConfig.java
  27. 74 0
      src/main/java/com/zsElectric/boot/sdk/model/SdkRequest.java
  28. 87 0
      src/main/java/com/zsElectric/boot/sdk/model/SdkResponse.java
  29. 92 0
      src/main/java/com/zsElectric/boot/sdk/model/SdkResult.java
  30. 185 0
      src/main/java/com/zsElectric/boot/sdk/util/SdkCryptoUtil.java
  31. 15 4
      src/main/java/com/zsElectric/boot/thirdParty/controller/ThirdPartyController.java
  32. 1 6
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeDeviceDetailRequestData.java
  33. 25 0
      src/main/java/com/zsElectric/boot/thirdParty/model/QueryChargingCostRequestData.java
  34. 166 0
      src/main/java/com/zsElectric/boot/thirdParty/model/QueryChargingCostResponseData.java
  35. 9 0
      src/main/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenService.java
  36. 95 2
      src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java
  37. 23 0
      src/main/resources/application-dev.yml
  38. 1 1
      src/main/resources/application-prod.yml
  39. 364 0
      src/main/resources/application-test.yml
  40. 40 0
      src/main/resources/mapper/business/UserAccountMapper.xml
  41. 0 1
      src/main/resources/mapper/business/UserRefundsOrderInfoMapper.xml
  42. 2 2
      src/main/resources/mapper/system/DataBoardMapper.xml
  43. 36 0
      src/test/java/com/zsElectric/boot/charging/DecryptDataMain.java
  44. 71 0
      src/test/java/com/zsElectric/boot/charging/GenerateTokenRequestMain.java
  45. 188 0
      src/test/java/com/zsElectric/boot/charging/QueryTokenMain.java
  46. 74 0
      src/test/java/com/zsElectric/boot/charging/VerifySigMain.java
  47. 5 5
      src/test/java/com/zsElectric/boot/thirdParty/service/QueryTokenMain.java

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

@@ -16,8 +16,9 @@
 - [8. 获取充电终端详情](#8-获取充电终端详情)
 - [9. 启动充电](#9-启动充电)
 - [10. 停止充电](#10-停止充电)
-- [11. 查询充电订单列表](#11-查询充电订单列表)
-- [12. 查询充电订单详情](#12-查询充电订单详情)
+- [11. 查询充电订单实时费用](#11-查询充电订单实时费用)
+- [12. 查询充电订单分页列表](#12-查询充电订单分页列表)
+- [13. 查询充电订单详情](#13-查询充电订单详情)
 - [附录:配置模板](#附录配置模板)
 
 ---
@@ -89,6 +90,7 @@
 - **请求格式:** JSON
 - **请求方式:** POST
 - **接口路径:** `/third-party/v1/query_token`
+- **⚠️注意:** 当前接口限制每分钟30次访问,请避免频繁调用。建议根据过期时间缓存Token,避免重复获取!
 
 ### 2.2 输入参数(data解密后)
 
@@ -284,12 +286,12 @@
 
 ---
 
-## 5. 充券购买
+## 5. 充券购买
 
 ### 5.1 接口描述
 
 - **接口名称:** charge_order_pay
-- **接口说明:** 第三方充点券购买
+- **接口说明:** 第三方充点充电券购买
 - **请求格式:** JSON
 - **请求方式:** POST
 - **接口路径:** `/third-party/v1/charge_order_pay`
@@ -559,11 +561,8 @@
 ### 8.2 输入参数(data解密后)
 
 | 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
-|----------|----------|----------|------|----------|
-| id | 设备主键ID | Long | 设备主键ID | 否 |
-| equipmentId | 设备编码 | String | 设备编码 | 否 |
-
-> 注:id 和 equipmentId 至少传一个
+|----------|----------|----------|------|-----|
+| connectorId | 设备编码 | String | 设备编码 | 是   |
 
 ### 8.3 请求示例
 
@@ -571,8 +570,7 @@
 
 ```json
 {
-  "id": 101,
-  "equipmentId": "EQ001"
+  "connectorId": "89825635646_1"
 }
 ```
 
@@ -657,14 +655,15 @@
 
 ### 9.2 输入参数(data解密后)
 
-| 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
-|----------|----------|----------|------|----------|
-| equipmentId | 充电桩编号 | String | 充电桩编号 | 是 |
-| stationId | 第三方充电站ID | String | 充电站ID | 是 |
-| connectorId | 充电设备接口编码 | String | 充电设备接口编码 | 是 |
-| channelOrderNo | 渠道方订单编号 | String | 第三方平台订单编号 | 是 |
-| channelUserPhone | 渠道方用户手机号 | String | 用户手机号 | 是 |
-| channelPreAmt | 渠道方预支付金额 | BigDecimal | 预支付金额 | 是 |
+| 参数名称             | 参数定义 | 参数类型 | 描述 | 是否必填 |
+|------------------|----------|----------|------|------|
+| equipmentId      | 充电桩编号 | String | 充电桩编号 | 是    |
+| stationId        | 第三方充电站ID | String | 充电站ID | 是    |
+| connectorId      | 充电设备接口编码 | String | 充电设备接口编码 | 是    |
+| channelOrderNo   | 渠道方订单编号 | String | 第三方平台订单编号 | 是    |
+| channelUserPhone | 渠道方用户手机号 | String | 用户手机号 | 是    |
+| channelPreAmt    | 渠道方预支付金额 | BigDecimal | 预支付金额 | 是    |
+| plateNum         | 车牌号 | String | 车牌号 | 否    |
 
 ### 9.3 请求示例
 
@@ -677,7 +676,8 @@
   "connectorId": "CONN001",
   "channelOrderNo": "CH2024031600001",
   "channelUserPhone": "13800138000",
-  "channelPreAmt": 100.00
+  "channelPreAmt": 100.00,
+  "plateNum": "京A88888"
 }
 ```
 
@@ -760,18 +760,104 @@
 
 ---
 
-## 11. 查询充电订单列表
+## 11. 查询充电订单实时费用
 
 ### 11.1 接口描述
 
+- **接口名称:** query_charging_cost
+- **接口说明:** 第三方查询充电订单实时费用(实时获取充电过程中的费用信息)
+- **请求格式:** JSON
+- **请求方式:** POST
+- **接口路径:** `/third-party/v1/query_charging_cost`
+- **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
+
+### 11.2 输入参数(data解密后)
+
+| 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
+|----------|----------|----------|------|----------|
+| chargeOrderNo | 充电订单号 | String | 充电订单号 | 是 |
+
+### 11.3 请求示例
+
+**data加密前:**
+
+```json
+{
+  "chargeOrderNo": "CD2024031600001"
+}
+```
+
+### 11.4 返回参数(data解密后)
+
+| 参数名称 | 参数定义 | 参数类型 | 描述 |
+|----------|----------|----------|------|
+| result | 查询结果 | Integer | 0-成功,1-失败 |
+| message | 结果消息 | String | 结果描述 |
+| chargeOrderNo | 充电订单号 | String | 充电订单号 |
+| stationName | 充电站名称 | String | 充电站名称 |
+| connectorName | 充电终端名称 | String | 充电终端名称 |
+| connectorCode | 充电接口编码 | String | 充电接口编码 |
+| orderStatus | 订单状态 | Integer | 1-启动中,2-充电中,3-停止中,4-已结束,5-未知 |
+| orderStatusDesc | 订单状态描述 | String | 订单状态描述 |
+| status | 充电状态 | String | 0-待启动,1-充电中,2-结算中,3-已完成,5-未成功充电 |
+| chargingDuration | 充电时长 | Long | 充电时长(秒) |
+| chargingDurationDesc | 充电时长描述 | String | 格式:HH:mm:ss |
+| totalPower | 累计充电量 | BigDecimal | 单位:kWh |
+| elecMoney | 累计电费 | BigDecimal | 单位:元 |
+| serviceMoney | 累计服务费 | BigDecimal | 单位:元 |
+| totalMoney | 累计总金额 | BigDecimal | 单位:元 |
+| soc | 电池SOC | Integer | 剩余电量百分比 |
+| current | 当前电流 | BigDecimal | 单位:A |
+| voltage | 当前电压 | BigDecimal | 单位:V |
+| power | 当前功率 | BigDecimal | 单位:kW |
+| startTime | 充电开始时间 | String | 格式:yyyy-MM-dd HH:mm:ss |
+| lastUpdateTime | 最后更新时间 | String | 格式:yyyy-MM-dd HH:mm:ss |
+
+
+### 11.5 返回示例
+
+**data解密后:**
+
+```json
+{
+  "result": 0,
+  "message": "查询成功",
+  "chargeOrderNo": "CD2024031600001",
+  "stationName": "XX充电站",
+  "connectorName": "101号充电口",
+  "connectorCode": "CONN001",
+  "orderStatus": 2,
+  "orderStatusDesc": "充电中",
+  "status": "1",
+  "chargingDuration": 1855,
+  "chargingDurationDesc": "00:30:55",
+  "totalPower": 25.68,
+  "elecMoney": 15.80,
+  "serviceMoney": 3.20,
+  "totalMoney": 19.00,
+  "soc": 85,
+  "current": 32.5,
+  "voltage": 220.0,
+  "power": 7.15,
+  "startTime": "2024-03-16 10:00:00",
+  "lastUpdateTime": "2024-03-16 10:30:55"
+}
+```
+
+---
+
+## 12. 查询充电订单分页列表
+
+### 12.1 接口描述
+
 - **接口名称:** query_charge_order_list
-- **接口说明:** 第三方查询充电订单列表(只查询渠道类型订单)
+- **接口说明:** 第三方查询充电订单分页列表(只查询所属运营商订单)
 - **请求格式:** JSON
 - **请求方式:** POST
 - **接口路径:** `/third-party/v1/query_charge_order_list`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
-### 11.2 输入参数(data解密后)
+### 12.2 输入参数(data解密后)
 
 | 参数名称 | 参数定义 | 参数类型   | 描述 | 是否必填 |
 |----------|----------|--------|------|----------|
@@ -781,7 +867,7 @@
 | pageNo | 页码 | Long   | 页码,默认1 | 否 |
 | pageSize | 每页记录数 | Long   | 每页记录数,默认10 | 否 |
 
-### 11.3 请求示例
+### 12.3 请求示例
 
 **data加密前:**
 
@@ -794,7 +880,7 @@
 }
 ```
 
-### 11.4 返回参数(data解密后)
+### 12.4 返回参数(data解密后)
 
 | 参数名称 | 参数定义 | 参数类型    | 描述 |
 |----------|----------|---------|------|
@@ -827,7 +913,7 @@
 | stopReason | 停止原因 | String | 停止原因描述 |
 | createTime | 创建时间 | String | 订单创建时间 |
 
-### 11.5 返回示例
+### 12.5 返回示例
 
 **data解密后:**
 
@@ -864,9 +950,9 @@
 
 ---
 
-## 12. 查询充电订单详情
+## 13. 查询充电订单详情
 
-### 12.1 接口描述
+### 13.1 接口描述
 
 - **接口名称:** query_charge_order_info
 - **接口说明:** 第三方查询充电订单详情(只查询渠道类型订单)
@@ -875,13 +961,13 @@
 - **接口路径:** `/third-party/v1/query_charge_order_info`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
-### 12.2 输入参数(data解密后)
+### 13.2 输入参数(data解密后)
 
 | 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
 |----------|----------|----------|------|----------|
 | chargeOrderNo | 充电订单号 | String | 充电订单号 | 是 |
 
-### 12.3 请求示例
+### 13.3 请求示例
 
 **data加密前:**
 
@@ -891,7 +977,7 @@
 }
 ```
 
-### 12.4 返回参数(data解密后)
+### 13.4 返回参数(data解密后)
 
 | 参数名称 | 参数定义 | 参数类型 | 描述 |
 |----------|----------|----------|------|
@@ -916,7 +1002,7 @@
 | stopReason | 停止原因 | String | 停止原因描述 |
 | createTime | 创建时间 | String | 订单创建时间 |
 
-### 12.5 返回示例
+### 13.5 返回示例
 
 **data解密后:**
 

+ 22 - 0
src/main/java/com/zsElectric/boot/business/controller/UserAccountController.java

@@ -1,6 +1,9 @@
 package com.zsElectric.boot.business.controller;
 
+import com.zsElectric.boot.business.model.vo.RefundCompensationVO;
 import com.zsElectric.boot.business.service.UserAccountService;
+import com.zsElectric.boot.common.annotation.Log;
+import com.zsElectric.boot.common.enums.LogModuleEnum;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -18,6 +21,8 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import jakarta.validation.Valid;
 
+import java.util.List;
+
 /**
  * 个人账户前端控制层
  *
@@ -78,4 +83,21 @@ public class UserAccountController  {
         boolean result = userAccountService.deleteUserAccounts(ids);
         return Result.judge(result);
     }
+
+    @Operation(summary = "查询负余额退款异常用户列表", description = "查询2026-01-01之后退款时余额为负值的异常用户")
+    @GetMapping("/compensation/list")
+    @PreAuthorize("@ss.hasPerm('business:user-account:query')")
+    public Result<List<RefundCompensationVO>> listNegativeBalanceRefundUsers() {
+        List<RefundCompensationVO> list = userAccountService.listNegativeBalanceRefundUsers();
+        return Result.success(list);
+    }
+
+    @Operation(summary = "执行负余额退款补偿操作", description = "将负余额退款异常用户的余额进行扣减补偿")
+    @PostMapping("/compensation/execute")
+    @PreAuthorize("@ss.hasPerm('business:user-account:edit')")
+    @Log(value = "执行负余额退款补偿操作", module = LogModuleEnum.ADMIN_ACTIONS)
+    public Result<Integer> executeCompensation() {
+        int affectedRows = userAccountService.executeCompensation();
+        return Result.success(affectedRows);
+    }
 }

+ 3 - 3
src/main/java/com/zsElectric/boot/business/controller/UserRefundsOrderInfoController.java

@@ -79,16 +79,16 @@ public class UserRefundsOrderInfoController  {
         return Result.judge(result);
     }
 
-    @Operation(summary = "导出退款失败订单(success_time为空)")
+    @Operation(summary = "导出退款订单")
     @PostMapping("/export")
     @PreAuthorize("@ss.hasPerm('business:user-refunds-order-info:export')")
     public void exportUserRefundsOrderInfo(@RequestBody UserRefundsOrderInfoExportQuery queryParams, HttpServletResponse response) throws IOException {
-        String fileName = "退款失败订单.xlsx";
+        String fileName = "退款订单.xlsx";
         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
         response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
 
         List<UserRefundsOrderInfoExportDTO> exportList = userRefundsOrderInfoService.listExportUserRefundsOrderInfo(queryParams);
-        EasyExcel.write(response.getOutputStream(), UserRefundsOrderInfoExportDTO.class).sheet("退款失败订单")
+        EasyExcel.write(response.getOutputStream(), UserRefundsOrderInfoExportDTO.class).sheet("退款订单")
                 .doWrite(exportList);
     }
 }

+ 5 - 0
src/main/java/com/zsElectric/boot/business/controller/applet/AppletWFTOrderController.java

@@ -5,6 +5,7 @@ import com.zsElectric.boot.business.model.form.applet.AppUserPayForm;
 import com.zsElectric.boot.business.model.vo.applet.WFTRefundQueryVO;
 import com.zsElectric.boot.business.service.WFTOrderService;
 import com.zsElectric.boot.common.annotation.Log;
+import com.zsElectric.boot.common.annotation.RepeatSubmit;
 import com.zsElectric.boot.common.constant.SystemConstants;
 import com.zsElectric.boot.common.enums.LogModuleEnum;
 import com.zsElectric.boot.common.util.IPUtils;
@@ -142,8 +143,12 @@ public class AppletWFTOrderController {
     @Operation(summary = "账户退款")
     @PutMapping("/refundOrder")
     @Log(value = "账户退款", module = LogModuleEnum.APP_ORDER)
+    @RepeatSubmit(expire = 30)
     public Result<String> refundOrder() throws Exception {
         Long userId = SecurityUtils.getUserId();
+        if (userId == null) {
+            return Result.failed("用户未登录或登录已过期");
+        }
         return Result.success(wftOrderService.refundOrder(userId, SystemConstants.STATUS_TWO));
     }
 

+ 22 - 0
src/main/java/com/zsElectric/boot/business/mapper/UserAccountMapper.java

@@ -5,7 +5,12 @@ import com.zsElectric.boot.business.model.entity.UserAccount;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zsElectric.boot.business.model.query.UserAccountQuery;
 import com.zsElectric.boot.business.model.vo.UserAccountVO;
+import com.zsElectric.boot.business.model.vo.RefundCompensationVO;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * 个人账户Mapper接口
@@ -25,4 +30,21 @@ public interface UserAccountMapper extends BaseMapper<UserAccount> {
      */
     Page<UserAccountVO> getUserAccountPage(Page<UserAccountVO> page, UserAccountQuery queryParams);
 
+    /**
+     * 查询负余额退款异常用户列表
+     * 用于查询退款时余额为负值的用户,这些用户退款后余额被异常置为0
+     *
+     * @param startTime 开始时间
+     * @return 异常用户列表
+     */
+    List<RefundCompensationVO> listNegativeBalanceRefundUsers(@Param("startTime") LocalDateTime startTime);
+
+    /**
+     * 执行补偿操作-批量更新用户余额
+     *
+     * @param startTime 开始时间
+     * @return 受影响的行数
+     */
+    int executeCompensation(@Param("startTime") LocalDateTime startTime);
+
 }

+ 0 - 20
src/main/java/com/zsElectric/boot/business/model/entity/UserVehicle.java

@@ -29,26 +29,6 @@ public class UserVehicle extends BaseEntity {
      */
     private String licensePlate;
 
-    /**
-     * 车辆品牌
-     */
-    private String brand;
-
-    /**
-     * 车辆型号
-     */
-    private String model;
-
-    /**
-     * 车辆颜色
-     */
-    private String color;
-
-    /**
-     * 车辆类型(1-新能源 2-燃油车 3-混合动力)
-     */
-    private Integer vehicleType;
-
     /**
      * 是否默认车辆(0-否 1-是)
      */

+ 0 - 12
src/main/java/com/zsElectric/boot/business/model/form/UserVehicleForm.java

@@ -26,18 +26,6 @@ public class UserVehicleForm {
     @NotBlank(message = "车牌号不能为空")
     private String licensePlate;
 
-    @Schema(description = "车辆品牌")
-    private String brand;
-
-    @Schema(description = "车辆型号")
-    private String model;
-
-    @Schema(description = "车辆颜色")
-    private String color;
-
-    @Schema(description = "车辆类型(1-新能源 2-燃油车 3-混合动力)")
-    private Integer vehicleType;
-
     @Schema(description = "是否默认车辆(0-否 1-是)")
     private Integer isDefault;
 

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

@@ -50,4 +50,7 @@ public class AppInvokeChargeForm implements Serializable {
 
     @Schema(description = "渠道方预支付金额")
     private BigDecimal channelPreAmt;
+
+    @Schema(description = "车牌号")
+    private String plateNum;
 }

+ 0 - 6
src/main/java/com/zsElectric/boot/business/model/query/UserVehicleQuery.java

@@ -21,10 +21,4 @@ public class UserVehicleQuery extends BasePageQuery {
 
     @Schema(description = "车牌号")
     private String licensePlate;
-
-    @Schema(description = "车辆品牌")
-    private String brand;
-
-    @Schema(description = "车辆类型(1-新能源 2-燃油车 3-混合动力)")
-    private Integer vehicleType;
 }

+ 45 - 0
src/main/java/com/zsElectric/boot/business/model/vo/RefundCompensationVO.java

@@ -0,0 +1,45 @@
+package com.zsElectric.boot.business.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 退款补偿VO
+ * 用于查询负余额退款导致归零的异常用户
+ *
+ * @author zsElectric
+ * @since 2026-03-24
+ */
+@Data
+@Schema(description = "退款补偿VO")
+public class RefundCompensationVO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "用户ID")
+    private Long userId;
+
+    @Schema(description = "退款前余额(负值)")
+    private BigDecimal refundBeforeBalance;
+
+    @Schema(description = "退款后余额")
+    private BigDecimal refundAfterBalance;
+
+    @Schema(description = "需要补偿的金额(负值的绝对值)")
+    private BigDecimal compensationAmount;
+
+    @Schema(description = "退款订单ID")
+    private Long refundOrderId;
+
+    @Schema(description = "退款时间")
+    private LocalDateTime refundTime;
+
+    @Schema(description = "当前账户余额")
+    private BigDecimal currentBalance;
+}

+ 0 - 12
src/main/java/com/zsElectric/boot/business/model/vo/UserVehicleVO.java

@@ -25,18 +25,6 @@ public class UserVehicleVO {
     @Schema(description = "车牌号")
     private String licensePlate;
 
-    @Schema(description = "车辆品牌")
-    private String brand;
-
-    @Schema(description = "车辆型号")
-    private String model;
-
-    @Schema(description = "车辆颜色")
-    private String color;
-
-    @Schema(description = "车辆类型(1-新能源 2-燃油车 3-混合动力)")
-    private Integer vehicleType;
-
     @Schema(description = "是否默认车辆(0-否 1-是)")
     private Integer isDefault;
 

+ 1 - 1
src/main/java/com/zsElectric/boot/business/model/vo/applet/AppChargeVO.java

@@ -15,7 +15,7 @@ public class AppChargeVO implements Serializable {
     @Serial
     private static final long serialVersionUID = 1L;
 
-    @Schema(description = "订单")
+    @Schema(description = "订单id")
     private Long chargeOrderId;
 
     @Schema(description = "订单号")

+ 19 - 0
src/main/java/com/zsElectric/boot/business/service/UserAccountService.java

@@ -4,9 +4,12 @@ import com.zsElectric.boot.business.model.entity.UserAccount;
 import com.zsElectric.boot.business.model.form.UserAccountForm;
 import com.zsElectric.boot.business.model.query.UserAccountQuery;
 import com.zsElectric.boot.business.model.vo.UserAccountVO;
+import com.zsElectric.boot.business.model.vo.RefundCompensationVO;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 
+import java.util.List;
+
 /**
  * 个人账户服务类
  *
@@ -68,4 +71,20 @@ public interface UserAccountService extends IService<UserAccount> {
     UserAccount updateAccountBalanceAndLog(Long userId, java.math.BigDecimal changeAmount,
                                           Integer changeType, String changeNote, Long changeId);
 
+    /**
+     * 查询负余额退款异常用户列表
+     * 用于查询退款时余额为负值的用户,这些用户退款后余额被异常置为0
+     *
+     * @return 异常用户列表
+     */
+    List<RefundCompensationVO> listNegativeBalanceRefundUsers();
+
+    /**
+     * 执行补偿操作
+     * 将负余额退款异常用户的余额进行扣减补偿
+     *
+     * @return 受影响的用户数
+     */
+    int executeCompensation();
+
 }

+ 174 - 61
src/main/java/com/zsElectric/boot/business/service/WFTOrderService.java

@@ -25,6 +25,9 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -33,6 +36,7 @@ import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 威富通支付服务
@@ -66,6 +70,19 @@ public class WFTOrderService {
     @Resource
     private UserRefundsOrderInfoMapper userRefundsOrderInfoMapper;
 
+    @Resource
+    private RedissonClient redissonClient;
+
+    /**
+     * 退款分布式锁key前缀
+     */
+    private static final String REFUND_LOCK_KEY = "lock:refund:user:";
+
+    /**
+     * 退款锁过期时间(秒)
+     */
+    private static final long REFUND_LOCK_EXPIRE = 60;
+
 
     /**
      * 创建商户订单号
@@ -785,19 +802,49 @@ public class WFTOrderService {
      */
     @Transactional(rollbackFor = Exception.class)
     public String refundOrder(Long userId, Integer type) throws Exception {
+        // 获取分布式锁,防止同一用户并发退款
+        String lockKey = REFUND_LOCK_KEY + userId;
+        RLock lock = redissonClient.getLock(lockKey);
 
-        //查询账户余额
+        boolean locked = false;
+        try {
+            // 尝试获取锁,等待0秒,锁过期时间60秒
+            locked = lock.tryLock(0, REFUND_LOCK_EXPIRE, TimeUnit.SECONDS);
+            if (!locked) {
+                log.warn("用户:{}退款操作正在进行中,请勿重复提交", userId);
+                throw new BusinessException("退款操作正在进行中,请勿重复提交");
+            }
+
+            return doRefundOrder(userId, type);
+        } finally {
+            // 释放锁
+            if (locked && lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
+    /**
+     * 执行退款逻辑(内部方法)
+     */
+    private String doRefundOrder(Long userId, Integer type) throws Exception {
+        // 查询账户余额
         UserAccount userAccount =
                 userAccountService.getOne(Wrappers.<UserAccount>lambdaQuery().eq(UserAccount::getUserId, userId).last("limit 1"));
+        if (userAccount == null) {
+            log.warn("用户:{}账户不存在", userId);
+            throw new BusinessException("账户信息异常,请联系客服处理");
+        }
         log.info("用户:{},申请退款:{}", userId, userAccount.getBalance());
-        if (userAccount.getBalance().compareTo(BigDecimal.ZERO) == 0) {
-            return "账户余额为 0";
+        if (userAccount.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
+            return "账户余额不足,无法退款";
         }
         BigDecimal refundMoney = userAccount.getBalance();
 
-        //查询一年内已支付且可退款的所有券订单(已支付和部分退款状态)
+        // 查询一年内已支付且可退款的所有券订单(已支付和部分退款状态)
         List<UserOrderInfo> userOrderInfoList = userOrderInfoMapper.selectList(Wrappers.<UserOrderInfo>lambdaQuery()
                 .eq(UserOrderInfo::getUserId, userId)
+                .ne(UserOrderInfo::getOrderType, 3) // 不为渠道方订单
                 .in(UserOrderInfo::getOrderStatus, SystemConstants.STATUS_TWO, SystemConstants.STATUS_FIVE)
                 .between(UserOrderInfo::getCreateTime, LocalDateTime.now().minusYears(1), LocalDateTime.now())
                 .orderByDesc(UserOrderInfo::getCreateTime) // 按创建时间降序,优先退近期订单
@@ -808,7 +855,7 @@ public class WFTOrderService {
             throw new BusinessException("无法进行退款操作,请联系客服处理!");
         }
 
-        //查询是否存在正在充电的订单
+        // 查询是否存在正在充电的订单
         List<ChargeOrderInfo> chargeOrderInfoList = chargeOrderInfoMapper.selectList(Wrappers.<ChargeOrderInfo>lambdaQuery()
                 .eq(ChargeOrderInfo::getUserId, userId)
                 .in(ChargeOrderInfo::getStatus, SystemConstants.STATUS_ONE, SystemConstants.STATUS_TWO)
@@ -819,69 +866,135 @@ public class WFTOrderService {
         }
 
         for (UserOrderInfo userOrderInfo : userOrderInfoList) {
-            if (refundMoney.compareTo(BigDecimal.ZERO) == 0) {
+            if (refundMoney.compareTo(BigDecimal.ZERO) <= 0) {
                 break;
             }
             // 处理退款金额为null的情况,默认为0
             BigDecimal alreadyRefundMoney = userOrderInfo.getRefundMoney() == null ? BigDecimal.ZERO : userOrderInfo.getRefundMoney();
             BigDecimal canRefundMoney = userOrderInfo.getOrderMoney().subtract(alreadyRefundMoney);
 
-            if (canRefundMoney.compareTo(refundMoney) >= 0) {
-                //可退款金额大于等于待退款金额,则直接退全部待退款金额
-                Long refundOrderId = refundOrder(userOrderInfo, refundMoney, "账户退款", type);
-
-                //计算本次退款后的累计退款金额
-                BigDecimal totalRefundMoney = alreadyRefundMoney.add(refundMoney);
-                boolean isFullRefund = totalRefundMoney.compareTo(userOrderInfo.getOrderMoney()) >= 0;
-
-                //账户变动及日志记录
-                userAccountService.updateAccountBalanceAndLog(
-                        userId,
-                        refundMoney,
-                        SystemConstants.CHANGE_TYPE_REDUCE,
-                        SystemConstants.ACCOUNT_LOG_REFUND_NOTE,
-                        refundOrderId
-                );
-
-                //修改订单状态(判断是全额退款还是部分退款)
-                if (isFullRefund) {
-                    // 退款金额等于或大于订单金额,全额退款
-                    userOrderInfo.setOrderStatus(SystemConstants.STATUS_FOUR);
-                } else {
-                    // 退款金额小于订单金额,部分退款
-                    userOrderInfo.setOrderStatus(SystemConstants.STATUS_FIVE);
-                }
-                userOrderInfoMapper.updateById(userOrderInfo);
-                refundMoney = BigDecimal.ZERO;
-                break;
-            }
-            if (canRefundMoney.compareTo(refundMoney) < 0) {
-                //退款金额小于订单金额,则先退订单金额
-                refundOrder(userOrderInfo, canRefundMoney, "账户退款", type);
-                //账户变动及日志记录(减少账户余额)
-                userAccountService.updateAccountBalanceAndLog(
-                        userId,
-                        canRefundMoney,
-                        SystemConstants.CHANGE_TYPE_REDUCE,
-                        SystemConstants.ACCOUNT_LOG_REFUND_NOTE,
-                        userOrderInfo.getId()
-                );
-                //修改订单状态(判断是全额退款还是部分退款)
-                BigDecimal totalRefundMoney = alreadyRefundMoney.add(canRefundMoney);
-                if (totalRefundMoney.compareTo(userOrderInfo.getOrderMoney()) >= 0) {
-                    // 退款金额等于订单金额,全额退款
-                    userOrderInfo.setOrderStatus(SystemConstants.STATUS_FOUR);
-                } else {
-                    // 退款金额小于订单金额,部分退款
-                    userOrderInfo.setOrderStatus(SystemConstants.STATUS_FIVE);
-                }
-                userOrderInfoMapper.updateById(userOrderInfo);
-                refundMoney = refundMoney.subtract(canRefundMoney);
+            // 防止异常数据导致可退款金额为负
+            if (canRefundMoney.compareTo(BigDecimal.ZERO) <= 0) {
+                log.warn("订单:{}可退款金额异常,orderMoney={},refundMoney={}",
+                        userOrderInfo.getOrderNo(), userOrderInfo.getOrderMoney(), alreadyRefundMoney);
+                continue;
             }
+
+            // 确定本次退款金额
+            BigDecimal thisRefundAmount = canRefundMoney.compareTo(refundMoney) >= 0 ? refundMoney : canRefundMoney;
+
+            // 调用支付接口退款
+            Long refundOrderId = executeRefund(userOrderInfo, thisRefundAmount, "账户退款", type);
+
+            // 账户变动及日志记录
+            userAccountService.updateAccountBalanceAndLog(
+                    userId,
+                    thisRefundAmount,
+                    SystemConstants.CHANGE_TYPE_REDUCE,
+                    SystemConstants.ACCOUNT_LOG_REFUND_NOTE,
+                    refundOrderId
+            );
+
+            // 计算本次退款后的累计退款金额并更新订单状态
+            BigDecimal totalRefundMoney = alreadyRefundMoney.add(thisRefundAmount);
+            boolean isFullRefund = totalRefundMoney.compareTo(userOrderInfo.getOrderMoney()) >= 0;
+
+            // 更新订单退款金额和状态
+            userOrderInfo.setRefundMoney(totalRefundMoney);
+            userOrderInfo.setRefundTime(LocalDateTime.now());
+            userOrderInfo.setOrderStatus(isFullRefund ? SystemConstants.STATUS_FOUR : SystemConstants.STATUS_FIVE);
+            userOrderInfoMapper.updateById(userOrderInfo);
+
+            refundMoney = refundMoney.subtract(thisRefundAmount);
         }
         return "账户退款,预计3个工作日内分一笔或多笔退还!到期如未收到,请联系客服处理!";
     }
 
+    /**
+     * 执行单笔退款(调用支付接口)
+     * 职责:只负责调用支付接口和记录退款订单,不负责更新原订单状态
+     *
+     * @param userOrderInfo 原订单信息
+     * @param refundAmount  退款金额
+     * @param reason        退款原因
+     * @param type          退款类型
+     * @return 退款订单ID
+     */
+    private Long executeRefund(UserOrderInfo userOrderInfo, BigDecimal refundAmount, String reason, Integer type) throws Exception {
+        log.info("进入退款接口------>退款金额:{}", refundAmount);
+        log.info("执行操作的 原支付交易对应的商户订单号:{}", userOrderInfo.getOrderNo());
+
+        // 退款单号
+        String outRefundNo = createOrderNo("TK", userOrderInfo.getId());
+
+        // 创建退款订单记录
+        UserRefundsOrderInfo userRefundsOrderInfo = new UserRefundsOrderInfo();
+        userRefundsOrderInfo.setOrderId(userOrderInfo.getId());
+        userRefundsOrderInfo.setOrderNo(userOrderInfo.getOrderNo());
+        userRefundsOrderInfo.setUserId(userOrderInfo.getUserId());
+        userRefundsOrderInfo.setOutRefundNo(outRefundNo);
+        userRefundsOrderInfo.setReason(reason);
+        userRefundsOrderInfo.setAmount(refundAmount);
+        userRefundsOrderInfo.setType(type);
+        userRefundsOrderInfo.setCreateTime(LocalDateTime.now());
+
+        // 构建支付接口参数
+        SortedMap<String, String> params = new TreeMap<>();
+        if (ObjectUtil.isNotEmpty(userOrderInfo.getTransactionId())) {
+            params.put("transaction_id", userOrderInfo.getTransactionId());
+            params.put("out_trade_no", userOrderInfo.getOrderNo());
+        } else {
+            params.put("out_trade_no", userOrderInfo.getOutTradeNo()); // 1.0的商户订单号
+        }
+        params.put("out_refund_no", outRefundNo);
+        params.put("attach", reason);
+        params.put("total_fee", amount_fee(userOrderInfo.getOrderMoney()));
+        params.put("refund_fee", amount_fee(refundAmount));
+        params.put("sign_type", "RSA_1_256");
+
+        // 调用支付接口
+        PayUtill payUtill = new PayUtill();
+        Map<String, String> refund = payUtill.refund(params, swiftpassConfig);
+        log.info("威富通退款接口返回数据:{}", refund);
+
+        // 检查退款调用结果
+        if (!Objects.equals(refund.get("status"), "0")) {
+            log.error("退款接口调用失败,订单:{}, 返回:{}", userOrderInfo.getOrderNo(), refund);
+            throw new BusinessException("退款失败,请稍后重试或联系客服");
+        }
+
+        log.info("退款调用成功!");
+        String resultCode = refund.get("result_code");
+
+        if (Objects.equals(resultCode, "0")) {
+            // 退款成功
+            log.info("订单:{},退款成功!原因:{}", userOrderInfo.getOrderNo(), reason);
+            userRefundsOrderInfo.setStatus("SUCCESS");
+            userRefundsOrderInfo.setSuccessTime(LocalDateTime.now());
+        } else {
+            // 退款处理中
+            log.info("订单:{},退款处理中", userOrderInfo.getOrderNo());
+            userRefundsOrderInfo.setStatus("PROCESSING");
+        }
+
+        // 保存退款记录
+        userRefundsOrderInfo.setRefundId(refund.get("refund_id"));
+        userRefundsOrderInfo.setNotifyRequest(refund.toString());
+        userRefundsOrderInfo.setTransactionId(userOrderInfo.getTransactionId());
+        userRefundsOrderInfo.setAcceptedTime(LocalDateTime.now());
+        userRefundsOrderInfoMapper.insert(userRefundsOrderInfo);
+
+        // 异步查询退款状态
+        if (userRefundsOrderInfo.getRefundId() != null) {
+            executeTaskQueryRefundOrder(userRefundsOrderInfo, userRefundsOrderInfo.getRefundId());
+        }
+
+        return userRefundsOrderInfo.getId();
+    }
+
+    /**
+     * 执行单笔订单退款(外部调用接口,包含订单状态更新)
+     */
     public Long refundOrder(UserOrderInfo userOrderInfo, BigDecimal refundAmount, String reason, Integer type) throws Exception {
         log.info("进入退款接口------>退款金额:{}",refundAmount);
         log.info("执行操作的 原支付交易对应的商户订单号:{}", userOrderInfo.getOrderNo());
@@ -1143,12 +1256,12 @@ public class WFTOrderService {
     public static void main(String[] args) throws IOException {
         SortedMap<String, String> params = new TreeMap<>();
         SwiftpassConfig swiftpassConfig = new SwiftpassConfig();
-        params.put("transaction_id", "2301202603032024626904");//商户订单号
+        params.put("transaction_id", "2301202603232029350379");//商户订单号
 //        params.put("out_trade_no", "ZSWL20251127000000019417");//商户订单号
-        params.put("out_refund_no", "TK2301202603032024626904");//商户退款单号
+        params.put("out_refund_no", "TK2301202603232029350379");//商户退款单号
         params.put("attach", "");//退款原因
-        params.put("total_fee", "5000");//原订单金额
-        params.put("refund_fee", "2737");//退款金额
+        params.put("total_fee", "500");//原订单金额
+        params.put("refund_fee", "500");//退款金额
         params.put("sign_type", "RSA_1_256");
         PayUtill payUtill = new PayUtill();
         swiftpassConfig.setKey("f5131b3f07acb965a59041b690a29911");

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

@@ -19,6 +19,7 @@ import com.zsElectric.boot.business.model.form.applet.AppStopChargeForm;
 import com.zsElectric.boot.business.model.query.ChargeOrderInfoQuery;
 import com.zsElectric.boot.business.model.query.applet.AppChargeOrderInfoQuery;
 import com.zsElectric.boot.business.model.vo.ChargeOrderInfoVO;
+import com.zsElectric.boot.business.model.vo.UserVehicleVO;
 import com.zsElectric.boot.business.model.vo.applet.AppChargeVO;
 import com.zsElectric.boot.business.model.vo.applet.AppUserInfoVO;
 import com.zsElectric.boot.business.service.AppletHomeService;
@@ -34,7 +35,9 @@ import com.zsElectric.boot.common.constant.SystemConstants;
 import com.zsElectric.boot.core.exception.BusinessException;
 import com.zsElectric.boot.security.util.SecurityUtils;
 import com.zsElectric.boot.system.mapper.UserMapper;
+import com.zsElectric.boot.system.model.entity.DictItem;
 import com.zsElectric.boot.system.model.entity.User;
+import com.zsElectric.boot.system.service.DictItemService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -91,6 +94,8 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
 
     private final UserMapper userMapper;
 
+    private final UserVehicleMapper userVehicleMapper;
+
     private final ThirdPartyChargeStatusMapper chargeStatusMapper;
     private final ThirdPartyConnectorInfoMapper connectorInfoMapper;
     private final ThirdPartyStationInfoMapper thirdPartyStationInfoMapper;
@@ -100,6 +105,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
     private final DiscountsActivityMapper discountsActivityMapper;
     private final ThirdPartyApiLogMapper thirdPartyApiLogMapper;
     private final ObjectMapper objectMapper;
+    private final DictItemService dictItemService;
 
     //充电订单号前缀
     private final String ORDER_NO_PREFIX = "CD";
@@ -214,6 +220,21 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
             if (count > 0){
                 throw new BusinessException("您有正在进行中的订单,请先停止充电");
             }
+
+            //校验用户余额是否满足起充价(防止充值后立即退款绕过前端校验)
+            DictItem upRecharge = dictItemService.getOne(Wrappers.<DictItem>lambdaQuery()
+                    .eq(DictItem::getDictCode, "up_recharge")
+                    .last("limit 1")
+            );
+            if (ObjectUtil.isNotEmpty(upRecharge)) {
+                BigDecimal chargeFee = new BigDecimal(upRecharge.getValue());
+                UserAccount userAccount = userAccountService.getOne(Wrappers.lambdaQuery(UserAccount.class)
+                        .eq(UserAccount::getUserId, userId)
+                        .last("limit 1"));
+                if (userAccount == null || userAccount.getBalance().compareTo(chargeFee) < 0) {
+                    throw new BusinessException("用户余额低于起充值 " + chargeFee + " 元,请前往充值后再充电!");
+                }
+            }
             //生成系统充电订单号及互联互通充电订单号 startChargeSeq equipAuthSeq (格式"运营商ID+唯一编号")
             String chargeOrderNo = generateNo(ORDER_NO_PREFIX, userId);
             String seq = ConnectivityConstants.OPERATOR_ID + chargeOrderNo;
@@ -275,6 +296,20 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
                     //预支付金额
                     .setChargingAmt(preAmt.toString())
             ;
+
+            //车牌号
+            if(ObjectUtil.isNotEmpty(formData.getPlateNum())) {
+                chargeOrderInfo.setPlateNum(formData.getPlateNum());
+                requestDTO.setPlateNum(formData.getPlateNum());
+            }else {
+                //获取当前用户的默认车牌号
+                UserVehicle userVehicle =
+                        userVehicleMapper.selectOne(Wrappers.lambdaQuery(UserVehicle.class).eq(UserVehicle::getUserId, userId).eq(UserVehicle::getIsDefault, SystemConstants.STATUS_ONE).last("limit 1"));
+                if (ObjectUtil.isNotEmpty(userVehicle)) {
+                    requestDTO.setPlateNum(userVehicle.getLicensePlate());
+                }
+            }
+
             StartChargingResponseVO startChargingResponseVO = chargingBusinessService.startCharging(requestDTO);
             if (!Objects.equals(startChargingResponseVO.getSuccStat(), SystemConstants.STATUS_ZERO)) {
                 throw new BusinessException(startChargingResponseVO.getFailReasonMsg());

+ 68 - 0
src/main/java/com/zsElectric/boot/business/service/impl/UserAccountServiceImpl.java

@@ -5,8 +5,10 @@ import com.zsElectric.boot.business.mapper.UserAccountLogMapper;
 import com.zsElectric.boot.business.mapper.UserInfoIntegralLogMapper;
 import com.zsElectric.boot.business.model.entity.UserAccountLog;
 import com.zsElectric.boot.business.model.entity.UserInfoIntegralLog;
+import com.zsElectric.boot.business.model.vo.RefundCompensationVO;
 import com.zsElectric.boot.common.constant.SystemConstants;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -18,8 +20,10 @@ import com.zsElectric.boot.business.model.form.UserAccountForm;
 import com.zsElectric.boot.business.model.query.UserAccountQuery;
 import com.zsElectric.boot.business.model.vo.UserAccountVO;
 import com.zsElectric.boot.business.converter.UserAccountConverter;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -33,6 +37,7 @@ import cn.hutool.core.util.StrUtil;
  * @author zsElectric
  * @since 2025-12-12 10:20
  */
+@Slf4j
 @Service
 @RequiredArgsConstructor
 public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {
@@ -201,4 +206,67 @@ public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserA
         return userAccount;
     }
 
+    /**
+     * 查询负余额退款异常用户列表
+     * 限定2026-01-01之后的数据
+     *
+     * @return 异常用户列表
+     */
+    @Override
+    public List<RefundCompensationVO> listNegativeBalanceRefundUsers() {
+        LocalDateTime startTime = LocalDateTime.of(2026, 1, 1, 0, 0, 0);
+        List<RefundCompensationVO> list = this.baseMapper.listNegativeBalanceRefundUsers(startTime);
+        log.info("查询负余额退款异常用户,开始时间: {}, 查询到 {} 条记录", startTime, list.size());
+        return list;
+    }
+
+    /**
+     * 执行补偿操作
+     * 将负余额退款异常用户的余额进行扣减补偿
+     * 限定2026-01-01之后的数据
+     *
+     * @return 受影响的用户数
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int executeCompensation() {
+        LocalDateTime startTime = LocalDateTime.of(2026, 1, 1, 0, 0, 0);
+        
+        // 先查询异常用户列表,用于记录日志
+        List<RefundCompensationVO> compensationList = this.baseMapper.listNegativeBalanceRefundUsers(startTime);
+        
+        if (compensationList.isEmpty()) {
+            log.info("没有需要补偿的异常用户");
+            return 0;
+        }
+        
+        log.info("开始执行补偿操作,开始时间: {}, 待补偿用户数: {}", startTime, compensationList.size());
+        
+        // 记录补偿前的用户信息
+        for (RefundCompensationVO vo : compensationList) {
+            log.info("补偿前 - 用户ID: {}, 退款前余额: {}, 退款后余额: {}, 补偿金额: {}, 当前余额: {}",
+                    vo.getUserId(), vo.getRefundBeforeBalance(), vo.getRefundAfterBalance(),
+                    vo.getCompensationAmount(), vo.getCurrentBalance());
+        }
+        
+        // 执行补偿操作
+        int affectedRows = this.baseMapper.executeCompensation(startTime);
+        
+        log.info("补偿操作完成,受影响的用户数: {}", affectedRows);
+        
+        // 记录补偿日志
+        for (RefundCompensationVO vo : compensationList) {
+            UserAccountLog accountLog = new UserAccountLog();
+            accountLog.setUserId(vo.getUserId());
+            accountLog.setChangeType(SystemConstants.CHANGE_TYPE_REDUCE);
+            accountLog.setChangeNote("异常退款补偿扣减(2026-01-01后)");
+            accountLog.setBeforeBalance(vo.getCurrentBalance());
+            accountLog.setChangeBalance(vo.getCurrentBalance().subtract(vo.getCompensationAmount()));
+            accountLog.setAccountType(SystemConstants.ACCOUNT_TYPE_PERSONAL);
+            userAccountLogMapper.insert(accountLog);
+        }
+        
+        return affectedRows;
+    }
+
 }

+ 14 - 4
src/main/java/com/zsElectric/boot/business/service/impl/UserVehicleServiceImpl.java

@@ -17,6 +17,8 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import com.zsElectric.boot.core.exception.BusinessException;
+
 import java.util.Arrays;
 import java.util.List;
 
@@ -43,8 +45,6 @@ public class UserVehicleServiceImpl extends ServiceImpl<UserVehicleMapper, UserV
         LambdaQueryWrapper<UserVehicle> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(queryParams.getUserId() != null, UserVehicle::getUserId, queryParams.getUserId())
                 .like(StrUtil.isNotBlank(queryParams.getLicensePlate()), UserVehicle::getLicensePlate, queryParams.getLicensePlate())
-                .like(StrUtil.isNotBlank(queryParams.getBrand()), UserVehicle::getBrand, queryParams.getBrand())
-                .eq(queryParams.getVehicleType() != null, UserVehicle::getVehicleType, queryParams.getVehicleType())
                 .orderByDesc(UserVehicle::getIsDefault)
                 .orderByDesc(UserVehicle::getCreateTime);
 
@@ -67,6 +67,11 @@ public class UserVehicleServiceImpl extends ServiceImpl<UserVehicleMapper, UserV
         return userVehicleConverter.toForm(entity);
     }
 
+    /**
+     * 用户最多可绑定的车辆数量
+     */
+    private static final int MAX_VEHICLE_COUNT = 5;
+
     /**
      * 新增用户车辆
      *
@@ -78,14 +83,19 @@ public class UserVehicleServiceImpl extends ServiceImpl<UserVehicleMapper, UserV
     public boolean saveUserVehicle(UserVehicleForm formData) {
         UserVehicle entity = userVehicleConverter.toEntity(formData);
         
+        // 校验用户绑定车辆数量是否超过限制
+        long count = this.count(new LambdaQueryWrapper<UserVehicle>()
+                .eq(UserVehicle::getUserId, entity.getUserId()));
+        if (count >= MAX_VEHICLE_COUNT) {
+            throw new BusinessException("用户最多只能绑定{}个车牌", MAX_VEHICLE_COUNT);
+        }
+        
         // 如果设置为默认车辆,先清除该用户其他车辆的默认状态
         if (entity.getIsDefault() != null && entity.getIsDefault() == 1) {
             this.baseMapper.clearUserDefaultVehicle(entity.getUserId());
         }
         
         // 如果是用户的第一辆车,自动设为默认
-        long count = this.count(new LambdaQueryWrapper<UserVehicle>()
-                .eq(UserVehicle::getUserId, entity.getUserId()));
         if (count == 0) {
             entity.setIsDefault(1);
         }

+ 1 - 1
src/main/java/com/zsElectric/boot/charging/controller/LinkDataController.java

@@ -79,7 +79,7 @@ public class LinkDataController {
             }
 
             //判断运营商ID与密钥是否正确
-            if (!queryTokenRequestParms.getOperatorID().equals(ConnectivityConstants.PLATFORM_OPERATOR_ID) || !queryTokenRequestParms.getOperatorSecret().equals(ConnectivityConstants.PLATFORM_OPERATOR_SECRET)) {
+            if (!queryTokenRequestParms.getOperatorID().equals(ConnectivityConstants.PLATFORM_OPERATOR_ID) || !queryTokenRequestParms.getOperatorSecret().equals(ConnectivityConstants.OPERATOR_SECRET)) {
                 responseParmsEntity.setRet(4004);
                 responseParmsEntity.setMsg("OperatorID或OperatorSecret错误!");
                 responseParmsEntity.setData("");

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

@@ -171,11 +171,11 @@ public class ChargingBusinessServiceImpl implements ChargingBusinessService {
     @Override
     public StartChargingResponseVO startCharging(StartChargingRequestDTO requestDTO) throws JsonProcessingException {
         Map<String, Object> stringObjectMap = BeanUtil.beanToMap(requestDTO);
-        log.info("设备接口状态查询请求参数:{}", stringObjectMap);
+        log.info("启动充电请求参数:{}", stringObjectMap);
         JsonNode jsonObject = chargingUtil.chargingRequest(ConnectivityConstants.TEST_DOMAIN + ConnectivityConstants.QUERY_START_CHARGE, stringObjectMap, true);
-        log.info("设备接口状态查询返回结果:{}", jsonObject);
+        log.info("启动充电返回结果:{}", jsonObject);
         JsonNode responseDecode = chargingUtil.responseDecode(jsonObject);
-        log.info("设备接口状态查询返回结果解密后:{}", responseDecode);
+        log.info("启动充电返回结果解密后:{}", responseDecode);
         return objectMapper.readValue(responseDecode.toString(), StartChargingResponseVO.class);
     }
 

+ 61 - 57
src/main/java/com/zsElectric/boot/common/util/AESCryptoUtils.java

@@ -148,65 +148,69 @@ public class AESCryptoUtils {
      */
     public static void main(String[] args) throws Exception {
 
-        BigDecimal a = new BigDecimal("6.099");
-        BigDecimal b = new BigDecimal("0.72");
-
-        System.out.println(a.multiply(b));
-        System.out.println((a.multiply(b)).setScale(2, BigDecimal.ROUND_HALF_UP));
+//        BigDecimal a = new BigDecimal("6.099");
+//        BigDecimal b = new BigDecimal("0.72");
+//
+//        System.out.println(a.multiply(b));
+//        System.out.println((a.multiply(b)).setScale(2, BigDecimal.ROUND_HALF_UP));
 
 
-//        try {
-//            // 测试数据
+        try {
+            // 测试数据
 //            String originalData = "{\"OperatorID\":\"MA6DP6BE7\",\"SuccStat\":0,\"AccessToken\":\"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJNQTZEUDZCRTciLCJpYXQiOjE3NjQ1ODE0NzQsImV4cCI6MTc2NDU4ODY3NH0.mfXp_Wly4QiVYILK6hNSOEGSU7XejlYZRing0CRc8MQW1xuUxrQQwOxeCAwI_Bnay5WzqaYYFQeVuNeignxAuQ\",\"TokenAvailableTime\":6885,\"FailReason\":0}";
-//            String key = DATA_SECRET;   // 16字节密钥
-//            String iv = DATA_SECRET_IV;   // 16字节初始化向量
-//
-//            System.out.println("=== AES-128-CBC-PKCS5Padding 加解密测试 ===");
-//            System.out.println("原始数据: " + originalData);
-//            System.out.println("密钥: " + key);
-//            System.out.println("初始化向量: " + iv);
-//
-//            // 加密
-//            long startTime = System.currentTimeMillis();
-//            String encryptedData = encrypt(originalData, key, iv);
-//            long encryptTime = System.currentTimeMillis() - startTime;
-//            System.out.println("加密结果: " + encryptedData);
-//            System.out.println("加密耗时: " + encryptTime + "ms");
-//
-//            String data = "KYWxoKWK3w8a8867aXCha+tgVE2cbZ4eR1Dc1YExri06DfZWBpUMAzlhY7rWR5SeU+xCVOauk4F7MxCJLN+5aJCBENCOAZtUksMM7VgsOz0=";
-//            System.out.println("测试的加密数据:"+data);
-//
-//            String key1 = DATA_SECRET;   // 16字节密钥
-//            String iv1 = DATA_SECRET_IV;
-//            System.out.println("密钥: " + key);
-//            System.out.println("初始化向量: " + iv);
-//            String string = decrypt(data, key1, iv1);
-//            System.out.println("测试的解密:"+string);
-//            // 解密
-//            startTime = System.currentTimeMillis();
-//            String decryptedData = decrypt(encryptedData, key, iv);
-//            long decryptTime = System.currentTimeMillis() - startTime;
-//            System.out.println("解密结果: " + decryptedData);
-//            System.out.println("解密耗时: " + decryptTime + "ms");
-//
-//            // 验证加解密一致性
-//            boolean isSuccess = originalData.equals(decryptedData);
-//            System.out.println("加解密验证: " + (isSuccess ? "成功" : "失败"));
-//
-//            // 测试随机密钥生成
-//            System.out.println("\n=== 随机密钥生成测试 ===");
-//            String randomKey = generateRandomKey();
-//            String randomIV = generateRandomIV();
-//            System.out.println("随机密钥: " + randomKey);
-//            System.out.println("随机IV: " + randomIV);
-//
-//            // 使用随机密钥进行加解密测试
-//            String testEncrypted = encrypt("测试数据", randomKey, randomIV);
-//            String testDecrypted = decrypt(testEncrypted, randomKey, randomIV);
-//            System.out.println("随机密钥加解密测试: " + ("测试数据".equals(testDecrypted) ? "成功" : "失败"));
-//
-//        } catch (Exception e) {
-//            e.printStackTrace();
-//        }
+            String originalData = "{\n" +
+                    "  \"OperatorID\": \"MA6HWN8L3\",\n" +
+                    "  \"OperatorSecret\": \"vfkh4k740lfg88kq\"\n" +
+                    "}";
+            String key = DATA_SECRET;   // 16字节密钥
+            String iv = DATA_SECRET_IV;   // 16字节初始化向量
+
+            System.out.println("=== AES-128-CBC-PKCS5Padding 加解密测试 ===");
+            System.out.println("原始数据: " + originalData);
+            System.out.println("密钥: " + key);
+            System.out.println("初始化向量: " + iv);
+
+            // 加密
+            long startTime = System.currentTimeMillis();
+            String encryptedData = encrypt(originalData, key, iv);
+            long encryptTime = System.currentTimeMillis() - startTime;
+            System.out.println("加密结果: " + encryptedData);
+            System.out.println("加密耗时: " + encryptTime + "ms");
+
+            String data = "KYWxoKWK3w8a8867aXCha+tgVE2cbZ4eR1Dc1YExri06DfZWBpUMAzlhY7rWR5SeU+xCVOauk4F7MxCJLN+5aJCBENCOAZtUksMM7VgsOz0=";
+            System.out.println("测试的加密数据:"+data);
+
+            String key1 = DATA_SECRET;   // 16字节密钥
+            String iv1 = DATA_SECRET_IV;
+            System.out.println("密钥: " + key);
+            System.out.println("初始化向量: " + iv);
+            String string = decrypt(data, key1, iv1);
+            System.out.println("测试的解密:"+string);
+            // 解密
+            startTime = System.currentTimeMillis();
+            String decryptedData = decrypt(encryptedData, key, iv);
+            long decryptTime = System.currentTimeMillis() - startTime;
+            System.out.println("解密结果: " + decryptedData);
+            System.out.println("解密耗时: " + decryptTime + "ms");
+
+            // 验证加解密一致性
+            boolean isSuccess = originalData.equals(decryptedData);
+            System.out.println("加解密验证: " + (isSuccess ? "成功" : "失败"));
+
+            // 测试随机密钥生成
+            System.out.println("\n=== 随机密钥生成测试 ===");
+            String randomKey = generateRandomKey();
+            String randomIV = generateRandomIV();
+            System.out.println("随机密钥: " + randomKey);
+            System.out.println("随机IV: " + randomIV);
+
+            // 使用随机密钥进行加解密测试
+            String testEncrypted = encrypt("测试数据", randomKey, randomIV);
+            String testDecrypted = decrypt(testEncrypted, randomKey, randomIV);
+            System.out.println("随机密钥加解密测试: " + ("测试数据".equals(testDecrypted) ? "成功" : "失败"));
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
     }
 }

+ 1 - 1
src/main/java/com/zsElectric/boot/common/util/HmacMD5Util.java

@@ -211,7 +211,7 @@ public class HmacMD5Util {
     public static void main(String[] args) {
         try {
             // 测试数据
-            String data = "KYWxoKWK3w8a8867aXCha+tgVE2cbZ4eR1Dc1YExri06DfZWBpUMAzlhY7rWR5SeU+xCVOauk4F7MxCJLN+5aJCBENCOAZtUksMM7VgsOz0=";
+            String data = "5vhSOLJq5SOWspDkp1SEcWqRJ4LQQFc1iIxJb9EzdkSr5GPcAdTL+5/2pmlknaKx/2J3C/wpou5f0XMMkzxixQ==";
             String key = "U9xFXjjdYAycq30C";
             
             System.out.println("=== HMAC-MD5签名测试 ===");

+ 2 - 2
src/main/java/com/zsElectric/boot/common/util/electric/queryToken/JwtTokenUtil.java

@@ -39,7 +39,7 @@ public class JwtTokenUtil {
     private static final String REDIS_OPERATOR_TOKENS_PREFIX = "third_party:operator_tokens:";
 
     public JwtTokenUtil(@Value("${third-party.jwt.secret:vgct1TZ4ZikKjaaeIiq3LUwIvpmcgYa6}") String secret,
-                       @Value("${third-party.jwt.expire:7200}") Long expireSeconds,
+                       @Value("${third-party.jwt.expire:9600}") Long expireSeconds,
                        RedisTemplate<String, Object> redisTemplate) {
         // 生成安全的密钥
         this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
@@ -166,7 +166,7 @@ public class JwtTokenUtil {
     }
 
     /**
-     * 查询操作员现有的有效Token
+     * 查询运营商现有的有效Token
      * 如果Token存在且剩余过期时间大于1分钟,则返回该Token
      */
     private String getExistingValidToken(String operatorId) {

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

@@ -1,12 +1,14 @@
 package com.zsElectric.boot.common.util.electric.queryToken;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zsElectric.boot.thirdParty.model.ThirdPartyResponse;
 import jakarta.annotation.Resource;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.http.MediaType;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
@@ -16,6 +18,7 @@ import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -26,8 +29,9 @@ public class ThirdPartyJwtAuthFilter extends OncePerRequestFilter {
 
     @Resource
     private JwtTokenUtil jwtTokenUtil; // 你的JWT工具类
-    @Resource
-    private JwtAuthenticationEntryPoint authenticationEntryPoint; // 统一认证异常处理
+    
+    private static final int RET_TOKEN_ERROR = 4002; // token错误
+    private final ObjectMapper objectMapper = new ObjectMapper();
 
     // 定义你的第三方接口路径模式,例如 /api/third-party/**
     private final List<String> thirdPartyApiPaths = Arrays.asList(
@@ -59,9 +63,7 @@ public class ThirdPartyJwtAuthFilter extends OncePerRequestFilter {
             
             if (token == null) {
                 log.error("Token缺失,请求URI: {}", requestUri);
-                // Token缺失,通过AuthenticationEntryPoint返回统一错误格式
-                authenticationEntryPoint.commence(request, response, 
-                    new AuthenticationServiceException("Missing or invalid Bearer token(缺少Authorization头)"));
+                writeThirdPartyErrorResponse(response, "Missing or invalid Bearer token(缺少Authorization头)");
                 return; // 重要:直接返回,不再执行过滤链后续操作
             }
             
@@ -83,14 +85,12 @@ public class ThirdPartyJwtAuthFilter extends OncePerRequestFilter {
                 } else {
                     log.error("Token验证失败,Token: {}...", token.substring(0, Math.min(20, token.length())));
                     // Token无效
-                    authenticationEntryPoint.commence(request, response, 
-                        new AuthenticationServiceException("Invalid token(token无效)"));
+                    writeThirdPartyErrorResponse(response, "Invalid token(token无效)");
                     return;
                 }
             } catch (Exception e) { // 捕获JWT解析等特定异常
                 log.error("Token验证异常: {}", e.getMessage(), e);
-                authenticationEntryPoint.commence(request, response, 
-                    new AuthenticationServiceException("Token validation failed(token验证失败): " + e.getMessage()));
+                writeThirdPartyErrorResponse(response, "Token validation failed(token验证失败): " + e.getMessage());
                 return;
             }
         } else {
@@ -111,4 +111,16 @@ public class ThirdPartyJwtAuthFilter extends OncePerRequestFilter {
         }
         return null;
     }
+    
+    /**
+     * 写入第三方格式的错误响应,返回code=4002(token错误)
+     */
+    private void writeThirdPartyErrorResponse(HttpServletResponse response, String msg) throws IOException {
+        ThirdPartyResponse errorResponse = ThirdPartyResponse.error(RET_TOKEN_ERROR, msg, "");
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
+        response.getWriter().flush();
+    }
 }

+ 133 - 0
src/main/java/com/zsElectric/boot/sdk/SdkExample.java

@@ -0,0 +1,133 @@
+package com.zsElectric.boot.sdk;
+
+import com.zsElectric.boot.sdk.model.SdkResult;
+
+import java.util.Map;
+
+/**
+ * SDK使用示例
+ *
+ * @author wzq
+ */
+public class SdkExample {
+
+    public static void main(String[] args) {
+        // 1. 创建SDK配置
+        // baseUrl只填写域名和端口,SDK内部会自动拼接 /third-party/v1
+        ZsElectricConfig config = ZsElectricConfig.builder()
+                .baseUrl("http://127.0.0.1:8989/third_party/v1")    // 服务端地址
+                .operatorId("12345qwer")               // 运营商ID
+                .operatorSecret("vfkh4k740lfg88kq")    // 运营商密钥
+                .dataSecret("bbkwy062pzyjhqmg")        // 数据加密密钥 (16位)
+                .dataSecretIV("xgbzfgwz6ki2gm5j")      // 数据加密IV (16位)
+                .sigSecret("8h9sf4zd5cbtlu8x")         // 签名密钥
+                .connectTimeout(10000)                  // 连接超时(毫秒)
+                .readTimeout(30000)                     // 读取超时(毫秒)
+                .build();
+
+        // 2. 创建SDK客户端
+        ZsElectricClient client = new ZsElectricClient(config);
+
+        // 3. 获取Token (首次调用其他接口前会自动获取,也可以手动获取)
+        System.out.println("========== 获取Token ==========");
+        SdkResult<Map<String, Object>> tokenResult = client.queryToken();
+        if (tokenResult.isSuccess()) {
+            System.out.println("Token获取成功: " + tokenResult.getData());
+        } else {
+            System.out.println("Token获取失败: " + tokenResult.getMessage());
+            return;
+        }
+
+        // 4. 查询充值档位列表
+        System.out.println("\n========== 查询充值档位列表 ==========");
+        SdkResult<Map<String, Object>> levelResult = client.queryRechargeLevelPage(1, 10);
+        if (levelResult.isSuccess()) {
+            System.out.println("充值档位列表: " + levelResult.getData());
+        } else {
+            System.out.println("查询失败: " + levelResult.getMessage());
+        }
+
+        // 5. 根据手机号获取用户信息
+        System.out.println("\n========== 查询用户信息 ==========");
+        SdkResult<Map<String, Object>> userResult = client.queryUserInfo("13800138000");
+        if (userResult.isSuccess()) {
+            System.out.println("用户信息: " + userResult.getData());
+        } else {
+            System.out.println("查询失败: " + userResult.getMessage());
+        }
+
+        // 6. 查询充电站列表
+        System.out.println("\n========== 查询充电站列表 ==========");
+        SdkResult<Map<String, Object>> stationResult = client.queryChargeStationList(1, 10, 30.5728, 104.0668);
+        if (stationResult.isSuccess()) {
+            System.out.println("充电站列表: " + stationResult.getData());
+        } else {
+            System.out.println("查询失败: " + stationResult.getMessage());
+        }
+
+        // 7. 查询充电站详情
+        System.out.println("\n========== 查询充电站详情 ==========");
+        SdkResult<Map<String, Object>> stationDetailResult = client.queryChargeStationDetail(1L);
+        if (stationDetailResult.isSuccess()) {
+            System.out.println("充电站详情: " + stationDetailResult.getData());
+        } else {
+            System.out.println("查询失败: " + stationDetailResult.getMessage());
+        }
+
+        // 8. 查询充电终端详情
+        System.out.println("\n========== 查询充电终端详情 ==========");
+        SdkResult<Map<String, Object>> deviceResult = client.queryChargeDeviceDetail(1L);
+        if (deviceResult.isSuccess()) {
+            System.out.println("充电终端详情: " + deviceResult.getData());
+        } else {
+            System.out.println("查询失败: " + deviceResult.getMessage());
+        }
+
+        // 9. 充点券购买
+        System.out.println("\n========== 充点券购买 ==========");
+        SdkResult<Map<String, Object>> payResult = client.chargeOrderPay("13800138000", 1L, "OUT_ORDER_001");
+        if (payResult.isSuccess()) {
+            System.out.println("购买成功: " + payResult.getData());
+        } else {
+            System.out.println("购买失败: " + payResult.getMessage());
+        }
+
+        // 10. 启动充电
+        System.out.println("\n========== 启动充电 ==========");
+        SdkResult<Map<String, Object>> invokeResult = client.invokeCharge("13800138000", 1L, "CHARGE_ORDER_001");
+        if (invokeResult.isSuccess()) {
+            System.out.println("启动充电成功: " + invokeResult.getData());
+        } else {
+            System.out.println("启动充电失败: " + invokeResult.getMessage());
+        }
+
+        // 11. 停止充电
+        System.out.println("\n========== 停止充电 ==========");
+        SdkResult<Map<String, Object>> stopResult = client.stopCharge("ORDER_NO_001");
+        if (stopResult.isSuccess()) {
+            System.out.println("停止充电成功: " + stopResult.getData());
+        } else {
+            System.out.println("停止充电失败: " + stopResult.getMessage());
+        }
+
+        // 12. 查询充电订单列表
+        System.out.println("\n========== 查询充电订单列表 ==========");
+        SdkResult<Map<String, Object>> orderListResult = client.queryChargeOrderList("13800138000", 1, 10);
+        if (orderListResult.isSuccess()) {
+            System.out.println("订单列表: " + orderListResult.getData());
+        } else {
+            System.out.println("查询失败: " + orderListResult.getMessage());
+        }
+
+        // 13. 查询充电订单详情
+        System.out.println("\n========== 查询充电订单详情 ==========");
+        SdkResult<Map<String, Object>> orderInfoResult = client.queryChargeOrderInfo("ORDER_NO_001");
+        if (orderInfoResult.isSuccess()) {
+            System.out.println("订单详情: " + orderInfoResult.getData());
+        } else {
+            System.out.println("查询失败: " + orderInfoResult.getMessage());
+        }
+
+        System.out.println("\n========== 示例执行完成 ==========");
+    }
+}

+ 369 - 0
src/main/java/com/zsElectric/boot/sdk/ZsElectricClient.java

@@ -0,0 +1,369 @@
+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;
+import com.zsElectric.boot.sdk.model.SdkResponse;
+import com.zsElectric.boot.sdk.model.SdkResult;
+import com.zsElectric.boot.sdk.util.SdkCryptoUtil;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 众水电力SDK客户端
+ * 提供第三方调用众水电力平台API的能力
+ *
+ * @author wzq
+ */
+public class ZsElectricClient {
+
+    private static final String API_PREFIX = "/third-party/v1";
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+    private final ZsElectricConfig config;
+    private final ObjectMapper objectMapper;
+    private final AtomicInteger seqCounter = new AtomicInteger(1);
+    private final String apiBaseUrl;
+
+    /**
+     * 缓存的AccessToken
+     */
+    private volatile String accessToken;
+    private volatile long tokenExpireTime = 0;
+
+    public ZsElectricClient(ZsElectricConfig config) {
+        this.config = config;
+        this.objectMapper = new ObjectMapper();
+        this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        
+        // 智能处理baseUrl:如果已包含API路径前缀,则不再拼接
+        String base = config.getBaseUrl();
+        if (base.contains("/third_party/") || base.contains("/third_party/")) {
+            // 统一路径分隔符为连字符格式
+            this.apiBaseUrl = base.replace("/third_party/", "/third_party/");
+        } else {
+            this.apiBaseUrl = base + API_PREFIX;
+        }
+    }
+
+    // ==================== 公开API方法 ====================
+
+    /**
+     * 获取Token
+     * Token作为全局唯一凭证,调用其他接口时需要携带
+     *
+     * @return Token信息
+     */
+    public SdkResult<Map<String, Object>> queryToken() {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("operatorId", config.getOperatorId());
+        requestData.put("operatorSecret", config.getOperatorSecret());
+
+        SdkResult<Map<String, Object>> result = executeRequest("/query_token", requestData, false);
+
+        // 缓存token
+        if (result.isSuccess() && result.getData() != null) {
+            Map<String, Object> data = result.getData();
+            Object token = data.get("accessToken");
+            Object expireTime = data.get("tokenExpirationTime");
+            if (token != null) {
+                this.accessToken = token.toString();
+                if (expireTime != null) {
+                    // 提前60秒过期,避免边界问题
+                    this.tokenExpireTime = System.currentTimeMillis() + (Long.parseLong(expireTime.toString()) - 60) * 1000;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 获取充值档位信息分页列表
+     *
+     * @param pageNum  页码
+     * @param pageSize 每页数量
+     * @return 充值档位列表
+     */
+    public SdkResult<Map<String, Object>> queryRechargeLevelPage(int pageNum, int pageSize) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("pageNum", pageNum);
+        requestData.put("pageSize", pageSize);
+        return executeRequest("/query_recharge_level_page", requestData, true);
+    }
+
+    /**
+     * 根据手机号获取用户信息
+     *
+     * @param mobile 手机号
+     * @return 用户信息
+     */
+    public SdkResult<Map<String, Object>> queryUserInfo(String mobile) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("mobile", mobile);
+        return executeRequest("/query_user_info", requestData, true);
+    }
+
+    /**
+     * 充点券购买
+     *
+     * @param mobile      用户手机号
+     * @param levelId     档位ID
+     * @param outOrderNo  外部订单号
+     * @return 购买结果
+     */
+    public SdkResult<Map<String, Object>> chargeOrderPay(String mobile, Long levelId, String outOrderNo) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("mobile", mobile);
+        requestData.put("levelId", levelId);
+        requestData.put("outOrderNo", outOrderNo);
+        return executeRequest("/charge_order_pay", requestData, true);
+    }
+
+    /**
+     * 获取充电站列表
+     *
+     * @param pageNum   页码
+     * @param pageSize  每页数量
+     * @param latitude  纬度 (可选)
+     * @param longitude 经度 (可选)
+     * @return 充电站列表
+     */
+    public SdkResult<Map<String, Object>> queryChargeStationList(int pageNum, int pageSize, Double latitude, Double longitude) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("pageNum", pageNum);
+        requestData.put("pageSize", pageSize);
+        if (latitude != null) {
+            requestData.put("latitude", latitude);
+        }
+        if (longitude != null) {
+            requestData.put("longitude", longitude);
+        }
+        return executeRequest("/query_charge_station_list", requestData, true);
+    }
+
+    /**
+     * 获取充电站详情与充电终端列表
+     *
+     * @param stationId 充电站ID
+     * @return 充电站详情
+     */
+    public SdkResult<Map<String, Object>> queryChargeStationDetail(Long stationId) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("stationId", stationId);
+        return executeRequest("/query_charge_station_detail", requestData, true);
+    }
+
+    /**
+     * 获取充电终端详情
+     *
+     * @param deviceId 设备ID
+     * @return 充电终端详情
+     */
+    public SdkResult<Map<String, Object>> queryChargeDeviceDetail(Long deviceId) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("deviceId", deviceId);
+        return executeRequest("/query_charge_device_detail", requestData, true);
+    }
+
+    /**
+     * 启动充电
+     *
+     * @param mobile      用户手机号
+     * @param connectorId 充电枪ID
+     * @param outOrderNo  外部订单号
+     * @return 启动结果
+     */
+    public SdkResult<Map<String, Object>> invokeCharge(String mobile, Long connectorId, String outOrderNo) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("mobile", mobile);
+        requestData.put("connectorId", connectorId);
+        requestData.put("outOrderNo", outOrderNo);
+        return executeRequest("/invoke_charge", requestData, true);
+    }
+
+    /**
+     * 停止充电
+     *
+     * @param orderNo 充电订单号
+     * @return 停止结果
+     */
+    public SdkResult<Map<String, Object>> stopCharge(String orderNo) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("orderNo", orderNo);
+        return executeRequest("/stop_charge", requestData, true);
+    }
+
+    /**
+     * 查询充电订单列表
+     *
+     * @param mobile    用户手机号 (可选)
+     * @param pageNum   页码
+     * @param pageSize  每页数量
+     * @return 订单列表
+     */
+    public SdkResult<Map<String, Object>> queryChargeOrderList(String mobile, int pageNum, int pageSize) {
+        Map<String, Object> requestData = new HashMap<>();
+        if (mobile != null && !mobile.isEmpty()) {
+            requestData.put("mobile", mobile);
+        }
+        requestData.put("pageNum", pageNum);
+        requestData.put("pageSize", pageSize);
+        return executeRequest("/query_charge_order_list", requestData, true);
+    }
+
+    /**
+     * 查询充电订单详情
+     *
+     * @param orderNo 充电订单号
+     * @return 订单详情
+     */
+    public SdkResult<Map<String, Object>> queryChargeOrderInfo(String orderNo) {
+        Map<String, Object> requestData = new HashMap<>();
+        requestData.put("orderNo", orderNo);
+        return executeRequest("/query_charge_order_info", requestData, true);
+    }
+
+    // ==================== 内部方法 ====================
+
+    /**
+     * 执行API请求
+     *
+     * @param apiPath      API路径
+     * @param requestData  业务数据
+     * @param needToken    是否需要Token
+     * @return 响应结果
+     */
+    @SuppressWarnings("unchecked")
+    private SdkResult<Map<String, Object>> executeRequest(String apiPath, Map<String, Object> requestData, boolean needToken) {
+        try {
+            // 如果需要Token且Token已过期,先刷新
+            if (needToken && isTokenExpired()) {
+                SdkResult<Map<String, Object>> tokenResult = queryToken();
+                if (!tokenResult.isSuccess()) {
+                    return SdkResult.error("获取Token失败: " + tokenResult.getMessage());
+                }
+            }
+
+            // 构建请求
+            String timeStamp = LocalDateTime.now().format(TIME_FORMATTER);
+            String seq = String.format("%04d", seqCounter.getAndIncrement() % 10000);
+
+            // 加密业务数据
+            String jsonData = objectMapper.writeValueAsString(requestData);
+            String encryptedData = SdkCryptoUtil.aesEncrypt(jsonData, config.getDataSecret(), config.getDataSecretIV());
+
+            // 生成签名
+            String sig = SdkCryptoUtil.genRequestSign(config.getOperatorId(), encryptedData, timeStamp, seq, config.getSigSecret());
+
+            // 构建请求体
+            SdkRequest request = new SdkRequest();
+            request.setOperatorId(config.getOperatorId());
+            request.setData(encryptedData);
+            request.setTimeStamp(timeStamp);
+            request.setSeq(seq);
+            request.setSig(sig);
+
+            // 发送HTTP请求
+            String url = apiBaseUrl + apiPath;
+            String requestBody = objectMapper.writeValueAsString(request);
+            String responseBody = sendHttpPost(url, requestBody, needToken ? accessToken : null);
+
+            // 解析响应
+            SdkResponse response = objectMapper.readValue(responseBody, SdkResponse.class);
+
+            // 检查响应是否有效(ret为null可能是服务端返回了非预期格式)
+            if (response.getRet() == null) {
+                return SdkResult.error("服务端响应格式异常,原始响应: " + responseBody);
+            }
+
+            if (!response.isSuccess()) {
+                return SdkResult.fail(response.getRet(), response.getMsg(), response);
+            }
+
+            // 解密响应数据
+            if (response.getData() != null && !response.getData().isEmpty()) {
+                String decryptedData = SdkCryptoUtil.aesDecrypt(response.getData(), config.getDataSecret(), config.getDataSecretIV());
+                Map<String, Object> resultData = objectMapper.readValue(decryptedData, Map.class);
+                return SdkResult.success(resultData, response);
+            }
+
+            return SdkResult.success(new HashMap<>(), response);
+
+        } catch (Exception e) {
+            return SdkResult.error("请求异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 发送HTTP POST请求
+     */
+    private String sendHttpPost(String urlStr, String body, String token) throws Exception {
+        URL url = new URL(urlStr);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setConnectTimeout(config.getConnectTimeout());
+        conn.setReadTimeout(config.getReadTimeout());
+        conn.setDoOutput(true);
+        conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
+
+        if (token != null && !token.isEmpty()) {
+            conn.setRequestProperty("Authorization", "Bearer " + token);
+        }
+
+        try (OutputStream os = conn.getOutputStream()) {
+            os.write(body.getBytes(StandardCharsets.UTF_8));
+            os.flush();
+        }
+
+        int responseCode = conn.getResponseCode();
+        BufferedReader reader;
+        if (responseCode >= 200 && responseCode < 300) {
+            reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
+        } else {
+            reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), StandardCharsets.UTF_8));
+        }
+
+        StringBuilder response = new StringBuilder();
+        String line;
+        while ((line = reader.readLine()) != null) {
+            response.append(line);
+        }
+        reader.close();
+        conn.disconnect();
+
+        return response.toString();
+    }
+
+    /**
+     * 检查Token是否过期
+     */
+    private boolean isTokenExpired() {
+        return accessToken == null || System.currentTimeMillis() >= tokenExpireTime;
+    }
+
+    /**
+     * 手动设置AccessToken(用于外部管理Token的场景)
+     */
+    public void setAccessToken(String accessToken, long expireTimeSeconds) {
+        this.accessToken = accessToken;
+        this.tokenExpireTime = System.currentTimeMillis() + (expireTimeSeconds - 60) * 1000;
+    }
+
+    /**
+     * 获取当前AccessToken
+     */
+    public String getAccessToken() {
+        return accessToken;
+    }
+}

+ 156 - 0
src/main/java/com/zsElectric/boot/sdk/ZsElectricConfig.java

@@ -0,0 +1,156 @@
+package com.zsElectric.boot.sdk;
+
+/**
+ * 众水电力SDK配置类
+ * 
+ * @author wzq
+ */
+public class ZsElectricConfig {
+
+    /**
+     * 服务端地址 (例: https://api.example.com)
+     */
+    private String baseUrl;
+
+    /**
+     * 运营商ID (9位,由数字及大小写字符组成)
+     */
+    private String operatorId;
+
+    /**
+     * 运营商密钥
+     */
+    private String operatorSecret;
+
+    /**
+     * 数据加密密钥 (16位)
+     */
+    private String dataSecret;
+
+    /**
+     * 数据加密IV (16位)
+     */
+    private String dataSecretIV;
+
+    /**
+     * 签名密钥
+     */
+    private String sigSecret;
+
+    /**
+     * 连接超时时间(毫秒),默认10秒
+     */
+    private int connectTimeout = 10000;
+
+    /**
+     * 读取超时时间(毫秒),默认30秒
+     */
+    private int readTimeout = 30000;
+
+    private ZsElectricConfig() {
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+        private final ZsElectricConfig config = new ZsElectricConfig();
+
+        public Builder baseUrl(String baseUrl) {
+            config.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
+            return this;
+        }
+
+        public Builder operatorId(String operatorId) {
+            config.operatorId = operatorId;
+            return this;
+        }
+
+        public Builder operatorSecret(String operatorSecret) {
+            config.operatorSecret = operatorSecret;
+            return this;
+        }
+
+        public Builder dataSecret(String dataSecret) {
+            config.dataSecret = dataSecret;
+            return this;
+        }
+
+        public Builder dataSecretIV(String dataSecretIV) {
+            config.dataSecretIV = dataSecretIV;
+            return this;
+        }
+
+        public Builder sigSecret(String sigSecret) {
+            config.sigSecret = sigSecret;
+            return this;
+        }
+
+        public Builder connectTimeout(int connectTimeout) {
+            config.connectTimeout = connectTimeout;
+            return this;
+        }
+
+        public Builder readTimeout(int readTimeout) {
+            config.readTimeout = readTimeout;
+            return this;
+        }
+
+        public ZsElectricConfig build() {
+            // 参数校验
+            if (config.baseUrl == null || config.baseUrl.isEmpty()) {
+                throw new IllegalArgumentException("baseUrl不能为空");
+            }
+            if (config.operatorId == null || config.operatorId.isEmpty()) {
+                throw new IllegalArgumentException("operatorId不能为空");
+            }
+            if (config.operatorSecret == null || config.operatorSecret.isEmpty()) {
+                throw new IllegalArgumentException("operatorSecret不能为空");
+            }
+            if (config.dataSecret == null || config.dataSecret.length() != 16) {
+                throw new IllegalArgumentException("dataSecret必须为16位");
+            }
+            if (config.dataSecretIV == null || config.dataSecretIV.length() != 16) {
+                throw new IllegalArgumentException("dataSecretIV必须为16位");
+            }
+            if (config.sigSecret == null || config.sigSecret.isEmpty()) {
+                throw new IllegalArgumentException("sigSecret不能为空");
+            }
+            return config;
+        }
+    }
+
+    // Getters
+    public String getBaseUrl() {
+        return baseUrl;
+    }
+
+    public String getOperatorId() {
+        return operatorId;
+    }
+
+    public String getOperatorSecret() {
+        return operatorSecret;
+    }
+
+    public String getDataSecret() {
+        return dataSecret;
+    }
+
+    public String getDataSecretIV() {
+        return dataSecretIV;
+    }
+
+    public String getSigSecret() {
+        return sigSecret;
+    }
+
+    public int getConnectTimeout() {
+        return connectTimeout;
+    }
+
+    public int getReadTimeout() {
+        return readTimeout;
+    }
+}

+ 74 - 0
src/main/java/com/zsElectric/boot/sdk/model/SdkRequest.java

@@ -0,0 +1,74 @@
+package com.zsElectric.boot.sdk.model;
+
+/**
+ * SDK统一请求实体
+ *
+ * @author wzq
+ */
+public class SdkRequest {
+
+    /**
+     * 运营商标识
+     */
+    private String operatorId;
+
+    /**
+     * 加密后的参数内容
+     */
+    private String data;
+
+    /**
+     * 时间戳 (格式: yyyyMMddHHmmss)
+     */
+    private String timeStamp;
+
+    /**
+     * 自增序列 (4位)
+     */
+    private String seq;
+
+    /**
+     * 数字签名
+     */
+    private String sig;
+
+    public String getOperatorId() {
+        return operatorId;
+    }
+
+    public void setOperatorId(String operatorId) {
+        this.operatorId = operatorId;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+
+    public String getTimeStamp() {
+        return timeStamp;
+    }
+
+    public void setTimeStamp(String timeStamp) {
+        this.timeStamp = timeStamp;
+    }
+
+    public String getSeq() {
+        return seq;
+    }
+
+    public void setSeq(String seq) {
+        this.seq = seq;
+    }
+
+    public String getSig() {
+        return sig;
+    }
+
+    public void setSig(String sig) {
+        this.sig = sig;
+    }
+}

+ 87 - 0
src/main/java/com/zsElectric/boot/sdk/model/SdkResponse.java

@@ -0,0 +1,87 @@
+package com.zsElectric.boot.sdk.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * SDK统一响应实体
+ *
+ * @author wzq
+ */
+public class SdkResponse {
+
+    /**
+     * 响应状态码
+     * 0: 请求成功
+     * 500: 系统错误
+     * 4001: 签名错误
+     * 4002: token错误
+     * 4003: 参数不合法
+     * 4004: 请求的业务参数不合法
+     */
+    @JsonProperty("ret")
+    private Integer ret;
+
+    /**
+     * 响应消息
+     */
+    @JsonProperty("msg")
+    private String msg;
+
+    /**
+     * 响应加密数据
+     */
+    @JsonProperty("data")
+    private String data;
+
+    /**
+     * 响应签名
+     */
+    @JsonProperty("sig")
+    private String sig;
+
+    public boolean isSuccess() {
+        return ret != null && ret == 0;
+    }
+
+    public Integer getRet() {
+        return ret;
+    }
+
+    public void setRet(Integer ret) {
+        this.ret = ret;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+
+    public String getSig() {
+        return sig;
+    }
+
+    public void setSig(String sig) {
+        this.sig = sig;
+    }
+
+    @Override
+    public String toString() {
+        return "SdkResponse{" +
+                "ret=" + ret +
+                ", msg='" + msg + '\'' +
+                ", data='" + data + '\'' +
+                ", sig='" + sig + '\'' +
+                '}';
+    }
+}

+ 92 - 0
src/main/java/com/zsElectric/boot/sdk/model/SdkResult.java

@@ -0,0 +1,92 @@
+package com.zsElectric.boot.sdk.model;
+
+/**
+ * SDK业务响应结果包装类
+ *
+ * @param <T> 业务数据类型
+ * @author wzq
+ */
+public class SdkResult<T> {
+
+    /**
+     * 是否成功
+     */
+    private boolean success;
+
+    /**
+     * 原始响应码
+     */
+    private Integer code;
+
+    /**
+     * 响应消息
+     */
+    private String message;
+
+    /**
+     * 解密后的业务数据
+     */
+    private T data;
+
+    /**
+     * 原始响应对象
+     */
+    private SdkResponse rawResponse;
+
+    public static <T> SdkResult<T> success(T data, SdkResponse rawResponse) {
+        SdkResult<T> result = new SdkResult<>();
+        result.success = true;
+        result.code = rawResponse.getRet();
+        result.message = rawResponse.getMsg();
+        result.data = data;
+        result.rawResponse = rawResponse;
+        return result;
+    }
+
+    public static <T> SdkResult<T> fail(int code, String message, SdkResponse rawResponse) {
+        SdkResult<T> result = new SdkResult<>();
+        result.success = false;
+        result.code = code;
+        result.message = message;
+        result.rawResponse = rawResponse;
+        return result;
+    }
+
+    public static <T> SdkResult<T> error(String message) {
+        SdkResult<T> result = new SdkResult<>();
+        result.success = false;
+        result.code = -1;
+        result.message = message;
+        return result;
+    }
+
+    public boolean isSuccess() {
+        return success;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public SdkResponse getRawResponse() {
+        return rawResponse;
+    }
+
+    @Override
+    public String toString() {
+        return "SdkResult{" +
+                "success=" + success +
+                ", code=" + code +
+                ", message='" + message + '\'' +
+                ", data=" + data +
+                '}';
+    }
+}

+ 185 - 0
src/main/java/com/zsElectric/boot/sdk/util/SdkCryptoUtil.java

@@ -0,0 +1,185 @@
+package com.zsElectric.boot.sdk.util;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+/**
+ * SDK加解密工具类
+ * 包含AES-128-CBC加解密和HMAC-MD5签名
+ *
+ * @author wzq
+ */
+public class SdkCryptoUtil {
+
+    // ==================== AES加解密 ====================
+    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final String AES = "AES";
+
+    /**
+     * AES-128-CBC加密
+     *
+     * @param data 待加密的明文
+     * @param key  密钥(必须为16字节)
+     * @param iv   初始化向量(必须为16字节)
+     * @return Base64编码的加密结果
+     */
+    public static String aesEncrypt(String data, String key, String iv) throws Exception {
+        if (data == null || data.isEmpty()) {
+            throw new IllegalArgumentException("加密数据不能为空");
+        }
+        if (key == null || key.length() != 16) {
+            throw new IllegalArgumentException("密钥必须为16位字符");
+        }
+        if (iv == null || iv.length() != 16) {
+            throw new IllegalArgumentException("初始化向量必须为16位字符");
+        }
+
+        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
+        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
+
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+
+        byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
+        return Base64.getEncoder().encodeToString(encryptedBytes);
+    }
+
+    /**
+     * AES-128-CBC解密
+     *
+     * @param encryptedData Base64编码的加密数据
+     * @param key           密钥(必须为16字节)
+     * @param iv            初始化向量(必须为16字节)
+     * @return 解密后的明文
+     */
+    public static String aesDecrypt(String encryptedData, String key, String iv) throws Exception {
+        if (encryptedData == null || encryptedData.isEmpty()) {
+            throw new IllegalArgumentException("解密数据不能为空");
+        }
+        if (key == null || key.length() != 16) {
+            throw new IllegalArgumentException("密钥必须为16位字符");
+        }
+        if (iv == null || iv.length() != 16) {
+            throw new IllegalArgumentException("初始化向量必须为16位字符");
+        }
+
+        SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);
+        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
+
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
+
+        byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
+        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+        return new String(decryptedBytes, StandardCharsets.UTF_8);
+    }
+
+    // ==================== HMAC-MD5签名 ====================
+    private static final int BLOCK_SIZE = 64;
+    private static final byte IPAD = 0x36;
+    private static final byte OPAD = 0x5C;
+
+    /**
+     * 生成请求签名
+     * 签名内容拼接顺序: operatorId + data + timeStamp + seq
+     *
+     * @param operatorId 运营商ID
+     * @param data       加密后的数据
+     * @param timeStamp  时间戳
+     * @param seq        序列号
+     * @param sigSecret  签名密钥
+     * @return 32位大写签名字符串
+     */
+    public static String genRequestSign(String operatorId, String data, String timeStamp, String seq, String sigSecret) 
+            throws NoSuchAlgorithmException {
+        String content = operatorId + data + timeStamp + seq;
+        return hmacMD5Hex(content, sigSecret);
+    }
+
+    /**
+     * 验证响应签名
+     * 签名内容拼接顺序: ret + msg + data
+     *
+     * @param ret       响应码
+     * @param msg       响应消息
+     * @param data      响应数据
+     * @param sig       待验证的签名
+     * @param sigSecret 签名密钥
+     * @return 签名是否有效
+     */
+    public static boolean verifyResponseSign(int ret, String msg, String data, String sig, String sigSecret) 
+            throws NoSuchAlgorithmException {
+        String content = ret + msg + data;
+        String calculatedSig = hmacMD5Hex(content, sigSecret);
+        return calculatedSig.equalsIgnoreCase(sig);
+    }
+
+    /**
+     * HMAC-MD5签名(十六进制字符串形式)
+     */
+    public static String hmacMD5Hex(String data, String key) throws NoSuchAlgorithmException {
+        byte[] signature = hmacMD5(data.getBytes(StandardCharsets.UTF_8), key.getBytes(StandardCharsets.UTF_8));
+        return bytesToHex(signature);
+    }
+
+    /**
+     * HMAC-MD5签名生成 (RFC 2104标准实现)
+     */
+    private static byte[] hmacMD5(byte[] data, byte[] key) throws NoSuchAlgorithmException {
+        byte[] k = prepareKey(key);
+        byte[] iPadXor = xorWithPad(k, IPAD);
+        byte[] firstInput = concatenate(iPadXor, data);
+        byte[] firstHash = md5(firstInput);
+        byte[] oPadXor = xorWithPad(k, OPAD);
+        byte[] secondInput = concatenate(oPadXor, firstHash);
+        return md5(secondInput);
+    }
+
+    private static byte[] prepareKey(byte[] key) throws NoSuchAlgorithmException {
+        byte[] result = new byte[BLOCK_SIZE];
+        if (key.length > BLOCK_SIZE) {
+            byte[] hashedKey = md5(key);
+            System.arraycopy(hashedKey, 0, result, 0, hashedKey.length);
+        } else {
+            System.arraycopy(key, 0, result, 0, key.length);
+        }
+        return result;
+    }
+
+    private static byte[] xorWithPad(byte[] key, byte pad) {
+        byte[] result = new byte[BLOCK_SIZE];
+        for (int i = 0; i < BLOCK_SIZE; i++) {
+            result[i] = (byte) (key[i] ^ pad);
+        }
+        return result;
+    }
+
+    private static byte[] concatenate(byte[] a, byte[] b) {
+        byte[] result = new byte[a.length + b.length];
+        System.arraycopy(a, 0, result, 0, a.length);
+        System.arraycopy(b, 0, result, a.length, b.length);
+        return result;
+    }
+
+    private static byte[] md5(byte[] input) throws NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        return md.digest(input);
+    }
+
+    private static String bytesToHex(byte[] bytes) {
+        StringBuilder hexString = new StringBuilder();
+        for (byte b : bytes) {
+            String hex = Integer.toHexString(0xff & b);
+            if (hex.length() == 1) {
+                hexString.append('0');
+            }
+            hexString.append(hex);
+        }
+        return hexString.toString().toUpperCase();
+    }
+}

+ 15 - 4
src/main/java/com/zsElectric/boot/thirdParty/controller/ThirdPartyController.java

@@ -1,5 +1,6 @@
 package com.zsElectric.boot.thirdParty.controller;
 
+import com.zsElectric.boot.common.annotation.ApiRateLimit;
 import com.zsElectric.boot.common.annotation.Log;
 import com.zsElectric.boot.common.enums.LogModuleEnum;
 import com.zsElectric.boot.common.util.AESCryptoUtils;
@@ -26,7 +27,7 @@ import java.util.Map;
 @RestController
 @RequiredArgsConstructor
 @Tag(name = "第三方接入接口")
-@RequestMapping("/third-party/v1")
+@RequestMapping("/third_party/v1")
 public class ThirdPartyController {
 
     private final ThirdPartyTokenService thirdPartyTokenService;
@@ -51,6 +52,7 @@ public class ThirdPartyController {
     @Operation(summary = "获取Token", description = "第三方平台获取访问令牌")
     @PostMapping("/query_token")
     @Log(value = "第三方获取Token", module = LogModuleEnum.OTHER, params = true, result = true)
+    @ApiRateLimit(prefix = "third_party:query_token", limitType = ApiRateLimit.LimitType.IP, count = 30, time = 60, message = "获取Token请求过于频繁,请稍后再试")
     public ThirdPartyResponse queryToken(@RequestBody ThirdPartyRequest request) {
         log.info("收到query_token请求, operatorId: {}", request.getOperatorId());
         return thirdPartyTokenService.queryToken(request);
@@ -74,10 +76,10 @@ public class ThirdPartyController {
         return thirdPartyTokenService.queryUserInfo(request, authorization);
     }
 
-    @Operation(summary = "充点券购买", description = "第三方充点券购买,需要在Header中携带Authorization")
+    @Operation(summary = "充电券购买", description = "第三方充电券购买,需要在Header中携带Authorization")
     @PostMapping("/charge_order_pay")
-    @Log(value = "第三方充券购买", module = LogModuleEnum.OTHER, params = true, result = true)
-    public ThirdPartyResponse charge_order_pay(
+    @Log(value = "第三方充券购买", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse chargeOrderPay(
             @RequestBody ThirdPartyRequest request,
             @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
         return thirdPartyTokenService.chargeOrderPay(request, authorization);
@@ -119,6 +121,15 @@ public class ThirdPartyController {
         return thirdPartyTokenService.invokeCharge(request, authorization);
     }
 
+    @Operation(summary = "查询充电订单实时费用", description = "第三方查询充电订单实时费用,需要在Header中携带Authorization")
+    @PostMapping("/query_charging_cost")
+    @Log(value = "第三方查询充电订单实时费用", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryChargingCost(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.queryChargingCost(request, authorization);
+    }
+
     @Operation(summary = "停止充电", description = "第三方停止充电,需要在Header中携带Authorization")
     @PostMapping("/stop_charge")
     @Log(value = "第三方停止充电", module = LogModuleEnum.OTHER, params = true, result = true)

+ 1 - 6
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeDeviceDetailRequestData.java

@@ -14,13 +14,8 @@ public class ChargeDeviceDetailRequestData implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    /**
-     * 设备主键ID
-     */
-    private Long id;
-
     /**
      * 设备编码
      */
-    private String equipmentId;
+    private String connectorId;
 }

+ 25 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/QueryChargingCostRequestData.java

@@ -0,0 +1,25 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 查询充电订单实时费用请求参数 (data加密前的内容)
+ *
+ * @author wzq
+ */
+@Data
+public class QueryChargingCostRequestData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 充电订单号
+     */
+    @JsonProperty("chargeOrderNo")
+    private String chargeOrderNo;
+}

+ 166 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/QueryChargingCostResponseData.java

@@ -0,0 +1,166 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 查询充电订单实时费用响应参数 (data加密前的内容)
+ *
+ * @author wzq
+ */
+@Data
+public class QueryChargingCostResponseData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 查询结果 (0-成功 1-失败)
+     */
+    @JsonProperty("result")
+    private Integer result;
+
+    /**
+     * 结果消息
+     */
+    @JsonProperty("message")
+    private String message;
+
+    /**
+     * 充电订单号
+     */
+    @JsonProperty("chargeOrderNo")
+    private String chargeOrderNo;
+
+    /**
+     * 充电站名称
+     */
+    @JsonProperty("stationName")
+    private String stationName;
+
+    /**
+     * 充电终端名称
+     */
+    @JsonProperty("connectorName")
+    private String connectorName;
+
+    /**
+     * 订单状态:1-启动中,2-充电中,3-停止中,4-已结束,5-未知
+     */
+    @JsonProperty("orderStatus")
+    private Integer orderStatus;
+
+    /**
+     * 订单状态描述
+     */
+    @JsonProperty("orderStatusDesc")
+    private String orderStatusDesc;
+
+    /**
+     * 充电时长(秒)
+     */
+    @JsonProperty("chargingDuration")
+    private Long chargingDuration;
+
+    /**
+     * 充电时长描述(如:00:12:35)
+     */
+    @JsonProperty("chargingDurationDesc")
+    private String chargingDurationDesc;
+
+    /**
+     * 累计充电量(度/kWh)
+     */
+    @JsonProperty("totalPower")
+    private BigDecimal totalPower;
+
+    /**
+     * 累计电费(元)
+     */
+    @JsonProperty("elecMoney")
+    private BigDecimal elecMoney;
+
+    /**
+     * 累计服务费(元)
+     */
+    @JsonProperty("serviceMoney")
+    private BigDecimal serviceMoney;
+
+    /**
+     * 累计总金额(元)
+     */
+    @JsonProperty("totalMoney")
+    private BigDecimal totalMoney;
+
+    /**
+     * 电池剩余电量SOC(%)
+     */
+    @JsonProperty("soc")
+    private Integer soc;
+
+    /**
+     * 当前电流(A)
+     */
+    @JsonProperty("current")
+    private BigDecimal current;
+
+    /**
+     * 当前电压(V)
+     */
+    @JsonProperty("voltage")
+    private BigDecimal voltage;
+
+    /**
+     * 当前功率(kW)
+     */
+    @JsonProperty("power")
+    private BigDecimal power;
+
+    /**
+     * 充电开始时间
+     */
+    @JsonProperty("startTime")
+    private String startTime;
+
+    /**
+     * 最后更新时间
+     */
+    @JsonProperty("lastUpdateTime")
+    private String lastUpdateTime;
+
+    /**
+     * 充电接口编码
+     */
+    @JsonProperty("connectorCode")
+    private String connectorCode;
+
+    /**
+     * 充电状态 状态0待启动 1充电中 2结算中 3已完成 5未成功充电
+     */
+    @JsonProperty("status")
+    private String status;
+
+    /**
+     * 创建成功响应数据
+     */
+    public static QueryChargingCostResponseData success() {
+        QueryChargingCostResponseData data = new QueryChargingCostResponseData();
+        data.setResult(0);
+        data.setMessage("查询成功");
+        return data;
+    }
+
+    /**
+     * 创建失败响应数据
+     */
+    public static QueryChargingCostResponseData fail(String message) {
+        QueryChargingCostResponseData data = new QueryChargingCostResponseData();
+        data.setResult(1);
+        data.setMessage(message);
+        return data;
+    }
+}

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

@@ -93,4 +93,13 @@ public interface ThirdPartyTokenService {
      * @return 响应结果
      */
     ThirdPartyResponse queryChargeOrderInfo(ThirdPartyRequest request, String authorization);
+
+    /**
+     * 查询充电订单实时费用
+     *
+     * @param request       第三方请求参数
+     * @param authorization 请求头中的Authorization
+     * @return 响应结果
+     */
+    ThirdPartyResponse queryChargingCost(ThirdPartyRequest request, String authorization);
 }

+ 95 - 2
src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java

@@ -33,6 +33,11 @@ import com.zsElectric.boot.business.service.ChargeOrderInfoService;
 import com.zsElectric.boot.business.model.form.applet.AppInvokeChargeForm;
 import com.zsElectric.boot.business.model.form.applet.AppStopChargeForm;
 import com.zsElectric.boot.business.model.vo.applet.AppChargeVO;
+import com.zsElectric.boot.business.model.vo.applet.AppChargingCostVO;
+import com.zsElectric.boot.business.service.AppletHomeService;
+import com.zsElectric.boot.charging.service.ChargingBusinessService;
+import com.zsElectric.boot.charging.vo.ChargingStatusQueryResponseVO;
+import com.zsElectric.boot.common.constant.ConnectivityConstants;
 import com.zsElectric.boot.common.constant.SystemConstants;
 import com.zsElectric.boot.common.util.AESCryptoUtils;
 import com.zsElectric.boot.common.util.HmacMD5Util;
@@ -75,6 +80,8 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
     private final ThirdPartyEquipmentInfoMapper thirdPartyEquipmentInfoMapper;
     private final ThirdPartyConnectorInfoMapper thirdPartyConnectorInfoMapper;
     private final ChargeOrderInfoService chargeOrderInfoService;
+    private final ChargingBusinessService chargingBusinessService;
+    private final AppletHomeService appletHomeService;
     private final ObjectMapper objectMapper = new ObjectMapper();
 
     /**
@@ -953,7 +960,7 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             log.info("解密后的请求数据: {}", decryptedData);
 
             ChargeDeviceDetailRequestData detailRequest = objectMapper.readValue(decryptedData, ChargeDeviceDetailRequestData.class);
-            if (detailRequest == null || (detailRequest.getId() == null && StrUtil.isBlank(detailRequest.getEquipmentId()))) {
+            if (detailRequest == null || detailRequest.getConnectorId() == null ) {
                 return buildErrorResponse(4003, "请求的业务参数不合法", thirdPartyInfo);
             }
 
@@ -962,7 +969,7 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             String currentTime = LocalTime.now().format(TIME_FORMATTER);
 
             AppletConnectorDetailVO result = thirdPartyConnectorInfoMapper.selectConnectorDetailById(
-                    detailRequest.getEquipmentId(), null, currentTime, null, null
+                    detailRequest.getConnectorId(), null, currentTime, null, null
             );
 
             // 8. 转换为响应数据
@@ -1466,4 +1473,90 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
         }
     }
 
+
+    @Override
+    public ThirdPartyResponse queryChargingCost(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);
+            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);
+            }
+
+            // 5. 解密并解析业务参数
+            String decryptedData = AESCryptoUtils.decrypt(
+                    request.getData(),
+                    thirdPartyInfo.getDataSecret(),
+                    thirdPartyInfo.getDataSecretIV()
+            );
+            log.info("查询充电订单实时费用-解密后的请求数据: {}", decryptedData);
+
+            QueryChargingCostRequestData costRequest = objectMapper.readValue(decryptedData, QueryChargingCostRequestData.class);
+            if (costRequest == null || StrUtil.isBlank(costRequest.getChargeOrderNo())) {
+                return buildErrorResponse(4003, "请求的业务参数不合法,chargeOrderNo不能为空", thirdPartyInfo);
+            }
+
+            // 6. 调用AppletHomeService查询充电订单实时费用
+            AppChargingCostVO chargingCost = appletHomeService.getChargingCost(costRequest.getChargeOrderNo());
+            if (chargingCost == null) {
+                QueryChargingCostResponseData failData = QueryChargingCostResponseData.fail("充电订单不存在或无实时费用数据");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
+
+            // 7. 构建响应数据
+            QueryChargingCostResponseData responseData = QueryChargingCostResponseData.success();
+            responseData.setChargeOrderNo(chargingCost.getChargeOrderNo());
+            responseData.setStationName(chargingCost.getStationName());
+            responseData.setConnectorName(chargingCost.getConnectorName());
+            responseData.setOrderStatus(chargingCost.getOrderStatus());
+            responseData.setOrderStatusDesc(chargingCost.getOrderStatusDesc());
+            responseData.setChargingDuration(chargingCost.getChargingDuration());
+            responseData.setChargingDurationDesc(chargingCost.getChargingDurationDesc());
+            responseData.setTotalPower(chargingCost.getTotalPower());
+            responseData.setElecMoney(chargingCost.getElecMoney());
+            responseData.setServiceMoney(chargingCost.getServiceMoney());
+            responseData.setTotalMoney(chargingCost.getTotalMoney());
+            responseData.setSoc(chargingCost.getSoc());
+            responseData.setCurrent(chargingCost.getCurrent());
+            responseData.setVoltage(chargingCost.getVoltage());
+            responseData.setPower(chargingCost.getPower());
+            responseData.setStartTime(chargingCost.getStartTime());
+            responseData.setLastUpdateTime(chargingCost.getLastUpdateTime());
+            responseData.setConnectorCode(chargingCost.getConnectorCode());
+            responseData.setStatus(chargingCost.getStatus());
+
+            log.info("查询充电订单实时费用成功, chargeOrderNo: {}", costRequest.getChargeOrderNo());
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_charging_cost请求异常", e);
+            return buildErrorResponse(500, "系统错误: " + e.getMessage(), null);
+        }
+    }
 }
+
+

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

@@ -122,6 +122,7 @@ security:
     - /favicon.ico
     - /charge-business/v1/linkData/**
     - /applet/v1/homePage/** # 用户端分页查询站点信息
+    - /third_party/v1/** # 第三方接入接口
   # 只走第三方过滤器、不走其他安全链
   third-party-urls:
     - /charge-business/v1/linkData/notification_start_charge_result
@@ -151,6 +152,7 @@ security:
       - /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
@@ -231,6 +233,7 @@ springdoc:
         - com.zsElectric.boot.charging.controller
         - com.zsElectric.boot.business.controller
         - com.zsElectric.boot.business.controller.applet
+        - com.zsElectric.boot.thirdParty.controller
   default-flat-param-object: true
 
 # knife4j 接口文档配置
@@ -291,6 +294,26 @@ captcha:
 
 # 微信小程序配置
 wx:
+  #商户号
+  mch_id: 105520131602
+
+  #小程序appid
+  appid: wx9894a01b9e92c368
+  #appid密钥
+  secret: b1e83dbcf83af310c38c0a138739ddcf
+
+  #md5密钥
+  key: f5131b3f07acb965a59041b690a29911
+  #同一支付请求地址
+  req_url: https://eoap.cebbank.com/uiap/bcac/pay/pay/gateway
+  #支付请求成功后的回调地址--需要配置正确的域名
+  #  notify_url: https://13cd4c06.r28.cpolar.top/applet/v1/wft/order/notify
+  notify_url: https://cd.admin.zswlgz.com/applet/v1/wft/order/notify
+  #私钥
+  mchPrivateKey: MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDeFB5G2OYT762PpUytCw7Du40i6WnzcmbvEE9IXPXi+QirPwMvW9mBqNDIUk5hQS3ZnHjj80YQRWG6yksjE6kHAYIWahCDiaPlBqYvYSJ8ePzbT61THZJbzqFaIG3svW7xq9nsUmzVBub0ATIzC1DQRu9ZTdrj/iuMUEhJyJ8IHrTP09eTwNYdoagHQlKWRVoNE3LuU4GXG3VCbkQ2ixbMo8dXBisDIi3GYOSFWzota6H+OCp9Mta1jTqdwALKAU9PNlRkQwOMLk2OmMqGUhImVVpl+eGrIYn3iARce0alNFg8hghFMJ8MKpSxJDM6YHNOJQ06S25YYhTpd+C2/VOBAgMBAAECggEBAM/W9ksJ/bJU0xOn+W3N9oB7C+jLmMwtmmZM1lZ8IefNeC6Ep59wD81ISDXiydY9YQLTbVSxPjZGKOPfJZjrcnrLD4uYsmHYtFnI8klPWC40MTmzhRxPhcWESgAGb7prw+RMGIUS0yY/8nAUmn2pLnXunVzv/1b3bpxAGpdrOmMmU28GBt9AlXiIpVmnxnkhp66c4zFj5gvvVoDrz9m/6Acyn6n7yccCHD2iYw0D78GPItEWky38tH/FV0lRcCCYAf5fc2nFnicrdgj1RYjqTWxM7A92UecviTAbiuPO6CQQl7+sMtGU2d6UeKj0Xrrl2gly+lOS97P/NcLtZf5vklECgYEA8SMlCg5AToSkIwal7dXYgM2VlNFwSRDuO3hEVoWe7bM/LkEp8dqSpV025ZtngTY/qziXsGP/7l8bcS1th19cX/+/MFFOWsoxtqvOam2Elp+Qg1johEfnWI+Bo4WiQ5CHYNQRN3cuiWTc5HHuI0KQAEPx/aYogD057X2FIsiu6S0CgYEA68RAFsupZ6R8BhJbfY2CK07XLNvXu5DAYBmU3trmQFnaxXeQFfhQ8hi9m5Awu14YCnRmHzc8+QXFD/GqTAbxtImc6AZQKWdLuUntkitPWmJK6dtJC8Is3U9Yqz5+CkSmgZSfqW0DvEt7jagoNfpKgy29Qq4r35b6JsDXtnTQICUCgYEAjmwnkEzihn2pRFbE4jiP62OBmagqHb22N8HM+x1oxRQ9mOA8GfDy9GCd7/ddpt+Xs1V1omUt4GikGLCwJGiacsjm727WTKFnw3CuNgYBbcVI4Ys9qgOeDJyWATMIp8dRbktS7+OgxN2h6fuwn3rM+psm7p2ZBkUjVbXxUJ4fUPECgYEAw1wmAv2VjRUF0/4oI5w7bWlx8XDljT1/uuHXsuZN/qq2FgRht2LAqCsKCjprtwZcA2W6LUmXU32Ncg29ICxs4j1ZcAWzLOu0GoAAxKrwoSNrkeYr2/t1M5kJDzTEOfvywNMHjduQSdl+Mr5RO5D/Zz1iYztxjV9MPwpydHTM9KUCgYAHuT98NkBilMxQNNmUB13E10MYQvZuiGVFZtT3up69Elpmtm7Z5cEW7QNG0g1LPPfkzfWPsq+6I98FmozLickqvjntdpul4czTITn8SNHqoxvdbcVnDipF1KwlHcnXhjO1KjSZg3a/iv554OR3/rbD9SWDzDAT7Zy6zX9n8OwGRA==
+  #公钥(商户平台的公钥,不是自己生成的公钥)
+  platPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2HOacYJOO9zsWPVmauV/YCeR78RsRDtusdLEngi/JkPkZVSE0X47z2RpJncGyV1QfdHv0udVEND4bvjXku4qUJp5DYAulm6pDXdcwWPcdI77V7dqoDvYm9Cc8kGj9s+/0xeuxX4qJmwzFTf7XjRfTT7+OVSvFnnButAkgMuD3cW1rtcQYeY9S9puQneN1i1+Lek5GCpW5PFsezK6QMgrpB1TFVSF5tloUODfc4fBDY5quGxn29Fo9gzJXO8ehoRft/JEaS4rNqmlfbvaJfEROXALlKoUX8Iki050ss7WwIBS6xuV08JnHTUHzHmAzOscwyYmT3RZChPgluWuyYW30wIDAQAB
+  # 微信小程序配置
   miniapp:
     # 微信小程序 AppID(在微信公众平台获取)
     app-id: wx9894a01b9e92c368

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

@@ -55,7 +55,7 @@ spring:
 third-party:
   jwt:
     secret: "vgct1TZ4ZikKjaaeIiq3LUwIvpmcgYa6ABCD1234EFGH5678IJKL9012MNOP3456"
-    expire: 7200
+    expire: 9600
 
 mybatis-plus:
   mapper-locations: classpath*:/mapper/**/*.xml

+ 364 - 0
src/main/resources/application-test.yml

@@ -0,0 +1,364 @@
+server:
+  port: 8989
+#test.zsdd.online
+spring:
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://rm-2vc2zl1990od9qvg0eo.mysql.cn-chengdu.rds.aliyuncs.com:3306/zs_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+    username: root
+    password: 1KQaNI+vPz8^xfYcb%l6
+  data:
+    redis:
+      database: 10
+      host: 47.108.180.74
+      port: 6379
+      password: ZswlGz$?998012
+      timeout: 10s
+      lettuce:
+        pool:
+          # 连接池最大连接数 默认8 ,负数表示没有限制
+          max-active: 8
+          # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1
+          max-wait: -1
+          # 连接池中的最大空闲连接 默认8
+          max-idle: 8
+          # 连接池中的最小空闲连接 默认0
+          min-idle: 0
+  cache:
+    enabled: false
+    # 缓存类型 redis、none(不使用缓存)
+    type: redis
+    # 缓存时间(单位:ms)
+    redis:
+      time-to-live: 3600000
+      # 缓存null值,防止缓存穿透
+      cache-null-values: true
+    caffeine:
+      spec: initialCapacity=50,maximumSize=1000,expireAfterWrite=600s
+  # 邮件配置
+  mail:
+    host: smtp.youlai.tech
+    port: 587
+    username: your-email@example.com
+    password: 123456
+    properties:
+      mail:
+        smtp:
+          auth: true
+          starttls:
+            enable: true
+    # 邮件发送者
+    from: youlaitech@163.com
+
+# 第三方认证配置
+third-party:
+  jwt:
+    secret: "vgct1TZ4ZikKjaaeIiq3LUwIvpmcgYa6ABCD1234EFGH5678IJKL9012MNOP3456"
+    expire: 7200
+
+mybatis-plus:
+  mapper-locations: classpath*:/mapper/**/*.xml
+  global-config:
+    db-config:
+      # 主键ID类型
+      id-type: none
+      # 逻辑删除对应的全局属性名(注意:须是对象属性名,不能是表字段名,如 isDeleted 而非 is_deleted,否则逻辑删除失效)
+      logic-delete-field: isDeleted
+      # 逻辑删除-删除值
+      logic-delete-value: 1
+      # 逻辑删除-未删除值
+      logic-not-delete-value: 0
+  configuration:
+    # 驼峰下划线转换
+    map-underscore-to-camel-case: true
+    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+
+# 异步线程池配置
+async:
+  thread-pool:
+    core-pool-size: 10
+    max-pool-size: 50
+    queue-capacity: 1000
+    keep-alive-seconds: 60
+    thread-name-prefix: "business-async-"
+    allow-core-thread-timeout: false
+    await-termination-seconds: 30
+
+# 安全配置
+security:
+  session:
+    type: jwt # 会话方式 jwt/redis-token
+    access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期
+    refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期
+    jwt:
+      secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
+    redis-token:
+      allow-multi-login: true # 是否允许多设备登录
+  # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等)
+  ignore-urls:
+    - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录)
+    - /api/v1/auth/captcha # 验证码获取接口
+    - /api/v1/auth/refresh-token # 刷新令牌接口
+    - /api/v1/auth/logout # 开放退出登录
+    - /api/v1/auth/wx/** # 微信相关所有接口
+    - /ws/** # WebSocket接口
+    - /applet/v1/homePage/** # 用户端分页查询站点信息
+    - /charge-business/charge-business/v1/linkData/query_token
+    - /applet/v1/station/connector/detail
+
+  # 非安全端点路径,完全绕过 Spring Security 的安全控制
+  unsecured-urls:
+    - ${springdoc.swagger-ui.path}
+    - /doc.html
+    - /doc.html/**
+    - /swagger-ui.html
+    - /swagger-ui/**
+    - /swagger-resources/**
+    - /v3/api-docs
+    - /v3/api-docs/**
+    - /webjars/**
+    - /favicon.ico
+    - /charge-business/v1/linkData/**
+    - /applet/v1/homePage/** # 用户端分页查询站点信息
+    - /third_party/v1/** # 第三方接入接口
+  # 只走第三方过滤器、不走其他安全链
+  third-party-urls:
+    - /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_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
+  write-timeout: 60s
+  retry-on-connection-failure: true
+  max-retry-count: 3
+  connection-pool:
+    max-idle-connections: 200
+    keep-alive-duration: 300s
+
+# 文件存储配置
+oss:
+  # OSS 类型 (目前支持aliyun、minio、local)
+  type: aliyun
+  # MinIO 对象存储服务
+  minio:
+    # MinIO 服务地址
+    endpoint: http://localhost:9000
+    # 访问凭据
+    access-key: minioadmin
+    # 凭据密钥
+    secret-key: minioadmin
+    # 存储桶名称
+    bucket-name: zsElectric
+    # (可选) 自定义域名:配置后,文件 URL 会使用该域名格式
+    custom-domain:
+  # 阿里云OSS对象存储服务
+  aliyun:
+    # 服务Endpoint
+    endpoint: oss-cn-beijing.aliyuncs.com
+    # 访问凭据`
+    access-key-id: LTAI5tJscqbev7wSugGCrEtt
+    # 凭据密钥
+    access-key-secret: xJkoJR1ILpXNSF2ERnxNq71UZTQNcB
+    # 存储桶名称
+    bucket-name: national-motion
+  # 本地存储
+  local:
+    # 文件存储路径 请注意下,mac用户请使用 /Users/your-username/your-path/,否则会有权限问题,windows用户请使用 D:/your-path/
+    storage-path: /Users/theo/home/
+# 短信配置
+sms:
+  # 阿里云短信
+  aliyun:
+    accessKeyId: xxx
+    accessKeySecret: xxx
+    domain: dysmsapi.aliyuncs.com
+    regionId: cn-shanghai
+    signName: 中数未来
+    templates:
+      #  注册短信验证码模板
+      register: SMS_22xxx771
+      # 登录短信验证码模板
+      login: SMS_22xxx772
+      # 修改手机号短信验证码模板
+      change-mobile: SMS_22xxx773
+
+# springdoc配置: https://springdoc.org/properties.html
+springdoc:
+  swagger-ui:
+    path: /swagger-ui.html
+    operations-sorter: alpha
+    tags-sorter: alpha
+  api-docs:
+    path: /v3/api-docs
+  group-configs:
+    - group: "系统管理"
+      paths-to-match: "/**"
+      packages-to-scan:
+        - com.zsElectric.boot.auth.controller
+        - com.zsElectric.boot.system.controller
+        - com.zsElectric.boot.platform.file.controller
+        - com.zsElectric.boot.platform.codegen.controller
+        - com.zsElectric.boot.charging.controller
+        - com.zsElectric.boot.business.controller
+        - com.zsElectric.boot.business.controller.applet
+        - com.zsElectric.boot.thirdParty.controller
+  default-flat-param-object: true
+
+# knife4j 接口文档配置
+knife4j:
+  # 是否开启 Knife4j 增强功能
+  enable: true # 设置为 true 表示开启增强功能
+  # 生产环境配置
+  production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用)
+  setting:
+    language: zh_cn
+
+# xxl-job 定时任务配置
+xxl:
+  job:
+    # 定时任务开关
+    enabled: false
+    admin:
+      # 调度中心地址,多个逗号分隔
+      addresses: http://127.0.0.1:8080/xxl-job-admin
+    accessToken: default_token
+    # 执行器配置
+    executor:
+      appname: xxl-job-executor-${spring.application.name} # 执行器AppName
+      address: # 执行器注册地址,默认为空,多网卡时可手动设置
+      ip: # 执行器IP,默认为空,多网卡时可手动设置
+      port: 9999 # 执行器通讯端口
+      logpath: /data/applogs/xxl-job/jobhandler # 任务运行日志文件存储磁盘路径
+      logretentiondays: 30 # 日志保存天数,值大于3时生效
+
+# 验证码配置
+captcha:
+  # 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码
+  type: circle
+  # 验证码宽度
+  width: 120
+  # 验证码高度
+  height: 40
+  # 验证码干扰元素个数
+  interfere-count: 2
+  # 文本透明度(0.0-1.0)
+  text-alpha: 0.8
+  # 验证码字符配置
+  code:
+    # 验证码字符类型 math-算术|random-随机字符
+    type: math
+    # 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数
+    length: 1
+  # 验证码字体
+  font:
+    # 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif
+    name: SansSerif
+    # 字体样式 0-普通|1-粗体|2-斜体
+    weight: 1
+    # 字体大小
+    size: 24
+  # 验证码有效期(秒)
+  expire-seconds: 120
+
+# 微信小程序配置
+wx:
+  miniapp:
+    # 微信小程序 AppID(在微信公众平台获取)
+    app-id: wx9894a01b9e92c368
+    # 微信小程序 AppSecret(在微信公众平台获取)
+    app-secret: b1e83dbcf83af310c38c0a138739ddcf
+
+# ==================== AI 命令系统配置 ====================
+ai:
+  # 是否启用 AI 功能
+  enabled: false
+
+  # 当前使用的提供商:qwen、deepseek、openai
+  provider: qwen
+
+  # 所有提供商配置(统一管理,扩展性强)
+  providers:
+    # 阿里通义千问(推荐:有免费额度)
+    qwen:
+      # API Key(https://bailian.console.aliyun.com/ 获取)
+      api-key: ${QWEN_API_KEY:sk-c2941d05bf2f411ca80424fcxxxxxxxx}
+
+      # Base URL(OpenAI 兼容端点)
+      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
+
+      # 模型:qwen-plus(推荐)、qwen-turbo、qwen-max、qwen-long
+      model: qwen-plus
+
+      # 显示名称
+      display-name: 阿里通义千问
+
+      # 超时时间(秒)
+      timeout: 30
+
+    # DeepSeek
+    deepseek:
+      api-key: ${DEEPSEEK_API_KEY:}
+      base-url: https://api.deepseek.com/v1
+      model: deepseek-chat
+      display-name: DeepSeek
+      timeout: 30
+
+    # OpenAI(添加新提供商只需配置,无需修改代码)
+    openai:
+      api-key: ${OPENAI_API_KEY:}
+      base-url: https://api.openai.com/v1
+      model: gpt-4
+      display-name: OpenAI GPT-4
+      timeout: 60
+
+  # 安全配置
+  security:
+    enable-audit: true
+    dangerous-operations-confirm: true
+    function-whitelist:
+      - deleteUser
+      - updateUser
+      - queryUsers
+      - assignRole
+    sensitive-params:
+      - password
+      - idCard
+      - bankCard
+      - token
+
+  # 限流配置
+  rate-limit:
+    max-executions-per-minute: 10
+    max-executions-per-day: 100

+ 40 - 0
src/main/resources/mapper/business/UserAccountMapper.xml

@@ -23,4 +23,44 @@
         ORDER BY create_time DESC
     </select>
 
+    <!-- 查询负余额退款异常用户列表 -->
+    <select id="listNegativeBalanceRefundUsers" resultType="com.zsElectric.boot.business.model.vo.RefundCompensationVO">
+        SELECT
+            log.user_id AS userId,
+            log.before_balance AS refundBeforeBalance,
+            log.change_balance AS refundAfterBalance,
+            ABS(log.before_balance) AS compensationAmount,
+            log.change_id AS refundOrderId,
+            log.create_time AS refundTime,
+            acc.balance AS currentBalance
+        FROM c_user_account_log log
+        LEFT JOIN c_user_account acc ON log.user_id = acc.user_id AND acc.is_deleted = 0
+        WHERE log.change_note = '账户退款'
+          AND log.change_type = 2
+          AND log.before_balance &lt; 0
+          AND log.is_deleted = 0
+          AND log.create_time &gt;= #{startTime}
+        ORDER BY log.create_time DESC
+    </select>
+
+    <!-- 执行补偿操作-批量更新用户余额 -->
+    <update id="executeCompensation">
+        UPDATE c_user_account acc
+        INNER JOIN (
+            SELECT
+                user_id,
+                SUM(ABS(before_balance)) AS total_compensation
+            FROM c_user_account_log
+            WHERE change_note = '账户退款'
+              AND change_type = 2
+              AND before_balance &lt; 0
+              AND is_deleted = 0
+              AND create_time &gt;= #{startTime}
+            GROUP BY user_id
+        ) comp ON acc.user_id = comp.user_id
+        SET acc.balance = acc.balance - comp.total_compensation,
+            acc.update_time = NOW()
+        WHERE acc.is_deleted = 0
+    </update>
+
 </mapper>

+ 0 - 1
src/main/resources/mapper/business/UserRefundsOrderInfoMapper.xml

@@ -57,7 +57,6 @@
             c_user_refunds_order_info
         <where>
             is_deleted = 0
-            AND success_time IS NULL
             <if test="queryParams.startTime != null">
                 AND create_time <![CDATA[  >=  ]]> #{queryParams.startTime,jdbcType=DATE}
             </if>

+ 2 - 2
src/main/resources/mapper/system/DataBoardMapper.xml

@@ -35,7 +35,7 @@
     <select id="selectTodayRefundAmount" resultType="java.math.BigDecimal">
         SELECT COALESCE(SUM(amount), 0)
         FROM c_user_refunds_order_info
-        WHERE status = 'SUCCESS' AND is_deleted = 0
+        WHERE is_deleted = 0
         AND success_time BETWEEN #{todayStart} AND #{todayEnd}
     </select>
 
@@ -51,7 +51,7 @@
             -- 2.0版本退款(2025-12-31及之后)
             (SELECT COALESCE(SUM(amount), 0)
              FROM c_user_refunds_order_info
-             WHERE status = 'SUCCESS' AND is_deleted = 0
+             WHERE is_deleted = 0
              AND success_time >= '2025-12-31 00:00:00')
         AS totalRefundAmount
     </select>

+ 36 - 0
src/test/java/com/zsElectric/boot/charging/DecryptDataMain.java

@@ -0,0 +1,36 @@
+package com.zsElectric.boot.charging;
+
+import com.zsElectric.boot.common.constant.ConnectivityConstants;
+import com.zsElectric.boot.common.util.AESCryptoUtils;
+
+/**
+ * 解密Data工具类
+ */
+public class DecryptDataMain {
+
+    public static void main(String[] args) {
+        try {
+            String encryptedData = "cffz/0ISC0EszLxywIsdypm4LM3k/3wsPy590mrp1rltHayGylgmqtwQA1xekTUNxR1kl2u+KoQez7ugGdie2A==";
+            
+            System.out.println("=== 解密Data ===");
+            System.out.println("加密数据: " + encryptedData);
+            System.out.println("密钥: " + ConnectivityConstants.DATA_SECRET);
+            System.out.println("IV: " + ConnectivityConstants.DATA_SECRET_IV);
+            System.out.println();
+            
+            String decrypted = AESCryptoUtils.decrypt(encryptedData, 
+                    ConnectivityConstants.DATA_SECRET, 
+                    ConnectivityConstants.DATA_SECRET_IV);
+            System.out.println("解密结果: " + decrypted);
+            
+            System.out.println();
+            System.out.println("=== 期望值 ===");
+            System.out.println("PLATFORM_OPERATOR_ID: " + ConnectivityConstants.PLATFORM_OPERATOR_ID);
+            System.out.println("PLATFORM_OPERATOR_SECRET: " + ConnectivityConstants.PLATFORM_OPERATOR_SECRET);
+            
+        } catch (Exception e) {
+            System.err.println("解密失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}

+ 71 - 0
src/test/java/com/zsElectric/boot/charging/GenerateTokenRequestMain.java

@@ -0,0 +1,71 @@
+package com.zsElectric.boot.charging;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.RequestParmsEntity;
+import com.zsElectric.boot.common.util.electric.queryToken.QueryTokenRequestParms;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 生成正确的query_token请求参数
+ */
+public class GenerateTokenRequestMain {
+
+    public static void main(String[] args) {
+        try {
+            ObjectMapper objectMapper = new ObjectMapper();
+            
+            // 1. 构建Data内容(使用正确的PLATFORM配置)
+            QueryTokenRequestParms tokenRequest = new QueryTokenRequestParms();
+            tokenRequest.setOperatorID(ConnectivityConstants.PLATFORM_OPERATOR_ID);
+            tokenRequest.setOperatorSecret(ConnectivityConstants.PLATFORM_OPERATOR_SECRET);
+            
+            String dataJson = objectMapper.writeValueAsString(tokenRequest);
+            System.out.println("=== Data原文 ===");
+            System.out.println(dataJson);
+            System.out.println();
+            
+            // 2. AES加密Data
+            String encryptedData = AESCryptoUtils.encrypt(dataJson,
+                    ConnectivityConstants.DATA_SECRET,
+                    ConnectivityConstants.DATA_SECRET_IV);
+            
+            // 3. 构建请求参数
+            String operatorID = ConnectivityConstants.OPERATOR_ID;
+            String timeStamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+            String seq = "0001";
+            
+            // 4. 生成签名
+            String sig = HmacMD5Util.genSign(operatorID, encryptedData, timeStamp, seq,
+                    ConnectivityConstants.SIG_SECRET);
+            
+            // 5. 构建完整请求体
+            RequestParmsEntity requestEntity = new RequestParmsEntity();
+            requestEntity.setOperatorID(operatorID);
+            requestEntity.setData(encryptedData);
+            requestEntity.setTimeStamp(timeStamp);
+            requestEntity.setSeq(seq);
+            requestEntity.setSig(sig);
+            
+            System.out.println("=== 正确的请求参数(可直接复制使用) ===");
+            System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestEntity));
+            
+            System.out.println();
+            System.out.println("=== 使用的密钥配置 ===");
+            System.out.println("OPERATOR_ID: " + ConnectivityConstants.OPERATOR_ID);
+            System.out.println("PLATFORM_OPERATOR_ID: " + ConnectivityConstants.PLATFORM_OPERATOR_ID);
+            System.out.println("PLATFORM_OPERATOR_SECRET: " + ConnectivityConstants.PLATFORM_OPERATOR_SECRET);
+            System.out.println("DATA_SECRET: " + ConnectivityConstants.DATA_SECRET);
+            System.out.println("DATA_SECRET_IV: " + ConnectivityConstants.DATA_SECRET_IV);
+            System.out.println("SIG_SECRET: " + ConnectivityConstants.SIG_SECRET);
+            
+        } catch (Exception e) {
+            System.err.println("生成失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}

+ 188 - 0
src/test/java/com/zsElectric/boot/charging/QueryTokenMain.java

@@ -0,0 +1,188 @@
+package com.zsElectric.boot.charging;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.RequestParmsEntity;
+import com.zsElectric.boot.common.util.electric.queryToken.QueryTokenRequestParms;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 获取Token接口测试类
+ * 用于模拟第三方调用 /query_token 接口
+ */
+public class QueryTokenMain {
+
+    // 测试环境接口地址(根据实际情况修改)
+    private static final String BASE_URL = "http://localhost:8081";
+    private static final String QUERY_TOKEN_URL = BASE_URL + "/evcs/v1.1/1/" + ConnectivityConstants.OPERATOR_ID + "/query_token";
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("=== 获取Token接口测试 ===");
+            System.out.println("请求地址: " + QUERY_TOKEN_URL);
+            System.out.println();
+
+            // 1. 构建请求Data内容
+            QueryTokenRequestParms tokenRequest = new QueryTokenRequestParms();
+            tokenRequest.setOperatorID(ConnectivityConstants.PLATFORM_OPERATOR_ID);
+            tokenRequest.setOperatorSecret(ConnectivityConstants.PLATFORM_OPERATOR_SECRET);
+
+            ObjectMapper objectMapper = new ObjectMapper();
+            String dataJson = objectMapper.writeValueAsString(tokenRequest);
+            System.out.println("原始Data数据: " + dataJson);
+
+            // 2. AES加密Data
+            String encryptedData = AESCryptoUtils.encrypt(dataJson,
+                    ConnectivityConstants.DATA_SECRET,
+                    ConnectivityConstants.DATA_SECRET_IV);
+            System.out.println("加密后Data: " + encryptedData);
+
+            // 3. 构建请求参数
+            String operatorID = ConnectivityConstants.OPERATOR_ID;
+            String timeStamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+            String seq = String.format("%04d", (int) (Math.random() * 10000));
+
+            // 4. 生成签名
+            String sig = HmacMD5Util.genSign(operatorID, encryptedData, timeStamp, seq,
+                    ConnectivityConstants.SIG_SECRET);
+            System.out.println("生成签名: " + sig);
+
+            // 5. 构建完整请求体
+            RequestParmsEntity requestEntity = new RequestParmsEntity();
+            requestEntity.setOperatorID(operatorID);
+            requestEntity.setData(encryptedData);
+            requestEntity.setTimeStamp(timeStamp);
+            requestEntity.setSeq(seq);
+            requestEntity.setSig(sig);
+
+            String requestJson = objectMapper.writeValueAsString(requestEntity);
+            System.out.println();
+            System.out.println("完整请求体: ");
+            System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestEntity));
+            System.out.println();
+
+            // 6. 发送HTTP POST请求
+            System.out.println("=== 发送请求 ===");
+            String response = sendPostRequest(QUERY_TOKEN_URL, requestJson);
+            System.out.println("响应结果: ");
+            System.out.println(response);
+
+            // 7. 解析响应
+            if (response != null && !response.isEmpty()) {
+                parseResponse(response, objectMapper);
+            }
+
+        } catch (Exception e) {
+            System.err.println("测试执行失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 发送HTTP POST请求
+     */
+    private static String sendPostRequest(String urlString, String jsonBody) {
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(urlString);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+            connection.setRequestProperty("Accept", "application/json");
+            connection.setDoOutput(true);
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(30000);
+
+            // 写入请求体
+            try (OutputStream os = connection.getOutputStream()) {
+                byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8);
+                os.write(input, 0, input.length);
+            }
+
+            // 读取响应
+            int responseCode = connection.getResponseCode();
+            System.out.println("HTTP响应码: " + responseCode);
+
+            BufferedReader reader;
+            if (responseCode >= 200 && responseCode < 300) {
+                reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
+            } else {
+                reader = new BufferedReader(new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8));
+            }
+
+            StringBuilder response = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                response.append(line);
+            }
+            reader.close();
+
+            return response.toString();
+
+        } catch (Exception e) {
+            System.err.println("HTTP请求失败: " + e.getMessage());
+            return null;
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 解析响应数据
+     */
+    private static void parseResponse(String response, ObjectMapper objectMapper) {
+        try {
+            System.out.println();
+            System.out.println("=== 解析响应 ===");
+
+            // 解析响应JSON
+            var responseNode = objectMapper.readTree(response);
+            int ret = responseNode.has("Ret") ? responseNode.get("Ret").asInt() : -1;
+            String msg = responseNode.has("Msg") ? responseNode.get("Msg").asText() : "";
+            String data = responseNode.has("Data") ? responseNode.get("Data").asText() : "";
+
+            System.out.println("Ret: " + ret);
+            System.out.println("Msg: " + msg);
+
+            if (ret == 0 && data != null && !data.isEmpty()) {
+                // 解密Data
+                String decryptedData = AESCryptoUtils.decrypt(data,
+                        ConnectivityConstants.DATA_SECRET,
+                        ConnectivityConstants.DATA_SECRET_IV);
+                System.out.println("解密后Data: " + decryptedData);
+
+                // 格式化输出
+                var dataNode = objectMapper.readTree(decryptedData);
+                System.out.println("解密后Data(格式化): ");
+                System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(dataNode));
+
+                // 提取Token信息
+                if (dataNode.has("AccessToken")) {
+                    System.out.println();
+                    System.out.println("=== Token信息 ===");
+                    System.out.println("AccessToken: " + dataNode.get("AccessToken").asText());
+                    System.out.println("TokenAvailableTime: " + dataNode.get("TokenAvailableTime").asInt() + " 秒");
+                    System.out.println("SuccStat: " + dataNode.get("SuccStat").asInt());
+                }
+            } else {
+                System.out.println("获取Token失败,错误码: " + ret + ", 错误信息: " + msg);
+            }
+
+        } catch (Exception e) {
+            System.err.println("解析响应失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}

+ 74 - 0
src/test/java/com/zsElectric/boot/charging/VerifySigMain.java

@@ -0,0 +1,74 @@
+package com.zsElectric.boot.charging;
+
+import com.zsElectric.boot.common.constant.ConnectivityConstants;
+import com.zsElectric.boot.common.util.HmacMD5Util;
+
+/**
+ * 签名验证排查工具
+ */
+public class VerifySigMain {
+
+    public static void main(String[] args) {
+        try {
+            // 用户提供的请求参数
+            String operatorID = "MA6HWN8L3";
+            String data = "cffz/0ISC0EszLxywIsdypm4LM3k/3wsPy590mrp1rltHayGylgmqtwQA1xekTUNxR1kl2u+KoQez7ugGdie2A==";
+            String timeStamp = "20260317174500";
+            String seq = "0001";
+            String sig = "F831C56B6243E85F49B70C24ED124C0E";
+
+            System.out.println("=== 签名验证排查 ===");
+            System.out.println();
+            
+            // 1. 显示原始参数
+            System.out.println("【请求参数】");
+            System.out.println("OperatorID: " + operatorID);
+            System.out.println("Data: " + data);
+            System.out.println("TimeStamp: " + timeStamp);
+            System.out.println("Seq: " + seq);
+            System.out.println("Sig: " + sig);
+            System.out.println();
+
+            // 2. 显示签名密钥
+            System.out.println("【签名密钥】");
+            System.out.println("SIG_SECRET: " + ConnectivityConstants.SIG_SECRET);
+            System.out.println();
+
+            // 3. 拼接签名内容(按接口代码逻辑)
+            String signContent = operatorID + data + timeStamp + seq;
+            System.out.println("【签名内容拼接】");
+            System.out.println("拼接方式: OperatorID + Data + TimeStamp + Seq");
+            System.out.println("拼接结果: " + signContent);
+            System.out.println("拼接长度: " + signContent.length());
+            System.out.println();
+
+            // 4. 使用 HmacMD5 生成正确的签名
+            String correctSig = HmacMD5Util.hmacMD5Hex(signContent, ConnectivityConstants.SIG_SECRET);
+            System.out.println("【签名计算】");
+            System.out.println("正确签名: " + correctSig);
+            System.out.println("请求签名: " + sig);
+            System.out.println("签名匹配: " + correctSig.equalsIgnoreCase(sig));
+            System.out.println();
+
+            // 5. 验证签名
+            boolean isValid = HmacMD5Util.verify(signContent, ConnectivityConstants.SIG_SECRET, sig);
+            System.out.println("【验证结果】");
+            System.out.println("签名验证: " + (isValid ? "通过" : "失败"));
+            
+            if (!isValid) {
+                System.out.println();
+                System.out.println("=== 问题分析 ===");
+                System.out.println("签名不匹配,可能原因:");
+                System.out.println("1. 签名时使用的密钥不是: " + ConnectivityConstants.SIG_SECRET);
+                System.out.println("2. 签名内容拼接顺序或格式不对");
+                System.out.println("3. Data内容在签名前后发生了变化");
+                System.out.println();
+                System.out.println("请使用正确的签名: " + correctSig);
+            }
+
+        } catch (Exception e) {
+            System.err.println("验证失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}

+ 5 - 5
src/test/java/com/zsElectric/boot/thirdParty/service/QueryTokenMain.java

@@ -21,15 +21,15 @@ public class QueryTokenMain {
 
     // ===== 配置参数(需根据数据库中的第三方配置信息修改) =====
     // 运营商ID
-    private static final String OPERATOR_ID = "12345qwer";
+    private static final String OPERATOR_ID = "MA9CU5DCB";
     // 运营商密钥
-    private static final String OPERATOR_SECRET = "vfkh4k740lfg88kq";
+    private static final String OPERATOR_SECRET = "C5rPZ2JIN66y3eBc";
     // 数据密钥(16位)
-    private static final String DATA_SECRET = "bbkwy062pzyjhqmg";
+    private static final String DATA_SECRET = "Y3qiVnd9LCLopZFM";
     // 数据密钥IV(16位)
-    private static final String DATA_SECRET_IV = "xgbzfgwz6ki2gm5j";
+    private static final String DATA_SECRET_IV = "FzUoFlJa9S2LnKPV";
     // 签名密钥
-    private static final String SIG_SECRET = "8h9sf4zd5cbtlu8x";
+    private static final String SIG_SECRET = "4zbia5MAsaKSoyNk";
 
     private static final ObjectMapper objectMapper = new ObjectMapper();