فهرست منبع

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

liuchengsen 1 ماه پیش
والد
کامیت
a4629bb68a

+ 20 - 8
zhijiayun-invite/src/main/java/com/xuekairui/invite/controller/AdminController.java → zhijiayun-invite/src/main/java/com/xuekairui/invite/controller/AdminInviteController.java

@@ -16,7 +16,15 @@ import com.xuekairui.user.service.OperationAuditLogService;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
 
 /**
  * 运营管理接口 - 邀请配置与爬虫次数发放
@@ -25,7 +33,7 @@ import org.springframework.web.bind.annotation.*;
 @RestController
 @RequestMapping("/api/admin")
 @RequiredArgsConstructor
-public class AdminController {
+public class AdminInviteController {
 
     private final InviteConfigService inviteConfigService;
     private final InviteService inviteService;
@@ -136,7 +144,7 @@ public class AdminController {
         String operatorRole = getOperatorRole(httpRequest);
         CrawlerQuotaGrant grant = inviteService.adminGrantQuota(request);
         auditLogService.logSuccess(operatorId != null ? operatorId : 0L, "管理员", operatorRole,
-                OperationType.CRAWLER_QUOTA_GRANT, "CRAWLER_QUOTA:" + request.getUserId(), request.getUserId());
+                OperationType.CRAWLER_QUOTA_GRANT, "CRAWLER_QUOTA:" + grant.getUserId(), grant.getUserId());
         return Result.success(grant);
     }
 
@@ -170,17 +178,21 @@ public class AdminController {
      * GET /api/admin/crawler/user-detail?userId=1
      */
     @GetMapping("/crawler/user-detail")
-    public Result<CrawlerUserDetailResponse> getCrawlerUserDetail(@RequestParam Long userId) {
-        return Result.success(inviteService.getCrawlerUserDetail(userId));
+    public Result<CrawlerUserDetailResponse> getCrawlerUserDetail(
+            @RequestParam(required = false) Long userId,
+            @RequestParam(required = false) String phone) {
+        return Result.success(inviteService.getCrawlerUserDetail(userId, phone));
     }
 
     /**
      * 查看用户邀请转化统计(点击-注册转化率)
-     * GET /api/admin/invite/conversion-stats?userId=1
+     * GET /api/admin/invite/conversion-stats?userId=1 或 ?phone=13800138000
      */
     @GetMapping("/invite/conversion-stats")
-    public Result<InviteConversionStatsResponse> getInviteConversionStats(@RequestParam Long userId) {
-        return Result.success(inviteService.getInviteConversionStats(userId));
+    public Result<InviteConversionStatsResponse> getInviteConversionStats(
+            @RequestParam(required = false) Long userId,
+            @RequestParam(required = false) String phone) {
+        return Result.success(inviteService.getInviteConversionStats(userId, phone));
     }
 
     private String getOperatorRole(HttpServletRequest request) {

+ 4 - 2
zhijiayun-invite/src/main/java/com/xuekairui/invite/dto/QuotaGrantRequest.java

@@ -14,10 +14,12 @@ import java.time.LocalDateTime;
 @Data
 public class QuotaGrantRequest {
 
-    /** 目标用户ID */
-    @NotNull(message = "用户ID不能为空")
+    /** 目标用户ID(和 phone 二选一) */
     private Long userId;
 
+    /** 目标用户手机号(和 userId 二选一) */
+    private String phone;
+
     /** 发放爬虫次数 */
     @NotNull(message = "发放次数不能为空")
     @Min(1)

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

@@ -10,6 +10,7 @@ import com.xuekairui.invite.entity.*;
 import com.xuekairui.invite.mapper.InviteCodeMapper;
 import com.xuekairui.invite.mapper.InviteRelationMapper;
 import com.xuekairui.user.dto.CrawlerPlatformStatsResponse;
+import com.xuekairui.user.dto.CrawlerQuotaGrantResponse;
 import com.xuekairui.user.dto.CrawlerUserDetailResponse;
 import com.xuekairui.user.entity.CrawlerPlatform;
 import com.xuekairui.user.entity.CrawlerQuotaGrant;
@@ -719,8 +720,20 @@ public class InviteService {
      */
     @Transactional
     public CrawlerQuotaGrant adminGrantQuota(QuotaGrantRequest request) {
+        Long userId = request.getUserId();
+        if (userId == null && request.getPhone() != null && !request.getPhone().isBlank()) {
+            User u = userMapper.selectOne(
+                    new LambdaQueryWrapper<User>().eq(User::getPhone, request.getPhone()));
+            if (u == null) {
+                throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+            }
+            userId = u.getId();
+        }
+        if (userId == null) {
+            throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID和手机号至少填写一个");
+        }
         return crawlerQuotaGrantService.grantAdminQuota(
-                request.getUserId(),
+                userId,
                 request.getQuotaCount(),
                 request.getExpireTime(),
                 request.getRemark());
@@ -834,20 +847,19 @@ public class InviteService {
 
         List<InviteRelation> relations = result.getRecords();
         if (relations.isEmpty()) {
-            Page<InviteRewardResponse> empty = new Page<>(pageNum, pageSize, 0);
-            return empty;
+            return new Page<>(pageNum, pageSize, 0);
         }
 
         // 批量查被邀请人和邀请码
         List<Long> inviteeIds = relations.stream()
                 .map(InviteRelation::getInviteeId).distinct().toList();
-        Map<Long, User> userMap = userMapper.selectBatchIds(inviteeIds).stream()
+        Map<Long, User> userMap = userMapper.selectByIds(inviteeIds).stream()
                 .collect(Collectors.toMap(User::getId, u -> u));
 
         List<Long> codeIds = relations.stream()
-                .map(InviteRelation::getInviteCodeId).filter(id -> id != null).distinct().toList();
+                .map(InviteRelation::getInviteCodeId).filter(Objects::nonNull).distinct().toList();
         Map<Long, String> codeMap = codeIds.isEmpty() ? Map.of()
-                : inviteCodeMapper.selectBatchIds(codeIds).stream()
+                : inviteCodeMapper.selectByIds(codeIds).stream()
                         .collect(Collectors.toMap(InviteCode::getId, InviteCode::getCode));
 
         // 邀请人信息只查一次
@@ -929,7 +941,7 @@ public class InviteService {
     /**
      * 获取发放记录列表
      */
-    public Page<com.xuekairui.user.dto.CrawlerQuotaGrantResponse> listGrants(Long userId, String grantType,
+    public Page<CrawlerQuotaGrantResponse> listGrants(Long userId, String grantType,
                                                                                int page, int size) {
         return crawlerQuotaGrantService.listGrants(userId, grantType, page, size);
     }
@@ -1078,7 +1090,14 @@ public class InviteService {
     /**
      * 获取用户爬虫使用详情(含平台分布统计)
      */
-    public CrawlerUserDetailResponse getCrawlerUserDetail(Long userId) {
+    public CrawlerUserDetailResponse getCrawlerUserDetail(Long userId, String phone) {
+        if (userId == null && phone != null && !phone.isBlank()) {
+            User u = userMapper.selectOne(
+                    new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
+            if (u == null) throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+            userId = u.getId();
+        }
+        if (userId == null) throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID和手机号至少填写一个");
         User user = userMapper.selectById(userId);
         if (user == null) {
             throw new BusinessException(ErrorCode.USER_NOT_FOUND);
@@ -1154,7 +1173,18 @@ public class InviteService {
     /**
      * 获取用户邀请转化统计(点击-注册转化率)
      */
-    public InviteConversionStatsResponse getInviteConversionStats(Long userId) {
+    public InviteConversionStatsResponse getInviteConversionStats(Long userId, String phone) {
+        if (userId == null && phone != null && !phone.isBlank()) {
+            User u = userMapper.selectOne(
+                    new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
+            if (u == null) {
+                throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+            }
+            userId = u.getId();
+        }
+        if (userId == null) {
+            throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID和手机号至少填写一个");
+        }
         // 获取用户邀请码
         InviteCodeResponse codeResponse = getOrCreateInviteCode(userId);
 

+ 8 - 0
zhijiayun-payment/src/main/java/com/xuekairui/payment/controller/AdminPaymentController.java

@@ -22,6 +22,14 @@ public class AdminPaymentController {
 
     private final PaymentPlanService paymentPlanService;
 
+    /**
+     * 查询所有支付方案(含已下架)
+     */
+    @GetMapping("/plans")
+    public Result<java.util.List<PaymentPlanResponse>> listPlans() {
+        return Result.success(paymentPlanService.listAll());
+    }
+
     /**
      * 创建支付方案
      */

+ 10 - 0
zhijiayun-payment/src/main/java/com/xuekairui/payment/service/PaymentPlanService.java

@@ -36,6 +36,16 @@ public class PaymentPlanService {
         return plans.stream().map(this::toResponse).collect(Collectors.toList());
     }
 
+    /**
+     * 查询所有支付方案(含已下架,管理端用)
+     */
+    public List<PaymentPlanResponse> listAll() {
+        List<PaymentPlan> plans = paymentPlanMapper.selectList(
+                new LambdaQueryWrapper<PaymentPlan>()
+                        .orderByAsc(PaymentPlan::getSortOrder));
+        return plans.stream().map(this::toResponse).collect(Collectors.toList());
+    }
+
     /**
      * 根据ID查询方案(不检查状态)
      */

+ 2 - 1
zhijiayun-test/src/main/resources/static/invite-test.html

@@ -1628,7 +1628,8 @@ function renderAdminLicenseModule() {
 function renderAdminPaymentModule() {
     return `
     <div class="batch-buttons"><button class="btn btn-warning btn-sm" onclick="runAdminPaymentTests()">运行全部支付方案测试</button></div>
-    ${testSection('admin-payment-create', '创建方案', 'POST', 'post', '/api/admin/payment/plans', '需认证', 'auth-required',
+    ${testSection('admin-payment-list', '方案列表(含下架)', 'GET', 'get', '/api/admin/payment/plans', '需认证', 'auth-required', null, true, null, '查询所有支付方案(含已下架),管理端专用')}
+        ${testSection('admin-payment-create', '创建方案', 'POST', 'post', '/api/admin/payment/plans', '需认证', 'auth-required',
         [{label:'方案编码(planCode)', name:'planCode', placeholder:'MONTHLY_PLUS', default:'MONTHLY_PLUS'},
          {label:'方案名称(planName)', name:'planName', placeholder:'普通会员·月卡', default:'普通会员·月卡'},
          {label:'价格(price)', name:'price', placeholder:'9.90', default:'9.90'},

+ 3 - 6
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/CrawlerQuotaGrantMapper.java

@@ -45,12 +45,9 @@ public interface CrawlerQuotaGrantMapper extends BaseMapper<CrawlerQuotaGrant> {
     @Update("UPDATE t_crawler_quota_grant " +
             "SET used_count = used_count + #{count}, " +
             "status = CASE WHEN (used_count + #{count}) >= quota_count THEN 2 ELSE status END " +
-            "WHERE user_id = #{userId} AND status = 1 " +
-            "AND (quota_count - used_count) >= #{count} " +
-            "AND (expire_time IS NULL OR expire_time > NOW()) " +
-            "ORDER BY COALESCE(expire_time, TIMESTAMP '9999-12-31 00:00:00') ASC " +
-            "LIMIT 1")
-    int consumeQuotaGrant(@Param("userId") Long userId, @Param("count") int count);
+            "WHERE id = #{grantId} AND status = 1 " +
+            "AND (quota_count - used_count) >= #{count}")
+    int consumeQuotaGrant(@Param("grantId") Long grantId, @Param("count") int count);
 
     /**
      * 查找用户最早过期的可用发放记录(用于扣减)

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

@@ -48,15 +48,6 @@ public class CrawlerQuotaGrantService {
     // 发放配额
     // ==========================================
 
-    /**
-     * 购买会员奖励 - 发放爬虫次数
-     */
-    @Transactional
-    public CrawlerQuotaGrant grantPurchaseReward(Long userId, int quotaCount,
-                                                  LocalDateTime expireTime, String remark) {
-        return grantQuota(userId, TYPE_PURCHASE, quotaCount, null, expireTime, remark);
-    }
-
     /**
      * 邀请奖励 - 发放爬虫次数
      */
@@ -189,7 +180,7 @@ public class CrawlerQuotaGrantService {
                 allUserIds.add(g.getSourceUserId());
             }
         }
-        Map<Long, User> userMap = userMapper.selectBatchIds(new ArrayList<>(allUserIds))
+        Map<Long, User> userMap = userMapper.selectByIds(new ArrayList<>(allUserIds))
                 .stream().collect(Collectors.toMap(User::getId, u -> u));
 
         List<CrawlerQuotaGrantResponse> list = grants.stream().map(g -> {
@@ -239,11 +230,12 @@ public class CrawlerQuotaGrantService {
                 break; // 没有可用的额外配额了
             }
 
+            long grantId = ((Number) grant.get("id")).longValue();
             int grantRemaining = ((Number) grant.get("remaining")).intValue();
             int toConsume = Math.min(remaining, grantRemaining);
 
-            // 扣减这条记录
-            crawlerQuotaGrantMapper.consumeQuotaGrant(userId, toConsume);
+            // 按 grantId 精确定位扣减
+            crawlerQuotaGrantMapper.consumeQuotaGrant(grantId, toConsume);
             totalConsumed += toConsume;
             remaining -= toConsume;