1
This commit is contained in:
@@ -46,7 +46,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当
|
|||||||
3. 登录弹窗继续复用现有独立 modal 和页签结构,不在页面中新增功能说明类文案,也不把邀请码输入放回登录面板。
|
3. 登录弹窗继续复用现有独立 modal 和页签结构,不在页面中新增功能说明类文案,也不把邀请码输入放回登录面板。
|
||||||
4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5,并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。
|
4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5,并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。
|
||||||
5. 小程序内需要登录时不展示 H5 登录弹窗,也不走手输手机号 / 短信验证码流程;统一通过原生 `button open-type="getPhoneNumber"` 获取微信手机号授权,再调用 `/api/auth/wechat/miniprogram-login` 与 `/api/auth/wechat/bind-phone` 换取系统登录态。
|
5. 小程序内需要登录时不展示 H5 登录弹窗,也不走手输手机号 / 短信验证码流程;统一通过原生 `button open-type="getPhoneNumber"` 获取微信手机号授权,再调用 `/api/auth/wechat/miniprogram-login` 与 `/api/auth/wechat/bind-phone` 换取系统登录态。
|
||||||
6. 账号信息面板只展示 `账号信息` 标题;绑定手机号和绑定微信以紧凑模块展示完整绑定值,换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。
|
6. 账号信息面板只展示 `账号信息` 标题;绑定手机号以紧凑模块展示完整手机号,绑定微信展示微信昵称而不是微信账号标识,换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。
|
||||||
|
|
||||||
## 账户与充值
|
## 账户与充值
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type AuthUser = {
|
|||||||
loginMethod: AuthLoginMethod;
|
loginMethod: AuthLoginMethod;
|
||||||
bindingStatus: AuthBindingStatus;
|
bindingStatus: AuthBindingStatus;
|
||||||
wechatBound: boolean;
|
wechatBound: boolean;
|
||||||
|
wechatDisplayName?: string | null;
|
||||||
wechatAccount?: string | null;
|
wechatAccount?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2665,6 +2665,10 @@ mod tests {
|
|||||||
bind_payload["user"]["wechatAccount"],
|
bind_payload["user"]["wechatAccount"],
|
||||||
Value::String("wx-mini-code-bind-001".to_string())
|
Value::String("wx-mini-code-bind-001".to_string())
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bind_payload["user"]["wechatDisplayName"],
|
||||||
|
Value::String("微信旅人".to_string())
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
bind_payload["token"]
|
bind_payload["token"]
|
||||||
.as_str()
|
.as_str()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub fn map_auth_user_payload(user: AuthUser) -> AuthUserPayload {
|
|||||||
login_method: user.login_method.as_str().to_string(),
|
login_method: user.login_method.as_str().to_string(),
|
||||||
binding_status: user.binding_status.as_str().to_string(),
|
binding_status: user.binding_status.as_str().to_string(),
|
||||||
wechat_bound: user.wechat_bound,
|
wechat_bound: user.wechat_bound,
|
||||||
|
wechat_display_name: user.wechat_display_name,
|
||||||
wechat_account: user.wechat_account,
|
wechat_account: user.wechat_account,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ pub struct AuthUser {
|
|||||||
pub binding_status: AuthBindingStatus,
|
pub binding_status: AuthBindingStatus,
|
||||||
pub wechat_bound: bool,
|
pub wechat_bound: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub wechat_display_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub wechat_account: Option<String>,
|
pub wechat_account: Option<String>,
|
||||||
pub token_version: u64,
|
pub token_version: u64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -105,12 +105,21 @@ fn hydrate_private_auth_fields(
|
|||||||
if hydrated.user.phone_number.is_none() {
|
if hydrated.user.phone_number.is_none() {
|
||||||
hydrated.user.phone_number = hydrated.phone_number.clone();
|
hydrated.user.phone_number = hydrated.phone_number.clone();
|
||||||
}
|
}
|
||||||
if hydrated.user.wechat_account.is_none() {
|
let hydrated_wechat_identity = state
|
||||||
hydrated.user.wechat_account = state
|
|
||||||
.wechat_identity_by_provider_uid
|
.wechat_identity_by_provider_uid
|
||||||
.values()
|
.values()
|
||||||
.find(|identity| identity.user_id == hydrated.user.id)
|
.find(|identity| identity.user_id == hydrated.user.id);
|
||||||
.map(|identity| identity.provider_uid.clone());
|
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 =
|
||||||
|
hydrated_wechat_identity.map(|identity| identity.provider_uid.clone());
|
||||||
}
|
}
|
||||||
hydrated
|
hydrated
|
||||||
}
|
}
|
||||||
@@ -1100,6 +1109,7 @@ impl InMemoryAuthStore {
|
|||||||
login_method: AuthLoginMethod::Password,
|
login_method: AuthLoginMethod::Password,
|
||||||
binding_status: AuthBindingStatus::Active,
|
binding_status: AuthBindingStatus::Active,
|
||||||
wechat_bound: false,
|
wechat_bound: false,
|
||||||
|
wechat_display_name: None,
|
||||||
wechat_account: None,
|
wechat_account: None,
|
||||||
token_version: 1,
|
token_version: 1,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -1242,6 +1252,7 @@ impl InMemoryAuthStore {
|
|||||||
login_method: AuthLoginMethod::Phone,
|
login_method: AuthLoginMethod::Phone,
|
||||||
binding_status: AuthBindingStatus::Active,
|
binding_status: AuthBindingStatus::Active,
|
||||||
wechat_bound: false,
|
wechat_bound: false,
|
||||||
|
wechat_display_name: None,
|
||||||
wechat_account: None,
|
wechat_account: None,
|
||||||
token_version: 1,
|
token_version: 1,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -1303,6 +1314,7 @@ impl InMemoryAuthStore {
|
|||||||
login_method: AuthLoginMethod::Password,
|
login_method: AuthLoginMethod::Password,
|
||||||
binding_status: AuthBindingStatus::Active,
|
binding_status: AuthBindingStatus::Active,
|
||||||
wechat_bound: false,
|
wechat_bound: false,
|
||||||
|
wechat_display_name: None,
|
||||||
wechat_account: None,
|
wechat_account: None,
|
||||||
token_version: 1,
|
token_version: 1,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -1349,6 +1361,8 @@ impl InMemoryAuthStore {
|
|||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.unwrap_or("微信旅人")
|
.unwrap_or("微信旅人")
|
||||||
.to_string();
|
.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 username = build_wechat_username(&display_name, &profile.provider_uid);
|
||||||
let provider_uid = normalize_required_string(&profile.provider_uid).unwrap_or_default();
|
let provider_uid = normalize_required_string(&profile.provider_uid).unwrap_or_default();
|
||||||
let user = AuthUser {
|
let user = AuthUser {
|
||||||
@@ -1362,6 +1376,7 @@ impl InMemoryAuthStore {
|
|||||||
login_method: AuthLoginMethod::Wechat,
|
login_method: AuthLoginMethod::Wechat,
|
||||||
binding_status: AuthBindingStatus::PendingBindPhone,
|
binding_status: AuthBindingStatus::PendingBindPhone,
|
||||||
wechat_bound: true,
|
wechat_bound: true,
|
||||||
|
wechat_display_name,
|
||||||
wechat_account: Some(provider_uid.clone()),
|
wechat_account: Some(provider_uid.clone()),
|
||||||
token_version: 1,
|
token_version: 1,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -1518,6 +1533,9 @@ impl InMemoryAuthStore {
|
|||||||
stored_user.user.display_name = display_name.to_string();
|
stored_user.user.display_name = display_name.to_string();
|
||||||
}
|
}
|
||||||
stored_user.user.wechat_account = Some(next_provider_uid.clone());
|
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()
|
stored_user.user.clone()
|
||||||
};
|
};
|
||||||
self.persist_wechat_state(&state)?;
|
self.persist_wechat_state(&state)?;
|
||||||
@@ -1753,6 +1771,7 @@ impl InMemoryAuthStore {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.ok_or(PhoneAuthError::UserStateMismatch)?;
|
.ok_or(PhoneAuthError::UserStateMismatch)?;
|
||||||
let pending_wechat_account = pending_wechat_identity.provider_uid.clone();
|
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
|
let pending_username = state
|
||||||
.users_by_username
|
.users_by_username
|
||||||
@@ -1782,6 +1801,7 @@ impl InMemoryAuthStore {
|
|||||||
.ok_or(PhoneAuthError::UserNotFound)?;
|
.ok_or(PhoneAuthError::UserNotFound)?;
|
||||||
target_user.user.wechat_bound = true;
|
target_user.user.wechat_bound = true;
|
||||||
target_user.user.wechat_account = Some(pending_wechat_account);
|
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() {
|
if target_user.user.phone_number.is_none() {
|
||||||
target_user.user.phone_number = target_user.phone_number.clone();
|
target_user.user.phone_number = target_user.phone_number.clone();
|
||||||
}
|
}
|
||||||
@@ -1799,6 +1819,11 @@ impl InMemoryAuthStore {
|
|||||||
.values()
|
.values()
|
||||||
.find(|identity| identity.user_id == pending_user_id)
|
.find(|identity| identity.user_id == pending_user_id)
|
||||||
.map(|identity| identity.provider_uid.clone());
|
.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
|
let stored_user = state
|
||||||
.users_by_username
|
.users_by_username
|
||||||
@@ -1812,6 +1837,9 @@ impl InMemoryAuthStore {
|
|||||||
if stored_user.user.wechat_account.is_none() {
|
if stored_user.user.wechat_account.is_none() {
|
||||||
stored_user.user.wechat_account = bound_wechat_account;
|
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);
|
stored_user.phone_number = Some(phone_number.e164);
|
||||||
let next_user = stored_user.user.clone();
|
let next_user = stored_user.user.clone();
|
||||||
self.persist_phone_state(&state)?;
|
self.persist_phone_state(&state)?;
|
||||||
@@ -3368,6 +3396,10 @@ mod tests {
|
|||||||
AuthBindingStatus::PendingBindPhone
|
AuthBindingStatus::PendingBindPhone
|
||||||
);
|
);
|
||||||
assert_eq!(first_wechat.user.username, "微信旅人甲_wx-openid-first");
|
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.starts_with("user_"));
|
||||||
assert!(!first_wechat.user.id.ends_with("00000001"));
|
assert!(!first_wechat.user.id.ends_with("00000001"));
|
||||||
|
|
||||||
@@ -3389,6 +3421,10 @@ mod tests {
|
|||||||
assert_ne!(second_wechat.user.id, phone_user.id);
|
assert_ne!(second_wechat.user.id, phone_user.id);
|
||||||
assert_eq!(second_wechat.user.login_method, AuthLoginMethod::Wechat);
|
assert_eq!(second_wechat.user.login_method, AuthLoginMethod::Wechat);
|
||||||
assert_eq!(second_wechat.user.username, first_wechat.user.username);
|
assert_eq!(second_wechat.user.username, first_wechat.user.username);
|
||||||
|
assert_eq!(
|
||||||
|
second_wechat.user.wechat_display_name.as_deref(),
|
||||||
|
Some("微信旅人乙")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -3438,6 +3474,10 @@ mod tests {
|
|||||||
wechat_user.binding_status,
|
wechat_user.binding_status,
|
||||||
AuthBindingStatus::PendingBindPhone
|
AuthBindingStatus::PendingBindPhone
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wechat_user.wechat_display_name.as_deref(),
|
||||||
|
Some("待绑定微信用户")
|
||||||
|
);
|
||||||
assert_ne!(wechat_user.id, phone_user.id);
|
assert_ne!(wechat_user.id, phone_user.id);
|
||||||
|
|
||||||
phone_service
|
phone_service
|
||||||
@@ -3465,6 +3505,10 @@ mod tests {
|
|||||||
assert_eq!(merged.user.id, phone_user.id);
|
assert_eq!(merged.user.id, phone_user.id);
|
||||||
assert_eq!(merged.user.binding_status, AuthBindingStatus::Active);
|
assert_eq!(merged.user.binding_status, AuthBindingStatus::Active);
|
||||||
assert!(merged.user.wechat_bound);
|
assert!(merged.user.wechat_bound);
|
||||||
|
assert_eq!(
|
||||||
|
merged.user.wechat_display_name.as_deref(),
|
||||||
|
Some("待绑定微信用户")
|
||||||
|
);
|
||||||
|
|
||||||
let reused_wechat_user = wechat_service
|
let reused_wechat_user = wechat_service
|
||||||
.resolve_login(ResolveWechatLoginInput {
|
.resolve_login(ResolveWechatLoginInput {
|
||||||
@@ -3482,5 +3526,9 @@ mod tests {
|
|||||||
assert!(!reused_wechat_user.created);
|
assert!(!reused_wechat_user.created);
|
||||||
assert_eq!(reused_wechat_user.user.id, phone_user.id);
|
assert_eq!(reused_wechat_user.user.id, phone_user.id);
|
||||||
assert!(reused_wechat_user.user.wechat_bound);
|
assert!(reused_wechat_user.user.wechat_bound);
|
||||||
|
assert_eq!(
|
||||||
|
reused_wechat_user.user.wechat_display_name.as_deref(),
|
||||||
|
Some("已归并微信用户")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub struct AuthUserPayload {
|
|||||||
pub login_method: String,
|
pub login_method: String,
|
||||||
pub binding_status: String,
|
pub binding_status: String,
|
||||||
pub wechat_bound: bool,
|
pub wechat_bound: bool,
|
||||||
|
pub wechat_display_name: Option<String>,
|
||||||
pub wechat_account: Option<String>,
|
pub wechat_account: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const baseUser: AuthUser = {
|
|||||||
loginMethod: 'phone',
|
loginMethod: 'phone',
|
||||||
bindingStatus: 'active',
|
bindingStatus: 'active',
|
||||||
wechatBound: true,
|
wechatBound: true,
|
||||||
|
wechatDisplayName: '微信旅人甲',
|
||||||
wechatAccount: 'wx-openid-bind-001',
|
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).getByText('13800138000')).toBeTruthy();
|
||||||
expect(within(accountDialog).queryByText('138****8000')).toBeNull();
|
expect(within(accountDialog).queryByText('138****8000')).toBeNull();
|
||||||
expect(within(accountDialog).getByText('绑定微信')).toBeTruthy();
|
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(
|
const compactCards = accountDialog.querySelectorAll(
|
||||||
'[data-account-binding-card]',
|
'[data-account-binding-card]',
|
||||||
|
|||||||
@@ -443,8 +443,8 @@ export function AccountModal({
|
|||||||
|
|
||||||
const boundPhoneNumber =
|
const boundPhoneNumber =
|
||||||
user.phoneNumber?.trim() || user.phoneNumberMasked || '未绑定';
|
user.phoneNumber?.trim() || user.phoneNumberMasked || '未绑定';
|
||||||
const boundWechatAccount =
|
const boundWechatDisplayName =
|
||||||
user.wechatAccount?.trim() || (user.wechatBound ? '已绑定' : '未绑定');
|
user.wechatDisplayName?.trim() || (user.wechatBound ? '已绑定' : '未绑定');
|
||||||
|
|
||||||
const sectionSummaries: Record<PrimarySettingsSection, string> = {
|
const sectionSummaries: Record<PrimarySettingsSection, string> = {
|
||||||
appearance:
|
appearance:
|
||||||
@@ -620,7 +620,7 @@ export function AccountModal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1.5 break-all text-sm font-semibold text-[var(--platform-text-strong)]">
|
<div className="mt-1.5 break-all text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||||
{boundWechatAccount}
|
{boundWechatDisplayName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user