修改代码,部署测试

This commit is contained in:
zhangxiaohua 2026-01-18 17:58:38 +08:00
parent e7819fc1c2
commit a79b24b463
31 changed files with 3455 additions and 196 deletions

Binary file not shown.

123
backend/ecosystem.config.js Normal file
View File

@ -0,0 +1,123 @@
/**
* PM2 进程管理器配置文件
*
* 环境区分说明
* 1. 通过 --env 参数指定环境pm2 start ecosystem.config.js --env <环境名>
* 2. 环境配置会自动合并基础配置(env) + 环境特定配置(env_<环境名>)
* 3. 测试环境: --env test (端口 3001, 2个实例)
* 4. 生产环境: --env production (端口 3000, 最大实例数)
*/
const baseAppConfig = {
script: './dist/src/main.js',
// 日志文件路径
error_file: './logs/pm2-error.log',
out_file: './logs/pm2-out.log',
log_file: './logs/pm2-combined.log',
// 日志日期格式
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
// 合并日志(所有实例的日志合并到一个文件)
merge_logs: true,
// 自动重启配置
autorestart: true,
// 监听文件变化(生产环境建议关闭)
watch: false,
// 忽略监听的文件/目录
ignore_watch: ['node_modules', 'logs', 'dist', '.git', '*.log'],
// 最大内存限制(超过后自动重启)
max_memory_restart: '1G',
// 最小正常运行时间(秒),小于此时间重启不计入重启次数
min_uptime: '10s',
// 最大重启次数(在 min_uptime 时间内)
max_restarts: 10,
// 重启延迟(毫秒)
restart_delay: 4000,
// 等待就绪信号的时间(毫秒)
wait_ready: true,
listen_timeout: 10000,
// 优雅关闭超时时间(毫秒)
kill_timeout: 5000,
// 应用启动后的等待时间(毫秒)
shutdown_with_message: true,
// 源代码映射支持
source_map_support: true,
// 实例间负载均衡策略
instance_var: 'INSTANCE_ID',
};
module.exports = {
apps: [
{
...baseAppConfig,
// 生产环境应用
name: 'competition-api',
instances: 2,
exec_mode: 'cluster',
env_file: '.env.production',
env: {
NODE_ENV: 'production',
PORT: 3234,
},
},
{
...baseAppConfig,
// 测试环境应用
name: 'competition-api-test',
instances: 2,
exec_mode: 'cluster',
env_file: '.env.test',
env: {
NODE_ENV: 'test',
PORT: 3234,
},
},
],
// ============================================
// 部署配置(用于 PM2 自动化部署)
// 使用方式: pm2 deploy ecosystem.config.js <环境名>
// ============================================
deploy: {
// 测试环境部署配置
test: {
user: 'deploy',
host: ['119.29.229.174'],
ref: 'origin/develop',
repo: 'git@github.com:your-username/competition-management-system.git',
path: '/var/www/competition-management-test',
'post-deploy':
'cd backend && pnpm install && pnpm run build && pm2 reload ecosystem.config.js --only competition-api-test',
'pre-setup': 'apt-get update && apt-get install -y git',
},
// 生产环境部署配置
production: {
user: 'deploy',
host: ['your-prod-server-ip'],
ref: 'origin/master',
repo: 'git@github.com:your-username/competition-management-system.git',
path: '/var/www/competition-management',
'post-deploy':
'cd backend && pnpm install && pnpm run build && pm2 reload ecosystem.config.js --only competition-api',
'pre-setup': 'apt-get update && apt-get install -y git',
},
},
};

View File

@ -16,9 +16,9 @@
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.3.3",
"@nestjs/serve-static": "^4.0.0",
"@prisma/client": "^6.19.0",
"adm-zip": "^0.5.16",
"axios": "^1.6.7",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
@ -28,12 +28,13 @@
"passport-local": "^1.0.0",
"reflect-metadata": "^0.2.1",
"rxjs": "^7.8.1",
"uuid": "^13.0.0"
"uuid": "^8.3.2"
},
"devDependencies": {
"@nestjs/cli": "^10.3.2",
"@nestjs/schematics": "^10.1.0",
"@nestjs/testing": "^10.3.3",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
@ -1915,39 +1916,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@nestjs/serve-static": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/serve-static/-/serve-static-4.0.2.tgz",
"integrity": "sha512-cT0vdWN5ar7jDI2NKbhf4LcwJzU4vS5sVpMkVrHuyLcltbrz6JdGi1TfIMMatP2pNiq5Ie/uUdPSFDVaZX/URQ==",
"license": "MIT",
"dependencies": {
"path-to-regexp": "0.2.5"
},
"peerDependencies": {
"@fastify/static": "^6.5.0 || ^7.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0",
"express": "^4.18.1",
"fastify": "^4.7.0"
},
"peerDependenciesMeta": {
"@fastify/static": {
"optional": true
},
"express": {
"optional": true
},
"fastify": {
"optional": true
}
}
},
"node_modules/@nestjs/serve-static/node_modules/path-to-regexp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.2.5.tgz",
"integrity": "sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q==",
"license": "MIT"
},
"node_modules/@nestjs/testing": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.20.tgz",
@ -2227,6 +2195,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/adm-zip": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz",
"integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3262,6 +3240,33 @@
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@ -4861,6 +4866,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@ -5602,6 +5622,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@ -6124,6 +6164,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -8757,6 +8812,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@ -10517,16 +10578,12 @@
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {

View File

@ -12,6 +12,10 @@
"start:dev": "set NODE_ENV=development&&nest start --watch",
"start:debug": "NODE_ENV=development nest start --debug --watch",
"start:prod": "NODE_ENV=production node dist/main",
"start:pm2:test": "pm2 start ecosystem.config.js --env test --only competition-api-test",
"start:pm2:prod": "pm2 start ecosystem.config.js --env production --only competition-api",
"stop:pm2:test": "pm2 stop competition-api-test",
"stop:pm2:prod": "pm2 stop competition-api",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "NODE_ENV=test jest",
"test:watch": "NODE_ENV=test jest --watch",
@ -42,7 +46,9 @@
"init:roles:super": "ts-node scripts/init-roles-permissions.ts --super",
"init:roles": "ts-node scripts/init-roles-permissions.ts",
"init:roles:all": "ts-node scripts/init-roles-permissions.ts --all",
"init:tenant": "ts-node scripts/init-tenant.ts"
"init:tenant": "ts-node scripts/init-tenant.ts",
"compress:tgz:prod:win": "node -p \"require('./package.json').version\" | xargs -I {} bash scripts/compress.sh --env production --version {}",
"compress:tgz:test:win": "node -p \"require('./package.json').version\" | xargs -I {} bash scripts/compress.sh --env test --version {}"
},
"dependencies": {
"@nestjs/common": "^10.3.3",
@ -52,7 +58,6 @@
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.3.3",
"@nestjs/serve-static": "^4.0.0",
"@prisma/client": "^6.19.0",
"adm-zip": "^0.5.16",
"axios": "^1.6.7",
@ -65,19 +70,19 @@
"passport-local": "^1.0.0",
"reflect-metadata": "^0.2.1",
"rxjs": "^7.8.1",
"uuid": "^13.0.0"
"uuid": "^8.3.2"
},
"devDependencies": {
"@nestjs/cli": "^10.3.2",
"@nestjs/schematics": "^10.1.0",
"@nestjs/testing": "^10.3.3",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
"@types/node": "^20.11.5",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.36",
"@types/adm-zip": "^0.5.5",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",

219
backend/scripts/compress.sh Normal file
View File

@ -0,0 +1,219 @@
#!/bin/bash
# 压缩脚本
# 使用方法:
# ./scripts/compress.sh # 使用默认配置(不包含 node_modules
# ./scripts/compress.sh --include-node-modules # 使用默认配置(包含 node_modules
# ./scripts/compress.sh -n # 使用默认配置(包含 node_modules简写
# ./scripts/compress.sh --env production --version 1.0.0 # 指定环境和版本
# ./scripts/compress.sh --env test --version 1.0.0 -n # 组合使用多个参数
# ./scripts/compress.sh src/ package.json # 自定义文件/文件夹列表
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 默认值
ENV=""
VERSION=""
INCLUDE_NODE_MODULES=false
# 解析命令行参数
CUSTOM_ITEMS=()
while [[ $# -gt 0 ]]; do
case $1 in
--include-node-modules|-n)
INCLUDE_NODE_MODULES=true
shift
;;
--env)
if [ -z "$2" ]; then
echo -e "${RED}错误: --env 参数需要一个值${NC}"
exit 1
fi
ENV="$2"
shift 2
;;
--version)
if [ -z "$2" ]; then
echo -e "${RED}错误: --version 参数需要一个值${NC}"
exit 1
fi
VERSION="$2"
shift 2
;;
*)
CUSTOM_ITEMS+=("$1")
shift
;;
esac
done
# 构建输出文件名
OUTPUT_FILE="competition-management-service"
if [ -n "$ENV" ]; then
OUTPUT_FILE="${OUTPUT_FILE}-${ENV}"
fi
if [ -n "$VERSION" ]; then
OUTPUT_FILE="${OUTPUT_FILE}-${VERSION}"
fi
OUTPUT_FILE="${OUTPUT_FILE}.tgz"
# 默认要压缩的文件和文件夹(如果用户没有指定)
DEFAULT_ITEMS=(
"dist"
"package.json"
"tsconfig.json"
"ecosystem.config.js"
"prisma/"
".env"
".env.development"
".env.production"
".env.test"
)
# 如果指定了包含 node_modules则添加到默认列表的开头
if [ "$INCLUDE_NODE_MODULES" = true ]; then
DEFAULT_ITEMS=("node_modules" "${DEFAULT_ITEMS[@]}")
fi
# 排除的文件和文件夹模式
EXCLUDE_PATTERNS=(
"docs/"
"scripts/"
"sql/"
".git"
".DS_Store"
"*.log"
"./logs"
"coverage"
"*.tmp"
"*.temp"
".cache"
".pnpm-store"
)
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# 切换到项目根目录
cd "$PROJECT_ROOT"
echo -e "${GREEN}📦 开始压缩项目...${NC}"
if [ -n "$ENV" ]; then
echo -e "${BLUE}环境: ${ENV}${NC}"
fi
if [ -n "$VERSION" ]; then
echo -e "${BLUE}版本: ${VERSION}${NC}"
fi
echo ""
# 确定要压缩的文件和文件夹
if [ ${#CUSTOM_ITEMS[@]} -eq 0 ]; then
if [ "$INCLUDE_NODE_MODULES" = true ]; then
echo -e "${YELLOW}未指定文件/文件夹,使用默认配置(包含 node_modules${NC}"
else
echo -e "${YELLOW}未指定文件/文件夹,使用默认配置(不包含 node_modules${NC}"
fi
ITEMS_TO_COMPRESS=("${DEFAULT_ITEMS[@]}")
else
echo -e "${YELLOW}使用自定义文件/文件夹列表${NC}"
ITEMS_TO_COMPRESS=("${CUSTOM_ITEMS[@]}")
fi
# 验证文件和文件夹是否存在
echo -e "${BLUE}检查文件/文件夹是否存在...${NC}"
MISSING_ITEMS=()
for item in "${ITEMS_TO_COMPRESS[@]}"; do
if [ ! -e "$item" ]; then
MISSING_ITEMS+=("$item")
echo -e "${RED} ⚠️ 警告: $item 不存在,将被跳过${NC}"
else
echo -e "${GREEN}$item${NC}"
fi
done
# 如果有缺失的文件,询问是否继续
if [ ${#MISSING_ITEMS[@]} -gt 0 ]; then
echo ""
echo -e "${YELLOW}发现 ${#MISSING_ITEMS[@]} 个不存在的文件/文件夹${NC}"
read -p "是否继续压缩? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${RED}已取消压缩${NC}"
exit 1
fi
fi
# 构建 tar 排除选项
EXCLUDE_ARGS=()
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
EXCLUDE_ARGS+=(--exclude="$pattern")
done
# 如果输出文件已存在,询问是否覆盖
if [ -f "$OUTPUT_FILE" ]; then
echo ""
echo -e "${YELLOW}输出文件 $OUTPUT_FILE 已存在${NC}"
read -p "是否覆盖? (y/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo -e "${RED}已取消压缩${NC}"
exit 1
fi
rm -f "$OUTPUT_FILE"
fi
# 执行压缩
echo ""
echo -e "${BLUE}正在压缩...${NC}"
# 使用 tar 压缩
tar -czf "$OUTPUT_FILE" \
"${EXCLUDE_ARGS[@]}" \
"${ITEMS_TO_COMPRESS[@]}" 2>/dev/null || {
echo -e "${RED}压缩失败${NC}"
exit 1
}
# 检查压缩结果
if [ ! -f "$OUTPUT_FILE" ]; then
echo -e "${RED}错误: 压缩文件未生成${NC}"
exit 1
fi
# 显示压缩文件信息
FILE_SIZE=$(du -h "$OUTPUT_FILE" | cut -f1)
echo ""
echo -e "${GREEN}═══════════════════════════════════════${NC}"
echo -e "${GREEN}✅ 压缩完成!${NC}"
echo -e "${GREEN}═══════════════════════════════════════${NC}"
echo ""
echo "📦 输出文件: $OUTPUT_FILE"
echo "📊 文件大小: $FILE_SIZE"
echo "📍 位置: $(pwd)/$OUTPUT_FILE"
if [ -n "$ENV" ]; then
echo "🌍 环境: $ENV"
fi
if [ -n "$VERSION" ]; then
echo "🏷️ 版本: $VERSION"
fi
echo ""
echo "📝 已压缩的内容:"
for item in "${ITEMS_TO_COMPRESS[@]}"; do
if [ -e "$item" ]; then
echo "$item"
fi
done
echo ""
echo "🚫 已排除的内容:"
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
echo "$pattern"
done
echo ""

1573
backend/sql/init_data.sql Normal file

File diff suppressed because one or more lines are too long

View File

@ -28,11 +28,10 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter';
imports: [
ConfigModule.forRoot({
isGlobal: true,
// envFilePath 指定配置文件路径
// 如果需要后备文件,可以取消下面的注释,但要注意 .env 会覆盖 .development.env 的值
// envFilePath 数组中第一个文件优先级最高
envFilePath: [
'.env',
`.env.${process.env.NODE_ENV || 'development'}`, // 优先加载
`.env.${process.env.NODE_ENV || 'development'}`, // 优先加载环境特定配置
'.env', // 通用配置作为后备
],
}),
PrismaModule,

View File

@ -375,7 +375,7 @@ export class ContestsService {
},
},
});
contestIds = [...new Set(teacherRecords.map((r) => r.registration.contestId as number))];
contestIds = Array.from(new Set(teacherRecords.map((r) => r.registration.contestId as number)));
} else {
// 学生/默认:查询报名的赛事
const registrationWhere: any = {

View File

@ -59,7 +59,7 @@ export class ResultsService {
validState: 1,
},
});
const judgeWeights = new Map(
const judgeWeights = new Map<number, number>(
judges.map((j) => [j.judgeId, Number(j.weight || 1)]),
);

View File

@ -608,7 +608,7 @@ export class ReviewsService {
},
});
const judgeWeights = new Map(
const judgeWeights = new Map<number, number>(
judges.map((j) => [j.judgeId, Number(j.weight || 1)]),
);

View File

@ -24,10 +24,8 @@ async function bootstrap() {
}),
);
// 验证环境配置加载
const configService = app.get(ConfigService);
const port = configService.get('PORT') || process.env.PORT || 3001;
const port = configService.get('PORT') || 3001;
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
}

250
docs/backend-deployment.md Normal file
View File

@ -0,0 +1,250 @@
# 后端部署文档
## 一、本地打包
### 1. 环境准备
```bash
cd backend
# 删除 pnpm 的 node_modules避免软链接问题
rm -rf node_modules
# 用 npm 安装依赖
npm install
# 构建项目
pnpm build
```
### 2. 打包命令
```bash
# 测试环境
bash scripts/compress.sh --env test --version 1.0.0 -n
# 生产环境
bash scripts/compress.sh --env production --version 1.0.0 -n
```
生成文件:`competition-management-service-{env}-{version}.tgz`
### 3. 打包注意事项
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 软链接失效 | pnpm 使用软链接管理依赖 | 打包前用 `npm install` 替代 `pnpm install` |
| logs 模块缺失 | 打包脚本排除了 `logs` 目录 | 修改 `compress.sh``"logs"``"./logs"` |
| uuid 版本问题 | uuid v9+ 是 ESM 模块 | 安装 `uuid@8.3.2` |
---
## 二、服务器部署
### 1. 上传并解压
```bash
# 本地上传
scp competition-management-service-test-1.0.0.tgz root@服务器IP:/data/web-servers/
# 服务器解压
cd /data/web-servers
tar -xzf competition-management-service-test-1.0.0.tgz
cd competition-management-service
```
### 2. 配置环境变量
```bash
vim .env
```
修改数据库连接:
```env
DATABASE_URL="mysql://用户名:密码@数据库地址:端口/数据库名"
```
示例(腾讯云数据库):
```env
DATABASE_URL="mysql://root:password@gz-cdb-xxx.sql.tencentcdb.com:20704/db_competition_management"
```
### 3. 生成 Prisma Client
```bash
# 如果遇到 SSL 证书问题
export NODE_TLS_REJECT_UNAUTHORIZED=0
npx prisma generate
```
### 4. 创建数据库表
```bash
npx prisma db push
```
### 5. 启动服务
```bash
# 使用 PM2 启动
pm2 start dist/src/main.js --name competition-api-test
# 保存进程列表
pm2 save
# 设置开机自启
pm2 startup
```
### 6. 验证服务
```bash
# 查看状态
pm2 status
# 查看日志
pm2 logs competition-api-test
# 测试接口
curl -X POST http://localhost:3234/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
```
---
## 三、常见问题
### 1. `prisma generate` 报 SSL 证书错误
```
Error: request to https://binaries.prisma.sh/... failed, reason: unable to get local issuer certificate
```
**解决**
```bash
export NODE_TLS_REJECT_UNAUTHORIZED=0
npx prisma generate
```
### 2. `pm2: command not found`
**原因**`/usr/local/bin` 不在 PATH 中
**解决**
```bash
export PATH=$PATH:/usr/local/bin
echo 'export PATH=$PATH:/usr/local/bin' >> ~/.bashrc
source ~/.bashrc
```
### 3. `MODULE_NOT_FOUND: ./logs/logs.module`
**原因**:打包脚本排除了 `logs` 目录
**解决**:修改 `compress.sh` 第 93 行:
```bash
# 修改前
"logs"
# 修改后
"./logs"
```
### 4. `ERR_REQUIRE_ESM: uuid`
**原因**uuid v9+ 是 ESM 模块,不支持 CommonJS
**解决**
```bash
npm install uuid@8.3.2
```
### 5. 数据库认证失败
```
PrismaClientInitializationError: Authentication failed against database server
```
**解决**:检查 `.env``DATABASE_URL` 的用户名、密码、地址是否正确
### 6. 服务启动后无法访问
**原因**
- 防火墙未开放端口
- 云服务器安全组未配置
**解决**
```bash
# 开放防火墙端口
firewall-cmd --permanent --add-port=3234/tcp
firewall-cmd --reload
```
腾讯云控制台 → 安全组 → 添加入站规则TCP 3234
---
## 四、PM2 常用命令
```bash
pm2 start dist/src/main.js --name app-name # 启动
pm2 stop app-name # 停止
pm2 restart app-name # 重启
pm2 delete app-name # 删除
pm2 status # 查看状态
pm2 logs app-name # 查看日志
pm2 logs app-name --lines 100 # 查看最近100行日志
pm2 save # 保存进程列表
pm2 startup # 设置开机自启
```
---
## 五、数据初始化
### 方式1本地初始化后导出导入
**本地操作**
```bash
cd backend
pnpm init:super-tenant
pnpm init:admin
pnpm init:menus
pnpm init:roles:all
# 导出数据
mysqldump -u root db_competition_management > init_data.sql
```
**导入到云数据库**(使用 DBeaver
1. 连接腾讯云数据库
2. 执行 `SET FOREIGN_KEY_CHECKS = 0;`
3. 执行 SQL 文件
4. 执行 `SET FOREIGN_KEY_CHECKS = 1;`
### 方式2DBeaver 数据传输
1. 连接本地数据库和云数据库
2. 选中本地数据库所有表
3. 右键 → 导出数据 → 数据库表
4. 选择云数据库作为目标
5. 执行传输
---
## 六、目录结构
```
/data/web-servers/competition-management-service/
├── dist/ # 编译后的代码
│ └── src/
│ └── main.js # 入口文件
├── node_modules/ # 依赖
├── prisma/ # Prisma schema
├── .env # 环境配置
├── .env.test # 测试环境配置
├── .env.production # 生产环境配置
├── ecosystem.config.js # PM2 配置
└── package.json
```

402
docs/frontend-deployment.md Normal file
View File

@ -0,0 +1,402 @@
# 前端部署文档
## 一、环境配置
### 1.1 环境文件
项目支持多环境配置:
- `.env.development` - 本地开发环境
- `.env.test` - 测试环境
- `.env.production` - 生产环境
**测试环境配置 (.env.test)**
```env
# 测试环境
VITE_BASE_URL=/web-test/
VITE_API_BASE_URL=/api-test
```
**生产环境配置 (.env.production)**
```env
# 生产环境
VITE_BASE_URL=/web/
VITE_API_BASE_URL=/api
```
### 1.2 Vite 配置
`vite.config.ts` 根据环境设置 base 路径:
```typescript
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import { resolve } from "path"
// 根据环境设置 base 路径
const getBase = (mode: string) => {
switch (mode) {
case "test":
return "/web-test/"
case "production":
return "/web/"
default:
return "/"
}
}
export default defineConfig(({ mode }) => {
return {
base: getBase(mode),
plugins: [vue()],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
server: {
port: 3000,
proxy: {
"/api": {
target: "http://localhost:3234",
changeOrigin: true,
},
},
},
}
})
```
### 1.3 路由配置
`src/router/index.ts` 需要配置 base 路径:
```typescript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: baseRoutes,
})
```
---
## 二、构建打包
### 2.1 构建命令
```bash
# 测试环境构建
pnpm build:test
# 生产环境构建
pnpm build:prod
```
### 2.2 压缩打包
```bash
# 测试环境压缩
pnpm compress:test
# 生产环境压缩
pnpm compress:prod
```
压缩后文件:
- 测试环境:`competition-web-test-v1.0.0.tgz`
- 生产环境:`competition-web-production-v1.0.0.tgz`
---
## 三、服务器部署
### 3.1 服务器信息
| 环境 | Nginx 服务器 | 后端服务器 |
|------|-------------|-----------|
| 测试 | 106.52.220.176 | 119.29.229.174:3234 |
| 生产 | 106.52.220.176 | 待定 |
域名:`cmp-3d.linkseaai.com`
### 3.2 部署步骤
**1. 上传压缩包**
```bash
scp competition-web-test-v1.0.0.tgz root@106.52.220.176:/home/
```
**2. 登录服务器**
```bash
ssh root@106.52.220.176
```
**3. 解压文件**
```bash
# 创建目录(首次部署)
mkdir -p /data/apps/cmp-3d/web-test
mkdir -p /data/apps/cmp-3d/web
# 清空旧文件并解压
rm -rf /data/apps/cmp-3d/web-test/*
cd /home
tar -xzf competition-web-test-v1.0.0.tgz -C /data/apps/cmp-3d/web-test
```
**4. 配置 Nginx**
配置文件路径:`/usr/local/nginx/conf/conf.d/cmp-3d.conf`
```nginx
# ========== 比赛管理系统 - cmp-3d.linkseaai.com ==========
server {
listen 443 ssl;
server_name cmp-3d.linkseaai.com;
include /usr/local/nginx/conf/conf.d/linkseaai.ssl.conf;
root /data/apps/cmp-3d/;
include /usr/local/nginx/conf/conf.d/error.conf;
include /usr/local/nginx/conf/conf.d/static.conf;
# ========== 超时配置 ==========
keepalive_timeout 300s;
send_timeout 180s;
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 300s;
# ========== 测试环境 - 前端 ==========
location /web-test/ {
root /data/apps/cmp-3d/;
index index.html index.htm;
try_files $uri $uri/ /web-test/index.html;
}
# ========== 测试环境 - API 代理 ==========
location /api-test/ {
proxy_redirect off;
proxy_pass http://119.29.229.174:3234/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ========== 生产环境 - 前端 ==========
location /web/ {
root /data/apps/cmp-3d/;
index index.html index.htm;
try_files $uri $uri/ /web/index.html;
}
# ========== 生产环境 - API 代理 ==========
location /api/ {
proxy_redirect off;
proxy_pass http://119.29.229.174:3234/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
**5. 在 nginx.conf 中添加 include**
编辑 `/usr/local/nginx/conf/nginx.conf`,在 `http {}` 块中添加:
```nginx
include ./conf.d/cmp-3d.conf;
```
**6. 重载 Nginx**
```bash
/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload
```
### 3.3 访问地址
| 环境 | URL |
|------|-----|
| 测试 | https://cmp-3d.linkseaai.com/web-test/super/login |
| 生产 | https://cmp-3d.linkseaai.com/web/super/login |
---
## 四、遇到的问题及解决方案
### 4.1 vite.config.js 覆盖问题
**问题描述:**
项目中同时存在 `vite.config.ts``vite.config.js`,导致 TypeScript 配置文件被 JavaScript 文件覆盖,`base` 配置不生效。
**表现:**
打包后的 `index.html` 中资源路径是 `/assets/...` 而不是 `/web-test/assets/...`
**解决方案:**
删除 `vite.config.js``vite.config.d.ts`,只保留 `vite.config.ts`
```bash
rm vite.config.js vite.config.d.ts
```
### 4.2 域名重定向到 linkseaaiglobal.com
**问题描述:**
访问 `https://cmp-3d.linkseaai.com/web-test/` 被重定向到 `https://linkseaaiglobal.com/`
**原因:**
`nginx.conf` 中没有 include `cmp-3d.conf` 配置文件。
**解决方案:**
`/usr/local/nginx/conf/nginx.conf``http {}` 块中添加:
```nginx
include ./conf.d/cmp-3d.conf;
```
### 4.3 租户编码获取错误
**问题描述:**
登录时提示"租户不存在",租户编码被识别为 `web-test` 而不是 `super`
**原因:**
`src/utils/auth.ts` 中的 `getTenantCodeFromUrl()` 函数直接从 URL 第一部分提取租户编码,没有考虑 base 路径。
URL `/web-test/super/login` 被解析为租户编码 `web-test`
**解决方案:**
修改 `getTenantCodeFromUrl()` 函数,去掉 base 路径后再提取租户编码:
```typescript
function getTenantCodeFromUrl(): string | null {
let path = window.location.pathname;
// 去掉 base 路径前缀(如 /web-test/ 或 /web/
const base = import.meta.env.BASE_URL || "/";
if (base !== "/" && path.startsWith(base)) {
path = path.slice(base.length - 1); // 保留开头的 /
}
const match = path.match(/^\/([^/]+)/);
return match ? match[1] : null;
}
```
同时修改 `setToken()``removeToken()` 函数Cookie 路径也需要包含 base 路径。
### 4.4 Vue Router base 路径问题
**问题描述:**
在 URL 中添加租户编码后,回车会自动去掉租户编码部分。
**原因:**
`createWebHistory()` 没有传入 base 路径。
**解决方案:**
修改 `src/router/index.ts`
```typescript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: baseRoutes,
})
```
### 4.5 API 路径 404 错误
**问题描述:**
登录接口返回 `Cannot POST /auth/login`
**原因:**
Nginx 代理配置中,`/api-test/` 被代理到 `http://119.29.229.174:3234/`,但后端路由前缀是 `/api`
请求 `/api-test/auth/login` 被代理到 `http://119.29.229.174:3234/auth/login`,而正确应该是 `/api/auth/login`
**解决方案:**
修改 Nginx 配置,将 `/api-test/` 代理到 `/api/`
```nginx
location /api-test/ {
proxy_pass http://119.29.229.174:3234/api/;
# ...
}
```
### 4.6 3D 建模实验室链接 404
**问题描述:**
点击 3D 建模实验室菜单,打开的 URL 是 `https://cmp-3d.linkseaai.com/school1/workbench/3d-lab`,缺少 `/web-test/` 前缀。
**原因:**
`src/layouts/BasicLayout.vue` 中生成 URL 时使用 `window.location.origin` 直接拼接路径,没有包含 base 路径。
**解决方案:**
修改 BasicLayout.vue 中的 URL 生成逻辑:
```typescript
const base = import.meta.env.BASE_URL || "/"
const basePath = base.endsWith("/") ? base.slice(0, -1) : base
const fullUrl = `${window.location.origin}${basePath}/${tenantCode}/workbench/3d-lab`
window.open(fullUrl, "_blank")
```
---
## 五、常用命令
### Nginx 命令
```bash
# Nginx 路径(编译安装)
/usr/local/nginx/sbin/nginx
# 测试配置
/usr/local/nginx/sbin/nginx -t
# 重载配置
/usr/local/nginx/sbin/nginx -s reload
# 查看完整配置
/usr/local/nginx/sbin/nginx -T
# 查看配置文件
cat /usr/local/nginx/conf/nginx.conf
cat /usr/local/nginx/conf/conf.d/cmp-3d.conf
```
### 部署快捷命令
```bash
# 一键部署测试环境
rm -rf /data/apps/cmp-3d/web-test/* && \
cd /home && \
tar -xzf competition-web-test-v1.0.0.tgz -C /data/apps/cmp-3d/web-test
```
---
## 六、目录结构
```
/data/apps/cmp-3d/
├── web-test/ # 测试环境前端
│ ├── index.html
│ └── assets/
└── web/ # 生产环境前端
├── index.html
└── assets/
```
---
## 七、注意事项
1. **base 路径一致性**:前端 `vite.config.ts`、`.env` 文件、Nginx 配置中的路径必须一致。
2. **API 代理路径**Nginx 代理到后端时,注意后端的路由前缀是 `/api`
3. **Cookie 路径**:登录后的 Token 存储在 Cookie 中,路径需要包含 base 路径。
4. **清除缓存**:部署后如果有问题,先清除浏览器缓存或使用无痕模式测试。
5. **Nginx include**:新增配置文件后,需要在 `nginx.conf` 中添加 include 语句。

View File

@ -0,0 +1,2 @@
# 开发环境
VITE_API_BASE_URL=/api

5
frontend/.env.production Normal file
View File

@ -0,0 +1,5 @@
# 生产环境
VITE_API_BASE_URL=/api
# 如果后端部署在不同域名,可以改成完整地址:
# VITE_API_BASE_URL=https://api.your-domain.com

3
frontend/.env.test Normal file
View File

@ -0,0 +1,3 @@
# 测试环境
VITE_BASE_URL=/web-test/
VITE_API_BASE_URL=/api-test

53
frontend/cmp-3d.conf Normal file
View File

@ -0,0 +1,53 @@
# ========== 比赛管理系统 - cmp-3d.linkseaai.com ==========
server {
listen 443 ssl;
server_name cmp-3d.linkseaai.com;
include /usr/local/nginx/conf/conf.d/linkseaai.ssl.conf;
root /data/apps/cmp-3d/;
include /usr/local/nginx/conf/conf.d/error.conf;
include /usr/local/nginx/conf/conf.d/static.conf;
# ========== 超时配置 ==========
keepalive_timeout 300s;
send_timeout 180s;
proxy_connect_timeout 10s;
proxy_send_timeout 10s;
proxy_read_timeout 300s;
# ========== 测试环境 - 前端 ==========
location /web-test/ {
root /data/apps/cmp-3d/;
index index.html index.htm;
try_files $uri $uri/ /web-test/index.html;
}
# ========== 测试环境 - API 代理 ==========
location /api-test/ {
proxy_redirect off;
proxy_pass http://119.29.229.174:3234/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ========== 生产环境 - 前端 ==========
location /web/ {
root /data/apps/cmp-3d/;
index index.html index.htm;
try_files $uri $uri/ /web/index.html;
}
# ========== 生产环境 - API 代理 ==========
location /api/ {
proxy_redirect off;
proxy_pass http://119.29.229.174:3234/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Binary file not shown.

View File

@ -3,8 +3,12 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"compress:test": "node scripts/compress.cjs test",
"compress:prod": "node scripts/compress.cjs production",
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "vue-tsc -b && vite build",
"build:test": "vite build --mode test",
"build:prod": "vite build --mode production",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
@ -35,6 +39,6 @@
"tailwindcss": "^3.4.1",
"typescript": "^5.4.3",
"vite": "^5.1.6",
"vue-tsc": "^1.8.27"
"vue-tsc": "^3.2.2"
}
}

View File

@ -0,0 +1,116 @@
const { execSync } = require("child_process")
const path = require("path")
const fs = require("fs")
/**
* 压缩前端打包文件
* 压缩包放在根目录下文件名格式: competition-web-{env}-v{version}.tgz
*
* 用法:
* node scripts/compress.cjs test - 测试环境
* node scripts/compress.cjs production - 生产环境
*/
function compressFrontend() {
const rootDir = path.join(__dirname, "..")
const sourceDir = path.join(rootDir, "dist")
const outputDir = rootDir
// 获取环境参数
const env = process.argv[2]
if (!env || !["test", "production"].includes(env)) {
console.error("❌ 错误: 请指定环境参数")
console.error(" 用法: node scripts/compress.cjs <test|production>")
console.error(" 示例: node scripts/compress.cjs test")
console.error(" 示例: node scripts/compress.cjs production")
process.exit(1)
}
// 从 package.json 读取版本号
const packageJsonPath = path.join(rootDir, "package.json")
let version = "1.0.0"
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
version = packageJson.version || "1.0.0"
} catch (error) {
console.warn(`⚠️ 无法读取 package.json 版本号,使用默认版本: ${version}`)
}
// 检查源目录是否存在
if (!fs.existsSync(sourceDir)) {
console.error("❌ 错误: 前端打包文件不存在")
console.error(` 路径: ${sourceDir}`)
console.error(
` 请先运行 pnpm build:${env === "test" ? "test" : ""} 构建前端项目`,
)
process.exit(1)
}
// 删除之前的所有压缩包
console.log("🧹 清理旧的压缩包...\n")
try {
const files = fs.readdirSync(outputDir)
const oldZipFiles = files.filter(
(file) => file.startsWith("competition-web-") && file.endsWith(".tgz"),
)
if (oldZipFiles.length > 0) {
oldZipFiles.forEach((file) => {
const filePath = path.join(outputDir, file)
try {
fs.unlinkSync(filePath)
console.log(` ✅ 已删除: ${file}`)
} catch (error) {
console.warn(` ⚠️ 删除失败: ${file} - ${error.message}`)
}
})
console.log("")
} else {
console.log(" 没有找到旧的压缩包\n")
}
} catch (error) {
console.warn(` ⚠️ 清理旧压缩包时出错: ${error.message}\n`)
}
// 生成文件名: competition-web-{env}-v{version}.tgz
const zipFileName = `competition-web-${env}-v${version}.tgz`
const zipFilePath = path.join(outputDir, zipFileName)
console.log("📦 开始压缩前端打包文件...\n")
console.log(` 环境: ${env}`)
console.log(` 版本: v${version}`)
console.log(` 源目录: ${sourceDir}`)
console.log(` 输出文件: ${zipFilePath}\n`)
try {
// 使用相对路径,避免 Windows tar 路径问题
const tarCommand = `tar -czf "${zipFileName}" -C dist .`
execSync(tarCommand, {
cwd: rootDir,
stdio: "inherit",
shell: true,
env: { ...process.env },
})
// 检查文件是否创建成功
if (fs.existsSync(zipFilePath)) {
const stats = fs.statSync(zipFilePath)
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2)
console.log(`\n✅ 压缩完成!`)
console.log(` 文件: ${zipFileName}`)
console.log(` 大小: ${fileSizeMB} MB`)
console.log(` 路径: ${zipFilePath}`)
} else {
throw new Error("压缩文件未生成")
}
} catch (error) {
console.error("\n❌ 压缩失败:", error.message)
console.error("\n提示:")
console.error(" 1. 确保已安装tar命令 (Windows 10+内置支持)")
console.error(" 2. 确保有足够的磁盘空间")
console.error(" 3. 确保输出目录有写入权限")
process.exit(1)
}
}
compressFrontend()

View File

@ -1,5 +1,5 @@
import request from "@/utils/request";
import type { PaginationParams, PaginationResponse } from "@/types/api";
import type { PaginationParams } from "@/types/api";
// ==================== AI 3D 任务相关类型 ====================

View File

@ -4,7 +4,7 @@ import type { LoginForm, LoginResponse, User } from "@/types/auth";
export const authApi = {
login: async (data: LoginForm): Promise<LoginResponse> => {
const response = await request.post("/auth/login", data);
return response as LoginResponse;
return response as unknown as LoginResponse;
},
logout: async (): Promise<void> => {
@ -13,11 +13,11 @@ export const authApi = {
getUserInfo: async (): Promise<User> => {
const response = await request.get("/auth/user-info");
return response as User;
return response as unknown as User;
},
refreshToken: async (): Promise<{ token: string }> => {
const response = await request.post("/auth/refresh-token");
return response as { token: string };
return response as unknown as { token: string };
},
};

View File

@ -209,7 +209,6 @@ export interface ContestRegistration {
};
};
};
registrant?: number;
teachers?: Array<{
id: number;
userId: number;

View File

@ -185,7 +185,9 @@ const handleMenuClick = ({ key }: { key: string }) => {
if (is3DLab || is3DLabByPath) {
// 3D hideSidebar
console.log("检测到3D建模实验室打开新窗口")
const fullUrl = `${window.location.origin}/${tenantCode}/workbench/3d-lab`
const base = import.meta.env.BASE_URL || "/"
const basePath = base.endsWith("/") ? base.slice(0, -1) : base
const fullUrl = `${window.location.origin}${basePath}/${tenantCode}/workbench/3d-lab`
window.open(fullUrl, "_blank")
return
}

View File

@ -249,7 +249,7 @@ const baseRoutes: RouteRecordRaw[] = [
]
const router = createRouter({
history: createWebHistory(),
history: createWebHistory(import.meta.env.BASE_URL),
routes: baseRoutes,
})

View File

@ -4,7 +4,14 @@ const TOKEN_KEY = "token";
* URL
*/
function getTenantCodeFromUrl(): string | null {
const path = window.location.pathname;
let path = window.location.pathname;
// 去掉 base 路径前缀(如 /web-test/ 或 /web/
const base = import.meta.env.BASE_URL || "/";
if (base !== "/" && path.startsWith(base)) {
path = path.slice(base.length - 1); // 保留开头的 /
}
const match = path.match(/^\/([^/]+)/);
return match ? match[1] : null;
}
@ -74,7 +81,10 @@ export const setToken = (token: string, tenantCode?: string): void => {
// 如果提供了租户编码,使用租户编码作为 path
// 否则从 URL 获取或使用默认路径
const urlTenantCode = tenantCode || getTenantCodeFromUrl();
const path = urlTenantCode ? `/${urlTenantCode}` : "/";
const base = import.meta.env.BASE_URL || "/";
// 组合 base 路径和租户编码
const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
const path = urlTenantCode ? `${basePath}/${urlTenantCode}` : basePath || "/";
// 设置 cookie过期时间设置为 7 天
const expires = 7 * 24 * 60 * 60; // 7 天(秒)
@ -82,17 +92,20 @@ export const setToken = (token: string, tenantCode?: string): void => {
};
export const removeToken = (tenantCode?: string): void => {
const base = import.meta.env.BASE_URL || "/";
const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
// 如果提供了租户编码,删除该路径下的 cookie
if (tenantCode) {
removeCookie(TOKEN_KEY, `/${tenantCode}`);
removeCookie(TOKEN_KEY, `${basePath}/${tenantCode}`);
} else {
// 否则从 URL 获取租户编码并删除对应路径下的 cookie
const urlTenantCode = getTenantCodeFromUrl();
if (urlTenantCode) {
removeCookie(TOKEN_KEY, `/${urlTenantCode}`);
removeCookie(TOKEN_KEY, `${basePath}/${urlTenantCode}`);
}
// 也删除根路径下的 cookie如果有
removeCookie(TOKEN_KEY, "/");
removeCookie(TOKEN_KEY, basePath || "/");
}
};

View File

@ -9,7 +9,7 @@ import { useAuthStore } from "@/stores/auth"
import router from "@/router"
const service: AxiosInstance = axios.create({
baseURL: "/api",
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
timeout: 30000,
})

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/ai-3d.ts","./src/api/auth.ts","./src/api/classes.ts","./src/api/config.ts","./src/api/contests.ts","./src/api/departments.ts","./src/api/dict.ts","./src/api/grades.ts","./src/api/homework.ts","./src/api/judges-management.ts","./src/api/logs.ts","./src/api/menus.ts","./src/api/permissions.ts","./src/api/roles.ts","./src/api/schools.ts","./src/api/students.ts","./src/api/teachers.ts","./src/api/tenants.ts","./src/api/upload.ts","./src/api/users.ts","./src/composables/uselistrequest.ts","./src/composables/usesimplelistrequest.ts","./src/directives/permission.ts","./src/router/index.ts","./src/stores/auth.ts","./src/types/api.ts","./src/types/auth.ts","./src/types/router.ts","./src/utils/auth.ts","./src/utils/avatar.ts","./src/utils/menu.ts","./src/utils/request.ts","./src/app.vue","./src/components/modelviewer.vue","./src/components/richtexteditor.vue","./src/layouts/basiclayout.vue","./src/layouts/emptylayout.vue","./src/views/activities/comments.vue","./src/views/activities/guidance.vue","./src/views/activities/review.vue","./src/views/activities/reviewdetail.vue","./src/views/activities/components/reviewworkmodal.vue","./src/views/auth/login.vue","./src/views/contests/activities.vue","./src/views/contests/create.vue","./src/views/contests/detail.vue","./src/views/contests/guidance.vue","./src/views/contests/index.vue","./src/views/contests/registerindividual.vue","./src/views/contests/registerteam.vue","./src/views/contests/components/addjudgedrawer.vue","./src/views/contests/components/addparticipantdrawer.vue","./src/views/contests/components/addteacherdrawer.vue","./src/views/contests/components/submitworkdrawer.vue","./src/views/contests/components/viewworkdrawer.vue","./src/views/contests/components/workdetailmodal.vue","./src/views/contests/judges/index.vue","./src/views/contests/notices/index.vue","./src/views/contests/registrations/index.vue","./src/views/contests/registrations/records.vue","./src/views/contests/results/detail.vue","./src/views/contests/results/index.vue","./src/views/contests/reviews/index.vue","./src/views/contests/reviews/progress.vue","./src/views/contests/reviews/progressdetail.vue","./src/views/contests/reviews/tasks.vue","./src/views/contests/works/index.vue","./src/views/contests/works/worksdetail.vue","./src/views/error/403.vue","./src/views/error/404.vue","./src/views/homework/index.vue","./src/views/homework/reviewrules.vue","./src/views/homework/studentdetail.vue","./src/views/homework/studentlist.vue","./src/views/homework/submissions.vue","./src/views/model/modelviewer.vue","./src/views/school/classes/index.vue","./src/views/school/departments/index.vue","./src/views/school/grades/index.vue","./src/views/school/schools/index.vue","./src/views/school/students/index.vue","./src/views/school/teachers/index.vue","./src/views/system/config/index.vue","./src/views/system/dict/index.vue","./src/views/system/logs/index.vue","./src/views/system/menus/index.vue","./src/views/system/permissions/index.vue","./src/views/system/roles/index.vue","./src/views/system/tenants/index.vue","./src/views/system/users/index.vue","./src/views/workbench/index.vue","./src/views/workbench/ai-3d/generate.vue","./src/views/workbench/ai-3d/history.vue","./src/views/workbench/ai-3d/index.vue"],"errors":true,"version":"5.9.3"}

View File

@ -1,23 +1,38 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import { resolve } from "path"
// 根据环境设置 base 路径
const getBase = (mode: string) => {
switch (mode) {
case "test":
return "/web-test/"
case "production":
return "/web/"
default:
return "/"
}
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
server: {
port: 3000,
proxy: {
"/api": {
target: "http://localhost:3001",
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
export default defineConfig(({ mode }) => {
return {
base: getBase(mode),
plugins: [vue()],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
},
});
server: {
port: 3000,
proxy: {
"/api": {
target: "http://localhost:3234",
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
}
})

629
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff