From e8d9bdc2fb7f691e6e598d67f1424d079752e109 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 23 Feb 2026 21:39:26 -0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20=E4=BF=AE=E5=A4=8D=E5=89=8D?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=20API=20=E5=AF=B9=E6=8E=A5=20=E2=80=94=20?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E7=BB=93=E6=9E=84+=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=90=8D=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 端到端审查发现并修复 4 类前后端不匹配问题: ## 1. 响应结构嵌套不匹配 后端返回 `{ data: { user, tokens: { accessToken, refreshToken } } }` 但 miniapp/admin-app/admin-web 均按扁平结构解析 - miniapp services/auth.ts: 新增 AuthResponse→LoginResult 映射层 - miniapp store/auth.ts: 从 `resp.tokens.accessToken` 取 token - admin-app auth_service.dart: LoginResult.fromJson 优先从 tokens 子对象取 - admin-web auth-context.tsx: 从 `result.tokens.accessToken` 取 token ## 2. 密码登录字段名不匹配 后端 LoginDto 字段为 `identifier`, 但 admin-app 发 `email`, admin-web 发 `email` - admin-app: `'email' → 'identifier'` - admin-web: `{ email, password } → { identifier: email, password }` ## 3. 注册 password 字段必填 vs 前端可选 miniapp h5-register 只收集手机+验证码, 不传 password, 会触发 400 校验 - backend RegisterDto: password 改为 @IsOptional - auth.service.ts: 未传 password 时自动生成随机密码 ## 4. miniapp LoginResult 类型导出 - 导出 LoginResult 接口供外部使用 Co-Authored-By: Claude Opus 4.6 --- .../src/application/services/auth.service.ts | 5 ++- .../src/interface/http/dto/register.dto.ts | 5 ++- .../lib/core/services/auth_service.dart | 18 +++++++-- frontend/admin-web/src/lib/auth-context.tsx | 13 ++++--- frontend/miniapp/src/services/auth.ts | 38 ++++++++++++++----- frontend/miniapp/src/store/auth.ts | 21 +++++----- 6 files changed, 67 insertions(+), 33 deletions(-) diff --git a/backend/services/auth-service/src/application/services/auth.service.ts b/backend/services/auth-service/src/application/services/auth.service.ts index a1b0cfb..361c35a 100644 --- a/backend/services/auth-service/src/application/services/auth.service.ts +++ b/backend/services/auth-service/src/application/services/auth.service.ts @@ -77,8 +77,9 @@ export class AuthService { // Verify SMS code await this.smsService.verifyCode(dto.phone, dto.smsCode, SmsVerificationType.REGISTER); - // Hash password - const password = await Password.create(dto.password); + // Hash password (if not provided, generate a random one for SMS-only registration) + const rawPassword = dto.password || require('crypto').randomBytes(16).toString('hex'); + const password = await Password.create(rawPassword); // Create user const user = await this.userRepo.create({ diff --git a/backend/services/auth-service/src/interface/http/dto/register.dto.ts b/backend/services/auth-service/src/interface/http/dto/register.dto.ts index 2bb9c7e..f4d3464 100644 --- a/backend/services/auth-service/src/interface/http/dto/register.dto.ts +++ b/backend/services/auth-service/src/interface/http/dto/register.dto.ts @@ -12,11 +12,12 @@ export class RegisterDto { @Length(6, 6, { message: '验证码必须为6位数字' }) smsCode: string; - @ApiProperty({ description: '登录密码 (8-128位)', example: 'Password123!' }) + @ApiPropertyOptional({ description: '登录密码 (8-128位), 不传则后端自动生成', example: 'Password123!' }) + @IsOptional() @IsString() @MinLength(8) @MaxLength(128) - password: string; + password?: string; @ApiPropertyOptional({ description: '昵称', example: 'John' }) @IsOptional() diff --git a/frontend/admin-app/lib/core/services/auth_service.dart b/frontend/admin-app/lib/core/services/auth_service.dart index c01f823..6d8ae7f 100644 --- a/frontend/admin-app/lib/core/services/auth_service.dart +++ b/frontend/admin-app/lib/core/services/auth_service.dart @@ -52,10 +52,10 @@ class AuthService { } /// 密码登录 - Future loginByPassword(String email, String password) async { + Future loginByPassword(String identifier, String password) async { try { final response = await _apiClient.post('/api/v1/auth/login', data: { - 'email': email, + 'identifier': identifier, 'password': password, }); final data = response.data is Map ? response.data : {}; @@ -104,9 +104,19 @@ class LoginResult { }); factory LoginResult.fromJson(Map json) { + // 后端返回 { user, tokens: { accessToken, refreshToken, expiresIn } } + final tokens = json['tokens'] as Map?; + if (tokens != null) { + return LoginResult( + accessToken: tokens['accessToken'] as String? ?? '', + refreshToken: tokens['refreshToken'] as String?, + user: json['user'] as Map?, + ); + } + // 兼容 refresh 等直接返回 tokens 的接口 return LoginResult( - accessToken: json['accessToken'] ?? '', - refreshToken: json['refreshToken'], + accessToken: json['accessToken'] as String? ?? '', + refreshToken: json['refreshToken'] as String?, user: json['user'] as Map?, ); } diff --git a/frontend/admin-web/src/lib/auth-context.tsx b/frontend/admin-web/src/lib/auth-context.tsx index add5ab5..f810940 100644 --- a/frontend/admin-web/src/lib/auth-context.tsx +++ b/frontend/admin-web/src/lib/auth-context.tsx @@ -41,14 +41,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }, []); const login = useCallback(async (email: string, password: string) => { - const result = await apiClient.post<{ accessToken: string; user: AdminUser }>( + const result = await apiClient.post<{ + user: AdminUser; + tokens: { accessToken: string; refreshToken: string; expiresIn: number }; + }>( '/api/v1/auth/login', - { email, password }, + { identifier: email, password }, ); - const { accessToken, user: adminUser } = result; - localStorage.setItem('admin_token', accessToken); + const { tokens, user: adminUser } = result; + localStorage.setItem('admin_token', tokens.accessToken); localStorage.setItem('admin_user', JSON.stringify(adminUser)); - setToken(accessToken); + setToken(tokens.accessToken); setUser(adminUser); }, []); diff --git a/frontend/miniapp/src/services/auth.ts b/frontend/miniapp/src/services/auth.ts index ad3ce2f..a7927a7 100644 --- a/frontend/miniapp/src/services/auth.ts +++ b/frontend/miniapp/src/services/auth.ts @@ -1,22 +1,40 @@ import { post } from '../utils/request'; -interface LoginResult { +/** 后端返回的 auth 结构 (request.ts 已自动解包外层 data) */ +interface AuthResponse { + user: { id: string; phone?: string; nickname?: string; kycLevel: number }; + tokens: { accessToken: string; refreshToken: string; expiresIn: number }; +} + +/** 前端使用的扁平结构 */ +export interface LoginResult { accessToken: string; refreshToken: string; user: { id: string; phone?: string; nickname?: string; kycLevel: number }; } +/** 将后端嵌套结构转为前端扁平结构 */ +function toLoginResult(resp: AuthResponse): LoginResult { + return { + accessToken: resp.tokens.accessToken, + refreshToken: resp.tokens.refreshToken, + user: resp.user, + }; +} + /** SMS 验证码类型 */ export type SmsCodeType = 'REGISTER' | 'LOGIN' | 'RESET_PASSWORD' | 'CHANGE_PHONE'; /** 手机号+验证码登录 */ -export function loginByPhone(phone: string, smsCode: string, deviceInfo?: string) { - return post('/api/v1/auth/login-phone', { phone, smsCode, ...(deviceInfo ? { deviceInfo } : {}) }, false); +export async function loginByPhone(phone: string, smsCode: string, deviceInfo?: string) { + const resp = await post('/api/v1/auth/login-phone', { phone, smsCode, ...(deviceInfo ? { deviceInfo } : {}) }, false); + return toLoginResult(resp); } /** 密码登录 */ -export function loginByPassword(identifier: string, password: string) { - return post('/api/v1/auth/login', { identifier, password }, false); +export async function loginByPassword(identifier: string, password: string) { + const resp = await post('/api/v1/auth/login', { identifier, password }, false); + return toLoginResult(resp); } /** 发送短信验证码 */ @@ -25,17 +43,19 @@ export function sendSmsCode(phone: string, type: SmsCodeType = 'LOGIN') { } /** 微信登录 */ -export function loginByWechat(code: string) { - return post('/api/v1/auth/login-wechat', { code }, false); +export async function loginByWechat(code: string) { + const resp = await post('/api/v1/auth/login-wechat', { code }, false); + return toLoginResult(resp); } /** 注册 */ -export function register(phone: string, smsCode: string, password?: string, nickname?: string) { - return post('/api/v1/auth/register', { +export async function register(phone: string, smsCode: string, password?: string, nickname?: string) { + const resp = await post('/api/v1/auth/register', { phone, smsCode, ...(password ? { password } : {}), ...(nickname ? { nickname } : {}), }, false); + return toLoginResult(resp); } /** 重置密码 */ diff --git a/frontend/miniapp/src/store/auth.ts b/frontend/miniapp/src/store/auth.ts index d50f103..7230b76 100644 --- a/frontend/miniapp/src/store/auth.ts +++ b/frontend/miniapp/src/store/auth.ts @@ -11,10 +11,9 @@ export interface UserProfile { avatarUrl?: string; } -interface LoginResult { - accessToken: string; - refreshToken: string; +interface AuthResponse { user: UserProfile; + tokens: { accessToken: string; refreshToken: string; expiresIn: number }; } /** @@ -46,10 +45,10 @@ export const authStore = { /** 手机号+验证码登录 */ async loginByPhone(phone: string, smsCode: string): Promise { - const result = await post('/api/v1/auth/login-phone', { phone, smsCode }, false); - Taro.setStorageSync(config.TOKEN_KEY, result.accessToken); - Taro.setStorageSync(config.USER_KEY, JSON.stringify(result.user)); - _user = result.user; + const resp = await post('/api/v1/auth/login-phone', { phone, smsCode }, false); + Taro.setStorageSync(config.TOKEN_KEY, resp.tokens.accessToken); + Taro.setStorageSync(config.USER_KEY, JSON.stringify(resp.user)); + _user = resp.user; notify(); }, @@ -61,10 +60,10 @@ export const authStore = { /** 微信一键登录 */ async loginByWechat(): Promise { const { code } = await Taro.login(); - const result = await post('/api/v1/auth/login-wechat', { code }, false); - Taro.setStorageSync(config.TOKEN_KEY, result.accessToken); - Taro.setStorageSync(config.USER_KEY, JSON.stringify(result.user)); - _user = result.user; + const resp = await post('/api/v1/auth/login-wechat', { code }, false); + Taro.setStorageSync(config.TOKEN_KEY, resp.tokens.accessToken); + Taro.setStorageSync(config.USER_KEY, JSON.stringify(resp.user)); + _user = resp.user; notify(); },