docs: refine wechat auth design and plan
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user