userApi.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import { authService } from './authService';
  2. // API Base URL - 从环境变量获取
  3. // 默认改为 8010,与当前后端一致。若有自定义域名/端口,请在 .env.local 配置 VITE_API_BASE_URL
  4. const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8010';
  5. // 用户信息接口(与后端API对应)
  6. export interface User {
  7. id: string;
  8. username: string;
  9. nickname: string;
  10. phone: string | null;
  11. email: string | null;
  12. avatar: string | null;
  13. real_name?: string | null;
  14. is_verified?: string;
  15. verified_at?: string | null;
  16. registration_date: string;
  17. created_at: string;
  18. updated_at: string;
  19. apikey?: string; // API密钥字段
  20. }
  21. // 注册请求接口
  22. export interface RegisterRequest {
  23. username: string;
  24. password: string;
  25. nickname: string;
  26. email?: string;
  27. phone?: string;
  28. sms_code?: string;
  29. email_code?: string;
  30. }
  31. // 登录响应接口
  32. export interface LoginResponse {
  33. access_token: string;
  34. token_type: string;
  35. user: User;
  36. }
  37. // 更新用户信息请求接口
  38. export interface UpdateUserRequest {
  39. nickname?: string;
  40. phone?: string;
  41. email?: string;
  42. avatar?: string;
  43. apikey?: string;
  44. }
  45. // 实名认证请求接口
  46. export interface VerifyUserRequest {
  47. real_name: string;
  48. id_card: string;
  49. }
  50. // 错误响应接口
  51. export interface ErrorResponse {
  52. detail?: string;
  53. message?: string;
  54. }
  55. // Token验证响应接口
  56. export interface TokenVerifyResponse {
  57. valid: boolean;
  58. user: User;
  59. }
  60. // 登出响应接口
  61. export interface LogoutResponse {
  62. message: string;
  63. user_id: string;
  64. }
  65. /**
  66. * 用户API服务类
  67. * 负责与后端用户中心API交互
  68. */
  69. class UserApiService {
  70. private baseUrl = `${API_BASE_URL}/api`;
  71. /**
  72. * 获取请求头(包含鉴权信息)
  73. */
  74. private getHeaders(requireAuth: boolean = false): Record<string, string> {
  75. const headers: Record<string, string> = {
  76. 'Content-Type': 'application/json'
  77. };
  78. if (requireAuth) {
  79. const authHeader = authService.getAuthHeader();
  80. if (authHeader) {
  81. headers['Authorization'] = authHeader;
  82. } else {
  83. throw new Error('未授权:请先登录');
  84. }
  85. }
  86. return headers;
  87. }
  88. /**
  89. * 处理API响应
  90. */
  91. private async handleResponse<T>(response: Response): Promise<T> {
  92. if (!response.ok) {
  93. // 尝试解析错误信息
  94. let errorMessage = `请求失败,请稍后重试`;
  95. try {
  96. const errorData: ErrorResponse = await response.json();
  97. const detail = errorData.detail || errorData.message || '';
  98. // 将后端错误信息转换为用户友好的中文提示
  99. errorMessage = this.translateErrorMessage(detail, response.status);
  100. } catch (e) {
  101. // 解析失败,使用默认错误信息
  102. }
  103. // 401 错误且不是登录接口时,清除登录状态(不调用后端API,因为Token已无效)
  104. if (response.status === 401 && !response.url.includes('/auth/login')) {
  105. authService.logout(false);
  106. }
  107. throw new Error(errorMessage);
  108. }
  109. return response.json();
  110. }
  111. /**
  112. * 将后端错误信息转换为用户友好的中文提示
  113. */
  114. private translateErrorMessage(detail: string, statusCode: number): string {
  115. // ── 精确英文 detail 映射 ──────────────────────────────────────────
  116. const exactMap: Record<string, string> = {
  117. 'Username already exists': '该用户名已被注册,请更换用户名',
  118. 'Email already exists': '该邮箱已被注册,请更换邮箱或使用该邮箱登录',
  119. 'Invalid username or password': '用户名/手机号或者密码错误',
  120. 'User not found': '用户不存在',
  121. 'Authentication required': '请先登录',
  122. '该邮箱未绑定任何账户': '该邮箱未绑定任何账户,请先注册或绑定邮箱',
  123. }
  124. if (exactMap[detail]) return exactMap[detail];
  125. // ── 中文 detail 直接透传(后端已是中文的直接用)──────────────────
  126. if (detail && !/^[A-Za-z]/.test(detail)) return detail;
  127. // ── 按状态码兜底 ─────────────────────────────────────────────────
  128. if (statusCode === 400) return detail || '请求参数有误,请检查后重试';
  129. if (statusCode === 401) return '登录已过期,请重新登录';
  130. if (statusCode === 403) return detail || '没有权限执行此操作';
  131. if (statusCode === 404) return detail || '请求的资源不存在';
  132. if (statusCode === 409) return detail || '数据冲突,请检查您的输入';
  133. if (statusCode === 429) return detail || '操作太频繁,请稍后再试';
  134. if (statusCode >= 500) return '服务器繁忙,请稍后重试';
  135. return detail || '请求失败,请稍后重试';
  136. }
  137. /**
  138. * 用户注册
  139. * POST /api/auth/register
  140. */
  141. async register(registerData: RegisterRequest, keyId: string | null = null): Promise<User> {
  142. const body = {
  143. ...registerData,
  144. // encrypted字段不需要传,后端默认为true
  145. ...(keyId && { key_id: keyId })
  146. };
  147. const response = await fetch(`${this.baseUrl}/auth/register`, {
  148. method: 'POST',
  149. headers: this.getHeaders(false),
  150. body: JSON.stringify(body),
  151. });
  152. return this.handleResponse<User>(response);
  153. }
  154. /**
  155. * 用户登录
  156. * POST /api/auth/login
  157. */
  158. async login(username: string, password: string, keyId: string | null = null): Promise<LoginResponse> {
  159. const body: any = {
  160. username,
  161. password
  162. // encrypted字段不需要传,后端默认为true
  163. };
  164. if (keyId) {
  165. body.key_id = keyId;
  166. }
  167. const response = await fetch(`${this.baseUrl}/auth/login`, {
  168. method: 'POST',
  169. headers: this.getHeaders(false),
  170. body: JSON.stringify(body),
  171. });
  172. return this.handleResponse<LoginResponse>(response);
  173. }
  174. /**
  175. * 获取当前用户信息
  176. * GET /api/users/me
  177. */
  178. async getCurrentUser(): Promise<User> {
  179. const response = await fetch(`${this.baseUrl}/users/me`, {
  180. method: 'GET',
  181. headers: this.getHeaders(true),
  182. });
  183. return this.handleResponse<User>(response);
  184. }
  185. /**
  186. * 更新当前用户信息
  187. * PUT /api/users/me
  188. */
  189. async updateCurrentUser(updateData: UpdateUserRequest): Promise<User> {
  190. const response = await fetch(`${this.baseUrl}/users/me`, {
  191. method: 'PUT',
  192. headers: this.getHeaders(true),
  193. body: JSON.stringify(updateData),
  194. });
  195. return this.handleResponse<User>(response);
  196. }
  197. /**
  198. * 删除当前用户(申请注销)
  199. * DELETE /api/users/me
  200. */
  201. async deleteCurrentUser(): Promise<{ message: string }> {
  202. const response = await fetch(`${this.baseUrl}/users/me`, {
  203. method: 'DELETE',
  204. headers: this.getHeaders(true),
  205. });
  206. return this.handleResponse<{ message: string }>(response);
  207. }
  208. /**
  209. * 验证Token是否有效
  210. * GET /api/auth/verify
  211. */
  212. async verifyToken(): Promise<TokenVerifyResponse> {
  213. const response = await fetch(`${this.baseUrl}/auth/verify`, {
  214. method: 'GET',
  215. headers: this.getHeaders(true),
  216. });
  217. return this.handleResponse<TokenVerifyResponse>(response);
  218. }
  219. /**
  220. * 刷新Token
  221. * POST /api/auth/refresh
  222. */
  223. async refreshToken(): Promise<LoginResponse> {
  224. const response = await fetch(`${this.baseUrl}/auth/refresh`, {
  225. method: 'POST',
  226. headers: this.getHeaders(true),
  227. });
  228. return this.handleResponse<LoginResponse>(response);
  229. }
  230. /**
  231. * 用户登出
  232. * POST /api/auth/logout
  233. */
  234. async logout(): Promise<LogoutResponse> {
  235. const response = await fetch(`${this.baseUrl}/auth/logout`, {
  236. method: 'POST',
  237. headers: this.getHeaders(true),
  238. });
  239. return this.handleResponse<LogoutResponse>(response);
  240. }
  241. /**
  242. * 获取RSA公钥
  243. * GET /api/users/rsa-public-key
  244. */
  245. async getRSAPublicKey(): Promise<{ public_key: string }> {
  246. const response = await fetch(`${this.baseUrl}/users/rsa-public-key`, {
  247. method: 'GET',
  248. headers: this.getHeaders(false),
  249. });
  250. return this.handleResponse<{ public_key: string }>(response);
  251. }
  252. /**
  253. * 提交实名认证(发送加密数据)
  254. * POST /api/users/me/verify
  255. */
  256. async submitVerification(encryptedData: string): Promise<User> {
  257. const response = await fetch(`${this.baseUrl}/users/me/verify`, {
  258. method: 'POST',
  259. headers: this.getHeaders(true),
  260. body: JSON.stringify({ encrypted_data: encryptedData }),
  261. });
  262. return this.handleResponse<User>(response);
  263. }
  264. /**
  265. * 获取公开配置
  266. * GET /api/users/config/public
  267. */
  268. async getPublicConfig(): Promise<{ enable_verification_reminder: boolean }> {
  269. const response = await fetch(`${this.baseUrl}/users/config/public`, {
  270. method: 'GET',
  271. headers: this.getHeaders(false),
  272. });
  273. return this.handleResponse<{ enable_verification_reminder: boolean }>(response);
  274. }
  275. /** 发送短信验证码 */
  276. async sendSmsCode(phone: string, scene: string = 'register'): Promise<void> {
  277. const response = await fetch(`${API_BASE_URL}/api/sms/send-code`, {
  278. method: 'POST',
  279. headers: { 'Content-Type': 'application/json' },
  280. body: JSON.stringify({ phone, scene }),
  281. });
  282. if (!response.ok) {
  283. const err = await response.json().catch(() => ({}));
  284. throw new Error(err.detail || '发送失败,请稍后重试');
  285. }
  286. }
  287. /** 验证验证码(不删除,供两步流程第一步使用) */
  288. async verifySmsCode(phone: string, sms_code: string): Promise<void> {
  289. const response = await fetch(`${API_BASE_URL}/api/sms/verify-code`, {
  290. method: 'POST',
  291. headers: { 'Content-Type': 'application/json' },
  292. body: JSON.stringify({ phone, sms_code }),
  293. });
  294. if (!response.ok) {
  295. const err = await response.json().catch(() => ({}));
  296. throw new Error(err.detail || '验证码错误');
  297. }
  298. }
  299. /** 手机号+验证码登录 */
  300. async loginByPhone(phone: string, sms_code: string): Promise<LoginResponse> {
  301. const response = await fetch(`${this.baseUrl}/auth/login/phone`, {
  302. method: 'POST',
  303. headers: this.getHeaders(false),
  304. body: JSON.stringify({ phone, sms_code }),
  305. });
  306. return this.handleResponse<LoginResponse>(response);
  307. }
  308. /** 手机验证码修改密码 */
  309. async resetPasswordByPhone(phone: string, sms_code: string, new_password: string): Promise<void> {
  310. const response = await fetch(`${this.baseUrl}/auth/reset-password/phone`, {
  311. method: 'POST',
  312. headers: this.getHeaders(false),
  313. body: JSON.stringify({ phone, sms_code, new_password }),
  314. });
  315. if (!response.ok) {
  316. const err = await response.json().catch(() => ({}));
  317. throw new Error(err.detail || '修改失败');
  318. }
  319. }
  320. /** 发送邮箱验证码 */
  321. async sendEmailCode(email: string, scene: string = 'register'): Promise<void> {
  322. const response = await fetch(`${API_BASE_URL}/api/email/send-code`, {
  323. method: 'POST',
  324. headers: { 'Content-Type': 'application/json' },
  325. body: JSON.stringify({ email, scene }),
  326. });
  327. if (!response.ok) {
  328. const err = await response.json().catch(() => ({}));
  329. throw new Error(err.detail || '发送失败,请稍后重试');
  330. }
  331. }
  332. /** 验证邮箱验证码(不删除,供两步流程第一步使用) */
  333. async verifyEmailCode(email: string, email_code: string): Promise<void> {
  334. const response = await fetch(`${API_BASE_URL}/api/email/verify-code`, {
  335. method: 'POST',
  336. headers: { 'Content-Type': 'application/json' },
  337. body: JSON.stringify({ email, email_code }),
  338. });
  339. if (!response.ok) {
  340. const err = await response.json().catch(() => ({}));
  341. throw new Error(err.detail || '验证码错误');
  342. }
  343. }
  344. /** 邮箱验证码修改密码 */
  345. async resetPasswordByEmail(email: string, email_code: string, new_password: string): Promise<void> {
  346. const response = await fetch(`${this.baseUrl}/auth/reset-password/email`, {
  347. method: 'POST',
  348. headers: this.getHeaders(false),
  349. body: JSON.stringify({ email, email_code, new_password }),
  350. });
  351. if (!response.ok) {
  352. const err = await response.json().catch(() => ({}));
  353. throw new Error(err.detail || '修改失败');
  354. }
  355. }
  356. }
  357. // 导出单例
  358. export const userApi = new UserApiService();