docs: refine wechat auth design and plan

This commit is contained in:
2026-05-31 23:37:02 +08:00
parent 280c3d5e7d
commit 2189ed24e6
2 changed files with 459 additions and 53 deletions
@@ -17,6 +17,7 @@
| Action | File | Responsibility |
| --- | --- | --- |
| Create | `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java` | Binds WeChat mini program appid, secret, and API base URL from application config. |
| Create | `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql` | Prevents duplicate non-null WeChat third-party identities. |
| Modify | `backend-single/src/main/resources/application.yml` | Adds default WeChat config keys and public login endpoint allowlist. |
| Modify | `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java` | Excludes `/auth/wechat/login` from JWT interception. |
| Modify | `backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java` | Excludes `/api/auth/wechat/login` from the interceptor's internal public endpoint list. |
@@ -27,6 +28,9 @@
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java` | Internal DTO for WeChat phone number response. |
| Create | `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java` | Interface for WeChat API calls. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | RestTemplate-backed WeChat client with Redis token caching. |
| Create | `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java` | Defines temporary WeChat user to phone user data transfer. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java` | Transfers user-owned rows and avoids singleton row conflicts. |
| Modify | `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java` | Adds non-sensitive `wechatBound` flag for frontend display logic. |
| Modify | `backend-single/src/main/java/com/emotion/service/UserService.java` | Adds third-party identity lookup helper. |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java` | Implements third-party identity lookup. |
| Modify | `backend-single/src/main/java/com/emotion/service/AuthService.java` | Adds WeChat login and phone binding operations. |
@@ -45,11 +49,13 @@
**Files:**
- Create: `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`
- Create: `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Test: backend compile
@@ -217,7 +223,43 @@ In `backend-single/src/main/resources/application.yml`, under `emotion:`, add:
base-url: https://api.weixin.qq.com
```
- [ ] **Step 5: Compile backend**
- [ ] **Step 5: Add WeChat identity uniqueness migration**
Before applying the migration in a shared environment, run:
```sql
SELECT third_party_type, third_party_id, COUNT(*) AS count
FROM t_user
WHERE third_party_type IS NOT NULL
AND third_party_id IS NOT NULL
AND is_deleted = 0
GROUP BY third_party_type, third_party_id
HAVING COUNT(*) > 1;
```
Expected: no rows. If rows exist, resolve duplicates before adding the index.
Create `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`:
```sql
ALTER TABLE t_user
ADD UNIQUE KEY uk_user_third_party_identity (third_party_type, third_party_id);
```
MySQL permits multiple `NULL` values in a unique index, so users without WeChat binding are unaffected. Implementation must clear a temporary WeChat user's third-party fields before transferring the openid to another user.
- [ ] **Step 6: Add non-sensitive WeChat binding flag**
In `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`, add:
```java
/**
* Whether the account has a WeChat mini program identity bound.
*/
private Boolean wechatBound;
```
- [ ] **Step 7: Compile backend**
Run:
@@ -228,15 +270,17 @@ mvn -q -DskipTests compile
Expected: command exits with status `0`.
- [ ] **Step 6: Commit**
- [ ] **Step 8: Commit**
```bash
git add backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
git add backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql
git add backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/main/resources/application.yml
git commit -m "feat: add wechat mini program auth DTOs and config"
```
@@ -374,7 +418,8 @@ public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
throw new BusinessException("微信接口调用凭证获取失败");
}
long ttl = Math.max(60L, response.getExpiresIn() - ACCESS_TOKEN_CACHE_BUFFER_SECONDS);
long expiresIn = response.getExpiresIn() == null ? 7200L : response.getExpiresIn();
long ttl = Math.max(60L, expiresIn - ACCESS_TOKEN_CACHE_BUFFER_SECONDS);
redisTemplate.opsForValue().set(ACCESS_TOKEN_CACHE_KEY, response.getAccessToken(), ttl, TimeUnit.SECONDS);
return response.getAccessToken();
}
@@ -442,13 +487,170 @@ git commit -m "feat: add wechat mini program API client"
---
### Task 3: Backend User Lookup And Auth Service Methods
### Task 3: Backend User Identity Merge Service
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`
- [ ] **Step 1: Create merge service interface**
Create `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`:
```java
package com.emotion.service;
public interface UserIdentityMergeService {
void mergeTemporaryWechatUserIntoPhoneUser(String temporaryWechatUserId, String phoneUserId);
}
```
- [ ] **Step 2: Create JdbcTemplate implementation**
Create `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`:
```java
package com.emotion.service.impl;
import com.emotion.service.UserIdentityMergeService;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserIdentityMergeServiceImpl implements UserIdentityMergeService {
private final JdbcTemplate jdbcTemplate;
private static final List<String> DIRECT_TRANSFER_TABLES = List.of(
"t_life_event",
"t_epic_script",
"t_life_path",
"t_life_path_step",
"t_social_content_item",
"t_social_profile_insight",
"t_user_consent_log",
"t_tts_task",
"t_analytics_event",
"t_conversation",
"t_message",
"t_coze_api_call",
"t_emotion_analysis",
"t_emotion_record",
"t_diary_post",
"t_diary_comment",
"t_community_post",
"t_comment",
"t_ai_call_log"
);
private static final List<String> SINGLETON_TABLES = List.of(
"t_user_profile",
"t_user_stats"
);
@Override
@Transactional(rollbackFor = Exception.class)
public void mergeTemporaryWechatUserIntoPhoneUser(String temporaryWechatUserId, String phoneUserId) {
if (!StringUtils.hasText(temporaryWechatUserId)
|| !StringUtils.hasText(phoneUserId)
|| temporaryWechatUserId.equals(phoneUserId)) {
return;
}
for (String table : DIRECT_TRANSFER_TABLES) {
updateUserId(table, temporaryWechatUserId, phoneUserId);
}
for (String table : SINGLETON_TABLES) {
transferSingletonIfTargetMissing(table, temporaryWechatUserId, phoneUserId);
}
}
private void updateUserId(String table, String fromUserId, String toUserId) {
jdbcTemplate.update("UPDATE " + table + " SET user_id = ? WHERE user_id = ?", toUserId, fromUserId);
}
private void transferSingletonIfTargetMissing(String table, String fromUserId, String toUserId) {
Integer targetCount = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM " + table + " WHERE user_id = ? AND is_deleted = 0",
Integer.class,
toUserId
);
if (targetCount != null && targetCount > 0) {
return;
}
updateUserId(table, fromUserId, toUserId);
}
}
```
- [ ] **Step 3: Add merge service test**
Create `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`:
```java
package com.emotion.service;
import com.emotion.service.impl.UserIdentityMergeServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class UserIdentityMergeServiceImplTest {
@Test
void mergeTransfersDirectTablesAndChecksSingletons() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.queryForObject(any(String.class), eq(Integer.class), eq("phone-user"))).thenReturn(0);
UserIdentityMergeService service = new UserIdentityMergeServiceImpl(jdbcTemplate);
service.mergeTemporaryWechatUserIntoPhoneUser("wx-temp", "phone-user");
verify(jdbcTemplate, atLeastOnce()).update(any(String.class), eq("phone-user"), eq("wx-temp"));
verify(jdbcTemplate, atLeastOnce()).queryForObject(any(String.class), eq(Integer.class), eq("phone-user"));
}
}
```
- [ ] **Step 4: Run merge service test**
```bash
cd backend-single
mvn -q -Dtest=UserIdentityMergeServiceImplTest test
```
Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java
git commit -m "feat: add wechat user identity merge service"
```
---
### Task 4: Backend User Lookup And Auth Service Methods
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/UserService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/service/AuthService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Test: `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`
- [ ] **Step 1: Add third-party lookup to UserService**
@@ -494,6 +696,7 @@ import com.emotion.dto.request.WechatLoginRequest;
import com.emotion.dto.request.WechatPhoneBindRequest;
import com.emotion.dto.wechat.WechatCodeSessionResponse;
import com.emotion.dto.wechat.WechatPhoneNumberResponse;
import com.emotion.service.UserIdentityMergeService;
import com.emotion.service.WechatMiniProgramService;
```
@@ -502,6 +705,9 @@ Add field:
```java
@Autowired
private WechatMiniProgramService wechatMiniProgramService;
@Autowired
private UserIdentityMergeService userIdentityMergeService;
```
Add constant:
@@ -532,7 +738,16 @@ private AuthResponse buildAuthResponse(User user) {
Then replace the duplicate token response block in `login`, `register`, and `refreshToken` with `return buildAuthResponse(user);`.
- [ ] **Step 5: Implement WeChat login**
- [ ] **Step 5: Add wechatBound to user info conversion**
In `AuthServiceImpl.convertToUserInfoResponse`, after copying properties and setting times, add:
```java
response.setWechatBound(WECHAT_MP_TYPE.equals(user.getThirdPartyType())
&& StringUtils.hasText(user.getThirdPartyId()));
```
- [ ] **Step 6: Implement WeChat login**
In `AuthServiceImpl`, add:
@@ -582,7 +797,7 @@ private User autoRegisterWechatUser(String openid, String nickname, String avata
}
```
- [ ] **Step 6: Implement phone binding and auto-identification**
- [ ] **Step 7: Implement phone binding, data transfer, and auto-identification**
In `AuthServiceImpl`, add:
@@ -618,8 +833,16 @@ public AuthResponse bindWechatPhone(String currentUserId, WechatPhoneBindRequest
throw new BusinessException("该手机号已绑定其他微信账号");
}
String openid = currentUser.getThirdPartyId();
userIdentityMergeService.mergeTemporaryWechatUserIntoPhoneUser(currentUser.getId(), phoneUser.getId());
currentUser.setThirdPartyType(null);
currentUser.setThirdPartyId(null);
currentUser.setStatus(0);
userService.updateById(currentUser);
phoneUser.setThirdPartyType(WECHAT_MP_TYPE);
phoneUser.setThirdPartyId(currentUser.getThirdPartyId());
phoneUser.setThirdPartyId(openid);
if (!StringUtils.hasText(phoneUser.getAvatar()) && StringUtils.hasText(currentUser.getAvatar())) {
phoneUser.setAvatar(currentUser.getAvatar());
}
@@ -632,7 +855,7 @@ public AuthResponse bindWechatPhone(String currentUserId, WechatPhoneBindRequest
}
```
- [ ] **Step 7: Compile backend**
- [ ] **Step 8: Compile backend**
```bash
cd backend-single
@@ -641,53 +864,165 @@ mvn -q -DskipTests compile
Expected: command exits with status `0`.
- [ ] **Step 8: Add service tests for identity rules**
- [ ] **Step 9: Add service tests for identity rules**
Create `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java` with a Spring test or Mockito-based test that covers:
Create `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`:
```java
@Test
void wechatLoginCreatesUserForNewOpenid() {
// Mock WechatMiniProgramService.code2Session to return openid.
// Mock UserService.getByThirdParty to return null.
// Mock UserService.createUser to return a new enabled user.
// Assert AuthResponse has accessToken and userInfo.id from the new user.
}
package com.emotion.service;
@Test
void bindWechatPhoneReturnsExistingPhoneUser() {
// Current user has thirdPartyType=wechat-mp and thirdPartyId=openid-a.
// Phone API returns 13800138000.
// UserService.getByPhone returns a different enabled user without thirdPartyId.
// Assert returned AuthResponse userInfo.id is the phone user's id.
import com.emotion.dto.request.WechatLoginRequest;
import com.emotion.dto.request.WechatPhoneBindRequest;
import com.emotion.dto.response.AuthResponse;
import com.emotion.dto.wechat.WechatCodeSessionResponse;
import com.emotion.dto.wechat.WechatPhoneNumberResponse;
import com.emotion.entity.User;
import com.emotion.service.impl.AuthServiceImpl;
import com.emotion.util.JwtUtil;
import com.emotion.util.TokenUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AuthServiceWechatTest {
@Mock
private UserService userService;
@Mock
private WechatMiniProgramService wechatMiniProgramService;
@Mock
private UserIdentityMergeService userIdentityMergeService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private ValueOperations<String, Object> valueOperations;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtUtil jwtUtil;
@Mock
private TokenUtil tokenUtil;
@InjectMocks
private AuthServiceImpl authService;
@BeforeEach
void setUp() {
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(jwtUtil.generateToken(anyString(), anyString())).thenReturn("access-token");
when(jwtUtil.generateRefreshToken(anyString(), anyString())).thenReturn("refresh-token");
when(passwordEncoder.encode(anyString())).thenReturn("encoded-password");
}
@Test
void wechatLoginCreatesUserForNewOpenid() {
WechatCodeSessionResponse session = new WechatCodeSessionResponse();
session.setOpenid("openid-a");
when(wechatMiniProgramService.code2Session("login-code")).thenReturn(session);
when(userService.getByThirdParty("wechat-mp", "openid-a")).thenReturn(null);
User created = new User();
created.setId("wx-user");
created.setUsername("wx-name");
created.setNickname("wx-name");
created.setStatus(1);
created.setIsDeleted(0);
when(userService.createUser(anyString(), anyString(), anyString(), eq(null), eq(null))).thenReturn(created);
WechatLoginRequest request = new WechatLoginRequest();
request.setCode("login-code");
AuthResponse response = authService.wechatLogin(request);
assertEquals("access-token", response.getAccessToken());
assertEquals("wx-user", response.getUserInfo().getId());
verify(userService).updateById(created);
}
@Test
void bindWechatPhoneReturnsExistingPhoneUserAndTransfersTemporaryData() {
User temporaryWechatUser = new User();
temporaryWechatUser.setId("wx-temp");
temporaryWechatUser.setUsername("wx-temp-name");
temporaryWechatUser.setThirdPartyType("wechat-mp");
temporaryWechatUser.setThirdPartyId("openid-a");
temporaryWechatUser.setStatus(1);
temporaryWechatUser.setIsDeleted(0);
when(userService.getById("wx-temp")).thenReturn(temporaryWechatUser);
WechatPhoneNumberResponse.PhoneInfo phoneInfo = new WechatPhoneNumberResponse.PhoneInfo();
phoneInfo.setPhoneNumber("13800138000");
phoneInfo.setPurePhoneNumber("13800138000");
when(wechatMiniProgramService.getPhoneNumber("phone-code")).thenReturn(phoneInfo);
User phoneUser = new User();
phoneUser.setId("phone-user");
phoneUser.setUsername("phone-name");
phoneUser.setPhone("13800138000");
phoneUser.setStatus(1);
phoneUser.setIsDeleted(0);
when(userService.getByPhone("13800138000")).thenReturn(phoneUser);
WechatPhoneBindRequest request = new WechatPhoneBindRequest();
request.setCode("phone-code");
AuthResponse response = authService.bindWechatPhone("wx-temp", request);
assertNotNull(response);
assertEquals("phone-user", response.getUserInfo().getId());
assertEquals("openid-a", phoneUser.getThirdPartyId());
assertEquals("wechat-mp", phoneUser.getThirdPartyType());
assertEquals(0, temporaryWechatUser.getStatus());
verify(userIdentityMergeService).mergeTemporaryWechatUserIntoPhoneUser("wx-temp", "phone-user");
verify(userService).updateById(temporaryWechatUser);
verify(userService).updateById(phoneUser);
}
}
```
Use the repository's existing test patterns from `AuthControllerTest` and Mockito. If direct unit construction is blocked by private helpers, use `@SpringBootTest` and `@MockBean` for `WechatMiniProgramService`.
- [ ] **Step 9: Run auth tests**
- [ ] **Step 10: Run auth tests**
```bash
cd backend-single
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest test
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,UserIdentityMergeServiceImplTest test
```
Expected: command exits with status `0`.
- [ ] **Step 10: Commit**
- [ ] **Step 11: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java
git add backend-single/src/main/java/com/emotion/service/AuthService.java
git add backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java
git commit -m "feat: add wechat auth identity binding logic"
```
---
### Task 4: Backend Auth Controller And Security Allowlist
### Task 5: Backend Auth Controller And Security Allowlist
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java`
@@ -809,7 +1144,7 @@ git commit -m "feat: expose wechat auth endpoints"
---
### Task 5: Mini Program Auth Service And Store
### Task 6: Mini Program Auth Service And Store
**Files:**
- Modify: `mini-program/src/services/auth.js`
@@ -913,7 +1248,49 @@ loginWithWechat,
bindWechatPhone,
```
- [ ] **Step 4: Build mini program**
- [ ] **Step 4: Add current user info refresh for restored sessions**
In `mini-program/src/stores/app.js`, add:
```javascript
const fetchCurrentUserInfo = async () => {
try {
const res = await authService.getCurrentUserInfo()
state.userInfo = res.data || null
return state.userInfo
} catch (error) {
logAuth('userInfo:fail', {
message: error.message,
statusCode: error.statusCode,
code: error.code
})
return null
}
}
```
Call it after every successful token validation or refresh in `restoreSession()`:
```javascript
state.isLoggedIn = true
await fetchCurrentUserInfo()
await fetchUserProfile({ throwOnError: true })
```
Also call it after successful existing phone login:
```javascript
state.userInfo = res.data?.userInfo || null
await fetchCurrentUserInfo()
```
Add it to the returned store object:
```javascript
fetchCurrentUserInfo,
```
- [ ] **Step 5: Build mini program**
```bash
cd mini-program
@@ -922,7 +1299,7 @@ npm run build:mp-weixin
Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
- [ ] **Step 6: Commit**
```bash
git add mini-program/src/services/auth.js
@@ -932,7 +1309,7 @@ git commit -m "feat: add mini program wechat auth service methods"
---
### Task 6: Login Page Supports WeChat And Phone Login
### Task 7: Login Page Supports WeChat And Phone Login
**Files:**
- Modify: `mini-program/src/pages/login/index.vue`
@@ -1108,7 +1485,7 @@ git commit -m "feat: add wechat login to mini program login page"
---
### Task 7: Logged-In Phone Binding Entry
### Task 8: Logged-In Phone Binding Entry
**Files:**
- Modify: `mini-program/src/pages/main/MineView.vue`
@@ -1120,8 +1497,8 @@ In `MineView.vue`, add:
```javascript
const currentUser = computed(() => store.userInfo || {})
const hasPhone = computed(() => Boolean(currentUser.value.phone || profile.value.phone))
const canBindWechatPhone = computed(() => store.isLoggedIn && !hasPhone.value)
const hasPhone = computed(() => Boolean(currentUser.value.phone))
const canBindWechatPhone = computed(() => store.isLoggedIn && currentUser.value.wechatBound && !hasPhone.value)
const handleWechatPhoneBind = async (event) => {
const code = event?.detail?.code
@@ -1209,7 +1586,7 @@ git commit -m "feat: add wechat phone binding entry"
---
### Task 8: Full Verification And Documentation Notes
### Task 9: Full Verification And Documentation Notes
**Files:**
- Read: `docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md`
@@ -1220,7 +1597,7 @@ git commit -m "feat: add wechat phone binding entry"
```bash
cd backend-single
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,WechatMiniProgramServiceImplTest test
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,WechatMiniProgramServiceImplTest,UserIdentityMergeServiceImplTest test
```
Expected: command exits with status `0`.
@@ -1285,16 +1662,19 @@ Expected: no uncommitted implementation changes except intentionally uncommitted
| Spec Requirement | Plan Coverage |
| --- | --- |
| Free WeChat openid login | Tasks 1, 2, 3, 4, 5, 6 |
| Keep phone/SMS login | Tasks 5 and 6 preserve existing `login(phone, smsCode)` and UI form |
| Optional phone authorization | Tasks 2, 3, 4, 5, 7 |
| Phone identifies existing account and auto-binds openid | Task 3 Step 6 |
| Existing phone user becomes canonical identity | Task 3 Step 6 returns tokens for `phoneUser` |
| Nickname/avatar saved when provided | Task 3 Step 5 |
| Reuse current JWT/AuthResponse/session restore | Tasks 3, 4, 5 |
| Login page matches existing style | Task 6 |
| Protected phone binding endpoint | Task 4 Steps 2-3 and test list |
| Build and verification | Task 8 |
| Free WeChat openid login | Tasks 1, 2, 4, 5, 6, 7 |
| Keep phone/SMS login | Tasks 6 and 7 preserve existing `login(phone, smsCode)` and UI form |
| Optional phone authorization | Tasks 2, 4, 5, 6, 8 |
| Phone identifies existing account and auto-binds openid | Task 4 Step 7 |
| Existing phone user becomes canonical identity | Task 4 Step 7 returns tokens for `phoneUser` |
| Temporary WeChat user data is not stranded | Task 3 and Task 4 Step 7 |
| Temporary WeChat identity is cleared after transfer | Task 4 Step 7 |
| Nickname/avatar saved when provided | Task 4 Step 6 |
| Reuse current JWT/AuthResponse/session restore | Tasks 4, 5, 6 |
| Login page matches existing style | Task 7 |
| Protected phone binding endpoint | Task 5 Steps 2-3 and test list |
| Restored sessions know phone and WeChat-bound state | Task 6 Step 4 |
| Build and verification | Task 9 |
## Placeholder Scan
@@ -1305,4 +1685,5 @@ This plan contains no incomplete placeholder markers, no deferred sections, and
- Backend request DTOs use `code`, `nickname`, and `avatar`; frontend service sends the same names.
- Backend phone binding DTO uses `code`; frontend `bindWechatPhone({ code })` sends the same name.
- `thirdPartyType` and `thirdPartyId` map to existing `User` fields.
- `UserInfoResponse.wechatBound` maps to frontend `currentUser.value.wechatBound`.
- `AuthResponse` stays the only frontend token response shape.