diff --git a/docs/【项目基线】当前产品与工程约束-2026-05-15.md b/docs/【项目基线】当前产品与工程约束-2026-05-15.md index 93556773..d1706f2f 100644 --- a/docs/【项目基线】当前产品与工程约束-2026-05-15.md +++ b/docs/【项目基线】当前产品与工程约束-2026-05-15.md @@ -46,7 +46,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当 3. 登录弹窗继续复用现有独立 modal 和页签结构,不在页面中新增功能说明类文案,也不把邀请码输入放回登录面板。 4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5,并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。 5. 小程序内需要登录时不展示 H5 登录弹窗,也不走手输手机号 / 短信验证码流程;统一通过原生 `button open-type="getPhoneNumber"` 获取微信手机号授权,再调用 `/api/auth/wechat/miniprogram-login` 与 `/api/auth/wechat/bind-phone` 换取系统登录态。 -6. 账号信息面板只展示 `账号信息` 标题;绑定手机号和绑定微信以紧凑模块展示完整绑定值,换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。 +6. 账号信息面板只展示 `账号信息` 标题;绑定手机号以紧凑模块展示完整手机号,绑定微信展示微信昵称而不是微信账号标识,换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。 ## 账户与充值 diff --git a/packages/shared/src/contracts/auth.ts b/packages/shared/src/contracts/auth.ts index 078edcd2..2a2aa6c6 100644 --- a/packages/shared/src/contracts/auth.ts +++ b/packages/shared/src/contracts/auth.ts @@ -11,6 +11,7 @@ export type AuthUser = { loginMethod: AuthLoginMethod; bindingStatus: AuthBindingStatus; wechatBound: boolean; + wechatDisplayName?: string | null; wechatAccount?: string | null; }; diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 9b061c12..08291aea 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -2665,6 +2665,10 @@ mod tests { bind_payload["user"]["wechatAccount"], Value::String("wx-mini-code-bind-001".to_string()) ); + assert_eq!( + bind_payload["user"]["wechatDisplayName"], + Value::String("微信旅人".to_string()) + ); assert!( bind_payload["token"] .as_str() diff --git a/server-rs/crates/api-server/src/auth_payload.rs b/server-rs/crates/api-server/src/auth_payload.rs index 15a0bfc7..4c2a6242 100644 --- a/server-rs/crates/api-server/src/auth_payload.rs +++ b/server-rs/crates/api-server/src/auth_payload.rs @@ -12,6 +12,7 @@ pub fn map_auth_user_payload(user: AuthUser) -> AuthUserPayload { login_method: user.login_method.as_str().to_string(), binding_status: user.binding_status.as_str().to_string(), wechat_bound: user.wechat_bound, + wechat_display_name: user.wechat_display_name, wechat_account: user.wechat_account, } } diff --git a/server-rs/crates/module-auth/src/domain.rs b/server-rs/crates/module-auth/src/domain.rs index e97a362c..19c8dae8 100644 --- a/server-rs/crates/module-auth/src/domain.rs +++ b/server-rs/crates/module-auth/src/domain.rs @@ -64,6 +64,8 @@ pub struct AuthUser { pub binding_status: AuthBindingStatus, pub wechat_bound: bool, #[serde(default)] + pub wechat_display_name: Option, + #[serde(default)] pub wechat_account: Option, pub token_version: u64, #[serde(default)] diff --git a/server-rs/crates/module-auth/src/lib.rs b/server-rs/crates/module-auth/src/lib.rs index f43a92bf..729a256a 100644 --- a/server-rs/crates/module-auth/src/lib.rs +++ b/server-rs/crates/module-auth/src/lib.rs @@ -105,12 +105,21 @@ fn hydrate_private_auth_fields( if hydrated.user.phone_number.is_none() { hydrated.user.phone_number = hydrated.phone_number.clone(); } + let hydrated_wechat_identity = state + .wechat_identity_by_provider_uid + .values() + .find(|identity| identity.user_id == hydrated.user.id); + if hydrated.user.wechat_display_name.is_none() { + hydrated.user.wechat_display_name = hydrated_wechat_identity + .and_then(|identity| identity.display_name.clone()) + .or_else(|| { + (hydrated.user.login_method == AuthLoginMethod::Wechat) + .then(|| hydrated.user.display_name.clone()) + }); + } if hydrated.user.wechat_account.is_none() { - hydrated.user.wechat_account = state - .wechat_identity_by_provider_uid - .values() - .find(|identity| identity.user_id == hydrated.user.id) - .map(|identity| identity.provider_uid.clone()); + hydrated.user.wechat_account = + hydrated_wechat_identity.map(|identity| identity.provider_uid.clone()); } hydrated } @@ -1100,6 +1109,7 @@ impl InMemoryAuthStore { login_method: AuthLoginMethod::Password, binding_status: AuthBindingStatus::Active, wechat_bound: false, + wechat_display_name: None, wechat_account: None, token_version: 1, created_at, @@ -1242,6 +1252,7 @@ impl InMemoryAuthStore { login_method: AuthLoginMethod::Phone, binding_status: AuthBindingStatus::Active, wechat_bound: false, + wechat_display_name: None, wechat_account: None, token_version: 1, created_at, @@ -1303,6 +1314,7 @@ impl InMemoryAuthStore { login_method: AuthLoginMethod::Password, binding_status: AuthBindingStatus::Active, wechat_bound: false, + wechat_display_name: None, wechat_account: None, token_version: 1, created_at, @@ -1349,6 +1361,8 @@ impl InMemoryAuthStore { .filter(|value| !value.is_empty()) .unwrap_or("微信旅人") .to_string(); + let wechat_display_name = normalize_optional_string(profile.display_name.clone()) + .or_else(|| Some(display_name.clone())); let username = build_wechat_username(&display_name, &profile.provider_uid); let provider_uid = normalize_required_string(&profile.provider_uid).unwrap_or_default(); let user = AuthUser { @@ -1362,6 +1376,7 @@ impl InMemoryAuthStore { login_method: AuthLoginMethod::Wechat, binding_status: AuthBindingStatus::PendingBindPhone, wechat_bound: true, + wechat_display_name, wechat_account: Some(provider_uid.clone()), token_version: 1, created_at, @@ -1518,6 +1533,9 @@ impl InMemoryAuthStore { stored_user.user.display_name = display_name.to_string(); } stored_user.user.wechat_account = Some(next_provider_uid.clone()); + if let Some(display_name) = next_display_name.clone() { + stored_user.user.wechat_display_name = Some(display_name); + } stored_user.user.clone() }; self.persist_wechat_state(&state)?; @@ -1753,6 +1771,7 @@ impl InMemoryAuthStore { .cloned() .ok_or(PhoneAuthError::UserStateMismatch)?; let pending_wechat_account = pending_wechat_identity.provider_uid.clone(); + let pending_wechat_display_name = pending_wechat_identity.display_name.clone(); let pending_username = state .users_by_username @@ -1782,6 +1801,7 @@ impl InMemoryAuthStore { .ok_or(PhoneAuthError::UserNotFound)?; target_user.user.wechat_bound = true; target_user.user.wechat_account = Some(pending_wechat_account); + target_user.user.wechat_display_name = pending_wechat_display_name; if target_user.user.phone_number.is_none() { target_user.user.phone_number = target_user.phone_number.clone(); } @@ -1799,6 +1819,11 @@ impl InMemoryAuthStore { .values() .find(|identity| identity.user_id == pending_user_id) .map(|identity| identity.provider_uid.clone()); + let bound_wechat_display_name = state + .wechat_identity_by_provider_uid + .values() + .find(|identity| identity.user_id == pending_user_id) + .and_then(|identity| identity.display_name.clone()); let stored_user = state .users_by_username @@ -1812,6 +1837,9 @@ impl InMemoryAuthStore { if stored_user.user.wechat_account.is_none() { stored_user.user.wechat_account = bound_wechat_account; } + if stored_user.user.wechat_display_name.is_none() { + stored_user.user.wechat_display_name = bound_wechat_display_name; + } stored_user.phone_number = Some(phone_number.e164); let next_user = stored_user.user.clone(); self.persist_phone_state(&state)?; @@ -3368,6 +3396,10 @@ mod tests { AuthBindingStatus::PendingBindPhone ); assert_eq!(first_wechat.user.username, "微信旅人甲_wx-openid-first"); + assert_eq!( + first_wechat.user.wechat_display_name.as_deref(), + Some("微信旅人甲") + ); assert!(first_wechat.user.id.starts_with("user_")); assert!(!first_wechat.user.id.ends_with("00000001")); @@ -3389,6 +3421,10 @@ mod tests { assert_ne!(second_wechat.user.id, phone_user.id); assert_eq!(second_wechat.user.login_method, AuthLoginMethod::Wechat); assert_eq!(second_wechat.user.username, first_wechat.user.username); + assert_eq!( + second_wechat.user.wechat_display_name.as_deref(), + Some("微信旅人乙") + ); } #[tokio::test] @@ -3438,6 +3474,10 @@ mod tests { wechat_user.binding_status, AuthBindingStatus::PendingBindPhone ); + assert_eq!( + wechat_user.wechat_display_name.as_deref(), + Some("待绑定微信用户") + ); assert_ne!(wechat_user.id, phone_user.id); phone_service @@ -3465,6 +3505,10 @@ mod tests { assert_eq!(merged.user.id, phone_user.id); assert_eq!(merged.user.binding_status, AuthBindingStatus::Active); assert!(merged.user.wechat_bound); + assert_eq!( + merged.user.wechat_display_name.as_deref(), + Some("待绑定微信用户") + ); let reused_wechat_user = wechat_service .resolve_login(ResolveWechatLoginInput { @@ -3482,5 +3526,9 @@ mod tests { assert!(!reused_wechat_user.created); assert_eq!(reused_wechat_user.user.id, phone_user.id); assert!(reused_wechat_user.user.wechat_bound); + assert_eq!( + reused_wechat_user.user.wechat_display_name.as_deref(), + Some("已归并微信用户") + ); } } diff --git a/server-rs/crates/shared-contracts/src/auth.rs b/server-rs/crates/shared-contracts/src/auth.rs index 1c3754a8..0c0f5809 100644 --- a/server-rs/crates/shared-contracts/src/auth.rs +++ b/server-rs/crates/shared-contracts/src/auth.rs @@ -24,6 +24,7 @@ pub struct AuthUserPayload { pub login_method: String, pub binding_status: String, pub wechat_bound: bool, + pub wechat_display_name: Option, pub wechat_account: Option, } diff --git a/src/components/auth/AccountModal.test.tsx b/src/components/auth/AccountModal.test.tsx index b41b2ea2..834ede9c 100644 --- a/src/components/auth/AccountModal.test.tsx +++ b/src/components/auth/AccountModal.test.tsx @@ -22,6 +22,7 @@ const baseUser: AuthUser = { loginMethod: 'phone', bindingStatus: 'active', wechatBound: true, + wechatDisplayName: '微信旅人甲', wechatAccount: 'wx-openid-bind-001', }; @@ -151,7 +152,8 @@ test('account panel uses compact binding cards and keeps logout actions at the b expect(within(accountDialog).getByText('13800138000')).toBeTruthy(); expect(within(accountDialog).queryByText('138****8000')).toBeNull(); expect(within(accountDialog).getByText('绑定微信')).toBeTruthy(); - expect(within(accountDialog).getByText('wx-openid-bind-001')).toBeTruthy(); + expect(within(accountDialog).getByText('微信旅人甲')).toBeTruthy(); + expect(within(accountDialog).queryByText('wx-openid-bind-001')).toBeNull(); const compactCards = accountDialog.querySelectorAll( '[data-account-binding-card]', diff --git a/src/components/auth/AccountModal.tsx b/src/components/auth/AccountModal.tsx index fee9793f..213c2055 100644 --- a/src/components/auth/AccountModal.tsx +++ b/src/components/auth/AccountModal.tsx @@ -443,8 +443,8 @@ export function AccountModal({ const boundPhoneNumber = user.phoneNumber?.trim() || user.phoneNumberMasked || '未绑定'; - const boundWechatAccount = - user.wechatAccount?.trim() || (user.wechatBound ? '已绑定' : '未绑定'); + const boundWechatDisplayName = + user.wechatDisplayName?.trim() || (user.wechatBound ? '已绑定' : '未绑定'); const sectionSummaries: Record = { appearance: @@ -620,7 +620,7 @@ export function AccountModal({
- {boundWechatAccount} + {boundWechatDisplayName}