import request from './request' // ==================== 管理员后台 API ==================== /** * 获取管理后台统计数据 * @returns {Promise} { totalAccounts, activatedAccounts, submissionsCount, ... } */ export function getStats(params = {}) { return request.get('/admin/stats', { params }) } /** * 获取账号列表(含筛选) * @param {object} params - { page, pageSize, keyword, group, status } * @returns {Promise} { list, total, page, pageSize } */ export function getAccounts(params) { return request.get('/admin/accounts', { params }) } /** * 获取小组(团队)激活状态清单(R84 续六) * @param {object} params - { group, activated, includeTest } * @returns {Promise} { list, summary, filteredTotal, filters } */ export function getTeams(params) { return request.get('/admin/teams', { params }) } /** * 导入 Excel 批量创建账号 * @param {FormData} formData - 包含 Excel 文件的 FormData * @returns {Promise} */ export function importExcel(formData) { // 注意:不要手动设置 Content-Type。axios/浏览器在发送 FormData 时会自动补上 // `multipart/form-data; boundary=...`;手动写死 'multipart/form-data'(无 boundary) // 会导致后端 getFileStream 解析失败(Missing boundary)-> 500,且永不落库。 // // silent: true —— 抑制响应拦截器的通用错误提示。本接口的**失败提示由调用方负责**: // 后端刻意只回传纯错误(admin.js importExcel catch 注释:「前端已统一加 // 「Excel 导入失败:」前缀,这里只回传纯错误,避免重复前缀」),故此处必须静默, // 否则会与 Admin.vue 的「Excel 导入失败:<原因>」构成同族双弹。 return request.post('/admin/import-excel', formData, { timeout: 60000, silent: true, }) } /** * 获取参赛作品列表(按组别) * @param {string} group - 组别标识 * @returns {Promise} 作品列表 */ export function getSubmissions(group) { return request.get(`/admin/submissions/${group}`) } /** * 获取比赛时间配置 * @returns {Promise} */ export function getTimes() { return request.get('/admin/times') } /** * 设置比赛时间 * @param {Object} data - { zhongzhi: {start, end}, gaozhi: ... } * @returns {Promise} */ export function setTimes(data) { // 后端要求 times 数组,格式:[{ group, startTime, endTime, deadline, choiceCount, judgeCount }] // 注意:必须携带 choiceCount / judgeCount,否则后端按缺省 50/20 处理,后台「抽取数量」配置无法落库 const times = Object.entries(data).map(([group, item]) => ({ group, startTime: item.start, endTime: item.end, deadline: item.deadline, choiceCount: item.choiceCount != null ? parseInt(item.choiceCount, 10) : 50, judgeCount: item.judgeCount != null ? parseInt(item.judgeCount, 10) : 20, })) return request.put('/admin/times', { times }) } /** * 设置评分 * @param {string} group - 组别标识 * @param {object} data - 评分数据 * @returns {Promise} */ export function setScores(group, data) { return request.put(`/admin/scores/${group}`, data) } /** * 获取考题列表(管理员,含答案,按题型分表返回) * @param {string} group - 组别标识 * @returns {Promise} { choice: [...], judge: [...], word: [...] } */ export function getQuestions(group) { return request.get(`/admin/exam/${group}/questions`) } /** * 获取作品管理统一列表(覆盖全部 4 个组别) * @param {string} group - 组别标识 * @returns {Promise} { list: [{ contestantId, name, idNumber, phone, group, files: [...] }] } */ export function getWorks(group) { return request.get(`/admin/works/${group}`) } /** * 批量删除题目(按题型) * @param {string} group - 组别标识 * @param {string} questionType - choice / judge / word * @param {number[]} ids - 题目 ID 数组 * @returns {Promise} */ export function bulkDeleteQuestions(group, questionType, ids) { return request.put(`/admin/exam/${group}/questions`, { question_type: questionType, action: 'bulkDelete', question_uids: ids, }) } /** * 设置考题(按题型分表管理) * @param {string} group - 组别标识 * @param {string} questionType - 题型:choice / judge / word * @param {object} data - 考题数据 { questions: [...], action: 'add'|'update'|'delete' } * @returns {Promise} */ export function setQuestions(group, questionType, data) { return request.put(`/admin/exam/${group}/questions`, { question_type: questionType, ...data, }) } /** * 客观题 Excel 批量导入(单工作表:题型/题目/选项A~D/正确答案/分值) * @param {string} group - 组别标识 * @param {FormData} formData - 包含 Excel 文件的 FormData(字段名 file) * @returns {Promise} { imported, skipped, errors } */ export function importQuestionsExcel(group, formData) { // 同上:不要手动设置 Content-Type,避免缺失 boundary 导致后端解析 500。 // silent: true —— 同 importExcel:失败提示由调用方(Admin.vue onExcelFileChange) // 以「Excel 导入失败:<原因>」形式统一给出,此处静默以避免同族双弹。 return request.post(`/admin/exam/${group}/questions/import-excel`, formData, { timeout: 120000, silent: true, }) } /** * 综合大题 Excel 批量导入(R73) * 首 sheet 为数据表(表头在第 1 行),可携带「填写说明」sheet * @param {string} group - 组别标识(zhongzhi / gaozhi) * @param {FormData} formData - 包含 Excel 文件的 FormData(字段名 file) * @returns {Promise} { imported, skipped, errors: [{ row, message }] } */ export function importBigQuestionsExcel(group, formData) { // silent: true —— 与 importQuestionsExcel 同口径:错误提示由调用方统一给出,避免同族双弹 return request.post(`/admin/big-questions/${group}/import-excel`, formData, { timeout: 300000, silent: true, }) } /** * 上传 Word 答题模板 * @param {string} group - 组别标识 * @param {FormData} formData - 包含 Word 文件的 FormData * @returns {Promise} */ export function uploadWordTemplate(group, formData) { return request.put(`/admin/exam/${group}/word-template`, formData, { timeout: 30000, }) } /** * 获取 Word 模板信息 * @param {string} group - 组别标识 * @returns {Promise} */ export function getWordTemplate(group) { return request.get(`/admin/exam/${group}/word-template`) } /** * 删除 Word 模板 * @param {string} group - 组别标识 * @returns {Promise} */ export function deleteWordTemplate(group) { return request.delete(`/admin/exam/${group}/word-template`) } // ==================== 备赛资料管理 ==================== /** * 获取备赛资料列表 * @param {string} group - 组别 * @returns {Promise} */ export function getMaterials(group) { return request.get(`/admin/materials/${group}`) } /** * 上传备赛资料 * @param {string} group - 组别 * @param {FormData} formData - 文件 * @returns {Promise} */ export function uploadMaterial(group, formData) { return request.post(`/admin/materials/${group}`, formData, { timeout: 60000 }) } /** * 删除备赛资料 * @param {string} group - 组别 * @param {string} fileName - 文件名 * @returns {Promise} */ export function deleteMaterial(group, fileName) { return request.delete(`/admin/materials/${group}/${encodeURIComponent(fileName)}`) } // ==================== 成绩管理 ==================== /** * 获取某组成绩列表(管理员) * @param {string} group - 组别标识 * @returns {Promise} 成绩列表 */ export function getScores(group) { return request.get(`/admin/scores/${group}`) } /** * 发布/撤回指定组别的成绩 * * 发布状态按组独立存储:发布本科组不会让中职组考生看到成绩。 * 不传 group 时对全部组生效(批量操作)。 * @param {string} group - 组别标识 * @param {boolean} publish - true: 发布, false: 撤回 * @returns {Promise} */ export function publishScores(group, publish) { return request.put('/admin/publish-scores', group ? { publish, group } : { publish }) } /** * 获取成绩发布状态 * @param {string} group - 组别标识;不传时 published 表示「全部组均已发布」 * @returns {Promise} { published, group, groups: { zhongzhi, gaozhi, benke, jiaoshi } } */ export function getPublishStatus(group) { return request.get('/admin/publish-scores', group ? { params: { group } } : undefined) } // ==================== 新闻管理 ==================== /** * 获取新闻列表(管理员) * @returns {Promise} */ export function listNews() { return request.get('/admin/news') } /** * 创建新闻 * @param {object} data - { title, content, category, coverImage } * @returns {Promise} */ export function createNews(data) { return request.post('/admin/news', data) } /** * 更新新闻 * @param {number|string} id - 新闻 ID * @param {object} data - 更新的字段 * @returns {Promise} */ export function updateNews(id, data) { return request.put(`/admin/news/${id}`, data) } /** * 删除新闻 * @param {number|string} id - 新闻 ID * @returns {Promise} */ export function deleteNews(id) { return request.delete(`/admin/news/${id}`) } /** * 更新新闻排序 * @param {Array<{id: number, sort_order: number}>} orders * @returns {Promise} */ export function updateNewsOrder(orders) { return request.put('/admin/news/order', { orders }) } // ==================== 轮播图管理 ==================== /** * 获取轮播图列表(管理员) * @returns {Promise} */ export function listCarousel() { return request.get('/admin/carousel') } /** * 添加轮播图 * @param {object} data - { imageUrl, linkUrl, sortOrder, title } * @returns {Promise} */ export function addCarousel(data) { return request.post('/admin/carousel', data) } /** * 更新轮播图 * @param {number|string} id - 轮播图 ID * @param {object} data - 更新的字段 * @returns {Promise} */ export function updateCarousel(id, data) { return request.put(`/admin/carousel/${id}`, data) } /** * 删除轮播图 * @param {number|string} id - 轮播图 ID * @returns {Promise} */ export function deleteCarousel(id) { return request.delete(`/admin/carousel/${id}`) } /** * 批量更新轮播图排序 * @param {Array} orders - [{ id, sort_order }] * @returns {Promise} */ export function updateCarouselOrder(orders) { return request.put('/admin/carousel/order', { orders }) } // ==================== 测试数据标记与重置 ==================== /** * 切换选手的测试数据标记 * @param {number} id - 选手ID * @param {number} testFlag - 0=生产数据, 1=测试数据 * @returns {Promise} */ export function toggleTestFlag(id, testFlag) { return request.post('/admin/toggle-test-flag', { id, test_flag: testFlag }) } /** * 重置测试数据(仅重置 test_flag=1 的选手及其提交记录) * * returnMessage: true —— 保留后端 message(不再被响应拦截器丢弃)。 * 后端提示含「已恢复为『未激活 + 默认口令 X』,需先用默认口令完成短信激活即可登录重测」 * 这一关键指引;前端自拼文案曾写成「可直接用默认口令登录重测」, * 与后端 activated:false 自相矛盾(用户反馈 Req2)。以后端文案为唯一真相源。 * @returns {Promise<{code:number,message:string,data:object}>} */ export function resetTestData() { // silent: true —— 抑制响应拦截器(request.js)的通用错误提示。 // 本调用方在 Admin.vue 的 catch 里已按上下文弹一次「重置失败:<后端原文>」, // 若不再静默,同一 403(生产环境禁止重置测试数据)会被拦截器与 catch 各弹一次, // 用户会看到两条完全相同的报错(R58 用户反馈)。 return request.post('/admin/reset-test-data', null, { returnMessage: true, silent: true }) } // ==================== 综合大题管理(中职/高职) ==================== /** * 获取综合大题题库(按组别) * * 仅中职/高职启用;本科/教师组调用返回 400(未启用综合大题)。 * @param {string} group - 组别标识(zhongzhi / gaozhi) * @returns {Promise} { * list: [{ id, questionUid, group, title, description, * subPrompt1, subPrompt2, subPrompt3, * scorePerSub, totalScore, sortOrder, status, createdAt, updatedAt }], * total, scorePerSub, subCount, maxChars * } */ export function getBigQuestions(group) { return request.get(`/admin/big-questions/${group}`) } /** * 新增大题 * * 题干 description 为富文本 HTML(支持表格);三小问提示 subPrompt1~3 与题干同存一行。 * @param {string} group - 组别标识 * @param {object} data - { title, description, subPrompt1, subPrompt2, subPrompt3, scorePerSub, sortOrder, status } * @returns {Promise} { id, questionUid } */ export function createBigQuestion(group, data) { return request.post(`/admin/big-questions/${group}`, data) } /** * 更新大题 * * 注意:已被考生抽取的题目仍可修改,但考生端展示的是抽题时刻的题干快照, * 不会受此修改影响(防纠纷设计)。 * @param {string} group - 组别标识 * @param {number|string} id - 大题 ID * @param {object} data - 同 createBigQuestion * @returns {Promise} { id } */ export function updateBigQuestion(group, id, data) { return request.put(`/admin/big-questions/${group}/${id}`, data) } /** * 删除大题 * * 已被考生抽取的题目服务端拒绝删除(返回 409),避免作答记录悬空。 * @param {string} group - 组别标识 * @param {number|string} id - 大题 ID * @returns {Promise} { deleted } */ export function deleteBigQuestion(group, id) { return request.delete(`/admin/big-questions/${group}/${id}`) } /** * 获取大题作答与查证列表(覆盖该组全部考生,含未抽题者) * * 每行含考生信息、抽题快照、三小问答案、已答数量、三小问得分、大题总分与提交时间, * 供管理员核对考生实际抽到的题目与作答内容,避免考生纠纷。 * @param {string} group - 组别标识 * @returns {Promise} { * list: [{ contestantId, aggregationUid, name, idNumber, phone, teamId, * drawn, bigQuestionUid, questionSnapshot, * answer1, answer2, answer3, answeredCount, * score1, score2, score3, bigScore, wordScore, submitTime }], * total, drawnCount * } */ export function getBigAnswers(group) { return request.get(`/admin/big-answers/${group}`) } /** * 录入三小问得分(小题分) * * 服务端逐问校验 0 ~ scorePerSub 区间,累加后经唯一评分写路径 * service.admin.updateScores 落 contestants.word_score(中职/高职总分 = exam_score + word_score)。 * @param {string} group - 组别标识 * @param {number|string} uid - 考生聚合 ID(aggregation_uid) * @param {{score_1: number, score_2: number, score_3: number}} scores * @returns {Promise} { total, perSubScore } */ export function setBigAnswerScores(group, uid, scores) { return request.put(`/admin/big-answers/${group}/${uid}/scores`, scores) } // ==================== 成绩复核 ==================== // 场景:考生对成绩有异议 → 按「姓名 / 身份证号 / 电话」任一检索 → 核对全卷题目与作答。 /** * 检索考生(姓名 / 身份证号 / 电话 任一,模糊匹配) * * 不传 group 则全组别检索 —— 复核时通常只拿到一个身份证号,不应强迫先猜组别。 * @param {string} keyword - 检索词 * @param {string} [group] - 可选:限定组别 * @returns {Promise} { list: [{ contestantId, name, idNumber, phone, group, objectiveApplicable }], total, truncated } */ export function reviewSearch(keyword, group) { const params = { keyword } if (group) params.group = group return request.get('/admin/review/search', { params }) } /** * 考生全卷明细(复核) * * 返回客观题逐题(题干 / 选项 / 考生答案 / **正确答案** / 对错 / 得分 / 满分) * + 综合大题(题干快照 + 三小问作答与得分)+ 本科/教师组作品文件清单。 * ⚠️ 含正确答案,仅管理员可用。 * @param {string} group - 组别标识(须与考生实际组别一致) * @param {number|string} contestantId - 考生 ID(contestants.id) * @returns {Promise} { name, idNumber, phone, group, objectiveApplicable, scorePublished, * objective: [{ seq, questionType, questionUid, question, options, correctAnswer, maxScore, * answer, answered, isCorrect, score, answerMatches, questionMissing }], * summary: { answered, total, correctCount, autoScore, wordScore, totalScore }, * bigQuestion, works, warnings, matchedKeys, questionOrderFixed } */ export function getContestantScore(group, contestantId) { return request.get(`/admin/review/score/${group}/${contestantId}`) }