Browse Source

Merge branch 'refs/heads/AI_model' into pre

# Conflicts:
#	netflix-common/src/main/java/com/cyksj/common/constant/Constant.java
#	netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java
zoujiajian 3 tháng trước cách đây
mục cha
commit
4dd97a2688
21 tập tin đã thay đổi với 731 bổ sung8 xóa
  1. 9 0
      netflix-dao/src/main/java/com/cyksj/mapper/AiModelMapper.java
  2. 7 0
      netflix-dao/src/main/java/com/cyksj/mapper/enterprise/EnterprisePurchaseLeadMapper.java
  3. 50 0
      netflix-dao/src/main/java/com/cyksj/model/entity/AiModel.java
  4. 33 0
      netflix-dao/src/main/java/com/cyksj/model/entity/EnterprisePurchaseLead.java
  5. 18 0
      netflix-dao/src/main/java/com/cyksj/model/request/EnterprisePurchaseLeadRemarkReq.java
  6. 18 0
      netflix-dao/src/main/java/com/cyksj/model/request/EnterprisePurchaseLeadStatusReq.java
  7. 40 0
      netflix-dao/src/main/java/com/cyksj/model/request/EnterprisePurchaseLeadSubmitReq.java
  8. 43 0
      netflix-dao/src/main/java/com/cyksj/model/views/AiModelGroupView.java
  9. 34 0
      netflix-dao/src/main/java/com/cyksj/model/views/EnterprisePurchaseLeadView.java
  10. 13 0
      netflix-service/src/main/java/com/cyksj/service/aimodel/AiModelService.java
  11. 73 0
      netflix-service/src/main/java/com/cyksj/service/aimodel/impl/AiModelServiceImpl.java
  12. 20 0
      netflix-service/src/main/java/com/cyksj/service/enterprise/EnterprisePurchaseLeadService.java
  13. 154 0
      netflix-service/src/main/java/com/cyksj/service/enterprise/impl/EnterprisePurchaseLeadServiceImpl.java
  14. 1 1
      netflix-service/src/main/java/com/cyksj/service/order/impl/OrderDonServiceImpl.java
  15. 4 2
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationClearServiceImpl.java
  16. 2 2
      netflix-service/src/main/java/com/cyksj/service/relation/impl/GroupRelationFrontServiceImpl.java
  17. 31 0
      netflix-web/src/main/java/com/cyksj/web/controller/aimodel/AiModelFrontController.java
  18. 43 0
      netflix-web/src/main/java/com/cyksj/web/controller/enterprise/EnterprisePurchaseLeadFrontController.java
  19. 87 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/aimodel/CmsAiModelController.java
  20. 46 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/enterprise/CmsEnterprisePurchaseLeadController.java
  21. 5 3
      netflix-web/src/main/java/com/cyksj/web/controller/manage/group/CmsGroupRelationController.java

+ 9 - 0
netflix-dao/src/main/java/com/cyksj/mapper/AiModelMapper.java

@@ -0,0 +1,9 @@
+package com.cyksj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.AiModel;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AiModelMapper extends BaseMapper<AiModel> {
+}

+ 7 - 0
netflix-dao/src/main/java/com/cyksj/mapper/enterprise/EnterprisePurchaseLeadMapper.java

@@ -0,0 +1,7 @@
+package com.cyksj.mapper.enterprise;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.EnterprisePurchaseLead;
+
+public interface EnterprisePurchaseLeadMapper extends BaseMapper<EnterprisePurchaseLead> {
+}

+ 50 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/AiModel.java

@@ -0,0 +1,50 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+/**
+ * AI 模型配置
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+public class AiModel extends BaseEntity {
+
+    /** 提供商标识,如 ANTHROPIC、OPENAI */
+    private String provider;
+
+    /** 提供商展示名称,如 Anthropic、OpenAI */
+    private String providerName;
+
+    /** 提供商 logo 图片地址 */
+    private String providerLogo;
+
+    /** 模型唯一标识,如 claude-opus-4-5 */
+    private String modelCode;
+
+    /** 模型展示名称 */
+    private String modelName;
+
+    /** 模型图标地址 */
+    private String modelIcon;
+
+    /** 模型描述 */
+    private String description;
+
+    /** 功能标签,逗号分隔,如 支持推理,支持工具 */
+    private String tags;
+
+    /** 上下文窗口,如 200K、100K */
+    private String contextWindow;
+
+    /** 价格档位标签,如 多档、商品级品质、专门优化 */
+    private String pricingBadge;
+
+    /** 排序值,越小越靠前 */
+    private Integer sort;
+
+    /** 状态:0-禁用,1-启用 */
+    private Integer status;
+}

+ 33 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/EnterprisePurchaseLead.java

@@ -0,0 +1,33 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+@Getter
+@Setter
+@Accessors(chain = true)
+@TableName("enterprise_purchase_lead")
+public class EnterprisePurchaseLead extends BaseEntity {
+
+    private String contactName;
+
+    private String companyName;
+
+    private String phone;
+
+    private String email;
+
+    private String modelCodes;
+
+    private String expectedUsage;
+
+    private String status;
+
+    private String remark;
+
+    private String submitIp;
+
+    private String userAgent;
+}

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/model/request/EnterprisePurchaseLeadRemarkReq.java

@@ -0,0 +1,18 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+@Getter
+@Setter
+public class EnterprisePurchaseLeadRemarkReq {
+
+    @NotNull(message = "id is required")
+    private Long id;
+
+    @Size(max = 2000, message = "remark max length is 2000")
+    private String remark;
+}

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/model/request/EnterprisePurchaseLeadStatusReq.java

@@ -0,0 +1,18 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@Getter
+@Setter
+public class EnterprisePurchaseLeadStatusReq {
+
+    @NotNull(message = "id is required")
+    private Long id;
+
+    @NotBlank(message = "status is required")
+    private String status;
+}

+ 40 - 0
netflix-dao/src/main/java/com/cyksj/model/request/EnterprisePurchaseLeadSubmitReq.java

@@ -0,0 +1,40 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+import java.util.List;
+
+@Getter
+@Setter
+public class EnterprisePurchaseLeadSubmitReq {
+
+    @NotBlank(message = "contactName is required")
+    @Size(max = 64, message = "contactName max length is 64")
+    private String contactName;
+
+    @NotBlank(message = "companyName is required")
+    @Size(max = 128, message = "companyName max length is 128")
+    private String companyName;
+
+    @NotBlank(message = "phone is required")
+    @Size(max = 32, message = "phone max length is 32")
+    private String phone;
+
+    @NotBlank(message = "email is required")
+    @Email(message = "email format is invalid")
+    @Size(max = 128, message = "email max length is 128")
+    private String email;
+
+    @NotEmpty(message = "models is required")
+    @Size(max = 20, message = "models max size is 20")
+    private List<String> models;
+
+    @NotBlank(message = "expectedUsage is required")
+    @Size(max = 128, message = "expectedUsage max length is 128")
+    private String expectedUsage;
+}

+ 43 - 0
netflix-dao/src/main/java/com/cyksj/model/views/AiModelGroupView.java

@@ -0,0 +1,43 @@
+package com.cyksj.model.views;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 用户端 AI 模型展示 - 按提供商分组
+ */
+@Data
+public class AiModelGroupView {
+
+    private String provider;
+
+    private String providerName;
+
+    private String providerLogo;
+
+    private List<AiModelItemView> models;
+
+    @Data
+    public static class AiModelItemView {
+
+        private Long id;
+
+        private String modelCode;
+
+        private String modelName;
+
+        private String modelIcon;
+
+        private String description;
+
+        /** 功能标签列表,由逗号分隔字段拆分而来 */
+        private List<String> tags;
+
+        private String contextWindow;
+
+        private String pricingBadge;
+
+        private Integer sort;
+    }
+}

+ 34 - 0
netflix-dao/src/main/java/com/cyksj/model/views/EnterprisePurchaseLeadView.java

@@ -0,0 +1,34 @@
+package com.cyksj.model.views;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+import java.util.List;
+
+@Getter
+@Setter
+public class EnterprisePurchaseLeadView {
+
+    private Long id;
+
+    private Date createdTime;
+
+    private Date updateTime;
+
+    private String contactName;
+
+    private String companyName;
+
+    private String phone;
+
+    private String email;
+
+    private List<String> models;
+
+    private String expectedUsage;
+
+    private String status;
+
+    private String remark;
+}

+ 13 - 0
netflix-service/src/main/java/com/cyksj/service/aimodel/AiModelService.java

@@ -0,0 +1,13 @@
+package com.cyksj.service.aimodel;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.model.entity.AiModel;
+import com.cyksj.model.views.AiModelGroupView;
+
+import java.util.List;
+
+public interface AiModelService extends IService<AiModel> {
+
+    /** 用户端:获取启用的模型列表,按提供商分组 */
+    List<AiModelGroupView> listGroupByProvider();
+}

+ 73 - 0
netflix-service/src/main/java/com/cyksj/service/aimodel/impl/AiModelServiceImpl.java

@@ -0,0 +1,73 @@
+package com.cyksj.service.aimodel.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.mapper.AiModelMapper;
+import com.cyksj.model.entity.AiModel;
+import com.cyksj.model.views.AiModelGroupView;
+import com.cyksj.service.aimodel.AiModelService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AiModelServiceImpl extends ServiceImpl<AiModelMapper, AiModel> implements AiModelService {
+
+    @Override
+    public List<AiModelGroupView> listGroupByProvider() {
+        List<AiModel> models = list(
+                Wrappers.lambdaQuery(AiModel.class)
+                        .eq(AiModel::getStatus, 1)
+                        .orderByAsc(AiModel::getSort)
+        );
+        if (models.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // 按 provider 保持顺序分组
+        Map<String, List<AiModel>> providerMap = new LinkedHashMap<>();
+        for (AiModel model : models) {
+            providerMap.computeIfAbsent(model.getProvider(), k -> new ArrayList<>()).add(model);
+        }
+
+        List<AiModelGroupView> result = new ArrayList<>();
+        for (Map.Entry<String, List<AiModel>> entry : providerMap.entrySet()) {
+            AiModel first = entry.getValue().get(0);
+            AiModelGroupView group = new AiModelGroupView();
+            group.setProvider(first.getProvider());
+            group.setProviderName(first.getProviderName());
+            group.setProviderLogo(first.getProviderLogo());
+
+            List<AiModelGroupView.AiModelItemView> items = entry.getValue().stream().map(m -> {
+                AiModelGroupView.AiModelItemView item = new AiModelGroupView.AiModelItemView();
+                item.setId(m.getId());
+                item.setModelCode(m.getModelCode());
+                item.setModelName(m.getModelName());
+                item.setModelIcon(m.getModelIcon());
+                item.setDescription(m.getDescription());
+                item.setContextWindow(m.getContextWindow());
+                item.setPricingBadge(m.getPricingBadge());
+                item.setSort(m.getSort());
+                if (StrUtil.isNotBlank(m.getTags())) {
+                    item.setTags(Arrays.asList(m.getTags().split(",")));
+                } else {
+                    item.setTags(Collections.emptyList());
+                }
+                return item;
+            }).collect(Collectors.toList());
+
+            group.setModels(items);
+            result.add(group);
+        }
+        return result;
+    }
+}

+ 20 - 0
netflix-service/src/main/java/com/cyksj/service/enterprise/EnterprisePurchaseLeadService.java

@@ -0,0 +1,20 @@
+package com.cyksj.service.enterprise;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.model.entity.EnterprisePurchaseLead;
+import com.cyksj.model.request.EnterprisePurchaseLeadRemarkReq;
+import com.cyksj.model.request.EnterprisePurchaseLeadStatusReq;
+import com.cyksj.model.request.EnterprisePurchaseLeadSubmitReq;
+import com.cyksj.model.views.EnterprisePurchaseLeadView;
+
+public interface EnterprisePurchaseLeadService extends IService<EnterprisePurchaseLead> {
+
+    Long submit(EnterprisePurchaseLeadSubmitReq req, String submitIp, String userAgent);
+
+    Page<EnterprisePurchaseLeadView> page(String status, String keyword, Integer start, Integer limit);
+
+    void updateStatus(EnterprisePurchaseLeadStatusReq req);
+
+    void updateRemark(EnterprisePurchaseLeadRemarkReq req);
+}

+ 154 - 0
netflix-service/src/main/java/com/cyksj/service/enterprise/impl/EnterprisePurchaseLeadServiceImpl.java

@@ -0,0 +1,154 @@
+package com.cyksj.service.enterprise.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.mapper.enterprise.EnterprisePurchaseLeadMapper;
+import com.cyksj.model.entity.EnterprisePurchaseLead;
+import com.cyksj.model.request.EnterprisePurchaseLeadRemarkReq;
+import com.cyksj.model.request.EnterprisePurchaseLeadStatusReq;
+import com.cyksj.model.request.EnterprisePurchaseLeadSubmitReq;
+import com.cyksj.model.views.EnterprisePurchaseLeadView;
+import com.cyksj.service.enterprise.EnterprisePurchaseLeadService;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+public class EnterprisePurchaseLeadServiceImpl extends ServiceImpl<EnterprisePurchaseLeadMapper, EnterprisePurchaseLead> implements EnterprisePurchaseLeadService {
+
+    private static final String STATUS_PENDING = "pending";
+    private static final String STATUS_CONTACTED = "contacted";
+    private static final String STATUS_INVALID = "invalid";
+    private static final Set<String> ALLOW_STATUSES = Set.of(STATUS_PENDING, STATUS_CONTACTED, STATUS_INVALID);
+
+    @Override
+    public Long submit(EnterprisePurchaseLeadSubmitReq req, String submitIp, String userAgent) {
+        List<String> models = normalizeModels(req.getModels());
+        if (models.isEmpty()) {
+            throw new BusinessRuntimeException("models is required");
+        }
+
+        EnterprisePurchaseLead lead = new EnterprisePurchaseLead()
+                .setContactName(limit(req.getContactName(), 64))
+                .setCompanyName(limit(req.getCompanyName(), 128))
+                .setPhone(limit(req.getPhone(), 32))
+                .setEmail(limit(req.getEmail(), 128))
+                .setModelCodes(String.join(",", models))
+                .setExpectedUsage(limit(req.getExpectedUsage(), 128))
+                .setStatus(STATUS_PENDING)
+                .setSubmitIp(limit(submitIp, 64))
+                .setUserAgent(limit(userAgent, 512))
+                .setRemark("");
+        save(lead);
+        return lead.getId();
+    }
+
+    @Override
+    public Page<EnterprisePurchaseLeadView> page(String status, String keyword, Integer start, Integer limit) {
+        int current = start == null || start < 1 ? 1 : start;
+        int size = limit == null || limit < 1 ? 10 : Math.min(limit, 100);
+
+        LambdaQueryWrapper<EnterprisePurchaseLead> wrapper = new LambdaQueryWrapper<>();
+        if (StrUtil.isNotBlank(status) && !"all".equals(status)) {
+            validateStatus(status);
+            wrapper.eq(EnterprisePurchaseLead::getStatus, status);
+        }
+        if (StrUtil.isNotBlank(keyword)) {
+            String kw = keyword.trim();
+            wrapper.and(w -> w.like(EnterprisePurchaseLead::getCompanyName, kw)
+                    .or().like(EnterprisePurchaseLead::getContactName, kw)
+                    .or().like(EnterprisePurchaseLead::getPhone, kw)
+                    .or().like(EnterprisePurchaseLead::getEmail, kw));
+        }
+        wrapper.orderByDesc(EnterprisePurchaseLead::getCreatedTime);
+
+        Page<EnterprisePurchaseLead> entityPage = page(new Page<>(current, size), wrapper);
+        Page<EnterprisePurchaseLeadView> viewPage = new Page<>(entityPage.getCurrent(), entityPage.getSize());
+        viewPage.setTotal(entityPage.getTotal());
+        viewPage.setRecords(entityPage.getRecords().stream().map(this::toView).collect(Collectors.toList()));
+        return viewPage;
+    }
+
+    @Override
+    public void updateStatus(EnterprisePurchaseLeadStatusReq req) {
+        validateStatus(req.getStatus());
+        EnterprisePurchaseLead lead = new EnterprisePurchaseLead()
+                .setStatus(req.getStatus());
+        lead.setId(req.getId());
+        if (!updateById(lead)) {
+            throw new BusinessRuntimeException("lead not found");
+        }
+    }
+
+    @Override
+    public void updateRemark(EnterprisePurchaseLeadRemarkReq req) {
+        EnterprisePurchaseLead lead = new EnterprisePurchaseLead()
+                .setRemark(limit(req.getRemark(), 2000));
+        lead.setId(req.getId());
+        if (!updateById(lead)) {
+            throw new BusinessRuntimeException("lead not found");
+        }
+    }
+
+    private EnterprisePurchaseLeadView toView(EnterprisePurchaseLead lead) {
+        EnterprisePurchaseLeadView view = new EnterprisePurchaseLeadView();
+        view.setId(lead.getId());
+        view.setCreatedTime(lead.getCreatedTime());
+        view.setUpdateTime(lead.getUpdateTime());
+        view.setContactName(lead.getContactName());
+        view.setCompanyName(lead.getCompanyName());
+        view.setPhone(lead.getPhone());
+        view.setEmail(lead.getEmail());
+        view.setModels(splitModels(lead.getModelCodes()));
+        view.setExpectedUsage(lead.getExpectedUsage());
+        view.setStatus(lead.getStatus());
+        view.setRemark(lead.getRemark());
+        return view;
+    }
+
+    private List<String> normalizeModels(List<String> models) {
+        if (models == null) {
+            return Collections.emptyList();
+        }
+        return models.stream()
+                .filter(StrUtil::isNotBlank)
+                .map(String::trim)
+                .map(item -> item.replace(",", ""))
+                .map(item -> limit(item, 128))
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    private List<String> splitModels(String modelCodes) {
+        if (StrUtil.isBlank(modelCodes)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(modelCodes.split(","))
+                .filter(StrUtil::isNotBlank)
+                .collect(Collectors.toList());
+    }
+
+    private void validateStatus(String status) {
+        if (!ALLOW_STATUSES.contains(status)) {
+            throw new BusinessRuntimeException("status is invalid");
+        }
+    }
+
+    private String limit(String value, int maxLength) {
+        if (value == null) {
+            return "";
+        }
+        String trim = value.trim();
+        if (trim.length() <= maxLength) {
+            return trim;
+        }
+        return StrUtil.subPre(trim, maxLength);
+    }
+}

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

@@ -929,7 +929,7 @@ public class OrderDonServiceImpl extends ServiceImpl<OrderDonMapper, OrderDon> i
 	}
 
     private void recordEnvTypeOrderRelate(Long orderId, Long goodsId, GoodsClickUserOrderRecord.EnvType envType, String purchaseNotify) {
-        if (goodsId != null && envType != null) {
+        if ((goodsId != null && envType != null) || StrUtil.isNotEmpty(purchaseNotify)) {
             GoodsClickUserOrderRecord goodsClickUserOrderRecord = new GoodsClickUserOrderRecord();
             goodsClickUserOrderRecord.setGoodsId(goodsId);
             goodsClickUserOrderRecord.setOrderId(orderId);

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

@@ -166,8 +166,10 @@ public class GroupRelationClearServiceImpl implements GroupRelationClearService
 
                 //codex
                 if (groupsRelation.getGoodsId() != null && groupsRelation.getGoodsId() == Constant.CODEX_GOODS_ID) {
-                    //清除套餐
-                    codexService.delUserPackages(groupsRelation.getUserId());
+                    if (source != UserTicketClearedRecord.Source.codex_deduct) {
+                        //清除套餐
+                        codexService.delUserPackages(groupsRelation.getUserId());
+                    }
                     //清除codex
                     TASK_POOL.execute(() -> codexUserMapper.delete(Wrappers.lambdaQuery(CodexUser.class).eq(CodexUser::getRelationId, relationId)));
                 }

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

@@ -242,7 +242,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
 
 	private final TicketRedemptionCodeRecordMapper ticketRedemptionCodeRecordMapper;
 
-	private static final Date GPT_ACTIVITY_PAY_TIME_BEGIN = DateUtil.parse("2026-05-03 00:00:00");
+	private static final Date GPT_ACTIVITY_PAY_TIME_BEGIN = DateUtil.parse("2026-05-01 00:00:00");
 
 	@Override
 	public SearchResult<RenewalView> getMyTicket(long userId, Boolean isLoginPopularize, String customId, String account, String orderNo, Integer cmt) {
@@ -2867,7 +2867,7 @@ public class GroupRelationFrontServiceImpl implements GroupRelationFrontService
             return false;
         }
         return (Constant.GPT_RECHARGE_GOODS_ID.equals(goodsId)
-                || (Constant.GPT_PLUS_GID.equals(goodsId) && Objects.equals(skuNum, 1))) && orderDon.getCreatedTime().after(DateUtil.parse("2026-05-03"));
+                || (Constant.GPT_PLUS_GID.equals(goodsId) && Objects.equals(skuNum, 1))) && orderDon.getCreatedTime().after(DateUtil.parse("2026-05-01"));
     }
 
     private GroupsRelation getReusableGptActivityGiftRelation(List<Long> userIds, Long giftSkuId, Long sourceGoodsId) {

+ 31 - 0
netflix-web/src/main/java/com/cyksj/web/controller/aimodel/AiModelFrontController.java

@@ -0,0 +1,31 @@
+package com.cyksj.web.controller.aimodel;
+
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.views.AiModelGroupView;
+import com.cyksj.service.aimodel.AiModelService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 用户端 - AI 模型列表
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RequestMapping("/applet/aiModel")
+@RestController
+public class AiModelFrontController {
+
+    private final AiModelService aiModelService;
+
+    /** 获取模型列表(按提供商分组) */
+    @GetMapping("/list")
+    public Result<List<AiModelGroupView>> list() {
+        return GatewayResponse.SUCCESS.newBuilder().toResult(aiModelService.listGroupByProvider());
+    }
+}

+ 43 - 0
netflix-web/src/main/java/com/cyksj/web/controller/enterprise/EnterprisePurchaseLeadFrontController.java

@@ -0,0 +1,43 @@
+package com.cyksj.web.controller.enterprise;
+
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.request.EnterprisePurchaseLeadSubmitReq;
+import com.cyksj.service.enterprise.EnterprisePurchaseLeadService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping({"/applet/enterprise/lead", "/applets/enterprise/lead"})
+public class EnterprisePurchaseLeadFrontController {
+
+    private final EnterprisePurchaseLeadService enterprisePurchaseLeadService;
+
+    private final HttpServletRequest request;
+
+    @PostMapping
+    public Result<Long> submit(@RequestBody @Validated EnterprisePurchaseLeadSubmitReq req) {
+        Long id = enterprisePurchaseLeadService.submit(req, getClientIp(), request.getHeader("User-Agent"));
+        return GatewayResponse.SUCCESS.newBuilder().toResult(id);
+    }
+
+    private String getClientIp() {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip != null && !ip.isBlank()) {
+            int commaIndex = ip.indexOf(',');
+            return commaIndex > -1 ? ip.substring(0, commaIndex).trim() : ip.trim();
+        }
+        ip = request.getHeader("X-Real-IP");
+        if (ip != null && !ip.isBlank()) {
+            return ip.trim();
+        }
+        return request.getRemoteAddr();
+    }
+}

+ 87 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/aimodel/CmsAiModelController.java

@@ -0,0 +1,87 @@
+package com.cyksj.web.controller.manage.aimodel;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.AiModel;
+import com.cyksj.service.aimodel.AiModelService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 管理端 - AI 模型管理
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RequestMapping("/manage/aiModel")
+@RestController
+public class CmsAiModelController {
+
+    private final AiModelService aiModelService;
+    private final BeanSearcher beanSearcher;
+    private final HttpServletRequest request;
+
+    /** 分页查询模型列表 */
+    @GetMapping("/get")
+    public Result<SearchResult<AiModel>> get() {
+        SearchResult<AiModel> result = beanSearcher.search(AiModel.class,
+                MapUtils.flatBuilder(request.getParameterMap())
+                        .orderBy(AiModel::getSort).asc()
+                        .build());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(result);
+    }
+
+    /** 新增模型 */
+    @PostMapping("/post")
+    public Result<String> add(@RequestBody AiModel aiModel) {
+        validateModelCode(aiModel.getModelCode(), null);
+        aiModelService.save(aiModel);
+        return GatewayResponse.SUCCESS.newBuilder().toResult("新增成功");
+    }
+
+    /** 编辑模型 */
+    @PutMapping("/put")
+    public Result<String> update(@RequestBody AiModel aiModel) {
+        if (aiModel.getId() == null) {
+            throw BusinessRuntimeException.getInstance("模型不存在");
+        }
+        validateModelCode(aiModel.getModelCode(), aiModel.getId());
+        aiModelService.updateById(aiModel);
+        return GatewayResponse.SUCCESS.newBuilder().toResult("编辑成功");
+    }
+
+    /** 删除模型 */
+    @DeleteMapping("/delete/{id}")
+    public Result<String> delete(@PathVariable Long id) {
+        aiModelService.removeById(id);
+        return GatewayResponse.SUCCESS.newBuilder().toResult("删除成功");
+    }
+
+    /** 启用/禁用 */
+    @PutMapping("/status/{id}/{status}")
+    public Result<String> updateStatus(@PathVariable Long id, @PathVariable Integer status) {
+        aiModelService.update(Wrappers.lambdaUpdate(AiModel.class)
+                .set(AiModel::getStatus, status)
+                .eq(AiModel::getId, id));
+        return GatewayResponse.SUCCESS.newBuilder().toResult("操作成功");
+    }
+
+    private void validateModelCode(String modelCode, Long excludeId) {
+        if (modelCode == null || modelCode.isBlank()) {
+            throw BusinessRuntimeException.getInstance("模型标识不能为空");
+        }
+        long count = aiModelService.count(Wrappers.lambdaQuery(AiModel.class)
+                .eq(AiModel::getModelCode, modelCode)
+                .ne(excludeId != null, AiModel::getId, excludeId));
+        if (count > 0) {
+            throw BusinessRuntimeException.getInstance("模型标识已存在");
+        }
+    }
+}

+ 46 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/enterprise/CmsEnterprisePurchaseLeadController.java

@@ -0,0 +1,46 @@
+package com.cyksj.web.controller.manage.enterprise;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.EnterprisePurchaseLead;
+import com.cyksj.model.request.EnterprisePurchaseLeadRemarkReq;
+import com.cyksj.model.request.EnterprisePurchaseLeadStatusReq;
+import com.cyksj.model.views.EnterprisePurchaseLeadView;
+import com.cyksj.service.enterprise.EnterprisePurchaseLeadService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/manage/enterprise/lead")
+public class CmsEnterprisePurchaseLeadController {
+
+    private final EnterprisePurchaseLeadService enterprisePurchaseLeadService;
+
+    @GetMapping
+    public Result<Page<EnterprisePurchaseLeadView>> page(String status,
+                                                         String keyword,
+                                                         @RequestParam(defaultValue = "1") Integer start,
+                                                         @RequestParam(defaultValue = "10") Integer limit) {
+        return GatewayResponse.SUCCESS.newBuilder().toResult(enterprisePurchaseLeadService.page(status, keyword, start, limit));
+    }
+
+    @GetMapping("/{id}")
+    public Result<EnterprisePurchaseLead> detail(@PathVariable Long id) {
+        return GatewayResponse.SUCCESS.newBuilder().toResult(enterprisePurchaseLeadService.getById(id));
+    }
+
+    @PutMapping("/status")
+    public Result<String> updateStatus(@RequestBody @Validated EnterprisePurchaseLeadStatusReq req) {
+        enterprisePurchaseLeadService.updateStatus(req);
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+
+    @PutMapping("/remark")
+    public Result<String> updateRemark(@RequestBody @Validated EnterprisePurchaseLeadRemarkReq req) {
+        enterprisePurchaseLeadService.updateRemark(req);
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+}

+ 5 - 3
netflix-web/src/main/java/com/cyksj/web/controller/manage/group/CmsGroupRelationController.java

@@ -13,6 +13,7 @@ import com.cyksj.common.annotation.Log;
 import com.cyksj.common.annotation.NoSubmit;
 import com.cyksj.common.constant.Constant;
 import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.util.StringUtil;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.BusinessType;
 import com.cyksj.enums.GatewayResponse;
@@ -117,14 +118,15 @@ public class CmsGroupRelationController {
      * 代充平台 用户账号车票列表
      */
     @GetMapping("/get/recharge")
-    public Result<SearchResult<GroupsRelationRechargeView>> getRechargeList(String rechargeStatus, String orderNo, Boolean isVipError) {
+    public Result<SearchResult<GroupsRelationRechargeView>> getRechargeList(String rechargeStatus, String orderNo, String transactionId, Boolean isVipError) {
         MapBuilder builder = MapUtils.flatBuilder(request.getParameterMap());
         if (StrUtil.isEmpty(rechargeStatus)) {
             builder.field(GroupsRelationRechargeView::getRechargeStatus).op(Operator.NotNull);
         }
-        if (StrUtil.isNotBlank(orderNo)) {
+        if (StrUtil.isNotBlank(orderNo) || StringUtil.isNotBlank(transactionId)) {
             OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
-                    .eq(OrderDon::getOrderNo, orderNo)
+                    .eq(StrUtil.isNotBlank(orderNo), OrderDon::getOrderNo, orderNo)
+                    .eq(StrUtil.isNotBlank(transactionId), OrderDon::getTransactionId, transactionId)
                     .last("limit 1"));
             Long relationId = 0l;
             if (orderDon != null) {