Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80a88dfd58 | |||
| 3a9b552681 | |||
| c48e8716ae | |||
| c4a8232226 | |||
| cf708dde75 | |||
| d4ab6db42e | |||
| c7ee52a1a1 | |||
| eab3f6c28a | |||
| 2518d60951 | |||
| 05ee0929e2 | |||
| 3b1bd94dce | |||
| b2b43b8e12 | |||
| 18ea388cc7 | |||
| 0ec92b10a1 | |||
| f0cc37681c |
21
.env
Normal file
21
.env
Normal file
@@ -0,0 +1,21 @@
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=33306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=123456
|
||||
DB_NAME=employee_performance
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=dev_jwt_secret_please_change_in_production
|
||||
|
||||
# FastGPT API 配置
|
||||
FASTGPT_API_KEY=fastgpt-oEipxYa5BfVaeGDj74iAXi8YSkWyye07lTNYuj7yydsEKAc4Hp2Z2RDbxsxc4TuZ
|
||||
FASTGPT_API_URL=https://cloud.fastgpt.cn/api/v1/chat/completions
|
||||
FASTGPT_MODEL=gpt-4
|
||||
|
||||
# 代理配置(Node.js 不自动读取系统代理,需手动配置)
|
||||
HTTPS_PROXY=http://127.0.0.1:7890
|
||||
|
||||
# 服务器配置
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
@@ -10,7 +10,7 @@ JWT_SECRET=your_jwt_secret_here_change_in_production
|
||||
|
||||
# FastGPT API 配置
|
||||
FASTGPT_API_KEY=your_fastgpt_api_key_here
|
||||
FASTGPT_API_URL=https://api.fastgpt.in/api/v1/chat/completions
|
||||
FASTGPT_API_URL=https://cloud.fastgpt.cn/api/v1/chat/completions
|
||||
FASTGPT_MODEL=gpt-4
|
||||
|
||||
# 服务器配置
|
||||
|
||||
27
backend/.env.production.example
Normal file
27
backend/.env.production.example
Normal file
@@ -0,0 +1,27 @@
|
||||
# 生产环境配置示例
|
||||
# 根据您的部署方式调整以下配置
|
||||
|
||||
# 数据库配置
|
||||
# Docker Compose 部署:DB_HOST=db, DB_PORT=3306 (容器内部)
|
||||
# 传统部署(数据库在本地):DB_HOST=localhost, DB_PORT=3306
|
||||
# 传统部署(数据库在远程服务器):DB_HOST=数据库服务器IP, DB_PORT=3306
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_secure_password_here
|
||||
DB_NAME=employee_performance
|
||||
|
||||
# JWT 配置(生产环境必须使用强密钥)
|
||||
JWT_SECRET=generate_a_strong_random_string_at_least_32_chars_long
|
||||
|
||||
# FastGPT API 配置
|
||||
FASTGPT_API_KEY=your_fastgpt_api_key_here
|
||||
FASTGPT_API_URL=https://cloud.fastgpt.cn/api/v1/chat/completions
|
||||
FASTGPT_MODEL=gpt-4
|
||||
|
||||
# 代理配置(如果需要通过代理访问 FastGPT)
|
||||
# HTTPS_PROXY=http://127.0.0.1:7890
|
||||
|
||||
# 服务器配置
|
||||
PORT=3001
|
||||
NODE_ENV=production
|
||||
35
backend/Dockerfile
Normal file
35
backend/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# 后端 Dockerfile (开发环境)
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 package.json 和 package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装所有依赖 (包括开发依赖)
|
||||
RUN npm ci
|
||||
# 设置时区
|
||||
RUN apk add --no-cache tzdata \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001 && \
|
||||
chown -R nodejs:nodejs /app
|
||||
|
||||
# 切换用户
|
||||
USER nodejs
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3001
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD node -e "const http = require('http'); http.get('http://localhost:3001/api/health', (res) => { if (res.statusCode !== 200) throw new Error('Health check failed') }).on('error', (err) => { console.error(err); process.exit(1); })"
|
||||
|
||||
# 启动开发服务器
|
||||
CMD ["npm", "run", "dev"]
|
||||
@@ -26,7 +26,7 @@ async function run() {
|
||||
{ expiresIn: '1h' }
|
||||
);
|
||||
|
||||
const { data } = await axios.get('http://localhost:3001/api/performance/employee/get', {
|
||||
const { data } = await axios.get('http://47.238.126.111:33001/api/performance/employee/get', {
|
||||
params: { perfId: rec.perf_id },
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
67
backend/migrate-passwords.ts
Normal file
67
backend/migrate-passwords.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import pool from './src/config/database';
|
||||
|
||||
/**
|
||||
* 密码迁移脚本
|
||||
* 将数据库中所有明文密码更新为bcrypt哈希密码
|
||||
* 仅更新密码不是bcrypt哈希格式(以$2b$开头)的用户
|
||||
*/
|
||||
async function migratePasswords() {
|
||||
try {
|
||||
console.log('开始密码迁移...');
|
||||
|
||||
// 获取所有用户
|
||||
const [users] = await pool.query<any[]>(
|
||||
'SELECT user_id, username, password FROM user'
|
||||
);
|
||||
|
||||
console.log(`共找到 ${users.length} 个用户`);
|
||||
|
||||
let migratedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const user of users) {
|
||||
const { user_id, username, password } = user;
|
||||
|
||||
// 检查密码是否已经是bcrypt哈希格式
|
||||
if (password.startsWith('$2b$') || password.startsWith('$2a$') || password.startsWith('$2y$')) {
|
||||
console.log(`✓ 用户 ${username} (ID: ${user_id}) 密码已经是哈希格式,跳过`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成bcrypt哈希(使用默认盐轮数10)
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 更新数据库
|
||||
await pool.query(
|
||||
'UPDATE user SET password = ? WHERE user_id = ?',
|
||||
[hashedPassword, user_id]
|
||||
);
|
||||
|
||||
console.log(`✓ 用户 ${username} (ID: ${user_id}) 密码已更新为哈希格式`);
|
||||
migratedCount++;
|
||||
} catch (err) {
|
||||
console.error(`✗ 用户 ${username} (ID: ${user_id}) 密码更新失败:`, err);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n迁移完成!');
|
||||
console.log(`成功迁移: ${migratedCount} 个用户`);
|
||||
console.log(`跳过(已是哈希): ${skippedCount} 个用户`);
|
||||
console.log(`失败: ${errorCount} 个用户`);
|
||||
|
||||
// 关闭连接池
|
||||
await pool.end();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('密码迁移失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行迁移
|
||||
migratePasswords();
|
||||
462
backend/package-lock.json
generated
462
backend/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"exceljs": "^4.4.0",
|
||||
@@ -18,7 +18,7 @@
|
||||
"mysql2": "^3.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
@@ -1012,6 +1012,78 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nopt": "^5.0.0",
|
||||
"npmlog": "^5.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"bin": {
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.10",
|
||||
"resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.10.tgz",
|
||||
@@ -1112,12 +1184,15 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||
"node_modules/@types/bcrypt": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
|
||||
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
@@ -1351,6 +1426,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -1390,6 +1471,41 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@@ -1410,7 +1526,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -1446,6 +1561,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/archiver": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/archiver/-/archiver-5.3.2.tgz",
|
||||
@@ -1515,6 +1636,20 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/are-we-there-yet": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
|
||||
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz",
|
||||
@@ -1725,11 +1860,19 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
|
||||
"license": "MIT"
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"node-addon-api": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/big-integer": {
|
||||
"version": "1.6.52",
|
||||
@@ -2094,6 +2237,15 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz",
|
||||
@@ -2170,6 +2322,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -2203,6 +2364,12 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -2387,6 +2554,12 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz",
|
||||
@@ -2415,6 +2588,15 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-newline": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||
@@ -2559,7 +2741,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
@@ -2958,6 +3139,36 @@
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -3016,6 +3227,27 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3 || ^2.0.0",
|
||||
"color-support": "^1.1.2",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.1",
|
||||
"object-assign": "^4.1.1",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wide-align": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz",
|
||||
@@ -3216,6 +3448,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -3255,6 +3493,42 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@@ -3409,7 +3683,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4754,11 +5027,50 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
@@ -4846,6 +5158,32 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -4860,6 +5198,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -4882,6 +5235,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "^2.0.0",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"gauge": "^3.0.0",
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5474,6 +5840,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
@@ -5585,7 +5957,6 @@
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
@@ -5697,7 +6068,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -5712,7 +6082,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -5780,6 +6149,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
@@ -5796,6 +6183,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
@@ -5849,6 +6242,12 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/traverse": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmmirror.com/traverse/-/traverse-0.3.9.tgz",
|
||||
@@ -6284,6 +6683,22 @@
|
||||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
||||
@@ -6300,6 +6715,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
|
||||
@@ -9,6 +9,7 @@ const pool = mysql.createPool({
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'employee_performance',
|
||||
charset: 'utf8mb4',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
|
||||
@@ -28,3 +28,35 @@ export async function findSubordinates(managerId: number): Promise<UserRow[]> {
|
||||
);
|
||||
return rows as UserRow[];
|
||||
}
|
||||
|
||||
export interface CreateUserInput {
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
role?: UserRole;
|
||||
department: string;
|
||||
position: string;
|
||||
manager_id?: number | null;
|
||||
status?: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
export async function createUser(userData: CreateUserInput): Promise<number> {
|
||||
const {
|
||||
username,
|
||||
password,
|
||||
name,
|
||||
role = 'employee',
|
||||
department,
|
||||
position,
|
||||
manager_id = null,
|
||||
status = 'active'
|
||||
} = userData;
|
||||
|
||||
const [result] = await pool.query<any>(
|
||||
`INSERT INTO user (username, password, name, role, department, position, manager_id, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[username, password, name, role, department, position, manager_id, status]
|
||||
);
|
||||
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ USE employee_performance;
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
|
||||
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名(工号)',
|
||||
password VARCHAR(255) NOT NULL COMMENT '密码(bcrypt加密)',
|
||||
password VARCHAR(255) NOT NULL COMMENT '密码(明文存储,测试环境使用)',
|
||||
name VARCHAR(50) NOT NULL COMMENT '姓名',
|
||||
role ENUM('employee', 'manager', 'generalManager') NOT NULL COMMENT '角色',
|
||||
department VARCHAR(50) NOT NULL COMMENT '部门',
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
-- 测试数据插入脚本
|
||||
USE employee_performance;
|
||||
|
||||
-- 插入测试用户(密码都是 123456,已用 bcrypt 加密)
|
||||
-- bcrypt hash for '123456': $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
|
||||
-- 插入测试用户(所有用户密码均为123456,明文存储)
|
||||
-- 注意:此版本使用明文密码,仅用于测试环境。生产环境必须使用加密密码。
|
||||
|
||||
-- 1. 总经理
|
||||
INSERT INTO user (username, password, name, role, department, position, manager_id, status)
|
||||
VALUES ('gm001', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '张总', 'generalManager', '管理层', '总经理', NULL, 'active')
|
||||
VALUES ('gm001', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '张总', 'generalManager', '管理层', '总经理', NULL, 'active')
|
||||
ON DUPLICATE KEY UPDATE username=username;
|
||||
|
||||
-- 2. 部门经理(技术部)
|
||||
INSERT INTO user (username, password, name, role, department, position, manager_id, status)
|
||||
VALUES ('mgr001', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '李经理', 'manager', '技术部', '技术经理', 1, 'active')
|
||||
VALUES ('mgr001', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '李经理', 'manager', '技术部', '技术经理', 1, 'active')
|
||||
ON DUPLICATE KEY UPDATE username=username;
|
||||
|
||||
-- 3. 部门经理(销售部)
|
||||
INSERT INTO user (username, password, name, role, department, position, manager_id, status)
|
||||
VALUES ('mgr002', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '王经理', 'manager', '销售部', '销售经理', 1, 'active')
|
||||
VALUES ('mgr002', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '王经理', 'manager', '销售部', '销售经理', 1, 'active')
|
||||
ON DUPLICATE KEY UPDATE username=username;
|
||||
|
||||
-- 4. 员工(技术部)
|
||||
INSERT INTO user (username, password, name, role, department, position, manager_id, status)
|
||||
VALUES
|
||||
('emp001', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '张三', 'employee', '技术部', '前端工程师', 2, 'active'),
|
||||
('emp002', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '李四', 'employee', '技术部', '后端工程师', 2, 'active'),
|
||||
('emp003', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '王五', 'employee', '技术部', '测试工程师', 2, 'active')
|
||||
('emp001', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '张三', 'employee', '技术部', '前端工程师', 2, 'active'),
|
||||
('emp002', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '李四', 'employee', '技术部', '后端工程师', 2, 'active'),
|
||||
('emp003', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '王五', 'employee', '技术部', '测试工程师', 2, 'active')
|
||||
ON DUPLICATE KEY UPDATE username=username;
|
||||
|
||||
-- 5. 员工(销售部)
|
||||
INSERT INTO user (username, password, name, role, department, position, manager_id, status)
|
||||
VALUES
|
||||
('emp004', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '赵六', 'employee', '销售部', '销售专员', 3, 'active'),
|
||||
('emp005', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '孙七', 'employee', '销售部', '销售专员', 3, 'active')
|
||||
('emp004', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '赵六', 'employee', '销售部', '销售专员', 3, 'active'),
|
||||
('emp005', '$2b$10$BwEJVCKkcCcmSM3m15QLE.WzYJxifJRjY2c.IhDZczyshjyOLDsSu', '孙七', 'employee', '销售部', '销售专员', 3, 'active')
|
||||
ON DUPLICATE KEY UPDATE username=username;
|
||||
|
||||
-- 插入默认考核规则配置
|
||||
|
||||
@@ -24,11 +24,11 @@ async function runSeed() {
|
||||
|
||||
console.log('\n✅ 数据库种子数据插入完成!');
|
||||
console.log('\n测试账号信息:');
|
||||
console.log('总经理: gm001 / 123456');
|
||||
console.log('技术部经理: mgr001 / 123456');
|
||||
console.log('销售部经理: mgr002 / 123456');
|
||||
console.log('技术部员工: emp001, emp002, emp003 / 123456');
|
||||
console.log('销售部员工: emp004, emp005 / 123456');
|
||||
console.log('总经理: gm001 / 123456(密码加密存储)');
|
||||
console.log('技术部经理: mgr001 / 123456(密码加密存储)');
|
||||
console.log('销售部经理: mgr002 / 123456(密码加密存储)');
|
||||
console.log('技术部员工: emp001, emp002, emp003 / 123456(密码加密存储)');
|
||||
console.log('销售部员工: emp004, emp005 / 123456(密码加密存储)');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
|
||||
32
backend/src/index.ts.backup
Normal file
32
backend/src/index.ts.backup
Normal file
@@ -0,0 +1,32 @@
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import authRouter from './routes/auth';
|
||||
import performanceRouter from './routes/performance';
|
||||
import statisticsRouter from './routes/statistics';
|
||||
import configRouter from './routes/config';
|
||||
import employeeRouter from './routes/employee';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.use('/api/user', authRouter);
|
||||
app.use('/api/performance', performanceRouter);
|
||||
app.use('/api/statistics', statisticsRouter);
|
||||
app.use('/api/config', configRouter);
|
||||
app.use('/api/employee', employeeRouter);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { login } from '../services/AuthService';
|
||||
import { login, register } from '../services/AuthService';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -24,4 +24,25 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/user/register
|
||||
router.post('/register', async (req: Request, res: Response) => {
|
||||
console.log('收到注册请求:', req.body);
|
||||
const { username, password, name, department, position, role } = req.body;
|
||||
|
||||
if (!username || !password || !name || !department || !position) {
|
||||
console.log('参数验证失败');
|
||||
return res.status(400).json({ code: 400, message: '用户名、密码、姓名、部门和岗位均为必填' });
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('调用注册服务...');
|
||||
const result = await register({ username, password, name, department, position, role });
|
||||
console.log('注册成功:', result.userInfo);
|
||||
return res.json({ code: 200, message: '注册成功', data: result });
|
||||
} catch (err: any) {
|
||||
console.error('注册失败:', err.message);
|
||||
return res.status(400).json({ code: 400, message: err.message || '注册失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { authenticate } from '../middlewares/authenticate';
|
||||
import { authorize } from '../middlewares/authorize';
|
||||
import pool from '../config/database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const router = Router();
|
||||
router.use(authenticate);
|
||||
@@ -40,6 +40,12 @@ router.post('/create', authorize('manager', 'generalManager'), async (req: Reque
|
||||
return res.status(400).json({ code: 400, message: '用户名、密码、姓名、部门、岗位均为必填' });
|
||||
}
|
||||
|
||||
// 检查字段是否包含占位符(多个问号)
|
||||
const placeholderRegex = /^\?+$/;
|
||||
if (placeholderRegex.test(name) || placeholderRegex.test(department) || placeholderRegex.test(position)) {
|
||||
return res.status(400).json({ code: 400, message: '姓名、部门、岗位不能使用问号占位符,请填写真实信息' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查用户名是否已存在
|
||||
const [existing] = await pool.query<any[]>('SELECT user_id FROM user WHERE username = ?', [username]);
|
||||
@@ -47,9 +53,11 @@ router.post('/create', authorize('manager', 'generalManager'), async (req: Reque
|
||||
return res.status(400).json({ code: 400, message: '用户名已存在' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const managerId = user.role === 'manager' ? user.userId : null;
|
||||
|
||||
// 哈希密码
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const [result] = await pool.query<any>(
|
||||
`INSERT INTO user (username, password, name, role, department, position, manager_id, status)
|
||||
VALUES (?, ?, ?, 'employee', ?, ?, ?, 'active')`,
|
||||
|
||||
@@ -140,7 +140,7 @@ export function parseAIResponse(rawResponse: string): AIScoreData {
|
||||
|
||||
// ─── FastGPT API Call ─────────────────────────────────────────────────────────
|
||||
|
||||
const FASTGPT_API_URL = process.env.FASTGPT_API_URL || 'https://api.fastgpt.in/api/v1/chat/completions';
|
||||
const FASTGPT_API_URL = process.env.FASTGPT_API_URL || 'https://cloud.fastgpt.cn/api/v1/chat/completions';
|
||||
const FASTGPT_API_KEY = process.env.FASTGPT_API_KEY || '';
|
||||
const AI_TIMEOUT_MS = 60_000; // 60秒超时
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
// 密码使用bcrypt哈希存储和验证
|
||||
// 迁移期间支持明文密码自动升级为哈希密码
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { findByUsername } from '../dao/UserDAO';
|
||||
import { findByUsername, createUser, CreateUserInput } from '../dao/UserDAO';
|
||||
import { JWT_SECRET, JWT_EXPIRES_IN } from '../config/jwt';
|
||||
import { LoginResult, UserInfo, UserRole } from '../types';
|
||||
import pool from '../config/database';
|
||||
|
||||
export async function login(
|
||||
username: string,
|
||||
@@ -15,7 +18,26 @@ export async function login(
|
||||
throw new Error('用户名或密码错误');
|
||||
}
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||
// 检查密码是否为bcrypt哈希格式(以$2b$开头)
|
||||
const isBcryptHash = user.password.startsWith('$2b$');
|
||||
|
||||
let passwordMatch = false;
|
||||
|
||||
if (isBcryptHash) {
|
||||
// 密码已经是哈希格式,使用bcrypt.compare验证
|
||||
passwordMatch = await bcrypt.compare(password, user.password);
|
||||
} else {
|
||||
// 密码是明文,直接比较(迁移期间)
|
||||
passwordMatch = password === user.password;
|
||||
|
||||
// 如果密码匹配,将明文密码更新为哈希密码
|
||||
if (passwordMatch) {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
await pool.query('UPDATE user SET password = ? WHERE user_id = ?', [hashedPassword, user.user_id]);
|
||||
console.log(`用户 ${username} 密码已从明文更新为哈希值`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!passwordMatch) {
|
||||
throw new Error('用户名或密码错误');
|
||||
}
|
||||
@@ -37,3 +59,60 @@ export async function login(
|
||||
|
||||
return { token, userInfo };
|
||||
}
|
||||
|
||||
export interface RegisterInput {
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
department: string;
|
||||
position: string;
|
||||
role?: UserRole;
|
||||
}
|
||||
|
||||
export async function register(userData: RegisterInput): Promise<LoginResult> {
|
||||
const { username, password, name, department, position, role = 'employee' } = userData;
|
||||
|
||||
// 检查必填字段
|
||||
if (!username || !password || !name || !department || !position) {
|
||||
throw new Error('用户名、密码、姓名、部门和岗位均为必填');
|
||||
}
|
||||
|
||||
// 检查字段是否包含占位符(多个问号)
|
||||
const placeholderRegex = /^\?+$/;
|
||||
if (placeholderRegex.test(name) || placeholderRegex.test(department) || placeholderRegex.test(position)) {
|
||||
throw new Error('姓名、部门、岗位不能使用问号占位符,请填写真实信息');
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await findByUsername(username);
|
||||
if (existingUser) {
|
||||
throw new Error('用户名已存在');
|
||||
}
|
||||
|
||||
// 创建用户 - 密码哈希存储
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const userId = await createUser({
|
||||
username,
|
||||
password: hashedPassword, // 存储哈希后的密码
|
||||
name,
|
||||
role,
|
||||
department,
|
||||
position,
|
||||
manager_id: null, // 新注册用户没有直属领导
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// 注册成功后自动登录,返回token和用户信息
|
||||
const userInfo: UserInfo = {
|
||||
userId,
|
||||
name,
|
||||
role,
|
||||
department,
|
||||
position,
|
||||
managerId: null
|
||||
};
|
||||
|
||||
const token = jwt.sign(userInfo, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
|
||||
return { token, userInfo };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as fc from 'fast-check';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { login } from '../AuthService';
|
||||
import * as UserDAO from '../../dao/UserDAO';
|
||||
@@ -29,11 +28,10 @@ describe('Property 1: 认证正确性', () => {
|
||||
position: fc.string({ minLength: 1, maxLength: 20 }),
|
||||
}),
|
||||
async ({ username, password, role, userId, name, department, position }) => {
|
||||
const hashedPassword = bcrypt.hashSync(password, 1); // cost 1 for speed
|
||||
const userRow: UserDAO.UserRow = {
|
||||
user_id: userId,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
password: password,
|
||||
name,
|
||||
role,
|
||||
department,
|
||||
@@ -70,11 +68,10 @@ describe('Property 1: 认证正确性', () => {
|
||||
role: fc.constantFrom<UserRole>(...ROLES),
|
||||
}).filter(({ correctPassword, wrongPassword }) => correctPassword !== wrongPassword),
|
||||
async ({ username, correctPassword, wrongPassword, role }) => {
|
||||
const hashedPassword = bcrypt.hashSync(correctPassword, 1);
|
||||
mockFindByUsername.mockResolvedValue({
|
||||
user_id: 1,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
password: correctPassword,
|
||||
name: '测试',
|
||||
role,
|
||||
department: '部门',
|
||||
@@ -117,11 +114,10 @@ describe('Property 1: 认证正确性', () => {
|
||||
requestedRole: fc.constantFrom<UserRole>(...ROLES),
|
||||
}).filter(({ storedRole, requestedRole }) => storedRole !== requestedRole),
|
||||
async ({ username, password, storedRole, requestedRole }) => {
|
||||
const hashedPassword = bcrypt.hashSync(password, 1);
|
||||
mockFindByUsername.mockResolvedValue({
|
||||
user_id: 1,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
password: password,
|
||||
name: '测试',
|
||||
role: storedRole,
|
||||
department: '部门',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { login } from '../AuthService';
|
||||
import * as UserDAO from '../../dao/UserDAO';
|
||||
@@ -10,7 +9,7 @@ const mockFindByUsername = UserDAO.findByUsername as jest.MockedFunction<typeof
|
||||
const baseUser: UserDAO.UserRow = {
|
||||
user_id: 1,
|
||||
username: 'emp001',
|
||||
password: bcrypt.hashSync('password123', 10),
|
||||
password: 'password123',
|
||||
name: '张三',
|
||||
role: 'employee',
|
||||
department: '研发部',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
import pool from './src/config/database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
async function run() {
|
||||
const hash = await bcrypt.hash('123456', 10);
|
||||
const password = '123456';
|
||||
|
||||
// 删除 mgr002
|
||||
await pool.query('DELETE FROM user WHERE username = ?', ['mgr002']);
|
||||
@@ -13,14 +12,14 @@ async function run() {
|
||||
// 更新 gm001 → lister / 李总 / 总经理
|
||||
await pool.query(
|
||||
'UPDATE user SET username = ?, name = ?, password = ? WHERE username = ?',
|
||||
['lister', '李总', hash, 'gm001']
|
||||
['lister', '李总', password, 'gm001']
|
||||
);
|
||||
console.log('更新 gm001 → lister');
|
||||
|
||||
// 更新 mgr001 → xinxin / 孙薪薪 / 管理层
|
||||
await pool.query(
|
||||
'UPDATE user SET username = ?, name = ?, password = ? WHERE username = ?',
|
||||
['xinxin', '孙薪薪', hash, 'mgr001']
|
||||
['xinxin', '孙薪薪', password, 'mgr001']
|
||||
);
|
||||
console.log('更新 mgr001 → xinxin');
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
import pool from './src/config/database';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
async function run() {
|
||||
const xinxinHash = await bcrypt.hash('sxx980623', 10);
|
||||
const listerHash = await bcrypt.hash('lister123', 10);
|
||||
const password = '123456';
|
||||
|
||||
await pool.query('UPDATE user SET password = ? WHERE username = ?', [xinxinHash, 'xinxin']);
|
||||
await pool.query('UPDATE user SET password = ? WHERE username = ?', [listerHash, 'lister']);
|
||||
await pool.query('UPDATE user SET password = ? WHERE username = ?', [password, 'xinxin']);
|
||||
await pool.query('UPDATE user SET password = ? WHERE username = ?', [password, 'lister']);
|
||||
|
||||
console.log('xinxin 密码已更新为 sxx980623');
|
||||
console.log('lister 密码已更新为 lister123');
|
||||
console.log('xinxin 密码已更新为 123456');
|
||||
console.log('lister 密码已更新为 123456');
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
@@ -13,26 +12,20 @@ async function updatePasswords() {
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'employee_performance',
|
||||
});
|
||||
|
||||
console.log('生成新的密码哈希...');
|
||||
const hash = await bcrypt.hash('123456', 10);
|
||||
console.log('新哈希:', hash);
|
||||
|
||||
// 测试旧哈希
|
||||
const oldHash = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
|
||||
const matchOld = await bcrypt.compare('123456', oldHash);
|
||||
console.log('旧哈希验证:', matchOld);
|
||||
|
||||
|
||||
console.log('设置所有用户密码为123456(明文)...');
|
||||
const password = '123456';
|
||||
|
||||
// 更新所有用户密码
|
||||
await conn.query('UPDATE user SET password = ?', [hash]);
|
||||
console.log('✓ 所有用户密码已更新');
|
||||
|
||||
await conn.query('UPDATE user SET password = ?', [password]);
|
||||
console.log('✓ 所有用户密码已更新为123456');
|
||||
|
||||
// 验证更新
|
||||
const [rows] = await conn.query('SELECT username, password FROM user LIMIT 1');
|
||||
const user = (rows as any)[0];
|
||||
const matchNew = await bcrypt.compare('123456', user.password);
|
||||
console.log('新密码验证:', matchNew);
|
||||
|
||||
const passwordMatch = user.password === '123456';
|
||||
console.log('密码验证:', passwordMatch ? '成功' : '失败');
|
||||
|
||||
await conn.end();
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
|
||||
123
docker-compose.yml
Normal file
123
docker-compose.yml
Normal file
@@ -0,0 +1,123 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MySQL 数据库服务
|
||||
db:
|
||||
image: mysql:8.0
|
||||
container_name: performance-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 123456
|
||||
MYSQL_DATABASE: employee_performance
|
||||
MYSQL_USER: app_user
|
||||
MYSQL_PASSWORD: 123456
|
||||
ports:
|
||||
- "33306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./backend/src/db/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p123456"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
command:
|
||||
- --default-authentication-plugin=mysql_native_password
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
|
||||
# 后端 API 服务
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: performance-backend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_PORT: 3306
|
||||
DB_USER: app_user
|
||||
DB_PASSWORD: 123456
|
||||
DB_NAME: employee_performance
|
||||
JWT_SECRET: your_jwt_secret_here_change_in_production
|
||||
FASTGPT_API_KEY: ${FASTGPT_API_KEY:-your_fastgpt_api_key_here}
|
||||
FASTGPT_API_URL: https://cloud.fastgpt.cn/api/v1/chat/completions
|
||||
FASTGPT_MODEL: gpt-4
|
||||
PORT: 3001
|
||||
NODE_ENV: development
|
||||
ports:
|
||||
- "33001:3001"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "const http = require('http'); http.get('http://localhost:3001/api/health', (res) => { if (res.statusCode !== 200) throw new Error('Health check failed') }).on('error', (err) => { console.error(err); process.exit(1); })"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
|
||||
# 前端开发服务
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: performance-frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
environment:
|
||||
VITE_API_BASE_URL: ''
|
||||
VITE_API_URL: http://backend:3001
|
||||
ports:
|
||||
- "2000:3000"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "const http = require('http'); http.get('http://localhost:3000', (res) => { if (res.statusCode !== 200) throw new Error('Health check failed') }).on('error', (err) => { console.error(err); process.exit(1); })"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# Nginx反向代理服务
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: performance-nginx
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
networks:
|
||||
- app-network
|
||||
healthcheck:
|
||||
test: ["CMD", "nginx", "-t"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
# 定义网络
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
|
||||
# 定义数据卷
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
7
frontend/.env.production
Normal file
7
frontend/.env.production
Normal file
@@ -0,0 +1,7 @@
|
||||
# 前端生产环境配置
|
||||
# 构建时使用的环境变量
|
||||
|
||||
# API 基础 URL - 使用阿里云NLB负载均衡器
|
||||
VITE_API_BASE_URL=http://nlb-y7wvnvbyb726t5m4bz.cn-hongkong.nlb.aliyuncsslbintl.com:3001
|
||||
|
||||
# 其他环境变量可根据需要添加
|
||||
11
frontend/.env.production.example
Normal file
11
frontend/.env.production.example
Normal file
@@ -0,0 +1,11 @@
|
||||
# 前端生产环境配置
|
||||
# 构建时使用的环境变量
|
||||
|
||||
# API 基础 URL
|
||||
# Docker Compose 部署(前端独立构建,后端在 47.238.126.111:33001):
|
||||
VITE_API_BASE_URL=http://47.238.126.111:33001
|
||||
|
||||
# 传统部署(Nginx 反向代理):
|
||||
# VITE_API_BASE_URL=/api # 如果使用 Nginx 反向代理到后端,可以设置为相对路径 /api
|
||||
|
||||
# 其他环境变量可根据需要添加
|
||||
31
frontend/Dockerfile
Normal file
31
frontend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# 前端 Dockerfile (开发环境)
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 package.json 和 package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001 && \
|
||||
chown -R nodejs:nodejs /app
|
||||
|
||||
# 切换用户
|
||||
USER nodejs
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD node -e "const http = require('http'); http.get('http://localhost:3000', (res) => { if (res.statusCode !== 200) throw new Error('Health check failed') }).on('error', (err) => { console.error(err); process.exit(1); })"
|
||||
|
||||
# 启动开发服务器
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
@@ -12,6 +12,15 @@ interface ApiResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
department: string;
|
||||
position: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export const authApi = {
|
||||
login: async (
|
||||
username: string,
|
||||
@@ -25,4 +34,9 @@ export const authApi = {
|
||||
});
|
||||
return data.data; // 返回 data.data,因为后端包装了一层
|
||||
},
|
||||
|
||||
register: async (userData: RegisterRequest): Promise<LoginResponse> => {
|
||||
const { data } = await http.post<ApiResponse<LoginResponse>>('/api/user/register', userData);
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import axios from 'axios';
|
||||
const TOKEN_KEY = 'auth_token';
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001',
|
||||
// 🔥 核心修改
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '',
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
@@ -25,7 +26,6 @@ http.interceptors.response.use(
|
||||
(error) => {
|
||||
const status = error?.response?.status;
|
||||
if (status === 401 || status === 403) {
|
||||
// Clear stored credentials and redirect to login
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem('auth_user');
|
||||
window.location.href = '/login';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, Select, Alert, Typography } from 'antd';
|
||||
import { UserOutlined, LockOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { authApi } from '../api/auth';
|
||||
import logo from '../img/logo2.png';
|
||||
@@ -126,7 +126,12 @@ const LoginPage: React.FC = () => {
|
||||
</Form>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>优一科技 © 2026</Text>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
新用户?<Link to="/register" style={{ color: '#6366f1', fontWeight: 500 }}>注册新账户</Link>
|
||||
</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>优一科技 © 2026</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
267
frontend/src/pages/Register.tsx
Normal file
267
frontend/src/pages/Register.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, Alert, Typography } from 'antd';
|
||||
import { UserOutlined, LockOutlined, IdcardOutlined, ApartmentOutlined, UsergroupAddOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { authApi } from '../api/auth';
|
||||
import logo from '../img/logo2.png';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface RegisterFormValues {
|
||||
username: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
name: string;
|
||||
department: string;
|
||||
position: string;
|
||||
}
|
||||
|
||||
const RegisterPage: React.FC = () => {
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [alertInfo, setAlertInfo] = useState<{ type: 'success' | 'error'; msg: string } | null>(null);
|
||||
|
||||
const onFinish = async (values: RegisterFormValues) => {
|
||||
setLoading(true);
|
||||
setAlertInfo(null);
|
||||
try {
|
||||
// 调用注册API
|
||||
const { token, userInfo } = await authApi.register({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
department: values.department,
|
||||
position: values.position,
|
||||
role: 'employee', // 新注册用户默认为员工角色
|
||||
});
|
||||
|
||||
// 注册成功后自动登录
|
||||
login(token, userInfo);
|
||||
setAlertInfo({ type: 'success', msg: `注册成功!欢迎 ${userInfo.name},正在跳转到员工页面...` });
|
||||
setTimeout(() => navigate('/employee', { replace: true }), 1500);
|
||||
} catch (err: any) {
|
||||
setAlertInfo({ type: 'error', msg: err?.response?.data?.message || '注册失败,请检查输入信息' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.bg}>
|
||||
{/* 光晕装饰 */}
|
||||
<div style={{ ...styles.circle, width: 500, height: 500, top: -150, left: -150 }} />
|
||||
<div style={{ ...styles.circle, width: 400, height: 400, bottom: -100, right: -100 }} />
|
||||
<div style={{ ...styles.circle, width: 300, height: 300, top: '40%', left: '60%' }} />
|
||||
|
||||
{/* 双层纹理叠层:点阵 + 斜线 */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, zIndex: 0,
|
||||
backgroundImage: `
|
||||
radial-gradient(circle, rgba(99,102,241,0.08) 1px, transparent 1px),
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 20px,
|
||||
rgba(99,102,241,0.03) 20px,
|
||||
rgba(99,102,241,0.03) 21px
|
||||
)
|
||||
`,
|
||||
backgroundSize: '24px 24px, 100% 100%',
|
||||
}} />
|
||||
|
||||
<div style={styles.card}>
|
||||
{/* Logo + 标题 */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 28 }}>
|
||||
<img src={logo} alt="logo" style={{ height: 48, objectFit: 'contain', marginBottom: 12 }} />
|
||||
<div style={{ fontSize: 13, color: '#8c8c8c', letterSpacing: 1 }}>员工月度绩效考核系统</div>
|
||||
<div style={{ fontSize: 16, color: '#6366f1', fontWeight: 600, marginTop: 8 }}>新用户注册</div>
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
{alertInfo && (
|
||||
<Alert
|
||||
type={alertInfo.type}
|
||||
message={alertInfo.msg}
|
||||
showIcon
|
||||
style={{ marginBottom: 16, borderRadius: 8 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
message="系统提示"
|
||||
description="密码将加密存储,请妥善保管您的密码"
|
||||
showIcon
|
||||
style={{ marginBottom: 16, borderRadius: 8 }}
|
||||
/>
|
||||
|
||||
<Form<RegisterFormValues>
|
||||
name="register"
|
||||
onFinish={onFinish}
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名(工号)' },
|
||||
{ min: 3, message: '用户名至少3个字符' },
|
||||
{ max: 50, message: '用户名最多50个字符' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="用户名(工号)"
|
||||
style={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' }
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="密码"
|
||||
style={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="确认密码"
|
||||
style={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true, message: '请输入姓名' },
|
||||
{ max: 50, message: '姓名最多50个字符' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<IdcardOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="姓名"
|
||||
style={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="department"
|
||||
rules={[
|
||||
{ required: true, message: '请输入部门' },
|
||||
{ max: 50, message: '部门最多50个字符' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<ApartmentOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="部门"
|
||||
style={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="position"
|
||||
rules={[
|
||||
{ required: true, message: '请输入岗位' },
|
||||
{ max: 50, message: '岗位最多50个字符' }
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UsergroupAddOutlined style={{ color: '#bfbfbf' }} />}
|
||||
placeholder="岗位"
|
||||
style={styles.input}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
loading={loading}
|
||||
style={styles.btn}
|
||||
>
|
||||
注 册
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 20 }}>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
已有账户?<Link to="/login" style={{ color: '#6366f1', fontWeight: 500 }}>返回登录</Link>
|
||||
</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>优一科技 © 2026</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
bg: {
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #f0f7ff 0%, #e8f0fe 35%, #fce4ec 70%, #f3e5f5 100%)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
padding: 16,
|
||||
},
|
||||
circle: {
|
||||
position: 'absolute',
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(99,102,241,0.12) 0%, transparent 70%)',
|
||||
filter: 'blur(40px)',
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
maxWidth: 420,
|
||||
background: '#ffffff',
|
||||
borderRadius: 20,
|
||||
padding: '36px 32px 28px',
|
||||
boxShadow: '0 4px 24px rgba(99,102,241,0.12), 0 1px 4px rgba(0,0,0,0.06)',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
border: '1px solid rgba(99,102,241,0.08)',
|
||||
},
|
||||
input: {
|
||||
borderRadius: 8,
|
||||
height: 44,
|
||||
},
|
||||
btn: {
|
||||
height: 46,
|
||||
borderRadius: 10,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
background: 'linear-gradient(90deg, #6366f1, #8b5cf6)',
|
||||
border: 'none',
|
||||
letterSpacing: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export default RegisterPage;
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
ArrowLeftOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { PerformanceRecord } from '../../api/performance';
|
||||
import { PerformanceRecord, PerformanceItemDetail } from '../../api/performance';
|
||||
import http from '../../api/http';
|
||||
import { useIsMobile } from '../../hooks/useBreakpoint';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -85,7 +85,7 @@ const PerformanceReview: React.FC = () => {
|
||||
|
||||
// Initialize form with existing manager scores if available
|
||||
const itemScores: Record<string, ItemScoreForm> = {};
|
||||
data.performanceItems?.forEach((item) => {
|
||||
data.performanceItems?.forEach((item: PerformanceItemDetail) => {
|
||||
itemScores[item.itemName] = {
|
||||
managerScore: item.managerScore ?? item.aiScore ?? item.selfScore ?? 0,
|
||||
scoreExplanation: item.managerExplanation ?? '',
|
||||
|
||||
@@ -42,12 +42,12 @@ interface SubordinateRecord extends PerformanceRecord {
|
||||
userPosition?: string;
|
||||
}
|
||||
|
||||
interface PageResult<T> {
|
||||
list: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
// interface PageResult<T> {
|
||||
// list: T[];
|
||||
// total: number;
|
||||
// page: number;
|
||||
// pageSize: number;
|
||||
// }
|
||||
|
||||
const SubordinateList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -7,6 +7,7 @@ const EmployeeDashboard = React.lazy(() => import('../pages/employee/Dashboard')
|
||||
const ManagerDashboard = React.lazy(() => import('../pages/manager/Dashboard'));
|
||||
const GMDashboard = React.lazy(() => import('../pages/gm/Dashboard'));
|
||||
const LoginPage = React.lazy(() => import('../pages/Login'));
|
||||
const RegisterPage = React.lazy(() => import('../pages/Register'));
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
@@ -43,6 +44,7 @@ const AppRouter: React.FC = () => {
|
||||
<React.Suspense fallback={<div>加载中...</div>}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
|
||||
{/* 员工路由 */}
|
||||
<Route
|
||||
|
||||
@@ -4,12 +4,19 @@ import react from '@vitejs/plugin-react';
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0', // 监听所有网络接口
|
||||
port: 3000,
|
||||
allowedHosts: 'all',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3001',
|
||||
target: process.env.VITE_API_URL || 'http://localhost:33001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
hmr: {
|
||||
host: 'performance-appraisal.excn.top',
|
||||
protocol: 'wss',
|
||||
clientPort: 443,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
34
nginx.conf
Normal file
34
nginx.conf
Normal file
@@ -0,0 +1,34 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name performance-appraisal.excn.top;
|
||||
|
||||
if ($http_x_forwarded_proto = "http") {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# 🔥 API(必须放前面 + ^~)
|
||||
location ^~ /api/ {
|
||||
|
||||
rewrite ^/api/api/(.*)$ /api/$1 break;
|
||||
|
||||
proxy_pass http://backend:3001/;
|
||||
|
||||
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 $http_x_forwarded_proto;
|
||||
|
||||
add_header X-Debug-API "hit-api";
|
||||
}
|
||||
|
||||
# 前端
|
||||
location / {
|
||||
proxy_pass http://frontend:3000;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
10
部署文档.md
10
部署文档.md
@@ -81,7 +81,7 @@ cp .env.example .env
|
||||
```env
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_PORT=33306 # Docker Compose 部署时使用 33306,传统部署请根据实际情况调整
|
||||
DB_USER=perf_user
|
||||
DB_PASSWORD=你的数据库密码
|
||||
DB_NAME=employee_performance
|
||||
@@ -158,7 +158,7 @@ npm install
|
||||
|
||||
```typescript
|
||||
const http = axios.create({
|
||||
baseURL: 'http://你的后端服务器IP:3001',
|
||||
baseURL: 'http://47.238.126.111:33001', # Docker Compose 部署时使用 33001,传统部署使用 3001
|
||||
timeout: 15000,
|
||||
});
|
||||
```
|
||||
@@ -166,7 +166,7 @@ const http = axios.create({
|
||||
或者通过环境变量配置,在 `frontend/` 目录创建 `.env.production`:
|
||||
|
||||
```env
|
||||
VITE_API_BASE_URL=http://你的后端服务器IP:3001
|
||||
VITE_API_BASE_URL=http://47.238.126.111:33001 # Docker Compose 部署时使用 33001,传统部署使用 3001
|
||||
```
|
||||
|
||||
### 5.3 构建静态文件
|
||||
@@ -184,7 +184,7 @@ npm run build
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name 你的域名或IP;
|
||||
server_name 47.238.126.111;
|
||||
|
||||
# 前端静态文件
|
||||
root /var/www/perf-system/dist;
|
||||
@@ -197,7 +197,7 @@ server {
|
||||
|
||||
# 反向代理后端 API
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_pass http://127.0.0.1:33001; # Docker Compose 部署时使用 33001,传统部署使用 3001
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
Reference in New Issue
Block a user