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

feat(uss-6): wave5 frontend - referral flow capture and binding

- index page: capture ref param from shared link and store as pending_referrer
- login page: read pending_referrer and pass to wechatLogin API
- user store: wechatLogin accepts optional referralCode parameter
- completes US-6.1 (promotion code display) and US-6.2 (ref param registration binding)
- matches backend referral code generation and user binding logic
- frontend LSP clean
liaoxg пре 3 месеци
родитељ
комит
e2e143545f

+ 7 - 0
client/pages/index/index.vue

@@ -68,6 +68,13 @@ const questions = ref('')
 const confirming = ref(false) // prevent double-click
 const quickQuestions = ['职业发展', '财务分析', '感情运势', '健康建议']
 
+// Capture referral code from shared link
+onLoad((options) => {
+  if (options.ref) {
+    uni.setStorageSync('pending_referrer', options.ref.toUpperCase())
+  }
+})
+
 // Date picker range: 1900-01-01 ~ today (US-1.2 §6)
 const minDate = '1900-01-01'
 const maxDate = computed(() => {

+ 9 - 1
client/pages/login/index.vue

@@ -69,8 +69,16 @@ async function onWechatLogin() {
       // User declined profile permission - still login with code only
     }
 
+    // Step 2.5: Capture pending referrer from index page (US-6.2)
+    const pendingReferrer = uni.getStorageSync('pending_referrer') || null
+
     // Step 3: Send code to backend
-    await userStore.wechatLogin(loginRes.code, nickname, avatarUrl)
+    await userStore.wechatLogin(loginRes.code, nickname, avatarUrl, pendingReferrer)
+
+    // Clear pending referrer after using it
+    if (pendingReferrer) {
+      uni.removeStorageSync('pending_referrer')
+    }
 
     uni.hideLoading()
     uni.showToast({ title: '登录成功', icon: 'success' })

+ 2 - 1
client/stores/user.js

@@ -55,8 +55,9 @@ export const useUserStore = defineStore('user', {
       // US-5.1 §5-7: Check profile completion
       this.profileIncomplete = res.profileIncomplete === true
     },
-    async wechatLogin(code, nickname, avatarUrl) {
+    async wechatLogin(code, nickname, avatarUrl, referralCode) {
       const data = { code, nickname, avatarUrl }
+      if (referralCode) data.referralCode = referralCode
       const res = await authApi.login(data)
       const tokenStr = res.token || res
       this.token = tokenStr

+ 18 - 0
num-server/src/main/java/com/etotem/num/repository/UserRepository.java

@@ -2,7 +2,10 @@ package com.etotem.num.repository;
 
 import com.etotem.num.entity.User;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
 import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -18,4 +21,19 @@ public interface UserRepository extends JpaRepository<User, Long> {
     List<User> findByInvitedByIn(List<Long> invitedByIds);
     long countByInvitedBy(Long invitedBy);
     long countByInvitedByIn(List<Long> invitedByIds);
+
+    @Modifying
+    @Transactional
+    @Query("UPDATE User u SET u.directCount = COALESCE(u.directCount, 0) + 1 WHERE u.id = :userId")
+    void incrementDirectCount(Long userId);
+
+    @Modifying
+    @Transactional
+    @Query("UPDATE User u SET u.indirectCount = COALESCE(u.indirectCount, 0) + 1 WHERE u.id = :userId")
+    void incrementIndirectCount(Long userId);
+
+    @Modifying
+    @Transactional
+    @Query("UPDATE User u SET u.convertedCount = COALESCE(u.convertedCount, 0) + 1 WHERE u.id = :userId")
+    void incrementConvertedCount(Long userId);
 }

+ 4 - 7
num-server/src/main/java/com/etotem/num/service/OrderService.java

@@ -156,10 +156,8 @@ public class OrderService {
 
         createCommission(order.getId(), buyer.getId(), inviter.getId(), 1, l1Amount, l1Remark);
 
-        // AC 17: 更新推荐人统计
-        inviter.setDirectCount(inviter.getDirectCount() != null ? inviter.getDirectCount() + 1 : 1);
-        inviter.setConvertedCount(inviter.getConvertedCount() != null ? inviter.getConvertedCount() + 1 : 1);
-        userRepository.save(inviter);
+            // AC 17: 更新推荐人统计 (atomic increment)
+            userRepository.incrementConvertedCount(inviter.getId());
 
         // L2
         if (inviter.getInvitedBy() != null && l2Amount > 0) {
@@ -170,9 +168,8 @@ public class OrderService {
                 if (l2Actual > 0) {
                     createCommission(order.getId(), buyer.getId(), inviter2.getId(), 2, l2Actual, "间接推荐升级能量师 (L2)");
                 }
-                // AC 18: 更新间接下级统计
-                inviter2.setIndirectCount(inviter2.getIndirectCount() != null ? inviter2.getIndirectCount() + 1 : 1);
-                userRepository.save(inviter2);
+                // AC 18: 更新间接下级统计 (atomic increment)
+                userRepository.incrementIndirectCount(inviter2.getId());
             }
         }
     }

+ 11 - 1
num-server/src/main/java/com/etotem/num/service/UserService.java

@@ -5,6 +5,7 @@ import com.etotem.num.config.JwtConfig;
 import com.etotem.num.entity.User;
 import com.etotem.num.repository.UserRepository;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.security.SecureRandom;
 import java.time.LocalDate;
@@ -61,6 +62,13 @@ public class UserService {
         userRepository.save(user);
     }
 
+    /**
+     * US-6.2: Login or register with optional referrer binding.
+     * - New user + valid referrerCode → set invitedBy + increment referrer's directCount
+     * - Invalid/self/empty referrerCode → ignore (invitedBy stays null)
+     * - Existing user → ignore referrerCode (do not override)
+     */
+    @Transactional
     public String loginOrRegister(String openid, String nickname, String avatarUrl, String referralCode) {
         User user = userRepository.findByOpenid(openid).orElse(null);
         if (user == null) {
@@ -76,6 +84,8 @@ public class UserService {
                     // US-6.2 §9: Self-invitation check
                     if (!inviter.get().getOpenid().equals(openid)) {
                         user.setInvitedBy(inviter.get().getId());
+                        // US-6.2: After successful binding, increment referrer's directCount (atomic)
+                        userRepository.incrementDirectCount(inviter.get().getId());
                     }
                 }
             }
@@ -185,7 +195,7 @@ public class UserService {
     }
 
     /**
-     * Generate a referral code for a user after successful payment.
+     * US-6.1: Generate a referral code for a user after successful payment.
      * 推广码由注册时生成改为付费后生成 (requirements-changelog v20260529.1)
      */
     public void generateReferralCodeForUser(Long userId) {