fix(frontend): 修复 5 条 P0(生产构建开关、令牌刷新、上传路径、WS 与开发地址)

- web/vite.config.ts:生产判据由 `mode === "prod"`(恒 false)改为 `production || prod`,
  压缩/去 console/剔除 vue-devtools 插件真正生效;同时修正 terser `comments: true`
  (是「保留全部注释」,与注释文字相反)为 `"some"`,保留 @license 等版权声明
- app 刷新令牌:改为发送 JSON 字符串(后端为 Body(str)),与 web 端已知可用实现一致,
  修掉移动端 token 过期必然 422 强制登出
- app 上传:路径补 /common(真实路径 /api/v1/common/file/upload),并改走统一 http 层的
  wd-upload upload-method 钩子,Authorization 由 http 层注入,不再页面自拼 URL
- app 生产 WS:ws://localhost:5180/ws → wss://service.fastapiadmin.com;开发 WS 指向本机
- app 开发接口地址:改为 http://127.0.0.1:8001,与 web 端开发策略一致

复核证据(队长独立执行):web/app type-check 均 exit 0;build:prod 与 build:h5 均 exit 0;
生产包内第一方 console 特征串 9/9 全部 0 命中(剩余 5 处 console 为字符串文案或
第三方 typeof 守卫,非可执行调用);H5 产物内 localhost:5180 命中 0、wss 域名命中 1、
裸 /file/upload 命中 0、/common/file/upload 命中 1。
This commit is contained in:
zhangtao
2026-10-04 04:33:42 +08:00
parent fb05defb82
commit 007864051a
7 changed files with 84 additions and 24 deletions
+5 -4
View File
@@ -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
+3 -1
View File
@@ -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
+11 -8
View File
@@ -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<LoginResult> {
refreshToken(refreshToken: string): Promise<LoginResult> {
// 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
+13 -3
View File
@@ -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<UniApp.UploadFileSuccessCallbackResult> {
return http.Post('/common/file/upload?upload_type=avatar', body, { requestType: 'upload' })
},
/**
+2 -1
View File
@@ -78,7 +78,8 @@ async function handleUnauthorized(method: Method, rawData: any): Promise<unknown
const refreshToken = userStore.getRefreshToken()
if (!refreshToken)
throw new Error('[认证失效]:请重新登录')
const res = await AuthAPI.refreshToken({ refresh_token: refreshToken })
// 后端要 JSON 字符串 body(Body(str)),AuthAPI.refreshToken 内部已 JSON.stringify
const res = await AuthAPI.refreshToken(refreshToken)
if (!res?.access_token)
throw new Error('[认证失效]:登录已过期')
userStore.setAccessToken(res.access_token)
@@ -1,4 +1,5 @@
<script setup lang="ts">
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<UserInfo>()
/** 头像上传:后端通用文件上传(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(() => {
<view class="mx-3 mb-3 flex flex-col items-center gap-2 py-4">
<wd-upload
v-model:file-list="avatarFileList"
:action="uploadAvatarAction"
:header="uploadHeader"
:upload-method="uploadAvatar"
:limit="1"
accept="image"
:max-size="5242880"
+15 -2
View File
@@ -25,7 +25,16 @@ const __APP_INFO__ = {
export default ({ mode }: { mode: string }) => {
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",
},
}
: {},