docs: 更新TRA编程培训课程材料、课件与AI技能交付包

- 新增全套考试卷、评估卷、参考答案与考试培训包模板
- 修正slidev培训课件:调整模块标题、链路图缩放,替换工单示例为通用业务对象
- 修正课堂红线表述为证据红线,将小组作业调整为个人作业要求
- 新增权限、状态流转、审计日志、看板指标四类可复用业务规则块
- 整理AI工程技能交付包,迁移旧资产至客户专属交付包并统一术语
- 新增考试输出模板与TRAE使用记录文档
This commit is contained in:
smart
2026-08-08 11:00:36 +08:00
parent e7d6db93c3
commit 67837df8d0
169 changed files with 6618 additions and 2887 deletions
+34
View File
@@ -0,0 +1,34 @@
# TRAE 编程培训考试包
本目录是 B 卷的唯一考试输入。评分以本目录给出的 GitHub 仓库、固定提交、指定 diff 和提交模板为准。
## 阅卷参考答案
`reference-answer.md` 仅供讲师和阅卷人员使用,不能与学生版 `exam-output/` 一同发放。它给出每题的关键证据、可接受结论、测试口径和 Review 要点;学员不必逐字复述,但结论必须能由固定版本源码或指定 diff 支撑。
## 公开源码与 diff
- GitHub 仓库:`https://github.com/jeecgboot/JeecgBoot.git`
- 固定版本:标签 `v3.9.2`,提交 `7df07a823fd558be857d0208ccae96342539fbc1`
- 指定变更:`review-diff.patch`,仅包含两处文件变更。
考试开始前完成源码下载;下载时间不计入作答时间。建议命令:
```bash
git clone --depth 1 --branch v3.9.2 https://github.com/jeecgboot/JeecgBoot.git
cd JeecgBoot
git rev-parse HEAD
```
输出必须为 `7df07a823fd558be857d0208ccae96342539fbc1`。版本不一致时,立即记录为环境问题并联系监考人员,不得基于其他版本作答。
## 允许与禁止
- 允许使用 TRAE、上述 GitHub 固定版本和本考试包中的材料。
- 不要求修改业务代码;不得将本次作答建立在未指定版本、历史成品或他人交付物上。
- 所有项目结论必须能回溯到固定工作区中的文件、符号或指定 diff。
- 测试只能如实标记为“已执行”或“未执行”;未执行时必须说明原因和计划验证方式。
## 提交方式
将 `exam-output/` 中的四个模板另存为个人提交目录并补全内容。不要改动文件名。
@@ -0,0 +1,21 @@
# 项目理解与影响分析
## 结论摘要
<!-- 用 3 至 5 条说明系统用户冻结/解冻链路与核心影响关系。 -->
## 证据表
| 结论 | 文件路径与符号 | 证据说明 | 判断类型 |
|---|---|---|---|
| | | | 事实 / 推断 / 待确认 |
## 影响与保护边界
<!-- 说明用户状态、冻结/解冻接口、缓存、副作用之间的关系,以及必须保持不变的行为。 -->
## 待确认项
| 待确认问题 | 原因 | 需要的证据或验证方式 |
|---|---|---|
| | | |
@@ -0,0 +1,23 @@
# 最小改造与测试安全网
## 改造目标与验收标准
## 校验位置与顺序
<!-- 写明方法、校验发生在何种状态写入或缓存副作用之前,以及依据。 -->
## 不改动项
| 不改动项 | 保持原因 | 代码依据 |
|---|---|---|
| | | |
## 测试矩阵
| 场景 | 预期结果 | 验证入口 | 执行状态 | 命令或未执行原因 |
|---|---|---|---|---|
| 空白或非法状态值 | | | 已执行 / 未执行 | |
| 正常冻结 | | | 已执行 / 未执行 | |
| 正常解冻 | | | 已执行 / 未执行 | |
| 无权限调用 | | | 已执行 / 未执行 | |
| 管理员保护与缓存/用户状态回归 | | | 已执行 / 未执行 | |
@@ -0,0 +1,31 @@
# Review、PR 与资产沉淀
## Review 结论
<!-- 可以得出“未发现阻断问题”的结论,但必须说明检查范围、证据和残余风险。 -->
| 等级或结论 | 证据 | 风险 | 建议的验收方式 |
|---|---|---|---|
| | | | |
## PR 说明
### 背景
### 实现
### 测试
### 风险与回退
## 可复用 Prompt
### 输入
### 约束与输出
### 验收方式
## Rule、Checklist 或 Skill 设计卡
<!-- 三选一,写清适用场景、输入、输出、边界和验收方式。 -->
@@ -0,0 +1,5 @@
# TRAE 使用记录
| Prompt 或操作 | TRAE 建议摘要 | 采纳或否决 | 人工判断点 | 证据或验证状态 |
|---|---|---|---|---|
| | | | | 已执行 / 未执行 |
+168
View File
@@ -0,0 +1,168 @@
# TRAE 编程培训考试评估卷 - 阅卷参考答案
> 阅卷专用。对应 `jeecgboot/JeecgBoot` 标签 `v3.9.2`、提交 `7df07a823fd558be857d0208ccae96342539fbc1` 及 `review-diff.patch`。学生不需要逐字复述本答案,但所有核心结论都必须有等价的源码依据。
## A 卷
### 任务 1:Prompt 改写(15 分)
下列内容是一份达到满分要求的参考 Prompt。允许更换表述,但不能省略范围、约束、交付格式和验收条件。
```text
你是负责 JeecgBoot 系统用户模块的后端工程师。请基于 GitHub 仓库
jeecgboot/JeecgBoot 的 v3.9.2(提交 7df07a823fd558be857d0208ccae96342539fbc1),
审查“批量冻结/解冻用户”的后端链路是否允许非法状态值进入数据层。
范围:只分析
1. SysUserController.frozenBatch;
2. ISysUserService.updateStatus 和 SysUserServiceImpl.updateStatus;
3. CommonConstant 中的用户状态常量;
4. 与本接口直接相关的权限、管理员保护、缓存和测试入口。
目标:给出最小改造建议,使 status 只允许 1(解冻)或 2(冻结),并在数据库写入前拒绝
空白或其他值。不要修改接口路径、权限 system:user:frozenBatch、管理员保护或缓存失效策略。
请按以下格式输出:
- 调用链和每个结论的“文件路径 + 方法/符号”;
- 变更位置、伪代码和不改动项;
- 空白/非法状态、冻结、解冻、无权限、管理员保护和缓存回归的测试矩阵;
- 仍需人工确认的问题。事实、推断和待确认项必须分别标识。
验收:每个结论均可回溯到上述固定提交;不编造测试结果;信息不足时先提出待确认项。
```
判分关键:背景与目标(3 分)、范围(2 分)、约束(4 分)、输出格式(3 分)、验收和证据要求(3 分)。只写“帮我检查有没有问题”不能得高分。
### 任务 2:AI 输出辨析(15 分)
题干中的 AI 结论“页面已限制状态值,因此 Service 不需要校验;直接调用接口也不会有问题”应判为错误,理由如下:
1. 前端限制不是服务端的可信边界。调用者可以绕过页面直接发送 `PUT /sys/user/frozenBatch` 请求。
2. `SysUserController.frozenBatch` 从 JSON 读取 `status` 后逐个调用 `sysUserService.updateStatus(id, status)`;原版 `updateStatus` 会直接执行 `Integer.parseInt(status)` 并更新数据库。
3. Service 入口是数据写入前的最后业务保护点。正确做法是在 `updateStatus` 内、`Integer.parseInt` 和 `userMapper.update` 之前,仅允许 `CommonConstant.USER_UNFREEZE`(1)和 `CommonConstant.USER_FREEZE`(2)。`null`、空串、空白串、`3` 及其他值均应抛出业务异常。
4. 不能因此删除接口权限、管理员保护或缓存注解;这些分别处理调用授权、关键账号保护和用户缓存一致性。
## B 卷任务 1:项目理解与影响分析(25 分)
### 满分结论摘要
1. 批量冻结/解冻的 HTTP 入口是 `PUT /sys/user/frozenBatch`,由 `SysUserController.frozenBatch` 处理,受 `system:user:frozenBatch` 权限保护。
2. Controller 从请求体读取 `ids`、先调用 `checkUserAdminRejectDel(ids)`,再读取 `status`,对每个非空 ID 调用 `updateStatus(id, status)`。
3. `updateStatus` 是最终写入入口:它将状态转换为整数,通过 `userMapper.update` 更新 `SysUser.status`,并用 `@CacheEvict(CacheConstant.SYS_USERS_CACHE, allEntries = true)` 失效用户缓存。
4. 用户状态常量中,`1` 表示解冻,`2` 表示冻结,`3` 表示注销;本题只允许前两种状态。
5. 冻结状态会影响登录有效性判断:服务在读取到 `USER_FREEZE` 时返回“该用户已冻结”。
### 至少应出现的证据
| 结论 | 文件路径与符号 | 证明内容 | 判断类型 |
|---|---|---|---|
| 接口、HTTP 方法和权限 | `.../controller/SysUserController.java`,`frozenBatch` | `@RequiresPermissions("system:user:frozenBatch")` 与 `@RequestMapping(... "/frozenBatch", PUT)` | 事实 |
| Controller 到 Service 调用链 | 同文件 `frozenBatch` | 读取 `ids`、管理员保护、读取 `status`,循环调用 `updateStatus` | 事实 |
| 服务契约 | `.../service/ISysUserService.java`,`updateStatus(String id, String status)` | 状态更新是服务接口的一部分 | 事实 |
| 数据库与缓存副作用 | `.../service/impl/SysUserServiceImpl.java`,`updateStatus` | `@CacheEvict`、`Integer.parseInt(status)`、`userMapper.update` | 事实 |
| 合法状态枚举 | `.../common/constant/CommonConstant.java`,`USER_UNFREEZE`、`USER_FREEZE`、`USER_QUIT` | 取值分别为 1、2、3 | 事实 |
| 管理员保护 | `.../service/impl/SysUserServiceImpl.java`,`checkUserAdminRejectDel` | 目标 ID 中含用户名 `admin` 时抛出业务异常;`frozenBatch` 会调用它 | 事实 |
| 登录影响 | `.../service/impl/SysUserServiceImpl.java`,登录有效性检查中对 `USER_FREEZE` 的分支 | 冻结用户不能正常登录 | 事实 |
### 影响和保护边界
- 非法状态若进入原版 Service,会在 `Integer.parseInt` 处异常,或在可解析但不受业务允许的值(如 `3`)场景写入不符合本题要求的状态;因此应在 Service 入口先拒绝。
- 校验位于 `userMapper.update` 前;抛出异常时不会完成本次用户状态写入。`@CacheEvict` 默认在方法成功返回后生效,因此异常路径不应把一次失败校验伪装为成功变更。
- 必须保持:接口路径和 PUT 方法、`system:user:frozenBatch` 权限、`checkUserAdminRejectDel(ids)` 调用、逐个用户更新的方式、缓存失效注解。
- 可接受的待确认项:批量操作是否要求原子性、前端是否会发送 `null` 或空白状态、缓存是否有跨节点配置。不得把这些未查证项写成事实。
## B 卷任务 2:最小改造与测试安全网(25 分)
### 改造位置和参考实现
文件:`jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java`。
方法:`updateStatus(String id, String status)`。
位置:保留已有 `@Override` 和 `@CacheEvict`,在 `Integer.parseInt(status)`、`userMapper.update(...)` 之前加入校验:
```java
if (!String.valueOf(CommonConstant.USER_UNFREEZE).equals(status)
&& !String.valueOf(CommonConstant.USER_FREEZE).equals(status)) {
throw new JeecgBootException("账号状态仅支持冻结或解冻");
}
```
该写法同时拒绝 `null`、空串、空白串、`3` 和其他非 `1`/`2` 的值;允许 `"1"` 与 `"2"`。它没有改变原有数据更新、缓存失效、权限或管理员保护路径。
### 必须明确的不改动项
| 不改动项 | 原因和代码依据 |
|---|---|
| 权限 | `SysUserController.frozenBatch` 的 `@RequiresPermissions("system:user:frozenBatch")` 仍是接口授权边界。 |
| 管理员保护 | Controller 继续先调用 `sysUserService.checkUserAdminRejectDel(ids)`;状态校验不能替代该保护。 |
| 缓存失效 | `SysUserServiceImpl.updateStatus` 的 `@CacheEvict(value = {CacheConstant.SYS_USERS_CACHE}, allEntries = true)` 保留,以保证成功修改后的用户缓存刷新。 |
| 数据写入方式 | 合法状态仍走原有 `userMapper.update(new SysUser().setStatus(...), ...)`。 |
### 参考测试矩阵
| 场景 | 预期结果 | 验证入口 | 执行状态的正确写法 |
|---|---|---|---|
| `null`、空串、空白串、`3` | 抛出 `JeecgBootException`;不调用 mapper,不成功失效缓存 | `SysUserServiceImpl.updateStatus` 单元测试 | 未实际运行时必须写“未执行”和原因;不能凭空写通过 |
| `"1"` 解冻 | 调用 mapper,写入状态 1;成功返回后缓存失效 | Service 单元测试或集成测试 | 记录实际命令和结果 |
| `"2"` 冻结 | 调用 mapper,写入状态 2;成功返回后缓存失效 | Service 单元测试或集成测试 | 记录实际命令和结果 |
| 无权限请求 | 被权限框架拒绝,不能执行状态写入 | `PUT /sys/user/frozenBatch` 的接口/集成测试 | 记录测试账号、HTTP 结果或未执行原因 |
| 目标包含 `admin` | `checkUserAdminRejectDel` 拒绝,不能更新管理员 | 接口或 Controller 集成测试 | 记录结果或未执行原因 |
| 冻结后登录 | 读取冻结用户时走“该用户已冻结”分支 | 登录集成测试 | 可标为待环境验证,不得臆造 |
`review-diff.patch` 已提供一个针对 `"3"` 的最小单元测试。它覆盖非法数值在访问 mapper 前被拒绝;高质量答案还应补充空白值和两个合法值的测试。项目父 POM 默认配置 `maven-surefire-plugin` 跳过测试,因此学员若未调整本地测试配置,应如实标记“未执行”,并给出启用测试后的计划命令,而不是伪造成功结果。
## B 卷任务 3:Review、PR 与资产沉淀(20 分)
### 参考 Review 结论
**结论:可以合入,但建议在合入前补充测试用例。**
检查范围是 `review-diff.patch` 的两处改动:`SysUserServiceImpl.updateStatus` 和新增的 `SysUserServiceImplTest`。
| 级别 | 结论 | 证据 | 建议 |
|---|---|---|---|
| 通过 | 校验位于 `Integer.parseInt` 和 `userMapper.update` 前,阻止非法状态写入 | 服务实现新增条件与业务异常 | 保持 |
| 通过 | 仅允许常量 1/2,`null` 和空白均不匹配,状态 3 也被拒绝 | 两个常量比较均通过 `String.valueOf` | 保持 |
| 通过 | 没有删除缓存失效注解、接口权限或管理员保护 | diff 只改 Service 校验和新增测试 | 保持 |
| 建议 | 新测试仅覆盖 `"3"`,未直接覆盖 `null`、空白、`"1"`、`"2"` 和 mapper 未调用 | 新增测试类只有一个 `assertThrows` | 加参数化负例和合法值的 mapper 交互断言 |
| 残余风险 | Controller 对异常统一返回的语义、批量更新原子性和集群缓存行为不属于本 diff 的直接变更 | 原始 Controller 和框架配置 | 作为后续集成测试范围,不阻断本次最小修复 |
不能仅写“代码没问题”。若给出“未发现阻断问题”,仍须写清检查范围、证据和残余风险。
### 参考 PR 说明
**背景**:系统用户批量冻结/解冻接口会将请求中的 `status` 传入 Service。前端选项限制不能替代服务端边界,非法值应在数据写入前被拒绝。
**实现**:在 `SysUserServiceImpl.updateStatus` 增加状态白名单,仅允许解冻(1)和冻结(2);非法值抛出 `JeecgBootException`。保留原缓存失效、Controller 权限和管理员保护。
**测试**:新增非法状态 `"3"` 的单元测试。合入前建议补充 `null`、空白、`"1"`、`"2"` 及接口权限/管理员保护的测试;每项测试状态必须按实际执行情况填写。
**风险与回退**:该变更会拒绝过去可能被错误接受的状态值,属于预期的业务收紧。出现兼容性问题时可回退本次 Service 白名单提交;不能通过放宽为任意整数解决。
### 参考可复用 Prompt
```text
基于指定 Git 提交审查一个状态更新 diff。先输出 Controller -> Service -> Mapper/缓存的调用链,
再判断校验是否发生在所有数据写入和可见副作用之前。只接受代码中已定义的状态常量;列出保留的
权限、关键账号保护、缓存和审计逻辑。最后按“阻断问题 / 建议 / 残余风险 / 测试矩阵”输出,
每项都给出文件路径和符号。没有执行的测试必须标为未执行,不能推断为通过。
```
验收方式:输出必须能标出校验位置、常量来源、未改动保护和至少一项负例及正例测试;不能凭空声称测试成功。
### 参考 Checklist 设计卡
| 项目 | 内容 |
|---|---|
| 名称 | 状态变更服务端边界 Checklist |
| 适用场景 | 新增或修改“启用/停用/冻结/解冻/审批状态”等服务端状态写入逻辑时 |
| 输入 | 固定提交、变更 diff、状态常量定义、调用入口 |
| 检查项 | 1. 合法状态是否白名单化;2. 校验是否早于解析、写库和副作用;3. 权限和关键账号保护是否保留;4. 缓存/审计/事件是否明确;5. 正负例和接口授权测试是否如实记录 |
| 输出 | Review 结论、证据表、测试矩阵、残余风险 |
| 边界 | 只辅助检查,不能替代实际权限配置、集成测试和人工业务审批 |
| 验收 | 每项结论都有文件路径和符号;未执行测试有原因与后续验证计划 |
## AI 使用记录的判分口径
不提供统一的“标准 Prompt 列表”。满分记录应至少能够说明:使用 AI 或 TRAE 做了什么、建议是什么、哪些被采纳或否决、人工为什么这样判断、相应证据或测试是否真的执行。工具名称可以不同;缺少 `trae-usage-record.md` 时,本题“AI 使用记录”2 分为 0 分。
+37
View File
@@ -0,0 +1,37 @@
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java
index c2fe868..236b4f2 100644
--- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java
@@ -1464,6 +1464,10 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
@Override
@CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, allEntries=true)
public void updateStatus(String id, String status) {
+ if (!String.valueOf(CommonConstant.USER_UNFREEZE).equals(status)
+ && !String.valueOf(CommonConstant.USER_FREEZE).equals(status)) {
+ throw new JeecgBootException("账号状态仅支持冻结或解冻");
+ }
userMapper.update(new SysUser().setStatus(Integer.parseInt(status)),
new UpdateWrapper<SysUser>().lambda().eq(SysUser::getId,id));
}
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/test/java/org/jeecg/modules/system/service/impl/SysUserServiceImplTest.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/test/java/org/jeecg/modules/system/service/impl/SysUserServiceImplTest.java
new file mode 100644
index 0000000..082f21a
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/test/java/org/jeecg/modules/system/service/impl/SysUserServiceImplTest.java
@@ -0,0 +1,16 @@
+package org.jeecg.modules.system.service.impl;
+
+import org.jeecg.common.exception.JeecgBootException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class SysUserServiceImplTest {
+
+ @Test
+ void rejectsAStatusOutsideFreezeAndUnfreezeBeforeUpdatingUser() {
+ SysUserServiceImpl service = new SysUserServiceImpl();
+
+ assertThrows(JeecgBootException.class, () -> service.updateStatus("user-1", "3"));
+ }
+}
+9
View File
@@ -0,0 +1,9 @@
repository=jeecgboot/JeecgBoot
repository_url=https://github.com/jeecgboot/JeecgBoot.git
base_ref=v3.9.2
base_commit=7df07a823fd558be857d0208ccae96342539fbc1
controller_path=jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java
service_path=jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java
test_path=jeecg-boot/jeecg-module-system/jeecg-system-biz/src/test/java/org/jeecg/modules/system/service/impl/SysUserServiceImplTest.java
diff_file=review-diff.patch
diff_command=git apply --check review-diff.patch
@@ -0,0 +1,264 @@
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()
+197
View File
@@ -0,0 +1,197 @@
from copy import deepcopy
from pathlib import Path
from docx import Document
from docx.enum.text import WD_BREAK
ROOT = Path('/Users/smart/Documents/TRAE培训/TRAE课件/exams')
SOURCE = ROOT / 'TRAE编程培训考试评估卷-实操与项目实战.docx'
TARGET = ROOT / 'TRAE编程培训考试评估卷-实操与项目实战(修订版).docx'
def replace_paragraph(paragraph, text, bold=None, size=None):
first_run = paragraph.runs[0] if paragraph.runs else None
old_bold = first_run.bold if first_run else None
old_size = first_run.font.size if first_run else None
paragraph.clear()
run = paragraph.add_run(text)
run.bold = old_bold if bold is None else bold
if size is not None:
run.font.size = size
elif old_size is not None:
run.font.size = old_size
return paragraph
def replace_cell(cell, text, bold=None):
for paragraph in cell.paragraphs[1:]:
paragraph._element.getparent().remove(paragraph._element)
paragraph = cell.paragraphs[0]
first_run = paragraph.runs[0] if paragraph.runs else None
old_bold = first_run.bold if first_run else None
old_size = first_run.font.size if first_run else None
paragraph.clear()
run = paragraph.add_run(text)
run.bold = old_bold if bold is None else bold
if old_size is not None:
run.font.size = old_size
def find_paragraph(document, text):
for paragraph in document.paragraphs:
if text in paragraph.text:
return paragraph
raise ValueError(f'Paragraph not found: {text}')
def set_table_rows(table, rows):
while len(table.rows) < len(rows):
table.add_row()
if len(table.rows) != len(rows):
raise ValueError('Unexpected table row count')
for row, values in zip(table.rows, rows):
if len(row.cells) != len(values):
raise ValueError('Unexpected table column count')
for cell, value in zip(row.cells, values):
replace_cell(cell, value)
def clone_paragraph(document, source_paragraph, text):
paragraph = document.add_paragraph()
placeholder = paragraph._p
element = deepcopy(source_paragraph._element)
placeholder.getparent().replace(placeholder, element)
paragraph._p = paragraph._element = element
replace_paragraph(paragraph, text)
return paragraph
def clone_table(document, source_table):
table = document.add_table(rows=1, cols=1)
placeholder = table._tbl
element = deepcopy(source_table._element)
placeholder.getparent().replace(placeholder, element)
table._tbl = table._element = element
return table
def insert_after(anchor, element):
anchor._element.addnext(element._element)
doc = Document(SOURCE)
replace_paragraph(find_paragraph(doc, '实操 + 公务用车项目实战综合卷'), '实操 + 系统用户状态管理综合卷')
# 1. Fixed time: 20 minutes for A, 60 for B, 10 for submission check.
set_table_rows(doc.tables[0], [
['项目', '说明'],
['适用对象', '参加 TRAE 编程培训并完成 JeecgBoot 实战的技术学员。'],
['考试形式', '允许使用 TRAE,上机完成;提交 Markdown 产物包与 AI 使用记录。'],
['建议时长', '90 分钟:A 卷 20 分钟,B 卷 60 分钟,提交检查 10 分钟。'],
['总分', '100 分:小实操 30 分,项目实战 70 分。'],
['评估目标', '考察学员能否把真实研发问题拆成 TRAE 可执行、团队可验收、后续可复用的工程任务。'],
])
set_table_rows(doc.tables[2], [
['部分', '时间', '分值', '形式', '核心能力'],
['A. 小实操', '20 分钟', '30 分', '个人完成', 'Prompt 改写、证据判断'],
['B. 项目实战', '60 分钟', '70 分', '个人完成', '项目理解、变更边界、测试安全网、Review 与交付'],
['提交检查', '10 分钟', '-', '个人完成', '核对目录、文件名与执行状态'],
['合计', '90 分钟', '100 分', '上机 + 文档交付', '个人基本功 + 项目落地能力'],
])
# 2. Deliverable templates and missing-file treatment.
replace_paragraph(find_paragraph(doc, '建议要求学员提交一个 Markdown 目录包'), '使用 exam-kit/exam-output/ 中的模板提交,目录和文件名不得改动:')
set_table_rows(doc.tables[3], [
['文件', '主要内容'],
['01-project-understanding.md', '结论摘要、证据表、影响与保护边界、待确认项'],
['02-change-and-test.md', '验收标准、校验位置、不改动项、测试矩阵、执行状态'],
['03-pr-and-assets.md', 'Review、PR 说明、可复用 Prompt、Rule/Checklist/Skill 设计卡'],
['trae-usage-record.md', 'Prompt、采纳/否决、人工判断点、证据或验证状态;缺失则任务 3 的如实记录项计 0 分'],
])
# Insert the fixed source / tools contract after the deliverable table.
input_heading = clone_paragraph(doc, find_paragraph(doc, '二、最终提交物'), '考试输入与作答约束')
input_intro = clone_paragraph(doc, find_paragraph(doc, '使用 exam-kit/exam-output/'), 'B 卷仅以 exam-kit/ 中的固定版本、指定 diff 和模板为准。')
input_table = clone_table(doc, doc.tables[3])
set_table_rows(input_table, [
['项目', '统一要求'],
['固定源码', 'GitHub jeecgboot/JeecgBoot;标签 v3.9.2:7df07a823fd558be857d0208ccae96342539fbc1。'],
['指定模块', 'jeecg-module-system;以 SysUserController.frozenBatch 和 SysUserServiceImpl.updateStatus 为事实入口。'],
['指定 diff', 'exam-kit/review-diff.patch;仅以该两文件变更为 Review 输入。'],
['允许材料', 'TRAE、GitHub 固定版本与 exam-kit;版本不一致时记录环境问题并联系监考人员。'],
])
anchor = doc.tables[3]
insert_after(anchor, input_heading)
insert_after(input_heading, input_intro)
insert_after(input_intro, input_table)
replace_paragraph(find_paragraph(doc, '本部分由学员个人完成,重点考察 TRAE 基础使用'), '本部分由学员个人完成,重点考察 TRAE 基础使用与 AI 协作基本功。限时 20 分钟。')
replace_cell(doc.tables[5].cell(0, 0), '原始 Prompt\n帮我看一下系统用户批量冻结/解冻功能有没有问题。')
replace_cell(doc.tables[7].cell(0, 0), 'AI 输出片段\n页面已限制状态值,因此 Service 不需要校验;直接调用接口也不会有问题。')
replace_paragraph(find_paragraph(doc, 'B 卷:公务用车项目实战'), 'B 卷:系统用户状态管理项目实战(70 分)')
replace_paragraph(find_paragraph(doc, '本部分使用 JeecgBoot 现有公务用车模块'), '本部分使用 exam-kit 指定版本的 JeecgBoot 系统用户模块,由学员个人完成。限时 60 分钟。')
replace_cell(doc.tables[9].cell(0, 0), '实战任务背景\n案例使用 GitHub 公开的 JeecgBoot v3.9.2 中系统用户模块。训练改造要求:批量冻结/解冻用户时,后端必须拒绝空白或 1、2 之外的状态值;页面限制不能替代 Service 入口的业务保护。本次考试不要求完整开发所有代码,重点考察如何使用 TRAE 完成项目理解、变更边界分析、测试设计、Review 和资产沉淀。')
# 3. Observable requirements for B tasks.
replace_paragraph(find_paragraph(doc, '使用 TRAE 输出公务用车模块结构说明'), '使用 TRAE 完成 01-project-understanding.md:说明系统用户模块结构、批量冻结/解冻接口、Service 入口、权限、管理员保护、缓存和测试入口。证据表至少含 5 条有效依据,每条必须写文件路径、符号或方法,以及它证明的结论;另说明非法状态值与接口、用户状态写入、缓存失效的影响关系,并区分事实、推断和待确认项。')
replace_paragraph(find_paragraph(doc, '输出最小改造方案和测试安全网'), '完成 02-change-and-test.md:说明应在哪个方法、哪次用户状态写入之前校验非法状态值;列出不改动的接口权限、管理员保护和缓存失效逻辑及依据;为空白或非法状态、正常冻结、正常解冻、无权限调用、管理员保护与缓存/用户状态回归逐项写明预期结果、验证入口、执行状态。已执行项必须给出命令和结果,未执行项必须说明原因和计划验证方式。')
replace_paragraph(find_paragraph(doc, '基于 `main...training/require-cancel-reason`'), '基于 exam-kit/review-diff.patch 的实际两文件 diff,完成 03-pr-and-assets.md:生成 Review 和 PR 说明,再输出一条可复用 Prompt 及一条 Rule、Checklist 或 Skill 设计卡。Review 可以得出“未发现阻断问题”的结论,但必须说明检查范围、证据和残余风险;所有已执行与未执行的验证必须如实标注。')
# 4. Replace vague scoring with observable, non-overlapping criteria.
set_table_rows(doc.tables[6], [
['评分项', '分值'],
['背景与目标清楚', '3'],
['范围可控,限定目录、文件、接口或页面', '2'],
['约束清楚,含项目规则、安全、权限、测试要求及信息不足时先提问', '4'],
['输出格式明确,如表格、清单、报告或 PR 说明', '3'],
['验收标准和证据要求明确', '3'],
])
set_table_rows(doc.tables[10], [
['评分项', '分值'],
['系统用户模块与批量冻结/解冻入口明确', '4'],
['frozenBatch 接口、Controller 与 Service 调用链明确', '5'],
['权限、管理员保护、缓存和测试入口明确', '4'],
['至少 5 条有效证据,每条含路径、符号和证明内容', '7'],
['影响与保护边界完整,并区分事实、推断和待确认项', '5'],
])
set_table_rows(doc.tables[11], [
['评分项', '分值'],
['状态校验改造目标和验收标准清楚', '4'],
['校验方法、位置和副作用前后顺序正确', '5'],
['不改动项及其依据清楚', '4'],
['非法状态、冻结、解冻、权限、管理员保护/缓存五类场景完整', '8'],
['验证命令、执行结果或未执行原因明确', '4'],
])
set_table_rows(doc.tables[12], [
['评分项', '分值'],
['Review 结论、证据、风险和验收方式完整', '5'],
['PR 说明包含背景、实现、测试和风险', '5'],
['可复用 Prompt 的输入、约束、输出和验收清楚', '5'],
['Rule、Checklist 或 Skill 设计卡的边界和验收清楚', '3'],
['AI 使用记录完整;不编造测试结果,能说明人工取舍', '2'],
])
replace_paragraph(find_paragraph(doc, '三、综合评分维度'), '三、阅卷通用原则(不重复计分)')
set_table_rows(doc.tables[13], [
['阅卷原则', '优秀表现', '使用方式'],
['证据意识', '结论能引用文件、函数、接口、日志或数据来源', '仅用于判断对应任务中的证据项,不单独追加扣分'],
['上下文选择', '知道先全局找路,再文件聚焦,再代码细查', '仅用于判断对应任务中的任务表达与证据项'],
['任务表达', '背景、目标、范围、约束、输出、验收完整', '仅用于判断 A 卷 Prompt 改写项'],
['风险意识', '能识别权限、状态、审计、异常态、测试缺口', '仅用于判断 B 卷影响、测试和 Review 项'],
['人机协作', '知道哪些让 AI 做,哪些必须人工确认', '仅用于判断 AI 使用记录与待确认项'],
['资产意识', '能沉淀可复用 Prompt、Rule、Checklist、Skill', '仅用于判断任务 3 的资产项'],
])
set_table_rows(doc.tables[15], [
['环节', '要求'],
['发卷前检查', '发放 exam-kit;核对 GitHub 仓库、v3.9.2 完整提交和 review-diff.patch 均可读取。'],
['考试输入', '考试前完成 GitHub 固定版本下载;版本不一致时由监考人员处理,不得由学员自行替换版本。'],
['过程记录', '必须提交 trae-usage-record.md;缺失时任务 3 的 AI 使用记录项计 0 分。'],
['阅卷方式', '先核对四个交付文件,再按对应评分表打分;通用原则不重复扣分。'],
['独立作答', '抽查关键路径、证据来源和人工取舍,确认交付物与作答者理解一致。'],
])
doc.core_properties.title = 'TRAE 编程培训考试评估卷(修订版)'
doc.core_properties.subject = '实操与系统用户状态管理综合卷'
doc.save(TARGET)
print(TARGET)