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

运营后台和普通用户的登录分开

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

+ 10 - 16
zhijiayun-invite/src/main/java/com/xuekairui/invite/controller/AdminController.java

@@ -18,8 +18,6 @@ import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
-
 /**
  * 运营管理接口 - 邀请配置与爬虫次数发放
  * 前缀: /api/admin
@@ -147,26 +145,22 @@ public class AdminController {
      * GET /api/admin/crawler/grants?userId=1&grantType=ADMIN
      */
     @GetMapping("/crawler/grants")
-    public Result<List<com.xuekairui.user.dto.CrawlerQuotaGrantResponse>> listGrants(
+    public Result<Page<com.xuekairui.user.dto.CrawlerQuotaGrantResponse>> listGrants(
             @RequestParam(required = false) Long userId,
-            @RequestParam(required = false) String grantType) {
-        return Result.success(inviteService.listGrants(userId, grantType));
+            @RequestParam(required = false) String grantType,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int size) {
+        return Result.success(inviteService.listGrants(userId, grantType, page, size));
     }
 
     // ==================== 邀请记录查看 ====================
 
-    /**
-     * 查看某用户的邀请奖励记录
-     * GET /api/admin/invite/rewards?userId=1
-     */
     @GetMapping("/invite/rewards")
-    public Result<List<InviteRewardResponse>> listInviteRewards(
-            @RequestParam(required = false) Long userId) {
-        // 如果指定了userId,查看该用户的邀请记录;否则返回空(可扩展为全部)
-        if (userId != null) {
-            return Result.success(inviteService.getInviteRewards(userId));
-        }
-        return Result.success(List.of());
+    public Result<Page<InviteRewardResponse>> listInviteRewards(
+            @RequestParam(required = false) Long userId,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int size) {
+        return Result.success(inviteService.getInviteRewards(userId, page, size));
     }
 
     // ==================== 用户爬虫详情查询 ====================

+ 24 - 11
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteService.java

@@ -1,6 +1,7 @@
 package com.xuekairui.invite.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.ErrorCode;
 import com.xuekairui.common.ValidateUtil;
@@ -817,17 +818,24 @@ public class InviteService {
                 .build();
     }
 
-    /**
-     * 获取用户邀请记录列表(批量查询被邀请人,避免 N+1)
-     */
     public List<InviteRewardResponse> getInviteRewards(Long userId) {
-        List<InviteRelation> relations = inviteRelationMapper.selectList(
-                new LambdaQueryWrapper<InviteRelation>()
-                        .eq(InviteRelation::getInviterId, userId)
-                        .orderByDesc(InviteRelation::getCreateTime));
+        return getInviteRewards(userId, 1, Integer.MAX_VALUE).getRecords();
+    }
 
+    /**
+     * 分页查询邀请记录(批量查询被邀请人,避免 N+1)
+     */
+    public Page<InviteRewardResponse> getInviteRewards(Long userId, int pageNum, int pageSize) {
+        Page<InviteRelation> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<InviteRelation> wrapper = new LambdaQueryWrapper<InviteRelation>()
+                .eq(InviteRelation::getInviterId, userId)
+                .orderByDesc(InviteRelation::getCreateTime);
+        Page<InviteRelation> result = inviteRelationMapper.selectPage(page, wrapper);
+
+        List<InviteRelation> relations = result.getRecords();
         if (relations.isEmpty()) {
-            return List.of();
+            Page<InviteRewardResponse> empty = new Page<>(pageNum, pageSize, 0);
+            return empty;
         }
 
         // 批量查被邀请人和邀请码
@@ -845,7 +853,7 @@ public class InviteService {
         // 邀请人信息只查一次
         String inviterNickname = getUserNickname(userId);
 
-        return relations.stream().map(r -> {
+        List<InviteRewardResponse> list = relations.stream().map(r -> {
             boolean isRegistered = r.getRegistered() != null && r.getRegistered() == 1;
             User invitee = userMap.get(r.getInviteeId());
             String inviteeNickname = isRegistered
@@ -870,6 +878,10 @@ public class InviteService {
                             r.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null)
                     .build();
         }).collect(Collectors.toList());
+
+        Page<InviteRewardResponse> responsePage = new Page<>(pageNum, pageSize, result.getTotal());
+        responsePage.setRecords(list);
+        return responsePage;
     }
 
     /**
@@ -917,8 +929,9 @@ public class InviteService {
     /**
      * 获取发放记录列表
      */
-    public List<com.xuekairui.user.dto.CrawlerQuotaGrantResponse> listGrants(Long userId, String grantType) {
-        return crawlerQuotaGrantService.listGrants(userId, grantType);
+    public Page<com.xuekairui.user.dto.CrawlerQuotaGrantResponse> listGrants(Long userId, String grantType,
+                                                                               int page, int size) {
+        return crawlerQuotaGrantService.listGrants(userId, grantType, page, size);
     }
 
     // ==========================================

+ 13 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/controller/AdminMembershipController.java

@@ -51,6 +51,19 @@ public class AdminMembershipController {
         return Result.success(membershipService.listMemberships(page, size, userId, level, source, phone, nickname));
     }
 
+    /**
+     * 会员用户快照列表(用户维度,每用户一行)
+     * GET /api/admin/membership/users?page=1&size=20&phone=138&level=PRO
+     */
+    @GetMapping("/users")
+    public Result<Page<com.xuekairui.user.dto.MembershipUserResponse>> listMembershipUsers(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "20") Integer size,
+            @RequestParam(required = false) String phone,
+            @RequestParam(required = false) String level) {
+        return Result.success(membershipService.listMembershipUsers(page, size, phone, level));
+    }
+
     private String getOperatorRole(HttpServletRequest request) {
         String role = (String) request.getAttribute("role");
         return role != null ? role : "ADMIN";

+ 3 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/dto/MembershipResponse.java

@@ -55,6 +55,8 @@ public class MembershipResponse {
     /** 备注 */
     private String remark;
 
-    /** 创建时间 */
+    /** 微信昵称 */
+    private String wechatNickname;
+
     private LocalDateTime createTime;
 }

+ 50 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/dto/MembershipUserResponse.java

@@ -0,0 +1,50 @@
+package com.xuekairui.user.dto;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 会员用户快照(用户维度,一行一个用户,不展示变动历史)
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MembershipUserResponse {
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long userId;
+
+    /** 手机号 */
+    private String phone;
+
+    /** 昵称 */
+    private String nickname;
+
+    /** 微信昵称 */
+    private String wechatNickname;
+
+    /** 当前会员等级 */
+    private String level;
+
+    /** 等级中文名 */
+    private String levelName;
+
+    /** 会员来源 */
+    private String source;
+
+    /** 会员到期时间(PLUS 为 null) */
+    private LocalDateTime expireAt;
+
+    /** 剩余天数(PLUS=-1,已过期=0) */
+    private Integer daysRemaining;
+
+    /** 注册时间 */
+    private LocalDateTime createTime;
+}

+ 12 - 4
zhijiayun-user/src/main/java/com/xuekairui/user/service/CrawlerQuotaGrantService.java

@@ -10,6 +10,7 @@ import com.xuekairui.user.entity.User;
 import com.xuekairui.user.mapper.CrawlerQuotaGrantMapper;
 import com.xuekairui.user.mapper.UserMapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -162,7 +163,9 @@ public class CrawlerQuotaGrantService {
     /**
      * 获取发放记录列表(批量查用户,返回 DTO)
      */
-    public List<CrawlerQuotaGrantResponse> listGrants(Long userId, String grantType) {
+    public Page<CrawlerQuotaGrantResponse> listGrants(Long userId, String grantType,
+                                                       int pageNum, int pageSize) {
+        Page<CrawlerQuotaGrant> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<CrawlerQuotaGrant> wrapper = new LambdaQueryWrapper<>();
         if (userId != null) {
             wrapper.eq(CrawlerQuotaGrant::getUserId, userId);
@@ -171,10 +174,11 @@ public class CrawlerQuotaGrantService {
             wrapper.eq(CrawlerQuotaGrant::getGrantType, grantType);
         }
         wrapper.orderByDesc(CrawlerQuotaGrant::getCreateTime);
-        List<CrawlerQuotaGrant> grants = crawlerQuotaGrantMapper.selectList(wrapper);
+        Page<CrawlerQuotaGrant> result = crawlerQuotaGrantMapper.selectPage(page, wrapper);
 
+        List<CrawlerQuotaGrant> grants = result.getRecords();
         if (grants.isEmpty()) {
-            return List.of();
+            return new Page<>(pageNum, pageSize, 0);
         }
 
         // 批量查目标用户和来源用户
@@ -188,7 +192,7 @@ public class CrawlerQuotaGrantService {
         Map<Long, User> userMap = userMapper.selectBatchIds(new ArrayList<>(allUserIds))
                 .stream().collect(Collectors.toMap(User::getId, u -> u));
 
-        return grants.stream().map(g -> {
+        List<CrawlerQuotaGrantResponse> list = grants.stream().map(g -> {
             User user = userMap.get(g.getUserId());
             User sourceUser = g.getSourceUserId() != null ? userMap.get(g.getSourceUserId()) : null;
             return CrawlerQuotaGrantResponse.builder()
@@ -207,6 +211,10 @@ public class CrawlerQuotaGrantService {
                     .createTime(g.getCreateTime())
                     .build();
         }).collect(Collectors.toList());
+
+        Page<CrawlerQuotaGrantResponse> responsePage = new Page<>(pageNum, pageSize, result.getTotal());
+        responsePage.setRecords(list);
+        return responsePage;
     }
 
     // ==========================================

+ 67 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/MembershipService.java

@@ -8,6 +8,7 @@ import com.xuekairui.user.dto.GrantMembershipRequest;
 import com.xuekairui.user.dto.LevelPricingPlan;
 import com.xuekairui.user.dto.MembershipBenefitResponse;
 import com.xuekairui.user.dto.MembershipResponse;
+import com.xuekairui.user.dto.MembershipUserResponse;
 import com.xuekairui.user.dto.UserLevelResponse;
 import com.xuekairui.user.entity.MembershipBenefit;
 import com.xuekairui.user.entity.User;
@@ -362,6 +363,71 @@ public class MembershipService {
         return responsePage;
     }
 
+    /**
+     * 会员用户快照列表(用户维度,查 t_user + 当前会员)
+     */
+    public Page<MembershipUserResponse> listMembershipUsers(Integer pageNum, Integer pageSize,
+                                                            String phone, String level) {
+        Page<User> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
+        if (phone != null && !phone.isBlank()) {
+            wrapper.like(User::getPhone, phone);
+        }
+        if (level != null && !level.isBlank()) {
+            MembershipLevel ml = MembershipLevel.fromCode(level);
+            if (ml != null) {
+                wrapper.eq(User::getMembershipLevel, ml);
+            }
+        }
+        wrapper.orderByDesc(User::getCreateTime);
+        Page<User> result = userMapper.selectPage(page, wrapper);
+
+        // 批量查活跃会员记录,取 source
+        List<Long> userIds = result.getRecords().stream().map(User::getId).toList();
+        Map<Long, String> sourceMap = userIds.isEmpty() ? Map.of()
+                : userMembershipMapper.selectList(
+                        new LambdaQueryWrapper<UserMembership>()
+                                .in(UserMembership::getUserId, userIds)
+                                .eq(UserMembership::getStatus, 1))
+                        .stream().collect(Collectors.toMap(
+                                UserMembership::getUserId,
+                                UserMembership::getSource,
+                                (a, b) -> a));
+
+        List<MembershipUserResponse> list = result.getRecords().stream().map(u -> {
+            MembershipLevel currentLevel = u.getMembershipLevel() != null
+                    ? u.getMembershipLevel() : MembershipLevel.PLUS;
+            String levelCode = currentLevel.getCode();
+            boolean isPlus = currentLevel == MembershipLevel.PLUS;
+            LocalDateTime expireAt = isPlus ? null : u.getMembershipExpireAt();
+            int daysRemaining;
+            if (isPlus) {
+                daysRemaining = -1;
+            } else if (expireAt != null && expireAt.isAfter(LocalDateTime.now())) {
+                daysRemaining = (int) ChronoUnit.DAYS.between(LocalDateTime.now(), expireAt);
+            } else {
+                daysRemaining = 0;
+            }
+
+            return MembershipUserResponse.builder()
+                    .userId(u.getId())
+                    .phone(u.getPhone())
+                    .nickname(u.getNickname())
+                    .wechatNickname(u.getWechatNickname())
+                    .level(levelCode)
+                    .levelName(currentLevel.getDescription())
+                    .source(isPlus ? "SYSTEM" : sourceMap.getOrDefault(u.getId(), ""))
+                    .expireAt(expireAt)
+                    .daysRemaining(daysRemaining)
+                    .createTime(u.getCreateTime())
+                    .build();
+        }).collect(Collectors.toList());
+
+        Page<MembershipUserResponse> responsePage = new Page<>(pageNum, pageSize, result.getTotal());
+        responsePage.setRecords(list);
+        return responsePage;
+    }
+
     private MembershipResponse toResponse(UserMembership membership) {
         if (membership == null) {
             return null;
@@ -402,6 +468,7 @@ public class MembershipService {
                 .membershipExpireAt(membership.getEffectiveTo())
                 .status(membership.getStatus())
                 .remark(membership.getRemark())
+                .wechatNickname(user != null ? user.getWechatNickname() : null)
                 .createTime(membership.getCreateTime())
                 .build();
     }