feat(miniapp+admin-app): 同步 SMS 认证 API 变更
Phase 7 — Taro miniapp + admin-app 前端同步后端 SMS 认证系统: miniapp (Taro/React): - auth.ts: 新增 SmsCodeType 类型,sendSmsCode 支持 type 参数 · 端点从 /auth/send-sms-code → /auth/sms/send · 新增 loginByPassword / resetPassword API · register 支持 password + nickname 可选参数 - store/auth.ts: sendSmsCode 同步 type 参数 + 新端点 - login/index.tsx: 发送验证码时指定 type='LOGIN' - h5-register/index.tsx: 发送验证码时指定 type='REGISTER' · 修复注册后 token 存储使用 config.TOKEN_KEY 而非硬编码 - i18n: 3语言新增 7 key (login_code_sent, login_error_*, register_success, register_error_agree) admin-app (Flutter): - auth_service.dart: 新增 SmsCodeType 枚举 · sendSmsCode 支持 type 参数,端点同步 /auth/sms/send · 返回 expiresIn (秒) - issuer_login_page.dart: 发送验证码时指定 SmsCodeType.login - i18n: 3语言新增 4 key (login_error_phone, login_error_code, login_error_network, login_code_sent) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4b1cdf9fb3
commit
c29067eee7
|
|
@ -78,6 +78,10 @@ class AppLocalizations {
|
||||||
'login_agreement': '《发行方服务协议》',
|
'login_agreement': '《发行方服务协议》',
|
||||||
'login_button': '登录',
|
'login_button': '登录',
|
||||||
'login_register': '还没有账号?申请入驻',
|
'login_register': '还没有账号?申请入驻',
|
||||||
|
'login_error_phone': '请输入手机号',
|
||||||
|
'login_error_code': '请输入6位验证码',
|
||||||
|
'login_error_network': '网络错误,请稍后重试',
|
||||||
|
'login_code_sent': '验证码已发送',
|
||||||
|
|
||||||
// ── Onboarding ──
|
// ── Onboarding ──
|
||||||
'onboarding_title': '企业入驻',
|
'onboarding_title': '企业入驻',
|
||||||
|
|
@ -639,6 +643,10 @@ class AppLocalizations {
|
||||||
'login_agreement': 'Issuer Service Agreement',
|
'login_agreement': 'Issuer Service Agreement',
|
||||||
'login_button': 'Sign In',
|
'login_button': 'Sign In',
|
||||||
'login_register': 'No account? Apply to join',
|
'login_register': 'No account? Apply to join',
|
||||||
|
'login_error_phone': 'Please enter your phone number',
|
||||||
|
'login_error_code': 'Please enter the 6-digit code',
|
||||||
|
'login_error_network': 'Network error, please try again',
|
||||||
|
'login_code_sent': 'Code sent',
|
||||||
|
|
||||||
// ── Onboarding ──
|
// ── Onboarding ──
|
||||||
'onboarding_title': 'Business Onboarding',
|
'onboarding_title': 'Business Onboarding',
|
||||||
|
|
@ -1200,6 +1208,10 @@ class AppLocalizations {
|
||||||
'login_agreement': '発行者サービス規約',
|
'login_agreement': '発行者サービス規約',
|
||||||
'login_button': 'ログイン',
|
'login_button': 'ログイン',
|
||||||
'login_register': 'アカウントがない場合は申請',
|
'login_register': 'アカウントがない場合は申請',
|
||||||
|
'login_error_phone': '電話番号を入力してください',
|
||||||
|
'login_error_code': '6桁の認証コードを入力してください',
|
||||||
|
'login_error_network': 'ネットワークエラーです。後でもう一度お試しください',
|
||||||
|
'login_code_sent': '認証コードを送信しました',
|
||||||
|
|
||||||
// ── Onboarding ──
|
// ── Onboarding ──
|
||||||
'onboarding_title': '企業登録',
|
'onboarding_title': '企業登録',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
|
|
||||||
|
/// SMS 验证码类型
|
||||||
|
enum SmsCodeType {
|
||||||
|
register('REGISTER'),
|
||||||
|
login('LOGIN'),
|
||||||
|
resetPassword('RESET_PASSWORD'),
|
||||||
|
changePhone('CHANGE_PHONE');
|
||||||
|
|
||||||
|
final String value;
|
||||||
|
const SmsCodeType(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
/// 认证服务
|
/// 认证服务
|
||||||
class AuthService {
|
class AuthService {
|
||||||
final ApiClient _apiClient;
|
final ApiClient _apiClient;
|
||||||
|
|
@ -8,13 +19,16 @@ class AuthService {
|
||||||
AuthService({ApiClient? apiClient})
|
AuthService({ApiClient? apiClient})
|
||||||
: _apiClient = apiClient ?? ApiClient.instance;
|
: _apiClient = apiClient ?? ApiClient.instance;
|
||||||
|
|
||||||
/// 发送短信验证码
|
/// 发送短信验证码 (支持类型)
|
||||||
Future<bool> sendSmsCode(String phone) async {
|
Future<int> sendSmsCode(String phone, {SmsCodeType type = SmsCodeType.login}) async {
|
||||||
try {
|
try {
|
||||||
await _apiClient.post('/api/v1/auth/send-sms-code', data: {
|
final response = await _apiClient.post('/api/v1/auth/sms/send', data: {
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
|
'type': type.value,
|
||||||
});
|
});
|
||||||
return true;
|
final data = response.data is Map<String, dynamic> ? response.data : {};
|
||||||
|
final inner = data['data'] ?? data;
|
||||||
|
return (inner is Map<String, dynamic>) ? (inner['expiresIn'] ?? 300) as int : 300;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[AuthService] sendSmsCode 失败: $e');
|
debugPrint('[AuthService] sendSmsCode 失败: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class _IssuerLoginPageState extends State<IssuerLoginPage> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _authService.sendSmsCode(phone);
|
await _authService.sendSmsCode(phone, type: SmsCodeType.login);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSendingCode = false;
|
_isSendingCode = false;
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,12 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||||
'login_code': '验证码',
|
'login_code': '验证码',
|
||||||
'login_btn': '登录',
|
'login_btn': '登录',
|
||||||
'login_agree': '登录即表示同意',
|
'login_agree': '登录即表示同意',
|
||||||
|
'login_code_sent': '验证码已发送',
|
||||||
|
'login_error_phone': '请输入正确的手机号',
|
||||||
|
'login_error_code': '请输入6位验证码',
|
||||||
|
'login_error_network': '网络错误,请稍后重试',
|
||||||
|
'register_success': '注册成功',
|
||||||
|
'register_error_agree': '请先同意用户协议',
|
||||||
|
|
||||||
// ── Detail (additional) ──
|
// ── Detail (additional) ──
|
||||||
'coupon_type_label': '类型',
|
'coupon_type_label': '类型',
|
||||||
|
|
@ -660,6 +666,12 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||||
'login_code': 'Verification code',
|
'login_code': 'Verification code',
|
||||||
'login_btn': 'Log In',
|
'login_btn': 'Log In',
|
||||||
'login_agree': 'By logging in, you agree to the',
|
'login_agree': 'By logging in, you agree to the',
|
||||||
|
'login_code_sent': 'Code sent',
|
||||||
|
'login_error_phone': 'Please enter a valid phone number',
|
||||||
|
'login_error_code': 'Please enter a 6-digit code',
|
||||||
|
'login_error_network': 'Network error, please try again',
|
||||||
|
'register_success': 'Registration successful',
|
||||||
|
'register_error_agree': 'Please agree to the terms first',
|
||||||
|
|
||||||
// ── Detail (additional) ──
|
// ── Detail (additional) ──
|
||||||
'coupon_type_label': 'Type',
|
'coupon_type_label': 'Type',
|
||||||
|
|
@ -1110,6 +1122,12 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||||
'login_code': '認証コード',
|
'login_code': '認証コード',
|
||||||
'login_btn': 'ログイン',
|
'login_btn': 'ログイン',
|
||||||
'login_agree': 'ログインすると以下に同意したものとみなされます',
|
'login_agree': 'ログインすると以下に同意したものとみなされます',
|
||||||
|
'login_code_sent': '認証コードを送信しました',
|
||||||
|
'login_error_phone': '正しい電話番号を入力してください',
|
||||||
|
'login_error_code': '6桁の認証コードを入力してください',
|
||||||
|
'login_error_network': 'ネットワークエラーです。後でもう一度お試しください',
|
||||||
|
'register_success': '登録が完了しました',
|
||||||
|
'register_error_agree': '利用規約に同意してください',
|
||||||
|
|
||||||
// ── Detail (additional) ──
|
// ── Detail (additional) ──
|
||||||
'coupon_type_label': '種類',
|
'coupon_type_label': '種類',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { register, sendSmsCode } from '../../services/auth';
|
import { register, sendSmsCode, type SmsCodeType } from '../../services/auth';
|
||||||
import { authStore } from '../../store/auth';
|
import { authStore } from '../../store/auth';
|
||||||
import logoIcon from '@/assets/images/logo_icon.png';
|
import logoIcon from '@/assets/images/logo_icon.png';
|
||||||
// Taro mini-program component
|
// Taro mini-program component
|
||||||
|
|
@ -24,7 +24,7 @@ const H5RegisterPage: React.FC = () => {
|
||||||
const handleSendCode = () => {
|
const handleSendCode = () => {
|
||||||
if (!phone || phone.length < 11 || codeSending || codeCountdown > 0) return;
|
if (!phone || phone.length < 11 || codeSending || codeCountdown > 0) return;
|
||||||
setCodeSending(true);
|
setCodeSending(true);
|
||||||
sendSmsCode(phone)
|
sendSmsCode(phone, 'REGISTER')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Taro.showToast({ title: t('login_code_sent') || '验证码已发送', icon: 'success' });
|
Taro.showToast({ title: t('login_code_sent') || '验证码已发送', icon: 'success' });
|
||||||
setCodeCountdown(60);
|
setCodeCountdown(60);
|
||||||
|
|
@ -47,8 +47,10 @@ const H5RegisterPage: React.FC = () => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
register(phone, smsCode)
|
register(phone, smsCode)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
// Store tokens via auth store if needed
|
// Store tokens via config keys for consistency
|
||||||
Taro.setStorageSync('token', result.accessToken);
|
const { config } = require('../../config');
|
||||||
|
Taro.setStorageSync(config.TOKEN_KEY, result.accessToken);
|
||||||
|
Taro.setStorageSync(config.USER_KEY, JSON.stringify(result.user));
|
||||||
Taro.showToast({ title: t('register_success') || '注册成功', icon: 'success' });
|
Taro.showToast({ title: t('register_success') || '注册成功', icon: 'success' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Taro.reLaunch({ url: '/pages/home/index' });
|
Taro.reLaunch({ url: '/pages/home/index' });
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||||
import { t } from '@/i18n';
|
import { t } from '@/i18n';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { authStore } from '../../store/auth';
|
import { authStore } from '../../store/auth';
|
||||||
import { sendSmsCode } from '../../services/auth';
|
import { sendSmsCode, type SmsCodeType } from '../../services/auth';
|
||||||
import logoIcon from '@/assets/images/logo_icon.png';
|
import logoIcon from '@/assets/images/logo_icon.png';
|
||||||
// Taro mini-program component
|
// Taro mini-program component
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ const LoginPage: React.FC = () => {
|
||||||
const handleSendCode = () => {
|
const handleSendCode = () => {
|
||||||
if (!phone || phone.length < 11 || codeSending || codeCountdown > 0) return;
|
if (!phone || phone.length < 11 || codeSending || codeCountdown > 0) return;
|
||||||
setCodeSending(true);
|
setCodeSending(true);
|
||||||
sendSmsCode(phone)
|
sendSmsCode(phone, 'LOGIN')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Taro.showToast({ title: t('login_code_sent') || '验证码已发送', icon: 'success' });
|
Taro.showToast({ title: t('login_code_sent') || '验证码已发送', icon: 'success' });
|
||||||
setCodeCountdown(60);
|
setCodeCountdown(60);
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,22 @@ interface LoginResult {
|
||||||
user: { id: string; phone?: string; nickname?: string; kycLevel: number };
|
user: { id: string; phone?: string; nickname?: string; kycLevel: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** SMS 验证码类型 */
|
||||||
|
export type SmsCodeType = 'REGISTER' | 'LOGIN' | 'RESET_PASSWORD' | 'CHANGE_PHONE';
|
||||||
|
|
||||||
/** 手机号+验证码登录 */
|
/** 手机号+验证码登录 */
|
||||||
export function loginByPhone(phone: string, smsCode: string) {
|
export function loginByPhone(phone: string, smsCode: string, deviceInfo?: string) {
|
||||||
return post<LoginResult>('/api/v1/auth/login-phone', { phone, smsCode }, false);
|
return post<LoginResult>('/api/v1/auth/login-phone', { phone, smsCode, ...(deviceInfo ? { deviceInfo } : {}) }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 密码登录 */
|
||||||
|
export function loginByPassword(identifier: string, password: string) {
|
||||||
|
return post<LoginResult>('/api/v1/auth/login', { identifier, password }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送短信验证码 */
|
/** 发送短信验证码 */
|
||||||
export function sendSmsCode(phone: string) {
|
export function sendSmsCode(phone: string, type: SmsCodeType = 'LOGIN') {
|
||||||
return post<{ success: boolean }>('/api/v1/auth/send-sms-code', { phone }, false);
|
return post<{ expiresIn: number }>('/api/v1/auth/sms/send', { phone, type }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 微信登录 */
|
/** 微信登录 */
|
||||||
|
|
@ -22,8 +30,17 @@ export function loginByWechat(code: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注册 */
|
/** 注册 */
|
||||||
export function register(phone: string, smsCode: string) {
|
export function register(phone: string, smsCode: string, password?: string, nickname?: string) {
|
||||||
return post<LoginResult>('/api/v1/auth/register', { phone, smsCode }, false);
|
return post<LoginResult>('/api/v1/auth/register', {
|
||||||
|
phone, smsCode,
|
||||||
|
...(password ? { password } : {}),
|
||||||
|
...(nickname ? { nickname } : {}),
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置密码 */
|
||||||
|
export function resetPassword(phone: string, smsCode: string, newPassword: string) {
|
||||||
|
return post<void>('/api/v1/auth/reset-password', { phone, smsCode, newPassword }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登出 */
|
/** 登出 */
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,9 @@ export const authStore = {
|
||||||
notify();
|
notify();
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 发送短信验证码 */
|
/** 发送短信验证码 (支持类型) */
|
||||||
async sendSmsCode(phone: string): Promise<void> {
|
async sendSmsCode(phone: string, type: 'REGISTER' | 'LOGIN' | 'RESET_PASSWORD' | 'CHANGE_PHONE' = 'LOGIN'): Promise<void> {
|
||||||
await post('/api/v1/auth/send-sms-code', { phone }, false);
|
await post('/api/v1/auth/sms/send', { phone, type }, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 微信一键登录 */
|
/** 微信一键登录 */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue