from pathlib import Path from docx import Document from docx.enum.section import WD_SECTION from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Inches, Pt, RGBColor ROOT = Path('/Users/smart/Documents/TRAE培训/TRAE课件/exams') TARGET = ROOT / 'TRAE编程培训考试评估卷-实操与项目实战(阅卷参考答案).docx' FONT = 'Microsoft YaHei' BODY = 11 BLUE = '2E74B5' DARK_BLUE = '1F4D78' HEADER_FILL = 'E8EEF5' LIGHT_FILL = 'F4F6F9' TABLE_WIDTH = 9360 def set_font(run, size=BODY, bold=False, color=None, italic=False): run.font.name = FONT run._element.rPr.rFonts.set(qn('w:ascii'), 'Calibri') run._element.rPr.rFonts.set(qn('w:hAnsi'), 'Calibri') run._element.rPr.rFonts.set(qn('w:eastAsia'), FONT) run.font.size = Pt(size) run.bold = bold run.italic = italic if color: run.font.color.rgb = RGBColor.from_string(color) def shade(cell, color): tc_pr = cell._tc.get_or_add_tcPr() shd = OxmlElement('w:shd') shd.set(qn('w:fill'), color) tc_pr.append(shd) def set_cell_margins(cell, top=80, start=120, bottom=80, end=120): tc_pr = cell._tc.get_or_add_tcPr() tc_mar = tc_pr.first_child_found_in('w:tcMar') if tc_mar is None: tc_mar = OxmlElement('w:tcMar') tc_pr.append(tc_mar) for side, value in [('top', top), ('start', start), ('bottom', bottom), ('end', end)]: element = tc_mar.find(qn(f'w:{side}')) if element is None: element = OxmlElement(f'w:{side}') tc_mar.append(element) element.set(qn('w:w'), str(value)) element.set(qn('w:type'), 'dxa') def set_table_geometry(table, widths): table.autofit = False tbl_pr = table._tbl.tblPr tbl_w = tbl_pr.first_child_found_in('w:tblW') tbl_w.set(qn('w:w'), str(sum(widths))) tbl_w.set(qn('w:type'), 'dxa') tbl_ind = tbl_pr.first_child_found_in('w:tblInd') if tbl_ind is None: tbl_ind = OxmlElement('w:tblInd') tbl_pr.append(tbl_ind) tbl_ind.set(qn('w:w'), '120') tbl_ind.set(qn('w:type'), 'dxa') grid_cols = table._tbl.tblGrid.gridCol_lst for grid_col, width in zip(grid_cols, widths): grid_col.set(qn('w:w'), str(width)) for row in table.rows: for cell, width in zip(row.cells, widths): tc_w = cell._tc.tcPr.tcW tc_w.set(qn('w:w'), str(width)) tc_w.set(qn('w:type'), 'dxa') cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER set_cell_margins(cell) def add_paragraph(doc, text='', style=None, before=0, after=6, line=1.1, color=None, bold=False, size=BODY): paragraph = doc.add_paragraph(style=style) paragraph.paragraph_format.space_before = Pt(before) paragraph.paragraph_format.space_after = Pt(after) paragraph.paragraph_format.line_spacing = line run = paragraph.add_run(text) set_font(run, size=size, bold=bold, color=color) return paragraph def add_heading(doc, text, level): sizes = {1: 16, 2: 13, 3: 12} colors = {1: BLUE, 2: BLUE, 3: DARK_BLUE} before = {1: 16, 2: 12, 3: 8} after = {1: 8, 2: 6, 3: 4} paragraph = doc.add_paragraph() paragraph.paragraph_format.space_before = Pt(before[level]) paragraph.paragraph_format.space_after = Pt(after[level]) paragraph.paragraph_format.keep_with_next = True run = paragraph.add_run(text) set_font(run, size=sizes[level], bold=True, color=colors[level]) return paragraph def add_table(doc, headers, rows, widths): table = doc.add_table(rows=1, cols=len(headers)) table.style = 'Table Grid' table.rows[0]._tr.get_or_add_trPr().append(OxmlElement('w:tblHeader')) for cell, text in zip(table.rows[0].cells, headers): shade(cell, HEADER_FILL) paragraph = cell.paragraphs[0] paragraph.paragraph_format.space_after = Pt(0) run = paragraph.add_run(text) set_font(run, size=10, bold=True) for values in rows: cells = table.add_row().cells for cell, text in zip(cells, values): paragraph = cell.paragraphs[0] paragraph.paragraph_format.space_after = Pt(0) paragraph.paragraph_format.line_spacing = 1.08 run = paragraph.add_run(text) set_font(run, size=9.5) set_table_geometry(table, widths) doc.add_paragraph().paragraph_format.space_after = Pt(2) return table def add_callout(doc, label, text): table = doc.add_table(rows=1, cols=1) table.style = 'Table Grid' shade(table.cell(0, 0), LIGHT_FILL) paragraph = table.cell(0, 0).paragraphs[0] paragraph.paragraph_format.space_after = Pt(0) label_run = paragraph.add_run(f'{label}:') set_font(label_run, bold=True, color=DARK_BLUE) text_run = paragraph.add_run(text) set_font(text_run) set_table_geometry(table, [TABLE_WIDTH]) doc.add_paragraph().paragraph_format.space_after = Pt(2) def build_document(): doc = Document() section = doc.sections[0] section.top_margin = Inches(1) section.bottom_margin = Inches(1) section.left_margin = Inches(1) section.right_margin = Inches(1) section.header_distance = Inches(0.492) section.footer_distance = Inches(0.492) styles = doc.styles normal = styles['Normal'] normal.font.name = 'Calibri' normal._element.rPr.rFonts.set(qn('w:eastAsia'), FONT) normal.font.size = Pt(BODY) normal.paragraph_format.space_after = Pt(6) normal.paragraph_format.line_spacing = 1.1 header = section.header.paragraphs[0] header.alignment = WD_ALIGN_PARAGRAPH.RIGHT header.paragraph_format.space_after = Pt(0) set_font(header.add_run('TRAE 编程培训 | 阅卷参考答案'), size=9, color='6B7280') footer = section.footer.paragraphs[0] footer.alignment = WD_ALIGN_PARAGRAPH.CENTER footer.paragraph_format.space_after = Pt(0) set_font(footer.add_run('阅卷专用 - 不随学生试卷发放'), size=9, color='6B7280') title = doc.add_paragraph() title.paragraph_format.space_before = Pt(16) title.paragraph_format.space_after = Pt(4) set_font(title.add_run('TRAE 编程培训考试评估卷'), size=23, bold=True, color='0B2545') subtitle = doc.add_paragraph() subtitle.paragraph_format.space_after = Pt(16) set_font(subtitle.add_run('实操与项目实战 - 阅卷参考答案'), size=14, color='475569') for label, value in [ ('适用试卷', '系统用户状态管理综合卷(修订版)'), ('固定源码', 'GitHub jeecgboot/JeecgBoot,v3.9.2'), ('固定提交', '7df07a823fd558be857d0208ccae96342539fbc1'), ('阅卷原则', '考查证据与判断,不要求逐字复述;禁止把未执行测试写成已通过。'), ]: paragraph = doc.add_paragraph() paragraph.paragraph_format.space_after = Pt(2) set_font(paragraph.add_run(f'{label}:'), bold=True, color='1F4D78') set_font(paragraph.add_run(value)) add_callout(doc, '使用说明', '本答案与 exam-kit/reference-answer.md 内容对应。学生提交可以有不同表述,但必须给出等价的源码证据、边界判断和如实的测试状态。') add_heading(doc, 'A 卷参考答案', 1) add_heading(doc, '任务 1:Prompt 改写', 2) add_paragraph(doc, '合格 Prompt 必须明确固定提交、分析范围、状态白名单目标、不可改动的保护项、输出结构和验收证据。可按以下六项核查:', after=4) add_table(doc, ['项目', '参考要求'], [ ('背景与目标', '在固定版本中审查批量冻结/解冻,保证 status 仅允许 1/2 且在写库前校验。'), ('范围', 'SysUserController.frozenBatch、ISysUserService.updateStatus、SysUserServiceImpl.updateStatus、状态常量及直接关联的保护。'), ('约束', '不得改接口路径、system:user:frozenBatch 权限、管理员保护和缓存失效;事实、推断、待确认分开。'), ('输出', '调用链与证据、最小改造、测试矩阵、待确认项。'), ('验收', '每项结论写文件路径和符号;未执行测试必须如实标注。'), ], [1800, 7560]) add_heading(doc, '任务 2:AI 输出辨析', 2) add_callout(doc, '正确结论', '“页面已限制状态值,因此 Service 不需要校验”是错误结论。页面不是可信边界,调用者可直接请求接口;Service.updateStatus 在原版中直接解析 status 并更新数据库。') add_table(doc, ['应说明的点', '参考答案'], [ ('正确校验位置', 'SysUserServiceImpl.updateStatus 内,在 Integer.parseInt(status) 与 userMapper.update 之前。'), ('允许值', 'CommonConstant.USER_UNFREEZE(1)和 USER_FREEZE(2)。'), ('拒绝值', 'null、空串、空白串、3 及其他非 1/2 值,抛 JeecgBootException。'), ('保留项', 'Controller 权限、管理员保护、@CacheEvict 和原有数据更新路径。'), ], [1800, 7560]) add_heading(doc, 'B 卷参考答案', 1) add_heading(doc, '任务 1:项目理解与影响分析', 2) add_table(doc, ['结论', '文件路径与符号', '证明内容', '类型'], [ ('接口与权限', 'SysUserController.java / frozenBatch', '@RequiresPermissions("system:user:frozenBatch");PUT /sys/user/frozenBatch。', '事实'), ('调用链', 'SysUserController.java / frozenBatch', '读取 ids,先做 checkUserAdminRejectDel,再读取 status,逐个调用 updateStatus。', '事实'), ('服务契约', 'ISysUserService.java / updateStatus', '状态更新是 Service 接口。', '事实'), ('写库与缓存', 'SysUserServiceImpl.java / updateStatus', '@CacheEvict;Integer.parseInt(status) 后调用 userMapper.update。', '事实'), ('状态枚举', 'CommonConstant.java / USER_UNFREEZE、USER_FREEZE、USER_QUIT', '取值为 1、2、3;本题只允许 1、2。', '事实'), ('管理员保护', 'SysUserServiceImpl.java / checkUserAdminRejectDel', '检测目标 ID 中的 admin 用户并抛业务异常。', '事实'), ('登录影响', 'SysUserServiceImpl.java / 用户登录有效性检查', '状态为 USER_FREEZE 时返回“该用户已冻结”。', '事实'), ], [1500, 3000, 3600, 1260]) add_callout(doc, '边界判断', 'Service 白名单必须早于解析和 mapper 写入。异常返回时不完成该次状态更新;缓存失效注解、接口权限和管理员保护都不是此变更的替代项。批量原子性与跨节点缓存可列为待确认,不能直接写成事实。') add_heading(doc, '任务 2:最小改造与测试安全网', 2) add_paragraph(doc, '参考改造:在 SysUserServiceImpl.updateStatus 方法开头增加状态白名单,保持已有 @CacheEvict 和 mapper 写入。', after=3) code_table = doc.add_table(rows=1, cols=1) code_table.style = 'Table Grid' shade(code_table.cell(0, 0), 'F8FAFC') code = 'if (!String.valueOf(CommonConstant.USER_UNFREEZE).equals(status)\n && !String.valueOf(CommonConstant.USER_FREEZE).equals(status)) {\n throw new JeecgBootException("账号状态仅支持冻结或解冻");\n}' paragraph = code_table.cell(0, 0).paragraphs[0] paragraph.paragraph_format.space_after = Pt(0) set_font(paragraph.add_run(code), size=9.5) set_table_geometry(code_table, [TABLE_WIDTH]) doc.add_paragraph().paragraph_format.space_after = Pt(2) add_table(doc, ['场景', '预期', '验证口径'], [ ('null、空串、空白串、3', '抛 JeecgBootException;不写库。', '负例单元测试;未跑则写明原因和计划。'), ('"1" 解冻', '更新状态 1,成功后缓存失效。', 'Service 单元测试或集成测试。'), ('"2" 冻结', '更新状态 2,成功后缓存失效。', 'Service 单元测试或集成测试。'), ('无权限调用', '被权限框架拒绝。', '接口/集成测试,记录账号与结果。'), ('目标含 admin', '管理员保护拒绝更新。', 'Controller/接口测试。'), ('冻结后登录', '登录有效性检查返回冻结错误。', '可列待环境验证,不得虚报通过。'), ], [1950, 3660, 3750]) add_callout(doc, '测试状态', 'review-diff.patch 中的最小测试只覆盖 status 为 "3"。父 POM 默认跳过 surefire 测试;未修改本地测试配置的学员应标注未执行,而不是将测试状态写为已通过。') add_heading(doc, '任务 3:Review、PR 与资产沉淀', 2) add_table(doc, ['结论', '证据', '阅卷判断'], [ ('可合入,建议补测', '白名单校验位于 parse 和 mapper 写入前;保护项未被删除。', '认可“无阻断问题”,但必须列检查范围和残余风险。'), ('测试覆盖不足', '新增测试只断言 "3" 抛异常。', '建议补 null、空白、"1"、"2" 与 mapper 交互断言。'), ('残余风险', '异常返回语义、批量原子性、跨节点缓存并非此 diff 的直接变更。', '可列后续集成验证;不能凭空断言已验证。'), ], [1950, 4200, 3210]) add_paragraph(doc, '参考 PR 要点:背景是前端限制不能替代服务端边界;实现是在 Service 白名单化 status;测试写明实际已跑或未跑状态;风险是业务收紧会拒绝此前错误状态,必要时回退本次白名单提交。', after=4) add_callout(doc, '资产答案', '可复用资产应围绕“状态变更服务端边界”设计:输入固定提交、diff 和状态常量;检查白名单、校验顺序、权限/关键账号保护、缓存/审计、副作用和正负例;输出 Review、证据表、测试矩阵和残余风险。') add_heading(doc, 'AI 使用记录的判分口径', 1) add_paragraph(doc, '不提供统一的逐字 Prompt。可得满分的记录需要写清:使用 AI/TRAE 做了什么、建议摘要、采纳或否决、人工判断和证据/验证状态。工具名称可以不同;缺少 trae-usage-record.md 时,任务 3 的该项 2 分为 0 分。') doc.core_properties.title = 'TRAE 编程培训考试评估卷(阅卷参考答案)' doc.core_properties.subject = '系统用户状态管理综合卷阅卷参考' doc.core_properties.author = 'TRAE 培训' doc.save(TARGET) print(TARGET) if __name__ == '__main__': build_document()