Files
TARE-Learn/exams/scripts/build_reference_answer_docx.py
smart 67837df8d0 docs: 更新TRA编程培训课程材料、课件与AI技能交付包
- 新增全套考试卷、评估卷、参考答案与考试培训包模板
- 修正slidev培训课件:调整模块标题、链路图缩放,替换工单示例为通用业务对象
- 修正课堂红线表述为证据红线,将小组作业调整为个人作业要求
- 新增权限、状态流转、审计日志、看板指标四类可复用业务规则块
- 整理AI工程技能交付包,迁移旧资产至客户专属交付包并统一术语
- 新增考试输出模板与TRAE使用记录文档
2026-08-08 11:00:36 +08:00

265 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()