修改代码,部署测试
This commit is contained in:
parent
e7819fc1c2
commit
a79b24b463
BIN
backend/competition-management-service-test-1.0.0.tgz
Normal file
BIN
backend/competition-management-service-test-1.0.0.tgz
Normal file
Binary file not shown.
123
backend/ecosystem.config.js
Normal file
123
backend/ecosystem.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
143
backend/package-lock.json
generated
143
backend/package-lock.json
generated
@ -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": {
|
||||
|
||||
@ -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
219
backend/scripts/compress.sh
Normal 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
1573
backend/sql/init_data.sql
Normal file
File diff suppressed because one or more lines are too long
@ -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,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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)]),
|
||||
);
|
||||
|
||||
|
||||
@ -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)]),
|
||||
);
|
||||
|
||||
|
||||
@ -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
250
docs/backend-deployment.md
Normal 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;`
|
||||
|
||||
### 方式2:DBeaver 数据传输
|
||||
|
||||
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
402
docs/frontend-deployment.md
Normal 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 语句。
|
||||
2
frontend/.env.development
Normal file
2
frontend/.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
# 开发环境
|
||||
VITE_API_BASE_URL=/api
|
||||
5
frontend/.env.production
Normal file
5
frontend/.env.production
Normal 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
3
frontend/.env.test
Normal file
@ -0,0 +1,3 @@
|
||||
# 测试环境
|
||||
VITE_BASE_URL=/web-test/
|
||||
VITE_API_BASE_URL=/api-test
|
||||
53
frontend/cmp-3d.conf
Normal file
53
frontend/cmp-3d.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
frontend/competition-web-test-v1.0.0.tgz
Normal file
BIN
frontend/competition-web-test-v1.0.0.tgz
Normal file
Binary file not shown.
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
116
frontend/scripts/compress.cjs
Normal file
116
frontend/scripts/compress.cjs
Normal 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()
|
||||
@ -1,5 +1,5 @@
|
||||
import request from "@/utils/request";
|
||||
import type { PaginationParams, PaginationResponse } from "@/types/api";
|
||||
import type { PaginationParams } from "@/types/api";
|
||||
|
||||
// ==================== AI 3D 任务相关类型 ====================
|
||||
|
||||
|
||||
@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@ -209,7 +209,6 @@ export interface ContestRegistration {
|
||||
};
|
||||
};
|
||||
};
|
||||
registrant?: number;
|
||||
teachers?: Array<{
|
||||
id: number;
|
||||
userId: number;
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -249,7 +249,7 @@ const baseRoutes: RouteRecordRaw[] = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: baseRoutes,
|
||||
})
|
||||
|
||||
|
||||
@ -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 || "/");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
|
||||
1
frontend/tsconfig.node.tsbuildinfo
Normal file
1
frontend/tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1
frontend/tsconfig.tsbuildinfo
Normal file
1
frontend/tsconfig.tsbuildinfo
Normal 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"}
|
||||
@ -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
629
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user