8 Commits
main ... dev

Author SHA1 Message Date
eab3f6c28a 修改gpt,url地址 2026-04-13 10:18:44 +08:00
2518d60951 增加密码加密功能 2026-04-12 15:46:55 +08:00
05ee0929e2 增加注册功能 2026-04-12 15:10:22 +08:00
3b1bd94dce 取消哈希加密,使用明文密码登录 2026-04-12 14:41:47 +08:00
b2b43b8e12 ddd 2026-04-12 13:35:18 +08:00
18ea388cc7 修改index.ts错误 2026-04-12 12:31:11 +08:00
0ec92b10a1 修改主机地址 2026-04-12 11:53:28 +08:00
f0cc37681c feat: 当前开发代码 2026-04-11 17:54:35 +08:00
104 changed files with 1324 additions and 93 deletions

21
.env Normal file
View 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

View File

@@ -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
# 服务器配置

View 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

31
backend/Dockerfile Normal file
View 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 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"]

View File

@@ -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}` },
});

View 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();

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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 '部门',

View File

@@ -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;
-- 插入默认考核规则配置

View File

@@ -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) {

View 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;

View File

@@ -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;

View File

@@ -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')`,

View File

@@ -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秒超时

View File

@@ -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 };
}

View File

@@ -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: '部门',

View File

@@ -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: '研发部',

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend/src

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend/src

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend/src

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system/backend

View File

@@ -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');

View File

@@ -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();
}

View File

@@ -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) {

102
docker-compose.yml Normal file
View File

@@ -0,0 +1,102 @@
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: http://47.238.126.111:33001
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
# 定义网络
networks:
app-network:
driver: bridge
# 定义数据卷
volumes:
mysql_data:
driver: local

View 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
View 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"]

View File

@@ -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;
},
};

View File

@@ -3,7 +3,7 @@ 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 || 'http://localhost:33001',
timeout: 15000,
});

View File

@@ -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>

View 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;

View File

@@ -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

View File

@@ -7,7 +7,7 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:3001',
target: process.env.VITE_API_URL || 'http://localhost:33001',
changeOrigin: true,
},
},

1
tmpclaude-044f-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-0c08-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-0d4b-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-17c0-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-18d3-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-1e60-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-25f4-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-2f0c-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-35f6-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-47c5-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-490d-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-4938-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-50b3-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-5a12-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-5a95-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-668c-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-671f-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-7ecb-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-85df-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-8604-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-8fe5-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-98cf-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-9de9-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-aa04-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-ad8d-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-b5ce-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-b88e-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-b968-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-ccb7-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-cec9-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-d140-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-d22b-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-d65b-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-d88b-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-dcee-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-e31c-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-e3e6-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

1
tmpclaude-e9ac-cwd Normal file
View File

@@ -0,0 +1 @@
/c/Users/99095/Desktop/优一科技/performance-evaluation-system

Some files were not shown because too many files have changed in this diff Show More