Prechádzať zdrojové kódy

Merge branch 'nano_func'

# Conflicts:
#	netflix-common/src/main/java/com/cyksj/common/constant/Constant.java
#	netflix-service/src/main/java/com/cyksj/service/mange/impl/CmsOrderDonServiceImpl.java
zoujiajian 11 mesiacov pred
rodič
commit
83d61aa46c
25 zmenil súbory, kde vykonal 688 pridanie a 8 odobranie
  1. 10 0
      netflix-common/src/main/java/com/cyksj/common/constant/Constant.java
  2. 10 0
      netflix-dao/src/main/java/com/cyksj/mapper/NanoFileRecordMapper.java
  3. 10 0
      netflix-dao/src/main/java/com/cyksj/mapper/NanoImageHistoryMapper.java
  4. 10 0
      netflix-dao/src/main/java/com/cyksj/mapper/NanoRefundFailureRecordMapper.java
  5. 10 0
      netflix-dao/src/main/java/com/cyksj/mapper/NanoUserQuotaMapper.java
  6. 6 0
      netflix-dao/src/main/java/com/cyksj/model/entity/GoodsDonSku.java
  7. 59 0
      netflix-dao/src/main/java/com/cyksj/model/entity/NanoFileRecord.java
  8. 89 0
      netflix-dao/src/main/java/com/cyksj/model/entity/NanoImageHistory.java
  9. 79 0
      netflix-dao/src/main/java/com/cyksj/model/entity/NanoRefundFailureRecord.java
  10. 68 0
      netflix-dao/src/main/java/com/cyksj/model/entity/NanoUserQuota.java
  11. 8 0
      netflix-dao/src/main/java/com/cyksj/model/manage/request/ReqGoodsSkuInsert.java
  12. 5 0
      netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuFrontView.java
  13. 9 0
      netflix-dao/src/main/java/com/cyksj/model/views/VipGoodsSkuFrontView.java
  14. 18 0
      netflix-dao/src/main/java/com/cyksj/model/views/YhShopGoodsSkuFrontView.java
  15. 5 1
      netflix-service/src/main/java/com/cyksj/service/groups/impl/GroupFunServiceImpl.java
  16. 16 0
      netflix-service/src/main/java/com/cyksj/service/mange/impl/CmsOrderDonServiceImpl.java
  17. 25 2
      netflix-service/src/main/java/com/cyksj/service/mange/vip/impl/CmsVipOrderServiceImpl.java
  18. 41 0
      netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java
  19. 43 3
      netflix-service/src/main/java/com/cyksj/service/order/impl/VipOrderDonServiceImpl.java
  20. 12 0
      netflix-service/src/main/java/com/cyksj/service/quota/NanoUserQuotaService.java
  21. 108 0
      netflix-service/src/main/java/com/cyksj/service/quota/impl/NanoUserQuotaServiceImpl.java
  22. 6 0
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationClearServiceImpl.java
  23. 2 0
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java
  24. 2 0
      netflix-service/src/main/java/com/cyksj/service/vip/impl/VipFrontServiceImpl.java
  25. 37 2
      netflix-web/src/main/java/com/cyksj/web/controller/mirror/MirrorController.java

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

@@ -326,6 +326,11 @@ public interface Constant {
 
 	String CLAUDE_CODE_ONLY_ACCOUNT = "claudecode123";
 
+	/**
+	 * nano
+	 */
+	String NANO_ONLY_ACCOUNT = "nano123";
+
 	/**
 	 * 免登录渠道
 	 */
@@ -668,4 +673,9 @@ public interface Constant {
 	 * CLAUDE CODE 积分卡number
 	 */
 	String CLAUDE_CODE_CARD_NUMBER = "CODE_CARD_NUMBER_";
+
+	/**
+	 * nano 商品id
+	 */
+	Long NANO_GOODS_ID = 104L;
 }

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/mapper/NanoFileRecordMapper.java

@@ -0,0 +1,10 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.NanoFileRecord;
+
+/**
+ * 文件记录表 Mapper
+ */
+public interface NanoFileRecordMapper extends BaseMapper<NanoFileRecord> {
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/mapper/NanoImageHistoryMapper.java

@@ -0,0 +1,10 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.NanoImageHistory;
+
+/**
+ * 图像生成历史记录表 Mapper
+ */
+public interface NanoImageHistoryMapper extends BaseMapper<NanoImageHistory> {
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/mapper/NanoRefundFailureRecordMapper.java

@@ -0,0 +1,10 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.NanoRefundFailureRecord;
+
+/**
+ * 退还失败记录表 Mapper
+ */
+public interface NanoRefundFailureRecordMapper extends BaseMapper<NanoRefundFailureRecord> {
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/mapper/NanoUserQuotaMapper.java

@@ -0,0 +1,10 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.NanoUserQuota;
+
+/**
+ * 用户固定配额管理表 Mapper
+ */
+public interface NanoUserQuotaMapper extends BaseMapper<NanoUserQuota> {
+}

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

@@ -279,4 +279,10 @@ public class GoodsDonSku extends BaseEntity {
      */
     @TableField(updateStrategy = FieldStrategy.IGNORED)
     private Integer codeCreditRecovery;
+
+    /**
+     * nano总配额次数
+     */
+    @TableField(updateStrategy = FieldStrategy.IGNORED)
+    private Integer nanoQuota;
 }

+ 59 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/NanoFileRecord.java

@@ -0,0 +1,59 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 文件记录表
+ */
+@Getter
+@Setter
+@TableName("nano_file_record")
+public class NanoFileRecord extends BaseEntity {
+
+    /**
+     * 用户token
+     */
+    private Long userToken;
+
+    /**
+     * 文件MD5哈希
+     */
+    private String fileMd5;
+
+    /**
+     * 文件名
+     */
+    private String fileName;
+
+    /**
+     * 访问URL
+     */
+    private String url;
+
+    /**
+     * CDN访问URL
+     */
+    private String cdnUrl;
+
+    /**
+     * OSS内部URL
+     */
+    private String ossUrl;
+
+    /**
+     * 原始文件名
+     */
+    private String originalFileName;
+
+    /**
+     * 软删除时间
+     */
+    @TableLogic
+    private Date deletedAt;
+}

+ 89 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/NanoImageHistory.java

@@ -0,0 +1,89 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 图像生成历史记录表
+ */
+@Getter
+@Setter
+@TableName("nano_image_history")
+public class NanoImageHistory extends BaseEntity {
+
+    /**
+     * 任务ID
+     */
+    private String taskId;
+
+    /**
+     * 用户令牌
+     */
+    private String usertoken;
+
+    /**
+     * 用户ID(可选关联)
+     */
+    private Integer userId;
+
+    /**
+     * 生成提示词
+     */
+    private String prompt;
+
+    /**
+     * 图像尺寸
+     */
+    private String size;
+
+    /**
+     * 图像数量
+     */
+    private Integer num;
+
+    /**
+     * 任务状态
+     */
+    private String status;
+
+    /**
+     * JSON格式存储源图片URL(编辑模式)
+     */
+    private String imgs;
+
+    /**
+     * JSON格式存储结果图片URL
+     */
+    private String reulstImgs;
+
+    /**
+     * 错误信息
+     */
+    private String errMsg;
+
+    /**
+     * 任务类型:常规,主任务,子任务
+     */
+    private String taskType;
+
+    /**
+     * 主任务ID(子任务关联用)
+     */
+    private String masterTaskId;
+
+    /**
+     * 是否收藏
+     */
+    private Boolean isstarred;
+
+    /**
+     * 软删除时间
+     */
+    @TableLogic
+    private Date deletedAt;
+}

+ 79 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/NanoRefundFailureRecord.java

@@ -0,0 +1,79 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 退还失败记录表
+ */
+@Getter
+@Setter
+@TableName("nano_refund_failure_record")
+public class NanoRefundFailureRecord extends BaseEntity {
+
+    /**
+     * 任务ID
+     */
+    private String taskId;
+
+    /**
+     * 用户token
+     */
+    private String userToken;
+
+    /**
+     * 需要退还的次数
+     */
+    private Integer refundCount;
+
+    /**
+     * 退还类型(full_failure/partial_failure/timeout)
+     */
+    private String refundType;
+
+    /**
+     * 失败原因
+     */
+    private String failureReason;
+
+    /**
+     * 重试次数
+     */
+    private Integer retryCount;
+
+    /**
+     * 最大重试次数
+     */
+    private Integer maxRetryCount;
+
+    /**
+     * 状态(pending/processing/success/failed)
+     */
+    private String status;
+
+    /**
+     * 最后重试时间
+     */
+    private Date lastRetryTime;
+
+    /**
+     * 下次重试时间
+     */
+    private Date nextRetryTime;
+
+    /**
+     * 正在处理的实例ID
+     */
+    private String processingInstance;
+
+    /**
+     * 软删除时间
+     */
+    @TableLogic
+    private Date deletedAt;
+}

+ 68 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/NanoUserQuota.java

@@ -0,0 +1,68 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 用户固定配额管理表(包含完整用户信息)
+ */
+@Getter
+@Setter
+@TableName("nano_user_quota")
+public class NanoUserQuota extends BaseEntity {
+
+    /**
+     * 用户token
+     */
+    private String userToken;
+
+    /**
+     * 用户唯一标识
+     */
+    private Long uid;
+
+    private Long relationId;
+
+    /**
+     * 用户昵称
+     */
+    private String nickname;
+
+    /**
+     * 用户头像URL
+     */
+    private String avatar;
+
+    /**
+     * 套餐类型:1基础版,2标准版,3专业版,4银河大会员
+     */
+    private Integer packageType;
+
+    /**
+     * 总配额次数
+     */
+    private Integer totalQuota;
+
+    /**
+     * 已使用配额次数
+     */
+    private Integer usedQuota;
+
+    /**
+     * 剩余配额次数
+     */
+    private Integer remainingQuota;
+
+    /**
+     * 购买日期
+     */
+    private Date purchaseDate;
+
+    /**
+     * 最后使用日期
+     */
+    private Date lastUsedDate;
+}

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

@@ -1,5 +1,7 @@
 package com.cyksj.model.manage.request;
 
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.cyksj.model.entity.GoodsDiscountPlusPurchaseRelation;
 import lombok.Getter;
 import lombok.Setter;
@@ -198,6 +200,12 @@ public class ReqGoodsSkuInsert {
          * claude code每小时恢复积分
          */
         private Integer codeCreditRecovery;
+
+        /**
+         * nano总配额次数
+         */
+        @TableField(updateStrategy = FieldStrategy.IGNORED)
+        private Integer nanoQuota;
     }
 
 }

+ 5 - 0
netflix-dao/src/main/java/com/cyksj/model/views/GoodsDonSkuFrontView.java

@@ -236,4 +236,9 @@ public class GoodsDonSkuFrontView {
 	 * claude code每小时恢复积分
 	 */
 	private Integer codeCreditRecovery;
+
+	/**
+	 * nano总配额次数
+	 */
+	private Integer nanoQuota;
 }

+ 9 - 0
netflix-dao/src/main/java/com/cyksj/model/views/VipGoodsSkuFrontView.java

@@ -22,6 +22,9 @@ public class VipGoodsSkuFrontView {
 	@DbField("vgs.goods_id")
 	private Long goodsId;
 
+	@DbField("g.logo")
+	private String logo;
+
 	@DbField("g.title")
 	private String title;
 
@@ -91,4 +94,10 @@ public class VipGoodsSkuFrontView {
 	 */
 	@DbField("sku.other_limit_time")
 	private Long otherLimitTime;
+
+	/**
+	 * nano总次数
+	 */
+	@DbField("sku.nano_quota")
+	private Integer nanoQuota;
 }

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/model/views/YhShopGoodsSkuFrontView.java

@@ -184,4 +184,22 @@ public class YhShopGoodsSkuFrontView {
 
 	@DbField("sku.remark")
 	private Boolean remark;
+
+	/**
+	 * claude code积分上限
+	 */
+	@DbField("sku.code_points")
+	private Integer codePoints;
+
+	/**
+	 * claude code每小时恢复积分
+	 */
+	@DbField("sku.code_credit_recovery")
+	private Integer codeCreditRecovery;
+
+	/**
+	 * nano总配额次数
+	 */
+	@DbField("sku.nano_quota")
+	private Integer nanoQuota;
 }

+ 5 - 1
netflix-service/src/main/java/com/cyksj/service/groups/impl/GroupFunServiceImpl.java

@@ -100,7 +100,7 @@ public class GroupFunServiceImpl implements GroupsFuncService {
 		Boolean isMirror = sku.getIsMirror();
 		Boolean isCar = sku.getIsCar();
 		String account = null;
-		if ((goodsId == Constant.CLAUDE_CODE_GOODS_ID) || (goodsId == 88 && isMirror) || (goodsId == 87 && isMirror) || (goodsId == 18 && isCar) || (goodsId == 26 && isMirror) || (goodsId == 75 && isCar)) {
+		if ((goodsId == Constant.NANO_GOODS_ID) || (goodsId == Constant.CLAUDE_CODE_GOODS_ID) || (goodsId == 88 && isMirror) || (goodsId == 87 && isMirror) || (goodsId == 18 && isCar) || (goodsId == 26 && isMirror) || (goodsId == 75 && isCar)) {
 			if (goodsId == 18) {
 				account = Constant.GPT_CAR_ONLY_ACCOUNT;
 			}
@@ -120,6 +120,10 @@ public class GroupFunServiceImpl implements GroupsFuncService {
 			if (goodsId == Constant.CLAUDE_CODE_GOODS_ID) {
 				account = Constant.CLAUDE_CODE_ONLY_ACCOUNT;
 			}
+			//nano code
+			if (goodsId == Constant.NANO_GOODS_ID) {
+				account = Constant.NANO_ONLY_ACCOUNT;
+			}
 			AccountView accountView = beanSearcher.searchFirst(AccountView.class, MapUtils.builder()
 					.field(AccountView::getGoodsId, goodsId)
 					.field(AccountView::getAccount, account).

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

@@ -231,6 +231,8 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
 
     private final ClaudeCodeService claudeCodeService;
 
+    private final NanoUserQuotaMapper nanoUserQuotaMapper;
+
     private static final GlobalThreadPoolTaskExecutor TASK_POOL = GlobalThreadPoolTaskExecutor.getInstance();
 
     @Override
@@ -482,6 +484,20 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
             }
         }
 
+        //扣除nano套餐次数
+        if (upgradePackage.getGoodsId() == Constant.NANO_GOODS_ID) {
+            NanoUserQuota nanoUserQuota = nanoUserQuotaMapper.selectOne(Wrappers.lambdaQuery(NanoUserQuota.class).eq(NanoUserQuota::getRelationId, orderDon.getRelationId()).last("limit 1"));
+            if (nanoUserQuota != null) {
+                int update = nanoUserQuotaMapper.update(null, Wrappers.lambdaUpdate(NanoUserQuota.class)
+                        .set(NanoUserQuota::getRemainingQuota, nanoUserQuota.getRemainingQuota() < upgradePackage.getNum() ? 0 : nanoUserQuota.getRemainingQuota() - upgradePackage.getNum())
+                        .set(NanoUserQuota::getTotalQuota, nanoUserQuota.getTotalQuota() <= upgradePackage.getNum() ? nanoUserQuota.getTotalQuota() : nanoUserQuota.getTotalQuota() - upgradePackage.getNum())
+                        .eq(NanoUserQuota::getId, nanoUserQuota.getId()));
+                if (update == 0) {
+                    log.info("nano user 套餐包增值失败 id:{},orderId:{}", nanoUserQuota.getId(), orderDon.getId());
+                }
+            }
+        }
+
         //扣除gpt套餐升级
         if (upgradePackage.getGoodsId() == 18) {
             ChatgptUser chatgptUser = chatgptUserMapper.selectOne(Wrappers.lambdaQuery(ChatgptUser.class).eq(ChatgptUser::getRelationId, orderDon.getRelationId()));

+ 25 - 2
netflix-service/src/main/java/com/cyksj/service/mange/vip/impl/CmsVipOrderServiceImpl.java

@@ -4,16 +4,19 @@ 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.mapper.GoodsDonSkuMapper;
 import com.cyksj.mapper.GroupsRelationMapper;
+import com.cyksj.mapper.NanoUserQuotaMapper;
 import com.cyksj.mapper.manage.cms.CmsUserMapper;
-import com.cyksj.mapper.OrderDonGalaxyCoinRecordMapper;
 import com.cyksj.mapper.manage.distribute.DistributeWaitingSendPointsMapper;
 import com.cyksj.mapper.manage.distribute.UserDistributeMapper;
 import com.cyksj.mapper.vip.RefundVipOrderDonMapper;
 import com.cyksj.mapper.vip.VipOrderDonMapper;
 import com.cyksj.mapper.vip.VipUserMapper;
 import com.cyksj.model.entity.*;
+import com.cyksj.model.manage.views.GroupsRelationView;
 import com.cyksj.model.request.OrderRefundReq;
 import com.cyksj.service.coin.UserGalaxyCoinService;
 import com.cyksj.service.mange.vip.CmsVipOrderService;
@@ -21,6 +24,9 @@ import com.cyksj.service.order.OrderRefundService;
 import com.cyksj.service.relation.GroupRelationClearService;
 import com.cyksj.service.user.UserBenefitsService;
 import com.cyksj.service.user.UserBindRelationService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
@@ -61,10 +67,14 @@ public class CmsVipOrderServiceImpl implements CmsVipOrderService {
 
 	private final CmsUserMapper cmsUserMapper;
 
-	private final OrderDonGalaxyCoinRecordMapper orderDonGalaxyCoinRecordMapper;
+	private final NanoUserQuotaMapper nanoUserQuotaMapper;
 
 	private final UserGalaxyCoinService userGalaxyCoinService;
 
+	private final BeanSearcher beanSearcher;
+
+	private final GoodsDonSkuMapper skuMapper;
+
 	@Override
 	public void refundOrders(OrderRefundReq refundReq) throws Exception {
 		VipOrderDon orderDon = vipOrderDonMapper.selectById(refundReq.getOrderId());
@@ -163,6 +173,19 @@ public class CmsVipOrderServiceImpl implements CmsVipOrderService {
 			//分销提成积分扣除
 			deductVipDsPoints(orderDon);
 		}
+
+		GroupsRelationView groupsRelationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder().field(GroupsRelationView::getGoodsId, Constant.NANO_GOODS_ID).field(GroupsRelationView::getUserId, userIdList).op(Operator.InList).field(GroupsRelationView::getIsVip, Boolean.TRUE).build());
+		if (groupsRelationView != null) {
+			Long relationId = groupsRelationView.getId();
+			NanoUserQuota nanoUserQuota = nanoUserQuotaMapper.selectOne(Wrappers.lambdaQuery(NanoUserQuota.class).eq(NanoUserQuota::getRelationId, relationId).last("limit 1"));
+			if (nanoUserQuota != null) {
+				GoodsDonSku sku = skuMapper.selectById(groupsRelationView.getSkuId());
+				Integer offsetNum = orderDon.getNum() * sku.getNanoQuota();
+				nanoUserQuota.setTotalQuota(nanoUserQuota.getTotalQuota() <= offsetNum ? nanoUserQuota.getTotalQuota() : nanoUserQuota.getTotalQuota() - offsetNum);
+				nanoUserQuota.setRemainingQuota(nanoUserQuota.getRemainingQuota() < offsetNum ? 0 : nanoUserQuota.getRemainingQuota() - offsetNum);
+				nanoUserQuotaMapper.updateById(nanoUserQuota);
+			}
+		}
 	}
 
 	private void deductVipDsPoints(VipOrderDon orderDon) {

+ 41 - 0
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java

@@ -6,6 +6,7 @@ import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUnit;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.lang.UUID;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.RandomUtil;
 import cn.hutool.core.util.StrUtil;
@@ -390,6 +391,8 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 
 	private final ClaudeCodeUserMapper claudeCodeUserMapper;
 
+	private final NanoUserQuotaMapper nanoUserQuotaMapper;
+
 	private final List<String> noChekGoodsTemp = List.of("Apple One", "Youtube", "Spotify");
 
 	@Transactional(rollbackFor = Throwable.class)
@@ -1878,10 +1881,34 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 					} catch (Exception e) {
 					}
 				}
+
+
+				if (order.getOrderType() == 1 && order.getGoodsId() == Constant.NANO_GOODS_ID) {
+					//创建nano user
+					createNanoUser(order.getUserId(), order.getRelationId(), sku);
+				}
 			}
 		}
 	}
 
+	private void createNanoUser(Long userId, Long relationId, GoodsDonSku sku) {
+		try {
+			User user = userMapper.selectById(userId);
+			NanoUserQuota nanoUserQuota = new NanoUserQuota();
+			nanoUserQuota.setUid(userId);
+			nanoUserQuota.setRelationId(relationId);
+			nanoUserQuota.setUserToken(UUID.randomUUID().toString());
+			nanoUserQuota.setNickname(user.getNickname());
+			nanoUserQuota.setAvatar(user.getHeadimgurl());
+			nanoUserQuota.setPurchaseDate(DateTime.now());
+			nanoUserQuota.setTotalQuota(sku.getNanoQuota());
+			nanoUserQuota.setRemainingQuota(nanoUserQuota.getTotalQuota());
+			nanoUserQuotaMapper.insert(nanoUserQuota);
+		} catch (DuplicateKeyException e) {
+
+		}
+	}
+
 	/**
 	 * 分销待发积分记录
 	 */
@@ -4328,6 +4355,20 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 			}
 		}
 
+		//nano
+		if (upgradePackage.getGoodsId() == Constant.NANO_GOODS_ID) {
+			NanoUserQuota nanoUserQuota = nanoUserQuotaMapper.selectOne(Wrappers.lambdaQuery(NanoUserQuota.class).eq(NanoUserQuota::getRelationId, orderDon.getRelationId()));
+			if (nanoUserQuota != null) {
+				int update = nanoUserQuotaMapper.update(null, Wrappers.lambdaUpdate(NanoUserQuota.class)
+						.set(NanoUserQuota::getRemainingQuota, nanoUserQuota.getRemainingQuota() + upgradePackage.getNum())
+						.set(NanoUserQuota::getTotalQuota, nanoUserQuota.getTotalQuota() + upgradePackage.getNum())
+						.eq(NanoUserQuota::getId, nanoUserQuota.getId()));
+				if (update == 0) {
+					log.info("nano user 套餐包增值失败 id:{},orderId:{}", nanoUserQuota.getId(), orderDon.getId());
+				}
+			}
+		}
+
 		//银河币返回
 		returnUpgradeGalaxyCoin(orderDon);
 		//查看该订单是否有关闭的余额

+ 43 - 3
netflix-service/src/main/java/com/cyksj/service/order/impl/VipOrderDonServiceImpl.java

@@ -3,6 +3,7 @@ package com.cyksj.service.order.impl;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.lang.UUID;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.alipay.api.AlipayApiException;
@@ -152,6 +153,8 @@ public class VipOrderDonServiceImpl extends ServiceImpl<VipOrderDonMapper, VipOr
 
 	private final UserBalanceSourceRecordMapper userBalanceSourceRecordMapper;
 
+	private final NanoUserQuotaMapper nanoUserQuotaMapper;
+
 	private static final Long EXPIRY_TIME = 1000 * 60 * 5L;
 
 	@Override
@@ -446,13 +449,27 @@ public class VipOrderDonServiceImpl extends ServiceImpl<VipOrderDonMapper, VipOr
 				}
 			} else vipUserMapper.updateById(vipUser);
 			//发送车票
-			sendVipTicketToUser(vipUser.getUserId(), vipUser.getStartTime(), vipUser.getExpiryTime(), userIdList, orderDon.getId());
+			sendVipTicketToUser(orderDon.getNum(), vipUser.getUserId(), vipUser.getStartTime(), vipUser.getExpiryTime(), userIdList, orderDon.getId());
 		} else {
 			//续费订单
 			vipUser.setExpiryTime(DateUtil.offsetMonth(vipUser.getExpiryTime(), offsetMonths));
 			vipUserMapper.updateById(vipUser);
 			//续费订单
 			setTicketToVipExpiryTime(userIdList, vipUser.getUserId(), vipUser.getExpiryTime());
+
+			//增加nano剩余次数
+			GroupsRelationView groupsRelationView = beanSearcher.searchFirst(GroupsRelationView.class, MapUtils.builder().field(GroupsRelationView::getGoodsId, Constant.NANO_GOODS_ID).field(GroupsRelationView::getUserId, userIdList).op(Operator.InList).field(GroupsRelationView::getIsVip, Boolean.TRUE).build());
+			if (groupsRelationView != null) {
+				NanoUserQuota nanoUserQuota = nanoUserQuotaMapper.selectOne(Wrappers.lambdaQuery(NanoUserQuota.class).eq(NanoUserQuota::getRelationId, groupsRelationView.getId()).last("limit 1"));
+				if (nanoUserQuota != null) {
+					Long skuId = groupsRelationView.getSkuId();
+					GoodsDonSku sku = skuMapper.selectById(skuId);
+					Integer offestNum = offsetMonths * sku.getNanoQuota();
+					nanoUserQuota.setTotalQuota(nanoUserQuota.getTotalQuota() + offestNum);
+					nanoUserQuota.setRemainingQuota(nanoUserQuota.getRemainingQuota() + offestNum);
+					nanoUserQuotaMapper.updateById(nanoUserQuota);
+				}
+			}
 		}
 		//金额大于0 是否是分销订单
 		if (orderDon.getMoney().compareTo(BigDecimal.ZERO) > 0) {
@@ -466,7 +483,7 @@ public class VipOrderDonServiceImpl extends ServiceImpl<VipOrderDonMapper, VipOr
 		deductIfCloseOrderReturnBalance(orderDon.getId(), UserBalanceSourceRecord.Source.vip_order_close);
 	}
 
-	private void sendVipTicketToUser(Long userId, Date vipStartTime, Date vipExpiryTime, List<Long> userIds, Long orderId) {
+	private void sendVipTicketToUser(Integer num, Long userId, Date vipStartTime, Date vipExpiryTime, List<Long> userIds, Long orderId) {
 		//是否存在抵扣 抵扣成功 清除车票
 		VipOrderDonDeductRelation vipOrderDonDeductRelation = vipOrderDonDeductRelationMapper.selectOne(Wrappers.lambdaQuery(VipOrderDonDeductRelation.class)
 				.eq(VipOrderDonDeductRelation::getOrderId, orderId)
@@ -525,7 +542,12 @@ public class VipOrderDonServiceImpl extends ServiceImpl<VipOrderDonMapper, VipOr
 		for (VipGoodsSku sku : skus) {
 			//未有抵扣 直接发送会员车票
 			if (deductSkuIds == null || !deductSkuIds.contains(sku.getSkuId())) {
-				groupRelationFrontService.getTicket(userId, sku.getSkuId(), Boolean.TRUE);
+				Long relationId = groupRelationFrontService.getTicket(userId, sku.getSkuId(), Boolean.TRUE);
+				if (relationId != null && sku.getGoodsId() == Constant.NANO_GOODS_ID) {
+					User user = userMapper.selectById(userId);
+					GoodsDonSku goodsDonSku = skuMapper.selectById(sku.getSkuId());
+					createNanoUser(relationId, user, goodsDonSku.getNanoQuota() * num);
+				}
 			}
 		}
 	}
@@ -954,4 +976,22 @@ public class VipOrderDonServiceImpl extends ServiceImpl<VipOrderDonMapper, VipOr
 			userBalanceSourceRecordMapper.deleteById(userBalanceSourceRecord.getId());
 		}
 	}
+
+	private NanoUserQuota createNanoUser(Long relationId, User user, Integer nanoQuota) {
+		NanoUserQuota nanoUserQuota = new NanoUserQuota();
+		nanoUserQuota.setUid(user.getId());
+		nanoUserQuota.setRelationId(relationId);
+		nanoUserQuota.setUserToken(UUID.randomUUID().toString());
+		nanoUserQuota.setNickname(user.getNickname());
+		nanoUserQuota.setAvatar(user.getHeadimgurl());
+		nanoUserQuota.setPurchaseDate(DateTime.now());
+		nanoUserQuota.setTotalQuota(nanoQuota);
+		nanoUserQuota.setRemainingQuota(nanoUserQuota.getTotalQuota());
+		try {
+			nanoUserQuotaMapper.insert(nanoUserQuota);
+		} catch (DuplicateKeyException e) {
+
+		}
+		return nanoUserQuota;
+	}
 }

+ 12 - 0
netflix-service/src/main/java/com/cyksj/service/quota/NanoUserQuotaService.java

@@ -0,0 +1,12 @@
+package com.cyksj.service.quota;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.model.entity.NanoUserQuota;
+
+/**
+ * 用户固定配额管理服务接口
+ */
+public interface NanoUserQuotaService extends IService<NanoUserQuota> {
+
+	NanoUserQuota getNanoUser(Long userId, Long relationId);
+}

+ 108 - 0
netflix-service/src/main/java/com/cyksj/service/quota/impl/NanoUserQuotaServiceImpl.java

@@ -0,0 +1,108 @@
+package com.cyksj.service.quota.impl;
+
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.lang.UUID;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.common.constant.Constant;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.mapper.*;
+import com.cyksj.model.entity.*;
+import com.cyksj.service.quota.NanoUserQuotaService;
+import com.cyksj.service.user.UserBindRelationService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 用户固定配额管理服务实现类
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class NanoUserQuotaServiceImpl extends ServiceImpl<NanoUserQuotaMapper, NanoUserQuota> implements NanoUserQuotaService {
+    private final UserBindRelationService userBindRelationService;
+
+    private final GroupsRelationMapper groupsRelationMapper;
+
+    private final GroupsMapper groupsMapper;
+
+    private final UserMapper userMapper;
+
+    private final GoodsDonSkuMapper skuMapper;
+
+    @Override
+    public NanoUserQuota getNanoUser(Long userId, Long relationId) {
+        GroupsRelation groupsRelation = getGroupsRelation(userId, relationId);
+        GroupsTrips groupsTrips = getGroupsTrips(groupsRelation);
+        GoodsDonSku goodsDonSku = skuMapper.selectById(groupsTrips.getSkuId());
+        if (goodsDonSku != null && goodsDonSku.getGoodsId() == Constant.NANO_GOODS_ID) {
+            return getNanoUser(userId, relationId, groupsRelation, goodsDonSku);
+        } else {
+            throw BusinessRuntimeException.getInstance("服务器出了点问题");
+        }
+    }
+
+    private GroupsRelation getGroupsRelation(Long userId, Long relationId) {
+        List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
+        GroupsRelation groupsRelation = groupsRelationMapper.selectOne(Wrappers.lambdaQuery(GroupsRelation.class).in(GroupsRelation::getUserId, userIdList).eq(GroupsRelation::getId, relationId));
+        if (groupsRelation == null) {
+            throw BusinessRuntimeException.getInstance("车票不存在");
+        }
+        return groupsRelation;
+    }
+
+    /**
+     * 获取车队信息
+     *
+     * @param groupsRelation
+     * @return
+     */
+    private GroupsTrips getGroupsTrips(GroupsRelation groupsRelation) {
+        GroupsTrips groupsTrips = groupsMapper.selectById(groupsRelation.getGroupsId());
+        if (groupsTrips == null) {
+            throw BusinessRuntimeException.getInstance("车队异常");
+        }
+        return groupsTrips;
+    }
+
+    private NanoUserQuota getNanoUser(Long userId, Long relationId, GroupsRelation groupsRelation, GoodsDonSku goodsDonSku) {
+        NanoUserQuota nanoUser = getOne(Wrappers.lambdaQuery(NanoUserQuota.class).eq(NanoUserQuota::getRelationId, relationId));
+        User user = userMapper.selectById(userId);
+        if (nanoUser == null) {
+            nanoUser = createNanoUser(groupsRelation, user, goodsDonSku);
+        } else {
+            updateLumaUser(user, nanoUser);
+        }
+        return nanoUser;
+    }
+
+
+    private NanoUserQuota createNanoUser(GroupsRelation groupsRelation, User user, GoodsDonSku sku) {
+        NanoUserQuota nanoUserQuota = new NanoUserQuota();
+        nanoUserQuota.setUid(user.getId());
+        nanoUserQuota.setRelationId(groupsRelation.getId());
+        nanoUserQuota.setUserToken(UUID.randomUUID().toString());
+        nanoUserQuota.setNickname(user.getNickname());
+        nanoUserQuota.setAvatar(user.getHeadimgurl());
+        nanoUserQuota.setPurchaseDate(DateTime.now());
+        nanoUserQuota.setTotalQuota(sku.getNanoQuota());
+        nanoUserQuota.setRemainingQuota(nanoUserQuota.getTotalQuota());
+        try {
+            save(nanoUserQuota);
+        } catch (DuplicateKeyException e) {
+
+        }
+        return nanoUserQuota;
+    }
+
+    private void updateLumaUser(User user, NanoUserQuota nanoUserQuota) {
+        nanoUserQuota.setNickname(user.getNickname());
+        nanoUserQuota.setAvatar(user.getHeadimgurl());
+        updateById(nanoUserQuota);
+    }
+
+}

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

@@ -45,6 +45,8 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 
 	private final OAuthTokenMapper oAuthTokenMapper;
 
+	private final NanoUserQuotaMapper nanoUserQuotaMapper;
+
 	@Override
 	public void clearTicket(GroupsRelation relation, UserTicketClearedRecord.Source source) {
 		Long relationId = relation.getId();
@@ -145,6 +147,10 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 					//清除claude code tokens
 					oAuthTokenMapper.delete(Wrappers.lambdaQuery(OAuthToken.class).eq(OAuthToken::getUserId, userId));
 				}
+				//nano
+				if (groupsRelation.getGoodsId() != null && groupsRelation.getGoodsId() == Constant.NANO_GOODS_ID) {
+					nanoUserQuotaMapper.delete(Wrappers.lambdaQuery(NanoUserQuota.class).eq(NanoUserQuota::getRelationId, relationId));
+				}
 			}
 		}
 	}

+ 2 - 0
netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java

@@ -2155,6 +2155,8 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 	 * 标记是否可抵扣会员车票
 	 */
 	private void markVipDeductTicket(List<Long> userIds, RenewalView ticket, Set<Long> vipGoodsIds, Boolean isVip) {
+		//nano 先不参与抵扣
+		vipGoodsIds.remove(Constant.NANO_GOODS_ID);
 		if (CollUtil.isNotEmpty(vipGoodsIds) && ticket.getIsMirror() != null && ticket.getIsMirror() && !isVip) {
 			Long goodsId = ticket.getGoodsId();
 			//只购买

+ 2 - 0
netflix-service/src/main/java/com/cyksj/service/vip/impl/VipFrontServiceImpl.java

@@ -82,6 +82,8 @@ public class VipFrontServiceImpl implements VipFrontService {
 		//会员规格
 		Set<Long> goodsIds = goodsDonSkuMapper.selectList(Wrappers.lambdaQuery(GoodsDonSku.class)
 				.in(GoodsDonSku::getId, vipSkuIds)).stream().map(GoodsDonSku::getGoodsId).collect(Collectors.toSet());
+		//todo 临时处理 nano先不参与抵扣
+		goodsIds.remove(Constant.NANO_GOODS_ID);
 		Number count = beanSearcher.searchCount(GroupsRelationView.class, MapUtils.builder().field(GroupsRelationView::getUserId, userIdList).op(Operator.InList).field(GroupsRelationView::getGoodsId, goodsIds).op(Operator.InList).field(GroupsRelationView::getIsMirror, Boolean.TRUE).field(GroupsRelationView::getExpiryTime, now).op(Operator.GreaterThan).build());
 		//可抵扣车票
 		if (count.intValue() > 0) {

+ 37 - 2
netflix-web/src/main/java/com/cyksj/web/controller/mirror/MirrorController.java

@@ -16,7 +16,6 @@ import com.cyksj.enums.GatewayResponse;
 import com.cyksj.mapper.GroupsMapper;
 import com.cyksj.mapper.GroupsRelationMapper;
 import com.cyksj.mapper.WebInfoMapper;
-import com.cyksj.mapper.WholesaleUserMapper;
 import com.cyksj.mapper.renew.RenewUpgradePackageMapper;
 import com.cyksj.model.dto.ChatgptUserVerifyDto;
 import com.cyksj.model.entity.*;
@@ -26,6 +25,7 @@ import com.cyksj.service.claude.ClaudeService;
 import com.cyksj.service.grok.GrokService;
 import com.cyksj.service.luma.LumaService;
 import com.cyksj.service.midjourney.MidjourneyAccountService;
+import com.cyksj.service.quota.NanoUserQuotaService;
 import com.cyksj.service.sys.SysConfigService;
 import com.cyksj.web.util.StpUserUtil;
 import com.ejlchina.searcher.BeanSearcher;
@@ -100,7 +100,7 @@ public class MirrorController {
 
     private final GrokService grokService;
 
-    private final WholesaleUserMapper wholesaleUserMapper;
+    private final NanoUserQuotaService nanoUserQuotaService;
 
     private final WebInfoMapper webInfoMapper;
 
@@ -628,4 +628,39 @@ public class MirrorController {
         return GatewayResponse.SUCCESS.newBuilder().toResult(webInfo);
     }
 
+    /**
+     * nano车票跳转登录
+     */
+    @GetMapping("/nanoUser/info")
+    public Result<NanoUserQuota> nanoUserInfo(Long relationId) {
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        NanoUserQuota nanoUserQuota = nanoUserQuotaService.getNanoUser(userId, relationId);
+        return GatewayResponse.SUCCESS.newBuilder().toResult(nanoUserQuota);
+    }
+
+    /**
+     * nano车票跳转登录
+     */
+    @GetMapping("/nanoUser/{relationId}")
+    public Result<String> nanoUserWithTokenRedirect(String domain, @PathVariable Long relationId) throws IOException {
+        Long userId = StpUserUtil.getLoginIdAsLong();
+        NanoUserQuota nanoUserQuota = nanoUserQuotaService.getNanoUser(userId, relationId);
+        String DOMAIN = domain;
+        if (StrUtil.isEmpty(DOMAIN)) {
+            try {
+                SysConfig sysConfig = sysConfigService.getOne(Wrappers.lambdaQuery(SysConfig.class).eq(SysConfig::getSysKey, "mirror_nano_domain"));
+                if (sysConfig != null) {
+                    String sysValue = sysConfig.getSysValue();
+                    if (JSONUtil.isJsonArray(sysValue)) {
+                        List<String> domainList = JSONUtil.parseArray(sysValue).toList(String.class);
+                        DOMAIN = domainList.get(RandomUtil.randomInt(domainList.size()));
+                    }
+                }
+            } catch (Exception e) {
+                log.error("nano随机抽取域名错误. msg:{}", StringUtil.getErrorText(e));
+            }
+        }
+        return GatewayResponse.SUCCESS.newBuilder().toResult((DOMAIN + "/login?usertoken=" + nanoUserQuota.getUserToken()));
+    }
+
 }