fix(api): 修复前后端 API 对接 — 响应结构+字段名对齐
端到端审查发现并修复 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 <noreply@anthropic.com>
This commit is contained in:
parent
7d00cade2f
commit
e8d9bdc2fb
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ class AuthService {
|
|||
}
|
||||
|
||||
/// 密码登录
|
||||
Future<LoginResult> loginByPassword(String email, String password) async {
|
||||
Future<LoginResult> 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<String, dynamic> ? response.data : {};
|
||||
|
|
@ -104,9 +104,19 @@ class LoginResult {
|
|||
});
|
||||
|
||||
factory LoginResult.fromJson(Map<String, dynamic> json) {
|
||||
// 后端返回 { user, tokens: { accessToken, refreshToken, expiresIn } }
|
||||
final tokens = json['tokens'] as Map<String, dynamic>?;
|
||||
if (tokens != null) {
|
||||
return LoginResult(
|
||||
accessToken: tokens['accessToken'] as String? ?? '',
|
||||
refreshToken: tokens['refreshToken'] as String?,
|
||||
user: json['user'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
// 兼容 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<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<LoginResult>('/api/v1/auth/login-phone', { phone, smsCode, ...(deviceInfo ? { deviceInfo } : {}) }, false);
|
||||
export async function loginByPhone(phone: string, smsCode: string, deviceInfo?: string) {
|
||||
const resp = await post<AuthResponse>('/api/v1/auth/login-phone', { phone, smsCode, ...(deviceInfo ? { deviceInfo } : {}) }, false);
|
||||
return toLoginResult(resp);
|
||||
}
|
||||
|
||||
/** 密码登录 */
|
||||
export function loginByPassword(identifier: string, password: string) {
|
||||
return post<LoginResult>('/api/v1/auth/login', { identifier, password }, false);
|
||||
export async function loginByPassword(identifier: string, password: string) {
|
||||
const resp = await post<AuthResponse>('/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<LoginResult>('/api/v1/auth/login-wechat', { code }, false);
|
||||
export async function loginByWechat(code: string) {
|
||||
const resp = await post<AuthResponse>('/api/v1/auth/login-wechat', { code }, false);
|
||||
return toLoginResult(resp);
|
||||
}
|
||||
|
||||
/** 注册 */
|
||||
export function register(phone: string, smsCode: string, password?: string, nickname?: string) {
|
||||
return post<LoginResult>('/api/v1/auth/register', {
|
||||
export async function register(phone: string, smsCode: string, password?: string, nickname?: string) {
|
||||
const resp = await post<AuthResponse>('/api/v1/auth/register', {
|
||||
phone, smsCode,
|
||||
...(password ? { password } : {}),
|
||||
...(nickname ? { nickname } : {}),
|
||||
}, false);
|
||||
return toLoginResult(resp);
|
||||
}
|
||||
|
||||
/** 重置密码 */
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
const result = await post<LoginResult>('/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<AuthResponse>('/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<void> {
|
||||
const { code } = await Taro.login();
|
||||
const result = await post<LoginResult>('/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<AuthResponse>('/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();
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue