Преглед изворни кода

优化代码,添加接口

liuchengsen пре 1 месец
родитељ
комит
47354155ca

+ 9 - 3
zhijiayun-invite/src/main/java/com/xuekairui/invite/dto/InviteStatsResponse.java

@@ -26,10 +26,10 @@ public class InviteStatsResponse {
     /** 已注册人数(通过邀请码注册成功的) */
     private Integer registeredCount;
 
-    /** 累计获得爬虫次数 */
+    /** 累计获得会员月数 */
     private Integer totalReward;
 
-    /** 摘要文本,如 "已邀请5人,3人注册 +21次" */
+    /** 摘要文本,如 "已邀请5人,3人注册,累计获得3个月高级会员" */
     private String summaryText;
 
     /** 今日已邀请人数 */
@@ -38,6 +38,12 @@ public class InviteStatsResponse {
     /** 今日剩余可邀请次数 */
     private Integer todayRemaining;
 
-    /** 每次邀请奖励数 */
+    /** 每次邀请奖励会员月数 */
     private Integer rewardPerInvite;
+
+    /** 今日爬虫剩余配额(与头像/爬虫状态页一致,来源 CrawlerService.getCrawlerStatus) */
+    private Integer crawlerTodayRemaining;
+
+    /** 邀请奖励贡献的额外爬虫配额(来源 t_crawler_quota_grant,grant_type=INVITE) */
+    private Integer crawlerQuotaFromInvites;
 }

+ 7 - 2
zhijiayun-invite/src/main/java/com/xuekairui/invite/entity/InviteConfig.java

@@ -22,9 +22,14 @@ public class InviteConfig {
     @TableId(type = IdType.AUTO)
     private Long id;
 
-    /** 每次邀请奖励爬虫次数(历史字段,已废弃) */
+    /**
+     * 每次邀请额外奖励爬虫次数(默认0=仅送1个月高级会员,不送额外配额)
+     * <p>
+     * 邀请奖励核心:邀请人固定获得1个月高级会员(PRO),被邀请人注册即得普通会员(PLUS)。
+     * 本字段为可选的额外爬虫配额,运营可按需配置。
+     */
     @Builder.Default
-    private Integer rewardCrawlerCount = 5;
+    private Integer rewardCrawlerCount = 0;
 
     /** 奖励类型:MEMBERSHIP-赠送会员时长 CRAWLER-爬虫次数(历史) */
     @Builder.Default

+ 42 - 9
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteService.java

@@ -44,6 +44,7 @@ public class InviteService {
     private final UserMapper userMapper;
     private final CrawlerUsageLogMapper crawlerUsageLogMapper;
     private final com.xuekairui.user.service.MembershipService membershipService;
+    private final com.xuekairui.user.service.CrawlerService crawlerService;
 
     /** 邀请链接基础URL,可通过配置覆盖 */
     @Value("${invite.base-url:https://app.zhijiayun.com}")
@@ -532,10 +533,16 @@ public class InviteService {
         inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
         inviteCodeMapper.updateById(inviteCode);
 
-        // 8. 给邀请人发放奖励(在奖励上限内赠送30天会员时长)
+        // 8. 给邀请人发放奖励(在奖励上限内赠送30天会员时长+额外爬虫配额
         if (rewardAllowed) {
             grantInviteReward(inviterId, inviteeId);
-            log.info("邀请成功: 用户 {} 邀请了用户 {},邀请人获赠30天会员时长", inviterId, inviteeId);
+            // 发放额外爬虫配额(由运营后台配置每次邀请赠送次数,默认0=不送)
+            int crawlerReward = config.getRewardCrawlerCount() != null ? config.getRewardCrawlerCount() : 0;
+            if (crawlerReward > 0) {
+                crawlerQuotaGrantService.grantInviteReward(inviterId, inviteeId, crawlerReward);
+            }
+            log.info("邀请成功: 用户 {} 邀请了用户 {},邀请人获赠30天会员时长 + {}次额外配额",
+                    inviterId, inviteeId, crawlerReward);
         } else {
             log.info("邀请成功: 用户 {} 邀请了用户 {},已达奖励上限,不发放会员时长", inviterId, inviteeId);
         }
@@ -636,11 +643,16 @@ public class InviteService {
         inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
         inviteCodeMapper.updateById(inviteCode);
 
-        // 10. 发放奖励:邀请人30天会员时长(在奖励上限内)
+        // 10. 发放奖励:邀请人30天会员时长 + 额外爬虫配额(在奖励上限内)
         if (rewardAllowed) {
             membershipService.grantInviteRewardMembership(inviterId, userId);
-            log.info("补填邀请码成功: 用户{}绑定邀请码{},邀请人{},邀请人获赠30天会员时长",
-                    userId, inviteCodeStr, inviterId);
+            // 发放额外爬虫配额
+            int crawlerReward = config.getRewardCrawlerCount() != null ? config.getRewardCrawlerCount() : 0;
+            if (crawlerReward > 0) {
+                crawlerQuotaGrantService.grantInviteReward(inviterId, userId, crawlerReward);
+            }
+            log.info("补填邀请码成功: 用户{}绑定邀请码{},邀请人{},邀请人获赠30天会员时长 + {}次额外配额",
+                    userId, inviteCodeStr, inviterId, crawlerReward);
             return "补填邀请码成功!邀请人获赠1个月会员时长";
         } else {
             log.info("补填邀请码成功: 用户{}绑定邀请码{},邀请人{},已达奖励上限,不发放会员时长",
@@ -699,12 +711,12 @@ public class InviteService {
                 .filter(r -> r.getRegistered() != null && r.getRegistered() == 1)
                 .count();
 
-        // 累计奖励
+        // 累计奖励会员月数
         int totalReward = allRelations.stream()
                 .mapToInt(InviteRelation::getRewardGranted)
                 .sum();
 
-        // 链接被打开次数(直接从已查到的邀请码响应中取,无需重复查库)
+        // 链接被打开次数
         int clickedCount = codeResponse.getClickCount() != null ? codeResponse.getClickCount() : 0;
 
         // 今日邀请数
@@ -716,9 +728,28 @@ public class InviteService {
 
         int todayRemaining = Math.max(0, config.getMaxInvitePerDay() - todayInvited.intValue());
 
-        // 生成摘要文本(每邀请1家注册 = 邀请人获1个月会员)
+        // 从爬虫服务获取统一配额数据(保证与头像/爬虫状态页一致)
+        int crawlerTodayRemaining = 0;
+        int crawlerQuotaFromInvites = 0;
+        try {
+            com.xuekairui.user.dto.CrawlerStatusResponse crawlerStatus =
+                    crawlerService.getCrawlerStatus(userId);
+            crawlerTodayRemaining = crawlerStatus.getTodayRemaining() != null
+                    ? crawlerStatus.getTodayRemaining() : 0;
+            // 从配额明细中提取邀请奖励的可用配额
+            if (crawlerStatus.getExtraQuotaBreakdown() != null) {
+                crawlerQuotaFromInvites = crawlerStatus.getExtraQuotaBreakdown().stream()
+                        .filter(item -> "INVITE".equals(item.getSourceType()))
+                        .mapToInt(item -> item.getAvailable() != null ? item.getAvailable() : 0)
+                        .sum();
+            }
+        } catch (Exception e) {
+            log.warn("获取爬虫配额状态失败: userId={}, error={}", userId, e.getMessage());
+        }
+
+        // 生成摘要文本(每邀请1家注册 = 邀请人获1个月会员,超上限不发奖励)
         String summaryText = String.format("已邀请%d家药店,%d家注册,累计获得%d个月高级会员",
-                totalInvited, registeredCount, registeredCount);
+                totalInvited, registeredCount, totalReward);
 
         return InviteStatsResponse.builder()
                 .myInviteCode(codeResponse.getCode())
@@ -731,6 +762,8 @@ public class InviteService {
                 .todayInvited(todayInvited.intValue())
                 .todayRemaining(todayRemaining)
                 .rewardPerInvite(config.getRewardMonths() != null ? config.getRewardMonths() : 1)
+                .crawlerTodayRemaining(crawlerTodayRemaining)
+                .crawlerQuotaFromInvites(crawlerQuotaFromInvites)
                 .build();
     }
 

+ 3 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/entity/User.java

@@ -74,6 +74,9 @@ public class User {
     /** 当前会员等级:PLUS/PRO/ULTRA */
     private MembershipLevel membershipLevel;
 
+    /** 会员过期时间(NULL=永久有效或无会员,普通会员为NULL) */
+    private LocalDateTime membershipExpireAt;
+
     /** 最后登录时间 */
     private LocalDateTime lastLoginTime;
 

+ 0 - 19
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -20,7 +20,6 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
-import java.time.temporal.ChronoUnit;
 
 /**
  * 认证服务
@@ -37,7 +36,6 @@ public class AuthService {
     private final ApplicationEventPublisher eventPublisher;
     private final PasswordUtil passwordUtil;
     private final OperationAuditLogService auditLogService;
-    private final MembershipService membershipService;
     private final UserLevelService userLevelService;
 
     /**
@@ -279,23 +277,6 @@ public class AuthService {
             } catch (Exception e) {
                 log.warn("获取用户等级信息失败: userId={}, error={}", user.getId(), e.getMessage());
             }
-            // 填充会员过期信息
-            if (levelInfo != null) {
-                try {
-                    MembershipResponse membership = membershipService.getMyMembership(user.getId());
-                    if (membership != null && membership.getEffectiveTo() != null) {
-                        levelInfo.setMembershipExpireAt(membership.getEffectiveTo());
-                        long daysRemaining = ChronoUnit.DAYS.between(LocalDateTime.now(), membership.getEffectiveTo());
-                        levelInfo.setMembershipDaysRemaining(daysRemaining > 0 ? (int) daysRemaining : 0);
-                    } else {
-                        // 永久会员或无会员记录
-                        levelInfo.setMembershipExpireAt(null);
-                        levelInfo.setMembershipDaysRemaining(-1);
-                    }
-                } catch (Exception e) {
-                    log.warn("获取用户会员信息失败: userId={}, error={}", user.getId(), e.getMessage());
-                }
-            }
         }
         return UserInfoResponse.builder()
                 .id(user.getId())

+ 16 - 5
zhijiayun-user/src/main/java/com/xuekairui/user/service/MembershipService.java

@@ -125,8 +125,9 @@ public class MembershipService {
                 .build();
         userMembershipMapper.insert(membership);
 
-        // 同步到用户表当前会员等级
+        // 同步到用户表当前会员等级与过期时间
         user.setMembershipLevel(level);
+        user.setMembershipExpireAt(membership.getEffectiveTo());
         userMapper.updateById(user);
 
         // 审计日志
@@ -152,6 +153,8 @@ public class MembershipService {
             return;
         }
 
+        LocalDateTime newExpireAt;
+
         UserMembership existing = userMembershipMapper.selectActiveByUserId(userId);
         if (existing != null && existing.getLevel() == level) {
             // 同等级续期:在原到期时间基础上追加
@@ -161,6 +164,7 @@ public class MembershipService {
                     : LocalDateTime.now();
             existing.setEffectiveTo(base.plusDays(days));
             userMembershipMapper.updateById(existing);
+            newExpireAt = existing.getEffectiveTo();
         } else {
             // 互斥:停用所有旧会员
             List<UserMembership> activeList = userMembershipMapper.selectList(
@@ -183,25 +187,31 @@ public class MembershipService {
                     .remark(remark)
                     .build();
             userMembershipMapper.insert(membership);
+            newExpireAt = membership.getEffectiveTo();
         }
 
-        // 更新用户当前等级(仅当新等级更高时)
+        // 同步用户表:等级 + 过期时间
+        // 仅当新等级更高时升级
         if (user.getMembershipLevel() == null
                 || user.getMembershipLevel().getWeight() < level.getWeight()) {
             user.setMembershipLevel(level);
-            userMapper.updateById(user);
         }
+        // 每次授予/续期都同步过期时间到用户表
+        user.setMembershipExpireAt(newExpireAt);
+        userMapper.updateById(user);
     }
 
     /**
      * 邀请人奖励:赠送1个月(30天)高级会员(PRO)
-     * 无论邀请人当前会员身份,统一赠送高级会员时长
+     * <p>
+     * 无论邀请人当前是什么等级,统一赠送高级会员时长。
+     * 同等级续期叠加,不同等级则停用旧会员并启用新的PRO会员。
      */
     @Transactional
     public void grantInviteRewardMembership(Long inviterId, Long inviteeId) {
         grantOrExtendMembership(inviterId, MembershipLevel.PRO, 30,
                 "INVITE", "邀请奖励:邀请用户" + inviteeId + "注册");
-        log.info("邀请奖励:用户{}获得30天高级会员", inviterId);
+        log.info("邀请奖励:用户{}获得30天高级会员(PRO)", inviterId);
     }
 
     /**
@@ -350,6 +360,7 @@ public class MembershipService {
                 User user = userMapper.selectById(userId);
                 if (user != null && user.getMembershipLevel() != MembershipLevel.PLUS) {
                     user.setMembershipLevel(MembershipLevel.PLUS);
+                    user.setMembershipExpireAt(null);
                     userMapper.updateById(user);
                     log.info("用户{}会员已到期,降级为普通会员", userId);
                 }

+ 36 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/service/UserLevelService.java

@@ -19,6 +19,8 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -116,7 +118,9 @@ public class UserLevelService {
         if (user == null) {
             throw new BusinessException(ErrorCode.USER_NOT_FOUND);
         }
-        return toResponse(resolveUserLevel(user));
+        UserLevelResponse response = toResponse(resolveUserLevel(user));
+        fillMembershipExpiration(response, user);
+        return response;
     }
 
     /**
@@ -175,6 +179,11 @@ public class UserLevelService {
         if (user == null) {
             return buildUserLevelWithBenefits(MembershipLevel.PLUS);
         }
+        // 实时过期检查:用户表 membership_expire_at 已过期,直接返回普通会员
+        if (user.getMembershipExpireAt() != null
+                && user.getMembershipExpireAt().isBefore(LocalDateTime.now())) {
+            return buildUserLevelWithBenefits(MembershipLevel.PLUS);
+        }
         // 查询用户当前有效的会员记录(PRO/ULTRA 等付费或赠送的会员)
         UserMembership activeMembership = userMembershipMapper.selectActiveByUserId(user.getId());
         if (activeMembership != null && activeMembership.getLevel() != null) {
@@ -241,6 +250,32 @@ public class UserLevelService {
                 .build();
     }
 
+    /**
+     * 填充会员过期信息到 UserLevelResponse
+     * <p>
+     * 高级会员(PRO/ULTRA):从用户表读取 membershipExpireAt 计算剩余天数;
+     * 普通会员(PLUS):无过期时间,membershipDaysRemaining=-1。
+     */
+    private void fillMembershipExpiration(UserLevelResponse response, User user) {
+        if (user == null) {
+            return;
+        }
+        if (user.getMembershipExpireAt() != null) {
+            response.setMembershipExpireAt(user.getMembershipExpireAt());
+            long daysRemaining = ChronoUnit.DAYS.between(LocalDateTime.now(), user.getMembershipExpireAt());
+            response.setMembershipDaysRemaining(daysRemaining > 0 ? (int) daysRemaining : 0);
+        } else if (user.getMembershipLevel() != null
+                && user.getMembershipLevel() != MembershipLevel.PLUS) {
+            // 高级会员但无过期时间(永久有效),如运营发放的永久会员
+            response.setMembershipExpireAt(null);
+            response.setMembershipDaysRemaining(-1);
+        } else {
+            // 普通会员,无过期时间概念
+            response.setMembershipExpireAt(null);
+            response.setMembershipDaysRemaining(-1);
+        }
+    }
+
     /**
      * 构建指定等级的定价方案列表
      * <p>