Ver código fonte

feat: 后端生成推广二维码接口

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 3 meses atrás
pai
commit
9074fd6799

+ 2 - 0
num-server/pom.xml

@@ -25,6 +25,8 @@
         <dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.10.1</version></dependency>
         <dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>test</scope></dependency>
         <dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.30</version></dependency>
+        <dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.2</version></dependency>
+        <dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.5.2</version></dependency>
     </dependencies>
     <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
 </project>

+ 188 - 167
num-server/src/main/java/com/etotem/num/controller/ProfileController.java

@@ -1,167 +1,188 @@
-package com.etotem.num.controller;
-
-import com.etotem.num.common.Result;
-import com.etotem.num.entity.User;
-import com.etotem.num.service.ChartService;
-import com.etotem.num.service.CommissionService;
-import com.etotem.num.service.ConfigService;
-import com.etotem.num.service.UserService;
-import javax.servlet.http.HttpServletRequest;
-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 java.time.LocalDate;
-import java.util.HashMap;
-import java.util.Map;
-
-@RestController
-@RequestMapping("/api/profile")
-public class ProfileController {
-
-    private final UserService userService;
-    private final CommissionService commissionService;
-    private final ChartService chartService;
-    private final ConfigService configService;
-
-    public ProfileController(UserService userService, CommissionService commissionService,
-                             ChartService chartService, ConfigService configService) {
-        this.userService = userService;
-        this.commissionService = commissionService;
-        this.chartService = chartService;
-        this.configService = configService;
-    }
-
-    @PostMapping("/info")
-    public Result<Map<String, Object>> getInfo(HttpServletRequest request) {
-        Long userId = (Long) request.getAttribute("userId");
-        User user = userService.getById(userId);
-        Map<String, Object> info = new HashMap<>();
-        info.put("id", user.getId());
-        info.put("nickname", user.getNickname());
-        info.put("avatarUrl", user.getAvatarUrl());
-        info.put("referralCode", user.getReferralCode());
-        info.put("isAdmin", user.getIsAdmin());
-        info.put("vipEndTime", user.getVipEndTime());
-        info.put("isVip", userService.isVip(userId));
-        info.put("isSeedPriceUser", userService.isSeedPriceUser(userId));
-        info.put("dailyChartCount", user.getDailyChartCount());
-        info.put("dailyChatCount", user.getDailyChatCount());
-        // US-5.1 profile fields
-        info.put("gender", user.getGender());
-        info.put("birthDate", user.getBirthDate());
-        info.put("bio", user.getBio());
-        info.put("city", user.getCity());
-        info.put("tags", user.getTags());
-        info.put("lookingFor", user.getLookingFor());
-        info.put("profileComplete", user.getProfileComplete());
-        info.put("birthYear", user.getBirthYear());
-        info.put("birthMonth", user.getBirthMonth());
-        info.put("birthDay", user.getBirthDay());
-        return Result.success(info);
-    }
-
-    /**
-     * Complete user profile on first registration (US-5.1).
-     */
-    @PostMapping("/complete")
-    public Result<?> completeProfile(HttpServletRequest request, @RequestBody Map<String, Object> body) {
-        Long userId = (Long) request.getAttribute("userId");
-        Integer gender = body.containsKey("gender") ? ((Number) body.get("gender")).intValue() : null;
-        String birthDateStr = (String) body.get("birthDate");
-        LocalDate birthDate = birthDateStr != null ? LocalDate.parse(birthDateStr) : null;
-        String bio = (String) body.get("bio");
-        String city = (String) body.get("city");
-        String tags = (String) body.get("tags");
-        Integer lookingFor = body.containsKey("lookingFor") ? ((Number) body.get("lookingFor")).intValue() : null;
-        String nickname = (String) body.get("nickname");
-        String avatarUrl = (String) body.get("avatarUrl");
-        userService.completeProfile(userId, gender, birthDate, bio, city, tags, lookingFor, nickname, avatarUrl);
-        return Result.success(null);
-    }
-
-    /**
-     * Update individual profile fields (US-5.3).
-     */
-    @PostMapping("/update")
-    public Result<?> updateProfile(HttpServletRequest request, @RequestBody Map<String, Object> body) {
-        Long userId = (Long) request.getAttribute("userId");
-        Integer gender = body.containsKey("gender") ? ((Number) body.get("gender")).intValue() : null;
-        String birthDateStr = (String) body.get("birthDate");
-        LocalDate birthDate = birthDateStr != null ? LocalDate.parse(birthDateStr) : null;
-        String bio = (String) body.get("bio");
-        String city = (String) body.get("city");
-        String tags = (String) body.get("tags");
-        Integer lookingFor = body.containsKey("lookingFor") ? ((Number) body.get("lookingFor")).intValue() : null;
-        String nickname = (String) body.get("nickname");
-        String avatarUrl = (String) body.get("avatarUrl");
-        userService.completeProfile(userId, gender, birthDate, bio, city, tags, lookingFor, nickname, avatarUrl);
-        return Result.success(null);
-    }
-
-    @PostMapping("/vip-status")
-    public Result<Map<String, Object>> vipStatus(HttpServletRequest request) {
-        Long userId = (Long) request.getAttribute("userId");
-        boolean isVip = userService.isVip(userId);
-        Map<String, Object> result = new HashMap<>();
-        result.put("isVip", isVip);
-        User user = userService.getById(userId);
-        result.put("vipEndTime", user.getVipEndTime());
-        return Result.success(result);
-    }
-
-    @PostMapping("/quota")
-    public Result<Map<String, Object>> quota(HttpServletRequest request) {
-        Long userId = (Long) request.getAttribute("userId");
-        // Reset daily counters if a new day has arrived, so frontend sees fresh data
-        User user = userService.getAndResetDailyQuota(userId);
-        boolean isVip = userService.isVip(userId);
-        Map<String, Object> result = new HashMap<>();
-        result.put("isVip", isVip);
-        result.put("dailyChartCount", user.getDailyChartCount());
-        result.put("dailyChatCount", user.getDailyChatCount());
-        result.put("dailyShareCount", user.getDailyShareCount());
-        result.put("chartLimit", isVip ? -1 : configService.getQuota("chart"));
-        result.put("chatLimit", isVip ? -1 : configService.getQuota("chat"));
-        result.put("shareLimit", isVip ? -1 : configService.getQuota("share"));
-        result.put("dailyAcademicCount", user.getDailyAcademicCount());
-        result.put("academicLimit", isVip ? -1 : configService.getQuota("academic"));
-        return Result.success(result);
-    }
-
-    /**
-     * Track a share action and consume share quota (US-2.1).
-     */
-    @PostMapping("/share/track")
-    public Result<Map<String, Object>> trackShare(HttpServletRequest request) {
-        Long userId = (Long) request.getAttribute("userId");
-        userService.consumeQuota(userId, "share");
-        User user = userService.getById(userId);
-        boolean isVip = userService.isVip(userId);
-        int shareLimit = isVip ? -1 : configService.getQuota("share");
-        Map<String, Object> result = new HashMap<>();
-        result.put("dailyShareCount", user.getDailyShareCount());
-        result.put("shareLimit", shareLimit);
-        result.put("exceeded", !isVip && shareLimit > 0 && user.getDailyShareCount() >= shareLimit);
-        return Result.success(result);
-    }
-
-    @PostMapping("/commission-summary")
-    public Result<Map<String, Object>> commissionSummary(HttpServletRequest request) {
-        Long userId = (Long) request.getAttribute("userId");
-        Map<String, Object> result = new HashMap<>();
-        result.put("totalEarnings", commissionService.getTotalCommission(userId));
-        result.put("availableBalance", commissionService.getAvailableBalance(userId));
-        return Result.success(result);
-    }
-
-    @PostMapping("/chart-count")
-    public Result<Map<String, Object>> chartCount(HttpServletRequest request) {
-        Long userId = (Long) request.getAttribute("userId");
-        int count = chartService.getByUserId(userId).size();
-        Map<String, Object> result = new HashMap<>();
-        result.put("count", count);
-        return Result.success(result);
-    }
-}
+package com.etotem.num.controller;
+
+import com.etotem.num.common.Result;
+import com.etotem.num.entity.User;
+import com.etotem.num.service.ChartService;
+import com.etotem.num.service.CommissionService;
+import com.etotem.num.service.ConfigService;
+import com.etotem.num.service.QrCodeService;
+import com.etotem.num.service.UserService;
+import javax.servlet.http.HttpServletRequest;
+import org.springframework.web.bind.annotation.GetMapping;
+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 java.time.LocalDate;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/profile")
+public class ProfileController {
+
+    private final UserService userService;
+    private final CommissionService commissionService;
+    private final ChartService chartService;
+    private final ConfigService configService;
+    private final QrCodeService qrCodeService;
+
+    public ProfileController(UserService userService, CommissionService commissionService,
+                             ChartService chartService, ConfigService configService,
+                             QrCodeService qrCodeService) {
+        this.userService = userService;
+        this.commissionService = commissionService;
+        this.chartService = chartService;
+        this.configService = configService;
+        this.qrCodeService = qrCodeService;
+    }
+
+    @PostMapping("/info")
+    public Result<Map<String, Object>> getInfo(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        User user = userService.getById(userId);
+        Map<String, Object> info = new HashMap<>();
+        info.put("id", user.getId());
+        info.put("nickname", user.getNickname());
+        info.put("avatarUrl", user.getAvatarUrl());
+        info.put("referralCode", user.getReferralCode());
+        info.put("isAdmin", user.getIsAdmin());
+        info.put("vipEndTime", user.getVipEndTime());
+        info.put("isVip", userService.isVip(userId));
+        info.put("isSeedPriceUser", userService.isSeedPriceUser(userId));
+        info.put("dailyChartCount", user.getDailyChartCount());
+        info.put("dailyChatCount", user.getDailyChatCount());
+        // US-5.1 profile fields
+        info.put("gender", user.getGender());
+        info.put("birthDate", user.getBirthDate());
+        info.put("bio", user.getBio());
+        info.put("city", user.getCity());
+        info.put("tags", user.getTags());
+        info.put("lookingFor", user.getLookingFor());
+        info.put("profileComplete", user.getProfileComplete());
+        info.put("birthYear", user.getBirthYear());
+        info.put("birthMonth", user.getBirthMonth());
+        info.put("birthDay", user.getBirthDay());
+        return Result.success(info);
+    }
+
+    /**
+     * Complete user profile on first registration (US-5.1).
+     */
+    @PostMapping("/complete")
+    public Result<?> completeProfile(HttpServletRequest request, @RequestBody Map<String, Object> body) {
+        Long userId = (Long) request.getAttribute("userId");
+        Integer gender = body.containsKey("gender") ? ((Number) body.get("gender")).intValue() : null;
+        String birthDateStr = (String) body.get("birthDate");
+        LocalDate birthDate = birthDateStr != null ? LocalDate.parse(birthDateStr) : null;
+        String bio = (String) body.get("bio");
+        String city = (String) body.get("city");
+        String tags = (String) body.get("tags");
+        Integer lookingFor = body.containsKey("lookingFor") ? ((Number) body.get("lookingFor")).intValue() : null;
+        String nickname = (String) body.get("nickname");
+        String avatarUrl = (String) body.get("avatarUrl");
+        userService.completeProfile(userId, gender, birthDate, bio, city, tags, lookingFor, nickname, avatarUrl);
+        return Result.success(null);
+    }
+
+    /**
+     * Update individual profile fields (US-5.3).
+     */
+    @PostMapping("/update")
+    public Result<?> updateProfile(HttpServletRequest request, @RequestBody Map<String, Object> body) {
+        Long userId = (Long) request.getAttribute("userId");
+        Integer gender = body.containsKey("gender") ? ((Number) body.get("gender")).intValue() : null;
+        String birthDateStr = (String) body.get("birthDate");
+        LocalDate birthDate = birthDateStr != null ? LocalDate.parse(birthDateStr) : null;
+        String bio = (String) body.get("bio");
+        String city = (String) body.get("city");
+        String tags = (String) body.get("tags");
+        Integer lookingFor = body.containsKey("lookingFor") ? ((Number) body.get("lookingFor")).intValue() : null;
+        String nickname = (String) body.get("nickname");
+        String avatarUrl = (String) body.get("avatarUrl");
+        userService.completeProfile(userId, gender, birthDate, bio, city, tags, lookingFor, nickname, avatarUrl);
+        return Result.success(null);
+    }
+
+    @PostMapping("/vip-status")
+    public Result<Map<String, Object>> vipStatus(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        boolean isVip = userService.isVip(userId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("isVip", isVip);
+        User user = userService.getById(userId);
+        result.put("vipEndTime", user.getVipEndTime());
+        return Result.success(result);
+    }
+
+    @PostMapping("/quota")
+    public Result<Map<String, Object>> quota(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        // Reset daily counters if a new day has arrived, so frontend sees fresh data
+        User user = userService.getAndResetDailyQuota(userId);
+        boolean isVip = userService.isVip(userId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("isVip", isVip);
+        result.put("dailyChartCount", user.getDailyChartCount());
+        result.put("dailyChatCount", user.getDailyChatCount());
+        result.put("dailyShareCount", user.getDailyShareCount());
+        result.put("chartLimit", isVip ? -1 : configService.getQuota("chart"));
+        result.put("chatLimit", isVip ? -1 : configService.getQuota("chat"));
+        result.put("shareLimit", isVip ? -1 : configService.getQuota("share"));
+        result.put("dailyAcademicCount", user.getDailyAcademicCount());
+        result.put("academicLimit", isVip ? -1 : configService.getQuota("academic"));
+        return Result.success(result);
+    }
+
+    /**
+     * Track a share action and consume share quota (US-2.1).
+     */
+    @PostMapping("/share/track")
+    public Result<Map<String, Object>> trackShare(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        userService.consumeQuota(userId, "share");
+        User user = userService.getById(userId);
+        boolean isVip = userService.isVip(userId);
+        int shareLimit = isVip ? -1 : configService.getQuota("share");
+        Map<String, Object> result = new HashMap<>();
+        result.put("dailyShareCount", user.getDailyShareCount());
+        result.put("shareLimit", shareLimit);
+        result.put("exceeded", !isVip && shareLimit > 0 && user.getDailyShareCount() >= shareLimit);
+        return Result.success(result);
+    }
+
+    @PostMapping("/commission-summary")
+    public Result<Map<String, Object>> commissionSummary(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalEarnings", commissionService.getTotalCommission(userId));
+        result.put("availableBalance", commissionService.getAvailableBalance(userId));
+        return Result.success(result);
+    }
+
+    @PostMapping("/chart-count")
+    public Result<Map<String, Object>> chartCount(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        int count = chartService.getByUserId(userId).size();
+        Map<String, Object> result = new HashMap<>();
+        result.put("count", count);
+        return Result.success(result);
+    }
+
+    /**
+     * Generate a QR code image URL for the user's referral/promotion page.
+     * GET /api/profile/qrcode
+     */
+    @GetMapping("/qrcode")
+    public Result<Map<String, Object>> getQrCode(HttpServletRequest request) {
+        Long userId = (Long) request.getAttribute("userId");
+        User user = userService.getById(userId);
+        String referralCode = user.getReferralCode();
+        String qrUrl = qrCodeService.generateQrCode(referralCode);
+        Map<String, Object> result = new HashMap<>();
+        result.put("qrCodeUrl", qrUrl);
+        result.put("referralCode", referralCode);
+        return Result.success(result);
+    }
+}

+ 61 - 0
num-server/src/main/java/com/etotem/num/service/QrCodeService.java

@@ -0,0 +1,61 @@
+package com.etotem.num.service;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.WriterException;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Service
+public class QrCodeService {
+
+    @Value("${upload.dir:uploads}")
+    private String uploadDir;
+
+    @Value("${app.base-url:http://localhost:8186}")
+    private String baseUrl;
+
+    private static final int QR_SIZE = 280;
+
+    /**
+     * Generates a QR code image for the given referral code.
+     * @param referralCode the user's referral code
+     * @return the relative URL path to the generated QR code image, e.g. "/uploads/qr/abc123.png"
+     */
+    public String generateQrCode(String referralCode) {
+        try {
+            String url = baseUrl + "/pages/index/index?ref=" + referralCode;
+            QRCodeWriter qrCodeWriter = new QRCodeWriter();
+            Map<EncodeHintType, Object> hints = new HashMap<>();
+            hints.put(EncodeHintType.MARGIN, 2);
+            BitMatrix bitMatrix = qrCodeWriter.encode(url, BarcodeFormat.QR_CODE, QR_SIZE, QR_SIZE, hints);
+            BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
+
+            // Ensure directory exists
+            Path dir = Paths.get(uploadDir, "qr");
+            Files.createDirectories(dir);
+
+            String filename = UUID.randomUUID().toString() + ".png";
+            Path filePath = dir.resolve(filename);
+            ImageIO.write(image, "PNG", filePath.toFile());
+
+            return "/uploads/qr/" + filename;
+        } catch (WriterException | IOException e) {
+            throw new RuntimeException("Failed to generate QR code", e);
+        }
+    }
+}