Browse Source

- 新增数据库驱动的渠道管理系统
- 实现内部任务ID统一管理
- 支持多渠道自动重试机制
- 接入OW520和987AI充值渠道
- 完善执行日志和错误处理
- 优化会员错误识别逻辑

chan 1 year ago
parent
commit
ea69bd0313
26 changed files with 1733 additions and 401 deletions
  1. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/GptInternalTaskMapper.java
  2. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/GptRechargeChannelMapper.java
  3. 12 0
      netflix-dao/src/main/java/com/cyksj/mapper/GptTaskExecutionLogMapper.java
  4. 61 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GptInternalTask.java
  5. 5 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GptRechargeCardKey.java
  6. 61 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GptRechargeChannel.java
  7. 56 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GptTaskExecutionLog.java
  8. 5 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GroupsRelation.java
  9. 0 376
      netflix-service/src/main/java/com/cyksj/server/recharge/impl/GptProxyRechargeServiceImpl.java
  10. 2 2
      netflix-service/src/main/java/com/cyksj/service/mange/gpt/GptRechargeService.java
  11. 4 4
      netflix-service/src/main/java/com/cyksj/service/mange/gpt/impl/GptRechargeServiceImpl.java
  12. 94 0
      netflix-service/src/main/java/com/cyksj/service/recharge/ChannelCardKeyManager.java
  13. 108 0
      netflix-service/src/main/java/com/cyksj/service/recharge/DatabaseChannelManager.java
  14. 4 12
      netflix-service/src/main/java/com/cyksj/service/recharge/GptProxyRechargeService.java
  15. 127 0
      netflix-service/src/main/java/com/cyksj/service/recharge/InternalTaskManager.java
  16. 70 0
      netflix-service/src/main/java/com/cyksj/service/recharge/channel/RechargeChannel.java
  17. 110 0
      netflix-service/src/main/java/com/cyksj/service/recharge/channel/impl/AbstractRechargeChannel.java
  18. 197 0
      netflix-service/src/main/java/com/cyksj/service/recharge/channel/impl/Ai987RechargeChannelImpl.java
  19. 188 0
      netflix-service/src/main/java/com/cyksj/service/recharge/channel/impl/Ow520RechargeChannelImpl.java
  20. 1 1
      netflix-service/src/main/java/com/cyksj/service/recharge/dto/CardKeyValidationResult.java
  21. 1 1
      netflix-service/src/main/java/com/cyksj/service/recharge/dto/TaskResult.java
  22. 1 1
      netflix-service/src/main/java/com/cyksj/service/recharge/dto/TaskSubmitResult.java
  23. 1 1
      netflix-service/src/main/java/com/cyksj/service/recharge/dto/TokenParseResult.java
  24. 169 0
      netflix-service/src/main/java/com/cyksj/service/recharge/executor/MultiChannelRechargeExecutor.java
  25. 429 0
      netflix-service/src/main/java/com/cyksj/service/recharge/impl/GptProxyRechargeServiceImpl.java
  26. 3 3
      netflix-web/src/main/java/com/cyksj/web/controller/recharge/GptRechargeController.java

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/mapper/GptInternalTaskMapper.java

@@ -0,0 +1,12 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.GptInternalTask;
+
+/**
+ * GPT内部任务Mapper
+ * @author chan
+ * @date 2025/8/13
+ */
+public interface GptInternalTaskMapper extends BaseMapper<GptInternalTask> {
+}

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/mapper/GptRechargeChannelMapper.java

@@ -0,0 +1,12 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.GptRechargeChannel;
+
+/**
+ * GPT充值渠道配置Mapper
+ * @author chan
+ * @date 2025/8/13
+ */
+public interface GptRechargeChannelMapper extends BaseMapper<GptRechargeChannel> {
+}

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/mapper/GptTaskExecutionLogMapper.java

@@ -0,0 +1,12 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.GptTaskExecutionLog;
+
+/**
+ * GPT任务执行日志Mapper
+ * @author chan
+ * @date 2025/8/13
+ */
+public interface GptTaskExecutionLogMapper extends BaseMapper<GptTaskExecutionLog> {
+}

+ 61 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GptInternalTask.java

@@ -0,0 +1,61 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * GPT内部任务表
+ * @author chan
+ * @date 2025/8/13
+ */
+@Getter
+@Setter
+@TableName("gpt_internal_task")
+public class GptInternalTask extends BaseEntity {
+    
+    /**
+     * 内部任务ID
+     */
+    private String internalTaskId;
+    
+    /**
+     * 关联表ID
+     */
+    private Long relationId;
+    
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    
+    /**
+     * 用户访问令牌
+     */
+    private String accessToken;
+    
+    /**
+     * 充值账户
+     */
+    private String rechargeAccount;
+    
+    /**
+     * 任务状态 pending=待处理 processing=处理中 completed=已完成 failed=已失败
+     */
+    private String taskStatus;
+    
+    /**
+     * 当前执行渠道
+     */
+    private String currentChannel;
+    
+    /**
+     * 总重试次数
+     */
+    private Integer totalRetryCount;
+    
+    /**
+     * 最终结果信息
+     */
+    private String finalResult;
+}

+ 5 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GptRechargeCardKey.java

@@ -17,6 +17,11 @@ public class GptRechargeCardKey extends BaseEntity{
 	private String cardKey;
 
 	private Boolean status;
+	
+	/**
+	 * 所属渠道编码
+	 */
+	private String channelCode;
 
 	@TableField(updateStrategy = FieldStrategy.IGNORED)
 	private Long orderId;

+ 61 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GptRechargeChannel.java

@@ -0,0 +1,61 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * GPT充值渠道配置表
+ * @author chan
+ * @date 2025/8/13
+ */
+@Getter
+@Setter
+@TableName("gpt_recharge_channel")
+public class GptRechargeChannel extends BaseEntity {
+    
+    /**
+     * 渠道编码
+     */
+    private String channelCode;
+    
+    /**
+     * 渠道名称
+     */
+    private String channelName;
+    
+    /**
+     * 渠道API基础URL
+     */
+    private String baseUrl;
+    
+    /**
+     * 优先级,数字越小优先级越高
+     */
+    private Integer priority;
+    
+    /**
+     * 是否启用 0=禁用 1=启用
+     */
+    private Boolean enabled;
+    
+    /**
+     * 最大并发任务数
+     */
+    private Integer maxConcurrentTasks;
+    
+    /**
+     * 请求超时时间(毫秒)
+     */
+    private Integer requestTimeout;
+    
+    /**
+     * 重试延迟时间(毫秒)
+     */
+    private Integer retryDelay;
+    
+    /**
+     * 渠道特定配置(JSON格式)
+     */
+    private String channelConfig;
+}

+ 56 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GptTaskExecutionLog.java

@@ -0,0 +1,56 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * GPT任务执行日志表
+ * @author chan
+ * @date 2025/8/13
+ */
+@Getter
+@Setter
+@TableName("gpt_task_execution_log")
+public class GptTaskExecutionLog extends BaseEntity {
+    
+    /**
+     * 内部任务ID
+     */
+    private String internalTaskId;
+    
+    /**
+     * 执行渠道编码
+     */
+    private String channelCode;
+    
+    /**
+     * 外部任务ID
+     */
+    private String externalTaskId;
+    
+    /**
+     * 使用的卡密
+     */
+    private String cardKey;
+    
+    /**
+     * 执行状态 success=成功 failed=失败 retry=重试
+     */
+    private String executionStatus;
+    
+    /**
+     * 错误信息
+     */
+    private String errorMessage;
+    
+    /**
+     * 是否可重试 0=不可重试 1=可重试
+     */
+    private Integer isRetryable;
+    
+    /**
+     * 响应数据
+     */
+    private String responseData;
+}

+ 5 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GroupsRelation.java

@@ -68,6 +68,11 @@ public class GroupsRelation extends BaseEntity {
      * gpt 代充token
      */
     private String gptToken;
+    
+    /**
+     * 内部任务ID
+     */
+    private String internalTaskId;
 
     private Date submitTime;
 

+ 0 - 376
netflix-service/src/main/java/com/cyksj/server/recharge/impl/GptProxyRechargeServiceImpl.java

@@ -1,376 +0,0 @@
-package com.cyksj.server.recharge.impl;
-
-import cn.hutool.core.date.DateTime;
-import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.http.HttpRequest;
-import cn.hutool.http.HttpResponse;
-import cn.hutool.json.JSONObject;
-import cn.hutool.json.JSONUtil;
-import com.baomidou.mybatisplus.core.toolkit.Wrappers;
-import com.cyksj.common.constant.Constant;
-import com.cyksj.common.exception.BusinessRuntimeException;
-import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
-import com.cyksj.mapper.*;
-import com.cyksj.model.entity.*;
-import com.cyksj.model.request.GroupsRelationRechargeReq;
-import com.cyksj.redis.RedisService;
-import com.cyksj.server.recharge.GptProxyRechargeService;
-import com.cyksj.server.recharge.dto.CardKeyValidationResult;
-import com.cyksj.server.recharge.dto.TaskResult;
-import com.cyksj.server.recharge.dto.TaskSubmitResult;
-import com.cyksj.server.recharge.dto.TokenParseResult;
-import com.cyksj.service.chatgpt.GptUserRechargeRecordService;
-import com.cyksj.service.user.UserBindRelationService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.Date;
-import java.util.List;
-
-/**
- * @author chan
- * @date 2025/7/30 18:55
- */
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class GptProxyRechargeServiceImpl implements GptProxyRechargeService {
-
-    private static final String BASE_URL = "https://api.ow520.com/api";
-
-    private final GptRechargeCardKeyMapper gptRechargeCardKeyMapper;
-
-    private final GroupsRelationMapper relationMapper;
-
-    private final UserBindRelationService userBindRelationService;
-
-    private final GroupsMapper groupsMapper;
-
-    private final GoodsDonSkuMapper skuMapper;
-
-    private final OrderDonMapper orderDonMapper;
-
-	private final GptUserRechargeRecordService gptUserRechargeRecordService;
-
-	private final RedisService redisService;
-
-	private static final GlobalThreadPoolTaskExecutor TASK_EXECUTOR = GlobalThreadPoolTaskExecutor.getInstance();
-
-	private HttpRequest createBaseRequest(String uri) {
-        return HttpRequest.get(BASE_URL + uri)
-                .header("accept", "application/json, text/plain, */*")
-                .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
-                .header("origin", "https://www.ow520.com")
-                .header("referer", "https://www.ow520.com/")
-                .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
-    }
-
-    public CardKeyValidationResult validateCardKey(String cardKey) {
-        log.info("开始验证卡密: {}", cardKey);
-        try {
-            HttpResponse response = createBaseRequest("/card-keys/" + cardKey)
-                    .execute();
-            String body = response.body();
-            log.info("卡密验证响应: {}", body);
-            
-            JSONObject jsonResponse = JSONUtil.parseObj(body);
-            CardKeyValidationResult result = new CardKeyValidationResult(
-                jsonResponse.getBool("available")
-            );
-            //{"available":false,"error":"卡密已被使用"}
-            log.info("卡密验证结果: {}", result);
-            return result;
-        } catch (Exception e) {
-            log.error("验证卡密失败: {}", e.getMessage(), e);
-            throw new RuntimeException("验证卡密失败: " + e.getMessage(), e);
-        }
-    }
-
-    public TokenParseResult parseToken(String accessToken) {
-        log.info("开始验证凭证: {}", accessToken.substring(0, Math.min(20, accessToken.length())) + "...");
-        try {
-            String url = BASE_URL + "/parse-token";
-            JSONObject requestBody = new JSONObject();
-            requestBody.put("access_token", accessToken);
-            
-            HttpResponse response = HttpRequest.post(url)
-                    .header("accept", "application/json, text/plain, */*")
-                    .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
-                    .header("content-type", "application/json")
-                    .header("origin", "https://www.ow520.com")
-                    .header("referer", "https://www.ow520.com/")
-                    .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
-                    .body(requestBody.toString())
-                    .execute();
-            String body = response.body();
-            log.info("凭证验证响应: {}", body);
-            
-            JSONObject jsonResponse = JSONUtil.parseObj(body);
-            TokenParseResult result = new TokenParseResult(
-                jsonResponse.getStr("message"),
-                jsonResponse.getBool("success")
-            );
-            log.info("凭证验证结果: {}", result);
-            return result;
-        } catch (Exception e) {
-            log.error("验证凭证失败: {}", e.getMessage(), e);
-            throw new RuntimeException("验证凭证失败: " + e.getMessage(), e);
-        }
-    }
-
-    public TaskSubmitResult submitTask(String cardKey, String accessToken, String idp) {
-        log.info("开始提交任务 - 卡密: {}, idp: {}", cardKey, idp);
-        try {
-            String url = BASE_URL + "/tasks";
-            JSONObject requestBody = new JSONObject();
-            requestBody.put("card_key", cardKey);
-            requestBody.put("access_token", accessToken);
-            requestBody.put("idp", idp);
-            
-            HttpResponse response = HttpRequest.post(url)
-                    .header("accept", "application/json, text/plain, */*")
-                    .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
-                    .header("content-type", "application/json")
-                    .header("origin", "https://www.ow520.com")
-                    .header("referer", "https://www.ow520.com/")
-                    .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
-                    .body(requestBody.toString())
-                    .execute();
-            String body = response.body();
-            log.info("任务提交响应: {}", body);
-            
-            JSONObject jsonResponse = JSONUtil.parseObj(body);
-            TaskSubmitResult result = new TaskSubmitResult(
-                jsonResponse.getStr("task_id"),
-                jsonResponse.getBool("success")
-            );
-            log.info("任务提交结果: {}", result);
-            return result;
-        } catch (Exception e) {
-            log.error("提交任务失败: {}", e.getMessage(), e);
-            throw new RuntimeException("提交任务失败: " + e.getMessage(), e);
-        }
-    }
-
-    public TaskResult getTaskResult(String taskId) {
-        log.info("开始查询任务结果: {}", taskId);
-        try {
-            String uri = "/tasks/" + taskId;
-            HttpResponse response = createBaseRequest(uri)
-                    .execute();
-            String body = response.body();
-            log.info("任务查询响应: {}", body);
-
-            JSONObject jsonResponse = JSONUtil.parseObj(body);
-            TaskResult result = new TaskResult(
-                jsonResponse.getStr("status"),
-                jsonResponse.getStr("result")
-            );
-            log.info("任务查询结果: {}", result);
-	        String status = result.getStatus();
-			String resultResult = result.getResult();
-			//更新代充状态
-			resetGroupsRelationRechargeStatus(taskId, status, resultResult);
-	        return result;
-        } catch (Exception e) {
-            log.error("获取任务结果失败: {}", e.getMessage(), e);
-            throw new RuntimeException("获取任务结果失败: " + e.getMessage(), e);
-        }
-    }
-
-	@Override
-	@Transactional(rollbackFor = Throwable.class)
-	public String confirmRecharge(GroupsRelationRechargeReq req) {
-		Long relationId = req.getRelationId();
-		Long userId = req.getUserId();
-		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
-		GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class)
-				.in(GroupsRelation::getUserId, userIdList)
-				.eq(GroupsRelation::getId, relationId)
-				.eq(GroupsRelation::getStatus, GroupsRelation.Status.validity));
-		if (relation == null) {
-			throw BusinessRuntimeException.getInstance("车票不存在");
-		}
-		GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
-		GoodsDonSku sku = skuMapper.selectById(groupsTrips.getSkuId());
-//		//GPT 代充
-//		if (sku.getGoodsId() != Constant.GPT_RECHARGE_GOODS_ID) {
-//			throw BusinessRuntimeException.getInstance("车票类型错误");
-//		}
-		Integer rechargeRemainNum = relation.getRechargeRemainNum();
-		if (rechargeRemainNum == null || rechargeRemainNum <= 0) {
-			throw BusinessRuntimeException.getInstance("剩余可代充次数为0");
-		}
-		if (relation.getRechargeStatus() == GroupsRelation.RechargeStatus.recharging) {
-			throw BusinessRuntimeException.getInstance("确认充值中");
-		}
-		if (relation.getRechargeStatus() == GroupsRelation.RechargeStatus.recharge_error) {
-			throw BusinessRuntimeException.getInstance("充值失败,请联系客服开通");
-		}
-		String key = RedisService.key.GPT_RECHARGE_KEY.getName() + relationId;
-		boolean b = redisService.setNx(key, relationId, RedisService.key.GPT_RECHARGE_KEY.getTimeout());
-		if (!b) {
-			throw BusinessRuntimeException.getInstance("确认充值中");
-		}
-		try {
-			int updateRemainNum = relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
-					.set(GroupsRelation::getRechargeRemainNum, rechargeRemainNum - 1)
-					.eq(GroupsRelation::getId, relationId)
-					.eq(GroupsRelation::getUserId, relation.getUserId())
-					.eq(GroupsRelation::getRechargeRemainNum, rechargeRemainNum));
-			if (updateRemainNum == 0) {
-				throw BusinessRuntimeException.getInstance("代充异常,请重试");
-			}
-			OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
-					.in(OrderDon::getUserId, userIdList)
-					.eq(OrderDon::getRelationId, relationId)
-					.notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
-					.orderByDesc(OrderDon::getId)
-					.last("limit 1"));
-			if (orderDon == null) {
-				throw BusinessRuntimeException.getInstance("代充订单不存在或已退款");
-			}
-			String accessToken = req.getAccessToken();
-			//验证用户token是否正确
-			TokenParseResult tokenParseResult = parseToken(accessToken);
-			if (!tokenParseResult.getSuccess()) {
-				throw BusinessRuntimeException.getInstance("您输入的凭证有误,请重新输入");
-			}
-			String rechargeAccount = tokenParseResult.getMessage();
-			//记录代充账号 token
-			relation.setAccount(rechargeAccount);
-			//获取GPT代充key
-			GptRechargeCardKey gptRechargeCardKey = gptRechargeCardKeyMapper.selectOne(Wrappers.lambdaQuery(GptRechargeCardKey.class)
-					.eq(GptRechargeCardKey::getStatus, Boolean.FALSE)
-					.last("order by rand() limit 1"));
-			if (gptRechargeCardKey == null) {
-				setRechargeRelationStatusAutoError(relation);
-				throw BusinessRuntimeException.getInstance("充值失败,请联系客服开通");
-			}
-			int update = gptRechargeCardKeyMapper.update(null, Wrappers.lambdaUpdate(GptRechargeCardKey.class)
-					.set(GptRechargeCardKey::getStatus, Boolean.TRUE)
-					.set(GptRechargeCardKey::getOrderId, orderDon.getId())
-					.set(GptRechargeCardKey::getOrderNo, orderDon.getOrderNo())
-					.eq(GptRechargeCardKey::getId, gptRechargeCardKey.getId())
-					.eq(GptRechargeCardKey::getStatus, Boolean.FALSE));
-			if (update == 0) {
-				setRechargeRelationStatusAutoError(relation);
-				throw BusinessRuntimeException.getInstance("充值失败,请联系客服开通");
-			}
-			String gptCardKey = gptRechargeCardKey.getCardKey();
-			//校验卡密是否可用
-			CardKeyValidationResult cardKeyValidationResult = validateCardKey(gptCardKey);
-			if (!cardKeyValidationResult.getAvailable()) {
-				setRechargeRelationStatusAutoError(relation);
-				throw BusinessRuntimeException.getInstance("充值失败,请联系客服开通");
-			}
-			//提交任务
-			TaskSubmitResult result = submitTask(gptCardKey, accessToken, "auth0");
-			if (!result.getSuccess()) {
-				setRechargeRelationStatusAutoError(relation);
-				throw BusinessRuntimeException.getInstance("充值失败,请联系客服开通");
-			}
-			String taskId = result.getTaskId();
-			relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
-					//记录代充账号
-					.set(GroupsRelation::getAccount, rechargeAccount)
-					.set(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharging)
-					.set(GroupsRelation::getCardKey, gptCardKey)
-					.set(GroupsRelation::getGptToken, accessToken)
-					.set(GroupsRelation::getGptTaskId, taskId)
-					.eq(GroupsRelation::getId, relationId));
-			return taskId;
-		} finally {
-			if (redisService.hasKey(key)) {
-				redisService.del(key);
-			}
-		}
-	}
-
-	/**
-	 * 设置代充自动充值异常
-	 */
-	public void setRechargeRelationStatusAutoError(GroupsRelation relation) {
-		TASK_EXECUTOR.execute(()->{
-			relation.setRechargeStatus(GroupsRelation.RechargeStatus.recharge_error);
-			relationMapper.updateById(relation);
-		});
-	}
-
-	/**
-	 * 更新代充状态
-	 */
-	private void resetGroupsRelationRechargeStatus(String taskId, String status, String result) {
-		if ("completed".equals(status) || "failed".equals(status) || "unknown".equals(status)) {
-			GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class)
-					.eq(GroupsRelation::getGptTaskId, taskId)
-					.last("limit 1"));
-			if (relation != null && relation.getRechargeStatus() == GroupsRelation.RechargeStatus.recharging) {
-				Integer rechargeRemainNum = relation.getRechargeRemainNum();
-				GroupsRelation.RechargeStatus rechargeStatus = "completed".equals(status) ? GroupsRelation.RechargeStatus.complete : GroupsRelation.RechargeStatus.recharge_error;
-				String cardKey = relation.getCardKey();
-				GptRechargeCardKey gptRechargeCardKey = gptRechargeCardKeyMapper.selectOne(Wrappers.lambdaQuery(GptRechargeCardKey.class)
-						.eq(GptRechargeCardKey::getCardKey, cardKey)
-						.last("limit 1"));
-				if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
-					if (gptRechargeCardKey != null) {
-						gptRechargeCardKeyMapper.update(null, Wrappers.lambdaUpdate(GptRechargeCardKey.class)
-								.set(GptRechargeCardKey::getStatus, Boolean.FALSE)
-								.set(GptRechargeCardKey::getOrderId, null)
-								.set(GptRechargeCardKey::getOrderNo, null)
-								.eq(GptRechargeCardKey::getId, gptRechargeCardKey.getId())
-								.eq(GptRechargeCardKey::getStatus, Boolean.TRUE));
-					}
-				}
-				if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
-					if (!redisService.setNx(RedisService.key.GPT_RECHARGE_RECOVER_KEY.getName() + taskId, taskId, RedisService.key.GPT_RECHARGE_RECOVER_KEY.getTimeout())) {
-						return;
-					}
-				}
-				Date startTime = relation.getStartTime();
-				DateTime expiryTime = null;
-				if (rechargeStatus == GroupsRelation.RechargeStatus.complete && startTime == null) {
-					GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
-					if (groupsTrips != null) {
-						startTime = DateTime.now();
-						GoodsDonSku sku = skuMapper.selectById(groupsTrips.getSkuId());
-						if (sku.getDays() != null && sku.getDays() > 0) {
-							expiryTime = DateUtil.offsetDay(startTime, sku.getDays());
-						} else {
-							expiryTime = DateUtil.offsetMonth(startTime, sku.getMonths());
-						}
-					}
-				}
-				String rechargeErrorInfo = null;
-				if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
-					rechargeErrorInfo = result;
-				}
-				relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
-						.set(rechargeStatus == GroupsRelation.RechargeStatus.recharge_error, GroupsRelation::getRechargeRemainNum, rechargeRemainNum + 1)
-						.set(GroupsRelation::getRechargeStatus, rechargeStatus)
-						.set(expiryTime != null, GroupsRelation::getStartTime, startTime)
-						.set(expiryTime != null, GroupsRelation::getExpiryTime, expiryTime)
-						.set(StrUtil.isNotEmpty(rechargeErrorInfo), GroupsRelation::getRechargeErrorInfo, rechargeErrorInfo)
-						.eq(GroupsRelation::getId, relation.getId())
-						.le(GroupsRelation::getRechargeRemainNum, relation.getRechargeNum())
-						.eq(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharging));
-				//记录代充记录
-				if (rechargeStatus == GroupsRelation.RechargeStatus.complete) {
-					gptUserRechargeRecordService.recordGptUserRechargeNum(relation.getUserId(), relation.getId(), relation.getAccount(), -1, null, "自动代充");
-					//修改对应订单状态
-					if (gptRechargeCardKey != null) {
-						Long orderId = gptRechargeCardKey.getOrderId();
-						OrderDon orderDon = orderDonMapper.selectById(orderId);
-						if (orderDon != null && orderDon.getStatus() == OrderDon.Status.hasPayment) {
-							orderDon.setStatus(OrderDon.Status.complete);
-							orderDonMapper.updateById(orderDon);
-						}
-					}
-				}
-			}
-		}
-	}
-}

+ 2 - 2
netflix-service/src/main/java/com/cyksj/service/mange/gpt/GptRechargeService.java

@@ -3,8 +3,8 @@ package com.cyksj.service.mange.gpt;
 import com.cyksj.model.entity.GroupsRelation;
 import com.cyksj.model.request.GptIndependentExportRechargeReq;
 import com.cyksj.model.request.GroupsRelationRechargeReq;
-import com.cyksj.server.recharge.dto.TaskResult;
-import com.cyksj.server.recharge.dto.TokenParseResult;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
 
 /**
  * 项目名: yhlxj11111111

+ 4 - 4
netflix-service/src/main/java/com/cyksj/service/mange/gpt/impl/GptRechargeServiceImpl.java

@@ -17,10 +17,10 @@ import com.cyksj.model.entity.*;
 import com.cyksj.model.request.GptIndependentExportRechargeReq;
 import com.cyksj.model.request.GroupsRelationRechargeReq;
 import com.cyksj.redis.RedisService;
-import com.cyksj.server.recharge.dto.CardKeyValidationResult;
-import com.cyksj.server.recharge.dto.TaskResult;
-import com.cyksj.server.recharge.dto.TaskSubmitResult;
-import com.cyksj.server.recharge.dto.TokenParseResult;
+import com.cyksj.service.recharge.dto.CardKeyValidationResult;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TaskSubmitResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
 import com.cyksj.service.chatgpt.GptUserRechargeRecordService;
 import com.cyksj.service.mange.gpt.GptRechargeService;
 import com.cyksj.service.user.UserBindRelationService;

+ 94 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/ChannelCardKeyManager.java

@@ -0,0 +1,94 @@
+package com.cyksj.service.recharge;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.mapper.GptRechargeCardKeyMapper;
+import com.cyksj.model.entity.GptRechargeCardKey;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 渠道卡密管理器
+ * @author chan
+ * @date 2025/8/13
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ChannelCardKeyManager {
+    
+    private final GptRechargeCardKeyMapper cardKeyMapper;
+    
+    /**
+     * 获取指定渠道的可用卡密
+     */
+    public GptRechargeCardKey getAvailableCardKey(String channelCode, Long orderId, String orderNo) {
+        log.info("获取渠道 {} 的可用卡密", channelCode);
+        
+        GptRechargeCardKey cardKey = cardKeyMapper.selectOne(
+            Wrappers.lambdaQuery(GptRechargeCardKey.class)
+                .eq(GptRechargeCardKey::getChannelCode, channelCode)
+                .eq(GptRechargeCardKey::getStatus, Boolean.FALSE)
+                .last("ORDER BY RAND() LIMIT 1")
+        );
+        
+        if (cardKey != null) {
+            // 标记卡密为已使用
+            int updated = cardKeyMapper.update(null, 
+                Wrappers.lambdaUpdate(GptRechargeCardKey.class)
+                    .set(GptRechargeCardKey::getStatus, Boolean.TRUE)
+                    .set(GptRechargeCardKey::getOrderId, orderId)
+                    .set(GptRechargeCardKey::getOrderNo, orderNo)
+                    .eq(GptRechargeCardKey::getId, cardKey.getId())
+                    .eq(GptRechargeCardKey::getStatus, Boolean.FALSE)
+            );
+            
+            if (updated > 0) {
+                log.info("成功获取渠道 {} 的卡密: {}", channelCode, cardKey.getCardKey());
+                return cardKey;
+            } else {
+                log.warn("卡密已被其他线程占用,渠道: {}", channelCode);
+                return null;
+            }
+        }
+        
+        log.warn("渠道 {} 无可用卡密", channelCode);
+        return null;
+    }
+    
+    /**
+     * 释放卡密(充值失败时)
+     */
+    public void releaseCardKey(String cardKey) {
+        if (cardKey == null) {
+            return;
+        }
+        
+        log.info("释放卡密: {}", cardKey);
+        int updated = cardKeyMapper.update(null, 
+            Wrappers.lambdaUpdate(GptRechargeCardKey.class)
+                .set(GptRechargeCardKey::getStatus, Boolean.FALSE)
+                .set(GptRechargeCardKey::getOrderId, null)
+                .set(GptRechargeCardKey::getOrderNo, null)
+                .eq(GptRechargeCardKey::getCardKey, cardKey)
+        );
+        
+        if (updated > 0) {
+            log.info("成功释放卡密: {}", cardKey);
+        } else {
+            log.warn("释放卡密失败,可能已被释放: {}", cardKey);
+        }
+    }
+    
+    /**
+     * 检查渠道是否有可用卡密
+     */
+    public boolean hasAvailableCardKey(String channelCode) {
+        Integer count = cardKeyMapper.selectCount(
+            Wrappers.lambdaQuery(GptRechargeCardKey.class)
+                .eq(GptRechargeCardKey::getChannelCode, channelCode)
+                .eq(GptRechargeCardKey::getStatus, Boolean.FALSE)
+        );
+        return count != null && count > 0;
+    }
+}

+ 108 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/DatabaseChannelManager.java

@@ -0,0 +1,108 @@
+package com.cyksj.service.recharge;
+
+import cn.hutool.core.util.RandomUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.mapper.GptRechargeChannelMapper;
+import com.cyksj.mapper.GptTaskExecutionLogMapper;
+import com.cyksj.model.entity.GptRechargeChannel;
+import com.cyksj.model.entity.GptTaskExecutionLog;
+import com.cyksj.service.recharge.channel.RechargeChannel;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 数据库渠道管理器
+ * @author chan
+ * @date 2025/8/13
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DatabaseChannelManager {
+    
+    private final GptRechargeChannelMapper channelMapper;
+    private final GptTaskExecutionLogMapper executionLogMapper;
+    
+    @Autowired
+    private Map<String, RechargeChannel> channelImplementations;
+    
+    /**
+     * 获取按优先级排序的可用渠道列表
+     */
+    public List<GptRechargeChannel> getAvailableChannels() {
+        return channelMapper.selectList(Wrappers.lambdaQuery(GptRechargeChannel.class)
+            .eq(GptRechargeChannel::getEnabled, true)
+            .orderByAsc(GptRechargeChannel::getPriority));
+    }
+    
+    /**
+     * 获取渠道实现
+     */
+    public RechargeChannel getChannelImplementation(String channelCode) {
+        String beanName = channelCode.toLowerCase() + "RechargeChannel";
+        RechargeChannel channel = channelImplementations.get(beanName);
+        if (channel == null) {
+            log.error("找不到渠道实现: {}, 可用的实现: {}", beanName, channelImplementations.keySet());
+            throw new RuntimeException("找不到渠道实现: " + channelCode);
+        }
+        
+        // 获取渠道配置并设置到实现中
+        GptRechargeChannel config = getChannelConfig(channelCode);
+        if (config != null) {
+            channel.setChannelConfig(config.getBaseUrl(), config.getChannelConfig());
+        }
+        
+        return channel;
+    }
+    
+    /**
+     * 获取渠道配置
+     */
+    public GptRechargeChannel getChannelConfig(String channelCode) {
+        return channelMapper.selectOne(Wrappers.lambdaQuery(GptRechargeChannel.class)
+            .eq(GptRechargeChannel::getChannelCode, channelCode)
+            .eq(GptRechargeChannel::getEnabled, true));
+    }
+    
+    /**
+     * 获取下一个可用渠道(排除已尝试的渠道)
+     */
+    public GptRechargeChannel getNextAvailableChannel(String internalTaskId) {
+        // 查询已尝试过的渠道
+        Set<String> triedChannels = getTriedChannels(internalTaskId);
+        
+        return getAvailableChannels().stream()
+            .filter(channel -> !triedChannels.contains(channel.getChannelCode()))
+            .findFirst()
+            .orElse(null);
+    }
+    
+    /**
+     * 获取已尝试过的渠道
+     */
+    public Set<String> getTriedChannels(String internalTaskId) {
+        List<String> triedChannels = executionLogMapper.selectList(
+            Wrappers.lambdaQuery(GptTaskExecutionLog.class)
+                .select(GptTaskExecutionLog::getChannelCode)
+                .eq(GptTaskExecutionLog::getInternalTaskId, internalTaskId)
+        ).stream()
+        .map(GptTaskExecutionLog::getChannelCode)
+        .collect(Collectors.toList());
+        
+        return Set.copyOf(triedChannels);
+    }
+    
+    /**
+     * 生成内部任务ID
+     */
+    public String generateInternalTaskId() {
+        return "IT" + System.currentTimeMillis() + RandomUtil.randomString(6);
+    }
+}

+ 4 - 12
netflix-service/src/main/java/com/cyksj/server/recharge/GptProxyRechargeService.java → netflix-service/src/main/java/com/cyksj/service/recharge/GptProxyRechargeService.java

@@ -1,28 +1,20 @@
-package com.cyksj.server.recharge;
+package com.cyksj.service.recharge;
 
 import com.cyksj.model.request.GroupsRelationRechargeReq;
-import com.cyksj.server.recharge.dto.*;
+import com.cyksj.service.recharge.dto.*;
 
 /**
  * @author chan
  * @date 2025/7/30 18:55
  */
 public interface GptProxyRechargeService {
-    
-    /**
-     * 验证卡密
-     */
-    CardKeyValidationResult validateCardKey(String cardKey);
+
     
     /**
      * 验证凭证
      */
     TokenParseResult parseToken(String accessToken);
-    
-    /**
-     * 提交任务
-     */
-    TaskSubmitResult submitTask(String cardKey, String accessToken, String idp);
+
     
     /**
      * 获取任务结果

+ 127 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/InternalTaskManager.java

@@ -0,0 +1,127 @@
+package com.cyksj.service.recharge;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.mapper.GptInternalTaskMapper;
+import com.cyksj.mapper.GptTaskExecutionLogMapper;
+import com.cyksj.model.entity.GptInternalTask;
+import com.cyksj.model.entity.GptTaskExecutionLog;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 内部任务管理器
+ * @author chan
+ * @date 2025/8/13
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class InternalTaskManager {
+    
+    private final GptInternalTaskMapper internalTaskMapper;
+    private final GptTaskExecutionLogMapper executionLogMapper;
+    
+    /**
+     * 创建内部任务
+     */
+    public String createInternalTask(Long relationId, Long userId, String accessToken, String rechargeAccount) {
+        log.info("创建内部任务 - relationId: {}, userId: {}", relationId, userId);
+        
+        String internalTaskId = generateInternalTaskId();
+        
+        GptInternalTask task = new GptInternalTask();
+        task.setInternalTaskId(internalTaskId);
+        task.setRelationId(relationId);
+        task.setUserId(userId);
+        task.setAccessToken(accessToken);
+        task.setRechargeAccount(rechargeAccount);
+        task.setTaskStatus("pending");
+        task.setTotalRetryCount(0);
+        
+        internalTaskMapper.insert(task);
+        log.info("内部任务创建成功: {}", internalTaskId);
+        return internalTaskId;
+    }
+    
+    /**
+     * 获取内部任务详情
+     */
+    public GptInternalTask getInternalTask(String internalTaskId) {
+        return internalTaskMapper.selectOne(
+            Wrappers.lambdaQuery(GptInternalTask.class)
+                .eq(GptInternalTask::getInternalTaskId, internalTaskId)
+        );
+    }
+    
+    /**
+     * 更新任务状态
+     */
+    public void updateTaskStatus(String internalTaskId, String status, String currentChannel, String finalResult) {
+        log.info("更新任务状态 - 任务ID: {}, 状态: {}, 渠道: {}", internalTaskId, status, currentChannel);
+        
+        internalTaskMapper.update(null,
+            Wrappers.lambdaUpdate(GptInternalTask.class)
+                .set(GptInternalTask::getTaskStatus, status)
+                .set(currentChannel != null, GptInternalTask::getCurrentChannel, currentChannel)
+                .set(finalResult != null, GptInternalTask::getFinalResult, finalResult)
+                .eq(GptInternalTask::getInternalTaskId, internalTaskId)
+        );
+    }
+    
+    /**
+     * 增加重试次数
+     */
+    public void incrementRetryCount(String internalTaskId) {
+        GptInternalTask task = getInternalTask(internalTaskId);
+        if (task != null) {
+            int newCount = (task.getTotalRetryCount() == null ? 0 : task.getTotalRetryCount()) + 1;
+            internalTaskMapper.update(null,
+                Wrappers.lambdaUpdate(GptInternalTask.class)
+                    .set(GptInternalTask::getTotalRetryCount, newCount)
+                    .eq(GptInternalTask::getInternalTaskId, internalTaskId)
+            );
+        }
+    }
+    
+    /**
+     * 记录渠道执行结果
+     */
+    public void logChannelExecution(String internalTaskId, String channelCode, 
+            String externalTaskId, String cardKey, String status, 
+            String errorMessage, boolean isRetryable, String responseData) {
+        
+        log.info("记录渠道执行日志 - 任务: {}, 渠道: {}, 状态: {}", internalTaskId, channelCode, status);
+        
+        GptTaskExecutionLog log = new GptTaskExecutionLog();
+        log.setInternalTaskId(internalTaskId);
+        log.setChannelCode(channelCode);
+        log.setExternalTaskId(externalTaskId);
+        log.setCardKey(cardKey);
+        log.setExecutionStatus(status);
+        log.setErrorMessage(errorMessage);
+        log.setIsRetryable(isRetryable ? 1 : 0);
+        log.setResponseData(responseData);
+        
+        executionLogMapper.insert(log);
+    }
+    
+    /**
+     * 获取任务的最后一次执行结果
+     */
+    public GptTaskExecutionLog getLastExecutionLog(String internalTaskId) {
+        return executionLogMapper.selectOne(
+            Wrappers.lambdaQuery(GptTaskExecutionLog.class)
+                .eq(GptTaskExecutionLog::getInternalTaskId, internalTaskId)
+                .orderByDesc(GptTaskExecutionLog::getId)
+                .last("LIMIT 1")
+        );
+    }
+    
+    /**
+     * 生成内部任务ID
+     */
+    private String generateInternalTaskId() {
+        return "IT" + System.currentTimeMillis() + String.format("%04d", (int)(Math.random() * 10000));
+    }
+}

+ 70 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/channel/RechargeChannel.java

@@ -0,0 +1,70 @@
+package com.cyksj.service.recharge.channel;
+
+import com.cyksj.service.recharge.dto.CardKeyValidationResult;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TaskSubmitResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
+
+/**
+ * 充值渠道接口
+ * @author chan
+ * @date 2025/8/13
+ */
+public interface RechargeChannel {
+    
+    /**
+     * 获取渠道编码
+     * @return 渠道编码
+     */
+    String getChannelCode();
+    
+    /**
+     * 获取渠道名称
+     * @return 渠道名称
+     */
+    String getChannelName();
+    
+    /**
+     * 验证卡密
+     * @param cardKey 卡密
+     * @return 验证结果
+     */
+    CardKeyValidationResult validateCardKey(String cardKey);
+    
+    /**
+     * 解析访问令牌
+     * @param accessToken 访问令牌
+     * @return 解析结果
+     */
+    TokenParseResult parseToken(String accessToken);
+    
+    /**
+     * 提交充值任务
+     * @param cardKey 卡密
+     * @param accessToken 访问令牌
+     * @param idp 身份提供者
+     * @return 任务提交结果
+     */
+    TaskSubmitResult submitTask(String cardKey, String accessToken, String idp);
+    
+    /**
+     * 获取任务结果
+     * @param taskId 任务ID
+     * @return 任务结果
+     */
+    TaskResult getTaskResult(String taskId);
+    
+    /**
+     * 判断错误是否可重试
+     * @param errorMessage 错误消息
+     * @return 是否可重试
+     */
+    boolean isRetryableError(String errorMessage);
+    
+    /**
+     * 设置渠道配置
+     * @param baseUrl 基础URL
+     * @param config 渠道特定配置
+     */
+    void setChannelConfig(String baseUrl, String config);
+}

+ 110 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/channel/impl/AbstractRechargeChannel.java

@@ -0,0 +1,110 @@
+package com.cyksj.service.recharge.channel.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.cyksj.service.recharge.channel.RechargeChannel;
+import com.cyksj.service.recharge.dto.CardKeyValidationResult;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TaskSubmitResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 抽象充值渠道基类
+ * @author chan
+ * @date 2025/8/13
+ */
+@Slf4j
+public abstract class AbstractRechargeChannel implements RechargeChannel {
+    
+    protected String baseUrl;
+    protected String channelConfig;
+    
+    @Override
+    public void setChannelConfig(String baseUrl, String config) {
+        if (StrUtil.isNotEmpty(baseUrl)) {
+            this.baseUrl = baseUrl;
+        }
+        this.channelConfig = config;
+    }
+    
+    /**
+     * 创建基础HTTP请求
+     * @param uri 请求路径
+     * @return HttpRequest
+     */
+    protected HttpRequest createBaseRequest(String uri) {
+        return HttpRequest.get(baseUrl + uri)
+                .header("accept", "application/json, text/plain, */*")
+                .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
+                .header("user-agent", getUserAgent());
+    }
+    
+    /**
+     * 创建POST请求
+     * @param uri 请求路径
+     * @param requestBody 请求体
+     * @return HttpRequest
+     */
+    protected HttpRequest createPostRequest(String uri, JSONObject requestBody) {
+        return HttpRequest.post(baseUrl + uri)
+                .header("accept", "application/json, text/plain, */*")
+                .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
+                .header("content-type", "application/json")
+                .header("user-agent", getUserAgent())
+                .body(requestBody.toString());
+    }
+    
+    /**
+     * 解析响应结果
+     * @param responseBody 响应体
+     * @return JSONObject
+     */
+    protected JSONObject parseResponse(String responseBody) {
+        try {
+            return JSONUtil.parseObj(responseBody);
+        } catch (Exception e) {
+            log.error("解析响应失败: {}", responseBody, e);
+            throw new RuntimeException("解析响应失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 获取User-Agent
+     * @return User-Agent字符串
+     */
+    protected String getUserAgent() {
+        return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
+    }
+    
+    @Override
+    public boolean isRetryableError(String errorMessage) {
+        if (StrUtil.isEmpty(errorMessage)) {
+            return true;
+        }
+        
+        // 通用的不可重试错误判断 - 只要包含"会员"关键词就不重试
+        String lowerErrorMessage = errorMessage.toLowerCase();
+        
+        // 主要判断:包含"会员"关键词就认为是已经是会员,不可重试
+        if (lowerErrorMessage.contains("会员") || 
+            lowerErrorMessage.contains("member") || 
+            lowerErrorMessage.contains("premium") || 
+            lowerErrorMessage.contains("subscribed") || 
+            lowerErrorMessage.contains("订阅")) {
+            return false;
+        }
+        
+        // 其他情况都认为可以重试
+        return true;
+    }
+    
+    /**
+     * 日志前缀
+     */
+    protected String getLogPrefix() {
+        return getChannelName() + "渠道";
+    }
+}

+ 197 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/channel/impl/Ai987RechargeChannelImpl.java

@@ -0,0 +1,197 @@
+package com.cyksj.service.recharge.channel.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.cyksj.service.recharge.channel.RechargeChannel;
+import com.cyksj.service.recharge.dto.CardKeyValidationResult;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TaskSubmitResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 987ai.vip渠道实现
+ * @author chan
+ * @date 2025/8/13
+ */
+@Slf4j
+@Component("987aiRechargeChannel")
+public class Ai987RechargeChannelImpl implements RechargeChannel {
+    
+    private String baseUrl = "https://987ai.vip/api";
+    private String channelConfig;
+    
+    @Override
+    public String getChannelCode() {
+        return "987AI";
+    }
+    
+    @Override
+    public String getChannelName() {
+        return "987AI充值渠道";
+    }
+    
+    @Override
+    public void setChannelConfig(String baseUrl, String config) {
+        if (StrUtil.isNotEmpty(baseUrl)) {
+            this.baseUrl = baseUrl;
+        }
+        this.channelConfig = config;
+    }
+    
+    private HttpRequest createBaseRequest(String uri) {
+        return HttpRequest.get(baseUrl + uri)
+                .header("accept", "application/json, text/plain, */*")
+                .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
+                .header("origin", "https://987ai.vip")
+                .header("referer", "https://987ai.vip/")
+                .header("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
+    }
+    
+    private HttpRequest createPostRequest(String uri, JSONObject requestBody) {
+        return HttpRequest.post(baseUrl + uri)
+                .header("accept", "application/json, text/plain, */*")
+                .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
+                .header("content-type", "application/json")
+                .header("origin", "https://987ai.vip")
+                .header("referer", "https://987ai.vip/")
+                .header("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+                .body(requestBody.toString());
+    }
+    
+    @Override
+    public CardKeyValidationResult validateCardKey(String cardKey) {
+        log.info("987AI渠道开始验证卡密: {}", cardKey);
+        try {
+            HttpResponse response = createBaseRequest("/card-keys/" + cardKey)
+                    .execute();
+            String body = response.body();
+            log.info("987AI渠道卡密验证响应: {}", body);
+            
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            CardKeyValidationResult result = new CardKeyValidationResult(
+                jsonResponse.getBool("available", false)
+            );
+            log.info("987AI渠道卡密验证结果: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("987AI渠道验证卡密失败: {}", e.getMessage(), e);
+            throw new RuntimeException("验证卡密失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public TokenParseResult parseToken(String accessToken) {
+        log.info("987AI渠道开始验证凭证: {}", accessToken.substring(0, Math.min(20, accessToken.length())) + "...");
+        try {
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("access_token", accessToken);
+            
+            HttpResponse response = createPostRequest("/parse-token", requestBody)
+                    .execute();
+            String body = response.body();
+            log.info("987AI渠道凭证验证响应: {}", body);
+            
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            TokenParseResult result = new TokenParseResult(
+                jsonResponse.getStr("message"),
+                jsonResponse.getBool("success", false)
+            );
+            log.info("987AI渠道凭证验证结果: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("987AI渠道验证凭证失败: {}", e.getMessage(), e);
+            throw new RuntimeException("验证凭证失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public TaskSubmitResult submitTask(String cardKey, String accessToken, String idp) {
+        log.info("987AI渠道开始提交任务 - 卡密: {}, idp: {}", cardKey, idp);
+        try {
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("card_key", cardKey);
+            requestBody.put("access_token", accessToken);
+            if (StrUtil.isNotEmpty(idp)) {
+                requestBody.put("idp", idp);
+            }
+            
+            HttpResponse response = createPostRequest("/tasks", requestBody)
+                    .execute();
+            String body = response.body();
+            log.info("987AI渠道任务提交响应: {}", body);
+            
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            TaskSubmitResult result = new TaskSubmitResult(
+                jsonResponse.getStr("task_id"),
+                jsonResponse.getBool("success", false)
+            );
+            log.info("987AI渠道任务提交结果: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("987AI渠道提交任务失败: {}", e.getMessage(), e);
+            throw new RuntimeException("提交任务失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public TaskResult getTaskResult(String taskId) {
+        log.info("987AI渠道开始查询任务结果: {}", taskId);
+        try {
+            HttpResponse response = createBaseRequest("/tasks/" + taskId)
+                    .execute();
+            String body = response.body();
+            log.info("987AI渠道任务查询响应: {}", body);
+
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            String status = jsonResponse.getStr("status");
+            
+            // 987AI的响应格式:
+            // 成功: {"status": "completed", "result": "充值成功信息", "error": null}
+            // 失败: {"status": "failed", "result": null, "error": "失败原因"}  
+            // 不存在: {"status": "unknown", "error": "任务不存在"}
+            
+            String resultMessage = null;
+            if ("completed".equals(status)) {
+                // 成功时从result字段获取信息
+                resultMessage = jsonResponse.getStr("result");
+            } else if ("failed".equals(status) || "unknown".equals(status)) {
+                // 失败或不存在时从error字段获取错误信息
+                resultMessage = jsonResponse.getStr("error");
+            }
+            
+            TaskResult taskResult = new TaskResult(status, resultMessage);
+            log.info("987AI渠道任务查询结果: {}", taskResult);
+            return taskResult;
+        } catch (Exception e) {
+            log.error("987AI渠道获取任务结果失败: {}", e.getMessage(), e);
+            throw new RuntimeException("获取任务结果失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public boolean isRetryableError(String errorMessage) {
+        if (StrUtil.isEmpty(errorMessage)) {
+            return true;
+        }
+        
+        // 987AI渠道不可重试的错误 - 只要包含"会员"关键词就不重试
+        String lowerErrorMessage = errorMessage.toLowerCase();
+        
+        // 主要判断:包含"会员"关键词就认为是已经是会员,不可重试
+        if (lowerErrorMessage.contains("会员") || 
+            lowerErrorMessage.contains("member") || 
+            lowerErrorMessage.contains("premium") || 
+            lowerErrorMessage.contains("subscribed") || 
+            lowerErrorMessage.contains("订阅")) {
+            return false;
+        }
+        
+        // 其他情况都认为可以重试
+        return true;
+    }
+}

+ 188 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/channel/impl/Ow520RechargeChannelImpl.java

@@ -0,0 +1,188 @@
+package com.cyksj.service.recharge.channel.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.cyksj.service.recharge.channel.RechargeChannel;
+import com.cyksj.service.recharge.dto.CardKeyValidationResult;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TaskSubmitResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * OW520渠道实现
+ * @author chan
+ * @date 2025/8/13
+ */
+@Slf4j
+@Component("ow520RechargeChannel")
+public class Ow520RechargeChannelImpl implements RechargeChannel {
+    
+    private String baseUrl = "https://api.ow520.com/api";
+    private String channelConfig;
+    
+    @Override
+    public String getChannelCode() {
+        return "OW520";
+    }
+    
+    @Override
+    public String getChannelName() {
+        return "OW520渠道";
+    }
+    
+    @Override
+    public void setChannelConfig(String baseUrl, String config) {
+        if (StrUtil.isNotEmpty(baseUrl)) {
+            this.baseUrl = baseUrl;
+        }
+        this.channelConfig = config;
+    }
+    
+    private HttpRequest createBaseRequest(String uri) {
+        return HttpRequest.get(baseUrl + uri)
+                .header("accept", "application/json, text/plain, */*")
+                .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
+                .header("origin", "https://www.ow520.com")
+                .header("referer", "https://www.ow520.com/")
+                .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36");
+    }
+    
+    @Override
+    public CardKeyValidationResult validateCardKey(String cardKey) {
+        log.info("OW520渠道开始验证卡密: {}", cardKey);
+        try {
+            HttpResponse response = createBaseRequest("/card-keys/" + cardKey)
+                    .execute();
+            String body = response.body();
+            log.info("OW520渠道卡密验证响应: {}", body);
+            
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            CardKeyValidationResult result = new CardKeyValidationResult(
+                jsonResponse.getBool("available")
+            );
+            log.info("OW520渠道卡密验证结果: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("OW520渠道验证卡密失败: {}", e.getMessage(), e);
+            throw new RuntimeException("验证卡密失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public TokenParseResult parseToken(String accessToken) {
+        log.info("OW520渠道开始验证凭证: {}", accessToken.substring(0, Math.min(20, accessToken.length())) + "...");
+        try {
+            String url = baseUrl + "/parse-token";
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("access_token", accessToken);
+            
+            HttpResponse response = HttpRequest.post(url)
+                    .header("accept", "application/json, text/plain, */*")
+                    .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
+                    .header("content-type", "application/json")
+                    .header("origin", "https://www.ow520.com")
+                    .header("referer", "https://www.ow520.com/")
+                    .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
+                    .body(requestBody.toString())
+                    .execute();
+            String body = response.body();
+            log.info("OW520渠道凭证验证响应: {}", body);
+            
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            TokenParseResult result = new TokenParseResult(
+                jsonResponse.getStr("message"),
+                jsonResponse.getBool("success")
+            );
+            log.info("OW520渠道凭证验证结果: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("OW520渠道验证凭证失败: {}", e.getMessage(), e);
+            throw new RuntimeException("验证凭证失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public TaskSubmitResult submitTask(String cardKey, String accessToken, String idp) {
+        log.info("OW520渠道开始提交任务 - 卡密: {}, idp: {}", cardKey, idp);
+        try {
+            String url = baseUrl + "/tasks";
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("card_key", cardKey);
+            requestBody.put("access_token", accessToken);
+            requestBody.put("idp", idp);
+            
+            HttpResponse response = HttpRequest.post(url)
+                    .header("accept", "application/json, text/plain, */*")
+                    .header("accept-language", "zh-CN,zh;q=0.9,en;q=0.8")
+                    .header("content-type", "application/json")
+                    .header("origin", "https://www.ow520.com")
+                    .header("referer", "https://www.ow520.com/")
+                    .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
+                    .body(requestBody.toString())
+                    .execute();
+            String body = response.body();
+            log.info("OW520渠道任务提交响应: {}", body);
+            
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            TaskSubmitResult result = new TaskSubmitResult(
+                jsonResponse.getStr("task_id"),
+                jsonResponse.getBool("success")
+            );
+            log.info("OW520渠道任务提交结果: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("OW520渠道提交任务失败: {}", e.getMessage(), e);
+            throw new RuntimeException("提交任务失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public TaskResult getTaskResult(String taskId) {
+        log.info("OW520渠道开始查询任务结果: {}", taskId);
+        try {
+            String uri = "/tasks/" + taskId;
+            HttpResponse response = createBaseRequest(uri)
+                    .execute();
+            String body = response.body();
+            log.info("OW520渠道任务查询响应: {}", body);
+
+            JSONObject jsonResponse = JSONUtil.parseObj(body);
+            TaskResult result = new TaskResult(
+                jsonResponse.getStr("status"),
+                jsonResponse.getStr("result")
+            );
+            log.info("OW520渠道任务查询结果: {}", result);
+            return result;
+        } catch (Exception e) {
+            log.error("OW520渠道获取任务结果失败: {}", e.getMessage(), e);
+            throw new RuntimeException("获取任务结果失败: " + e.getMessage(), e);
+        }
+    }
+    
+    @Override
+    public boolean isRetryableError(String errorMessage) {
+        if (StrUtil.isEmpty(errorMessage)) {
+            return true;
+        }
+        
+        // OW520渠道不可重试的错误 - 只要包含"会员"关键词就不重试
+        String lowerErrorMessage = errorMessage.toLowerCase();
+        
+        // 主要判断:包含"会员"关键词就认为是已经是会员,不可重试
+        if (lowerErrorMessage.contains("会员") || 
+            lowerErrorMessage.contains("member") || 
+            lowerErrorMessage.contains("premium") || 
+            lowerErrorMessage.contains("subscribed") || 
+            lowerErrorMessage.contains("订阅")) {
+            return false;
+        }
+        
+        // 其他情况都认为可以重试
+        return true;
+    }
+}

+ 1 - 1
netflix-service/src/main/java/com/cyksj/server/recharge/dto/CardKeyValidationResult.java → netflix-service/src/main/java/com/cyksj/service/recharge/dto/CardKeyValidationResult.java

@@ -1,4 +1,4 @@
-package com.cyksj.server.recharge.dto;
+package com.cyksj.service.recharge.dto;
 
 /**
  * 卡密验证结果

+ 1 - 1
netflix-service/src/main/java/com/cyksj/server/recharge/dto/TaskResult.java → netflix-service/src/main/java/com/cyksj/service/recharge/dto/TaskResult.java

@@ -1,4 +1,4 @@
-package com.cyksj.server.recharge.dto;
+package com.cyksj.service.recharge.dto;
 
 /**
  * 任务查询结果

+ 1 - 1
netflix-service/src/main/java/com/cyksj/server/recharge/dto/TaskSubmitResult.java → netflix-service/src/main/java/com/cyksj/service/recharge/dto/TaskSubmitResult.java

@@ -1,4 +1,4 @@
-package com.cyksj.server.recharge.dto;
+package com.cyksj.service.recharge.dto;
 
 /**
  * 任务提交结果

+ 1 - 1
netflix-service/src/main/java/com/cyksj/server/recharge/dto/TokenParseResult.java → netflix-service/src/main/java/com/cyksj/service/recharge/dto/TokenParseResult.java

@@ -1,4 +1,4 @@
-package com.cyksj.server.recharge.dto;
+package com.cyksj.service.recharge.dto;
 
 /**
  * 凭证解析结果

+ 169 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/executor/MultiChannelRechargeExecutor.java

@@ -0,0 +1,169 @@
+package com.cyksj.service.recharge.executor;
+
+import com.cyksj.model.entity.GptRechargeCardKey;
+import com.cyksj.model.entity.GptRechargeChannel;
+import com.cyksj.service.recharge.ChannelCardKeyManager;
+import com.cyksj.service.recharge.DatabaseChannelManager;
+import com.cyksj.service.recharge.InternalTaskManager;
+import com.cyksj.service.recharge.channel.RechargeChannel;
+import com.cyksj.service.recharge.dto.CardKeyValidationResult;
+import com.cyksj.service.recharge.dto.TaskSubmitResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 多渠道充值执行器
+ * @author chan
+ * @date 2025/8/13
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MultiChannelRechargeExecutor {
+    
+    private final DatabaseChannelManager channelManager;
+    private final ChannelCardKeyManager cardKeyManager;
+    private final InternalTaskManager taskManager;
+    
+    /**
+     * 执行多渠道充值任务
+     */
+    public void executeRechargeTask(String internalTaskId, String accessToken, Long orderId, String orderNo) {
+        log.info("开始执行多渠道充值任务: {}", internalTaskId);
+        
+        boolean taskCompleted = false;
+        
+        while (!taskCompleted) {
+            GptRechargeChannel channel = channelManager.getNextAvailableChannel(internalTaskId);
+            
+            if (channel == null) {
+                // 所有渠道都尝试过了,任务失败
+                log.warn("所有渠道都尝试失败,任务ID: {}", internalTaskId);
+                taskManager.updateTaskStatus(internalTaskId, "failed", null, "所有渠道均充值失败");
+                taskCompleted = true;
+                continue;
+            }
+            
+            log.info("尝试使用渠道: {} 执行充值任务: {}", channel.getChannelName(), internalTaskId);
+            taskManager.incrementRetryCount(internalTaskId);
+            
+            try {
+                // 检查渠道是否有可用卡密
+                if (!cardKeyManager.hasAvailableCardKey(channel.getChannelCode())) {
+                    log.warn("渠道 {} 无可用卡密,跳过该渠道", channel.getChannelName());
+                    taskManager.logChannelExecution(internalTaskId, channel.getChannelCode(), 
+                        null, null, "failed", "该渠道无可用卡密", true, null);
+                    continue;
+                }
+                
+                // 获取渠道对应的卡密
+                GptRechargeCardKey cardKey = cardKeyManager.getAvailableCardKey(
+                    channel.getChannelCode(), orderId, orderNo);
+                
+                if (cardKey == null) {
+                    log.warn("获取渠道 {} 卡密失败,可能被其他线程抢占", channel.getChannelName());
+                    taskManager.logChannelExecution(internalTaskId, channel.getChannelCode(), 
+                        null, null, "failed", "获取卡密失败", true, null);
+                    continue;
+                }
+                
+                // 获取渠道实现并执行充值
+                String externalTaskId = executeChannelRecharge(channel, cardKey.getCardKey(), accessToken);
+                
+                if (externalTaskId != null) {
+                    // 任务提交成功
+                    log.info("渠道 {} 充值任务提交成功,外部任务ID: {}", channel.getChannelName(), externalTaskId);
+                    taskManager.updateTaskStatus(internalTaskId, "processing", channel.getChannelCode(), null);
+                    taskManager.logChannelExecution(internalTaskId, channel.getChannelCode(), 
+                        externalTaskId, cardKey.getCardKey(), "success", null, false, null);
+                    taskCompleted = true;
+                } else {
+                    // 任务提交失败,释放卡密,尝试下一个渠道
+                    log.warn("渠道 {} 任务提交失败", channel.getChannelName());
+                    cardKeyManager.releaseCardKey(cardKey.getCardKey());
+                    taskManager.logChannelExecution(internalTaskId, channel.getChannelCode(), 
+                        null, cardKey.getCardKey(), "failed", "任务提交失败", true, null);
+                }
+                
+            } catch (Exception e) {
+                // 检查是否为不可重试错误
+                boolean isRetryable = isRetryableError(e.getMessage());
+                
+                log.error("渠道 {} 执行失败: {}, 是否可重试: {}", 
+                    channel.getChannelName(), e.getMessage(), isRetryable, e);
+                
+                taskManager.logChannelExecution(internalTaskId, channel.getChannelCode(), 
+                    null, null, "failed", e.getMessage(), isRetryable, null);
+                
+                if (!isRetryable) {
+                    // 不可重试错误,直接失败
+                    taskManager.updateTaskStatus(internalTaskId, "failed", channel.getChannelCode(), e.getMessage());
+                    taskCompleted = true;
+                }
+                // 如果是可重试错误,继续尝试下一个渠道
+            }
+        }
+    }
+    
+    /**
+     * 执行具体渠道的充值逻辑
+     */
+    private String executeChannelRecharge(GptRechargeChannel channelConfig, String cardKey, String accessToken) {
+        try {
+            RechargeChannel channel = channelManager.getChannelImplementation(channelConfig.getChannelCode());
+
+            // 1. 验证卡密
+            log.info("验证渠道 {} 卡密", channelConfig.getChannelName());
+            CardKeyValidationResult validation = channel.validateCardKey(cardKey);
+            if (!validation.getAvailable()) {
+                throw new RuntimeException("卡密不可用");
+            }
+            
+            // 2. 验证token
+            log.info("验证渠道 {} 访问令牌", channelConfig.getChannelName());
+            TokenParseResult tokenResult = channel.parseToken(accessToken);
+            if (!tokenResult.getSuccess()) {
+                throw new RuntimeException("访问令牌无效: " + tokenResult.getMessage());
+            }
+            
+            // 3. 提交充值任务
+            log.info("提交渠道 {} 充值任务", channelConfig.getChannelName());
+            TaskSubmitResult submitResult = channel.submitTask(cardKey, accessToken, "auth0");
+            if (!submitResult.getSuccess()) {
+                throw new RuntimeException("任务提交失败");
+            }
+            
+            return submitResult.getTaskId();
+            
+        } catch (Exception e) {
+            log.error("执行渠道充值失败: {}", e.getMessage(), e);
+            throw e;
+        }
+    }
+    
+    /**
+     * 检查是否为不可重试错误
+     */
+    private boolean isRetryableError(String errorMessage) {
+        if (errorMessage == null) {
+            return true;
+        }
+        
+        // 统一的不可重试错误判断 - 只要包含"会员"关键词就不重试
+        String lowerErrorMessage = errorMessage.toLowerCase();
+        
+        // 主要判断:包含"会员"关键词就认为是已经是会员,不可重试
+        if (lowerErrorMessage.contains("会员") || 
+            lowerErrorMessage.contains("member") || 
+            lowerErrorMessage.contains("premium") || 
+            lowerErrorMessage.contains("subscribed") || 
+            lowerErrorMessage.contains("订阅")) {
+            return false;
+        }
+        
+        // 其他情况都认为可以重试
+        return true;
+    }
+}

+ 429 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/impl/GptProxyRechargeServiceImpl.java

@@ -0,0 +1,429 @@
+package com.cyksj.service.recharge.impl;
+
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.constant.Constant;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
+import com.cyksj.mapper.*;
+import com.cyksj.model.entity.*;
+import com.cyksj.model.request.GroupsRelationRechargeReq;
+import com.cyksj.redis.RedisService;
+import com.cyksj.service.chatgpt.GptUserRechargeRecordService;
+import com.cyksj.service.recharge.DatabaseChannelManager;
+import com.cyksj.service.recharge.GptProxyRechargeService;
+import com.cyksj.service.recharge.InternalTaskManager;
+import com.cyksj.service.recharge.channel.RechargeChannel;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
+import com.cyksj.service.recharge.executor.MultiChannelRechargeExecutor;
+import com.cyksj.service.user.UserBindRelationService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author chan
+ * @date 2025/7/30 18:55
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class GptProxyRechargeServiceImpl implements GptProxyRechargeService {
+
+    private final GptRechargeCardKeyMapper gptRechargeCardKeyMapper;
+    private final GroupsRelationMapper relationMapper;
+    private final UserBindRelationService userBindRelationService;
+    private final GroupsMapper groupsMapper;
+    private final GoodsDonSkuMapper skuMapper;
+    private final OrderDonMapper orderDonMapper;
+    private final GptUserRechargeRecordService gptUserRechargeRecordService;
+    private final RedisService redisService;
+    
+    // 新增的多渠道管理组件
+    private final DatabaseChannelManager channelManager;
+    private final InternalTaskManager taskManager;
+    private final MultiChannelRechargeExecutor rechargeExecutor;
+    
+    private static final GlobalThreadPoolTaskExecutor TASK_EXECUTOR = GlobalThreadPoolTaskExecutor.getInstance();
+
+    /**
+     * 解析Token(现在委托给渠道实现)
+     */
+    public TokenParseResult parseToken(String accessToken) {
+        log.info("解析访问令牌");
+        try {
+            // 使用OW520渠道解析token
+            RechargeChannel channel = channelManager.getChannelImplementation("OW520");
+            return channel.parseToken(accessToken);
+        } catch (Exception e) {
+            log.error("解析访问令牌失败: {}", e.getMessage(), e);
+            throw new RuntimeException("解析访问令牌失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 获取任务结果(支持内部任务ID)
+     */
+    public TaskResult getTaskResult(String taskId) {
+        log.info("查询任务结果: {}", taskId);
+        
+        // 检查是否为内部任务ID
+        if (taskId.startsWith("IT")) {
+            return getInternalTaskResult(taskId);
+        } else {
+            // 兼容旧的外部任务ID查询
+            return getExternalTaskResult(taskId);
+        }
+    }
+    
+    /**
+     * 获取内部任务结果
+     */
+    private TaskResult getInternalTaskResult(String internalTaskId) {
+        GptInternalTask internalTask = taskManager.getInternalTask(internalTaskId);
+        if (internalTask == null) {
+            throw new RuntimeException("任务不存在");
+        }
+        
+        // 如果任务正在处理中,查询最新的外部任务状态
+        if ("processing".equals(internalTask.getTaskStatus())) {
+            GptTaskExecutionLog lastLog = taskManager.getLastExecutionLog(internalTaskId);
+            if (lastLog != null && lastLog.getExternalTaskId() != null) {
+                try {
+                    // 获取渠道实现并查询外部任务状态
+                    RechargeChannel channel = channelManager.getChannelImplementation(lastLog.getChannelCode());
+                    TaskResult externalResult = channel.getTaskResult(lastLog.getExternalTaskId());
+                    
+                    // 更新内部任务状态
+                    updateInternalTaskFromExternal(internalTaskId, externalResult, internalTask.getRelationId());
+                    return externalResult;
+                } catch (Exception e) {
+                    log.error("查询外部任务状态失败: {}", e.getMessage(), e);
+                }
+            }
+        }
+        
+        // 返回内部任务状态
+        return new TaskResult(internalTask.getTaskStatus(), internalTask.getFinalResult());
+    }
+    
+    /**
+     * 获取外部任务结果(兼容旧接口)
+     */
+    private TaskResult getExternalTaskResult(String externalTaskId) {
+        try {
+            // 默认使用OW520渠道查询(向后兼容)
+            RechargeChannel channel = channelManager.getChannelImplementation("OW520");
+            TaskResult result = channel.getTaskResult(externalTaskId);
+            
+            // 更新关联的代充状态
+            resetGroupsRelationRechargeStatus(externalTaskId, result.getStatus(), result.getResult());
+            return result;
+        } catch (Exception e) {
+            log.error("查询外部任务结果失败: {}", e.getMessage(), e);
+            throw new RuntimeException("查询任务结果失败: " + e.getMessage(), e);
+        }
+    }
+
+	@Override
+	@Transactional(rollbackFor = Throwable.class)
+	public String confirmRecharge(GroupsRelationRechargeReq req) {
+		Long relationId = req.getRelationId();
+		Long userId = req.getUserId();
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class)
+				.in(GroupsRelation::getUserId, userIdList)
+				.eq(GroupsRelation::getId, relationId)
+				.eq(GroupsRelation::getStatus, GroupsRelation.Status.validity));
+		if (relation == null) {
+			throw BusinessRuntimeException.getInstance("车票不存在");
+		}
+		GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+		GoodsDonSku sku = skuMapper.selectById(groupsTrips.getSkuId());
+		
+		Integer rechargeRemainNum = relation.getRechargeRemainNum();
+		if (rechargeRemainNum == null || rechargeRemainNum <= 0) {
+			throw BusinessRuntimeException.getInstance("剩余可代充次数为0");
+		}
+		if (relation.getRechargeStatus() == GroupsRelation.RechargeStatus.recharging) {
+			throw BusinessRuntimeException.getInstance("确认充值中");
+		}
+		if (relation.getRechargeStatus() == GroupsRelation.RechargeStatus.recharge_error) {
+			throw BusinessRuntimeException.getInstance("充值失败,请联系客服开通");
+		}
+		
+		String key = RedisService.key.GPT_RECHARGE_KEY.getName() + relationId;
+		boolean b = redisService.setNx(key, relationId, RedisService.key.GPT_RECHARGE_KEY.getTimeout());
+		if (!b) {
+			throw BusinessRuntimeException.getInstance("确认充值中");
+		}
+		
+		try {
+			// 扣除剩余次数
+			int updateRemainNum = relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+					.set(GroupsRelation::getRechargeRemainNum, rechargeRemainNum - 1)
+					.eq(GroupsRelation::getId, relationId)
+					.eq(GroupsRelation::getUserId, relation.getUserId())
+					.eq(GroupsRelation::getRechargeRemainNum, rechargeRemainNum));
+			if (updateRemainNum == 0) {
+				throw BusinessRuntimeException.getInstance("代充异常,请重试");
+			}
+			
+			// 获取订单信息
+			OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
+					.in(OrderDon::getUserId, userIdList)
+					.eq(OrderDon::getRelationId, relationId)
+					.notIn(OrderDon::getStatus, Constant.noOrderAllStatus)
+					.orderByDesc(OrderDon::getId)
+					.last("limit 1"));
+			if (orderDon == null) {
+				throw BusinessRuntimeException.getInstance("代充订单不存在或已退款");
+			}
+			
+			String accessToken = req.getAccessToken();
+			// 验证用户token
+			TokenParseResult tokenParseResult = parseToken(accessToken);
+			if (!tokenParseResult.getSuccess()) {
+				throw BusinessRuntimeException.getInstance("您输入的凭证有误,请重新输入");
+			}
+			String rechargeAccount = tokenParseResult.getMessage();
+			
+			// *** 核心变更:创建内部任务ID,异步执行多渠道充值 ***
+			String internalTaskId = taskManager.createInternalTask(relationId, userId, accessToken, rechargeAccount);
+			
+			// 更新关系表记录内部任务ID
+			relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+					.set(GroupsRelation::getAccount, rechargeAccount)
+					.set(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharging)
+					.set(GroupsRelation::getGptToken, accessToken)
+					.set(GroupsRelation::getInternalTaskId, internalTaskId)
+					.eq(GroupsRelation::getId, relationId));
+			
+			// 异步执行多渠道充值任务
+			TASK_EXECUTOR.execute(() -> {
+				try {
+					rechargeExecutor.executeRechargeTask(internalTaskId, accessToken, orderDon.getId(), orderDon.getOrderNo());
+				} catch (Exception e) {
+					log.error("多渠道充值执行失败: {}", e.getMessage(), e);
+					taskManager.updateTaskStatus(internalTaskId, "failed", null, "系统异常: " + e.getMessage());
+				}
+			});
+			
+			// 返回内部任务ID给用户
+			log.info("充值任务提交成功,内部任务ID: {}", internalTaskId);
+			return internalTaskId;
+		} finally {
+			if (redisService.hasKey(key)) {
+				redisService.del(key);
+			}
+		}
+	}
+
+	/**
+	 * 设置代充自动充值异常
+	 */
+	public void setRechargeRelationStatusAutoError(GroupsRelation relation) {
+		TASK_EXECUTOR.execute(()->{
+			relation.setRechargeStatus(GroupsRelation.RechargeStatus.recharge_error);
+			relationMapper.updateById(relation);
+		});
+	}
+	
+	/**
+	 * 从外部任务结果更新内部任务状态
+	 */
+	private void updateInternalTaskFromExternal(String internalTaskId, TaskResult externalResult, Long relationId) {
+		String status = externalResult.getStatus();
+		String result = externalResult.getResult();
+		
+		if ("completed".equals(status)) {
+			taskManager.updateTaskStatus(internalTaskId, "completed", null, "充值成功");
+		} else if ("failed".equals(status) || "unknown".equals(status)) {
+			taskManager.updateTaskStatus(internalTaskId, "failed", null, result);
+		}
+		// processing状态不更新,继续等待
+		
+		// 更新关联的GroupsRelation状态
+		resetGroupsRelationRechargeStatusByInternalTask(internalTaskId, status, result);
+	}
+	
+	/**
+	 * 通过内部任务ID更新代充状态
+	 */
+	private void resetGroupsRelationRechargeStatusByInternalTask(String internalTaskId, String status, String result) {
+		if ("completed".equals(status) || "failed".equals(status) || "unknown".equals(status)) {
+			GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class)
+					.eq(GroupsRelation::getInternalTaskId, internalTaskId)
+					.last("limit 1"));
+			if (relation != null && relation.getRechargeStatus() == GroupsRelation.RechargeStatus.recharging) {
+				processRechargeResult(relation, status, result);
+			}
+		}
+	}
+	
+	/**
+	 * 处理充值结果
+	 */
+	private void processRechargeResult(GroupsRelation relation, String status, String result) {
+		Integer rechargeRemainNum = relation.getRechargeRemainNum();
+		GroupsRelation.RechargeStatus rechargeStatus = "completed".equals(status) ? 
+			GroupsRelation.RechargeStatus.complete : GroupsRelation.RechargeStatus.recharge_error;
+		
+		// 获取使用的卡密信息(从最后一次执行日志中获取)
+		GptTaskExecutionLog lastLog = taskManager.getLastExecutionLog(relation.getInternalTaskId());
+		String cardKey = lastLog != null ? lastLog.getCardKey() : null;
+		
+		GptRechargeCardKey gptRechargeCardKey = null;
+		if (StrUtil.isNotEmpty(cardKey)) {
+			gptRechargeCardKey = gptRechargeCardKeyMapper.selectOne(Wrappers.lambdaQuery(GptRechargeCardKey.class)
+					.eq(GptRechargeCardKey::getCardKey, cardKey)
+					.last("limit 1"));
+		}
+		
+		// 如果充值失败,释放卡密
+		if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
+			if (gptRechargeCardKey != null) {
+				gptRechargeCardKeyMapper.update(null, Wrappers.lambdaUpdate(GptRechargeCardKey.class)
+						.set(GptRechargeCardKey::getStatus, Boolean.FALSE)
+						.set(GptRechargeCardKey::getOrderId, null)
+						.set(GptRechargeCardKey::getOrderNo, null)
+						.eq(GptRechargeCardKey::getId, gptRechargeCardKey.getId())
+						.eq(GptRechargeCardKey::getStatus, Boolean.TRUE));
+			}
+			
+			// 防重复处理
+			if (!redisService.setNx(RedisService.key.GPT_RECHARGE_RECOVER_KEY.getName() + relation.getInternalTaskId(), 
+					relation.getInternalTaskId(), RedisService.key.GPT_RECHARGE_RECOVER_KEY.getTimeout())) {
+				return;
+			}
+		}
+		
+		// 处理时间相关逻辑
+		Date startTime = relation.getStartTime();
+		DateTime expiryTime = null;
+		if (rechargeStatus == GroupsRelation.RechargeStatus.complete && startTime == null) {
+			GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+			if (groupsTrips != null) {
+				startTime = DateTime.now();
+				GoodsDonSku sku = skuMapper.selectById(groupsTrips.getSkuId());
+				if (sku.getDays() != null && sku.getDays() > 0) {
+					expiryTime = DateUtil.offsetDay(startTime, sku.getDays());
+				} else {
+					expiryTime = DateUtil.offsetMonth(startTime, sku.getMonths());
+				}
+			}
+		}
+		
+		String rechargeErrorInfo = null;
+		if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
+			rechargeErrorInfo = result;
+		}
+		
+		// 更新关系表状态
+		relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+				.set(rechargeStatus == GroupsRelation.RechargeStatus.recharge_error, GroupsRelation::getRechargeRemainNum, rechargeRemainNum + 1)
+				.set(GroupsRelation::getRechargeStatus, rechargeStatus)
+				.set(expiryTime != null, GroupsRelation::getStartTime, startTime)
+				.set(expiryTime != null, GroupsRelation::getExpiryTime, expiryTime)
+				.set(StrUtil.isNotEmpty(rechargeErrorInfo), GroupsRelation::getRechargeErrorInfo, rechargeErrorInfo)
+				.eq(GroupsRelation::getId, relation.getId())
+				.le(GroupsRelation::getRechargeRemainNum, relation.getRechargeNum())
+				.eq(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharging));
+				
+		// 记录代充记录
+		if (rechargeStatus == GroupsRelation.RechargeStatus.complete) {
+			gptUserRechargeRecordService.recordGptUserRechargeNum(relation.getUserId(), relation.getId(), relation.getAccount(), -1, null, "多渠道自动代充");
+			
+			// 修改对应订单状态
+			if (gptRechargeCardKey != null) {
+				Long orderId = gptRechargeCardKey.getOrderId();
+				OrderDon orderDon = orderDonMapper.selectById(orderId);
+				if (orderDon != null && orderDon.getStatus() == OrderDon.Status.hasPayment) {
+					orderDon.setStatus(OrderDon.Status.complete);
+					orderDonMapper.updateById(orderDon);
+				}
+			}
+		}
+	}
+
+	/**
+	 * 更新代充状态
+	 */
+	private void resetGroupsRelationRechargeStatus(String taskId, String status, String result) {
+		if ("completed".equals(status) || "failed".equals(status) || "unknown".equals(status)) {
+			GroupsRelation relation = relationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class)
+					.eq(GroupsRelation::getGptTaskId, taskId)
+					.last("limit 1"));
+			if (relation != null && relation.getRechargeStatus() == GroupsRelation.RechargeStatus.recharging) {
+				Integer rechargeRemainNum = relation.getRechargeRemainNum();
+				GroupsRelation.RechargeStatus rechargeStatus = "completed".equals(status) ? GroupsRelation.RechargeStatus.complete : GroupsRelation.RechargeStatus.recharge_error;
+				String cardKey = relation.getCardKey();
+				GptRechargeCardKey gptRechargeCardKey = gptRechargeCardKeyMapper.selectOne(Wrappers.lambdaQuery(GptRechargeCardKey.class)
+						.eq(GptRechargeCardKey::getCardKey, cardKey)
+						.last("limit 1"));
+				if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
+					if (gptRechargeCardKey != null) {
+						gptRechargeCardKeyMapper.update(null, Wrappers.lambdaUpdate(GptRechargeCardKey.class)
+								.set(GptRechargeCardKey::getStatus, Boolean.FALSE)
+								.set(GptRechargeCardKey::getOrderId, null)
+								.set(GptRechargeCardKey::getOrderNo, null)
+								.eq(GptRechargeCardKey::getId, gptRechargeCardKey.getId())
+								.eq(GptRechargeCardKey::getStatus, Boolean.TRUE));
+					}
+				}
+				if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
+					if (!redisService.setNx(RedisService.key.GPT_RECHARGE_RECOVER_KEY.getName() + taskId, taskId, RedisService.key.GPT_RECHARGE_RECOVER_KEY.getTimeout())) {
+						return;
+					}
+				}
+				Date startTime = relation.getStartTime();
+				DateTime expiryTime = null;
+				if (rechargeStatus == GroupsRelation.RechargeStatus.complete && startTime == null) {
+					GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+					if (groupsTrips != null) {
+						startTime = DateTime.now();
+						GoodsDonSku sku = skuMapper.selectById(groupsTrips.getSkuId());
+						if (sku.getDays() != null && sku.getDays() > 0) {
+							expiryTime = DateUtil.offsetDay(startTime, sku.getDays());
+						} else {
+							expiryTime = DateUtil.offsetMonth(startTime, sku.getMonths());
+						}
+					}
+				}
+				String rechargeErrorInfo = null;
+				if (rechargeStatus == GroupsRelation.RechargeStatus.recharge_error) {
+					rechargeErrorInfo = result;
+				}
+				relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+						.set(rechargeStatus == GroupsRelation.RechargeStatus.recharge_error, GroupsRelation::getRechargeRemainNum, rechargeRemainNum + 1)
+						.set(GroupsRelation::getRechargeStatus, rechargeStatus)
+						.set(expiryTime != null, GroupsRelation::getStartTime, startTime)
+						.set(expiryTime != null, GroupsRelation::getExpiryTime, expiryTime)
+						.set(StrUtil.isNotEmpty(rechargeErrorInfo), GroupsRelation::getRechargeErrorInfo, rechargeErrorInfo)
+						.eq(GroupsRelation::getId, relation.getId())
+						.le(GroupsRelation::getRechargeRemainNum, relation.getRechargeNum())
+						.eq(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharging));
+				//记录代充记录
+				if (rechargeStatus == GroupsRelation.RechargeStatus.complete) {
+					gptUserRechargeRecordService.recordGptUserRechargeNum(relation.getUserId(), relation.getId(), relation.getAccount(), -1, null, "自动代充");
+					//修改对应订单状态
+					if (gptRechargeCardKey != null) {
+						Long orderId = gptRechargeCardKey.getOrderId();
+						OrderDon orderDon = orderDonMapper.selectById(orderId);
+						if (orderDon != null && orderDon.getStatus() == OrderDon.Status.hasPayment) {
+							orderDon.setStatus(OrderDon.Status.complete);
+							orderDonMapper.updateById(orderDon);
+						}
+					}
+				}
+			}
+		}
+	}
+}

+ 3 - 3
netflix-web/src/main/java/com/cyksj/web/controller/recharge/GptRechargeController.java

@@ -3,9 +3,9 @@ package com.cyksj.web.controller.recharge;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.GatewayResponse;
 import com.cyksj.model.request.GroupsRelationRechargeReq;
-import com.cyksj.server.recharge.GptProxyRechargeService;
-import com.cyksj.server.recharge.dto.TaskResult;
-import com.cyksj.server.recharge.dto.TokenParseResult;
+import com.cyksj.service.recharge.GptProxyRechargeService;
+import com.cyksj.service.recharge.dto.TaskResult;
+import com.cyksj.service.recharge.dto.TokenParseResult;
 import com.cyksj.web.controller.recharge.req.ParseTokenRequest;
 import com.cyksj.web.util.StpUserUtil;
 import lombok.RequiredArgsConstructor;