diff --git a/docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md b/docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md new file mode 100644 index 0000000..3ca9705 --- /dev/null +++ b/docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md @@ -0,0 +1,240 @@ +# Mini Program WeChat Auth Design + +## Problem + +The mini program currently supports phone number plus SMS code login only. The new requirement is to add full WeChat authorization login while keeping the existing phone login available. WeChat login must use the free openid-based login flow as the primary path, and phone number authorization should be integrated when available so the backend can identify and bind existing phone users automatically. + +## Goals + +- Add WeChat mini program login with `wx.login`/`uni.login` and backend `code2Session` exchange. +- Keep the existing phone plus SMS code login flow unchanged as a supported fallback and alternate login method. +- Use WeChat openid as the free, reliable identity for mini program login. +- If WeChat phone authorization succeeds, use the returned phone number to identify an existing user and automatically bind the WeChat openid to that account. +- Save nickname and avatar into existing `t_user.nickname` and `t_user.avatar` fields when the user provides them. +- Reuse the current JWT response, token storage, session restore, onboarding routing, and profile flow. +- Match the current mini program dark glass/cosmic visual style. + +## Non-Goals + +- Do not remove, hide permanently, or degrade phone SMS login. +- Do not introduce paid third-party login or SMS providers. +- Do not require phone number authorization before a user can log in with WeChat openid. +- Do not redesign unrelated onboarding, main tabs, or profile features. +- Do not store WeChat `session_key` on the client. + +## Current Findings + +- `mini-program/src/pages/login/index.vue` contains the existing phone/SMS login UI and routes to main or onboarding based on profile presence. +- `mini-program/src/stores/app.js` already centralizes login, logout, token restore, and profile fetch behavior. +- `mini-program/src/services/auth.js` stores `access_token` and `refresh_token` from `AuthResponse`. +- `backend-single/src/main/java/com/emotion/controller/AuthController.java` exposes `/auth/login`, `/auth/refreshToken`, and token validation endpoints. +- `t_user` already has `phone`, `avatar`, `nickname`, `third_party_id`, and `third_party_type`, which can support WeChat binding without a required schema change. +- The current backend issues JWT plus Redis-backed access/refresh tokens through `AuthServiceImpl`. +- The mini program manifest already has a WeChat appid: `wxaf2eaba72d28f0e4`. +- `application.yml` security ignore URLs must include the new public WeChat login endpoint, while the phone binding endpoint must remain protected by JWT. + +## Recommended Approach + +Use a two-track login page: + +1. Primary action: WeChat login. The mini program calls `uni.login({ provider: 'weixin' })`, sends the returned code to the backend, and receives the same `AuthResponse` shape used by existing login. +2. Secondary action: phone SMS login. The current phone/code form remains available and continues to call `/auth/login`. + +Phone authorization is a post-login enhancement: + +1. After WeChat login succeeds, show or surface a "bind phone" action using WeChat `open-type="getPhoneNumber"` when the platform supports it. +2. The mini program sends the phone authorization `code` to the backend. +3. The backend calls WeChat's phone-number API. +4. If the phone already belongs to an existing user, bind the current openid to that existing user and return a fresh `AuthResponse` for that existing user. +5. If the phone does not exist, write it to the current WeChat user and return updated user info. + +This keeps WeChat openid login free and resilient while letting phone number authorization unify identities when available. + +## Backend Design + +### Configuration + +Add configuration under `emotion.wechat.mini-program`: + +- `app-id` +- `app-secret` +- `base-url`, default `https://api.weixin.qq.com` + +Secrets must come from environment/profile configuration in deploy environments. `app-secret` must not be sent to the client or logged. + +### DTOs + +Add request/response DTOs: + +- `WechatLoginRequest` + - `code`: required login code from `uni.login`. + - `nickname`: optional user-provided nickname. + - `avatar`: optional user-provided avatar URL. + +- `WechatPhoneBindRequest` + - `code`: required phone authorization code from `getPhoneNumber`. + +- The response can reuse `AuthResponse` for login and phone binding so token handling stays consistent. + +### Service Boundaries + +Add `WechatMiniProgramService` for WeChat API calls: + +- `code2Session(code)` returns openid/session metadata. +- `getStableAccessToken()` obtains and caches the mini program interface token in Redis. +- `getPhoneNumber(phoneCode)` returns the verified phone number. +- Translate WeChat API error codes into clear `BusinessException` messages. + +Extend `AuthService` with: + +- `wechatLogin(WechatLoginRequest request)` +- `bindWechatPhone(String currentUserId, WechatPhoneBindRequest request)` + +Extend `UserService` with focused lookup helpers: + +- `getByThirdParty(String thirdPartyType, String thirdPartyId)` +- `getByPhone(String phone)` already exists and should be reused for phone identity matching. + +The controller stays thin and delegates all identity decisions to the service layer. + +### Identity Matching + +For WeChat login: + +1. Exchange login code for openid. +2. Query user by `third_party_type = "wechat-mp"` and `third_party_id = openid`. +3. If found and enabled, issue tokens for that user. +4. If not found, create a user: + - `account = "wx_" + openid` + - `username =` generated username + - `nickname =` request nickname or generated username + - `avatar =` request avatar if present + - `third_party_type = "wechat-mp"` + - `third_party_id = openid` + - `member_level = "free"` + - `status = 1` + +For phone binding: + +1. Require a valid current JWT. +2. Resolve the current WeChat user's openid from `third_party_id`. +3. Exchange the phone authorization code for phone number. +4. Query existing user by phone. +5. If no phone user exists, set current user's `phone` and mark verified. +6. If the phone user is the current user, ensure openid and phone are both present. +7. If another enabled user owns the phone, bind the openid to that phone user and issue new tokens for the phone user. +8. If the phone user already has a different WeChat openid, reject with a clear "phone already bound to another WeChat account" message. + +When the phone belongs to an existing account, the existing account is the canonical identity. The response must return tokens for that account so future openid login lands on the same user as phone SMS login. + +## Frontend Design + +### Login Page + +Update `mini-program/src/pages/login/index.vue` while preserving the current visual language: + +- Keep the dark background, glass card, purple accents, and safe-area handling. +- Add a primary WeChat login button at the top of the card. +- Keep the existing phone number and SMS code form as an alternate method below a subtle divider. +- Use one `loading` state for the active login method and prevent duplicate taps. +- On successful login, reuse current routing: + - has profile -> `/pages/main/index?tab=script` + - no profile -> `/pages/onboarding/index` + +### Frontend Services And Store + +Add to `mini-program/src/services/auth.js`: + +- `wechatLogin({ code, nickname, avatar })` +- `bindWechatPhone({ code })` + +Both methods store returned tokens exactly like existing `login()` and `refreshToken()`. + +Add to `mini-program/src/stores/app.js`: + +- `wechatLogin(profilePayload)` +- `bindWechatPhone(phoneCode)` +- Keep `login(phone, smsCode)` unchanged. +- After phone binding returns new tokens, refresh current profile/user state. + +### Phone Authorization UI + +Add a small phone-bind entry in the logged-in experience, preferably in `MineView.vue` or onboarding completion area: + +- Show only when logged in and `userInfo/userProfile` has no phone. +- Use WeChat `button open-type="getPhoneNumber"` on mp-weixin. +- On non-WeChat/H5 builds, show the existing phone SMS login/bind fallback. +- If authorization succeeds and backend returns tokens for an existing phone user, overwrite local tokens and refresh profile. +- If the user denies authorization, keep them logged in and show a non-blocking message. + +### Nickname And Avatar + +Use WeChat's current nickname/avatar collection pattern in profile or onboarding: + +- Nickname input can populate existing `registrationData.nickname`. +- Avatar selection can populate existing avatar fields. +- Save through existing user/profile update endpoints where possible. +- Do not block WeChat openid login on nickname or avatar. + +## Error Handling + +- Missing backend WeChat appid/secret: backend returns configuration error; frontend keeps phone login available. +- `uni.login` failure: show a concise "WeChat login failed, please try again later" message and keep phone login visible. +- `code2Session` failure or invalid code: show a concise login failure message. +- Disabled user: reject login consistently with existing auth behavior. +- Phone authorization denied: do not log out; show a non-blocking message that the user can bind a phone later in the profile center. +- Phone API unavailable or permission not enabled: keep openid login active and route user normally. +- Existing phone is bound to another openid: reject binding and ask user to use phone login or contact support. +- Network failures: preserve current session restore behavior and avoid clearing tokens unless backend explicitly rejects auth. + +## Data Flow + +### WeChat Login + +1. User taps WeChat login. +2. Mini program calls `uni.login`. +3. Mini program posts `{ code }` to `/auth/wechat/login`. +4. Backend exchanges code for openid through WeChat. +5. Backend finds or creates user by openid. +6. Backend returns `AuthResponse`. +7. Mini program stores tokens, fetches profile, and routes. + +### Phone Auto-Binding + +1. Logged-in user taps authorize phone. +2. WeChat returns a phone authorization code. +3. Mini program posts `{ code }` to `/auth/wechat/phone`. +4. Backend exchanges code for phone number. +5. Backend finds existing user by phone. +6. Backend binds openid to the phone user or writes phone to current user. +7. Backend returns `AuthResponse`. +8. Mini program replaces tokens and refreshes user/profile state. + +## Testing + +Backend: + +- WeChat login creates a new user for a new openid. +- WeChat login returns the same user for a known openid. +- Phone binding writes phone to current WeChat user when no existing phone user exists. +- Phone binding binds openid to an existing phone user and returns that user's tokens. +- Phone binding rejects a phone user already bound to a different openid. +- Existing phone SMS login still works. +- Token validation and refresh still work after WeChat login. +- `/api/auth/wechat/login` is public, and `/api/auth/wechat/phone` rejects unauthenticated requests. + +Mini program: + +- Build `npm run build:mp-weixin`. +- Login page supports both WeChat and phone SMS login. +- WeChat login success routes through the current profile/onboarding rules. +- Phone login success routes through the current profile/onboarding rules. +- Denying phone authorization does not break logged-in state. +- Successful phone authorization refreshes tokens and profile. +- Personal center reflects nickname/avatar/phone state where available. + +## References + +- WeChat Mini Program `code2Session`: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html +- WeChat Mini Program phone number API: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html +- WeChat Mini Program avatar/nickname capability: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html