zoujiajian преди 11 месеца
родител
ревизия
f13bceaaf8
променени са 20 файла, в които са добавени 1129 реда и са изтрити 9 реда
  1. 15 0
      netflix-dao/src/main/java/com/cyksj/mapper/CodexUserMapper.java
  2. 56 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CodexUser.java
  3. 22 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GoodsDonSku.java
  4. 17 0
      netflix-dao/src/main/java/com/cyksj/model/manage/request/ReqGoodsSkuInsert.java
  5. 24 0
      netflix-dao/src/main/java/com/cyksj/model/request/UpdateDailyLimitRequest.java
  6. 27 0
      netflix-dao/src/main/java/com/cyksj/model/request/codex/CodexPlanReq.java
  7. 49 0
      netflix-dao/src/main/java/com/cyksj/model/request/codex/CodexUserPackageReq.java
  8. 13 0
      netflix-dao/src/main/java/com/cyksj/model/request/codex/CreateKeyRequest.java
  9. 40 0
      netflix-dao/src/main/java/com/cyksj/model/response/codex/CodexPlanResp.java
  10. 47 0
      netflix-dao/src/main/java/com/cyksj/model/response/codex/CodexUserPackageResp.java
  11. 10 0
      netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserInfoView.java
  12. 81 0
      netflix-dao/src/main/java/com/cyksj/model/views/CodexUserInfoView.java
  13. 1 1
      netflix-service/src/main/java/com/cyksj/service/claude/ClaudeCodeService.java
  14. 37 7
      netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java
  15. 60 0
      netflix-service/src/main/java/com/cyksj/service/codex/CodexService.java
  16. 501 0
      netflix-service/src/main/java/com/cyksj/service/codex/impl/CodexServiceImpl.java
  17. 3 0
      netflix-service/src/main/java/com/cyksj/service/mange/impl/CmsOrderDonServiceImpl.java
  18. 3 1
      netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java
  19. 4 0
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationClearServiceImpl.java
  20. 119 0
      netflix-web/src/main/java/com/cyksj/web/controller/codex/CodexController.java

+ 15 - 0
netflix-dao/src/main/java/com/cyksj/mapper/CodexUserMapper.java

@@ -0,0 +1,15 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CodexUser;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUserMapper
+ * 创建者: Claude
+ * 创建时间:2025/9/15
+ */
+@Mapper
+public interface CodexUserMapper extends BaseMapper<CodexUser> {
+}

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

@@ -0,0 +1,56 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUser
+ * 创建者: Claude
+ * 创建时间:2025/9/15
+ */
+@Getter
+@Setter
+public class CodexUser extends BaseEntity {
+    
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 套餐名
+     */
+    private String planName;
+    
+    /**
+     * 每日预算上限
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer openaiDailyLimit;
+    
+    /**
+     * 每月预算上限
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer openaiQuota;
+    
+    /**
+     * 座位ID
+     */
+    private Long relationId;
+
+    /**
+     * 过期时间
+     */
+    private Date expiryTime;
+
+    /**
+     * 是否重试更新
+     */
+    private Boolean isRetry;
+}

+ 22 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/GoodsDonSku.java

@@ -291,4 +291,26 @@ public class GoodsDonSku extends BaseEntity {
      */
     @TableField(updateStrategy = FieldStrategy.IGNORED)
     private Integer highestUsage;
+
+    /**
+     * 是否赠送codex
+     */
+    private Boolean isCodex;
+
+    /**
+     * codex套餐名
+     */
+    private String codexPlanName;
+
+    /**
+     * 每日预算上限
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer openaiDailyLimit;
+
+    /**
+     * 每月预算上限
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer openaiQuota;
 }

+ 17 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/request/ReqGoodsSkuInsert.java

@@ -212,6 +212,23 @@ public class ReqGoodsSkuInsert {
          */
         @TableField(updateStrategy = FieldStrategy.IGNORED)
         private Integer highestUsage;
+
+        /**
+         * 是否赠送codex
+         */
+        private Boolean isCodex;
+
+        /**
+         * 每日预算上限
+         */
+        @TableField(updateStrategy = FieldStrategy.IGNORED)
+        private Integer openaiDailyLimit;
+
+        /**
+         * 每月预算上限
+         */
+        @TableField(updateStrategy = FieldStrategy.IGNORED)
+        private Integer openaiQuota;
     }
 
 }

+ 24 - 0
netflix-dao/src/main/java/com/cyksj/model/request/UpdateDailyLimitRequest.java

@@ -0,0 +1,24 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: UpdateDailyLimitRequest
+ * 创建者: JavaZou
+ * 创建时间:2025/9/16 18:15
+ */
+@Getter
+@Setter
+public class UpdateDailyLimitRequest {
+	/**
+	 * 每日美金限额
+	 */
+	private Integer dailyLimit;
+
+	/**
+	 * 总美金额度
+	 */
+	private Integer totalQuota;
+}

+ 27 - 0
netflix-dao/src/main/java/com/cyksj/model/request/codex/CodexPlanReq.java

@@ -0,0 +1,27 @@
+package com.cyksj.model.request.codex;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexPlanReq
+ * 创建者: Claude
+ * 创建时间:2025/9/16
+ */
+@Getter
+@Setter
+@Builder
+public class CodexPlanReq {
+    
+    /**
+     * 每日预算上限
+     */
+    private Integer openaiDailyLimit;
+    
+    /**
+     * 每月预算上限
+     */
+    private Integer openaiQuota;
+}

+ 49 - 0
netflix-dao/src/main/java/com/cyksj/model/request/codex/CodexUserPackageReq.java

@@ -0,0 +1,49 @@
+package com.cyksj.model.request.codex;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUserPackageReq
+ * 创建者: Claude
+ * 创建时间:2025/9/16
+ */
+@Getter
+@Setter
+@Builder
+public class CodexUserPackageReq {
+
+    /**
+     * 套餐名
+     */
+    private String planName;
+    
+    /**
+     * 每日预算上限
+     */
+    private Integer openaiDailyLimit;
+    
+    /**
+     * 每月预算上限
+     */
+    private Integer openaiQuota;
+    
+    /**
+     * 座位ID (内部设置,用户请求时不需要)
+     */
+    private Long relationId;
+    
+    /**
+     * 用户ID (内部设置,用户请求时不需要)
+     */
+    private Long userId;
+
+    /**
+     * 过期时间
+     */
+    private Date expiryTime;
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/model/request/codex/CreateKeyRequest.java

@@ -0,0 +1,13 @@
+package com.cyksj.model.request.codex;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 创建密钥请求参数
+ */
+@Getter
+@Setter
+public class CreateKeyRequest {
+	private String keyName;
+}

+ 40 - 0
netflix-dao/src/main/java/com/cyksj/model/response/codex/CodexPlanResp.java

@@ -0,0 +1,40 @@
+package com.cyksj.model.response.codex;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexPlanResp
+ * 创建者: Claude
+ * 创建时间:2025/9/16
+ */
+@Getter
+@Setter
+public class CodexPlanResp {
+    
+    /**
+     * 成功标识
+     */
+    private Boolean success;
+    
+    /**
+     * 错误消息
+     */
+    private String message;
+    
+    /**
+     * 用户ID
+     */
+    private String userId;
+    
+    /**
+     * 每日预算上限
+     */
+    private Integer openaiDailyLimit;
+    
+    /**
+     * 每月预算上限
+     */
+    private Integer openaiQuota;
+}

+ 47 - 0
netflix-dao/src/main/java/com/cyksj/model/response/codex/CodexUserPackageResp.java

@@ -0,0 +1,47 @@
+package com.cyksj.model.response.codex;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUserPackageResp
+ * 创建者: Claude
+ * 创建时间:2025/9/16
+ */
+@Getter
+@Setter
+@Builder
+public class CodexUserPackageResp {
+    
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    
+    /**
+     * 每日预算上限
+     */
+    private Integer openaiDailyLimit;
+    
+    /**
+     * 每月预算上限
+     */
+    private Integer openaiQuota;
+    
+    /**
+     * 座位ID
+     */
+    private Long relationId;
+    
+    /**
+     * 操作结果消息
+     */
+    private String message;
+    
+    /**
+     * 是否为新创建的用户
+     */
+    private Boolean isNewUser;
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/model/views/ClaudeCodeUserInfoView.java

@@ -54,4 +54,14 @@ public class ClaudeCodeUserInfoView {
 	 * cc最高约可使用次数
 	 */
 	private Integer highestUsage;
+
+	/**
+	 * codex 每日限额
+	 */
+	private Integer openaiDailyLimit;
+
+	/**
+	 * codex 总限额
+	 */
+	private Integer openaiQuota;
 }

+ 81 - 0
netflix-dao/src/main/java/com/cyksj/model/views/CodexUserInfoView.java

@@ -0,0 +1,81 @@
+package com.cyksj.model.views;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexUserInfoView
+ * 创建者: Claude
+ * 创建时间:2025/9/15
+ */
+@Getter
+@Setter
+@Builder
+public class CodexUserInfoView {
+    
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    
+    /**
+     * 每日预算上限
+     */
+    private Integer openaiDailyLimit;
+    
+    /**
+     * 每月预算上限
+     */
+    private Integer openaiQuota;
+    
+    /**
+     * 座位ID
+     */
+    private Long relationId;
+    
+    /**
+     * Claude Code套餐信息
+     */
+    private ClaudeCodeInfo claudeCodeInfo;
+    
+    @Getter
+    @Setter
+    @Builder
+    public static class ClaudeCodeInfo {
+        /**
+         * 套餐名
+         */
+        private String planName;
+
+        private Long relationId;
+        
+        /**
+         * claude code积分上限
+         */
+        private Integer codePoints;
+        
+        /**
+         * claude code每小时恢复积分
+         */
+        private Integer codeCreditRecovery;
+        
+        /**
+         * code 有效期
+         */
+        private Date expiryTime;
+        
+        /**
+         * 续费升级skuId
+         */
+        private Long renewSkuId;
+        
+        /**
+         * 续费升级过期时间
+         */
+        private Date renewExpiryTime;
+    }
+}

+ 1 - 1
netflix-service/src/main/java/com/cyksj/service/claude/ClaudeCodeService.java

@@ -63,7 +63,7 @@ public interface ClaudeCodeService {
 
 	Boolean checkClaudeCodeUser(Long userId);
 
-	void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime);
+	void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime, GoodsDonSku sku);
 
 	/**
 	 * 删除用户claude code 套餐

+ 37 - 7
netflix-service/src/main/java/com/cyksj/service/claude/impl/ClaudeCodeServiceImpl.java

@@ -27,6 +27,7 @@ import com.cyksj.model.request.ClaudeCodeApiKeysReq;
 import com.cyksj.model.request.ClaudeCodeCreditCardUseReq;
 import com.cyksj.model.request.ClaudeCodeDelReq;
 import com.cyksj.model.request.CreditResetReq;
+import com.cyksj.model.request.codex.CodexUserPackageReq;
 import com.cyksj.model.response.ClaudeCodePointsHistoryResp;
 import com.cyksj.model.response.ClaudeCodeRewardDetailsResp;
 import com.cyksj.model.response.CreditResetResp;
@@ -38,6 +39,7 @@ import com.cyksj.model.views.ClaudeCodeUserInfoView;
 import com.cyksj.model.views.ClaudeCodeUserResetRecordView;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.codex.CodexService;
 import com.cyksj.service.user.UserBindRelationService;
 import com.ejlchina.searcher.BeanSearcher;
 import com.ejlchina.searcher.SearchResult;
@@ -100,6 +102,10 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 
 	private final RedisService redisService;
 
+	private final CodexService codexService;
+	
+	private final CodexUserMapper codexUserMapper;
+
 	private final static String CLAUDE_CODE_API_PREFIX = "https://relay01.yhlxj.com/";
 
 	public static final String CLAUDE_CODE_HEARD_KEY = "X-Admin-Key";
@@ -148,7 +154,7 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 			//获取最新的claude code user
 			claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
 			//生成claude code或更新用户套餐
-			ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(relation.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), claudeCodeUser.getExpiryTime());
+			ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(relation.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), relation.getStartTime(), claudeCodeUser.getExpiryTime(), sku);
 			if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
 				//重试更新code userInfo数据
 				setClaudeCodeUserRetryUpdateInfo(relationId);
@@ -365,7 +371,8 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 		//是否需要重试更新 cladueCode userInfo信息
 		if (claudeCodeUser.getIsRetry()) {
 			try {
-				ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), claudeCodeUser.getExpiryTime());
+				GroupsRelation relation = relationMapper.selectById(claudeCodeUser.getRelationId());
+				ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), relation.getStartTime(), claudeCodeUser.getExpiryTime(), sku);
 				if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
 					claudeCodeUser.setIsRetry(Boolean.TRUE);
 					claudeCodeUserMapper.updateById(claudeCodeUser);
@@ -390,17 +397,33 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 				.build();
 	}
 
-	public ClaudeCodeResp createOrUpdateClaudeCodeUserPackage(Long userId, String planName, Integer codePoints, Integer codeCreditRecovery, Date expiryTime) throws Exception {
+	public ClaudeCodeResp createOrUpdateClaudeCodeUserPackage(Long userId, String planName, Integer codePoints, Integer codeCreditRecovery, Date startTime, Date expiryTime, GoodsDonSku sku) throws Exception {
 		Map<String, Object> parmas = new HashMap<>();
 		parmas.put("plan_name", planName.replaceAll(";", ""));
 		parmas.put("credit_limit", codePoints);
 		parmas.put("credit_recovery", codeCreditRecovery);
-		parmas.put("expire_time", DateUtil.format(expiryTime,"yyyy-MM-dd HH:mm:ss"));
+		if (sku.getIsCodex()) {
+			parmas.put("openai_daily_limit", sku.getOpenaiDailyLimit());
+			parmas.put("openai_quota", sku.getOpenaiQuota());
+		}
+		parmas.put("start_date", DateUtil.format(startTime, "yyyy-MM-dd HH:mm:ss"));
+		parmas.put("end_date", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss"));
 		String planUrl = String.format(CLAUDE_CODE_API_PREFIX + "api/users/%s/plan", userId);
 		ClaudeCodeResp claudeCodeResp = executeClaudeCodePostApi(planUrl, Jsons.toJson(parmas));
 		if (!Constant.SUCCESS.equals(claudeCodeResp.getMessage())) {
 			log.info("用户claude code用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, claudeCodeResp.getMessage());
 		}
+
+		//是否赠送codex
+		if (sku.getIsCodex()) {
+			CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder()
+					.userId(userId)
+					.planName(sku.getCodexPlanName())
+					.openaiDailyLimit(sku.getOpenaiDailyLimit())
+					.openaiQuota(sku.getOpenaiQuota())
+					.expiryTime(expiryTime).build();
+			codexService.createOrUpdateUserPackage(codexPackageReq);
+		}
 		return claudeCodeResp;
 	}
 
@@ -418,9 +441,10 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 	}
 
 	@Override
-	public void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime) {
+	public void updateClaudeCodeUserPackageInfo(ClaudeCodeUser claudeCodeUser, Date expiryTime, GoodsDonSku sku) {
 		try {
-			ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), expiryTime);
+			GroupsRelation relation = relationMapper.selectById(claudeCodeUser.getRelationId());
+			ClaudeCodeResp codeUserPackage = createOrUpdateClaudeCodeUserPackage(claudeCodeUser.getUserId(), claudeCodeUser.getPlanName(), claudeCodeUser.getCodePoints(), claudeCodeUser.getCodeCreditRecovery(), relation.getStartTime(), expiryTime, sku);
 			if (!Constant.SUCCESS.equals(codeUserPackage.getMessage())) {
 				//重试标记
 				claudeCodeUser.setIsRetry(Boolean.TRUE);
@@ -607,7 +631,7 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 	}
 
 	/**
-	 * claude code用户信息重试更新信息
+	 * claude code、codex user用户信息重试更新信息
 	 */
 	public void setClaudeCodeUserRetryUpdateInfo(Long relationId) {
 		ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId).last("limit 1"));
@@ -615,6 +639,12 @@ public class ClaudeCodeServiceImpl implements ClaudeCodeService {
 			claudeCodeUser.setIsRetry(Boolean.TRUE);
 			claudeCodeUserMapper.updateById(claudeCodeUser);
 		}
+
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
+		if (codexUser != null) {
+			codexUser.setIsRetry(Boolean.TRUE);
+			codexUserMapper.updateById(codexUser);
+		}
 	}
 
 	public Retryer getApiRetryer(Integer sleepSecond, Integer attemptNum) {

+ 60 - 0
netflix-service/src/main/java/com/cyksj/service/codex/CodexService.java

@@ -0,0 +1,60 @@
+package com.cyksj.service.codex;
+
+import com.cyksj.model.request.UpdateDailyLimitRequest;
+import com.cyksj.model.request.codex.CodexUserPackageReq;
+import com.cyksj.model.response.claudecode.ClaudeCodeResp;
+import com.cyksj.model.views.CodexUserInfoView;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexService
+ * 创建者: Claude
+ * 创建时间:2025/9/15
+ */
+public interface CodexService {
+    
+    /**
+     * 获取用户Codex信息
+     */
+    CodexUserInfoView getCodexUserInfo(Long userId);
+    
+    /**
+     * 创建或更新用户套餐
+     */
+    void createOrUpdateUserPackage(CodexUserPackageReq req);
+    
+    /**
+     * 获取用户OpenAI使用统计
+     */
+    ClaudeCodeResp getUserOpenAIUsage(Long userId, String period);
+    
+    /**
+     * 更新用户OpenAI每日美金限额
+     */
+    ClaudeCodeResp updateUserOpenAIDailyLimit(Long userId, UpdateDailyLimitRequest request);
+    
+    /**
+     * 获取系统OpenAI指标统计
+     */
+    ClaudeCodeResp getSystemMetrics(String period, String metric);
+
+    /**
+     * 获取用户密钥列表
+     */
+    ClaudeCodeResp getUserKeys(Long userId);
+    
+    /**
+     * 创建用户密钥
+     */
+    ClaudeCodeResp createUserKey(Long userId, String keyName);
+    
+    /**
+     * 删除用户密钥
+     */
+    ClaudeCodeResp deleteUserKey(Long userId, String keyId);
+    
+    /**
+     * 获取用户仪表盘数据
+     */
+    ClaudeCodeResp getUserDashboard(Long userId);
+}

+ 501 - 0
netflix-service/src/main/java/com/cyksj/service/codex/impl/CodexServiceImpl.java

@@ -0,0 +1,501 @@
+package com.cyksj.service.codex.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.http.Method;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.util.Jsons;
+import com.cyksj.common.util.StringUtil;
+import com.cyksj.mapper.*;
+import com.cyksj.model.entity.*;
+import com.cyksj.model.request.UpdateDailyLimitRequest;
+import com.cyksj.model.request.codex.CodexUserPackageReq;
+import com.cyksj.model.response.claudecode.ClaudeCodeResp;
+import com.cyksj.model.views.CodexUserInfoView;
+import com.cyksj.service.codex.CodexService;
+import com.cyksj.service.user.UserBindRelationService;
+import com.github.rholder.retry.Retryer;
+import com.github.rholder.retry.RetryerBuilder;
+import com.github.rholder.retry.StopStrategies;
+import com.github.rholder.retry.WaitStrategies;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexServiceImpl
+ * 创建者: Claude
+ * 创建时间:2025/9/15
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CodexServiceImpl implements CodexService {
+
+	private final CodexUserMapper codexUserMapper;
+
+	private final ClaudeCodeUserMapper claudeCodeUserMapper;
+
+	private final UserBindRelationService userBindRelationService;
+
+	private final GoodsDonSkuMapper skuMapper;
+
+	private final GroupsRelationMapper relationMapper;
+
+	private final GroupsMapper groupsMapper;
+
+	/**
+	 * Codex API 基础URL
+	 */
+	private static final String CODEX_API_PREFIX = "https://relaydev.nf.video/v1/";
+
+	/**
+	 * Codex API Header Key
+	 */
+	public static final String CODEX_HEARD_KEY = "X-Admin-Key";
+
+	/**
+	 * Codex API Admin Key
+	 */
+	public static final String CODEX_API_ADMIN_KEY = "codexadminkey";
+
+	/**
+	 * 连接超时时间 毫秒
+	 */
+	public static final int CONNECT_MILLISECONDS = 10000;
+
+	@Override
+	public CodexUserInfoView getCodexUserInfo(Long userId) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+		// 查询Claude Code信息
+		CodexUserInfoView.ClaudeCodeInfo claudeCodeInfo = getClaudeCodeInfo(userId);
+
+		if (codexUser == null) {
+			//是否有claude code
+			if (claudeCodeInfo.getCodePoints() != null) {
+				GoodsDonSku sku = null;
+				if (claudeCodeInfo.getRenewSkuId() != null) {
+					sku = skuMapper.selectById(claudeCodeInfo.getRenewSkuId());
+				}
+				//续费升级规格不存在 或者 不赠送codex
+				if (sku == null || !sku.getIsCodex()) {
+					GroupsRelation relation = relationMapper.selectById(claudeCodeInfo.getRelationId());
+					GroupsTrips groupsTrips = groupsMapper.selectById(relation.getGroupsId());
+					sku = skuMapper.selectById(groupsTrips.getSkuId());
+				}
+
+				//sku 存在并且开启赠送codex
+				if (sku != null && sku.getIsCodex()) {
+					CodexUserPackageReq codexPackageReq = CodexUserPackageReq.builder()
+							.userId(userId)
+							.planName(sku.getCodexPlanName())
+							.openaiDailyLimit(sku.getOpenaiDailyLimit())
+							.openaiQuota(sku.getOpenaiQuota())
+							.expiryTime(claudeCodeInfo.getExpiryTime())
+							.build();
+					createOrUpdateUserPackage(codexPackageReq);
+					codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+				}
+			}
+			if (codexUser == null) {
+				return null;
+			}
+		}
+
+
+		return CodexUserInfoView.builder().userId(codexUser.getUserId()).openaiDailyLimit(codexUser.getOpenaiDailyLimit()).openaiQuota(codexUser.getOpenaiQuota()).relationId(codexUser.getRelationId()).claudeCodeInfo(claudeCodeInfo).build();
+	}
+
+	private CodexUserInfoView.ClaudeCodeInfo getClaudeCodeInfo(Long userId) {
+		// 获取用户关联的ID列表
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+
+		ClaudeCodeUser claudeCodeUser = claudeCodeUserMapper.selectOne(Wrappers.lambdaQuery(ClaudeCodeUser.class).in(ClaudeCodeUser::getUserId, userIdList).last("limit 1"));
+
+		if (claudeCodeUser == null) {
+			return null;
+		}
+
+		return CodexUserInfoView.ClaudeCodeInfo.builder().planName(claudeCodeUser.getPlanName()).relationId(claudeCodeUser.getRelationId()).codePoints(claudeCodeUser.getCodePoints()).codeCreditRecovery(claudeCodeUser.getCodeCreditRecovery()).expiryTime(claudeCodeUser.getExpiryTime()).renewSkuId(claudeCodeUser.getRenewSkuId()).renewExpiryTime(claudeCodeUser.getRenewExpiryTime()).build();
+	}
+
+	@Override
+	public void createOrUpdateUserPackage(CodexUserPackageReq req) {
+		Long userId = req.getUserId();
+		// 构建完整的套餐参数
+		String planName = req.getPlanName();
+		Date expiryTime = req.getExpiryTime();
+		Integer openaiDailyLimit = req.getOpenaiDailyLimit();
+		Integer openaiQuota = req.getOpenaiQuota();
+		Long relationId = req.getRelationId();
+		try {
+			createOrUpdateCodexUserPackage(userId, planName, openaiDailyLimit, openaiQuota, expiryTime);
+		} catch (Exception e) {
+			setCodexUserRetryUpdateInfo(relationId);
+			return;
+		}
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser existingCodexUser = Optional.ofNullable(codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"))).orElse(new CodexUser());
+
+		existingCodexUser.setPlanName(planName);
+		existingCodexUser.setRelationId(relationId);
+		existingCodexUser.setUserId(userId);
+		existingCodexUser.setOpenaiDailyLimit(openaiDailyLimit);
+		existingCodexUser.setOpenaiQuota(openaiQuota);
+		existingCodexUser.setExpiryTime(expiryTime);
+
+		if (existingCodexUser.getId() == null) {
+			try {
+				codexUserMapper.insert(existingCodexUser);
+			} catch (DuplicateKeyException e) {
+
+			}
+			return;
+		}
+		codexUserMapper.updateById(existingCodexUser);
+	}
+
+	/**
+	 * 创建或更新Codex用户套餐
+	 */
+	public ClaudeCodeResp createOrUpdateCodexUserPackage(Long userId, String planName, Integer openaiDailyLimit, Integer openaiQuota, Date expiryTime) throws Exception {
+		Map<String, Object> params = new HashMap<>();
+		params.put("plan_name", planName.replaceAll(";", ""));
+		params.put("openai_daily_limit", openaiDailyLimit);
+		params.put("openai_quota", openaiQuota);
+		params.put("end_date", DateUtil.format(expiryTime, "yyyy-MM-dd HH:mm:ss"));
+
+		String planUrl = String.format(CODEX_API_PREFIX + "api/users/%s/plan", userId);
+		ClaudeCodeResp codexResp = executeCodexPostApi(planUrl, Jsons.toJson(params));
+
+		if (!"success".equals(codexResp.getMessage())) {
+			log.info("用户codex用户:{}套餐:{}创建或更新失败,info:{}", userId, planName, codexResp.getMessage());
+			throw BusinessRuntimeException.getInstance("调用codex 套餐接口失败");
+		}
+
+		return codexResp;
+	}
+
+
+	private Retryer<ClaudeCodeResp> getApiRetryer(int wait, int stop) {
+		return RetryerBuilder.<ClaudeCodeResp>newBuilder().retryIfResult(result -> result == null).retryIfException().withWaitStrategy(WaitStrategies.fixedWait(wait, TimeUnit.SECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(stop)).build();
+	}
+
+	/**
+	 * Codex POST请求 - 复制ClaudeCodeServiceImpl的实现模式
+	 */
+	public ClaudeCodeResp executeCodexPostApi(String url, String body) {
+		Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
+		try {
+			return build.call(() -> {
+				try {
+					HttpResponse execute = HttpUtil.createPost(url).header(CODEX_HEARD_KEY, CODEX_API_ADMIN_KEY).setConnectionTimeout(CONNECT_MILLISECONDS).body(body).execute();
+					ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
+					if (!"success".equals(resp.getMessage())) {
+						log.error("codex POST URL:{}接口返回msg:{}", url, resp.getMessage());
+					}
+					return resp;
+				} catch (Exception e) {
+					log.error("codex POST请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
+					return null;
+				}
+			});
+		} catch (ExecutionException | com.github.rholder.retry.RetryException e) {
+			log.info("重试调用codex POST请求 url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
+			throw BusinessRuntimeException.getInstance("创建或更新codex user失败");
+		}
+	}
+
+	/**
+	 * codex用户信息重试更新信息
+	 */
+	public void setCodexUserRetryUpdateInfo(Long relationId) {
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId).last("limit 1"));
+		if (codexUser != null) {
+			codexUser.setIsRetry(Boolean.TRUE);
+			codexUserMapper.updateById(codexUser);
+		}
+	}
+
+	@Override
+	public ClaudeCodeResp getUserOpenAIUsage(Long userId, String period) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+
+		if (codexUser == null) {
+			return null;
+		}
+		// 调用Codex API获取实时使用统计
+		String usageUrl = String.format(CODEX_API_PREFIX + "api/users/%s/usage?period=%s", userId, period);
+		ClaudeCodeResp codexResp = executeCodexGetApi(usageUrl);
+		return codexResp;
+	}
+
+	@Override
+	public ClaudeCodeResp updateUserOpenAIDailyLimit(Long userId, UpdateDailyLimitRequest request) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+
+		if (codexUser == null) {
+			throw new BusinessRuntimeException("用户未开通Codex服务");
+		}
+		Integer dailyLimit = request.getDailyLimit();
+		Integer totalQuota = request.getTotalQuota();
+
+		// 验证每日限额参数
+		if (dailyLimit == null || dailyLimit <= 0) {
+			throw new BusinessRuntimeException("每日限额必须大于0");
+		}
+		
+		// 保存原始值用于回滚
+		Integer originalDailyLimit = codexUser.getOpenaiDailyLimit();
+		
+		// 更新数据库中的每日限额
+		codexUser.setOpenaiDailyLimit(dailyLimit);
+		codexUserMapper.updateById(codexUser);
+		
+		try {
+			// 调用Codex API更新每日限额
+			String updateUrl = String.format(CODEX_API_PREFIX + "api/users/%s/limit", userId);
+			Map<String, Object> params = new HashMap<>();
+			params.put("daily_limit", dailyLimit);
+			params.put("total_quota", totalQuota);
+
+			ClaudeCodeResp codexResp = executeCodexPostApi(updateUrl, Jsons.toJson(params));
+			
+			if (!"success".equals(codexResp.getMessage())) {
+				log.error("更新用户{}每日限额{}失败: {}", userId, dailyLimit, codexResp.getMessage());
+				// API调用失败时回滚数据库更新
+				codexUser.setOpenaiDailyLimit(originalDailyLimit);
+				codexUserMapper.updateById(codexUser);
+				throw new BusinessRuntimeException("更新每日限额失败");
+			}
+			
+			log.info("成功更新用户{}每日限额为{}", userId, dailyLimit);
+			return codexResp;
+			
+		} catch (Exception e) {
+			log.error("更新用户{}每日限额{}异常: {}", userId, dailyLimit, StringUtil.getErrorText(e));
+			// 异常时回滚数据库更新
+			codexUser.setOpenaiDailyLimit(originalDailyLimit);
+			codexUserMapper.updateById(codexUser);
+			throw new BusinessRuntimeException("更新每日限额失败");
+		}
+	}
+
+	@Override
+	public ClaudeCodeResp getSystemMetrics(String period, String metric) {
+		try {
+			// 构建系统指标API URL
+			String metricsUrl = String.format(CODEX_API_PREFIX + "api/openai/system/metrics");
+			
+			// 添加查询参数
+			List<String> queryParams = new ArrayList<>();
+			if (period != null && !period.isEmpty()) {
+				queryParams.add("period=" + period);
+			}
+			if (metric != null && !metric.isEmpty()) {
+				queryParams.add("metric=" + metric);
+			}
+			
+			if (!queryParams.isEmpty()) {
+				metricsUrl += "?" + String.join("&", queryParams);
+			}
+			
+			// 调用Codex API获取系统指标
+			ClaudeCodeResp codexResp = executeCodexGetApi(metricsUrl);
+			
+			if (codexResp == null) {
+				log.error("获取系统指标失败: API返回null");
+				throw new BusinessRuntimeException("获取系统指标失败");
+			}
+			
+			log.info("成功获取系统指标, period={}, metric={}", period, metric);
+			return codexResp;
+			
+		} catch (Exception e) {
+			log.error("获取系统指标异常, period={}, metric={}, error={}", period, metric, StringUtil.getErrorText(e));
+			throw new BusinessRuntimeException("获取系统指标失败");
+		}
+	}
+
+	@Override
+	public ClaudeCodeResp getUserKeys(Long userId) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+
+		if (codexUser == null) {
+			throw new BusinessRuntimeException("用户未开通Codex服务");
+		}
+		
+		try {
+			// 调用Codex API获取用户密钥列表
+			String keysUrl = String.format(CODEX_API_PREFIX + "api/users/%s/keys", userId);
+			ClaudeCodeResp codexResp = executeCodexGetApi(keysUrl);
+			
+			if (codexResp == null) {
+				log.error("获取用户{}密钥列表失败: API返回null", userId);
+				throw new BusinessRuntimeException("获取用户密钥列表失败");
+			}
+			
+			log.info("成功获取用户{}密钥列表", userId);
+			return codexResp;
+			
+		} catch (Exception e) {
+			log.error("获取用户{}密钥列表异常, error={}", userId, StringUtil.getErrorText(e));
+			throw new BusinessRuntimeException("获取用户密钥列表失败");
+		}
+	}
+
+	@Override
+	public ClaudeCodeResp createUserKey(Long userId, String keyName) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+
+		if (codexUser == null) {
+			throw new BusinessRuntimeException("用户未开通Codex服务");
+		}
+		
+		try {
+			// 调用Codex API创建用户密钥
+			String createKeyUrl = String.format(CODEX_API_PREFIX + "api/users/%s/keys", userId);
+			Map<String, Object> params = new HashMap<>();
+			params.put("name", keyName);
+			
+			ClaudeCodeResp codexResp = executeCodexPostApi(createKeyUrl, Jsons.toJson(params));
+			
+			if (!"success".equals(codexResp.getMessage())) {
+				log.error("创建用户{}密钥失败: {}", userId, codexResp.getMessage());
+				throw new BusinessRuntimeException("创建用户密钥失败");
+			}
+			
+			log.info("成功创建用户{}密钥, keyName={}", userId, keyName);
+			return codexResp;
+			
+		} catch (Exception e) {
+			log.error("创建用户{}密钥异常, keyName={}, error={}", userId, keyName, StringUtil.getErrorText(e));
+			throw new BusinessRuntimeException("创建用户密钥失败");
+		}
+	}
+
+	@Override
+	public ClaudeCodeResp deleteUserKey(Long userId, String keyId) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+
+		if (codexUser == null) {
+			throw new BusinessRuntimeException("用户未开通Codex服务");
+		}
+		
+		try {
+			// 调用Codex API删除用户密钥
+			String deleteKeyUrl = String.format(CODEX_API_PREFIX + "api/users/%s/keys/%s", userId, keyId);
+			ClaudeCodeResp codexResp = executeCodexDeleteApi(deleteKeyUrl);
+			
+			if (!"success".equals(codexResp.getMessage())) {
+				log.error("删除用户{}密钥{}失败: {}", userId, keyId, codexResp.getMessage());
+				throw new BusinessRuntimeException("删除用户密钥失败");
+			}
+			
+			log.info("成功删除用户{}密钥{}", userId, keyId);
+			return codexResp;
+			
+		} catch (Exception e) {
+			log.error("删除用户{}密钥{}异常, error={}", userId, keyId, StringUtil.getErrorText(e));
+			throw new BusinessRuntimeException("删除用户密钥失败");
+		}
+	}
+
+	@Override
+	public ClaudeCodeResp getUserDashboard(Long userId) {
+		List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+		CodexUser codexUser = codexUserMapper.selectOne(Wrappers.lambdaQuery(CodexUser.class).in(CodexUser::getUserId, userIdList).last("limit 1"));
+
+		if (codexUser == null) {
+			throw new BusinessRuntimeException("用户未开通Codex服务");
+		}
+		
+		try {
+			// 调用Codex API获取用户仪表盘数据
+			String dashboardUrl = String.format(CODEX_API_PREFIX + "api/openai/users/%s/dashboard", userId);
+			ClaudeCodeResp codexResp = executeCodexGetApi(dashboardUrl);
+			
+			if (codexResp == null) {
+				log.error("获取用户{}仪表盘数据失败: API返回null", userId);
+				throw new BusinessRuntimeException("获取用户仪表盘数据失败");
+			}
+			
+			log.info("成功获取用户{}仪表盘数据", userId);
+			return codexResp;
+			
+		} catch (Exception e) {
+			log.error("获取用户{}仪表盘数据异常, error={}", userId, StringUtil.getErrorText(e));
+			throw new BusinessRuntimeException("获取用户仪表盘数据失败");
+		}
+	}
+
+	/**
+	 * Codex DELETE请求
+	 */
+	public ClaudeCodeResp executeCodexDeleteApi(String url) {
+		Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
+		try {
+			return build.call(() -> {
+				try {
+					HttpResponse execute = HttpUtil.createRequest(Method.DELETE, url)
+							.header(CODEX_HEARD_KEY, CODEX_API_ADMIN_KEY)
+							.setConnectionTimeout(CONNECT_MILLISECONDS)
+							.execute();
+					ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
+					if (!"success".equals(resp.getMessage())) {
+						log.error("codex DELETE URL:{}接口返回msg:{}", url, resp.getMessage());
+					}
+					return resp;
+				} catch (Exception e) {
+					log.error("codex DELETE请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
+					return null;
+				}
+			});
+		} catch (ExecutionException | com.github.rholder.retry.RetryException e) {
+			log.error("重试调用codex DELETE请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
+			throw BusinessRuntimeException.getInstance("删除操作失败");
+		}
+	}
+
+	/**
+	 * Codex GET请求
+	 */
+	public ClaudeCodeResp executeCodexGetApi(String url) {
+		Retryer<ClaudeCodeResp> build = getApiRetryer(1, 3);
+		try {
+			return build.call(() -> {
+				try {
+					HttpResponse execute = HttpUtil.createGet(url)
+							.header(CODEX_HEARD_KEY, CODEX_API_ADMIN_KEY)
+							.setConnectionTimeout(CONNECT_MILLISECONDS)
+							.execute();
+					ClaudeCodeResp resp = Jsons.parseObject(execute.body(), ClaudeCodeResp.class);
+					if (!"success".equals(resp.getMessage())) {
+						log.error("codex GET URL:{}接口返回msg:{}", url, resp.getMessage());
+					}
+					return resp;
+				} catch (Exception e) {
+					log.error("codex GET请求异常: url={}, error={}", url, StringUtil.getErrorText(e));
+					return null;
+				}
+			});
+		} catch (ExecutionException | com.github.rholder.retry.RetryException e) {
+			log.error("重试调用codex GET请求url->{}失败,msg->{}", url, StringUtil.getErrorText(e));
+			throw BusinessRuntimeException.getInstance("获取codex使用统计失败");
+		}
+	}
+}

+ 3 - 0
netflix-service/src/main/java/com/cyksj/service/mange/impl/CmsOrderDonServiceImpl.java

@@ -53,6 +53,7 @@ import com.cyksj.model.request.OrderRefundReq;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.chatgpt.GptUserRechargeRecordService;
 import com.cyksj.service.claude.ClaudeCodeService;
+import com.cyksj.service.codex.CodexService;
 import com.cyksj.service.coin.UserGalaxyCoinService;
 import com.cyksj.service.coupon.CouponFontService;
 import com.cyksj.service.distribute.equipment.EquipmentDistributeService;
@@ -231,6 +232,8 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
 
     private final ClaudeCodeService claudeCodeService;
 
+    private final CodexService codexService;
+
     private final NanoUserQuotaMapper nanoUserQuotaMapper;
 
     private final AccountNetflixFrozenTicketReceivedRecordMapper receivedRecordMapper;

+ 3 - 1
netflix-service/src/main/java/com/cyksj/service/mange/impl/MirrorUserCleatServiceImpl.java

@@ -66,6 +66,8 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 
 	private final OrderDonMapper orderDonMapper;
 
+	private final CodexUserMapper codexUserMapper;
+
 	/**
 	 * 删除镜像User
 	 */
@@ -206,7 +208,7 @@ public class MirrorUserCleatServiceImpl implements MirrorUserCleatService {
 			log.info("还原镜像claude code user套餐成功");
 			//调用api更新claude code用户信息
 			Date expiryTime = relation.getExpiryTime();
-			claudeCodeService.updateClaudeCodeUserPackageInfo(claudeCodeUser, expiryTime);
+			claudeCodeService.updateClaudeCodeUserPackageInfo(claudeCodeUser, expiryTime, finalSku);
 			return;
 		}
 		OrderDonRenewRecord orderDonRenewRecord = orderDonRenewRecordMapper.selectOne(Wrappers.lambdaQuery(OrderDonRenewRecord.class)

+ 4 - 0
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationClearServiceImpl.java

@@ -47,6 +47,8 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 
 	private final NanoUserQuotaMapper nanoUserQuotaMapper;
 
+	private final CodexUserMapper codexUserMapper;
+
 	@Override
 	public void clearTicket(GroupsRelation relation, UserTicketClearedRecord.Source source) {
 		Long relationId = relation.getId();
@@ -146,6 +148,8 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 					claudeCodeUserMapper.delete(Wrappers.lambdaQuery(ClaudeCodeUser.class).eq(ClaudeCodeUser::getRelationId, relationId));
 					//清除claude code tokens
 					oAuthTokenMapper.delete(Wrappers.lambdaQuery(OAuthToken.class).eq(OAuthToken::getUserId, userId));
+					//清除codex
+					codexUserMapper.delete(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId));
 				}
 				//nano
 				if (source != UserTicketClearedRecord.Source.vip_deduct && groupsRelation.getGoodsId() != null && groupsRelation.getGoodsId() == Constant.NANO_GOODS_ID) {

+ 119 - 0
netflix-web/src/main/java/com/cyksj/web/controller/codex/CodexController.java

@@ -0,0 +1,119 @@
+package com.cyksj.web.controller.codex;
+
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.request.UpdateDailyLimitRequest;
+import com.cyksj.model.request.codex.CreateKeyRequest;
+import com.cyksj.model.response.claudecode.ClaudeCodeResp;
+import com.cyksj.model.views.CodexUserInfoView;
+import com.cyksj.service.codex.CodexService;
+import com.cyksj.web.util.StpUserUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 项目名: yhlxj11111111
+ * 文件名: CodexController
+ * 创建者: Claude
+ * 创建时间:2025/9/15
+ */
+@Slf4j
+@RestController
+@RequestMapping("/applet/codex")
+@RequiredArgsConstructor
+public class CodexController {
+    
+    private final CodexService codexService;
+    
+    /**
+     * 获取用户Codex信息
+     */
+    @GetMapping("/info")
+    public Result<CodexUserInfoView> getCodexUserInfo() {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        CodexUserInfoView codexUserInfoView = codexService.getCodexUserInfo(userId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(codexUserInfoView);
+    }
+
+    /**
+     * 获取用户OpenAI使用统计
+     * period 时间周期(today/7d/30d)
+     */
+    @GetMapping("/users/usage")
+    public ClaudeCodeResp getUserOpenAIUsage(@RequestParam(defaultValue = "today") String period) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        ClaudeCodeResp claudeCodeResp = codexService.getUserOpenAIUsage(userId, period);
+        return claudeCodeResp;
+    }
+    
+    /**
+     * 更新用户OpenAI每日美金限额
+     */
+    @PostMapping("/users/daily-limit")
+    public Result<ClaudeCodeResp> updateUserOpenAIDailyLimit(@RequestBody UpdateDailyLimitRequest request) {
+        // 验证请求用户是否为当前登录用户或有权限访问
+        long userId = StpUserUtil.getLoginIdAsLong();
+        ClaudeCodeResp response = codexService.updateUserOpenAIDailyLimit(userId, request);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(response);
+    }
+    
+    /**
+     * 获取系统OpenAI指标统计
+     * period 时间周期(today/7d/30d/all)
+     * metric 指标类型(usage/cost/requests/errors/all)
+     */
+    @GetMapping("/api/openai/system/metrics")
+    public ClaudeCodeResp getSystemMetrics(@RequestParam(defaultValue = "today") String period,
+                                          @RequestParam(defaultValue = "all") String metric) {
+        // 系统级指标查询,通常需要管理员权限
+        // 这里可以添加权限验证逻辑
+        long currentUserId = StpUserUtil.getLoginIdAsLong();
+
+        ClaudeCodeResp response = codexService.getSystemMetrics(period, metric);
+        return response;
+    }
+    
+    /**
+     * 获取用户密钥列表
+     */
+    @GetMapping("/users/keys")
+    public Result<ClaudeCodeResp> getUserKeys() {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        
+        ClaudeCodeResp response = codexService.getUserKeys(userId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(response);
+    }
+    
+    /**
+     * 创建用户密钥
+     */
+    @PostMapping("/users/keys")
+    public Result<ClaudeCodeResp> createUserKey(@RequestBody CreateKeyRequest request) {
+        // 权限检查:只允许用户创建自己的密钥
+        long userId = StpUserUtil.getLoginIdAsLong();
+        ClaudeCodeResp response = codexService.createUserKey(userId, request.getKeyName());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(response);
+    }
+    
+    /**
+     * 删除用户密钥
+     */
+    @DeleteMapping("/users/keys/{keyId}")
+    public Result<ClaudeCodeResp> deleteUserKey(@PathVariable String keyId) {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        ClaudeCodeResp response = codexService.deleteUserKey(userId, keyId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(response);
+    }
+    
+    /**
+     * 获取用户仪表盘数据
+     * 返回用户的OpenAI使用概览、限额信息、最近活动等综合数据
+     */
+    @GetMapping("/api/openai/users/dashboard")
+    public Result<ClaudeCodeResp> getUserDashboard() {
+        long userId = StpUserUtil.getLoginIdAsLong();
+        ClaudeCodeResp response = codexService.getUserDashboard(userId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(response);
+    }
+}