ソースを参照

feat(system): 新增用户流失率统计及流失用户数据导出

- 完善流失用户判定时间窗口,支持近7天、近1个月、近3个月流失率计算
- 增加导出流失用户列表接口,支持导出用户信息及最后一次充电时间段
- 新增ChurnUserExportDTO定义,匹配导出字段格式
- 数据看板控制器新增流失率相关API接口及导出功能实现
- DataBoardMapper新增查询流失用户列表SQL支持
- DataBoardService接口及实现新增流失用户导出方法逻辑
- 优化历史营业数据按月/日趋势查询逻辑,支持动态月份截止日期
- 新增多个与充电站、设备、订单相关的数据模型,完善第三方模块数据定义
- 增加充电站详情、设备列表等响应数据模型,丰富数据板块功能展示
wzq 1 週間 前
コミット
68417f442f
38 ファイル変更3441 行追加65 行削除
  1. 3 0
      .vscode/settings.json
  2. 26 0
      sql/mysql/zs_electric.sql
  3. 30 0
      src/main/java/com/zsElectric/boot/business/model/entity/ThirdPartyInfo.java
  4. 18 0
      src/main/java/com/zsElectric/boot/business/model/form/ThirdPartyInfoForm.java
  5. 2 0
      src/main/java/com/zsElectric/boot/business/model/query/StationInfoQuery.java
  6. 18 0
      src/main/java/com/zsElectric/boot/business/model/vo/ThirdPartyInfoVO.java
  7. 1 0
      src/main/java/com/zsElectric/boot/charging/quartz/ChargingJob.java
  8. 1 0
      src/main/java/com/zsElectric/boot/common/constant/SystemConstants.java
  9. 131 6
      src/main/java/com/zsElectric/boot/system/controller/DataBoardController.java
  10. 16 0
      src/main/java/com/zsElectric/boot/system/mapper/DataBoardMapper.java
  11. 33 0
      src/main/java/com/zsElectric/boot/system/model/dto/ChurnUserExportDTO.java
  12. 3 1
      src/main/java/com/zsElectric/boot/system/model/query/HistoryBusinessQuery.java
  13. 19 1
      src/main/java/com/zsElectric/boot/system/service/DataBoardService.java
  14. 161 56
      src/main/java/com/zsElectric/boot/system/service/impl/DataBoardServiceImpl.java
  15. 112 0
      src/main/java/com/zsElectric/boot/thirdParty/controller/ThirdPartyController.java
  16. 51 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChangeOrderPayRequestData.java
  17. 26 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeDeviceDetailRequestData.java
  18. 98 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeDeviceDetailResponseData.java
  19. 80 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeOrderPayResponseData.java
  20. 33 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationDetailRequestData.java
  21. 119 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationDetailResponseData.java
  22. 39 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationListRequestData.java
  23. 96 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationListResponseData.java
  24. 31 0
      src/main/java/com/zsElectric/boot/thirdParty/model/QueryTokenRequestData.java
  25. 79 0
      src/main/java/com/zsElectric/boot/thirdParty/model/QueryTokenResponseData.java
  26. 26 0
      src/main/java/com/zsElectric/boot/thirdParty/model/QueryUserInfoRequestData.java
  27. 117 0
      src/main/java/com/zsElectric/boot/thirdParty/model/QueryUserInfoResponseData.java
  28. 31 0
      src/main/java/com/zsElectric/boot/thirdParty/model/RechargeLevelPageRequestData.java
  29. 104 0
      src/main/java/com/zsElectric/boot/thirdParty/model/RechargeLevelPageResponseData.java
  30. 51 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ThirdPartyRequest.java
  31. 74 0
      src/main/java/com/zsElectric/boot/thirdParty/model/ThirdPartyResponse.java
  32. 60 0
      src/main/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenService.java
  33. 1072 0
      src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java
  34. 6 0
      src/main/resources/mapper/business/ThirdPartyInfoMapper.xml
  35. 41 1
      src/main/resources/mapper/system/DataBoardMapper.xml
  36. 107 0
      src/test/java/com/zsElectric/boot/thirdParty/service/QueryRechargeLevelPageMain.java
  37. 104 0
      src/test/java/com/zsElectric/boot/thirdParty/service/QueryTokenMain.java
  38. 422 0
      src/test/java/com/zsElectric/boot/thirdParty/service/ThirdPartyTokenServiceTest.java

+ 3 - 0
.vscode/settings.json

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

+ 26 - 0
sql/mysql/zs_electric.sql

@@ -626,4 +626,30 @@ INSERT INTO `sys_user_role` VALUES (1, 1);
 INSERT INTO `sys_user_role` VALUES (2, 2);
 INSERT INTO `sys_user_role` VALUES (3, 3);
 
+-- ----------------------------
+-- Table structure for c_user_vehicle
+-- ----------------------------
+DROP TABLE IF EXISTS `c_user_vehicle`;
+CREATE TABLE `c_user_vehicle`
+(
+    `id`            bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `user_id`       bigint(20) NOT NULL COMMENT '用户ID,关联c_user_info表',
+    `license_plate` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '车牌号',
+    `brand`         varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '车辆品牌',
+    `model`         varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '车辆型号',
+    `color`         varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '车辆颜色',
+    `vehicle_type`  tinyint(4) NULL DEFAULT 1 COMMENT '车辆类型(1-新能源 2-燃油车 3-混合动力)',
+    `is_default`    tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否默认车辆(0-否 1-是)',
+    `remark`        varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `create_time`   datetime NULL DEFAULT NULL COMMENT '创建时间',
+    `create_by`     bigint(20) NULL DEFAULT NULL COMMENT '创建人ID',
+    `update_time`   datetime NULL DEFAULT NULL COMMENT '更新时间',
+    `update_by`     bigint(20) NULL DEFAULT NULL COMMENT '更新人ID',
+    `is_deleted`    tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除(0-未删除 1-已删除)',
+    PRIMARY KEY (`id`) USING BTREE,
+    INDEX           `idx_user_id`(`user_id`) USING BTREE COMMENT '用户ID索引',
+    INDEX           `idx_license_plate`(`license_plate`) USING BTREE COMMENT '车牌号索引',
+    UNIQUE INDEX `uk_user_default`(`user_id`, `is_default`, `is_deleted`) USING BTREE COMMENT '用户默认车辆唯一约束'
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户车辆信息表' ROW_FORMAT = Dynamic;
+
 SET FOREIGN_KEY_CHECKS = 1;

+ 30 - 0
src/main/java/com/zsElectric/boot/business/model/entity/ThirdPartyInfo.java

@@ -61,6 +61,36 @@ public class ThirdPartyInfo{
      */
     private Integer status;
 
+    /**
+     * 运营商ID
+     */
+    private String operatorId;
+
+    /**
+     * API基础地址
+     */
+    private String apiBaseUrl;
+
+    /**
+     * 运营商密钥
+     */
+    private String operatorSecret;
+
+    /**
+     * 签名密钥
+     */
+    private String sigSecret;
+
+    /**
+     * 数据密钥
+     */
+    private String dataSecret;
+
+    /**
+     * 数据密钥IV
+     */
+    private String dataSecretIV;
+
     /**
      * 备注
      */

+ 18 - 0
src/main/java/com/zsElectric/boot/business/model/form/ThirdPartyInfoForm.java

@@ -40,6 +40,24 @@ public class ThirdPartyInfoForm {
     @Schema(description = "状态(0-正常 1-停用)")
     private Integer status;
 
+    @Schema(description = "运营商ID")
+    private String operatorId;
+
+    @Schema(description = "API基础地址")
+    private String apiBaseUrl;
+
+    @Schema(description = "运营商密钥")
+    private String operatorSecret;
+
+    @Schema(description = "签名密钥")
+    private String sigSecret;
+
+    @Schema(description = "数据密钥")
+    private String dataSecret;
+
+    @Schema(description = "数据密钥IV")
+    private String dataSecretIV;
+
     @Schema(description = "备注")
     private String remark;
 }

+ 2 - 0
src/main/java/com/zsElectric/boot/business/model/query/StationInfoQuery.java

@@ -4,6 +4,7 @@ import com.zsElectric.boot.common.base.BasePageQuery;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.experimental.Accessors;
 
 import java.math.BigDecimal;
 
@@ -16,6 +17,7 @@ import java.math.BigDecimal;
 @Schema(description = "小程序首页站点信息查询对象")
 @Getter
 @Setter
+@Accessors(chain = true)
 public class StationInfoQuery extends BasePageQuery {
 
     /**

+ 18 - 0
src/main/java/com/zsElectric/boot/business/model/vo/ThirdPartyInfoVO.java

@@ -40,6 +40,24 @@ public class ThirdPartyInfoVO {
     @Schema(description = "状态(0-正常 1-停用)")
     private Integer status;
 
+    @Schema(description = "运营商ID")
+    private String operatorId;
+
+    @Schema(description = "API基础地址")
+    private String apiBaseUrl;
+
+    @Schema(description = "运营商密钥")
+    private String operatorSecret;
+
+    @Schema(description = "签名密钥")
+    private String sigSecret;
+
+    @Schema(description = "数据密钥")
+    private String dataSecret;
+
+    @Schema(description = "数据密钥IV")
+    private String dataSecretIV;
+
     @Schema(description = "备注")
     private String remark;
 

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

@@ -8,6 +8,7 @@ import com.zsElectric.boot.charging.vo.ChargingPricePolicyVO;
 import com.zsElectric.boot.charging.vo.QueryStationsInfoVO;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 

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

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

+ 131 - 6
src/main/java/com/zsElectric/boot/system/controller/DataBoardController.java

@@ -1,8 +1,10 @@
 package com.zsElectric.boot.system.controller;
 
+import cn.idev.excel.EasyExcel;
 import com.zsElectric.boot.common.annotation.Log;
 import com.zsElectric.boot.common.enums.LogModuleEnum;
 import com.zsElectric.boot.core.web.Result;
+import com.zsElectric.boot.system.model.dto.ChurnUserExportDTO;
 import com.zsElectric.boot.system.model.query.HistoryBusinessQuery;
 import com.zsElectric.boot.system.model.query.StationRankQuery;
 import com.zsElectric.boot.system.model.vo.*;
@@ -10,6 +12,7 @@ import com.zsElectric.boot.system.service.DataBoardService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springdoc.core.annotations.ParameterObject;
@@ -18,8 +21,30 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
 /**
  * 数据看板控制器
+ * <p>
+ * 提供首页数据看板的各项统计数据接口,包括:
+ * <ul>
+ *     <li>实时数据:累计总数据和今日数据</li>
+ *     <li>充电度数时段对比趋势:今日与对比日的24小时充电度数对比</li>
+ *     <li>历史营业数据:按日/月维度展示充电度数、金额、订单、用户趋势</li>
+ *     <li>电站充电数据:热门充电站排名和波动排名</li>
+ *     <li>用户流失率:近7天、近1月、近3月的用户流失率统计</li>
+ * </ul>
+ * </p>
+ * <p>
+ * 数据格式规范:
+ * <ul>
+ *     <li>金额、度数等数值保留2位小数</li>
+ *     <li>充电金额、充电度数的统计日切时间以订单完成时间为准</li>
+ * </ul>
+ * </p>
  *
  * @author zsElectric
  * @since 2026-03-09
@@ -33,7 +58,17 @@ public class DataBoardController {
 
     private final DataBoardService dataBoardService;
 
-    @Operation(summary = "获取实时数据(累计总数据)")
+    // ==================== 实时数据模块 ====================
+
+    /**
+     * 获取实时数据(累计总数据)
+     * <p>
+     * 包含字段:总充电度数、总充电金额、总抵扣券购买金额、总服务费金额、总退款金额、
+     * 总实付金额、总首单立减、总优惠券减免、总企业专享价减、总分销佣金
+     * </p>
+     */
+    @Operation(summary = "获取实时数据(累计总数据)",
+            description = "获取平台累计的各项统计数据,包括充电度数、金额、退款、佣金等")
     @GetMapping("/realTimeData")
     @Log(value = "获取实时数据", module = LogModuleEnum.OTHER)
     public Result<DataBoardRealTimeVO> getRealTimeData() {
@@ -41,7 +76,15 @@ public class DataBoardController {
         return Result.success(data);
     }
 
-    @Operation(summary = "获取今日实时数据")
+    /**
+     * 获取今日实时数据
+     * <p>
+     * 包含字段:今日充电度数、今日充电金额、今日抵扣券购买金额、今日服务费金额、今日退款金额、
+     * 今日实付金额、今日首单立减、今日优惠券减免、今日企业专享价减、今日佣金
+     * </p>
+     */
+    @Operation(summary = "获取今日实时数据",
+            description = "获取今日的各项统计数据,数据会实时更新")
     @GetMapping("/todayData")
     @Log(value = "获取今日实时数据", module = LogModuleEnum.OTHER)
     public Result<DataBoardTodayVO> getTodayData() {
@@ -49,7 +92,19 @@ public class DataBoardController {
         return Result.success(data);
     }
 
-    @Operation(summary = "获取充电度数时段对比趋势")
+    // ==================== 充电度数时段对比趋势模块 ====================
+
+    /**
+     * 获取充电度数时段对比趋势
+     * <p>
+     * 返回今日与对比日的24小时充电度数数据,用于绘制时段对比趋势图。
+     * 横坐标为24小时(0-23),纵坐标为充电度数。
+     * </p>
+     *
+     * @param compareDate 对比日期,格式:MM-dd,可选范围:近15日(昨天之前的15天,含昨天),不传则默认昨天
+     */
+    @Operation(summary = "获取充电度数时段对比趋势",
+            description = "获取今日与对比日的24小时充电度数对比数据,用于绘制日时段对比趋势图")
     @GetMapping("/chargePowerTrend")
     @Log(value = "获取充电度数时段趋势", module = LogModuleEnum.OTHER)
     public Result<ChargePowerTrendVO> getChargePowerTrend(
@@ -58,7 +113,22 @@ public class DataBoardController {
         return Result.success(data);
     }
 
-    @Operation(summary = "获取历史营业数据")
+    // ==================== 历史营业数据模块 ====================
+
+    /**
+     * 获取历史营业数据
+     * <p>
+     * 支持按日趋势或月趋势查询,数据类型包括:充电度数、充电金额、有效订单、注册用户。
+     * </p>
+     * <ul>
+     *     <li>日趋势:yearMonth 格式为 yyyy-MM,查询该月每日数据</li>
+     *     <li>月趋势:yearMonth 格式为 yyyy,查询该年每月汇总数据</li>
+     * </ul>
+     *
+     * @param query 查询参数,包含:timeDimension(时间维度)、dataType(数据类型)、yearMonth(时间参数,必传)
+     */
+    @Operation(summary = "获取历史营业数据",
+            description = "获取历史营业趋势数据。日趋势时yearMonth传yyyy-MM查询该月每日数据;月趋势时yearMonth传yyyy查询该年每月汇总数据")
     @GetMapping("/historyBusinessData")
     @Log(value = "获取历史营业数据", module = LogModuleEnum.OTHER)
     public Result<HistoryBusinessDataVO> getHistoryBusinessData(@ParameterObject HistoryBusinessQuery query) {
@@ -66,7 +136,19 @@ public class DataBoardController {
         return Result.success(data);
     }
 
-    @Operation(summary = "获取站点排名数据")
+    // ==================== 电站充电数据模块 ====================
+
+    /**
+     * 获取站点排名数据
+     * <p>
+     * 返回热门充电站排名(按充电度数排序)和波动充电站排名(按度数波动百分比排序)。
+     * 波动计算方式:(当前周期 - 上一周期) / 上一周期 * 100%
+     * </p>
+     *
+     * @param query 查询参数,包含:timeRange(时间范围)、startDate(开始日期)、endDate(结束日期)
+     */
+    @Operation(summary = "获取站点排名数据",
+            description = "获取电站充电数据排名,包括热门充电站排名和波动充电站排名")
     @GetMapping("/stationRankData")
     @Log(value = "获取站点排名数据", module = LogModuleEnum.OTHER)
     public Result<StationRankListVO> getStationRankData(@ParameterObject StationRankQuery query) {
@@ -74,11 +156,54 @@ public class DataBoardController {
         return Result.success(data);
     }
 
-    @Operation(summary = "获取用户流失率")
+    // ==================== 用户流失率模块 ====================
+
+    /**
+     * 获取用户流失率
+     * <p>
+     * 返回近7天、近1个月、近3个月的用户流失率。
+     * 计算公式:(某期间活跃但后续未活跃的用户数 / 该期间活跃用户数) * 100%
+     * </p>
+     */
+    @Operation(summary = "获取用户流失率",
+            description = "获取近7天、近1个月、近3个月的用户流失率统计")
     @GetMapping("/userChurnRate")
     @Log(value = "获取用户流失率", module = LogModuleEnum.OTHER)
     public Result<UserChurnRateVO> getUserChurnRate() {
         UserChurnRateVO data = dataBoardService.getUserChurnRate();
         return Result.success(data);
     }
+
+    /**
+     * 导出流失用户列表
+     * <p>
+     * 导出指定时段的流失用户,包含用户个人信息和最后一次充电信息。
+     * </p>
+     *
+     * @param periodType 时段类型:7d-近7天,1m-近1个月,3m-近3个月
+     */
+    @Operation(summary = "导出流失用户列表",
+            description = "导出流失用户的个人信息和最后一次充电信息")
+    @GetMapping("/churnUsers/export")
+    @Log(value = "导出流失用户", module = LogModuleEnum.OTHER)
+    public void exportChurnUsers(
+            @Parameter(description = "时段类型:7d-近7天,1m-近1个月,3m-近3个月", example = "1m")
+            @RequestParam(defaultValue = "1m") String periodType,
+            HttpServletResponse response) throws IOException {
+
+        String periodName = switch (periodType) {
+            case "7d" -> "近7天";
+            case "3m" -> "近3个月";
+            default -> "近1个月";
+        };
+        String fileName = periodName + "用户流失情况表.xlsx";
+
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
+
+        List<ChurnUserExportDTO> exportList = dataBoardService.getChurnUserExportList(periodType);
+        EasyExcel.write(response.getOutputStream(), ChurnUserExportDTO.class)
+                .sheet(periodName + " 用户流失情况表")
+                .doWrite(exportList);
+    }
 }

+ 16 - 0
src/main/java/com/zsElectric/boot/system/mapper/DataBoardMapper.java

@@ -1,5 +1,6 @@
 package com.zsElectric.boot.system.mapper;
 
+import com.zsElectric.boot.system.model.dto.ChurnUserExportDTO;
 import com.zsElectric.boot.system.model.vo.DataBoardRealTimeVO;
 import com.zsElectric.boot.system.model.vo.DataBoardTodayVO;
 import com.zsElectric.boot.system.model.vo.StationRankVO;
@@ -152,4 +153,19 @@ public interface DataBoardMapper {
      * @return 累计首单金额
      */
     BigDecimal selectTotalFirstOrderAmount();
+
+    /**
+     * 获取流失用户列表(在早期有充电记录,但在最近时间段内没有充电记录的用户)
+     * <p>
+     * 包含用户个人信息和最后一次充电信息
+     * </p>
+     *
+     * @param earlyStart  早期开始日期
+     * @param earlyEnd    早期结束日期
+     * @param recentStart 最近开始日期
+     * @param recentEnd   最近结束日期
+     * @return 流失用户列表
+     */
+    List<ChurnUserExportDTO> selectChurnUserList(@Param("earlyStart") String earlyStart, @Param("earlyEnd") String earlyEnd,
+                                                 @Param("recentStart") String recentStart, @Param("recentEnd") String recentEnd);
 }

+ 33 - 0
src/main/java/com/zsElectric/boot/system/model/dto/ChurnUserExportDTO.java

@@ -0,0 +1,33 @@
+package com.zsElectric.boot.system.model.dto;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
+import lombok.Data;
+
+/**
+ * 流失用户导出DTO
+ * <p>
+ * 根据原型图格式:序号、用户昵称、用户手机号码、最后一次充电时间段
+ * </p>
+ *
+ * @author zsElectric
+ * @since 2026-03-10
+ */
+@Data
+@ColumnWidth(20)
+public class ChurnUserExportDTO {
+
+    @ExcelProperty(value = "序号", index = 0)
+    private Integer rowNum;
+
+    @ExcelProperty(value = "用户昵称", index = 1)
+    private String nickname;
+
+    @ColumnWidth(15)
+    @ExcelProperty(value = "用户手机号码", index = 2)
+    private String phone;
+
+    @ColumnWidth(40)
+    @ExcelProperty(value = "最后一次充电时间段", index = 3)
+    private String lastChargeTimePeriod;
+}

+ 3 - 1
src/main/java/com/zsElectric/boot/system/model/query/HistoryBusinessQuery.java

@@ -1,6 +1,7 @@
 package com.zsElectric.boot.system.model.query;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
 import lombok.Data;
 
 /**
@@ -19,6 +20,7 @@ public class HistoryBusinessQuery {
     @Schema(description = "数据类型:chargePower-充电度数,chargeAmount-充电金额,validOrders-有效订单,registerUsers-注册用户", example = "chargePower")
     private String dataType;
 
-    @Schema(description = "年月(格式:yyyy-MM,月趋势时使用)", example = "2026-03")
+    @NotBlank(message = "时间参数不能为空")
+    @Schema(description = "时间参数(必传):日趋势时传年月(格式:yyyy-MM),月趋势时传年(格式:yyyy)", example = "2026-03", requiredMode = Schema.RequiredMode.REQUIRED)
     private String yearMonth;
 }

+ 19 - 1
src/main/java/com/zsElectric/boot/system/service/DataBoardService.java

@@ -1,9 +1,12 @@
 package com.zsElectric.boot.system.service;
 
+import com.zsElectric.boot.system.model.dto.ChurnUserExportDTO;
 import com.zsElectric.boot.system.model.query.HistoryBusinessQuery;
 import com.zsElectric.boot.system.model.query.StationRankQuery;
 import com.zsElectric.boot.system.model.vo.*;
 
+import java.util.List;
+
 /**
  * 数据看板服务接口
  *
@@ -36,8 +39,12 @@ public interface DataBoardService {
 
     /**
      * 获取历史营业数据
+     * <p>
+     * 日趋势:yearMonth 格式为 yyyy-MM,查询该月每日数据;
+     * 月趋势:yearMonth 格式为 yyyy,查询该年每月汇总数据
+     * </p>
      *
-     * @param query 查询参数
+     * @param query 查询参数(yearMonth 必传)
      * @return 历史营业数据VO
      */
     HistoryBusinessDataVO getHistoryBusinessData(HistoryBusinessQuery query);
@@ -56,4 +63,15 @@ public interface DataBoardService {
      * @return 用户流失率VO
      */
     UserChurnRateVO getUserChurnRate();
+
+    /**
+     * 获取流失用户列表(用于导出)
+     * <p>
+     * 包含用户个人信息和最后一次充电信息
+     * </p>
+     *
+     * @param periodType 时段类型:7d-近7天,1m-近1个月,3m-近3个月
+     * @return 流失用户列表
+     */
+    List<ChurnUserExportDTO> getChurnUserExportList(String periodType);
 }

+ 161 - 56
src/main/java/com/zsElectric/boot/system/service/impl/DataBoardServiceImpl.java

@@ -2,6 +2,7 @@ package com.zsElectric.boot.system.service.impl;
 
 import cn.hutool.core.util.StrUtil;
 import com.zsElectric.boot.system.mapper.DataBoardMapper;
+import com.zsElectric.boot.system.model.dto.ChurnUserExportDTO;
 import com.zsElectric.boot.system.model.query.HistoryBusinessQuery;
 import com.zsElectric.boot.system.model.query.StationRankQuery;
 import com.zsElectric.boot.system.model.vo.*;
@@ -35,6 +36,7 @@ public class DataBoardServiceImpl implements DataBoardService {
 
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final DateTimeFormatter MM_DD_FORMATTER = DateTimeFormatter.ofPattern("MM-dd");
 
     @Override
     public DataBoardRealTimeVO getRealTimeData() {
@@ -125,70 +127,116 @@ public class DataBoardServiceImpl implements DataBoardService {
         dataVO.setDataType(query.getDataType());
         dataVO.setTimeDimension(query.getTimeDimension());
 
-        // 计算日期范围
-        LocalDate startDate;
-        LocalDate endDate;
-
-        if ("month".equalsIgnoreCase(query.getTimeDimension()) && StrUtil.isNotBlank(query.getYearMonth())) {
-            // 按月趋势:查询指定月份的每日数据
-            YearMonth yearMonth = YearMonth.parse(query.getYearMonth());
-            startDate = yearMonth.atDay(1);
-            endDate = yearMonth.atEndOfMonth();
-        } else {
-            // 按日趋势:默认查询最近30天
-            endDate = LocalDate.now();
-            startDate = endDate.minusDays(29);
-        }
-
-        String startDateStr = startDate.format(DATE_FORMATTER);
-        String endDateStr = endDate.format(DATE_FORMATTER);
+        LocalDate today = LocalDate.now();
+        List<String> labels = new ArrayList<>();
+        List<BigDecimal> values = new ArrayList<>();
 
-        List<Map<String, Object>> rawData;
         String dataType = query.getDataType();
         if (StrUtil.isBlank(dataType)) {
             dataType = "chargePower";
         }
 
+        if ("month".equalsIgnoreCase(query.getTimeDimension())) {
+            // 月趋势:yearMonth 格式为 yyyy,查询该年每月汇总数据
+            String yearStr = query.getYearMonth();
+            if (yearStr.contains("-")) {
+                throw new IllegalArgumentException("月趋势时yearMonth格式应为yyyy(如2026),当前传入:" + yearStr);
+            }
+            int year = Integer.parseInt(yearStr);
+            int currentYear = today.getYear();
+            int endMonth = (year == currentYear) ? today.getMonthValue() : 12;
+
+            for (int month = 1; month <= endMonth; month++) {
+                YearMonth ym = YearMonth.of(year, month);
+                LocalDate monthStart = ym.atDay(1);
+                LocalDate monthEnd = ym.atEndOfMonth();
+                // 如果是当月,截止到今天
+                if (year == currentYear && month == today.getMonthValue()) {
+                    monthEnd = today;
+                }
+
+                String startDateStr = monthStart.format(DATE_FORMATTER);
+                String endDateStr = monthEnd.format(DATE_FORMATTER);
+
+                BigDecimal monthValue = getMonthlyDataByType(dataType, startDateStr, endDateStr);
+                labels.add(String.format("%02d", month));
+                values.add(monthValue);
+            }
+        } else {
+            // 日趋势:yearMonth 格式为 yyyy-MM,查询该月每日数据
+            String yearMonthStr = query.getYearMonth();
+            if (!yearMonthStr.contains("-")) {
+                throw new IllegalArgumentException("日趋势时yearMonth格式应为yyyy-MM(如2026-03),当前传入:" + yearMonthStr);
+            }
+            YearMonth yearMonth = YearMonth.parse(yearMonthStr);
+            LocalDate startDate = yearMonth.atDay(1);
+            LocalDate endDate;
+            // 如果是当月则到今天
+            if (yearMonth.equals(YearMonth.from(today))) {
+                endDate = today;
+            } else {
+                endDate = yearMonth.atEndOfMonth();
+            }
+
+            String startDateStr = startDate.format(DATE_FORMATTER);
+            String endDateStr = endDate.format(DATE_FORMATTER);
+
+            List<Map<String, Object>> rawData = getDailyDataByType(dataType, startDateStr, endDateStr);
+
+            // 转换数据
+            Map<String, BigDecimal> dataMap = new LinkedHashMap<>();
+            for (Map<String, Object> item : rawData) {
+                String label = String.valueOf(item.get("dateLabel"));
+                BigDecimal value = item.get("value") != null ?
+                        new BigDecimal(String.valueOf(item.get("value"))) : BigDecimal.ZERO;
+                dataMap.put(label, value);
+            }
+
+            // 填充所有日期
+            LocalDate current = startDate;
+            DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM.dd");
+            while (!current.isAfter(endDate)) {
+                String label = current.format(labelFormatter);
+                labels.add(label);
+                values.add(dataMap.getOrDefault(label, BigDecimal.ZERO));
+                current = current.plusDays(1);
+            }
+        }
+
+        dataVO.setLabels(labels);
+        dataVO.setValues(values);
+        return dataVO;
+    }
+
+    /**
+     * 根据数据类型获取每日数据
+     */
+    private List<Map<String, Object>> getDailyDataByType(String dataType, String startDateStr, String endDateStr) {
         switch (dataType) {
             case "chargeAmount":
-                rawData = dataBoardMapper.selectDailyChargeAmount(startDateStr, endDateStr);
-                break;
+                return dataBoardMapper.selectDailyChargeAmount(startDateStr, endDateStr);
             case "validOrders":
-                rawData = dataBoardMapper.selectDailyValidOrders(startDateStr, endDateStr);
-                break;
+                return dataBoardMapper.selectDailyValidOrders(startDateStr, endDateStr);
             case "registerUsers":
-                rawData = dataBoardMapper.selectDailyRegisterUsers(startDateStr, endDateStr);
-                break;
+                return dataBoardMapper.selectDailyRegisterUsers(startDateStr, endDateStr);
             case "chargePower":
             default:
-                rawData = dataBoardMapper.selectDailyChargePower(startDateStr, endDateStr);
-                break;
+                return dataBoardMapper.selectDailyChargePower(startDateStr, endDateStr);
         }
+    }
 
-        // 转换数据
-        List<String> labels = new ArrayList<>();
-        List<BigDecimal> values = new ArrayList<>();
-        Map<String, BigDecimal> dataMap = new LinkedHashMap<>();
+    /**
+     * 根据数据类型获取月度汇总数据
+     */
+    private BigDecimal getMonthlyDataByType(String dataType, String startDateStr, String endDateStr) {
+        List<Map<String, Object>> rawData = getDailyDataByType(dataType, startDateStr, endDateStr);
+        BigDecimal total = BigDecimal.ZERO;
         for (Map<String, Object> item : rawData) {
-            String label = String.valueOf(item.get("dateLabel"));
             BigDecimal value = item.get("value") != null ?
                     new BigDecimal(String.valueOf(item.get("value"))) : BigDecimal.ZERO;
-            dataMap.put(label, value);
+            total = total.add(value);
         }
-
-        // 填充所有日期
-        LocalDate current = startDate;
-        DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM.dd");
-        while (!current.isAfter(endDate)) {
-            String label = current.format(labelFormatter);
-            labels.add(label);
-            values.add(dataMap.getOrDefault(label, BigDecimal.ZERO));
-            current = current.plusDays(1);
-        }
-
-        dataVO.setLabels(labels);
-        dataVO.setValues(values);
-        return dataVO;
+        return total;
     }
 
     @Override
@@ -257,20 +305,30 @@ public class DataBoardServiceImpl implements DataBoardService {
     public UserChurnRateVO getUserChurnRate() {
         UserChurnRateVO churnRateVO = new UserChurnRateVO();
         LocalDate today = LocalDate.now();
-
-        // 计算近7天流失率
-        BigDecimal sevenDayRate = calculateChurnRate(today.minusDays(14), today.minusDays(7),
-                today.minusDays(6), today);
+        LocalDate yesterday = today.minusDays(1);
+
+        // 计算近7天流失率(近7天=昨天之前7天,含昨天,即 today-7 到 today-1)
+        // 早期用户:7天之前有充电的用户(today-14 到 today-8)
+        // 流失判断:这些用户在近7天内(today-7 到 today-1)没有充电
+        BigDecimal sevenDayRate = calculateChurnRate(
+                today.minusDays(14), today.minusDays(8),
+                today.minusDays(7), yesterday);
         churnRateVO.setSevenDayChurnRate(sevenDayRate);
 
-        // 计算近1个月流失率
-        BigDecimal oneMonthRate = calculateChurnRate(today.minusDays(60), today.minusDays(30),
-                today.minusDays(29), today);
+        // 计算近1个月流失率(近1个月=昨天之前30天,含昨天,即 today-30 到 today-1)
+        // 早期用户:30天之前有充电的用户(today-60 到 today-31)
+        // 流失判断:这些用户在近30天内(today-30 到 today-1)没有充电
+        BigDecimal oneMonthRate = calculateChurnRate(
+                today.minusDays(60), today.minusDays(31),
+                today.minusDays(30), yesterday);
         churnRateVO.setOneMonthChurnRate(oneMonthRate);
 
-        // 计算近3个月流失率
-        BigDecimal threeMonthRate = calculateChurnRate(today.minusDays(180), today.minusDays(90),
-                today.minusDays(89), today);
+        // 计算近3个月流失率(近3个月=昨天之前90天,含昨天,即 today-90 到 today-1)
+        // 早期用户:90天之前有充电的用户(today-180 到 today-91)
+        // 流失判断:这些用户在近90天内(today-90 到 today-1)没有充电
+        BigDecimal threeMonthRate = calculateChurnRate(
+                today.minusDays(180), today.minusDays(91),
+                today.minusDays(90), yesterday);
         churnRateVO.setThreeMonthChurnRate(threeMonthRate);
 
         return churnRateVO;
@@ -407,4 +465,51 @@ public class DataBoardServiceImpl implements DataBoardService {
         if (vo.getTodayFirmDiscountAmount() == null) vo.setTodayFirmDiscountAmount(BigDecimal.ZERO);
         if (vo.getTodayCommissionAmount() == null) vo.setTodayCommissionAmount(BigDecimal.ZERO);
     }
+
+    @Override
+    public List<ChurnUserExportDTO> getChurnUserExportList(String periodType) {
+        LocalDate today = LocalDate.now();
+        LocalDate yesterday = today.minusDays(1);
+
+        LocalDate earlyStart;
+        LocalDate earlyEnd;
+        LocalDate recentStart;
+        LocalDate recentEnd = yesterday;
+
+        // 根据时段类型计算日期范围
+        switch (periodType) {
+            case "7d":
+                // 近7天流失用户
+                earlyStart = today.minusDays(14);
+                earlyEnd = today.minusDays(8);
+                recentStart = today.minusDays(7);
+                break;
+            case "3m":
+                // 近3个月流失用户
+                earlyStart = today.minusDays(180);
+                earlyEnd = today.minusDays(91);
+                recentStart = today.minusDays(90);
+                break;
+            case "1m":
+            default:
+                // 近1个月流失用户(默认)
+                earlyStart = today.minusDays(60);
+                earlyEnd = today.minusDays(31);
+                recentStart = today.minusDays(30);
+                break;
+        }
+
+        List<ChurnUserExportDTO> list = dataBoardMapper.selectChurnUserList(
+                earlyStart.format(DATE_FORMATTER),
+                earlyEnd.format(DATE_FORMATTER),
+                recentStart.format(DATE_FORMATTER),
+                recentEnd.format(DATE_FORMATTER)
+        );
+
+        // 设置序号
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setRowNum(i + 1);
+        }
+        return list;
+    }
 }

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

@@ -0,0 +1,112 @@
+package com.zsElectric.boot.thirdParty.controller;
+
+import com.zsElectric.boot.common.annotation.Log;
+import com.zsElectric.boot.common.enums.LogModuleEnum;
+import com.zsElectric.boot.common.util.AESCryptoUtils;
+import com.zsElectric.boot.thirdParty.model.ThirdPartyRequest;
+import com.zsElectric.boot.thirdParty.model.ThirdPartyResponse;
+import com.zsElectric.boot.thirdParty.service.ThirdPartyTokenService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 第三方接入控制器
+ * 提供给第三方平台调用的接口
+ *
+ * @author wzq
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@Tag(name = "第三方接入接口")
+@RequestMapping("/third-party/v1")
+public class ThirdPartyController {
+
+    private final ThirdPartyTokenService thirdPartyTokenService;
+
+    /**
+     * 获取Token
+     * Token作为全局唯一凭证,调用各接口时均需要使用
+     * <p>
+     * 请求格式:
+     * - 消息头: Content-Type: application/json
+     * - 入参消息体: operatorId, data(加密), timeStamp, seq, sig
+     * <p>
+     * 加密说明:
+     * - data 里的内容为接口的实际入参,需要密文传输
+     * - 加解密方法: AES 128位, CBC模式, PKCS5Padding
+     * - 签名(sig): HMAC-MD5算法, 入参拼接顺序: operatorId+data+timeStamp+seq
+     * - 签名必须大写
+     *
+     * @param request 请求参数
+     * @return 响应结果
+     */
+    @Operation(summary = "获取Token", description = "第三方平台获取访问令牌")
+    @PostMapping("/query_token")
+    @Log(value = "第三方获取Token", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryToken(@RequestBody ThirdPartyRequest request) {
+        log.info("收到query_token请求, operatorId: {}", request.getOperatorId());
+        return thirdPartyTokenService.queryToken(request);
+    }
+
+    @Operation(summary = "充值档位信息分页列表", description = "第三方获取充值档位分页数据,需要在Header中携带Authorization")
+    @PostMapping("/query_recharge_level_page")
+    @Log(value = "第三方查询充值档位", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryRechargeLevelPage(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.queryRechargeLevelPage(request, authorization);
+    }
+
+    @Operation(summary = "获取用户信息", description = "第三方根据手机号获取用户信息,需要在Header中携带Authorization")
+    @PostMapping("/query_user_info")
+    @Log(value = "第三方根据手机号获取用户信息", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryUserInfo(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.queryUserInfo(request, authorization);
+    }
+
+    @Operation(summary = "充点券购买", description = "第三方充点券购买,需要在Header中携带Authorization")
+    @PostMapping("/charge_order_pay")
+    @Log(value = "第三方充点券购买", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse charge_order_pay(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.chargeOrderPay(request, authorization);
+    }
+
+    @Operation(summary = "获取充电站列表", description = "第三方获取充电站列表,需要在Header中携带Authorization")
+    @PostMapping("/query_charge_station_list")
+    @Log(value = "第三方获取充电站列表", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryChargeStationList(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.queryChargeStationList(request, authorization);
+    }
+
+    @Operation(summary = "获取充电站详情与充电终端列表", description = "第三方获取充电站详情与充电设备列表,需要在Header中携带Authorization")
+    @PostMapping("/query_charge_station_detail")
+    @Log(value = "第三方获取充电站详情与充电终端列表", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryChargeStationDetail(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.queryChargeStationDetail(request, authorization);
+    }
+
+    @Operation(summary = "获取充电终端详情", description = "第三方获取充电终端详情,需要在Header中携带Authorization")
+    @PostMapping("/query_charge_device_detail")
+    @Log(value = "第三方获取充电终端详情", module = LogModuleEnum.OTHER, params = true, result = true)
+    public ThirdPartyResponse queryChargeDeviceDetail(
+            @RequestBody ThirdPartyRequest request,
+            @Parameter(description = "访问令牌,格式: Bearer {accessToken}") @RequestHeader(value = "Authorization", required = false) String authorization) {
+        return thirdPartyTokenService.queryChargeDeviceDetail(request, authorization);
+    }
+}

+ 51 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChangeOrderPayRequestData.java

@@ -0,0 +1,51 @@
+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 ChangeOrderPayRequestData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 充值档位ID
+     */
+    @JsonProperty("levelId")
+    private Long levelId;
+    /**
+     * 用户ID
+     */
+    @JsonProperty("userId")
+    private Long userId;
+    /**
+     * 手机号
+     */
+    @JsonProperty("phone")
+    private String phone;
+    /**
+     * 订单号
+     */
+    @JsonProperty("orderNo")
+    private String orderNo;
+    /**
+     * 支付时间
+     */
+    @JsonProperty("payTime")
+    private String payTime;
+    /**
+     * 总金额
+     */
+    @JsonProperty("totalMoney")
+    private String totalMoney;
+
+}

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

@@ -0,0 +1,26 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 充电设备详情请求数据
+ *
+ * @author wzq
+ */
+@Data
+public class ChargeDeviceDetailRequestData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 设备主键ID
+     */
+    private Long id;
+
+    /**
+     * 设备编码
+     */
+    private String equipmentId;
+}

+ 98 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeDeviceDetailResponseData.java

@@ -0,0 +1,98 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 充电设备详情响应数据
+ *
+ * @author wzq
+ */
+@Data
+public class ChargeDeviceDetailResponseData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "充电设备接口ID")
+    private Long connectorId;
+
+    @Schema(description = "充电设备接口编码")
+    private String connectorCode;
+
+    @Schema(description = "充电设备接口名称")
+    private String connectorName;
+
+    @Schema(description = "充电站ID")
+    private Long stationId;
+
+    @Schema(description = "充电站名称")
+    private String stationName;
+
+    @Schema(description = "充电站地址")
+    private String stationAddress;
+
+    @Schema(description = "设备ID")
+    private Long equipmentId;
+
+    @Schema(description = "设备编码")
+    private String equipmentCode;
+
+    @Schema(description = "设备名称")
+    private String equipmentName;
+
+    @Schema(description = "设备类型:1-直流设备 2-交流设备 3-交直流一体设备 4-无线设备 5-其他")
+    private Integer equipmentType;
+
+    @Schema(description = "设备类型名称")
+    private String equipmentTypeName;
+
+    @Schema(description = "车位号")
+    private String parkNo;
+
+    @Schema(description = "终端状态:0-离网 1-空闲 2-占用(未充电) 3-占用(充电中) 4-占用(预约锁定) 255-故障")
+    private Integer status;
+
+    @Schema(description = "终端状态名称")
+    private String statusName;
+
+    @Schema(description = "接口类型:1-家用插座 2-交流接口插座 3-交流接口插头 4-直流接口枪头 5-无线充电座 6-其他")
+    private Integer connectorType;
+
+    @Schema(description = "接口类型名称")
+    private String connectorTypeName;
+
+    @Schema(description = "额定电压上限(V)")
+    private Integer voltageUpperLimits;
+
+    @Schema(description = "额定电压下限(V)")
+    private Integer voltageLowerLimits;
+
+    @Schema(description = "额定电流(A)")
+    private Integer current;
+
+    @Schema(description = "额定功率(kW)")
+    private BigDecimal power;
+
+    @Schema(description = "国家标准:1-2011 2-2015")
+    private Integer nationalStandard;
+
+    @Schema(description = "国家标准名称")
+    private String nationalStandardName;
+
+    @Schema(description = "常规价格(元/度),电价+服务费+常规运营费+增值费用")
+    private BigDecimal currentPrice;
+
+    @Schema(description = "时段标志(1-尖,2-峰,3-平,4-谷)")
+    private Integer periodFlag;
+
+    @Schema(description = "当前时段描述")
+    private String currentPeriodDesc;
+
+    @Schema(description = "提示语/停车费说明")
+    private String parkingTips;
+
+
+}

+ 80 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeOrderPayResponseData.java

@@ -0,0 +1,80 @@
+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 ChargeOrderPayResponseData 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("rechargeMoney")
+    private BigDecimal rechargeMoney;
+
+    /**
+     * 充值后余额
+     */
+    @JsonProperty("balanceAfter")
+    private BigDecimal balanceAfter;
+
+    /**
+     * 第三方订单号
+     */
+    @JsonProperty("orderNo")
+    private String orderNo;
+
+    /**
+     * 创建成功响应数据
+     */
+    public static ChargeOrderPayResponseData success(Long userId, BigDecimal rechargeMoney, BigDecimal balanceAfter, String orderNo) {
+        ChargeOrderPayResponseData data = new ChargeOrderPayResponseData();
+        data.setResult(0);
+        data.setMessage("充值成功");
+        data.setUserId(userId);
+        data.setRechargeMoney(rechargeMoney);
+        data.setBalanceAfter(balanceAfter);
+        data.setOrderNo(orderNo);
+        return data;
+    }
+
+    /**
+     * 创建失败响应数据
+     */
+    public static ChargeOrderPayResponseData fail(String message) {
+        ChargeOrderPayResponseData data = new ChargeOrderPayResponseData();
+        data.setResult(1);
+        data.setMessage(message);
+        return data;
+    }
+}

+ 33 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationDetailRequestData.java

@@ -0,0 +1,33 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import lombok.Data;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 充电站详情请求数据
+ *
+ * @author wzq
+ */
+@Data
+public class ChargeStationDetailRequestData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 充电站ID
+     */
+    private Long stationId;
+
+    /**
+     * 经度
+     */
+    private BigDecimal longitude;
+
+    /**
+     * 纬度
+     */
+    private BigDecimal latitude;
+}

+ 119 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationDetailResponseData.java

@@ -0,0 +1,119 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.zsElectric.boot.business.model.vo.AppletStationDetailVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 充电站详情响应数据(含设备列表)
+ *
+ * @author wzq
+ */
+@Data
+public class ChargeStationDetailResponseData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "站点ID")
+    private Long stationId;
+
+    @Schema(description = "站点名称")
+    private String stationName;
+
+    @Schema(description = "提示语/标签(如:充电减免2小时停车费,超出部分按每小时3元计费)")
+    private String tips;
+
+    @Schema(description = "距离(km)")
+    private BigDecimal distance;
+
+    @Schema(description = "详细地址")
+    private String address;
+
+    @Schema(description = "经度")
+    private BigDecimal longitude;
+
+    @Schema(description = "纬度")
+    private BigDecimal latitude;
+
+    @Schema(description = "站点图片列表(JSON数组)")
+    private String pictures;
+
+    // ========== 费用信息 ==========
+
+    @Schema(description = "当前价(元/度)")
+    private BigDecimal currentPrice;
+
+    @Schema(description = "当前时段(如:峰10:00-13:00)")
+    private String currentPeriod;
+
+    @Schema(description = "原价/划线价(元/度)")
+    private BigDecimal originalPrice;
+
+    @Schema(description = "时段标志(1-尖,2-峰,3-平,4-谷)")
+    private Integer periodFlag;
+
+
+    // ========== 充电终端统计 ==========
+
+    @Schema(description = "空闲终端数")
+    private Integer idleCount;
+
+    @Schema(description = "占用终端数")
+    private Integer occupiedCount;
+
+    @Schema(description = "离线终端数")
+    private Integer offlineCount;
+
+    @Schema(description = "充电终端列表")
+    private List<ChargeStationDetailResponseData.ConnectorInfoVO> connectorList;
+
+    // ========== 电站信息 ==========
+
+    @Schema(description = "营业时间(如:周一至周日 00:00-24:00)")
+    private String businessHours;
+
+    @Schema(description = "服务提供方")
+    private String serviceProvider;
+
+    @Schema(description = "发票提供方")
+    private String invoiceProvider;
+
+    @Schema(description = "客服热线")
+    private String customerServiceHotline;
+
+    /**
+     * 充电终端信息
+     */
+    @Getter
+    @Setter
+    @Schema(description = "充电终端信息")
+    public static class ConnectorInfoVO implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+        @Schema(description = "充电终端ID")
+        private Long connectorId;
+
+        @Schema(description = "终端名称(如:101号直流充电桩)")
+        private String connectorName;
+
+        @Schema(description = "终端分类(如:直流设备)")
+        private String equipmentType;
+
+        @Schema(description = "终端编码")
+        private String connectorCode;
+
+        @Schema(description = "状态:0-离线 1-空闲 2-占用")
+        private Integer status;
+
+        @Schema(description = "状态名称")
+        private String statusName;
+    }
+}

+ 39 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ChargeStationListRequestData.java

@@ -0,0 +1,39 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.zsElectric.boot.common.base.BasePageQuery;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 充电站列表请求数据
+ *
+ * @author wzq
+ */
+@Data
+public class ChargeStationListRequestData extends BasePageQuery implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 请求类别:1-离我最近、2-空闲最多、3-电费最低
+     */
+    @Schema(description = "请求类别:1-离我最近、2-空闲最多、3-电费最低")
+    private Integer sortType;
+
+    /**
+     * 经度
+     */
+    @Schema(description = "经度")
+    private BigDecimal longitude;
+
+    /**
+     * 纬度
+     */
+    @Schema(description = "纬度")
+    private BigDecimal latitude;
+}

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

@@ -0,0 +1,96 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 充电站列表响应数据
+ *
+ * @author wzq
+ */
+@Data
+public class ChargeStationListResponseData implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 总记录数
+     */
+    private Long total;
+
+    /**
+     * 当前页码
+     */
+    private Integer pageNum;
+
+    /**
+     * 每页大小
+     */
+    private Integer pageSize;
+
+    /**
+     * 总页数
+     */
+    private Long pages;
+
+    /**
+     * 站点列表
+     */
+    private List<StationItem> list;
+
+    /**
+     * 站点信息项
+     */
+    @Data
+    public static class StationItem implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        @Schema(description = "站点ID")
+        private Long stationId;
+
+        @Schema(description = "站点名称")
+        private String stationName;
+
+        @Schema(description = "提示语")
+        private String tips;
+
+        @Schema(description = "距离(km)")
+        private BigDecimal distance;
+
+        @Schema(description = "快充(格式:空闲/总数)")
+        private String fastCharging;
+
+        @Schema(description = "慢充(格式:空闲/总数)")
+        private String slowCharging;
+
+        @Schema(description = "当前峰值")
+        private String peakValue;
+
+        @Schema(description = "峰时段时间")
+        private String peakTime;
+
+        @Schema(description = "时段标志(1-尖,2-峰,3-平,4-谷)")
+        private Integer periodFlag;
+
+        @Schema(description = "平台价")
+        private BigDecimal platformPrice;
+
+    }
+
+    /**
+     * 构建成功响应
+     */
+    public static ChargeStationListResponseData of(Long total, Integer pageNum, Integer pageSize, Long pages, List<StationItem> list) {
+        ChargeStationListResponseData data = new ChargeStationListResponseData();
+        data.setTotal(total);
+        data.setPageNum(pageNum);
+        data.setPageSize(pageSize);
+        data.setPages(pages);
+        data.setList(list);
+        return data;
+    }
+}

+ 31 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/QueryTokenRequestData.java

@@ -0,0 +1,31 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * query_token 接口请求参数 (data解密后的内容)
+ *
+ * @author wzq
+ */
+@Data
+public class QueryTokenRequestData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 运营商标识 - 请求方唯一编号
+     */
+    @JsonProperty("operatorId")
+    private String operatorId;
+
+    /**
+     * 运营商密钥 - 被调用方分配的唯一识别密钥
+     */
+    @JsonProperty("operatorSecret")
+    private String operatorSecret;
+}

+ 79 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/QueryTokenResponseData.java

@@ -0,0 +1,79 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * query_token 接口响应参数 (data加密前的内容)
+ *
+ * @author wzq
+ */
+@Data
+public class QueryTokenResponseData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 运营商标识
+     */
+    @JsonProperty("operatorId")
+    private String operatorId;
+
+    /**
+     * 操作结果标识
+     * 0: 成功
+     * 1: 失败
+     */
+    @JsonProperty("resultStatus")
+    private Integer resultStatus;
+
+    /**
+     * 访问令牌 - token令牌
+     */
+    @JsonProperty("accessToken")
+    private String accessToken;
+
+    /**
+     * token有效期 - 单位秒
+     */
+    @JsonProperty("tokenExpirationTime")
+    private Integer tokenExpirationTime;
+
+    /**
+     * 失败原因
+     * 0: 无
+     * 1: operatorId无效
+     */
+    @JsonProperty("failReason")
+    private Integer failReason;
+
+    /**
+     * 创建成功响应数据
+     */
+    public static QueryTokenResponseData success(String operatorId, String accessToken, Integer expirationTime) {
+        QueryTokenResponseData data = new QueryTokenResponseData();
+        data.setOperatorId(operatorId);
+        data.setResultStatus(0);
+        data.setAccessToken(accessToken);
+        data.setTokenExpirationTime(expirationTime);
+        data.setFailReason(0);
+        return data;
+    }
+
+    /**
+     * 创建失败响应数据
+     */
+    public static QueryTokenResponseData fail(String operatorId, Integer failReason) {
+        QueryTokenResponseData data = new QueryTokenResponseData();
+        data.setOperatorId(operatorId);
+        data.setResultStatus(1);
+        data.setAccessToken("");
+        data.setTokenExpirationTime(0);
+        data.setFailReason(failReason);
+        return data;
+    }
+}

+ 26 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/QueryUserInfoRequestData.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 QueryUserInfoRequestData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 手机号
+     */
+    @JsonProperty("phone")
+    private String phone;
+
+}

+ 117 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/QueryUserInfoResponseData.java

@@ -0,0 +1,117 @@
+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 QueryUserInfoResponseData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 是否新用户 (0-否 1-是)
+     */
+    @JsonProperty("isNewUser")
+    private Integer isNewUser;
+
+    /**
+     * 用户信息
+     */
+    @JsonProperty("userInfo")
+    private UserInfoItem userInfo;
+
+    /**
+     * 账户信息
+     */
+    @JsonProperty("accountInfo")
+    private AccountInfoItem accountInfo;
+
+    /**
+     * 用户信息
+     */
+    @Data
+    public static class UserInfoItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 用户ID
+         */
+        @JsonProperty("userId")
+        private Long userId;
+
+        /**
+         * 昵称
+         */
+        @JsonProperty("nickName")
+        private String nickName;
+
+        /**
+         * 手机号
+         */
+        @JsonProperty("phone")
+        private String phone;
+
+        /**
+         * 微信openid
+         */
+        @JsonProperty("openid")
+        private String openid;
+    }
+
+    /**
+     * 账户信息
+     */
+    @Data
+    public static class AccountInfoItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 账户ID
+         */
+        @JsonProperty("accountId")
+        private Long accountId;
+
+        /**
+         * 可用抵用券余额
+         */
+        @JsonProperty("balance")
+        private BigDecimal balance;
+
+        /**
+         * 兑换余额
+         */
+        @JsonProperty("redeemBalance")
+        private BigDecimal redeemBalance;
+
+        /**
+         * 积分
+         */
+        @JsonProperty("integral")
+        private BigDecimal integral;
+    }
+
+    /**
+     * 创建成功响应数据
+     */
+    public static QueryUserInfoResponseData of(Integer isNewUser, UserInfoItem userInfo, AccountInfoItem accountInfo) {
+        QueryUserInfoResponseData data = new QueryUserInfoResponseData();
+        data.setIsNewUser(isNewUser);
+        data.setUserInfo(userInfo);
+        data.setAccountInfo(accountInfo);
+        return data;
+    }
+}

+ 31 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/RechargeLevelPageRequestData.java

@@ -0,0 +1,31 @@
+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 RechargeLevelPageRequestData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 页码(默认1)
+     */
+    @JsonProperty("pageNum")
+    private Integer pageNum = 1;
+
+    /**
+     * 每页记录数(默认10)
+     */
+    @JsonProperty("pageSize")
+    private Integer pageSize = 10;
+}

+ 104 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/RechargeLevelPageResponseData.java

@@ -0,0 +1,104 @@
+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;
+import java.util.List;
+
+/**
+ * 充值档位分页查询响应参数 (data加密前的内容)
+ *
+ * @author wzq
+ */
+@Data
+public class RechargeLevelPageResponseData implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 总记录数
+     */
+    @JsonProperty("total")
+    private Long total;
+
+    /**
+     * 当前页码
+     */
+    @JsonProperty("pageNum")
+    private Integer pageNum;
+
+    /**
+     * 每页记录数
+     */
+    @JsonProperty("pageSize")
+    private Integer pageSize;
+
+    /**
+     * 总页数
+     */
+    @JsonProperty("pages")
+    private Long pages;
+
+    /**
+     * 数据列表
+     */
+    @JsonProperty("records")
+    private List<RechargeLevelItem> records;
+
+    /**
+     * 充值档位单项信息
+     */
+    @Data
+    public static class RechargeLevelItem implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 档位ID
+         */
+        @JsonProperty("id")
+        private Long id;
+
+        /**
+         * 充值档位名称
+         */
+        @JsonProperty("name")
+        private String name;
+
+        /**
+         * 充值金额
+         */
+        @JsonProperty("money")
+        private BigDecimal money;
+
+        /**
+         * 状态 (0-不可用 1-可用)
+         */
+        @JsonProperty("status")
+        private Integer status;
+
+        /**
+         * 充值提示
+         */
+        @JsonProperty("tips")
+        private String tips;
+    }
+
+    /**
+     * 创建成功响应数据
+     */
+    public static RechargeLevelPageResponseData of(Long total, Integer pageNum, Integer pageSize, Long pages, List<RechargeLevelItem> records) {
+        RechargeLevelPageResponseData data = new RechargeLevelPageResponseData();
+        data.setTotal(total);
+        data.setPageNum(pageNum);
+        data.setPageSize(pageSize);
+        data.setPages(pages);
+        data.setRecords(records);
+        return data;
+    }
+}

+ 51 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ThirdPartyRequest.java

@@ -0,0 +1,51 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 第三方接入通用请求实体
+ * 所有第三方请求均需满足此格式
+ *
+ * @author wzq
+ */
+@Data
+public class ThirdPartyRequest implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 运营商标识 - 请求方唯一编号(长度9位,由数字及大小写字符组成)
+     */
+    @JsonProperty("operatorId")
+    private String operatorId;
+
+    /**
+     * 加密后的参数内容 - AES 128位加密(CBC模式, PKCS5Padding)
+     */
+    @JsonProperty("data")
+    private String data;
+
+    /**
+     * 时间戳 - 格式: yyyyMMddHHmmss
+     */
+    @JsonProperty("timeStamp")
+    private String timeStamp;
+
+    /**
+     * 自增序列 - 4位序列号
+     */
+    @JsonProperty("seq")
+    private String seq;
+
+    /**
+     * 数字签名 - HMAC-MD5签名(大写)
+     * 入参拼接顺序: operatorId + data + timeStamp + seq
+     */
+    @JsonProperty("sig")
+    private String sig;
+}

+ 74 - 0
src/main/java/com/zsElectric/boot/thirdParty/model/ThirdPartyResponse.java

@@ -0,0 +1,74 @@
+package com.zsElectric.boot.thirdParty.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 第三方接入通用响应实体
+ *
+ * @author wzq
+ */
+@Data
+public class ThirdPartyResponse implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 响应状态码
+     * 0000: 请求成功
+     * 500: 系统错误
+     * 4001: 签名错误
+     * 4002: token错误
+     * 4003: 参数不合法,缺少必需的参数: operatorId、sig、timeStamp、data、seq
+     * 4004: 请求的业务参数不合法
+     */
+    @JsonProperty("ret")
+    private Integer ret;
+
+    /**
+     * 响应消息
+     */
+    @JsonProperty("msg")
+    private String msg;
+
+    /**
+     * 响应加密数据 - AES 128位加密(CBC模式, PKCS5Padding)
+     */
+    @JsonProperty("data")
+    private String data;
+
+    /**
+     * 响应签名 - HMAC-MD5签名(大写)
+     * 出参拼接顺序: ret + msg + data
+     */
+    @JsonProperty("sig")
+    private String sig;
+
+    /**
+     * 创建成功响应
+     */
+    public static ThirdPartyResponse success(String encryptedData, String sig) {
+        ThirdPartyResponse response = new ThirdPartyResponse();
+        response.setRet(0);
+        response.setMsg("");
+        response.setData(encryptedData);
+        response.setSig(sig);
+        return response;
+    }
+
+    /**
+     * 创建错误响应
+     */
+    public static ThirdPartyResponse error(Integer ret, String msg, String sig) {
+        ThirdPartyResponse response = new ThirdPartyResponse();
+        response.setRet(ret);
+        response.setMsg(msg);
+        response.setData("");
+        response.setSig(sig);
+        return response;
+    }
+}

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

@@ -0,0 +1,60 @@
+package com.zsElectric.boot.thirdParty.service;
+
+import com.zsElectric.boot.thirdParty.model.ThirdPartyRequest;
+import com.zsElectric.boot.thirdParty.model.ThirdPartyResponse;
+
+/**
+ * 第三方Token服务接口
+ *
+ * @author wzq
+ */
+public interface ThirdPartyTokenService {
+
+    /**
+     * 处理获取Token请求
+     *
+     * @param request 第三方请求参数
+     * @return 响应结果
+     */
+    ThirdPartyResponse queryToken(ThirdPartyRequest request);
+
+    /**
+     * 获取充值档位分页列表
+     *
+     * @param request       第三方请求参数
+     * @param authorization 请求头中的Authorization (格式: Bearer {accessToken})
+     * @return 响应结果
+     */
+    ThirdPartyResponse queryRechargeLevelPage(ThirdPartyRequest request, String authorization);
+
+    ThirdPartyResponse queryUserInfo(ThirdPartyRequest request, String authorization);
+
+    ThirdPartyResponse chargeOrderPay(ThirdPartyRequest request, String authorization);
+
+    /**
+     * 获取充电站列表
+     *
+     * @param request       第三方请求参数
+     * @param authorization 请求头中的Authorization
+     * @return 响应结果
+     */
+    ThirdPartyResponse queryChargeStationList(ThirdPartyRequest request, String authorization);
+
+    /**
+     * 获取充电站详情与充电设备列表
+     *
+     * @param request       第三方请求参数
+     * @param authorization 请求头中的Authorization
+     * @return 响应结果
+     */
+    ThirdPartyResponse queryChargeStationDetail(ThirdPartyRequest request, String authorization);
+
+    /**
+     * 获取充电设备详情
+     *
+     * @param request       第三方请求参数
+     * @param authorization 请求头中的Authorization
+     * @return 响应结果
+     */
+    ThirdPartyResponse queryChargeDeviceDetail(ThirdPartyRequest request, String authorization);
+}

+ 1072 - 0
src/main/java/com/zsElectric/boot/thirdParty/service/impl/ThirdPartyTokenServiceImpl.java

@@ -0,0 +1,1072 @@
+package com.zsElectric.boot.thirdParty.service.impl;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zsElectric.boot.business.mapper.RechargeLevelMapper;
+import com.zsElectric.boot.business.mapper.ThirdPartyEquipmentInfoMapper;
+import com.zsElectric.boot.business.mapper.ThirdPartyInfoMapper;
+import com.zsElectric.boot.business.mapper.ThirdPartyStationInfoMapper;
+import com.zsElectric.boot.business.model.vo.applet.AppletConnectorDetailVO;
+import com.zsElectric.boot.charging.mapper.ThirdPartyConnectorInfoMapper;
+import com.zsElectric.boot.business.mapper.UserAccountMapper;
+import com.zsElectric.boot.business.mapper.UserOrderInfoMapper;
+import com.zsElectric.boot.business.model.entity.RechargeLevel;
+import com.zsElectric.boot.business.model.entity.ThirdPartyInfo;
+import com.zsElectric.boot.business.model.entity.UserAccount;
+import com.zsElectric.boot.business.model.entity.UserInfo;
+import com.zsElectric.boot.business.model.entity.UserOrderInfo;
+import com.zsElectric.boot.business.model.query.StationInfoQuery;
+import com.zsElectric.boot.business.model.vo.AppletStationDetailVO;
+import com.zsElectric.boot.business.model.vo.StationInfoVO;
+import com.zsElectric.boot.charging.entity.ThirdPartyConnectorInfo;
+import com.zsElectric.boot.charging.entity.ThirdPartyEquipmentInfo;
+import com.zsElectric.boot.charging.entity.ThirdPartyStationInfo;
+import com.zsElectric.boot.business.service.UserInfoService;
+import com.zsElectric.boot.business.service.UserAccountService;
+import com.zsElectric.boot.common.constant.SystemConstants;
+import com.zsElectric.boot.common.util.AESCryptoUtils;
+import com.zsElectric.boot.common.util.HmacMD5Util;
+import com.zsElectric.boot.thirdParty.model.*;
+import com.zsElectric.boot.thirdParty.service.ThirdPartyTokenService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 第三方Token服务实现类
+ *
+ * @author wzq
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ThirdPartyTokenServiceImpl implements ThirdPartyTokenService {
+
+    private final ThirdPartyInfoMapper thirdPartyInfoMapper;
+    private final RechargeLevelMapper rechargeLevelMapper;
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final UserInfoService userInfoService;
+    private final UserAccountMapper userAccountMapper;
+    private final UserAccountService userAccountService;
+    private final UserOrderInfoMapper userOrderInfoMapper;
+    private final ThirdPartyStationInfoMapper thirdPartyStationInfoMapper;
+    private final ThirdPartyEquipmentInfoMapper thirdPartyEquipmentInfoMapper;
+    private final ThirdPartyConnectorInfoMapper thirdPartyConnectorInfoMapper;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * Redis Token Key 前缀
+     */
+    private static final String TOKEN_KEY_PREFIX = "third_party_access:token:";
+
+    /**
+     * Redis OperatorId Token映射 Key 前缀
+     */
+    private static final String OPERATOR_TOKEN_KEY_PREFIX = "third_party_access:operator:";
+
+    /**
+     * 默认Token有效期(秒)
+     */
+    private static final int DEFAULT_TOKEN_EXPIRE_SECONDS = 7200;
+
+    @Override
+    public ThirdPartyResponse queryToken(ThirdPartyRequest request) {
+        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. 解密并解析业务参数
+            String decryptedData = AESCryptoUtils.decrypt(
+                    request.getData(),
+                    thirdPartyInfo.getDataSecret(),
+                    thirdPartyInfo.getDataSecretIV()
+            );
+            log.info("解密后的请求数据: {}", decryptedData);
+
+            QueryTokenRequestData tokenRequest = objectMapper.readValue(decryptedData, QueryTokenRequestData.class);
+            if (tokenRequest == null || StrUtil.isBlank(tokenRequest.getOperatorId()) || StrUtil.isBlank(tokenRequest.getOperatorSecret())) {
+                return buildErrorResponse(4004, "请求的业务参数不合法", thirdPartyInfo);
+            }
+
+            // 5. 验证运营商ID和密钥
+            if (!thirdPartyInfo.getOperatorId().equals(tokenRequest.getOperatorId())
+                    || !thirdPartyInfo.getOperatorSecret().equals(tokenRequest.getOperatorSecret())) {
+                log.warn("运营商ID或密钥错误, operatorId: {}", tokenRequest.getOperatorId());
+                // 返回失败响应数据
+                QueryTokenResponseData failData = QueryTokenResponseData.fail(tokenRequest.getOperatorId(), 1);
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
+
+            // 6. 生成或获取已有Token
+            String accessToken = getOrGenerateToken(tokenRequest.getOperatorId());
+            Integer remainingTTL = getRemainingTTL(accessToken);
+
+            // 7. 构建成功响应
+            QueryTokenResponseData responseData = QueryTokenResponseData.success(
+                    tokenRequest.getOperatorId(),
+                    accessToken,
+                    remainingTTL
+            );
+            log.info("生成Token成功, operatorId: {}, token有效期: {}秒", tokenRequest.getOperatorId(), remainingTTL);
+
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_token请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    @Override
+    public ThirdPartyResponse queryRechargeLevelPage(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);
+
+            RechargeLevelPageRequestData pageRequest = objectMapper.readValue(decryptedData, RechargeLevelPageRequestData.class);
+            if (pageRequest == null) {
+                pageRequest = new RechargeLevelPageRequestData();
+            }
+
+            // 6. 查询充值档位分页数据
+            IPage<RechargeLevel> pageResult = queryRechargeLevelFromDb(pageRequest);
+
+            // 7. 转换为响应数据
+            List<RechargeLevelPageResponseData.RechargeLevelItem> records = pageResult.getRecords().stream()
+                    .map(this::convertToItem)
+                    .collect(Collectors.toList());
+
+            RechargeLevelPageResponseData responseData = RechargeLevelPageResponseData.of(
+                    pageResult.getTotal(),
+                    (int) pageResult.getCurrent(),
+                    (int) pageResult.getSize(),
+                    pageResult.getPages(),
+                    records
+            );
+
+            log.info("查询充值档位成功, operatorId: {}, total: {}", request.getOperatorId(), pageResult.getTotal());
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_recharge_level_page请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    @Override
+    public ThirdPartyResponse queryUserInfo(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);
+
+            QueryUserInfoRequestData userInfoRequest = objectMapper.readValue(decryptedData, QueryUserInfoRequestData.class);
+            if (userInfoRequest == null || StrUtil.isBlank(userInfoRequest.getPhone())) {
+                log.warn("手机号为空, operatorId: {}", request.getOperatorId());
+                return buildErrorResponse(4004, "请求的业务参数不合法,手机号不能为空", thirdPartyInfo);
+            }
+
+            String phone = userInfoRequest.getPhone();
+
+            // 6. 根据手机号查询用户信息
+            UserInfo userInfo = userInfoService.getUserInfoByPhone(phone);
+
+            Integer isNewUser = 0;
+            if (userInfo == null) {
+                // 用户不存在,注册新用户
+                log.info("用户不存在,注册新用户, phone: {}", phone);
+                userInfo = userInfoService.registerOrUpdateUserByPhone(phone, null);
+                if (userInfo == null) {
+                    log.error("注册用户失败, phone: {}", phone);
+                    return buildErrorResponse(500, "注册用户失败", thirdPartyInfo);
+                }
+                isNewUser = 1;
+            }
+
+            // 7. 查询用户账户信息
+            UserAccount userAccount = userAccountMapper.selectOne(
+                    Wrappers.<UserAccount>lambdaQuery()
+                            .eq(UserAccount::getUserId, userInfo.getId())
+                            .eq(UserAccount::getIsDeleted, 0)
+            );
+
+            // 如果账户不存在,创建账户
+            if (userAccount == null) {
+                log.info("用户账户不存在,创建账户, userId: {}", userInfo.getId());
+                userAccount = new UserAccount();
+                userAccount.setUserId(userInfo.getId());
+                userAccount.setBalance(BigDecimal.ZERO);
+                userAccount.setRedeemBalance(BigDecimal.ZERO);
+                userAccount.setIntegral(BigDecimal.ZERO);
+                userAccountMapper.insert(userAccount);
+            }
+
+            // 8. 构建响应数据
+            QueryUserInfoResponseData.UserInfoItem userInfoItem = new QueryUserInfoResponseData.UserInfoItem();
+            userInfoItem.setUserId(userInfo.getId());
+            userInfoItem.setNickName(userInfo.getNickName());
+            userInfoItem.setPhone(userInfo.getPhone());
+            userInfoItem.setOpenid(userInfo.getOpenid());
+
+            QueryUserInfoResponseData.AccountInfoItem accountInfoItem = new QueryUserInfoResponseData.AccountInfoItem();
+            accountInfoItem.setAccountId(userAccount.getId());
+            accountInfoItem.setBalance(userAccount.getBalance() != null ? userAccount.getBalance() : BigDecimal.ZERO);
+            accountInfoItem.setRedeemBalance(userAccount.getRedeemBalance() != null ? userAccount.getRedeemBalance() : BigDecimal.ZERO);
+            accountInfoItem.setIntegral(userAccount.getIntegral() != null ? userAccount.getIntegral() : BigDecimal.ZERO);
+
+            QueryUserInfoResponseData responseData = QueryUserInfoResponseData.of(isNewUser, userInfoItem, accountInfoItem);
+
+            log.info("根据手机号查询用户信息成功, operatorId: {}, phone: {}, isNewUser: {}", request.getOperatorId(), phone, isNewUser);
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_user_info请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    @Override
+    public ThirdPartyResponse chargeOrderPay(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);
+
+            ChangeOrderPayRequestData payRequest = objectMapper.readValue(decryptedData, ChangeOrderPayRequestData.class);
+            if (payRequest == null || StrUtil.isBlank(payRequest.getPhone()) || StrUtil.isBlank(payRequest.getOrderNo())) {
+                log.warn("请求参数不完整, operatorId: {}", request.getOperatorId());
+                return buildErrorResponse(4004, "请求的业务参数不合法,手机号和订单号不能为空", thirdPartyInfo);
+            }
+
+            Long userId = payRequest.getUserId();
+            String phone = payRequest.getPhone();
+            //根据userId和phone校验用户
+            UserInfo userInfo = userInfoService.getOne(Wrappers.lambdaQuery(UserInfo.class).eq(UserInfo::getPhone, phone).eq(UserInfo::getId, userId).last("limit 1"));
+            if (ObjectUtil.isNull(userInfo)) {
+                log.warn("用户不存在, userId: {}, phone: {}", userId, phone);
+                ChargeOrderPayResponseData failData = ChargeOrderPayResponseData.fail("用户不存在");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
+            String orderNo = payRequest.getOrderNo();
+            UserAccount userAccount = userAccountService.getOne(Wrappers.lambdaQuery(UserAccount.class).eq(UserAccount::getUserId, userId).last("limit 1"));
+            // 如果账户不存在,创建账户
+            if (ObjectUtil.isNull(userAccount)) {
+                log.info("用户账户不存在,创建账户, userId: {}", userInfo.getId());
+                userAccount = new UserAccount();
+                userAccount.setUserId(userInfo.getId());
+                userAccount.setBalance(BigDecimal.ZERO);
+                userAccount.setRedeemBalance(BigDecimal.ZERO);
+                userAccount.setIntegral(BigDecimal.ZERO);
+                userAccountMapper.insert(userAccount);
+            }
+
+            // 6. 幂等性检查 - 通过Redis检查订单是否已处理
+            String orderKey = "third_party_pay:order:" + orderNo;
+            Boolean isProcessed = redisTemplate.hasKey(orderKey);
+            if (Boolean.TRUE.equals(isProcessed)) {
+                log.warn("订单已处理,请勿重复提交, orderNo: {}", orderNo);
+                ChargeOrderPayResponseData failData = ChargeOrderPayResponseData.fail("订单已处理,请勿重复提交");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
+
+            // 7. 查询充值档位信息(如果有levelId)
+            BigDecimal rechargeMoney;
+            if (payRequest.getLevelId() != null) {
+                RechargeLevel rechargeLevel = rechargeLevelMapper.selectById(payRequest.getLevelId());
+                if (rechargeLevel == null) {
+                    log.warn("充值档位不存在, levelId: {}", payRequest.getLevelId());
+                    ChargeOrderPayResponseData failData = ChargeOrderPayResponseData.fail("充值档位不存在");
+                    return buildSuccessResponse(failData, thirdPartyInfo);
+                }
+                rechargeMoney = rechargeLevel.getMoney();
+            } else if (StrUtil.isNotBlank(payRequest.getTotalMoney())) {
+                // 使用传入的金额
+                rechargeMoney = new BigDecimal(payRequest.getTotalMoney());
+            } else {
+                log.warn("充值金额未指定, operatorId: {}", request.getOperatorId());
+                ChargeOrderPayResponseData failData = ChargeOrderPayResponseData.fail("充值金额未指定");
+                return buildSuccessResponse(failData, thirdPartyInfo);
+            }
+
+            // 8. 增加充值订单记录到c_user_order_info表
+            UserOrderInfo orderInfo = new UserOrderInfo();
+            orderInfo.setUserId(userId);
+            orderInfo.setOrderNo(orderNo);
+            orderInfo.setOrderMoney(rechargeMoney);
+            orderInfo.setPayMoney(rechargeMoney);
+            orderInfo.setLastMoney(rechargeMoney);
+            // 解析支付时间
+            if (StrUtil.isNotBlank(payRequest.getPayTime())) {
+                try {
+                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+                    orderInfo.setPayTime(LocalDateTime.parse(payRequest.getPayTime(), formatter));
+                } catch (Exception e) {
+                    orderInfo.setPayTime(LocalDateTime.now());
+                }
+            } else {
+                orderInfo.setPayTime(LocalDateTime.now());
+            }
+            orderInfo.setLevelId(payRequest.getLevelId());
+            orderInfo.setOutTradeNo(orderNo);  // 第三方订单号
+            orderInfo.setOrderStatus(2);  // 已支付
+            orderInfo.setOrderType(3);    // 渠道方
+            orderInfo.setRemark("第三方渠道充值-" + request.getOperatorId());
+            userOrderInfoMapper.insert(orderInfo);
+            log.info("创建充值订单记录成功, orderNo: {}, userId: {}", orderNo, userId);
+
+            // 9. 充值账户余额并记录日志
+            // 使用订单号的Id值作为变更记录ID
+            Long changeId = orderInfo.getId();
+            UserAccount updatedAccount = userAccountService.updateAccountBalanceAndLog(
+                    userId,
+                    rechargeMoney,
+                    SystemConstants.CHANGE_TYPE_ADD,
+                    SystemConstants.ACCOUNT_LOG_THIRD_PARTY_PAY_NOTE + "-" + orderNo,
+                    changeId
+            );
+
+            BigDecimal balanceAfter = updatedAccount != null ? updatedAccount.getBalance() : userAccount.getBalance().add(rechargeMoney);
+
+            // 10. 构建成功响应
+            ChargeOrderPayResponseData responseData = ChargeOrderPayResponseData.success(
+                    userId,
+                    rechargeMoney,
+                    balanceAfter,
+                    orderNo
+            );
+
+            log.info("充电券购买成功, operatorId: {}, phone: {}, orderNo: {}, rechargeMoney: {}, balanceAfter: {}",
+                    request.getOperatorId(), phone, orderNo, rechargeMoney, balanceAfter);
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理charge_order_pay请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    /**
+     * 从Authorization Header中解析Token
+     * 支持格式: "Bearer {token}" 或直接传 "{token}"
+     */
+    private String parseAccessToken(String authorization) {
+        if (StrUtil.isBlank(authorization)) {
+            return null;
+        }
+        // 支持 Bearer Token 格式
+        if (authorization.startsWith("Bearer ")) {
+            return authorization.substring(7).trim();
+        }
+        // 直接返回Token
+        return authorization.trim();
+    }
+
+    /**
+     * 验证访问Token
+     */
+    private boolean validateAccessToken(String accessToken, String operatorId) {
+        String tokenKey = TOKEN_KEY_PREFIX + accessToken;
+        String storedOperatorId = (String) redisTemplate.opsForValue().get(tokenKey);
+        return operatorId.equals(storedOperatorId);
+    }
+
+    /**
+     * 从数据库查询充值档位分页数据
+     */
+    private IPage<RechargeLevel> queryRechargeLevelFromDb(RechargeLevelPageRequestData pageRequest) {
+        Page<RechargeLevel> page = new Page<>(pageRequest.getPageNum(), pageRequest.getPageSize());
+        LambdaQueryWrapper<RechargeLevel> queryWrapper = new LambdaQueryWrapper<>();
+
+        // 按ID排序
+        queryWrapper.eq(RechargeLevel::getStatus, SystemConstants.STATUS_ONE);
+        queryWrapper.orderByAsc(RechargeLevel::getId);
+
+        return rechargeLevelMapper.selectPage(page, queryWrapper);
+    }
+
+    /**
+     * 转换为响应项
+     */
+    private RechargeLevelPageResponseData.RechargeLevelItem convertToItem(RechargeLevel entity) {
+        RechargeLevelPageResponseData.RechargeLevelItem item = new RechargeLevelPageResponseData.RechargeLevelItem();
+        item.setId(entity.getId());
+        item.setName(entity.getName());
+        item.setMoney(entity.getMoney());
+        item.setStatus(entity.getStatus());
+        item.setTips(entity.getTips());
+        return item;
+    }
+
+    /**
+     * 通过operatorId查询第三方配置信息
+     */
+    private ThirdPartyInfo getThirdPartyInfoByOperatorId(String operatorId) {
+        LambdaQueryWrapper<ThirdPartyInfo> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(ThirdPartyInfo::getOperatorId, operatorId)
+                .eq(ThirdPartyInfo::getStatus, 0)  // 状态正常
+                .last("LIMIT 1");
+        return thirdPartyInfoMapper.selectOne(queryWrapper);
+    }
+
+    /**
+     * 校验请求参数完整性
+     */
+    private boolean validateRequestParams(ThirdPartyRequest request) {
+        return request != null
+                && StrUtil.isNotBlank(request.getOperatorId())
+                && StrUtil.isNotBlank(request.getData())
+                && StrUtil.isNotBlank(request.getTimeStamp())
+                && StrUtil.isNotBlank(request.getSeq())
+                && StrUtil.isNotBlank(request.getSig());
+    }
+
+    /**
+     * 获取或生成Token
+     * 如果Redis中存在有效Token则返回,否则生成新Token
+     */
+    private String getOrGenerateToken(String operatorId) {
+        // 检查是否有已存在的有效Token
+        String operatorTokenKey = OPERATOR_TOKEN_KEY_PREFIX + operatorId;
+        String existingToken = (String) redisTemplate.opsForValue().get(operatorTokenKey);
+
+        if (StrUtil.isNotBlank(existingToken)) {
+            // 验证Token是否仍然有效
+            String tokenKey = TOKEN_KEY_PREFIX + existingToken;
+            Boolean hasKey = redisTemplate.hasKey(tokenKey);
+            if (Boolean.TRUE.equals(hasKey)) {
+                log.info("返回已存在的有效Token, operatorId: {}", operatorId);
+                return existingToken;
+            }
+        }
+
+        // 生成新Token
+        String newToken = IdUtil.fastSimpleUUID() + IdUtil.fastSimpleUUID();
+
+        // 存储Token
+        String tokenKey = TOKEN_KEY_PREFIX + newToken;
+        redisTemplate.opsForValue().set(tokenKey, operatorId, DEFAULT_TOKEN_EXPIRE_SECONDS, TimeUnit.SECONDS);
+
+        // 存储OperatorId与Token的映射关系
+        redisTemplate.opsForValue().set(operatorTokenKey, newToken, DEFAULT_TOKEN_EXPIRE_SECONDS, TimeUnit.SECONDS);
+
+        log.info("生成新Token, operatorId: {}, expireSeconds: {}", operatorId, DEFAULT_TOKEN_EXPIRE_SECONDS);
+        return newToken;
+    }
+
+    /**
+     * 获取Token剩余有效时间(秒)
+     */
+    private Integer getRemainingTTL(String token) {
+        String tokenKey = TOKEN_KEY_PREFIX + token;
+        Long ttl = redisTemplate.getExpire(tokenKey, TimeUnit.SECONDS);
+        return ttl != null && ttl > 0 ? ttl.intValue() : 0;
+    }
+
+    /**
+     * 构建成功响应(通用)
+     */
+    private ThirdPartyResponse buildSuccessResponse(Object responseData, ThirdPartyInfo thirdPartyInfo) throws Exception {
+        String jsonData = objectMapper.writeValueAsString(responseData);
+        String encryptedData = AESCryptoUtils.encrypt(
+                jsonData,
+                thirdPartyInfo.getDataSecret(),
+                thirdPartyInfo.getDataSecretIV()
+        );
+        String sig = HmacMD5Util.genSign(0, "", encryptedData, thirdPartyInfo.getSigSecret());
+        return ThirdPartyResponse.success(encryptedData, sig);
+    }
+
+    /**
+     * 构建错误响应
+     */
+    private ThirdPartyResponse buildErrorResponse(Integer ret, String msg, ThirdPartyInfo thirdPartyInfo) {
+        try {
+            String sigSecret = thirdPartyInfo != null ? thirdPartyInfo.getSigSecret() : "";
+            String sig = StrUtil.isNotBlank(sigSecret) ? HmacMD5Util.genSign(ret, msg, "", sigSecret) : "";
+            return ThirdPartyResponse.error(ret, msg, sig);
+        } catch (Exception e) {
+            log.error("生成错误响应签名失败", e);
+            ThirdPartyResponse response = new ThirdPartyResponse();
+            response.setRet(ret);
+            response.setMsg(msg);
+            response.setData("");
+            response.setSig("");
+            return response;
+        }
+    }
+
+    // ==================== 充电站与设备查询接口 ====================
+    /**
+     * 时间格式化器 HHmmss
+     */
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HHmmss");
+
+    @Override
+    public ThirdPartyResponse queryChargeStationList(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);
+
+            ChargeStationListRequestData listRequest = objectMapper.readValue(decryptedData, ChargeStationListRequestData.class);
+            if (listRequest == null) {
+                listRequest = new ChargeStationListRequestData();
+            }
+
+            // 6. 查询充电站分页数据
+            // 获取当前时间(HHmmss格式)
+            String currentTime = LocalTime.now().format(TIME_FORMATTER);
+
+            // 构建分页对象
+            Page<StationInfoVO> page = new Page<>(listRequest.getPageNum(), listRequest.getPageSize());
+
+            StationInfoQuery stationInfoQuery = new StationInfoQuery().setLatitude(listRequest.getLatitude()).setLongitude(listRequest.getLongitude()).setSortType(listRequest.getSortType());
+            // 执行查询
+            IPage<StationInfoVO> pageResult = thirdPartyStationInfoMapper.selectAppletStationInfoPage(
+                    page, stationInfoQuery, currentTime, null
+            );
+
+            // 7. 转换为响应数据
+            List<ChargeStationListResponseData.StationItem> records = pageResult.getRecords().stream()
+                    .map(this::convertToStationItem)
+                    .collect(Collectors.toList());
+
+            ChargeStationListResponseData responseData = ChargeStationListResponseData.of(
+                    pageResult.getTotal(),
+                    (int) pageResult.getCurrent(),
+                    (int) pageResult.getSize(),
+                    pageResult.getPages(),
+                    records
+            );
+
+            log.info("查询充电站列表成功, operatorId: {}, total: {}", request.getOperatorId(), pageResult.getTotal());
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_charge_station_list请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    @Override
+    public ThirdPartyResponse queryChargeStationDetail(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);
+
+            ChargeStationDetailRequestData detailRequest = objectMapper.readValue(decryptedData, ChargeStationDetailRequestData.class);
+            if (detailRequest == null || (detailRequest.getStationId() == null )) {
+                return buildErrorResponse(4003, "请求的业务参数不合法", thirdPartyInfo);
+            }
+
+            // 6. 查询充电站详情
+            // 获取当前时间(HHmmss格式)
+            String currentTime = LocalTime.now().format(TIME_FORMATTER);
+
+            // 查询站点详情基本信息
+            AppletStationDetailVO result = thirdPartyStationInfoMapper.selectStationDetail(
+                    detailRequest.getStationId(), detailRequest.getLongitude(), detailRequest.getLatitude(), currentTime, null
+            );
+
+            if (result == null) {
+                return buildErrorResponse(4004, "充电站不存在", thirdPartyInfo);
+            }
+
+            // 查询站点对应的stationId
+            ThirdPartyStationInfo stationInfo = thirdPartyStationInfoMapper.selectById(detailRequest.getStationId());
+            if (stationInfo == null) {
+                return buildErrorResponse(4004, "充电站不存在", thirdPartyInfo);
+            }
+
+            // 根据站点stationId查询设备列表(third_party_equipment_info)
+            List<ThirdPartyEquipmentInfo> equipmentList = thirdPartyEquipmentInfoMapper.selectList(
+                    new LambdaQueryWrapper<ThirdPartyEquipmentInfo>()
+                            .eq(ThirdPartyEquipmentInfo::getStationId, stationInfo.getStationId())
+            );
+
+            // 构建设备ID到设备类型的映射
+            Map<String, Integer> equipmentTypeMap = equipmentList.stream()
+                    .collect(Collectors.toMap(
+                            ThirdPartyEquipmentInfo::getEquipmentId,
+                            e -> e.getEquipmentType() != null ? e.getEquipmentType() : 0,
+                            (v1, v2) -> v1
+                    ));
+
+            // 根据设备列表的equipmentId查询充电接口信息(third_party_connector_info)
+            List<ThirdPartyConnectorInfo> connectorList = new ArrayList<>();
+            if (!equipmentList.isEmpty()) {
+                List<String> equipmentIds = equipmentList.stream()
+                        .map(ThirdPartyEquipmentInfo::getEquipmentId)
+                        .collect(Collectors.toList());
+                connectorList = thirdPartyConnectorInfoMapper.selectList(
+                        new LambdaQueryWrapper<ThirdPartyConnectorInfo>()
+                                .in(ThirdPartyConnectorInfo::getEquipmentId, equipmentIds)
+                );
+            }
+
+            // 统计终端状态
+            int idleCount = 0;
+            int occupiedCount = 0;
+            int offlineCount = 0;
+
+            List<AppletStationDetailVO.ConnectorInfoVO> connectorVOList = new ArrayList<>();
+            for (ThirdPartyConnectorInfo connector : connectorList) {
+                AppletStationDetailVO.ConnectorInfoVO vo = new AppletStationDetailVO.ConnectorInfoVO();
+                vo.setConnectorId(connector.getId());
+                vo.setConnectorName(connector.getConnectorName());
+                vo.setConnectorCode(connector.getConnectorId());
+
+                // 设备分类:从设备表获取设备类型(1-直流设备,2-交流设备,3-交直流一体设备,4-无线设备,5-其他)
+                Integer equipmentType = equipmentTypeMap.get(connector.getEquipmentId());
+                if (equipmentType != null) {
+                    switch (equipmentType) {
+                        case 1:
+                            vo.setEquipmentType("直流设备");
+                            break;
+                        case 2:
+                            vo.setEquipmentType("交流设备");
+                            break;
+                        case 3:
+                            vo.setEquipmentType("交直流一体设备");
+                            break;
+                        case 4:
+                            vo.setEquipmentType("无线设备");
+                            break;
+                        case 5:
+                            vo.setEquipmentType("其他");
+                            break;
+                        default:
+                            vo.setEquipmentType("未知");
+                    }
+                }
+
+                // 从充电接口表获取实际状态
+                // 0-离网,1-空闲,2-占用(未充电),3-占用(充电中),4-占用(预约锁定),255-故障
+                Integer connectorStatus = connector.getStatus();
+                if (connectorStatus == null) {
+                    // 默认为离网
+                    connectorStatus = 0;
+                }
+                vo.setStatus(connectorStatus);
+
+                // 设置状态名称并统计
+                switch (connectorStatus) {
+                    case 0:
+                        vo.setStatusName("离网");
+                        offlineCount++;
+                        break;
+                    case 1:
+                        vo.setStatusName("空闲");
+                        idleCount++;
+                        break;
+                    case 2:
+                        vo.setStatusName("占用(未充电)");
+                        occupiedCount++;
+                        break;
+                    case 3:
+                        vo.setStatusName("占用(充电中)");
+                        occupiedCount++;
+                        break;
+                    case 4:
+                        vo.setStatusName("占用(预约锁定)");
+                        occupiedCount++;
+                        break;
+                    case 255:
+                        vo.setStatusName("故障");
+                        offlineCount++;
+                        break;
+                    default:
+                        vo.setStatusName("未知");
+                        offlineCount++;
+                }
+
+                connectorVOList.add(vo);
+            }
+
+            result.setConnectorList(connectorVOList);
+            result.setIdleCount(idleCount);
+            result.setOccupiedCount(occupiedCount);
+            result.setOfflineCount(offlineCount);
+
+            // 7. 转换为响应数据
+            ChargeStationDetailResponseData responseData = convertToStationDetail(result);
+
+            log.info("查询充电站详情成功, operatorId: {}, stationId: {}", request.getOperatorId(), stationInfo.getStationId());
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_charge_station_detail请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    @Override
+    public ThirdPartyResponse queryChargeDeviceDetail(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);
+
+            ChargeDeviceDetailRequestData detailRequest = objectMapper.readValue(decryptedData, ChargeDeviceDetailRequestData.class);
+            if (detailRequest == null || (detailRequest.getId() == null && StrUtil.isBlank(detailRequest.getEquipmentId()))) {
+                return buildErrorResponse(4003, "请求的业务参数不合法", thirdPartyInfo);
+            }
+
+            // 6. 查询充电设备详情
+            // 获取当前时间(HHmmss格式)
+            String currentTime = LocalTime.now().format(TIME_FORMATTER);
+
+            AppletConnectorDetailVO result = thirdPartyConnectorInfoMapper.selectConnectorDetailById(
+                    detailRequest.getEquipmentId(), null, currentTime, null, null
+            );
+
+            // 8. 转换为响应数据
+            ChargeDeviceDetailResponseData responseData = convertToDeviceDetail(result);
+
+            log.info("查询充电终端详情成功, operatorId: {}, equipmentId: {}", request.getOperatorId(), result.getConnectorId());
+            return buildSuccessResponse(responseData, thirdPartyInfo);
+
+        } catch (Exception e) {
+            log.error("处理query_charge_device_detail请求异常", e);
+            return buildErrorResponse(500, "系统错误", null);
+        }
+    }
+
+    /**
+     * 转换充电站信息为详情响应(从AppletStationDetailVO转换)
+     */
+    private ChargeStationDetailResponseData convertToStationDetail(AppletStationDetailVO result) {
+        ChargeStationDetailResponseData detail = new ChargeStationDetailResponseData();
+        detail.setStationId(result.getStationId());
+        detail.setStationName(result.getStationName());
+        detail.setTips(result.getTips());
+        detail.setDistance(result.getDistance());
+        detail.setAddress(result.getAddress());
+        detail.setLongitude(result.getLongitude());
+        detail.setLatitude(result.getLatitude());
+        detail.setPictures(result.getPictures());
+        detail.setCurrentPrice(result.getCurrentPrice());
+        detail.setCurrentPeriod(result.getCurrentPeriod());
+        detail.setOriginalPrice(result.getOriginalPrice());
+        detail.setPeriodFlag(result.getPeriodFlag());
+        detail.setIdleCount(result.getIdleCount());
+        detail.setOccupiedCount(result.getOccupiedCount());
+        detail.setOfflineCount(result.getOfflineCount());
+        detail.setBusinessHours(result.getBusinessHours());
+        detail.setServiceProvider(result.getServiceProvider());
+        detail.setInvoiceProvider(result.getInvoiceProvider());
+        detail.setCustomerServiceHotline(result.getCustomerServiceHotline());
+
+        // 转换充电终端列表
+        if (result.getConnectorList() != null) {
+            List<ChargeStationDetailResponseData.ConnectorInfoVO> connectorVOList = result.getConnectorList().stream()
+                    .map(connector -> {
+                        ChargeStationDetailResponseData.ConnectorInfoVO vo = new ChargeStationDetailResponseData.ConnectorInfoVO();
+                        vo.setConnectorId(connector.getConnectorId());
+                        vo.setConnectorName(connector.getConnectorName());
+                        vo.setEquipmentType(connector.getEquipmentType());
+                        vo.setConnectorCode(connector.getConnectorCode());
+                        vo.setStatus(connector.getStatus());
+                        vo.setStatusName(connector.getStatusName());
+                        return vo;
+                    })
+                    .collect(Collectors.toList());
+            detail.setConnectorList(connectorVOList);
+        }
+
+        return detail;
+    }
+
+    /**
+     * 转换充电站信息为列表项
+     */
+    private ChargeStationListResponseData.StationItem convertToStationItem(StationInfoVO stationInfoVO) {
+        ChargeStationListResponseData.StationItem item = new ChargeStationListResponseData.StationItem();
+        item.setStationId(stationInfoVO.getStationId());
+        item.setStationName(stationInfoVO.getStationName());
+        item.setTips(stationInfoVO.getTips());
+        item.setDistance(stationInfoVO.getDistance());
+        item.setFastCharging(stationInfoVO.getFastCharging());
+        item.setSlowCharging(stationInfoVO.getSlowCharging());
+        item.setPeakValue(stationInfoVO.getPeakValue());
+        item.setPeakTime(stationInfoVO.getPeakTime());
+        item.setPeriodFlag(stationInfoVO.getPeriodFlag());
+        item.setPlatformPrice(stationInfoVO.getPlatformPrice());
+        return item;
+    }
+
+    /**
+     * 转换充电设备信息为详情响应(从AppletConnectorDetailVO转换)
+     */
+    private ChargeDeviceDetailResponseData convertToDeviceDetail(AppletConnectorDetailVO connectorDetail) {
+        ChargeDeviceDetailResponseData detail = new ChargeDeviceDetailResponseData();
+        detail.setConnectorId(connectorDetail.getConnectorId());
+        detail.setConnectorCode(connectorDetail.getConnectorCode());
+        detail.setConnectorName(connectorDetail.getConnectorName());
+        detail.setStationId(connectorDetail.getStationId());
+        detail.setStationName(connectorDetail.getStationName());
+        detail.setStationAddress(connectorDetail.getStationAddress());
+        detail.setEquipmentId(connectorDetail.getEquipmentId());
+        detail.setEquipmentCode(connectorDetail.getEquipmentCode());
+        detail.setEquipmentName(connectorDetail.getEquipmentName());
+        detail.setEquipmentType(connectorDetail.getEquipmentType());
+        detail.setEquipmentTypeName(connectorDetail.getEquipmentTypeName());
+        detail.setParkNo(connectorDetail.getParkNo());
+        detail.setStatus(connectorDetail.getStatus());
+        detail.setStatusName(connectorDetail.getStatusName());
+        detail.setConnectorType(connectorDetail.getConnectorType());
+        detail.setConnectorTypeName(connectorDetail.getConnectorTypeName());
+        detail.setVoltageUpperLimits(connectorDetail.getVoltageUpperLimits());
+        detail.setVoltageLowerLimits(connectorDetail.getVoltageLowerLimits());
+        detail.setCurrent(connectorDetail.getCurrent());
+        detail.setPower(connectorDetail.getPower());
+        detail.setNationalStandard(connectorDetail.getNationalStandard());
+        detail.setNationalStandardName(connectorDetail.getNationalStandardName());
+        detail.setCurrentPrice(connectorDetail.getCurrentPrice());
+        detail.setPeriodFlag(connectorDetail.getPeriodFlag());
+        detail.setCurrentPeriodDesc(connectorDetail.getCurrentPeriodDesc());
+        detail.setParkingTips(connectorDetail.getParkingTips());
+        return detail;
+    }
+
+}

+ 6 - 0
src/main/resources/mapper/business/ThirdPartyInfoMapper.xml

@@ -12,6 +12,12 @@
             app_id,
             auth_code,
             status,
+        operator_id,
+        api_base_url,
+        operator_secret,
+        sig_secret,
+        data_secret,
+        data_secret_iv AS dataSecretIV,
             remark,
             create_time
         FROM c_third_party_info

+ 41 - 1
src/main/resources/mapper/system/DataBoardMapper.xml

@@ -115,7 +115,7 @@
             s.station_name AS stationName,
             COALESCE(SUM(o.total_charge), 0) AS chargePower
         FROM c_charge_order_info o
-        LEFT JOIN t_third_party_station_info s ON o.third_party_station_id = s.station_id
+                 LEFT JOIN third_party_station_info s ON o.third_party_station_id = s.id
         WHERE o.status = 3 AND o.is_deleted = 0
         AND DATE(o.create_time) BETWEEN #{startDate} AND #{endDate}
         GROUP BY o.third_party_station_id, s.id, s.station_name
@@ -189,4 +189,44 @@
         WHERE first_order.status = 3 AND first_order.is_deleted = 0
     </select>
 
+    <!-- 获取流失用户列表(包含用户信息和最后一次充电信息) -->
+    <select id="selectChurnUserList" resultType="com.zsElectric.boot.system.model.dto.ChurnUserExportDTO">
+        SELECT
+        u.nick_name AS nickname,
+        u.phone AS phone,
+        CONCAT(DATE_FORMAT(last_order.start_time, '%Y-%m-%d %H:%i:%s'), ' 至 ', DATE_FORMAT(last_order.end_time, '%Y-%m-%d %H:%i:%s')) AS
+        lastChargeTimePeriod
+        FROM (
+        <!-- 早期有充电记录的用户 -->
+        SELECT DISTINCT user_id
+        FROM c_charge_order_info
+        WHERE status = 3 AND is_deleted = 0
+        AND DATE(create_time) BETWEEN #{earlyStart} AND #{earlyEnd}
+        ) early_users
+        <!-- 排除近期有充电记录的用户 -->
+        LEFT JOIN (
+        SELECT DISTINCT user_id
+        FROM c_charge_order_info
+        WHERE status = 3 AND is_deleted = 0
+        AND DATE(create_time) BETWEEN #{recentStart} AND #{recentEnd}
+        ) recent_users ON early_users.user_id = recent_users.user_id
+        <!-- 关联用户信息 -->
+        LEFT JOIN c_user_info u ON early_users.user_id = u.id
+        <!-- 关联最后一次充电订单 -->
+        LEFT JOIN (
+        SELECT o1.*
+        FROM c_charge_order_info o1
+        INNER JOIN (
+        SELECT user_id, MAX(create_time) AS max_time
+        FROM c_charge_order_info
+        WHERE status = 3 AND is_deleted = 0
+        GROUP BY user_id
+        ) o2 ON o1.user_id = o2.user_id AND o1.create_time = o2.max_time
+        WHERE o1.status = 3 AND o1.is_deleted = 0
+        ) last_order ON early_users.user_id = last_order.user_id
+        WHERE recent_users.user_id IS NULL
+        AND u.is_deleted = 0
+        ORDER BY last_order.create_time DESC
+    </select>
+
 </mapper>

+ 107 - 0
src/test/java/com/zsElectric/boot/thirdParty/service/QueryRechargeLevelPageMain.java

@@ -0,0 +1,107 @@
+package com.zsElectric.boot.thirdParty.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zsElectric.boot.common.util.AESCryptoUtils;
+import com.zsElectric.boot.common.util.HmacMD5Util;
+import com.zsElectric.boot.thirdParty.model.RechargeLevelPageRequestData;
+import com.zsElectric.boot.thirdParty.model.ThirdPartyRequest;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 模拟调用 queryRechargeLevelPage 方法的 main 测试类
+ * 用于构建并打印完整的请求参数,可复制到接口测试工具(如Postman)中测试
+ *
+ * @author wzq
+ */
+public class QueryRechargeLevelPageMain {
+
+    // ===== 配置参数(需根据数据库中的第三方配置信息修改) =====
+    // 运营商ID
+    private static final String OPERATOR_ID = "12345qwer";
+    // 运营商密钥
+    private static final String OPERATOR_SECRET = "vfkh4k740lfg88kq";
+    // 数据密钥(16位)
+    private static final String DATA_SECRET = "bbkwy062pzyjhqmg";
+    // 数据密钥IV(16位)
+    private static final String DATA_SECRET_IV = "xgbzfgwz6ki2gm5j";
+    // 签名密钥
+    private static final String SIG_SECRET = "8h9sf4zd5cbtlu8x";
+
+    // ===== Token配置(需要先调用queryToken获取) =====
+    // 访问Token(从queryToken接口获取,格式可以是 "Bearer xxx" 或直接 "xxx")
+    private static final String ACCESS_TOKEN = "Bearer 045b862a1c7f4867aeea48568fdf1fc686c5b6d10c7a4922a779fe9321a64ac7";
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("========== 模拟 queryRechargeLevelPage 请求构建 ==========\n");
+
+            // 1. 构建业务数据(分页查询参数)
+            RechargeLevelPageRequestData pageRequestData = new RechargeLevelPageRequestData();
+            pageRequestData.setPageNum(1);      // 第1页
+            pageRequestData.setPageSize(10);    // 每页10条
+
+            String jsonData = objectMapper.writeValueAsString(pageRequestData);
+            System.out.println("1. 业务数据(明文JSON):");
+            System.out.println("   " + jsonData);
+
+            // 2. AES加密业务数据
+            String encryptedData = AESCryptoUtils.encrypt(jsonData, DATA_SECRET, DATA_SECRET_IV);
+            System.out.println("\n2. 加密后的data:");
+            System.out.println("   " + encryptedData);
+
+            // 3. 生成时间戳(格式:yyyyMMddHHmmss)
+            String timeStamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+            System.out.println("\n3. 时间戳(timeStamp):");
+            System.out.println("   " + timeStamp);
+
+            // 4. 序列号(4位)
+            String seq = "0001";
+            System.out.println("\n4. 序列号(seq):");
+            System.out.println("   " + seq);
+
+            // 5. 生成签名
+            // 签名内容拼接顺序: operatorId + data + timeStamp + seq
+            String signContent = OPERATOR_ID + encryptedData + timeStamp + seq;
+            String sig = HmacMD5Util.hmacMD5Hex(signContent, SIG_SECRET);
+            System.out.println("\n5. 签名内容(signContent):");
+            System.out.println("   " + signContent);
+            System.out.println("\n6. 签名结果(sig):");
+            System.out.println("   " + sig);
+
+            // 6. 构建完整请求对象
+            ThirdPartyRequest request = new ThirdPartyRequest();
+            request.setOperatorId(OPERATOR_ID);
+            request.setData(encryptedData);
+            request.setTimeStamp(timeStamp);
+            request.setSeq(seq);
+            request.setSig(sig);
+
+            String requestJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
+            System.out.println("\n========== 完整请求JSON ==========");
+            System.out.println(requestJson);
+
+            // 7. 打印Authorization Header
+            System.out.println("\n========== Authorization Header ==========");
+            System.out.println("Authorization: " + ACCESS_TOKEN);
+            System.out.println("\n注意:ACCESS_TOKEN需要先调用queryToken接口获取,然后替换上面的值!");
+
+            // 打印cURL命令(方便复制测试)
+            System.out.println("\n========== cURL命令(POST请求) ==========");
+            String compactJson = objectMapper.writeValueAsString(request);
+            System.out.println("curl -X POST http://localhost:8080/thirdParty/query_recharge_level_page \\");
+            System.out.println("  -H \"Content-Type: application/json\" \\");
+            System.out.println("  -H \"Authorization: " + ACCESS_TOKEN + "\" \\");
+            System.out.println("  -d '" + compactJson + "'");
+
+            System.out.println("\n========== 测试完成 ==========");
+
+        } catch (Exception e) {
+            System.err.println("构建请求失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}

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

@@ -0,0 +1,104 @@
+package com.zsElectric.boot.thirdParty.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zsElectric.boot.common.util.AESCryptoUtils;
+import com.zsElectric.boot.common.util.HmacMD5Util;
+import com.zsElectric.boot.thirdParty.model.QueryTokenRequestData;
+import com.zsElectric.boot.thirdParty.model.ThirdPartyRequest;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 模拟调用 queryToken 方法的 main 测试类
+ * 用于构建并打印完整的请求参数,可复制到接口测试工具(如Postman)中测试
+ *
+ * @author wzq
+ */
+public class QueryTokenMain {
+
+    // ===== 配置参数(需根据数据库中的第三方配置信息修改) =====
+    // 运营商ID
+    private static final String OPERATOR_ID = "12345qwer";
+    // 运营商密钥
+    private static final String OPERATOR_SECRET = "vfkh4k740lfg88kq";
+    // 数据密钥(16位)
+    private static final String DATA_SECRET = "bbkwy062pzyjhqmg";
+    // 数据密钥IV(16位)
+    private static final String DATA_SECRET_IV = "xgbzfgwz6ki2gm5j";
+    // 签名密钥
+    private static final String SIG_SECRET = "8h9sf4zd5cbtlu8x";
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("========== 模拟 queryToken 请求构建 ==========\n");
+
+            // 1. 构建业务数据(query_token请求的data解密后内容)
+            QueryTokenRequestData tokenRequestData = new QueryTokenRequestData();
+            tokenRequestData.setOperatorId(OPERATOR_ID);
+            tokenRequestData.setOperatorSecret(OPERATOR_SECRET);
+
+            Map<String,Object> map = new HashMap<>();
+            map.put("sortType",1);
+            map.put("longitude",23.129163);
+            map.put("latitude",113.264435);
+
+            String jsonData = objectMapper.writeValueAsString(map);
+            System.out.println("1. 业务数据(明文JSON):");
+            System.out.println("   " + jsonData);
+
+            // 2. AES加密业务数据
+            String encryptedData = AESCryptoUtils.encrypt(jsonData, DATA_SECRET, DATA_SECRET_IV);
+            System.out.println("\n2. 加密后的data:");
+            System.out.println("   " + encryptedData);
+
+            // 3. 生成时间戳(格式:yyyyMMddHHmmss)
+            String timeStamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+            System.out.println("\n3. 时间戳(timeStamp):");
+            System.out.println("   " + timeStamp);
+
+            // 4. 序列号(4位)
+            String seq = "0001";
+            System.out.println("\n4. 序列号(seq):");
+            System.out.println("   " + seq);
+
+            // 5. 生成签名
+            // 签名内容拼接顺序: operatorId + data + timeStamp + seq
+            String signContent = OPERATOR_ID + encryptedData + timeStamp + seq;
+            String sig = HmacMD5Util.hmacMD5Hex(signContent, SIG_SECRET);
+            System.out.println("\n5. 签名内容(signContent):");
+            System.out.println("   " + signContent);
+            System.out.println("\n6. 签名结果(sig):");
+            System.out.println("   " + sig);
+
+            // 6. 构建完整请求对象
+            ThirdPartyRequest request = new ThirdPartyRequest();
+            request.setOperatorId(OPERATOR_ID);
+            request.setData(encryptedData);
+            request.setTimeStamp(timeStamp);
+            request.setSeq(seq);
+            request.setSig(sig);
+
+            String requestJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(request);
+            System.out.println("\n========== 完整请求JSON ==========");
+            System.out.println(requestJson);
+
+            // 打印cURL命令(方便复制测试)
+            System.out.println("\n========== cURL命令(POST请求) ==========");
+            String compactJson = objectMapper.writeValueAsString(request);
+            System.out.println("curl -X POST http://localhost:8080/thirdParty/query_token \\");
+            System.out.println("  -H \"Content-Type: application/json\" \\");
+            System.out.println("  -d '" + compactJson + "'");
+
+            System.out.println("\n========== 测试完成 ==========");
+
+        } catch (Exception e) {
+            System.err.println("构建请求失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}

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

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