2
2

jobs.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. const express = require('express');
  2. const router = express.Router();
  3. const { pool } = require('../config/db');
  4. const crypto = require('crypto');
  5. // 安全解析 JSON 或返回原值
  6. const safeParseJSON = (data) => {
  7. if (typeof data === 'string') {
  8. try {
  9. return JSON.parse(data);
  10. } catch (e) {
  11. return null;
  12. }
  13. }
  14. return data;
  15. };
  16. router.put('/jobs/:id', async (req, res) => {
  17. const { id } = req.params;
  18. const updates = req.body;
  19. try {
  20. // 构建动态更新语句
  21. const updateFields = [];
  22. const values = [];
  23. for (const [key, value] of Object.entries(updates)) {
  24. updateFields.push(`${key} = ?`);
  25. values.push(value);
  26. }
  27. if (updateFields.length === 0) {
  28. return res.status(400).json({ error: 'No fields to update' });
  29. }
  30. values.push(id);
  31. const query = `
  32. UPDATE jobs
  33. SET ${updateFields.join(', ')}, updated_at = NOW()
  34. WHERE id = ?
  35. `;
  36. const [result] = await pool.query(query, values);
  37. if (result.affectedRows === 0) {
  38. return res.status(404).json({ error: 'Job not found' });
  39. }
  40. // 返回更新后的职位信息
  41. const [updatedJob] = await pool.query('SELECT * FROM jobs WHERE id = ?', [id]);
  42. res.json(updatedJob[0]);
  43. } catch (error) {
  44. console.error('Update job error:', error);
  45. res.status(500).json({ error: 'Database error' });
  46. }
  47. });
  48. // 获取所有职位
  49. router.get('/', async (req, res) => {
  50. try {
  51. const [jobs] = await pool.query(`
  52. SELECT id, title, department, location, description, status,
  53. pending_resumes AS pendingResumes,
  54. passed_resumes AS passedResumes,
  55. ai_criteria AS aiCriteria,
  56. created_at AS createdAt,
  57. updated_at AS updatedAt
  58. FROM jobs
  59. WHERE status != 'deleted'
  60. ORDER BY created_at DESC
  61. `);
  62. // 安全解析 aiCriteria
  63. const processedJobs = jobs.map(job => ({
  64. ...job,
  65. aiCriteria: safeParseJSON(job.aiCriteria)
  66. }));
  67. res.json(processedJobs);
  68. } catch (error) {
  69. console.error('Get jobs error:', error);
  70. res.status(500).json({ error: 'Server error' });
  71. }
  72. });
  73. // 更新职位信息
  74. router.put('/:id', async (req, res) => {
  75. const { id } = req.params;
  76. const updates = req.body;
  77. // 验证必要字段
  78. if (!updates || Object.keys(updates).length === 0) {
  79. return res.status(400).json({
  80. error: '请求体不能为空',
  81. example: {
  82. title: "新职位标题",
  83. description: "职位描述"
  84. }
  85. });
  86. }
  87. try {
  88. // 只允许更新特定字段
  89. const allowedFields = ['title', 'description', 'department', 'location', 'status'];
  90. const updateFields = [];
  91. const values = [];
  92. for (const [key, value] of Object.entries(updates)) {
  93. if (allowedFields.includes(key)) {
  94. updateFields.push(`${key} = ?`);
  95. values.push(value);
  96. }
  97. }
  98. if (updateFields.length === 0) {
  99. return res.status(400).json({
  100. error: '没有有效的字段可更新',
  101. allowedFields
  102. });
  103. }
  104. values.push(id);
  105. const query = `
  106. UPDATE jobs
  107. SET ${updateFields.join(', ')}, updated_at = NOW()
  108. WHERE id = ?
  109. `;
  110. const [result] = await pool.query(query, values);
  111. if (result.affectedRows === 0) {
  112. return res.status(404).json({
  113. error: '职位不存在或未更改',
  114. jobId: id
  115. });
  116. }
  117. // 返回更新后的数据
  118. const [updatedJob] = await pool.query(
  119. 'SELECT * FROM jobs WHERE id = ?',
  120. [id]
  121. );
  122. res.json({
  123. success: true,
  124. data: updatedJob[0]
  125. });
  126. } catch (error) {
  127. console.error('数据库更新错误:', error);
  128. res.status(500).json({
  129. error: '更新职位信息失败',
  130. details: process.env.NODE_ENV === 'development' ? error.message : null
  131. });
  132. }
  133. });
  134. // 创建新职位(修正版)
  135. router.post('/', async (req, res) => {
  136. const connection = await pool.getConnection();
  137. try {
  138. await connection.beginTransaction();
  139. const {
  140. title,
  141. department,
  142. location,
  143. description = '', // 默认空字符串
  144. status = 'draft',
  145. pendingResumes = 0, // 添加默认值
  146. passedResumes = 0, // 添加默认值
  147. aiCriteria = {} // 默认空对象
  148. } = req.body;
  149. // 直接插入,使用数据库自增ID
  150. const [result] = await connection.query(
  151. `INSERT INTO jobs (
  152. title, department, location, description, status,
  153. pending_resumes, passed_resumes, ai_criteria,
  154. created_at, updated_at
  155. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
  156. [
  157. title,
  158. department,
  159. location,
  160. description,
  161. status,
  162. pendingResumes,
  163. passedResumes,
  164. JSON.stringify(aiCriteria),
  165. new Date(),
  166. new Date()
  167. ]
  168. );
  169. // 获取新创建的记录
  170. const [newJob] = await connection.query(
  171. 'SELECT * FROM jobs WHERE id = ?',
  172. [result.insertId]
  173. );
  174. await connection.commit();
  175. res.status(201).json({
  176. ...newJob[0],
  177. id: newJob[0].id, // 保持原始数值ID
  178. pendingResumes: newJob[0].pending_resumes,
  179. passedResumes: newJob[0].passed_resumes,
  180. aiCriteria: safeParseJSON(newJob[0].ai_criteria),
  181. createdAt: newJob[0].created_at,
  182. updatedAt: newJob[0].updated_at
  183. });
  184. } catch (error) {
  185. await connection.rollback();
  186. console.error('Create job error:', error);
  187. res.status(500).json({
  188. error: 'Failed to create job',
  189. details: process.env.NODE_ENV === 'development' ? {
  190. message: error.message,
  191. stack: error.stack
  192. } : undefined
  193. });
  194. } finally {
  195. connection.release();
  196. }
  197. });
  198. router.delete('/:id', async (req, res) => {
  199. const { id } = req.params;
  200. try {
  201. // 使用软删除,将状态标记为deleted
  202. const [result] = await pool.query(
  203. `UPDATE jobs SET status = 'deleted', updated_at = NOW() WHERE id = ? AND status != 'deleted'`,
  204. [id]
  205. );
  206. if (result.affectedRows === 0) {
  207. return res.status(404).json({
  208. success: false,
  209. error: '职位不存在或已被删除'
  210. });
  211. }
  212. // 成功删除返回204 No Content
  213. res.status(204).end();
  214. } catch (error) {
  215. console.error('删除职位失败:', error);
  216. res.status(500).json({
  217. success: false,
  218. error: '删除职位失败',
  219. details: process.env.NODE_ENV === 'development' ? error.message : undefined
  220. });
  221. }
  222. });
  223. // 更新职位
  224. router.put('/:id/status', async (req, res) => {
  225. const { id } = req.params;
  226. const { status } = req.body;
  227. // 验证状态值
  228. if (!['active', 'paused'].includes(status)) {
  229. return res.status(400).json({
  230. success: false,
  231. error: '无效的状态值',
  232. allowed: ['active', 'paused']
  233. });
  234. }
  235. try {
  236. const [result] = await pool.query(
  237. `UPDATE jobs SET status = ?, updated_at = NOW() WHERE id = ?`,
  238. [status, id]
  239. );
  240. if (result.affectedRows === 0) {
  241. return res.status(404).json({
  242. success: false,
  243. error: '职位不存在'
  244. });
  245. }
  246. res.json({
  247. success: true,
  248. message: '状态更新成功'
  249. });
  250. } catch (error) {
  251. console.error('状态更新失败:', error);
  252. res.status(500).json({
  253. success: false,
  254. error: '服务器错误'
  255. });
  256. }
  257. });
  258. // 触发职位筛选
  259. router.post('/:id/screen', async (req, res) => {
  260. try {
  261. const { id } = req.params;
  262. // 获取职位信息
  263. const [jobs] = await pool.query(
  264. 'SELECT * FROM jobs WHERE id = ?',
  265. [id]
  266. );
  267. if (jobs.length === 0) {
  268. return res.status(404).json({ error: 'Job not found' });
  269. }
  270. const job = jobs[0];
  271. // 使用安全解析方法
  272. const aiCriteria = safeParseJSON(job.ai_criteria);
  273. if (!aiCriteria) {
  274. return res.status(400).json({ error: 'Invalid AI criteria format' });
  275. }
  276. // 这里应该调用AI筛选逻辑
  277. // 模拟筛选过程
  278. await new Promise(resolve => setTimeout(resolve, 2000));
  279. // 更新筛选结果
  280. const [candidates] = await pool.query(
  281. 'SELECT id FROM candidates WHERE job_id = ?',
  282. [id]
  283. );
  284. const passedCount = Math.floor(candidates.length * 0.6);
  285. await pool.query(
  286. `UPDATE jobs
  287. SET pending_resumes = ?, passed_resumes = ?, updated_at = ?
  288. WHERE id = ?`,
  289. [candidates.length, passedCount, new Date(), id]
  290. );
  291. // 更新候选人状态
  292. await pool.query(
  293. `UPDATE candidates
  294. SET status = CASE WHEN RAND() < 0.6 THEN 'passed' ELSE 'rejected' END
  295. WHERE job_id = ? AND status = 'pending'`,
  296. [id]
  297. );
  298. const [updatedJob] = await pool.query(
  299. 'SELECT * FROM jobs WHERE id = ?',
  300. [id]
  301. );
  302. res.json({
  303. ...updatedJob[0],
  304. pendingResumes: updatedJob[0].pending_resumes,
  305. passedResumes: updatedJob[0].passed_resumes,
  306. aiCriteria: safeParseJSON(updatedJob[0].ai_criteria),
  307. createdAt: updatedJob[0].created_at,
  308. updatedAt: updatedJob[0].updated_at
  309. });
  310. } catch (error) {
  311. console.error('Screen job error:', error);
  312. res.status(500).json({ error: 'Server error' });
  313. }
  314. });
  315. module.exports = router;