userApi.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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. registration_date: string;
  14. created_at: string;
  15. updated_at: string;
  16. apikey?: string; // API密钥字段
  17. }
  18. // 注册请求接口
  19. export interface RegisterRequest {
  20. username: string;
  21. password: string;
  22. nickname: string;
  23. email?: string;
  24. phone?: string;
  25. sms_code?: string;
  26. }
  27. // 登录响应接口
  28. export interface LoginResponse {
  29. access_token: string;
  30. token_type: string;
  31. user: User;
  32. }
  33. // 更新用户信息请求接口
  34. export interface UpdateUserRequest {
  35. nickname?: string;
  36. phone?: string;
  37. email?: string;
  38. avatar?: string;
  39. apikey?: string;
  40. }
  41. // 错误响应接口
  42. export interface ErrorResponse {
  43. detail?: string;
  44. message?: string;
  45. }
  46. // Token验证响应接口
  47. export interface TokenVerifyResponse {
  48. valid: boolean;
  49. user: User;
  50. }
  51. // 登出响应接口
  52. export interface LogoutResponse {
  53. message: string;
  54. user_id: string;
  55. }
  56. /**
  57. * 用户API服务类
  58. * 负责与后端用户中心API交互
  59. */
  60. class UserApiService {
  61. private baseUrl = `${API_BASE_URL}/api`;
  62. /**
  63. * 获取请求头(包含鉴权信息)
  64. */
  65. private getHeaders(requireAuth: boolean = false): Record<string, string> {
  66. const headers: Record<string, string> = {
  67. 'Content-Type': 'application/json'
  68. };
  69. if (requireAuth) {
  70. const authHeader = authService.getAuthHeader();
  71. if (authHeader) {
  72. headers['Authorization'] = authHeader;
  73. } else {
  74. throw new Error('未授权:请先登录');
  75. }
  76. }
  77. return headers;
  78. }
  79. /**
  80. * 处理API响应
  81. */
  82. private async handleResponse<T>(response: Response): Promise<T> {
  83. if (!response.ok) {
  84. // 尝试解析错误信息
  85. let errorMessage = `请求失败,请稍后重试`;
  86. try {
  87. const errorData: ErrorResponse = await response.json();
  88. const detail = errorData.detail || errorData.message || '';
  89. // 将后端错误信息转换为用户友好的中文提示
  90. errorMessage = this.translateErrorMessage(detail, response.status);
  91. } catch (e) {
  92. // 解析失败,使用默认错误信息
  93. }
  94. // 401 错误且不是登录接口时,清除登录状态(不调用后端API,因为Token已无效)
  95. if (response.status === 401 && !response.url.includes('/auth/login')) {
  96. authService.logout(false);
  97. }
  98. throw new Error(errorMessage);
  99. }
  100. return response.json();
  101. }
  102. /**
  103. * 将后端错误信息转换为用户友好的中文提示
  104. */
  105. private translateErrorMessage(detail: string, statusCode: number): string {
  106. // 注册相关错误
  107. if (detail === 'Username already exists') {
  108. return '该用户名已被注册,请更换用户名';
  109. }
  110. if (detail === 'Email already exists') {
  111. return '该邮箱已被注册,请更换邮箱或使用该邮箱登录';
  112. }
  113. // 登录相关错误
  114. if (detail === 'Invalid username or password') {
  115. return '用户名/手机号或者密码错误';
  116. }
  117. // 权限相关错误
  118. if (statusCode === 401) {
  119. return '登录已过期,请重新登录';
  120. }
  121. if (statusCode === 403) {
  122. return '没有权限执行此操作';
  123. }
  124. if (statusCode === 404) {
  125. return '请求的资源不存在';
  126. }
  127. if (statusCode === 409) {
  128. return '数据冲突,请检查您的输入';
  129. }
  130. // 服务器错误
  131. if (statusCode >= 500) {
  132. return '服务器繁忙,请稍后重试';
  133. }
  134. // 如果有detail且不是英文技术信息,直接返回
  135. if (detail && !/^[A-Z]/.test(detail)) {
  136. return detail;
  137. }
  138. return '请求失败,请稍后重试';
  139. }
  140. /**
  141. * 用户注册
  142. * POST /api/auth/register
  143. */
  144. async register(registerData: RegisterRequest, keyId: string | null = null): Promise<User> {
  145. const body = {
  146. ...registerData,
  147. // encrypted字段不需要传,后端默认为true
  148. ...(keyId && { key_id: keyId })
  149. };
  150. const response = await fetch(`${this.baseUrl}/auth/register`, {
  151. method: 'POST',
  152. headers: this.getHeaders(false),
  153. body: JSON.stringify(body),
  154. });
  155. return this.handleResponse<User>(response);
  156. }
  157. /**
  158. * 用户登录
  159. * POST /api/auth/login
  160. */
  161. async login(username: string, password: string, keyId: string | null = null): Promise<LoginResponse> {
  162. const body: any = {
  163. username,
  164. password
  165. // encrypted字段不需要传,后端默认为true
  166. };
  167. if (keyId) {
  168. body.key_id = keyId;
  169. }
  170. const response = await fetch(`${this.baseUrl}/auth/login`, {
  171. method: 'POST',
  172. headers: this.getHeaders(false),
  173. body: JSON.stringify(body),
  174. });
  175. return this.handleResponse<LoginResponse>(response);
  176. }
  177. /**
  178. * 获取当前用户信息
  179. * GET /api/users/me
  180. */
  181. async getCurrentUser(): Promise<User> {
  182. const response = await fetch(`${this.baseUrl}/users/me`, {
  183. method: 'GET',
  184. headers: this.getHeaders(true),
  185. });
  186. return this.handleResponse<User>(response);
  187. }
  188. /**
  189. * 更新当前用户信息
  190. * PUT /api/users/me
  191. */
  192. async updateCurrentUser(updateData: UpdateUserRequest): Promise<User> {
  193. const response = await fetch(`${this.baseUrl}/users/me`, {
  194. method: 'PUT',
  195. headers: this.getHeaders(true),
  196. body: JSON.stringify(updateData),
  197. });
  198. return this.handleResponse<User>(response);
  199. }
  200. /**
  201. * 删除当前用户(申请注销)
  202. * DELETE /api/users/me
  203. */
  204. async deleteCurrentUser(): Promise<{ message: string }> {
  205. const response = await fetch(`${this.baseUrl}/users/me`, {
  206. method: 'DELETE',
  207. headers: this.getHeaders(true),
  208. });
  209. return this.handleResponse<{ message: string }>(response);
  210. }
  211. /**
  212. * 验证Token是否有效
  213. * GET /api/auth/verify
  214. */
  215. async verifyToken(): Promise<TokenVerifyResponse> {
  216. const response = await fetch(`${this.baseUrl}/auth/verify`, {
  217. method: 'GET',
  218. headers: this.getHeaders(true),
  219. });
  220. return this.handleResponse<TokenVerifyResponse>(response);
  221. }
  222. /**
  223. * 刷新Token
  224. * POST /api/auth/refresh
  225. */
  226. async refreshToken(): Promise<LoginResponse> {
  227. const response = await fetch(`${this.baseUrl}/auth/refresh`, {
  228. method: 'POST',
  229. headers: this.getHeaders(true),
  230. });
  231. return this.handleResponse<LoginResponse>(response);
  232. }
  233. /**
  234. * 用户登出
  235. * POST /api/auth/logout
  236. */
  237. async logout(): Promise<LogoutResponse> {
  238. const response = await fetch(`${this.baseUrl}/auth/logout`, {
  239. method: 'POST',
  240. headers: this.getHeaders(true),
  241. });
  242. return this.handleResponse<LogoutResponse>(response);
  243. }
  244. /**
  245. * 验证验证码(不删除,供两步流程使用)
  246. */
  247. async verifySmsCode(phone: string, sms_code: string): Promise<void> {
  248. const response = await fetch(`${API_BASE_URL}/api/sms/verify-code`, {
  249. method: 'POST',
  250. headers: { 'Content-Type': 'application/json' },
  251. body: JSON.stringify({ phone, sms_code }),
  252. });
  253. if (!response.ok) {
  254. const err = await response.json().catch(() => ({}));
  255. throw new Error(err.detail || '验证码错误');
  256. }
  257. }
  258. /**
  259. * 发送短信验证码
  260. */
  261. async sendSmsCode(phone: string, scene: string = 'register'): Promise<void> {
  262. const response = await fetch(`${API_BASE_URL}/api/sms/send-code`, {
  263. method: 'POST',
  264. headers: { 'Content-Type': 'application/json' },
  265. body: JSON.stringify({ phone, scene }),
  266. });
  267. if (!response.ok) {
  268. const err = await response.json().catch(() => ({}));
  269. throw new Error(err.detail || '发送失败,请稍后重试');
  270. }
  271. }
  272. /**
  273. * 手机号+验证码登录
  274. */
  275. async loginByPhone(phone: string, sms_code: string): Promise<LoginResponse> {
  276. const response = await fetch(`${this.baseUrl}/auth/login/phone`, {
  277. method: 'POST',
  278. headers: this.getHeaders(false),
  279. body: JSON.stringify({ phone, sms_code }),
  280. });
  281. return this.handleResponse<LoginResponse>(response);
  282. }
  283. /**
  284. * 手机验证码修改密码
  285. */
  286. async resetPasswordByPhone(phone: string, sms_code: string, new_password: string): Promise<void> {
  287. const response = await fetch(`${this.baseUrl}/auth/reset-password/phone`, {
  288. method: 'POST',
  289. headers: this.getHeaders(false),
  290. body: JSON.stringify({ phone, sms_code, new_password }),
  291. });
  292. if (!response.ok) {
  293. const err = await response.json().catch(() => ({}));
  294. throw new Error(err.detail || '修改失败');
  295. }
  296. }
  297. }
  298. // 导出单例
  299. export const userApi = new UserApiService();