40 KiB
Mini Program WeChat Auth Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add WeChat openid login to the mini program while preserving phone/SMS login, and automatically bind WeChat openid to an existing phone user when WeChat phone authorization succeeds.
Architecture: The backend owns all WeChat secrets and identity decisions. The mini program sends WeChat login or phone authorization codes to backend auth endpoints, stores the returned existing AuthResponse tokens, and reuses the current session/profile routing. Phone/SMS login remains the alternate path and shares the same token/session system.
Tech Stack: Java 17, Spring Boot 2.7, MyBatis-Plus, Redis, RestTemplate, UniApp, Vue 3, WeChat Mini Program APIs.
Spec: docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md
File Structure
| 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. |
| 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. |
| Create | backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java |
Request body for WeChat openid login. |
| Create | backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java |
Request body for WeChat phone binding. |
| Create | backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java |
Internal DTO for WeChat code2Session response. |
| Create | backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java |
Internal DTO for WeChat access token response. |
| 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. |
| 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. |
| Modify | backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java |
Implements openid login, phone identity merge/bind, and token issuing reuse. |
| Modify | backend-single/src/main/java/com/emotion/controller/AuthController.java |
Adds /auth/wechat/login and /auth/wechat/phone. |
| Test | backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java |
Verifies controller contract. |
| Test | backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java |
Verifies identity matching and binding behavior. |
| Modify | mini-program/src/services/auth.js |
Adds wechatLogin and bindWechatPhone. |
| Modify | mini-program/src/stores/app.js |
Adds store methods and tracks returned auth user info. |
| Modify | mini-program/src/pages/login/index.vue |
Adds WeChat primary login while keeping phone/SMS form. |
| Modify | mini-program/src/pages/main/MineView.vue |
Adds phone binding entry for logged-in WeChat users without a phone. |
Task 1: Backend WeChat Configuration And DTOs
Files:
-
Create:
backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java -
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/resources/application.yml -
Test: backend compile
-
Step 1: Create WeChat configuration properties
Create backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java:
package com.emotion.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "emotion.wechat.mini-program")
public class WechatMiniProgramProperties {
private String appId;
private String appSecret;
private String baseUrl = "https://api.weixin.qq.com";
}
- Step 2: Create public request DTOs
Create backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java:
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class WechatLoginRequest {
@NotBlank(message = "微信登录凭证不能为空")
private String code;
private String nickname;
private String avatar;
}
Create backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java:
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class WechatPhoneBindRequest {
@NotBlank(message = "微信手机号授权凭证不能为空")
private String code;
}
- Step 3: Create internal WeChat API response DTOs
Create backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java:
package com.emotion.dto.wechat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class WechatCodeSessionResponse {
private String openid;
@JsonProperty("session_key")
private String sessionKey;
private String unionid;
private Integer errcode;
private String errmsg;
public boolean isSuccess() {
return errcode == null || errcode == 0;
}
}
Create backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java:
package com.emotion.dto.wechat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class WechatAccessTokenResponse {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("expires_in")
private Integer expiresIn;
private Integer errcode;
private String errmsg;
public boolean isSuccess() {
return (errcode == null || errcode == 0) && accessToken != null && !accessToken.isBlank();
}
}
Create backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java:
package com.emotion.dto.wechat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class WechatPhoneNumberResponse {
private Integer errcode;
private String errmsg;
@JsonProperty("phone_info")
private PhoneInfo phoneInfo;
public boolean isSuccess() {
return (errcode == null || errcode == 0)
&& phoneInfo != null
&& phoneInfo.getPhoneNumber() != null
&& !phoneInfo.getPhoneNumber().isBlank();
}
@Data
public static class PhoneInfo {
@JsonProperty("phoneNumber")
private String phoneNumber;
@JsonProperty("purePhoneNumber")
private String purePhoneNumber;
@JsonProperty("countryCode")
private String countryCode;
}
}
- Step 4: Add WeChat config keys
In backend-single/src/main/resources/application.yml, under emotion:, add:
wechat:
mini-program:
app-id: ${WECHAT_MINI_PROGRAM_APP_ID:wxaf2eaba72d28f0e4}
app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:}
base-url: https://api.weixin.qq.com
- Step 5: Compile backend
Run:
cd backend-single
mvn -q -DskipTests compile
Expected: command exits with status 0.
- Step 6: Commit
git add backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
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/resources/application.yml
git commit -m "feat: add wechat mini program auth DTOs and config"
Task 2: Backend WeChat API Client
Files:
-
Create:
backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java -
Create:
backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java -
Test:
backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java -
Step 1: Create service interface
Create backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java:
package com.emotion.service;
import com.emotion.dto.wechat.WechatCodeSessionResponse;
import com.emotion.dto.wechat.WechatPhoneNumberResponse;
public interface WechatMiniProgramService {
WechatCodeSessionResponse code2Session(String code);
WechatPhoneNumberResponse.PhoneInfo getPhoneNumber(String phoneCode);
}
- Step 2: Create RestTemplate implementation
Create backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java:
package com.emotion.service.impl;
import com.emotion.config.WechatMiniProgramProperties;
import com.emotion.dto.wechat.WechatAccessTokenResponse;
import com.emotion.dto.wechat.WechatCodeSessionResponse;
import com.emotion.dto.wechat.WechatPhoneNumberResponse;
import com.emotion.exception.BusinessException;
import com.emotion.service.WechatMiniProgramService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
private static final String ACCESS_TOKEN_CACHE_KEY = "wechat:mini-program:access-token";
private static final long ACCESS_TOKEN_CACHE_BUFFER_SECONDS = 300L;
private final RestTemplate restTemplate;
private final RedisTemplate<String, Object> redisTemplate;
private final WechatMiniProgramProperties properties;
@Override
public WechatCodeSessionResponse code2Session(String code) {
validateConfigured();
String url = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl() + "/sns/jscode2session")
.queryParam("appid", properties.getAppId())
.queryParam("secret", properties.getAppSecret())
.queryParam("js_code", code)
.queryParam("grant_type", "authorization_code")
.toUriString();
WechatCodeSessionResponse response = restTemplate.getForObject(url, WechatCodeSessionResponse.class);
if (response == null || !response.isSuccess() || !StringUtils.hasText(response.getOpenid())) {
throw new BusinessException("微信登录凭证校验失败");
}
return response;
}
@Override
public WechatPhoneNumberResponse.PhoneInfo getPhoneNumber(String phoneCode) {
validateConfigured();
String accessToken = getStableAccessToken();
String url = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl() + "/wxa/business/getuserphonenumber")
.queryParam("access_token", accessToken)
.toUriString();
Map<String, String> body = new HashMap<>();
body.put("code", phoneCode);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
WechatPhoneNumberResponse response = restTemplate.postForObject(
url,
new HttpEntity<>(body, headers),
WechatPhoneNumberResponse.class
);
if (response == null || !response.isSuccess()) {
throw new BusinessException("微信手机号授权失败");
}
return response.getPhoneInfo();
}
private String getStableAccessToken() {
Object cached = redisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY);
if (cached instanceof String && StringUtils.hasText((String) cached)) {
return (String) cached;
}
String url = UriComponentsBuilder.fromHttpUrl(properties.getBaseUrl() + "/cgi-bin/stable_token")
.toUriString();
Map<String, Object> body = new HashMap<>();
body.put("grant_type", "client_credential");
body.put("appid", properties.getAppId());
body.put("secret", properties.getAppSecret());
body.put("force_refresh", false);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
WechatAccessTokenResponse response = restTemplate.postForObject(
url,
new HttpEntity<>(body, headers),
WechatAccessTokenResponse.class
);
if (response == null || !response.isSuccess()) {
throw new BusinessException("微信接口调用凭证获取失败");
}
long ttl = Math.max(60L, response.getExpiresIn() - ACCESS_TOKEN_CACHE_BUFFER_SECONDS);
redisTemplate.opsForValue().set(ACCESS_TOKEN_CACHE_KEY, response.getAccessToken(), ttl, TimeUnit.SECONDS);
return response.getAccessToken();
}
private void validateConfigured() {
if (!StringUtils.hasText(properties.getAppId()) || !StringUtils.hasText(properties.getAppSecret())) {
throw new BusinessException("微信小程序登录未配置");
}
}
}
- Step 3: Create focused unit tests for error behavior
Create backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java:
package com.emotion.service;
import com.emotion.config.WechatMiniProgramProperties;
import com.emotion.exception.BusinessException;
import com.emotion.service.impl.WechatMiniProgramServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.client.RestTemplate;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
class WechatMiniProgramServiceImplTest {
@Test
void code2SessionRejectsMissingSecret() {
WechatMiniProgramProperties properties = new WechatMiniProgramProperties();
properties.setAppId("wx-test");
properties.setAppSecret("");
WechatMiniProgramService service = new WechatMiniProgramServiceImpl(
mock(RestTemplate.class),
mock(RedisTemplate.class),
properties
);
assertThrows(BusinessException.class, () -> service.code2Session("login-code"));
}
}
- Step 4: Run test
cd backend-single
mvn -q -Dtest=WechatMiniProgramServiceImplTest test
Expected: command exits with status 0.
- Step 5: Commit
git add backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java
git add backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java
git commit -m "feat: add wechat mini program API client"
Task 3: 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 -
Test:
backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java -
Step 1: Add third-party lookup to UserService
In backend-single/src/main/java/com/emotion/service/UserService.java, add:
User getByThirdParty(String thirdPartyType, String thirdPartyId);
In backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java, add:
@Override
public User getByThirdParty(String thirdPartyType, String thirdPartyId) {
if (!StringUtils.hasText(thirdPartyType) || !StringUtils.hasText(thirdPartyId)) {
return null;
}
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getThirdPartyType, thirdPartyType)
.eq(User::getThirdPartyId, thirdPartyId)
.eq(User::getIsDeleted, 0);
return this.getOne(wrapper);
}
- Step 2: Add AuthService methods
In backend-single/src/main/java/com/emotion/service/AuthService.java, import the two request DTOs and add:
AuthResponse wechatLogin(WechatLoginRequest request);
AuthResponse bindWechatPhone(String currentUserId, WechatPhoneBindRequest request);
- Step 3: Add AuthServiceImpl constants and dependency
In AuthServiceImpl, add imports:
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.WechatMiniProgramService;
Add field:
@Autowired
private WechatMiniProgramService wechatMiniProgramService;
Add constant:
private static final String WECHAT_MP_TYPE = "wechat-mp";
- Step 4: Add shared token response helper
In AuthServiceImpl, add this private helper near generateRefreshToken:
private AuthResponse buildAuthResponse(User user) {
String accessToken = generateAccessToken(user);
String refreshToken = generateRefreshToken(user);
userService.updateLastActiveTime(user.getId(), LocalDateTime.now());
AuthResponse response = new AuthResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn((long) TOKEN_EXPIRE_HOURS * 3600);
response.setUserInfo(convertToUserInfoResponse(user));
response.setLoginTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
return response;
}
Then replace the duplicate token response block in login, register, and refreshToken with return buildAuthResponse(user);.
- Step 5: Implement WeChat login
In AuthServiceImpl, add:
@Override
public AuthResponse wechatLogin(WechatLoginRequest request) {
WechatCodeSessionResponse session = wechatMiniProgramService.code2Session(request.getCode());
String openid = session.getOpenid();
User user = userService.getByThirdParty(WECHAT_MP_TYPE, openid);
if (user == null) {
user = autoRegisterWechatUser(openid, request.getNickname(), request.getAvatar());
} else if (user.getStatus() != 1) {
throw new AuthException("账号已被禁用");
} else {
boolean changed = false;
if (StringUtils.hasText(request.getNickname()) && !StringUtils.hasText(user.getNickname())) {
user.setNickname(request.getNickname());
changed = true;
}
if (StringUtils.hasText(request.getAvatar()) && !StringUtils.hasText(user.getAvatar())) {
user.setAvatar(request.getAvatar());
changed = true;
}
if (changed) {
userService.updateById(user);
}
}
return buildAuthResponse(user);
}
private User autoRegisterWechatUser(String openid, String nickname, String avatar) {
String username = generateRandomUsername();
String account = "wx_" + openid;
User user = userService.createUser(account, username, generateRandomPassword(), null, null);
user.setThirdPartyType(WECHAT_MP_TYPE);
user.setThirdPartyId(openid);
if (StringUtils.hasText(nickname)) {
user.setNickname(nickname);
}
if (StringUtils.hasText(avatar)) {
user.setAvatar(avatar);
}
userService.updateById(user);
return user;
}
- Step 6: Implement phone binding and auto-identification
In AuthServiceImpl, add:
@Override
public AuthResponse bindWechatPhone(String currentUserId, WechatPhoneBindRequest request) {
User currentUser = userService.getById(currentUserId);
if (currentUser == null || currentUser.getIsDeleted() == 1) {
throw new AuthException("用户不存在");
}
if (!WECHAT_MP_TYPE.equals(currentUser.getThirdPartyType()) || !StringUtils.hasText(currentUser.getThirdPartyId())) {
throw new BusinessException("当前账号未绑定微信身份");
}
WechatPhoneNumberResponse.PhoneInfo phoneInfo = wechatMiniProgramService.getPhoneNumber(request.getCode());
String phone = StringUtils.hasText(phoneInfo.getPurePhoneNumber())
? phoneInfo.getPurePhoneNumber()
: phoneInfo.getPhoneNumber();
User phoneUser = userService.getByPhone(phone);
if (phoneUser == null || phoneUser.getId().equals(currentUser.getId())) {
currentUser.setPhone(phone);
currentUser.setIsVerified(1);
userService.updateById(currentUser);
return buildAuthResponse(currentUser);
}
if (phoneUser.getStatus() != 1) {
throw new AuthException("手机号对应账号已被禁用");
}
if (StringUtils.hasText(phoneUser.getThirdPartyId())
&& !currentUser.getThirdPartyId().equals(phoneUser.getThirdPartyId())) {
throw new BusinessException("该手机号已绑定其他微信账号");
}
phoneUser.setThirdPartyType(WECHAT_MP_TYPE);
phoneUser.setThirdPartyId(currentUser.getThirdPartyId());
if (!StringUtils.hasText(phoneUser.getAvatar()) && StringUtils.hasText(currentUser.getAvatar())) {
phoneUser.setAvatar(currentUser.getAvatar());
}
if (!StringUtils.hasText(phoneUser.getNickname()) && StringUtils.hasText(currentUser.getNickname())) {
phoneUser.setNickname(currentUser.getNickname());
}
phoneUser.setIsVerified(1);
userService.updateById(phoneUser);
return buildAuthResponse(phoneUser);
}
- Step 7: Compile backend
cd backend-single
mvn -q -DskipTests compile
Expected: command exits with status 0.
- Step 8: 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:
@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.
}
@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.
}
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
cd backend-single
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest test
Expected: command exits with status 0.
- Step 10: Commit
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/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
Files:
-
Modify:
backend-single/src/main/java/com/emotion/controller/AuthController.java -
Modify:
backend-single/src/main/java/com/emotion/config/WebMvcConfig.java -
Modify:
backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java -
Test:
backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java -
Step 1: Add controller endpoints
In AuthController.java, import:
import com.emotion.dto.request.WechatLoginRequest;
import com.emotion.dto.request.WechatPhoneBindRequest;
Add methods:
@PostMapping("/wechat/login")
@Operation(summary = "微信小程序登录", description = "使用微信登录 code 换取 openid 并登录或自动注册")
public Result<AuthResponse> wechatLogin(@Valid @RequestBody WechatLoginRequest request) {
AuthResponse response = authService.wechatLogin(request);
return Result.success("微信登录成功", response);
}
@PostMapping("/wechat/phone")
@Operation(summary = "绑定微信授权手机号", description = "使用微信手机号授权 code 获取手机号并自动识别绑定已有用户")
public Result<AuthResponse> bindWechatPhone(@Valid @RequestBody WechatPhoneBindRequest request) {
AuthResponse response = authService.bindWechatPhone(UserContextUtils.requireCurrentUserId(), request);
return Result.success("手机号绑定成功", response);
}
- Step 2: Allow public WeChat login in WebMvcConfig
In WebMvcConfig.java, add to excludePathPatterns:
"/auth/wechat/login",
Do not exclude /auth/wechat/phone; it must require JWT.
- Step 3: Allow public WeChat login in JwtAuthInterceptor internal list
In JwtAuthInterceptor.java, add to publicEndpoints:
"/api/auth/wechat/login",
Do not add /api/auth/wechat/phone.
- Step 4: Add controller tests
In AuthControllerTest.java, add static imports for post and content if missing:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
Add tests:
@Test
public void testWechatLogin() throws Exception {
AuthResponse response = new AuthResponse();
response.setAccessToken("access");
response.setRefreshToken("refresh");
when(authService.wechatLogin(org.mockito.ArgumentMatchers.any())).thenReturn(response);
mockMvc.perform(post("/api/auth/wechat/login").contextPath("/api")
.contentType("application/json")
.content("{\"code\":\"wx-code\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.accessToken").value("access"));
}
@Test
public void testWechatPhoneBind() throws Exception {
AuthResponse response = new AuthResponse();
response.setAccessToken("phone-access");
response.setRefreshToken("phone-refresh");
when(authService.bindWechatPhone(org.mockito.ArgumentMatchers.eq("test-user"), org.mockito.ArgumentMatchers.any()))
.thenReturn(response);
mockMvc.perform(post("/api/auth/wechat/phone").contextPath("/api")
.contentType("application/json")
.content("{\"code\":\"phone-code\"}")
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.accessToken").value("phone-access"));
}
- Step 5: Run controller tests
cd backend-single
mvn -q -Dtest=AuthControllerTest test
Expected: command exits with status 0.
- Step 6: Commit
git add backend-single/src/main/java/com/emotion/controller/AuthController.java
git add backend-single/src/main/java/com/emotion/config/WebMvcConfig.java
git add backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java
git add backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java
git commit -m "feat: expose wechat auth endpoints"
Task 5: Mini Program Auth Service And Store
Files:
-
Modify:
mini-program/src/services/auth.js -
Modify:
mini-program/src/stores/app.js -
Test: mini program build
-
Step 1: Add shared token persistence helper
In mini-program/src/services/auth.js, add near the top:
const persistAuthTokens = (response) => {
if (!response.data) return
const { accessToken, refreshToken } = response.data
if (accessToken) {
uni.setStorageSync('access_token', accessToken)
}
if (refreshToken) {
uni.setStorageSync('refresh_token', refreshToken)
}
}
Then replace the duplicated token-save block in login() and refreshToken() with:
persistAuthTokens(response)
- Step 2: Add WeChat service methods
In mini-program/src/services/auth.js, add:
export const wechatLogin = async ({ code, nickname, avatar }) => {
const response = await post('/auth/wechat/login', { code, nickname, avatar })
persistAuthTokens(response)
return response
}
export const bindWechatPhone = async ({ code }) => {
const response = await post('/auth/wechat/phone', { code })
persistAuthTokens(response)
return response
}
Add both methods to the default export:
wechatLogin,
bindWechatPhone,
- Step 3: Store returned userInfo
In mini-program/src/stores/app.js, add userInfo updates in existing login after authService.login:
state.userInfo = res.data?.userInfo || null
Add store methods:
const loginWithWechat = async (payload = {}) => {
state.isLoading = true
try {
const res = await authService.wechatLogin(payload)
state.userInfo = res.data?.userInfo || null
state.isLoggedIn = true
await fetchUserProfile()
return { success: true, hasProfile: hasProfile.value, userInfo: state.userInfo }
} catch (error) {
return { success: false, error: error.message }
} finally {
state.isLoading = false
}
}
const bindWechatPhone = async (code) => {
state.isLoading = true
try {
const res = await authService.bindWechatPhone({ code })
state.userInfo = res.data?.userInfo || null
state.isLoggedIn = true
await fetchUserProfile()
return { success: true, userInfo: state.userInfo, hasProfile: hasProfile.value }
} catch (error) {
return { success: false, error: error.message }
} finally {
state.isLoading = false
}
}
Add them to the returned object:
loginWithWechat,
bindWechatPhone,
- Step 4: Build mini program
cd mini-program
npm run build:mp-weixin
Expected: command exits with status 0.
- Step 5: Commit
git add mini-program/src/services/auth.js
git add mini-program/src/stores/app.js
git commit -m "feat: add mini program wechat auth service methods"
Task 6: Login Page Supports WeChat And Phone Login
Files:
-
Modify:
mini-program/src/pages/login/index.vue -
Test: mini program build
-
Step 1: Add WeChat login handler
In mini-program/src/pages/login/index.vue, add:
const activeMethod = ref('')
const routeAfterLogin = (hasProfile) => {
if (hasProfile) {
uni.redirectTo({ url: '/pages/main/index?tab=script' })
} else {
uni.redirectTo({ url: '/pages/onboarding/index' })
}
}
const getWechatLoginCode = () => {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code) {
resolve(res.code)
} else {
reject(new Error('微信登录未返回凭证'))
}
},
fail: (error) => reject(error)
})
})
}
const handleWechatLogin = async () => {
if (loading.value) return
loading.value = true
activeMethod.value = 'wechat'
try {
const code = await getWechatLoginCode()
const result = await store.loginWithWechat({ code })
if (result.success) {
routeAfterLogin(result.hasProfile)
} else {
uni.showToast({ title: result.error || '微信登录失败', icon: 'none' })
}
} catch (error) {
uni.showToast({ title: '微信登录失败,请稍后重试', icon: 'none' })
} finally {
loading.value = false
activeMethod.value = ''
}
}
Change handleLogin success routing to:
routeAfterLogin(result.hasProfile)
- Step 2: Add primary WeChat button above phone form
In the .login-card template, above <view class="form">, add:
<button
class="wechat-btn"
:class="{ disabled: loading }"
:disabled="loading"
@click="handleWechatLogin"
>
<text v-if="loading && activeMethod === 'wechat'">微信登录中...</text>
<text v-else>微信一键登录</text>
</button>
<view class="login-divider">
<view></view>
<text>或使用手机号登录</text>
<view></view>
</view>
- Step 3: Update phone button loading text
Change the phone login button text block to:
<text v-if="loading && activeMethod === 'phone'">登录中...</text>
<text v-else>开启旅程</text>
At the start of handleLogin, set:
activeMethod.value = 'phone'
And in the finally, clear:
activeMethod.value = ''
- Step 4: Add styles matching current glass theme
Add styles:
.wechat-btn {
width: 100%;
height: 88rpx;
margin-bottom: 26rpx;
padding: 0;
border: 0;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 28rpx;
font-weight: 700;
background: linear-gradient(135deg, #34d399 0%, #22c55e 48%, #16a34a 100%);
box-shadow:
0 14rpx 34rpx rgba(34, 197, 94, 0.26),
inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
.wechat-btn::after {
border: 0;
}
.wechat-btn.disabled {
opacity: 0.52;
}
.login-divider {
display: flex;
align-items: center;
gap: 18rpx;
margin-bottom: 30rpx;
}
.login-divider view {
flex: 1;
height: 1rpx;
background: rgba(217, 190, 255, 0.18);
}
.login-divider text {
color: rgba(225, 209, 255, 0.52);
font-size: 22rpx;
}
- Step 5: Build mini program
cd mini-program
npm run build:mp-weixin
Expected: command exits with status 0.
- Step 6: Commit
git add mini-program/src/pages/login/index.vue
git commit -m "feat: add wechat login to mini program login page"
Task 7: Logged-In Phone Binding Entry
Files:
-
Modify:
mini-program/src/pages/main/MineView.vue -
Test: mini program build
-
Step 1: Add phone binding computed state and handler
In MineView.vue, add:
const currentUser = computed(() => store.userInfo || {})
const hasPhone = computed(() => Boolean(currentUser.value.phone || profile.value.phone))
const canBindWechatPhone = computed(() => store.isLoggedIn && !hasPhone.value)
const handleWechatPhoneBind = async (event) => {
const code = event?.detail?.code
if (!code) {
uni.showToast({ title: '可稍后在个人中心绑定手机号', icon: 'none' })
return
}
const result = await store.bindWechatPhone(code)
if (result.success) {
uni.showToast({ title: '手机号绑定成功', icon: 'success' })
return
}
uni.showToast({ title: result.error || '手机号绑定失败', icon: 'none' })
}
- Step 2: Add menu item for WeChat phone binding
In the .menu-list, before the account switching menu item, add:
<!-- #ifdef MP-WEIXIN -->
<button
v-if="canBindWechatPhone"
class="menu-item bind-phone-button"
open-type="getPhoneNumber"
@getphonenumber="handleWechatPhoneBind"
>
<view class="menu-icon phone-icon"></view>
<text>绑定微信手机号</text>
<text class="menu-arrow">›</text>
</button>
<!-- #endif -->
- Step 3: Add button reset styles and phone icon
Add styles:
.bind-phone-button {
width: 100%;
margin: 0;
text-align: left;
border: 1rpx solid rgba(160, 112, 255, 0.14);
}
.bind-phone-button::after {
border: 0;
}
.phone-icon {
border: 3rpx solid currentColor;
border-radius: 8rpx;
}
.phone-icon::before {
content: '';
position: absolute;
left: 10rpx;
right: 10rpx;
bottom: 5rpx;
height: 3rpx;
border-radius: 999rpx;
background: currentColor;
}
- Step 4: Build mini program
cd mini-program
npm run build:mp-weixin
Expected: command exits with status 0.
- Step 5: Commit
git add mini-program/src/pages/main/MineView.vue
git commit -m "feat: add wechat phone binding entry"
Task 8: Full Verification And Documentation Notes
Files:
-
Read:
docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md -
Optional Modify:
mini-program/README.md -
Test: backend tests and mini program build
-
Step 1: Run backend auth tests
cd backend-single
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,WechatMiniProgramServiceImplTest test
Expected: command exits with status 0.
- Step 2: Compile backend
cd backend-single
mvn -q -DskipTests compile
Expected: command exits with status 0.
- Step 3: Build mini program
cd mini-program
npm run build:mp-weixin
Expected: command exits with status 0.
- Step 4: Manual WeChat DevTools check
Open mini-program/unpackage/dist/build/mp-weixin in WeChat DevTools and verify:
- Login page shows "微信一键登录" and the phone/SMS form.
- Phone/SMS login still submits to
/auth/login. - WeChat login submits to
/auth/wechat/login. - Successful login follows existing profile routing.
- Personal center shows phone binding when no phone is present.
- Denying phone authorization keeps the user logged in.
- Step 5: Add README note if config is not documented elsewhere
If no deployment doc already lists WeChat env vars, add this section to mini-program/README.md or backend-single/部署说明.md:
### WeChat Mini Program Login
Backend environment variables:
```bash
WECHAT_MINI_PROGRAM_APP_ID=wxaf2eaba72d28f0e4
WECHAT_MINI_PROGRAM_APP_SECRET=<mini-program-secret>
WeChat openid login works without phone authorization. Phone authorization is optional and, when available, is used to bind the WeChat openid to an existing phone account.
- [ ] **Step 6: Final status**
```bash
git status --short
Expected: no uncommitted implementation changes except intentionally uncommitted local environment files.
Self-Review: Spec Coverage Check
| 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 |
Placeholder Scan
This plan contains no incomplete placeholder markers, no deferred sections, and no undefined task names. Each code-changing step includes exact files and code snippets.
Type Consistency Check
- Backend request DTOs use
code,nickname, andavatar; frontend service sends the same names. - Backend phone binding DTO uses
code; frontendbindWechatPhone({ code })sends the same name. thirdPartyTypeandthirdPartyIdmap to existingUserfields.AuthResponsestays the only frontend token response shape.