Kaynağa Gözat

Merge remote-tracking branch 'ywf-yhlxj/master'

zoujiajian 2 gün önce
ebeveyn
işleme
210039f77b

+ 5 - 0
netflix-common/src/main/java/com/cyksj/common/constant/Constant.java

@@ -869,6 +869,11 @@ public interface Constant {
      */
     Long REDEEM_CLOUD_CHANNEL_ID = 17L;
 
+    /**
+     * auto17
+     */
+    Long AUTO17_CHANNEL_ID = 18L;
+
     /**
 	 * Adobe 全家桶
 	 */

+ 43 - 0
netflix-dao/src/main/java/com/cyksj/model/response/recharge/Auto17CardKeyCheckResponse.java

@@ -0,0 +1,43 @@
+package com.cyksj.model.response.recharge;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class Auto17CardKeyCheckResponse {
+
+    private Integer code;
+
+    private String message;
+
+    private CardKeyData data;
+
+    public boolean isSuccess() {
+        return code != null && code == 0;
+    }
+
+    @Getter
+    @Setter
+    public static class CardKeyData {
+
+        private Boolean exists;
+
+        @JsonProperty("is_used")
+        private Boolean used;
+
+        private String status;
+
+        @JsonProperty("used_at")
+        private String usedAt;
+
+        private String type;
+
+        public boolean isAvailable() {
+            return Boolean.TRUE.equals(exists)
+                    && Boolean.FALSE.equals(used)
+                    && "unused".equalsIgnoreCase(status);
+        }
+    }
+}

+ 45 - 0
netflix-dao/src/main/java/com/cyksj/model/response/recharge/Auto17OrderCreateResponse.java

@@ -0,0 +1,45 @@
+package com.cyksj.model.response.recharge;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Getter
+@Setter
+public class Auto17OrderCreateResponse {
+
+    private Integer code;
+
+    private String message;
+
+    private OrderData data;
+
+    public boolean isSuccess() {
+        return code != null && code == 0;
+    }
+
+    @Getter
+    @Setter
+    public static class OrderData {
+
+        @JsonProperty("pay_link")
+        private String payLink;
+
+        @JsonProperty("order_no")
+        private String orderNo;
+
+        private String status;
+
+        private String email;
+
+        @JsonProperty("payment_amount")
+        private BigDecimal paymentAmount;
+
+        @JsonProperty("payment_currency")
+        private String paymentCurrency;
+
+        private String message;
+    }
+}

+ 90 - 0
netflix-dao/src/main/java/com/cyksj/model/response/recharge/Auto17OrderStatusResponse.java

@@ -0,0 +1,90 @@
+package com.cyksj.model.response.recharge;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+@Getter
+@Setter
+public class Auto17OrderStatusResponse {
+
+    private Integer code;
+
+    private String message;
+
+    private OrderData data;
+
+    public boolean isSuccess() {
+        return code != null && code == 0;
+    }
+
+    @Getter
+    @Setter
+    public static class OrderData {
+
+        @JsonProperty("pay_link")
+        private String payLink;
+
+        @JsonProperty("order_no")
+        private String orderNo;
+
+        @JsonProperty("source_ip")
+        private String sourceIp;
+
+        @JsonProperty("order_type")
+        private String orderType;
+
+        @JsonProperty("plan_type")
+        private String planType;
+
+        @JsonProperty("card_key")
+        private String cardKey;
+
+        private String email;
+
+        @JsonProperty("payment_amount")
+        private BigDecimal paymentAmount;
+
+        @JsonProperty("payment_currency")
+        private String paymentCurrency;
+
+        @JsonProperty("is_subscription_cancelled")
+        private Integer subscriptionCancelled;
+
+        @JsonProperty("retry_batch_no")
+        private String retryBatchNo;
+
+        @JsonProperty("retry_attempt")
+        private Integer retryAttempt;
+
+        private String status;
+
+        @JsonProperty("failure_reason")
+        private String failureReason;
+
+        @JsonProperty("payment_result")
+        private PaymentResult paymentResult;
+
+        @JsonProperty("finished_at")
+        private String finishedAt;
+
+        @JsonProperty("created_at")
+        private String createdAt;
+
+        @JsonProperty("updated_at")
+        private String updatedAt;
+    }
+
+    @Getter
+    @Setter
+    public static class PaymentResult {
+
+        private String stage;
+
+        private String message;
+
+        private Boolean success;
+    }
+}

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

@@ -85,6 +85,8 @@ public class GptProxyRechargeServiceImpl implements GptProxyRechargeService {
 
     private final RedeemCloudRechargeService redeemCloudRechargeService;
 
+    private final Auto17RechargeService auto17RechargeService;
+
     private final GptUserRechargeNumRecordMapper gptUserRechargeNumRecordMapper;
 
 	private static final GlobalThreadPoolTaskExecutor TASK_EXECUTOR = GlobalThreadPoolTaskExecutor.getInstance();
@@ -229,6 +231,13 @@ public class GptProxyRechargeServiceImpl implements GptProxyRechargeService {
             }
             if (Constant.REDEEM_CLOUD_CHANNEL_ID.equals(gptRechargeCardKey.getChannelId())) {
                 return redeemCloudRechargeService.getTaskResult(relation);
+            }
+            if (Constant.AUTO17_CHANNEL_ID.equals(gptRechargeCardKey.getChannelId())) {
+                TaskResult taskResult = auto17RechargeService.getTaskResult(relation);
+                if (taskResult != null && "failed".equals(taskResult.getStatus())) {
+                    taskResult.setError("充值失败,请联系客服开通");
+                }
+                return taskResult;
             }
 	        String BASE_URL = gptRechargeCardKeyChannelService.getCardKeyDomain(cardKey);
 	        String url = BASE_URL + "/tasks/" + taskId;
@@ -479,6 +488,10 @@ public class GptProxyRechargeServiceImpl implements GptProxyRechargeService {
             //redeemcloud
             if (Constant.REDEEM_CLOUD_CHANNEL_ID.equals(channelId)) {
                 return redeemCloudRechargeService.confirmRecharge(relation, gptCardKey, null);
+            }
+            //auto17
+            if (Constant.AUTO17_CHANNEL_ID.equals(channelId)) {
+                return auto17RechargeService.confirmRecharge(relation, gptCardKey, null);
             }
 			//校验卡密是否可用
 			CardKeyValidationResult cardKeyValidationResult = validateCardKey(gptCardKey);

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

@@ -102,6 +102,8 @@ public class GptRechargeServiceImpl implements GptRechargeService {
 
     private final RedeemCloudRechargeService redeemCloudRechargeService;
 
+    private final Auto17RechargeService auto17RechargeService;
+
 	private HttpRequest createBaseRequest(String url) {
 		return HttpRequest.get(url)
 				.header("accept", "application/json, text/plain, */*")
@@ -315,6 +317,12 @@ public class GptRechargeServiceImpl implements GptRechargeService {
                 req.setCmsUser(cmsUser);
                 relation.setCardKey(gptCardKey);
                 return redeemCloudRechargeService.confirmRecharge(relation, gptCardKey, cmsUser);
+            }
+            //auto17
+            if (Constant.AUTO17_CHANNEL_ID.equals(gptRechargeCardKey.getChannelId())) {
+                req.setCmsUser(cmsUser);
+                relation.setCardKey(gptCardKey);
+                return auto17RechargeService.confirmRecharge(relation, gptCardKey, cmsUser);
             }
 			//校验卡密是否可用
 			CardKeyValidationResult cardKeyValidationResult = validateCardKey(gptCardKey);
@@ -389,6 +397,9 @@ public class GptRechargeServiceImpl implements GptRechargeService {
             }
             if (Constant.REDEEM_CLOUD_CHANNEL_ID.equals(gptRechargeCardKey.getChannelId())) {
                 return redeemCloudRechargeService.getTaskResult(relation);
+            }
+            if (Constant.AUTO17_CHANNEL_ID.equals(gptRechargeCardKey.getChannelId())) {
+                return auto17RechargeService.getTaskResult(relation);
             }
 			String BASE_URL = gptRechargeCardKeyChannelService.getCardKeyDomain(cardKey);
 			String url = BASE_URL + "/tasks/" + taskId;

+ 15 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/Auto17RechargeService.java

@@ -0,0 +1,15 @@
+package com.cyksj.service.recharge;
+
+import com.cyksj.model.entity.CmsUser;
+import com.cyksj.model.entity.GroupsRelation;
+import com.cyksj.model.response.recharge.Auto17CardKeyCheckResponse;
+import com.cyksj.server.recharge.dto.TaskResult;
+
+public interface Auto17RechargeService {
+
+    Auto17CardKeyCheckResponse validateCardKey(String cardKey) throws Exception;
+
+    String confirmRecharge(GroupsRelation relation, String cardKey, CmsUser user);
+
+    TaskResult getTaskResult(GroupsRelation relation);
+}

+ 422 - 0
netflix-service/src/main/java/com/cyksj/service/recharge/impl/Auto17RechargeServiceImpl.java

@@ -0,0 +1,422 @@
+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 cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONArray;
+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.common.util.Jsons;
+import com.cyksj.mapper.GoodsDonSkuMapper;
+import com.cyksj.mapper.GptRechargeCardKeyMapper;
+import com.cyksj.mapper.GroupsMapper;
+import com.cyksj.mapper.GroupsRelationMapper;
+import com.cyksj.mapper.OrderDonMapper;
+import com.cyksj.model.entity.CmsUser;
+import com.cyksj.model.entity.GoodsDonSku;
+import com.cyksj.model.entity.GptRechargeCardKey;
+import com.cyksj.model.entity.GroupsRelation;
+import com.cyksj.model.entity.GroupsTrips;
+import com.cyksj.model.entity.OrderDon;
+import com.cyksj.model.response.recharge.Auto17CardKeyCheckResponse;
+import com.cyksj.model.response.recharge.Auto17OrderCreateResponse;
+import com.cyksj.model.response.recharge.Auto17OrderStatusResponse;
+import com.cyksj.redis.RedisService;
+import com.cyksj.server.recharge.dto.TaskResult;
+import com.cyksj.service.chatgpt.GptUserRechargeRecordService;
+import com.cyksj.service.recharge.Auto17RechargeService;
+import com.cyksj.service.recharge.GptRechargeCardKeyChannelService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class Auto17RechargeServiceImpl implements Auto17RechargeService {
+
+    private static final String CHECK_CARD_KEY_PATH = "/card-keys/check-usage";
+    private static final String CREATE_ORDER_PATH = "/orders";
+    private static final String ORDER_STATUS_PATH = "/orders/status/";
+
+    private static final GlobalThreadPoolTaskExecutor TASK_EXECUTOR = GlobalThreadPoolTaskExecutor.getInstance();
+
+    private final GptRechargeCardKeyChannelService gptRechargeCardKeyChannelService;
+
+    private final GroupsRelationMapper relationMapper;
+
+    private final GroupsMapper groupsMapper;
+
+    private final GoodsDonSkuMapper skuMapper;
+
+    private final RedisService redisService;
+
+    private final OrderDonMapper orderDonMapper;
+
+    private final GptRechargeCardKeyMapper gptRechargeCardKeyMapper;
+
+    private final GptUserRechargeRecordService gptUserRechargeRecordService;
+
+    @Override
+    public Auto17CardKeyCheckResponse validateCardKey(String cardKey) throws Exception {
+        JSONObject requestBody = new JSONObject();
+        requestBody.putOpt("key", cardKey);
+
+        String BASE_URL = gptRechargeCardKeyChannelService.getCardKeyDomain(cardKey);
+        String url = buildApiBaseUrl(BASE_URL) + CHECK_CARD_KEY_PATH;
+        HttpResponse response = HttpRequest.post(url)
+                .header("Content-Type", "application/json")
+                .timeout(Constant.CONNECT_MILLISECONDS)
+                .body(requestBody.toString())
+                .execute();
+        Auto17CardKeyCheckResponse result = parseResponse(response.body(), Auto17CardKeyCheckResponse.class, "卡密校验");
+        if (!response.isOk() || result == null || !result.isSuccess() || result.getData() == null) {
+            throw requestException(result != null ? result.getMessage() : null, "卡密校验失败");
+        }
+        log.info("auto17卡密校验完成, exists:{}, used:{}, status:{}",
+                result.getData().getExists(), result.getData().getUsed(), result.getData().getStatus());
+        return result;
+    }
+
+    @Override
+    public String confirmRecharge(GroupsRelation relation, String cardKey, CmsUser cmsUser) {
+        Long relationId = relation.getId();
+        try {
+            Auto17CardKeyCheckResponse cardKeyResponse = validateCardKey(cardKey);
+            if (!cardKeyResponse.getData().isAvailable()) {
+                if (cmsUser == null) {
+                    throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
+                }
+                throw BusinessRuntimeException.getInstance("卡密:{0}不可用", cardKey);
+            }
+        } catch (BusinessRuntimeException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("auto17校验卡密失败: {}", e.getMessage(), e);
+            if (cmsUser == null) {
+                throw BusinessRuntimeException.getInstance("系统繁忙,请重试");
+            }
+            throw BusinessRuntimeException.getInstance("卡密校验失败,{0}", e.getMessage());
+        }
+
+        try {
+            Auto17OrderCreateResponse createResponse = submitOrder(cardKey, relation.getGptToken());
+            String taskId = createResponse.getData().getOrderNo();
+            String operator = null;
+            if (cmsUser != null) {
+                operator = cmsUser.getNickname();
+            }
+            String password = relation.getPassword();
+
+            relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+                    .set(GroupsRelation::getAccount, relation.getAccount())
+                    .set(GroupsRelation::getOperator, operator)
+                    .set(StrUtil.isNotBlank(password), GroupsRelation::getPassword, password)
+                    .set(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharging)
+                    .set(GroupsRelation::getCardKey, cardKey)
+                    .set(GroupsRelation::getGptToken, relation.getGptToken())
+                    .set(GroupsRelation::getGptTaskId, taskId)
+                    .eq(GroupsRelation::getId, relationId));
+            relation.setRechargeStatus(GroupsRelation.RechargeStatus.recharging);
+            relation.setCardKey(cardKey);
+            relation.setGptTaskId(taskId);
+            relation.setOperator(operator);
+            return taskId;
+        } catch (Exception e) {
+            setRechargeRelationStatusAutoError(relation);
+            log.error("auto17提交充值任务失败: {}", e.getMessage(), e);
+            if (cmsUser == null) {
+                throw BusinessRuntimeException.getInstance("充值失败,请联系客服开通");
+            }
+            throw BusinessRuntimeException.getInstance("充值失败,{0}", e.getMessage());
+        }
+    }
+
+    @Override
+    public TaskResult getTaskResult(GroupsRelation relation) {
+        try {
+            Auto17OrderStatusResponse response = queryOrderStatus(relation.getCardKey(), relation.getGptTaskId());
+            TaskResult taskResult = convertTaskResult(response.getData());
+            String result = StrUtil.blankToDefault(taskResult.getError(), taskResult.getResult());
+            resetGroupsRelationRechargeStatus(relation, taskResult.getStatus(), result);
+            return taskResult;
+        } catch (Exception e) {
+            log.error("auto17查询充值任务失败: {}", e.getMessage(), e);
+            throw new RuntimeException("获取任务结果失败: " + e.getMessage(), e);
+        }
+    }
+
+    private Auto17OrderCreateResponse submitOrder(String cardKey, String rechargePayload) throws Exception {
+        JSONObject requestBody = new JSONObject();
+        requestBody.putOpt("cardKey", cardKey);
+        requestBody.putOpt("token", parseTokenPayload(rechargePayload));
+
+        String BASE_URL = gptRechargeCardKeyChannelService.getCardKeyDomain(cardKey);
+        String url = buildApiBaseUrl(BASE_URL) + CREATE_ORDER_PATH;
+        HttpResponse response = HttpRequest.post(url)
+                .header("Content-Type", "application/json")
+                .timeout(Constant.CONNECT_MILLISECONDS)
+                .body(requestBody.toString())
+                .execute();
+        Auto17OrderCreateResponse result = parseResponse(response.body(), Auto17OrderCreateResponse.class, "提交订单");
+        if (response.getStatus() < 200 || response.getStatus() >= 300 || result == null || !result.isSuccess()
+                || result.getData() == null || StrUtil.isBlank(result.getData().getOrderNo())) {
+            throw requestException(result != null ? result.getMessage() : null, "提交订单失败");
+        }
+        log.info("auto17订单提交成功, orderNo:{}, status:{}",
+                result.getData().getOrderNo(), result.getData().getStatus());
+        return result;
+    }
+
+    private Auto17OrderStatusResponse queryOrderStatus(String cardKey, String taskId) throws Exception {
+        String BASE_URL = gptRechargeCardKeyChannelService.getCardKeyDomain(cardKey);
+        String url = buildApiBaseUrl(BASE_URL) + ORDER_STATUS_PATH + encode(cardKey);
+        HttpResponse response = HttpRequest.get(url)
+                .timeout(Constant.CONNECT_MILLISECONDS)
+                .execute();
+        Auto17OrderStatusResponse result = parseResponse(response.body(), Auto17OrderStatusResponse.class, "查询订单");
+        if (!response.isOk() || result == null || !result.isSuccess() || result.getData() == null) {
+            throw requestException(result != null ? result.getMessage() : null, "查询订单失败");
+        }
+        if (StrUtil.isNotBlank(taskId) && StrUtil.isNotBlank(result.getData().getOrderNo())
+                && !StrUtil.equals(taskId, result.getData().getOrderNo())) {
+            throw BusinessRuntimeException.getInstance("订单号不匹配");
+        }
+        log.info("auto17订单状态查询完成, orderNo:{}, status:{}, retryAttempt:{}",
+                result.getData().getOrderNo(), result.getData().getStatus(), result.getData().getRetryAttempt());
+        return result;
+    }
+
+    private TaskResult convertTaskResult(Auto17OrderStatusResponse.OrderData data) {
+        TaskResult result = new TaskResult();
+        String status = data.getStatus();
+        String message = getStatusMessage(data);
+        if ("success".equalsIgnoreCase(status)) {
+            result.setStatus("completed");
+            result.setResult(message);
+        } else if ("failed".equalsIgnoreCase(status)) {
+            result.setStatus("failed");
+            result.setError(message);
+        } else {
+            result.setStatus("recharging");
+            result.setResult(StrUtil.blankToDefault(message, status));
+        }
+        return result;
+    }
+
+    private String getStatusMessage(Auto17OrderStatusResponse.OrderData data) {
+        if (StrUtil.isNotBlank(data.getFailureReason())) {
+            return data.getFailureReason();
+        }
+        if (data.getPaymentResult() != null && StrUtil.isNotBlank(data.getPaymentResult().getMessage())) {
+            return data.getPaymentResult().getMessage();
+        }
+        return data.getStatus();
+    }
+
+    private JSONObject parseTokenPayload(String rechargePayload) throws Exception {
+        String payload = unwrapJsonString(rechargePayload);
+        if (StrUtil.isBlank(payload)) {
+            throw BusinessRuntimeException.getInstance("提交任务需要token");
+        }
+        if (StrUtil.startWith(payload, "[")) {
+            JSONArray array = JSONUtil.parseArray(payload);
+            if (array.isEmpty()) {
+                throw BusinessRuntimeException.getInstance("token不能为空");
+            }
+            payload = unwrapJsonString(String.valueOf(array.get(0)));
+        }
+        if (!StrUtil.startWith(payload, "{")) {
+            JSONObject token = new JSONObject();
+            token.putOpt("accessToken", payload);
+            return token;
+        }
+
+        JSONObject token = JSONUtil.parseObj(payload);
+        Object nestedToken = token.get("token");
+        if (nestedToken instanceof JSONObject) {
+            return (JSONObject) nestedToken;
+        }
+        if (nestedToken instanceof CharSequence) {
+            return parseTokenObject(nestedToken.toString());
+        }
+        Object nestedPayload = token.get("payload");
+        if (nestedPayload instanceof JSONObject) {
+            return (JSONObject) nestedPayload;
+        }
+        if (nestedPayload instanceof CharSequence) {
+            return parseTokenObject(nestedPayload.toString());
+        }
+        String accessToken = token.getStr("accessToken");
+        if (!isCompleteSession(token) && StrUtil.isNotBlank(accessToken)) {
+            String accessTokenPayload = unwrapJsonString(accessToken);
+            if (StrUtil.startWith(accessTokenPayload, "{")) {
+                return JSONUtil.parseObj(accessTokenPayload);
+            }
+        }
+        return token;
+    }
+
+    private JSONObject parseTokenObject(String value) throws Exception {
+        String payload = unwrapJsonString(value);
+        if (StrUtil.startWith(payload, "{")) {
+            return JSONUtil.parseObj(payload);
+        }
+        JSONObject token = new JSONObject();
+        token.putOpt("accessToken", payload);
+        return token;
+    }
+
+    private boolean isCompleteSession(JSONObject token) {
+        return token.get("user") instanceof JSONObject
+                || token.get("account") instanceof JSONObject
+                || token.containsKey("expires")
+                || token.containsKey("sessionToken")
+                || token.containsKey("authProvider");
+    }
+
+    private String unwrapJsonString(String value) throws Exception {
+        String result = StrUtil.trim(value);
+        for (int i = 0; i < 3 && StrUtil.startWith(result, "\""); i++) {
+            result = StrUtil.trim(Jsons.parseObject(result, String.class));
+        }
+        return result;
+    }
+
+    private String buildApiBaseUrl(String baseUrl) {
+        if (StrUtil.isBlank(baseUrl)) {
+            throw BusinessRuntimeException.getInstance("充值渠道域名未配置");
+        }
+        baseUrl = StrUtil.removeSuffix(StrUtil.trim(baseUrl), "/");
+        if (StrUtil.endWithIgnoreCase(baseUrl, "/api")) {
+            baseUrl = baseUrl + "/v1";
+        } else if (!StrUtil.endWithIgnoreCase(baseUrl, "/api/v1")) {
+            baseUrl = baseUrl + "/api/v1";
+        }
+        return baseUrl;
+    }
+
+    private String encode(String value) {
+        try {
+            return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private <T> T parseResponse(String body, Class<T> responseType, String apiName) {
+        try {
+            return Jsons.parseObject(body, responseType);
+        } catch (Exception e) {
+            throw BusinessRuntimeException.getInstance("{0}响应解析失败", apiName);
+        }
+    }
+
+    private BusinessRuntimeException requestException(String message, String defaultMessage) {
+        return BusinessRuntimeException.getInstance(StrUtil.blankToDefault(message, defaultMessage));
+    }
+
+    private void setRechargeRelationStatusAutoError(GroupsRelation relation) {
+        TASK_EXECUTOR.execute(() -> relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+                .set(GroupsRelation::getAccount, relation.getAccount())
+                .set(GroupsRelation::getGptToken, relation.getGptToken())
+                .set(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharge_error)
+                .eq(GroupsRelation::getId, relation.getId())));
+    }
+
+    private void resetGroupsRelationRechargeStatus(GroupsRelation relation, String status, String message) {
+        if (!"completed".equals(status) && !"failed".equals(status)) {
+            return;
+        }
+        relation = relationMapper.selectById(relation.getId());
+        if (relation == null || relation.getRechargeStatus() != GroupsRelation.RechargeStatus.recharging) {
+            return;
+        }
+
+        boolean completed = "completed".equals(status);
+        int rechargeNum = getRechargeNum(relation);
+        Integer rechargeRemainNum = relation.getRechargeRemainNum();
+        GroupsRelation.RechargeStatus rechargeStatus = completed
+                ? GroupsRelation.RechargeStatus.complete
+                : GroupsRelation.RechargeStatus.recharge_error;
+        GptRechargeCardKey cardKey = gptRechargeCardKeyMapper.selectOne(Wrappers.lambdaQuery(GptRechargeCardKey.class)
+                .eq(GptRechargeCardKey::getCardKey, relation.getCardKey())
+                .last("limit 1"));
+
+        if (!completed && !redisService.setNx(
+                RedisService.key.GPT_RECHARGE_RECOVER_KEY.getName() + relation.getGptTaskId(),
+                relation.getGptTaskId(), RedisService.key.GPT_RECHARGE_RECOVER_KEY.getTimeout())) {
+            return;
+        }
+        if (!completed && cardKey != null) {
+            gptRechargeCardKeyMapper.update(null, Wrappers.lambdaUpdate(GptRechargeCardKey.class)
+                    .set(GptRechargeCardKey::getStatus, Boolean.FALSE)
+                    .set(GptRechargeCardKey::getOrderId, null)
+                    .set(GptRechargeCardKey::getOrderNo, null)
+                    .eq(GptRechargeCardKey::getId, cardKey.getId())
+                    .eq(GptRechargeCardKey::getStatus, Boolean.TRUE));
+        }
+
+        Date startTime = relation.getStartTime();
+        DateTime expiryTime = null;
+        if (completed && startTime == null) {
+            GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+            if (groupsTrips != null) {
+                GoodsDonSku sku = skuMapper.selectById(groupsTrips.getSkuId());
+                if (sku != null) {
+                    startTime = DateTime.now();
+                    if (sku.getDays() != null && sku.getDays() > 0) {
+                        expiryTime = DateUtil.offsetDay(startTime, sku.getDays());
+                    } else if (sku.getMonths() != null) {
+                        expiryTime = DateUtil.offsetMonth(startTime, sku.getMonths());
+                    }
+                }
+            }
+        }
+
+        int totalRechargeNum = relation.getRechargeNum() == null ? rechargeRemainNum + rechargeNum : relation.getRechargeNum();
+        int recoveredRemainNum = Math.min(totalRechargeNum, rechargeRemainNum + rechargeNum);
+        int update = relationMapper.update(null, Wrappers.lambdaUpdate(GroupsRelation.class)
+                .set(!completed, GroupsRelation::getRechargeRemainNum, recoveredRemainNum)
+                .set(GroupsRelation::getRechargeStatus, rechargeStatus)
+                .set(expiryTime != null, GroupsRelation::getStartTime, startTime)
+                .set(expiryTime != null, GroupsRelation::getExpiryTime, expiryTime)
+                .set(!completed && StrUtil.isNotBlank(message), GroupsRelation::getRechargeErrorInfo, message)
+                .eq(GroupsRelation::getId, relation.getId())
+                .eq(GroupsRelation::getRechargeStatus, GroupsRelation.RechargeStatus.recharging));
+        if (update == 0 || !completed) {
+            return;
+        }
+
+        String remark = StrUtil.isNotBlank(relation.getOperator()) ? "后台自动代充" : "自动代充";
+        gptUserRechargeRecordService.recordGptUserRechargeNum(relation.getCardKey(), relation.getUserId(),
+                relation.getId(), relation.getAccount(), -rechargeNum, relation.getOperator(), remark);
+        if (cardKey != null) {
+            OrderDon order = orderDonMapper.selectById(cardKey.getOrderId());
+            if (order != null && order.getStatus() == OrderDon.Status.hasPayment) {
+                order.setStatus(OrderDon.Status.complete);
+                orderDonMapper.updateById(order);
+            }
+        }
+    }
+
+    private int getRechargeNum(GroupsRelation relation) {
+        GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+        if (groupsTrips == null) {
+            return 1;
+        }
+        GoodsDonSku sku = skuMapper.selectById(groupsTrips.getSkuId());
+        return sku != null && Integer.valueOf(2).equals(sku.getRechargeType()) ? 12 : 1;
+    }
+}