mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-14 21:07:40 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' })
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
: {},
|
||||
|
||||
Reference in New Issue
Block a user