diff --git a/frontend/app/.env.development b/frontend/app/.env.development index 983a0dd0..19866288 100644 --- a/frontend/app/.env.development +++ b/frontend/app/.env.development @@ -2,8 +2,9 @@ # 在开发模式下使用 (npm run dev) # API 基础 URL - 开发环境 -# VITE_API_BASE_URL=http://localhost:8001 -VITE_API_BASE_URL=https://service.fastapiadmin.com +# 本机后端;与 web 端 frontend/web/.env.development 的 VITE_API_BASE_URL 策略保持一致。 +# 需要联调线上环境时,改为 https://service.fastapiadmin.com(注意会直接读写生产数据)。 +VITE_API_BASE_URL=http://127.0.0.1:8001 # 环境名称 VITE_ENV_NAME=development @@ -21,5 +22,5 @@ VITE_APP_PORT=5180 VITE_TIMEOUT=10000 # WebSocket 服务器的 URL,不配置默认关闭 WebSocket -# VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws -VITE_APP_WS_ENDPOINT = wss://service.fastapiadmin.com +# 开发环境同样指向本机后端,避免 dev 构建连到生产 WS(只填基址,路径由代码拼接) +VITE_APP_WS_ENDPOINT=ws://127.0.0.1:8001 diff --git a/frontend/app/.env.production b/frontend/app/.env.production index 830efda9..d52b4f6b 100644 --- a/frontend/app/.env.production +++ b/frontend/app/.env.production @@ -20,4 +20,6 @@ VITE_APP_PORT=5180 VITE_TIMEOUT=10000 # WebSocket 服务器的 URL,不配置默认关闭 WebSocket -VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws +# 只填基址,业务路径由代码拼接:{VITE_APP_WS_ENDPOINT}{VITE_APP_BASE_API}/ai/chat/ws +# 站点为 HTTPS,必须使用 wss,否则会被浏览器/小程序平台拦截 +VITE_APP_WS_ENDPOINT=wss://service.fastapiadmin.com diff --git a/frontend/app/src/api/module_system/auth.ts b/frontend/app/src/api/module_system/auth.ts index 85cf5a0a..1ae3ac15 100644 --- a/frontend/app/src/api/module_system/auth.ts +++ b/frontend/app/src/api/module_system/auth.ts @@ -28,13 +28,21 @@ const AuthAPI = { /** * 刷新令牌 - * @param body 刷新令牌请求体 + * + * 后端 `POST /system/auth/token/refresh` 的入参是 `Annotated[str, Body(...)]` + * (`backend/app/modules/system/auth/controller.py:57`),即请求体必须是一个 + * **JSON 字符串**(形如 `"eyJ..."`),而不是 `{ "refresh_token": "..." }` 对象, + * 否则 FastAPI 解析失败返回 422。因此这里与 `logout` 一致,先 + * `JSON.stringify` 让请求体成为合法 JSON 字符串(uni.request 对字符串原样发送)。 + * web 端 `frontend/web/src/api/module_system/auth.ts` 同名方法即直接发字符串。 + * + * @param refreshToken 刷新令牌原文 * @returns 新的访问令牌 */ - refreshToken(body: RefreshToekenBody): Promise { + refreshToken(refreshToken: string): Promise { // authRole: 'refreshToken':让 http 层 401 处理器识别刷新请求,自身 401 时不触发刷新逻辑(避免死循环) // silent:刷新失败由 http 层统一跳转登录,无需全局 toast - return http.Post(`${AUTH_BASE_URL}/token/refresh`, body, { + return http.Post(`${AUTH_BASE_URL}/token/refresh`, JSON.stringify(refreshToken), { meta: { ignoreAuth: true, silent: true, authRole: 'refreshToken' }, }) }, @@ -131,11 +139,6 @@ export interface LoginFormData { login_type?: string } -/** 刷新令牌请求体 */ -export interface RefreshToekenBody { - refresh_token: string -} - /** JWT 响应 */ export interface LoginResult { access_token: string diff --git a/frontend/app/src/api/module_system/user.ts b/frontend/app/src/api/module_system/user.ts index 19743c1e..da4afc73 100644 --- a/frontend/app/src/api/module_system/user.ts +++ b/frontend/app/src/api/module_system/user.ts @@ -19,13 +19,23 @@ const UserAPI = { /** * 当前用户头像上传 * + * 路径必须是 `/common/file/upload`:后端 `FileRouter` 自身前缀是 `/file`,但挂在 + * `/common` 域下(`backend/app/api/v1/routers.py:63`),配合 `VITE_APP_BASE_API=/api/v1` + * 得到 `/api/v1/common/file/upload`;`upload_type` 是 Query 参数、文件字段名约定为 `file` + * (`backend/app/modules/common/file/controller.py:18-31`)。 + * 旧实现写成 `/file/upload`,后端无此路由 → 404。 + * + * 走统一 http 层(`requestType: 'upload'`):请求头由 http 层 `beforeRequest` 统一注入 + * `Authorization`(该接口要求 `module_common:file:upload` 权限),失败复用全局错误处理, + * 调用方无需自己拼 URL 与请求头。 + * * @param body 上传参数 * @param body.filePath 本地临时文件路径(uni.chooseImage 选择结果) * @param body.name 上传字段名,后端约定为 file - * @returns uni.uploadFile 成功回调(statusCode + data 响应体字符串,需调用方解析) + * @returns uni.uploadFile 成功回调结果(statusCode + data 响应体字符串,需调用方解析) */ - uploadCurrentUserAvatar(body: { filePath: string, name?: string }): Promise<{ statusCode: number, data: string }> { - return http.Post('/file/upload?upload_type=avatar', body, { requestType: 'upload' }) + uploadCurrentUserAvatar(body: { filePath: string, name?: string }): Promise { + return http.Post('/common/file/upload?upload_type=avatar', body, { requestType: 'upload' }) }, /** diff --git a/frontend/app/src/http/adapters/alova.ts b/frontend/app/src/http/adapters/alova.ts index 506904b1..ae071ddd 100644 --- a/frontend/app/src/http/adapters/alova.ts +++ b/frontend/app/src/http/adapters/alova.ts @@ -78,7 +78,8 @@ async function handleUnauthorized(method: Method, rawData: any): Promise +import type { UploadMethod } from '@wot-ui/ui/components/wd-upload/types' import type { UserInfo, UserProfileForm } from '@/api/module_system/user' import { onLoad } from '@dcloudio/uni-app' import { computed, reactive, ref } from 'vue' @@ -22,9 +23,39 @@ const loading = ref(false) const saving = ref(false) const userProfile = ref() -/** 头像上传:后端通用文件上传(upload_type=avatar),字段名 file */ -const uploadAvatarAction = `${import.meta.env.VITE_API_BASE_URL || ''}${import.meta.env.VITE_APP_BASE_API || ''}/file/upload?upload_type=avatar` -const uploadHeader = { Authorization: `Bearer ${userStore.getAccessToken() || ''}` } +/** + * 头像上传:交给统一 http 层处理(alova 适配器 `requestType: 'upload'` → uni.uploadFile)。 + * + * 之前由页面自己拼 `VITE_API_BASE_URL + VITE_APP_BASE_API + /file/upload` 并手工设置 + * `Authorization`,有两个问题: + * 1. 路径缺 `/common` 前缀,后端真实路径是 `/api/v1/common/file/upload`(404); + * 2. 绕过 http 层,拿不到统一的令牌注入与错误处理。 + * 现在通过 `wd-upload` 的 `upload-method` 钩子走 `UserAPI.uploadCurrentUserAvatar`, + * 请求头由 http 层 `beforeRequest` 注入。 + */ +const uploadAvatar: UploadMethod = async (uploadFile, formData, options) => { + try { + const res = await UserAPI.uploadCurrentUserAvatar({ + filePath: uploadFile.url, + name: options.name || 'file', + }) + // 与 wd-upload 默认上传保持一致:statusCode 命中 successStatus(默认 200)才算成功 + const successStatus = options.statusCode ?? 200 + const isSuccess = Array.isArray(successStatus) + ? successStatus.includes(res.statusCode) + : res.statusCode === successStatus + + if (isSuccess) { + options.onSuccess(res, uploadFile, formData) + } + else { + options.onError({ ...res, errMsg: res.errMsg || `HTTP ${res.statusCode}` }, uploadFile, formData) + } + } + catch (error) { + options.onError({ errMsg: (error as Error)?.message || 'upload failed' } as UniApp.GeneralCallbackResult, uploadFile, formData) + } +} const avatarFileList = ref<{ url: string }[]>([]) const genderText = computed(() => { @@ -145,8 +176,7 @@ onLoad(() => { { const root = process.cwd(); const env = loadEnv(mode, root); - const isProduction = mode === "prod"; + /** + * 生产分支判定。 + * + * `vite build`(`package.json` 的 `build`)默认 mode 为 `"production"`, + * `build:prod` 显式传 `--mode production`;仓库中不存在 `--mode prod` 的脚本。 + * 历史写法 `mode === "prod"` 与任何实际脚本都不匹配,恒为 false,导致 + * minify / terserOptions.drop_console / gzip-brotli 预压缩 / 剔除 vue-devtools 全部失效。 + * 这里同时接受 `"production"` 与 `"prod"`,兼容两种模式名。 + */ + const isProduction = mode === "production" || mode === "prod"; return defineConfig({ define: { @@ -73,7 +82,11 @@ export default ({ mode }: { mode: string }) => { pure_funcs: ["console.log", "console.info"], // 移除指定的函数调用 }, format: { - comments: true, // 删除注释 + // `comments: true` 是「保留全部注释」,与这里想表达的「删除注释」相反: + // 它会把第三方与自身源码里的 JSDoc(含示例代码中的 console.log(...))原样带进产物。 + // 用 terser 默认的 "some":删除普通注释,仅保留 @license/@preserve/! 开头的 + // 版权注释(MIT 等许可证要求保留版权声明,不能一律删干净)。 + comments: "some", }, } : {},