Просмотр исходного кода

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

- 修改AESCryptoUtils主函数注释,更新测试加解密数据
- AppChargeVO订单号改为订单id描述,AppInvokeChargeForm新增车牌号字段
- AppletWFTOrderController退款接口新增防重提交及用户登录校验
- application-dev.yml及application-prod.yml中增加微信小程序配置及调整第三方接口过滤路径
- 调整ChargeDeviceDetailRequestData设备编码字段名称,保持字段语义准确
- ChargeOrderInfoServiceImpl新增用户余额校验防止起充价不足导致异常
- 充电订单中实现车牌号优先使用表单数据,不存在时使用用户默认车牌
- ChargingBusinessServiceImpl优化启动充电日志输出,准确反映请求及响应信息
- 修改DataBoardMapper中退款金额统计SQL,去除重复状态过滤条件,保持数据准确
- 新增充电相关工具测试类:DecryptDataMain与GenerateTokenRequestMain,辅助数据加解密测试
wzq 1 неделя назад
Родитель
Сommit
70ad42b98c
20 измененных файлов с 498 добавлено и 43 удалено
  1. 84 28
      doc/第三方接入API文档.md
  2. 4 0
      src/main/java/com/zsElectric/boot/business/model/entity/UserInfo.java
  3. 6 0
      src/main/java/com/zsElectric/boot/business/model/vo/StationInfoVO.java
  4. 2 0
      src/main/java/com/zsElectric/boot/business/model/vo/UserInfoVO.java
  5. 4 0
      src/main/java/com/zsElectric/boot/business/model/vo/applet/AppUserInfoVO.java
  6. 4 0
      src/main/java/com/zsElectric/boot/business/service/UserInfoService.java
  7. 8 6
      src/main/java/com/zsElectric/boot/business/service/impl/ChargeOrderInfoServiceImpl.java
  8. 54 0
      src/main/java/com/zsElectric/boot/business/service/impl/UserInfoServiceImpl.java
  9. 1 0
      src/main/java/com/zsElectric/boot/common/constant/SystemConstants.java
  10. 9 0
      src/main/java/com/zsElectric/boot/thirdParty/controller/ThirdPartyController.java
  11. 1 1
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeDeviceDetailRequestData.java
  12. 26 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ClearBalanceRequestData.java
  13. 73 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ClearBalanceResponseData.java
  14. 9 0
      src/main/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenService.java
  15. 110 5
      src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java
  16. 3 0
      src/main/resources/application-prod.yml
  17. 4 0
      src/main/resources/mapper/business/ThirdPartyStationInfoMapper.xml
  18. 2 0
      src/main/resources/mapper/business/UserInfoMapper.xml
  19. 5 3
      src/test/java/com/zsElectric/boot/thirdParty/service/QueryTokenMain.java
  20. 89 0
      src/test/java/com/zsElectric/boot/thirdParty/service/VerifyResponseSigMain.java

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

@@ -19,6 +19,7 @@
 - [11. 查询充电订单实时费用](#11-查询充电订单实时费用)
 - [12. 查询充电订单分页列表](#12-查询充电订单分页列表)
 - [13. 查询充电订单详情](#13-查询充电订单详情)
+- [14. 清除用户余额](#14-清除用户余额)
 - [附录:配置模板](#附录配置模板)
 
 ---
@@ -89,7 +90,7 @@
 - **接口说明:** Token作为全局唯一凭证,调用各接口时均需要使用
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_token`
+- **接口路径:** `/third_party/v1/query_token`
 - **⚠️注意:** 当前接口限制每分钟30次访问,请避免频繁调用。建议根据过期时间缓存Token,避免重复获取!
 
 ### 2.2 输入参数(data解密后)
@@ -144,7 +145,7 @@
 - **接口说明:** 第三方获取充值档位分页数据
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_recharge_level_page`
+- **接口路径:** `/third_party/v1/query_recharge_level_page`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 3.2 输入参数(data解密后)
@@ -217,7 +218,7 @@
 - **接口说明:** 第三方根据手机号获取用户信息
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_user_info`
+- **接口路径:** `/third_party/v1/query_user_info`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 4.2 输入参数(data解密后)
@@ -294,7 +295,7 @@
 - **接口说明:** 第三方充点充电券购买
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/charge_order_pay`
+- **接口路径:** `/third_party/v1/charge_order_pay`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 5.2 输入参数(data解密后)
@@ -359,7 +360,7 @@
 - **接口说明:** 第三方获取充电站列表
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_charge_station_list`
+- **接口路径:** `/third_party/v1/query_charge_station_list`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 6.2 输入参数(data解密后)
@@ -398,18 +399,20 @@
 
 **list 数组元素:**
 
-| 参数名称 | 参数定义 | 参数类型 | 描述 |
-|----------|----------|----------|------|
-| stationId | 站点ID | Long | 充电站ID |
-| stationName | 站点名称 | String | 充电站名称 |
-| tips | 提示语 | String | 站点提示信息 |
-| distance | 距离 | BigDecimal | 距离(km) |
-| fastCharging | 快充 | String | 格式:空闲/总数 |
-| slowCharging | 慢充 | String | 格式:空闲/总数 |
-| peakValue | 当前峰值 | String | 当前峰值 |
-| peakTime | 峰时段时间 | String | 峰时段时间 |
-| periodFlag | 时段标志 | Integer | 1-尖,2-峰,3-平,4-谷 |
-| platformPrice | 平台价 | BigDecimal | 平台价格 |
+| 参数名称 | 参数定义  | 参数类型 | 描述              |
+|----------|-------|----------|-----------------|
+| stationId | 站点ID  | Long | 充电站ID           |
+| stationName | 站点名称  | String | 充电站名称           |
+| tips | 提示语   | String | 站点提示信息          |
+| distance | 距离    | BigDecimal | 距离(km)          |
+| fastCharging | 快充    | String | 格式:空闲/总数        |
+| slowCharging | 慢充    | String | 格式:空闲/总数        |
+| peakValue | 当前峰值  | String | 当前峰值            |
+| peakTime | 峰时段时间 | String | 峰时段时间           |
+| periodFlag | 时段标志  | Integer | 1-尖,2-峰,3-平,4-谷 |
+| platformPrice | 平台价   | BigDecimal | 平台价格            |
+| longitude | 经度    | BigDecimal | 站点经度            |
+| latitude | 纬度    | BigDecimal | 站点纬度            |
 
 ### 6.5 返回示例
 
@@ -448,7 +451,7 @@
 - **接口说明:** 第三方获取充电站详情与充电设备列表
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_charge_station_detail`
+- **接口路径:** `/third_party/v1/query_charge_station_detail`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 7.2 输入参数(data解密后)
@@ -555,14 +558,14 @@
 - **接口说明:** 第三方获取充电终端详情
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_charge_device_detail`
+- **接口路径:** `/third_party/v1/query_charge_device_detail`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 8.2 输入参数(data解密后)
 
-| 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
-|----------|----------|----------|------|-----|
-| connectorId | 设备编码 | String | 设备编码 | 是   |
+| 参数名称       | 参数定义 | 参数类型 | 描述 | 是否必填 |
+|------------|----------|----------|------|-----|
+| connectorCode | 设备编码 | String | 设备编码 | 是   |
 
 ### 8.3 请求示例
 
@@ -570,7 +573,7 @@
 
 ```json
 {
-  "connectorId": "89825635646_1"
+  "connectorCode": "89825635646_1"
 }
 ```
 
@@ -650,7 +653,7 @@
 - **接口说明:** 第三方启动充电
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/invoke_charge`
+- **接口路径:** `/third_party/v1/invoke_charge`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 9.2 输入参数(data解密后)
@@ -715,7 +718,7 @@
 - **接口说明:** 第三方停止充电
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/stop_charge`
+- **接口路径:** `/third_party/v1/stop_charge`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 10.2 输入参数(data解密后)
@@ -768,7 +771,7 @@
 - **接口说明:** 第三方查询充电订单实时费用(实时获取充电过程中的费用信息)
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_charging_cost`
+- **接口路径:** `/third_party/v1/query_charging_cost`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 11.2 输入参数(data解密后)
@@ -854,7 +857,7 @@
 - **接口说明:** 第三方查询充电订单分页列表(只查询所属运营商订单)
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_charge_order_list`
+- **接口路径:** `/third_party/v1/query_charge_order_list`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 12.2 输入参数(data解密后)
@@ -958,7 +961,7 @@
 - **接口说明:** 第三方查询充电订单详情(只查询渠道类型订单)
 - **请求格式:** JSON
 - **请求方式:** POST
-- **接口路径:** `/third-party/v1/query_charge_order_info`
+- **接口路径:** `/third_party/v1/query_charge_order_info`
 - **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
 
 ### 13.2 输入参数(data解密后)
@@ -1033,6 +1036,59 @@
 
 ---
 
+## 14. 清除用户余额
+
+### 14.1 接口描述
+
+- **接口名称:** clear_user_balance
+- **接口说明:** 第三方清除用户账户余额,将用户可用抵用券余额清零并记录动账日志
+- **请求格式:** JSON
+- **请求方式:** POST
+- **接口路径:** `/third_party/v1/clear_user_balance`
+- **认证方式:** 需要在Header中携带 `Authorization: Bearer {accessToken}`
+
+### 14.2 输入参数(data解密后)
+
+| 参数名称 | 参数定义 | 参数类型 | 描述 | 是否必填 |
+|----------|----------|----------|------|----------|
+| userId | 用户ID | Long | 用户ID | 是 |
+
+### 14.3 请求示例
+
+**data加密前:**
+
+```json
+{
+  "userId": 10001
+}
+```
+
+### 14.4 返回参数(data解密后)
+
+| 参数名称 | 参数定义 | 参数类型 | 描述 |
+|----------|----------|----------|------|
+| result | 操作结果 | Integer | 0-成功,1-失败 |
+| message | 结果消息 | String | 结果描述 |
+| userId | 用户ID | Long | 用户ID |
+| balanceBefore | 清除前余额 | BigDecimal | 清除前账户余额 |
+| balanceAfter | 清除后余额 | BigDecimal | 清除后账户余额 |
+
+### 14.5 返回示例
+
+**data解密后:**
+
+```json
+{
+  "result": 0,
+  "message": "清除余额成功",
+  "userId": 10001,
+  "balanceBefore": 150.00,
+  "balanceAfter": 0.00
+}
+```
+
+---
+
 ## 附录:配置模板
 
 对接需要配置以下参数,由平台给第三方平台提供这些参数信息:

+ 4 - 0
src/main/java/com/zsElectric/boot/business/model/entity/UserInfo.java

@@ -33,6 +33,10 @@ public class UserInfo extends BaseEntity {
      * 微信openid
      */
     private String openid;
+    /**
+     * 渠道方ID
+     */
+    private Long thirdPartId;
     /**
      * 逻辑删除(0-未删除 1-已删除)
      */

+ 6 - 0
src/main/java/com/zsElectric/boot/business/model/vo/StationInfoVO.java

@@ -60,4 +60,10 @@ public class StationInfoVO implements Serializable {
 
     @Schema(description = "是否企业用户(true-是 false-否)")
     private Boolean firmUser;
+
+    @Schema(description = "经度")
+    private BigDecimal longitude;
+
+    @Schema(description = "纬度")
+    private BigDecimal latitude;
 }

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

@@ -37,6 +37,8 @@ public class UserInfoVO implements Serializable {
     private BigDecimal balance;
     @Schema(description = "累计消费")
     private BigDecimal totalConsumption;
+    @Schema(description = "渠道方ID")
+    private Long thirdPartId;
     @Schema(description = "创建时间")
     private LocalDateTime createTime;
 }

+ 4 - 0
src/main/java/com/zsElectric/boot/business/model/vo/applet/AppUserInfoVO.java

@@ -60,5 +60,9 @@ public class AppUserInfoVO implements Serializable {
      * 企业名称
      */
     private String firmName;
+    /**
+     * 渠道方ID
+     */
+    private Long thirdPartId;
 
 }

+ 4 - 0
src/main/java/com/zsElectric/boot/business/service/UserInfoService.java

@@ -95,4 +95,8 @@ public interface UserInfoService extends IService<UserInfo> {
      * @return 恢复的用户数量
      */
     int restoreDeletedUsersByOrderAndAccount();
+
+    UserInfo getUserInfoByPhoneAndOperatorId(String phone, Long thirdPartId);
+
+    UserInfo registerThirdPartyUserByPhone(String phone, Long thirdPartId);
 }

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

@@ -308,6 +308,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
             //渠道方订单设置运营商ID
             if (Objects.equals(formData.getOrderType(), SystemConstants.CHARGE_ORDER_TYPE_CHANNEL)){
                 chargeOrderInfo.setOperatorId(formData.getOperatorId());
+                chargeOrderInfo.setPreAmt(formData.getChannelPreAmt());
             }
 
             //判断用户是否绑定企业
@@ -412,10 +413,7 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
         chargeOrderInfo.setPreAmt(formData.getChannelPreAmt());
         //渠道方订单设置运营商ID
         chargeOrderInfo.setOperatorId(formData.getOperatorId());
-        //车牌号
-        if(ObjectUtil.isNotEmpty(formData.getPlateNum())) {
-            chargeOrderInfo.setPlateNum(formData.getPlateNum());
-        }
+
         //启动充电
         StartChargingRequestDTO requestDTO = new StartChargingRequestDTO();
         requestDTO
@@ -423,8 +421,12 @@ public class ChargeOrderInfoServiceImpl extends ServiceImpl<ChargeOrderInfoMappe
                 .setConnectorID(formData.getConnectorId())
                 .setPhoneNum(formData.getChannelUserPhone())
                 //预支付金额
-                .setChargingAmt(formData.getChannelPreAmt().toString())
-        ;
+                .setChargingAmt(formData.getChannelPreAmt().toString());
+        //车牌号
+        if(ObjectUtil.isNotEmpty(formData.getPlateNum())) {
+            chargeOrderInfo.setPlateNum(formData.getPlateNum());
+            requestDTO.setPlateNum(formData.getPlateNum());
+        }
         StartChargingResponseVO startChargingResponseVO = chargingBusinessService.startCharging(requestDTO);
         if (!Objects.equals(startChargingResponseVO.getSuccStat(), SystemConstants.STATUS_ZERO)) {
             throw new BusinessException(startChargingResponseVO.getFailReasonMsg());

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

@@ -303,4 +303,58 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
         return restoredCount;
     }
 
+    @Override
+    public UserInfo getUserInfoByPhoneAndOperatorId(String phone, Long thirdPartId) {
+        if (StrUtil.isBlank(phone)) {
+            return null;
+        }
+        return this.getOne(
+                new LambdaQueryWrapper<UserInfo>()
+                        .eq(UserInfo::getPhone, phone)
+                        .eq(UserInfo::getThirdPartId, thirdPartId)
+        );
+    }
+
+    @Override
+    public UserInfo registerThirdPartyUserByPhone(String phone, Long thirdPartId) {
+        if (StrUtil.isBlank(phone)) {
+            log.warn("注册用户失败:手机号为空");
+            return null;
+        }
+
+        // 查询用户是否已存在
+
+        UserInfo existingUser = getUserInfoByPhoneAndOperatorId(phone, thirdPartId);
+
+        if (existingUser != null) {
+            log.info("用户已存在,ID: {}, 手机号: {},thirdPartId: {}", existingUser.getId(), phone,thirdPartId);
+            return existingUser;
+        }
+
+        // 用户不存在,创建新用户
+        log.info("创建新用户,手机号: {}, thirdPartId: {}", phone, thirdPartId);
+        UserInfo newUser = new UserInfo();
+        newUser.setPhone(phone);
+        newUser.setThirdPartId(thirdPartId);
+        newUser.setNickName("微信用户_" + phone.substring(phone.length() - 4));
+
+
+        boolean saved = this.save(newUser);
+        if (!saved) {
+            log.error("保存用户失败,手机号: {}", phone);
+            return null;
+        }
+        log.info("用户创建成功,ID: {}, 手机号: {}", newUser.getId(), phone);
+
+        //创建用户账户
+        UserAccount userAccount = new UserAccount();
+        userAccount.setUserId(newUser.getId());
+        userAccount.setBalance(BigDecimal.ZERO);
+        userAccount.setRedeemBalance(BigDecimal.ZERO);
+        userAccount.setIntegral(BigDecimal.ZERO);
+        userAccountMapper.insert(userAccount);
+
+        return newUser;
+    }
+
 }

+ 1 - 0
src/main/java/com/zsElectric/boot/common/constant/SystemConstants.java

@@ -52,6 +52,7 @@ public interface SystemConstants {
     String ACCOUNT_LOG_BACK_TAX_NOTE = "补缴欠费";
     String CHARGE_DEDUCT_NOTE = "充电扣款";
     String ACCOUNT_LOG_THIRD_PARTY_PAY_NOTE = "第三方渠道充值";
+    String ACCOUNT_LOG_THIRD_PARTY_CLEAR_NOTE = "第三方渠道清除余额";
 
     /**
      * 变更记录类型  1-增加 2-减少 3-兑换增加 账户退款

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

@@ -156,4 +156,13 @@ public class ThirdPartyController {
             @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
         return thirdPartyTokenService.queryChargeOrderInfo(request, authorization);
     }
+
+    @Operation(summary = "清除用户余额", description = "第三方清除用户账户余额,需要在Header中携带Authorization")
+    @PostMapping("/clear_user_balance")
+    @Log(value = "第三方清除用户余额", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse clearAccountBalance(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.clearAccountBalance(request, authorization);
+    }
 }

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

@@ -17,5 +17,5 @@ public class ChargeDeviceDetailRequestData implements Serializable {
     /**
      * 设备编码
      */
-    private String connectorId;
+    private String connectorCode;
 }

+ 26 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ClearBalanceRequestData.java

@@ -0,0 +1,26 @@
+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 ClearBalanceRequestData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    @JsonProperty("userId")
+    private Long userId;
+
+}

+ 73 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ClearBalanceResponseData.java

@@ -0,0 +1,73 @@
+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 ClearBalanceResponseData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 结果 (0-成功 1-失败)
+     */
+    @JsonProperty("result")
+    private Integer result;
+
+    /**
+     * 结果消息
+     */
+    @JsonProperty("message")
+    private String message;
+
+    /**
+     * 用户ID
+     */
+    @JsonProperty("userId")
+    private Long userId;
+
+    /**
+     * 清除前余额
+     */
+    @JsonProperty("balanceBefore")
+    private BigDecimal balanceBefore;
+
+    /**
+     * 清除后余额
+     */
+    @JsonProperty("balanceAfter")
+    private BigDecimal balanceAfter;
+
+    /**
+     * 创建成功响应数据
+     */
+    public static ClearBalanceResponseData success(Long userId, BigDecimal balanceBefore, BigDecimal balanceAfter) {
+        ClearBalanceResponseData data = new ClearBalanceResponseData();
+        data.setResult(0);
+        data.setMessage("清除余额成功");
+        data.setUserId(userId);
+        data.setBalanceBefore(balanceBefore);
+        data.setBalanceAfter(balanceAfter);
+        return data;
+    }
+
+    /**
+     * 创建失败响应数据
+     */
+    public static ClearBalanceResponseData fail(String message) {
+        ClearBalanceResponseData data = new ClearBalanceResponseData();
+        data.setResult(1);
+        data.setMessage(message);
+        return data;
+    }
+}

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

@@ -102,4 +102,13 @@ public interface ThirdPartyTokenService {
      * @return 响应结果
      */
     ThirdPartyResponse queryChargingCost(ThirdPartyRequest request, String authorization);
+
+    /**
+     * 清除账户余额
+     *
+     * @param request       第三方请求参数
+     * @param authorization 请求头中的Authorization
+     * @return 响应结果
+     */
+    ThirdPartyResponse clearAccountBalance(ThirdPartyRequest request, String authorization);
 }

+ 110 - 5
src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java

@@ -284,13 +284,13 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             String phone = userInfoRequest.getPhone();
 
             // 6. 根据手机号查询用户信息
-            UserInfo userInfo = userInfoService.getUserInfoByPhone(phone);
+            UserInfo userInfo = userInfoService.getUserInfoByPhoneAndOperatorId(phone, thirdPartyInfo.getId());
 
             Integer isNewUser = 0;
             if (userInfo == null) {
                 // 用户不存在,注册新用户
                 log.info("用户不存在,注册新用户, phone: {}", phone);
-                userInfo = userInfoService.registerOrUpdateUserByPhone(phone, null);
+                userInfo = userInfoService.registerThirdPartyUserByPhone(phone, thirdPartyInfo.getId());
                 if (userInfo == null) {
                     log.error("注册用户失败, phone: {}", phone);
                     return buildErrorResponse(500, "注册用户失败", thirdPartyInfo);
@@ -960,7 +960,7 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             log.info("解密后的请求数据: {}", decryptedData);
 
             ChargeDeviceDetailRequestData detailRequest = objectMapper.readValue(decryptedData, ChargeDeviceDetailRequestData.class);
-            if (detailRequest == null || detailRequest.getConnectorId() == null ) {
+            if (detailRequest == null || detailRequest.getConnectorCode() == null ) {
                 return buildErrorResponse(4003, "请求的业务参数不合法", thirdPartyInfo);
             }
 
@@ -969,13 +969,19 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             String currentTime = LocalTime.now().format(TIME_FORMATTER);
 
             AppletConnectorDetailVO result = thirdPartyConnectorInfoMapper.selectConnectorDetailById(
-                    detailRequest.getConnectorId(), null, currentTime, null, null
+                    detailRequest.getConnectorCode(), null, currentTime, null, null
             );
 
+            // 7. 判空校验
+            if (result == null) {
+                log.warn("充电终端不存在, connectorCode: {}", detailRequest.getConnectorCode());
+                return buildErrorResponse(4004, "充电终端不存在", thirdPartyInfo);
+            }
+
             // 8. 转换为响应数据
             ChargeDeviceDetailResponseData responseData = convertToDeviceDetail(result);
 
-            log.info("查询充电终端详情成功, operatorId: {}, equipmentId: {}", request.getOperatorId(), result.getConnectorId());
+            log.info("查询充电终端详情成功, operatorId: {}, connectorCode: {}", request.getOperatorId(), result.getConnectorCode());
             return buildSuccessResponse(responseData, thirdPartyInfo);
 
         } catch (Exception e) {
@@ -1557,6 +1563,105 @@ public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
             return buildErrorResponse(500, "系统错误: " + e.getMessage(), null);
         }
     }
+
+    @Override
+    public ThirdPartyResponse clearAccountBalance(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);
+
+            ClearBalanceRequestData clearRequest = objectMapper.readValue(decryptedData, ClearBalanceRequestData.class);
+            if (clearRequest == null || clearRequest.getUserId() == null) {
+                return buildErrorResponse(4003, "请求的业务参数不合法,userId不能为空", thirdPartyInfo);
+            }
+
+            Long userId = clearRequest.getUserId();
+
+            // 6. 校验用户是否存在
+            UserInfo userInfo = userInfoService.getOne(Wrappers.lambdaQuery(UserInfo.class).eq(UserInfo::getId, userId).eq(UserInfo::getThirdPartId
+                    , thirdPartyInfo.getId()));
+            if (userInfo == null) {
+                log.warn("用户不存在, userId: {}", userId);
+                ClearBalanceResponseData failData = ClearBalanceResponseData.fail("用户不存在");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
+
+            // 7. 查询用户账户
+            UserAccount userAccount = userAccountService.getOne(
+                    Wrappers.lambdaQuery(UserAccount.class)
+                            .eq(UserAccount::getUserId, userId)
+                            .eq(UserAccount::getIsDeleted, 0)
+                            .last("LIMIT 1")
+            );
+
+            if (userAccount == null) {
+                log.warn("用户账户不存在, userId: {}", userId);
+                ClearBalanceResponseData failData = ClearBalanceResponseData.fail("用户账户不存在");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
+
+            BigDecimal balanceBefore = userAccount.getBalance() != null ? userAccount.getBalance() : BigDecimal.ZERO;
+
+            // 8. 余额为0无需清除
+            if (balanceBefore.compareTo(BigDecimal.ZERO) == 0) {
+                log.info("用户余额已为0, userId: {}", userId);
+                ClearBalanceResponseData responseData = ClearBalanceResponseData.success(userId, BigDecimal.ZERO, BigDecimal.ZERO);
+                return buildSuccessResponse(responseData, thirdPartyInfo);
+            }
+
+            // 9. 清除余额并记录动账日志
+            userAccountService.updateAccountBalanceAndLog(
+                    userId,
+                    balanceBefore.negate(),
+                    SystemConstants.CHANGE_TYPE_REDUCE,
+                    SystemConstants.ACCOUNT_LOG_THIRD_PARTY_CLEAR_NOTE + "-" + request.getOperatorId(),
+                    null
+            );
+
+            log.info("清除账户余额成功, operatorId: {}, userId: {}, balanceBefore: {}", request.getOperatorId(), userId, balanceBefore);
+            ClearBalanceResponseData responseData = ClearBalanceResponseData.success(userId, balanceBefore, BigDecimal.ZERO);
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理clear_account_balance请求异常", e);
+            return buildErrorResponse(500, "系统错误: " + e.getMessage(), null);
+        }
+    }
 }
 
 

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

@@ -123,6 +123,7 @@ security:
     - /charge-business/v1/linkData/**
     - /applet/v1/homePage/** # 用户端分页查询站点信息
     - /applet/v1/wft/order/notify
+    - /third_party/v1/** # 第三方接入接口
   # 只走第三方过滤器、不走其他安全链
   third-party-urls:
     - /charge-business/v1/linkData/notification_start_charge_result
@@ -151,6 +152,8 @@ security:
       - /charge-business/v1/linkData/notification_stop_charge_result
       - /charge-business/v1/linkData/notification_stationStatus
       - /charge-business/v1/linkData/query_token
+      - /applet/v1/homePage/test  # 首页测试接口(包含加密数据)
+      - /third-party/v1/** # 第三方接入接口(数据为加密的Base64,会误判)
     # 额外需要检查的请求头(默认已检查 User-Agent、Referer、X-Forwarded-For)
     check-headers:
 #      - Referer

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

@@ -156,6 +156,8 @@
         <result property="thirdPartPrice" column="third_part_price"/>
         <result property="platformPrice" column="platform_price"/>
         <result property="enterprisePrice" column="enterprise_price"/>
+        <result property="longitude" column="station_lng"/>
+        <result property="latitude" column="station_lat"/>
     </resultMap>
 
     <!-- 小程序首页分页查询站点信息 -->
@@ -164,6 +166,8 @@
             tpsi.id AS station_info_id,
             tpsi.station_name,
             tpsi.station_tips,
+            tpsi.station_lng,
+            tpsi.station_lat,
             <!-- 计算距离(km) -->
             <if test="query.longitude != null and query.latitude != null">
                 ROUND(

+ 2 - 0
src/main/resources/mapper/business/UserInfoMapper.xml

@@ -10,6 +10,7 @@
         fi.name AS firmName,
         ui.phone,
         ui.openid,
+        ui.third_part_id AS thirdPartId,
         IFNULL(ua.balance, 0) AS balance,
         IFNULL(log.total_consumption, 0) AS totalConsumption,
         ui.create_time
@@ -57,6 +58,7 @@
                 ui.nick_name,
                 ui.phone,
                 ui.openid,
+                ui.third_part_id AS thirdPartId,
                 ua.integral,
                 ua.balance,
                 ua.redeem_balance,

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

@@ -43,9 +43,11 @@ public class QueryTokenMain {
             tokenRequestData.setOperatorSecret(OPERATOR_SECRET);
 
             Map<String,Object> map = new HashMap<>();
-            map.put("sortType",1);
-            map.put("longitude",23.129163);
-            map.put("latitude",113.264435);
+//            map.put("sortType",1);
+//            map.put("longitude",23.129163);
+//            map.put("latitude",113.264435);
+            map.put("operatorId","MA9CU5DCB");
+            map.put("operatorSecret","C5rPZ2JIN66y3eBc");
 
             String jsonData = objectMapper.writeValueAsString(map);
             System.out.println("1. 业务数据(明文JSON):");

+ 89 - 0
src/test/java/com/zsElectric/boot/thirdParty/service/VerifyResponseSigMain.java

@@ -0,0 +1,89 @@
+package com.zsElectric.boot.thirdParty.service;
+
+import com.zsElectric.boot.common.util.AESCryptoUtils;
+import com.zsElectric.boot.common.util.HmacMD5Util;
+
+/**
+ * 模拟第三方接收响应后的验签校验
+ * 验证平台返回的响应数据签名是否正确,并解密data内容
+ *
+ * @author wzq
+ */
+public class VerifyResponseSigMain {
+
+    // ===== 配置参数(需根据第三方配置信息修改) =====
+    private static final String SIG_SECRET = "4zbia5MAsaKSoyNk";
+    private static final String DATA_SECRET = "Y3qiVnd9LCLopZFM";
+    private static final String DATA_SECRET_IV = "FzUoFlJa9S2LnKPV";
+
+    public static void main(String[] args) {
+        try {
+            // ===== 接口返回内容 =====
+            int ret = 0;
+            String msg = "";
+            String data = "zGresU7BpTV676xPaLDBjFO5I0W/XUBXlQWnP8eidziDBtrO4JVlNxPWW0tQbiAl1rekKE6l01GHFrlxwLn/YGvZjrMPC/RLjs6M5Dnat99gFhWiRj69LwGDyZTlLT3aSgEyCNNcgBY68gwsOydEEmnojK4kT6WnE7Uslg6ofzrIz3dESjRvIzCp8W+Zr1Ywipom6d/lDSfVq9cgLkCk14+7awOrlnaJP15bUt9SBH4=";
+            String sig = "22EA57A2DC08004BB4B3A778448F25CE";
+
+            System.out.println("========== 第三方接收响应验签校验 ==========\n");
+
+            // 1. 显示响应参数
+            System.out.println("【响应参数】");
+            System.out.println("Ret:  " + ret);
+            System.out.println("Msg:  " + msg);
+            System.out.println("Data: " + data);
+            System.out.println("Sig:  " + sig);
+            System.out.println();
+
+            // 2. 拼接签名内容(响应签名拼接顺序: Ret + Msg + Data)
+            String signContent = ret + msg + data;
+            System.out.println("【签名内容拼接】");
+            System.out.println("拼接方式: Ret + Msg + Data");
+            System.out.println("拼接结果: " + signContent);
+            System.out.println("拼接长度: " + signContent.length());
+            System.out.println();
+
+            // 3. 使用HmacMD5生成签名(注意:genSign方法内部会对拼接内容做toUpperCase)
+            String calculatedSig = HmacMD5Util.genSign(ret, msg, data, SIG_SECRET);
+            System.out.println("【签名计算(genSign, 含toUpperCase)】");
+            System.out.println("计算签名: " + calculatedSig);
+            System.out.println("响应签名: " + sig);
+            System.out.println("签名匹配: " + calculatedSig.equalsIgnoreCase(sig));
+            System.out.println();
+
+            // 4. 直接用原始拼接内容计算签名(不做toUpperCase,对比差异)
+            String rawSig = HmacMD5Util.hmacMD5Hex(signContent, SIG_SECRET);
+            System.out.println("【签名计算(原始拼接, 无toUpperCase)】");
+            System.out.println("计算签名: " + rawSig);
+            System.out.println("响应签名: " + sig);
+            System.out.println("签名匹配: " + rawSig.equalsIgnoreCase(sig));
+            System.out.println();
+
+            // 5. 验签结果
+            boolean isValid = calculatedSig.equalsIgnoreCase(sig) || rawSig.equalsIgnoreCase(sig);
+            System.out.println("【验签结果】");
+            System.out.println("验签: " + (isValid ? "通过 ✓" : "失败 ✗"));
+            System.out.println();
+
+            // 6. 解密data内容
+            if (isValid) {
+                System.out.println("========== 解密响应Data ==========\n");
+                try {
+                    String decryptedData = AESCryptoUtils.decrypt(data, DATA_SECRET, DATA_SECRET_IV);
+                    System.out.println("解密结果:");
+                    System.out.println(decryptedData);
+                } catch (Exception e) {
+                    System.err.println("解密失败: " + e.getMessage());
+                }
+            } else {
+                System.out.println("=== 验签失败,可能原因 ===");
+                System.out.println("1. SIG_SECRET 不正确");
+                System.out.println("2. 签名内容拼接方式与平台不一致");
+                System.out.println("3. 响应数据在传输过程中被篡改");
+            }
+
+        } catch (Exception e) {
+            System.err.println("验证失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}