先更新1版记录下

This commit is contained in:
2026-04-16 11:30:30 +08:00
parent ead8e2edd5
commit 2f2d796e30
40 changed files with 393714 additions and 4071 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
urbanLifeline
Tik
schoolNewsServ
.idea/
.vscode/
.DS_Store

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 5.4 MiB

View File

@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<key for="node" id="nodegraph" yfiles.type="nodegraphics"/>
<key for="edge" id="edgegraph" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<node id="entity0">
<data key="nodegraph">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="370" width="421.48" x="440" y="40"/>
<y:Fill color="#FFFFE1" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.name" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="internal" modelPosition="t" backgroundColor="#FFFFE1" textColor="#FFFFFF" visible="true" horizontalTextPosition="center" iconTextGap="4" height="22" width="357" x="0" y="4">tb_ai_graph_entity</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="custom" modelPosition="t" backgroundColor="#FFFFFF" textColor="#000000" visible="true" horizontalTextPosition="center" iconTextGap="4" height="304" width="357" x="2" y="31.66796875">entity_id: varchar(64) NOT NULL - 实体ID
file_id: varchar(64) - 来源知识文件ID
entity_type: varchar(64) NOT NULL - 实体类型
entity_name: varchar(255) NOT NULL - 实体名称
normalized_name: varchar(255) - 标准化实体名称
graph_vertex_id: varchar(128) - 图数据库顶点ID
entity_status: varchar(32) NOT NULL - 实体状态
sync_status: varchar(32) NOT NULL - 同步状态
properties_json: jsonb NOT NULL - 实体属性JSON
adcode: varchar(12) - 行政区划编码
tenant_id: varchar(64) - 租户ID
tenant_path: varchar(255) - 租户路径
dept_id: varchar(64) - 部门ID
dept_path: varchar(255) - 部门路径
created_at: timestamp NOT NULL - 创建时间
updated_at: timestamp NOT NULL - 更新时间 <y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="entity1">
<data key="nodegraph">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="390" width="411.24" x="40" y="40"/>
<y:Fill color="#FFFFE1" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.name" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="internal" modelPosition="t" backgroundColor="#FFFFE1" textColor="#FFFFFF" visible="true" horizontalTextPosition="center" iconTextGap="4" height="22" width="341" x="0" y="4">tb_ai_graph_relation</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="custom" modelPosition="t" backgroundColor="#FFFFFF" textColor="#000000" visible="true" horizontalTextPosition="center" iconTextGap="4" height="324" width="341" x="2" y="31.66796875">relation_id: varchar(64) NOT NULL - 关系ID
src_entity_id: varchar(64) NOT NULL - 源实体ID
dst_entity_id: varchar(64) NOT NULL - 目标实体ID
relation_type: varchar(64) NOT NULL - 关系类型
relation_name: varchar(128) - 关系名称
graph_edge_id: varchar(128) - 图数据库边ID
relation_weight: numeric(6, 4) NOT NULL - 关系权重
relation_status: varchar(32) NOT NULL - 关系状态
sync_status: varchar(32) NOT NULL - 同步状态
properties_json: jsonb NOT NULL - 关系属性JSON
adcode: varchar(12) - 行政区划编码
tenant_id: varchar(64) - 租户ID
tenant_path: varchar(255) - 租户路径
dept_id: varchar(64) - 部门ID
dept_path: varchar(255) - 部门路径
created_at: timestamp NOT NULL - 创建时间
updated_at: timestamp NOT NULL - 更新时间 <y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="entity2">
<data key="nodegraph">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="430" width="545.28" x="940" y="220"/>
<y:Fill color="#FFFFE1" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.name" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="internal" modelPosition="t" backgroundColor="#FFFFE1" textColor="#FFFFFF" visible="true" horizontalTextPosition="center" iconTextGap="4" height="22" width="452" x="0" y="4">tb_ai_knowledge_file</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="custom" modelPosition="t" backgroundColor="#FFFFFF" textColor="#000000" visible="true" horizontalTextPosition="center" iconTextGap="4" height="364" width="452" x="2" y="31.66796875">file_id: varchar(64) NOT NULL - 知识文件ID
file_code: varchar(64) NOT NULL - 知识文件编码
file_name: varchar(255) NOT NULL - 知识文件名称
source_file_id: varchar(64) - 来源文件IDupms.tb_sys_file.file_id
file_type: varchar(32) NOT NULL - 文件类型
file_status: varchar(32) NOT NULL - 文件状态
graph_sync_status: varchar(32) NOT NULL - 图谱同步状态
vector_sync_status: varchar(32) NOT NULL - 向量库同步状态
graph_doc_ref: varchar(128) - 图谱侧文档/对象引用
vector_doc_ref: varchar(128) - 向量库侧文档/对象引用
content_checksum: varchar(128) - 内容校验值
metadata_json: jsonb NOT NULL - 文件元数据JSON
adcode: varchar(12) - 行政区划编码
tenant_id: varchar(64) - 租户ID
tenant_path: varchar(255) - 租户路径
dept_id: varchar(64) - 部门ID
dept_path: varchar(255) - 部门路径
created_at: timestamp NOT NULL - 创建时间
updated_at: timestamp NOT NULL - 更新时间 <y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="entity3">
<data key="nodegraph">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="350" width="520.84" x="440" y="480"/>
<y:Fill color="#FFFFE1" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.name" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="internal" modelPosition="t" backgroundColor="#FFFFE1" textColor="#FFFFFF" visible="true" horizontalTextPosition="center" iconTextGap="4" height="22" width="429" x="0" y="4">tb_ai_knowledge_sync_task</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Courier" fontSize="12" fontStyle="plain" hasLineColor="false" modelName="custom" modelPosition="t" backgroundColor="#FFFFFF" textColor="#000000" visible="true" horizontalTextPosition="center" iconTextGap="4" height="284" width="429" x="2" y="31.66796875">task_id: varchar(64) NOT NULL - 同步任务ID
file_id: varchar(64) NOT NULL - 知识文件ID
target_store: varchar(16) NOT NULL - 目标存储GRAPH/VECTOR
task_type: varchar(16) NOT NULL - 任务类型UPSERT/DELETE/REBUILD
task_status: varchar(32) NOT NULL - 任务状态
retry_count: int4 NOT NULL - 重试次数
error_message: text - 错误信息
payload_json: jsonb NOT NULL - 任务载荷JSON
adcode: varchar(12) - 行政区划编码
tenant_id: varchar(64) - 租户ID
tenant_path: varchar(255) - 租户路径
dept_id: varchar(64) - 部门ID
dept_path: varchar(255) - 部门路径
created_at: timestamp NOT NULL - 创建时间
updated_at: timestamp NOT NULL - 更新时间 <y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<edge id="edge0-0" source="entity0" target="entity2">
<data key="edgegraph">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
<y:Arrows source="white_diamond" target="circle"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="edge1-0" source="entity1" target="entity0">
<data key="edgegraph">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
<y:Arrows source="white_diamond" target="circle"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="edge1-1" source="entity1" target="entity0">
<data key="edgegraph">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
<y:Arrows source="white_diamond" target="circle"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="edge3-0" source="entity3" target="entity2">
<data key="edgegraph">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="dashed" width="1.0"/>
<y:Arrows source="white_diamond" target="circle"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
</graph>
</graphml>

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 978 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 6.6 MiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 9.3 MiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 5.7 MiB

View File

@@ -0,0 +1,122 @@
# AI智能学习系统功能清单结构化版
## 一、学生端(微信小程序)
| 序号 | 模块 | 功能点 | 详细描述 |
|---|---|---|---|
| 1 | 账号与登录 | 多方式登录 | 微信一键登录/手机号+验证码登录;年级/班级信息绑定;登录态校验与提示 |
| 2 | 资料与作业上传 | 机构作业接收 | 查看教师发布的班级作业/学习资料;按学科、年级分类展示 |
| 3 | 资料与作业上传 | 多形式上传 | 拍照上传(裁剪/旋转/增强文件上传PDF/Word/图片);手动录入答题 |
| 4 | 资料与作业上传 | 双模式批改 | 绑定机构作业批改;自主任意作业批改 |
| 5 | 作业处理 | 批改结果查看 | 客观题+主观题批改结果展示;生成知识点/薄弱项/学习趋势报告 |
| 6 | 作业处理 | 作业评分与等级展示 | 展示作业得分、超越同学比例、答题用时、质量评级A/B/C |
| 7 | 错题本 | 错题归集管理 | 机构作业/自主练习错题自动收录;按学科/知识点/错因分类;筛选/搜索/收藏 |
| 8 | 错题本 | 错题关联学习 | 点击错题自动关联知识点讲解课程 |
| 9 | 错题本 | 错题状态管理 | 错题按待学习/复习中/已掌握状态分类;支持按错因类型筛选 |
| 10 | 错题本 | 错题复习次数统计 | 记录每道错题复习次数,展示复习轨迹 |
| 11 | 错因分析 | 错因可视化 | 个人错因分布饼图/柱状图 |
| 12 | 错因分析 | 细维度错因拆解 | 展示审题不清、计算失误、概念模糊等具体错因及优化建议 |
| 13 | 举一反三训练 | AI变式题生成 | 基于单题错题生成同类变式题;梯度难度练习 |
| 14 | 举一反三训练 | 在线答题批改 | 提交后自动批改;实时错因提示 |
| 15 | 费曼学习法 | 错题重做 | 变式题后进入错题重做;错误时自动关联知识库课程 |
| 16 | 费曼学习法 | 语音讲解 | 1-3分钟讲解录制预览/重录/提交;提供讲解规范 |
| 17 | 费曼学习法 | 讲解评估 | 知识/逻辑/表达/迁移多维度评分;通过/未通过+改进建议 |
| 18 | 复习计划 | 艾宾浩斯提醒 | 复习日历+节点提醒;个性化复习任务推送 |
| 19 | 复习计划 | 复习内容匹配 | 自动关联复习要点课程 |
| 20 | 个性化推荐 | 学习内容推荐 | 首页推送薄弱知识点微课/专项训练/同类错题 |
| 21 | 学习激励 | 成长轨迹展示 | 知识点掌握度曲线/错因减少趋势;学习勋章/打卡/时长统计 |
| 22 | 学习激励 | 成长指标量化 | 展示努力指数、进步率、学习韧性等量化指标 |
| 23 | 学习激励 | 连续打卡统计 | 记录连续学习天数,展示打卡 streak |
| 24 | 消息通知 | 学习提醒 | 作业/复习/批改/推荐内容提醒;消息列表已读/删除 |
## 二、教师端Web
| 序号 | 模块 | 功能点 | 详细描述 |
|---|---|---|---|
| 1 | 账号鉴权 | 教师登录校验 | 账号密码/验证码登录;生成 Token登录日志记录 |
| 2 | 班级与学员管理 | 班级管理 | 班级创建/编辑/解散;批量导入/导出学员 |
| 3 | 班级与学员管理 | 学员管理 | 学员信息增删改;班级绑定/解绑;状态管理 |
| 4 | 作业与资料发布 | 资料发布 | 上传课件/习题/文档;按班级推送 |
| 5 | 作业管理 | 作业批改 | AI自动批改+教师复核;批改结果存储/查询/导出 |
| 6 | 学情分析 | 学员档案 | 聚合作业/错题/讲解/复习数据;生成个人学习档案 |
| 7 | 学情分析 | 班级学情 | 成绩/知识点/错因统计;进度排名/热力图 |
| 8 | 学情分析 | 班级能力层级可视化 | 展示班级知识点掌握度、认知能力层级分布 |
| 9 | 教学干预 | 讲解点评 | 查看学生语音讲解;文字/语音点评与评分 |
| 10 | 教学干预 | 教学建议 | AI生成教学建议周/月报表生成导出 |
## 三、机构端Web
| 序号 | 模块 | 功能点 | 详细描述 |
|---|---|---|---|
| 1 | 管理员鉴权 | 高权限登录 | 管理员账号校验;生成高权限 Token操作日志审计 |
| 2 | 用户管理 | 教师管理 | 账号创建/编辑/禁用;批量导入/导出 |
| 3 | 用户管理 | 学员管理 | 全机构学员统一管理;跨班级/跨校区调配 |
| 4 | 用户管理 | 权限配置 | 校长/教务/教师角色权限自定义;分级管控 |
| 5 | 数据可视化 | 全机构数据 | 校区/年级/学科汇总;运营仪表盘展示 |
| 6 | 标准化管理 | 教学规范 | 批改/错因/复习计划标准统一 |
| 7 | 标准化管理 | 知识库规范 | 分类/标签/内容格式统一 |
| 8 | 质量监控 | 教学监控 | 教师/学员数据异常预警 |
| 9 | 质量监控 | 内容监控 | 课程合规性审核;违规预警处理 |
| 10 | 质量监控 | 质量报表 | 教学质量/学习效果报表导出 |
| 11 | 决策支持 | 数据分析 | 趋势分析/瓶颈识别 |
## 四、公共支撑层:知识库
| 序号 | 模块 | 功能点 | 详细描述 |
|---|---|---|---|
| 1 | 课程基础管理 | 课程存储 | 图文/PDF/视频多格式存储;学科→年级→知识点分类 |
| 2 | 课程基础管理 | 课程标签 | 自动/手动打标签;全机构标签统一 |
| 3 | 内容检索 | 智能匹配 | 错题→知识点课程关联;关键词/错因检索 |
| 4 | 内容生成 | 题目生成 | 基础/变式/拓展题生成;按难度/题量定制 |
## 五、公共支撑层:个性化推荐算法
| 序号 | 模块 | 功能点 | 详细描述 |
|---|---|---|---|
| 1 | 学员画像 | 多维度画像 | 基础:年级/学科/班级;学习:错题/错因/通过率;行为:学习偏好/习惯 |
| 2 | 推荐策略 | 内容推荐 | 错题→知识点+变式题;错因→专项训练;遗忘节点→复习内容 |
| 3 | 效果优化 | 算法优化 | 收集学习反馈;动态调整权重;统计推荐效果 |
## 六、与 `init` 表结构 + `docs/architecture` 架构流转对齐分析
### 6.1 模块级覆盖评估
| 功能域 | 架构流转对应 | 当前数据落点init/pg | 覆盖结论 | 说明 |
|---|---|---|---|---|
| 组织与鉴权 | `modules/00-组织与权限上下文/*`,系统架构「认证与用户中心」 | `upms.tb_sys_*``auth.tb_auth_*` | 已覆盖 | 租户/组织/RBAC/登录审计链路完整 |
| 课程学习与知识点 | `modules/01-课程学习/*`,系统架构「课程与内容服务」 | `course.cl_*` | 已覆盖 | 课程-章节-课时-知识点-学习会话/进度已落表 |
| 习题与作业 | `modules/02-习题与作业/*`,多角色流转 P2 | `homework.hw_*` + `upms.tb_sys_file` | 已覆盖 | 出题、组卷、发布、提交、附件引用链路完整 |
| 批改与反馈 | `modules/03-批改与反馈/*`,多角色流转 P3/P7 | `grading.gd_*` | 部分覆盖(有实现差异) | 核心能力已落地,但与架构 ER 命名/拆表不完全一致 |
| 错题本与复习计划 | 多角色流转 P4 | `grading.gd_wrong_question``grading.gd_review_plan` | 已覆盖 | 错题沉淀、复习计划、复习次数有落点 |
| 站内信提醒 | 多角色流转 D703 模块站内信链路 | `upms.tb_sys_message``upms.tb_sys_message_recipient` | 已覆盖 | 支持 `content_object_id` + `web_jump_url` 跳转链路 |
| 学习激励/成就 | 系统架构「学习激励」能力 | `achievement.ac_*` | 已扩展 | 超出原架构图细粒度,具备成就定义/发放/进度 |
| AI知识库与检索 | 系统架构「Python AI处理层」「知识库/题库」 | `ai.tb_ai_knowledge_graph``ai.tb_ai_vector_*``ai.tb_ai_retrieval_log` | 已扩展 | 图谱+向量检索能力具备,支持 RAG 基础设施 |
| 个性化推荐 | 多角色流转 P5系统架构「画像与推荐策略」 | 仅间接依赖 `grading/course/ai`,缺独立推荐域表 | 部分覆盖 | 缺推荐结果、曝光点击、反馈回流等闭环数据 |
| 教学运营监控 | 系统架构「质量监控/运营分析」 | 主要依赖 upms 消息与业务表;缺独立质控事实表 | 部分覆盖 | 可查询但难做稳定报表与规则预警闭环 |
### 6.2 当前缺失项(功能清单有诉求,但表结构/流转未完全落地)
| 缺失能力 | 当前现状 | 建议补充 |
|---|---|---|
| 班级实体与班级成员关系 | 目前主要依赖 `dept`/`target_ref_id` 间接表达 | 新增班级主表、班级成员表、班级-课程关联表 |
| 费曼语音讲解全链路 | 仅有教师点评和文件能力,缺“讲解提交/评估”专表 | 新增讲解记录、讲解评估、讲解维度评分明细表 |
| AI变式题生成可追踪 | 无“变式题生成任务/来源错题/生成结果”结构 | 新增变式题任务表、变式题结果表、题源映射表 |
| 推荐反馈回流闭环 | 缺推荐结果曝光/点击/完成反馈事实表 | 新增推荐任务、推荐结果、用户反馈与效果统计表 |
| 学习激励行为明细 | 有成就域,但缺打卡 streak/努力指数原始行为表 | 新增学习打卡流水、学习时长明细、成长指标快照表 |
| 机构质量监控与内容合规 | 清单有诉求,缺独立事件与处置记录 | 新增质量监控事件表、内容审核表、处置流程表 |
| 周/月报表可追溯快照 | 现可临时聚合,缺报表固化快照 | 新增学情报表快照表、报表任务执行日志表 |
### 6.3 需修正项(`docs/architecture` 与 `init` 不一致)
| 修正点 | 架构图当前表达 | `init` 实际实现 | 建议 |
|---|---|---|---|
| 批改结果拆表方式 | 03 ER 使用 `gd_objective_score` + `gd_subjective_review` | 采用统一表 `grading.gd_answer_grade`(含 `grade_mode`/`grade_status` | 更新 ER 图为统一批改结果模型,减少实现偏差 |
| 批改规则集实体 | 03 ER 有 `gd_grading_rule_set` | 当前无对应表,策略主要在 `hw_paper.grading_policy_json` 与服务层 | 二选一:补规则集表,或在 ER 中改为“策略JSON+策略版本” |
| 教师点评关联键 | 03 ER 中 `gd_teacher_comment` 关联 `review_id` | 实际关联 `answer_grade_id` | 统一为 `answer_grade_id`,避免主观/客观分支分叉 |
| 作答附件模型 | 02 ER 体现 `hw_submission_attachment` 独立表 | 实际为 `hw_submission_answer.file_id/file_type` + `upms.tb_sys_file` | ER 图改为“答案行内引用文件”模型 |
| 题目版本模型 | 02 ER 含 `hw_question_version` | 当前为 `hw_question_item` 单表(含 `answer_payload`/`scoring_rule_json` | 若需版本回溯,补版本表;否则 ER 去掉版本实体 |
### 6.4 可扩展项(建议)
| 扩展方向 | 建议能力 | 目标收益 |
|---|---|---|
| 推荐域独立化 | 新建 `recommendation` schema策略、召回、排序、结果、反馈 | 打通“推荐-点击-学习效果”闭环优化 |
| 画像域标准化 | 新增学员画像快照与特征版本表 | 支撑可解释推荐与分层教学 |
| AI任务细分 | 在 `ai.tb_ai_task_log` 之上增加 OCR/ASR/讲解评测明细 | 提升 AI 调用可观测性与成本分析能力 |
| 教学运营报表化 | 固化班级/机构日报周报月报事实快照 | 降低临时聚合成本,提高口径一致性 |
| 复习策略强化 | 复习计划增加策略来源与命中规则 | 支撑艾宾浩斯与个性化策略并行迭代 |
### 6.5 可融合项(跨域打通)
| 融合目标 | 融合对象 | 融合方式 |
|---|---|---|
| 统一学员画像 | `course.cl_learning_*` + `homework.hw_submission*` + `grading.gd_*` + `achievement.ac_*` | 形成统一画像特征层,服务推荐/学情/激励 |
| 错题-推荐-复习闭环 | `gd_wrong_question` + `gd_review_plan` + 推荐结果域 | 用复习结果反哺推荐权重,形成闭环 |
| 主观题待审阅闭环 | `gd_answer_grade` + `upms.tb_sys_message*` + 教师复核页面 | 统一消息跳转、回执、处理状态追踪 |
| 知识点统一索引 | `course.cl_knowledge_point` + `homework.hw_question_kp_rel` + `ai.tb_ai_kg_*` | 建立知识点主索引,统一课程/题目/图谱引用 |
| 内容资产统一管理 | `upms.tb_sys_file` + `course.cl_course_resource` + 讲解音频能力 | 统一文件引用规范与生命周期管理 |
## 七、优先级建议(落地顺序)
1. **P0先修正**:先统一 `docs/architecture``init` 的 ER 表达差异(批改模型、附件模型、题目版本)。
2. **P1补缺口**:优先补齐“班级实体”“推荐闭环”“语音讲解评估”三条主链路。
3. **P2做融合**:建立统一学员画像层,打通错题-复习-推荐-激励数据闭环。

26
docs/architecture.md Normal file
View File

@@ -0,0 +1,26 @@
# K12Study 项目架构文档(建设中)
## 1. 当前已确认技术选型
- 图数据库NebulaGraph主选Neo4j Community备选
- 向量库Milvus
- 关系库PostgreSQL存储原始业务表
## 2. 分层职责(先行约定)
- PostgreSQL业务主数据与原始表数据唯一事实源
- NebulaGraph / Neo4j知识点、题目、学生作答、知识关系等图结构查询
- Milvus教材/题解/知识片段向量检索与语义召回
## 3. 三库协同原则(首版)
- 关系库到图库:通过同步任务进行实体与关系映射
- 关系库到向量库:通过切片与 embedding 任务进行索引
- 图谱节点保留源表标识(如 `source_table``source_pk`),支持点击节点回查原始表数据
## 4. 架构图产物(待补充)
- 系统流程图:建设中
- ER 图:建设中
- 数据流图:建设中

View File

@@ -1,77 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-04-14T10:41:00.000Z" agent="Oz" version="24.7.17">
<diagram id="ctx-er" name="ER图">
<mxGraphModel dx="1800" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="00 组织与权限上下文 - ER图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="450" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="统一隔离键adcode / tenant_id / tenant_path / dept_id / dept_path" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="720" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="tb_sys_tenant&lt;br&gt;PK tenant_id&lt;br&gt;FK parent_tenant_id -&gt; tb_sys_tenant.tenant_id&lt;br&gt;tenant_name, tenant_type, status&lt;br&gt;adcode, tenant_path, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="120" width="280" height="140" as="geometry"/>
</mxCell>
<mxCell id="11" value="tb_sys_dept&lt;br&gt;PK dept_id&lt;br&gt;FK parent_dept_id -&gt; tb_sys_dept.dept_id&lt;br&gt;FK tenant_id -&gt; tb_sys_tenant.tenant_id&lt;br&gt;dept_name, dept_type&lt;br&gt;adcode, tenant_path, dept_path, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="360" y="120" width="300" height="150" as="geometry"/>
</mxCell>
<mxCell id="12" value="tb_sys_user&lt;br&gt;PK user_id, UK username&lt;br&gt;FK tenant_id -&gt; tb_sys_tenant.tenant_id&lt;br&gt;FK dept_id -&gt; tb_sys_dept.dept_id&lt;br&gt;display_name, password_hash, status&lt;br&gt;adcode, tenant_path, dept_path, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="700" y="120" width="320" height="150" as="geometry"/>
</mxCell>
<mxCell id="13" value="tb_sys_role&lt;br&gt;PK role_id, UK role_code&lt;br&gt;FK tenant_id -&gt; tb_sys_tenant.tenant_id&lt;br&gt;role_name&lt;br&gt;dept_id, dept_path, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="700" y="320" width="290" height="130" as="geometry"/>
</mxCell>
<mxCell id="14" value="tb_sys_permission&lt;br&gt;PK permission_id, UK permission_code&lt;br&gt;FK tenant_id -&gt; tb_sys_tenant.tenant_id&lt;br&gt;permission_name&lt;br&gt;dept_id, dept_path, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1030" y="320" width="320" height="130" as="geometry"/>
</mxCell>
<mxCell id="15" value="rel_user_role&lt;br&gt;PK (user_id, role_id)&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;FK role_id -&gt; tb_sys_role.role_id&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1030" y="120" width="290" height="130" as="geometry"/>
</mxCell>
<mxCell id="16" value="rel_role_permission&lt;br&gt;PK (role_id, permission_id)&lt;br&gt;FK role_id -&gt; tb_sys_role.role_id&lt;br&gt;FK permission_id -&gt; tb_sys_permission.permission_id&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1360" y="120" width="320" height="130" as="geometry"/>
</mxCell>
<mxCell id="17" value="tb_auth_refresh_token&lt;br&gt;PK token_id&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;refresh_token, expire_at, revoked&lt;br&gt;tenant_id, dept_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="40" y="330" width="300" height="130" as="geometry"/>
</mxCell>
<mxCell id="18" value="tb_auth_login_audit&lt;br&gt;PK audit_id&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;username, login_ip, login_status&lt;br&gt;tenant_id, dept_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="360" y="330" width="300" height="130" as="geometry"/>
</mxCell>
<mxCell id="19" value="设计方法与原因&lt;br&gt;1) DDD上下文先行权限域作为业务域前置依赖&lt;br&gt;2) RBAC最小闭环用户-角色-权限拆分降低耦合&lt;br&gt;3) 多租户统一隔离键:保证后续业务表可水平扩展&lt;br&gt;4) 认证审计分离:令牌高频写与审计查询分流" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1360" y="320" width="320" height="170" as="geometry"/>
</mxCell>
<mxCell id="100" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="11" target="10" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="101" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="12" target="10" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="102" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" parent="1" source="12" target="11" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="103" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#9673a6;" parent="1" source="13" target="10" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="104" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="15" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="105" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="15" target="13" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="106" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="16" target="13" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="107" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" parent="1" source="16" target="14" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="108" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="17" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="109" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" parent="1" source="18" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<mxfile host="65bd71144e">
<diagram id="ctx-er-v2" name="ER图">
<mxGraphModel dx="1312" dy="773" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="00 组织与权限上下文 - ER图含班级模型" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="620" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="全局技术栈对齐:业务主数据落 PostgreSQLAI知识检索能力由 Milvus + NebulaGraph/Neo4j 承担;同步调度统一采用 REST/JAR本模块仅维护组织权限" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=12;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="1400" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="upms.tb_sys_tenant&lt;br&gt;PK tenant_id&lt;br&gt;parent_tenant_id, tenant_name, tenant_type&lt;br&gt;adcode, tenant_path, status" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="40" y="90" width="280" height="120" as="geometry"/>
</mxCell>
<mxCell id="11" value="upms.tb_sys_dept&lt;br&gt;PK dept_id&lt;br&gt;FK tenant_id -&gt; tb_sys_tenant.tenant_id&lt;br&gt;parent_dept_id, dept_name, dept_type&lt;br&gt;adcode, tenant_path, dept_path" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="360" y="90" width="320" height="140" as="geometry"/>
</mxCell>
<mxCell id="12" value="upms.tb_sys_user&lt;br&gt;PK user_id, UK username&lt;br&gt;FK tenant_id -&gt; tb_sys_tenant.tenant_id&lt;br&gt;FK dept_id -&gt; tb_sys_dept.dept_id&lt;br&gt;display_name, status, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="730" y="90" width="340" height="140" as="geometry"/>
</mxCell>
<mxCell id="13" value="upms.tb_sys_role&lt;br&gt;PK role_id, UK role_code&lt;br&gt;tenant_id, role_name, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="1110" y="90" width="280" height="110" as="geometry"/>
</mxCell>
<mxCell id="14" value="upms.tb_sys_menu&lt;br&gt;PK route_id&lt;br&gt;parent_route_id, route_path, component_key&lt;br&gt;permission_code, hidden, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="1430" y="90" width="320" height="130" as="geometry"/>
</mxCell>
<mxCell id="15" value="upms.tb_sys_role_menu&lt;br&gt;PK (role_id, route_id)&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1430" y="260" width="300" height="100" as="geometry"/>
</mxCell>
<mxCell id="16" value="auth.tb_auth_refresh_token&lt;br&gt;PK token_id&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;refresh_token, expire_at, revoked" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="40" y="290" width="320" height="120" as="geometry"/>
</mxCell>
<mxCell id="17" value="auth.tb_auth_login_audit&lt;br&gt;PK audit_id&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;username, login_ip, login_status" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="400" y="290" width="320" height="120" as="geometry"/>
</mxCell>
<mxCell id="18" value="upms.tb_school_class&lt;br&gt;PK class_id&lt;br&gt;FK dept_id -&gt; tb_sys_dept.dept_id&lt;br&gt;class_name, grade_code, status&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="760" y="290" width="340" height="140" as="geometry"/>
</mxCell>
<mxCell id="19" value="upms.tb_school_class_member&lt;br&gt;PK (class_id, user_id)&lt;br&gt;FK class_id -&gt; tb_school_class.class_id&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;member_role, member_status, joined_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="1140" y="450" width="350" height="150" as="geometry"/>
</mxCell>
<mxCell id="20" value="upms.tb_school_class_course_rel&lt;br&gt;PK (class_id, course_id)&lt;br&gt;FK class_id -&gt; tb_school_class.class_id&lt;br&gt;relation_status, tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="1530" y="390" width="320" height="120" as="geometry"/>
</mxCell>
<mxCell id="30" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=1;exitY=0.35;entryX=0;entryY=0.35;" parent="1" source="10" target="11" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="31" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=1;exitY=0;entryX=0;entryY=0;" parent="1" source="10" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="700" y="70"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="32" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=0.6;exitY=1;entryX=0.4;entryY=0;" parent="1" source="13" target="15" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="33" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=0.4;exitY=1;entryX=0.6;entryY=0;" parent="1" source="14" target="15" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="34" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=0;exitY=0.7;entryX=1;entryY=0.35;" parent="1" source="12" target="16" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="35" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=0;exitY=0.85;entryX=1;entryY=0.45;" parent="1" source="12" target="17" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="36" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=1;exitY=0.8;entryX=0;entryY=0.4;" parent="1" source="11" target="18" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="37" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=1;exitY=0.6;entryX=0;entryY=0.4;" parent="1" source="18" target="19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="38" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;exitX=1;exitY=0.85;entryX=0;entryY=0.2;" parent="1" source="18" target="20" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -8,6 +8,9 @@
<mxCell id="2" value="00 组织与权限上下文 - 对象类数据流图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="520" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="本模块边界说明:仅承载组织/权限与认证AI 图谱与向量同步任务由 ai 模块统一调度REST/JAR目标存储为 Milvus + NebulaGraph/Neo4j" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=12;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="1200" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="Actor&lt;br&gt;学生" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="40" y="120" width="120" height="70" as="geometry"/>
</mxCell>
@@ -38,7 +41,7 @@
<mxCell id="41" value="Entity&lt;br&gt;TenantDeptAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="820" y="210" width="200" height="70" as="geometry"/>
</mxCell>
<mxCell id="42" value="Entity&lt;br&gt;RolePermissionAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
<mxCell id="42" value="Entity&lt;br&gt;RoleMenuAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="820" y="310" width="200" height="70" as="geometry"/>
</mxCell>
<mxCell id="43" value="Entity&lt;br&gt;AuthTokenStore" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
@@ -50,7 +53,7 @@
<mxCell id="51" value="Redis(Session/Token)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1090" y="320" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="60" value="设计方法与原因&lt;br&gt;1) BCE分层明确交互职责&lt;br&gt;2) 鉴权与查询控制器分离,便于独立扩展&lt;br&gt;3) 权判定集中到Policy服务降低重复逻辑&lt;br&gt;4) Token热数据落Redis用户组织主数据落PG" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxCell id="60" value="设计方法与原因&lt;br&gt;1) BCE分层明确交互职责&lt;br&gt;2) 鉴权与查询控制器分离,便于独立扩展&lt;br&gt;3) 菜单授权判定集中到Policy服务降低重复逻辑&lt;br&gt;4) Token热数据落Redis用户组织主数据落PG&lt;br&gt;5) AI同步调度配置通过权限体系统一管控" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1360" y="190" width="360" height="180" as="geometry"/>
</mxCell>
<mxCell id="100" value="登录/路由请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="10" target="20" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
@@ -61,7 +64,7 @@
<mxCell id="105" value="权限判定" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="31" target="32" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="106" value="用户读取" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="30" target="40" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="107" value="组织读取" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="31" target="41" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="108" value="角色权限读取" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="32" target="42" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="108" value="角色菜单读取" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="32" target="42" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="109" value="Token签发/撤销" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="30" target="43" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="110" value="持久化" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="40" target="50" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="111" value="持久化" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="41" target="50" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>

View File

@@ -1,64 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-04-14T10:42:00.000Z" agent="Oz" version="24.7.17">
<diagram id="course-er" name="ER图">
<mxGraphModel dx="1800" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="01 课程学习 - ER图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="360" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="课程主数据 + 学习过程数据分层;所有业务主表均携带 tenant_id/adcode/dept_path" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="860" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="cl_course&lt;br&gt;PK course_id&lt;br&gt;title, subject_code, grade_code&lt;br&gt;difficulty_level, status&lt;br&gt;tenant_id, adcode, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="40" y="120" width="250" height="130" as="geometry"/>
</mxCell>
<mxCell id="11" value="cl_course_chapter&lt;br&gt;PK chapter_id&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;chapter_no, chapter_title&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="330" y="120" width="250" height="130" as="geometry"/>
</mxCell>
<mxCell id="12" value="cl_course_lesson&lt;br&gt;PK lesson_id&lt;br&gt;FK chapter_id -&gt; cl_course_chapter.chapter_id&lt;br&gt;lesson_no, lesson_title, duration_sec&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="620" y="120" width="270" height="130" as="geometry"/>
</mxCell>
<mxCell id="13" value="cl_knowledge_point&lt;br&gt;PK kp_id&lt;br&gt;kp_code, kp_name, subject_code, grade_code&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="930" y="120" width="260" height="120" as="geometry"/>
</mxCell>
<mxCell id="14" value="cl_course_knowledge_rel&lt;br&gt;PK (course_id, kp_id)&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;weight, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1230" y="120" width="280" height="130" as="geometry"/>
</mxCell>
<mxCell id="15" value="cl_course_resource&lt;br&gt;PK resource_id&lt;br&gt;FK lesson_id -&gt; cl_course_lesson.lesson_id&lt;br&gt;resource_type(pdf/video/doc), resource_url&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="620" y="290" width="290" height="130" as="geometry"/>
</mxCell>
<mxCell id="16" value="cl_course_tag&lt;br&gt;PK tag_id&lt;br&gt;tag_name, tag_type&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="40" y="300" width="230" height="110" as="geometry"/>
</mxCell>
<mxCell id="17" value="cl_course_tag_rel&lt;br&gt;PK (course_id, tag_id)&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;FK tag_id -&gt; cl_course_tag.tag_id&lt;br&gt;tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="300" y="300" width="270" height="120" as="geometry"/>
</mxCell>
<mxCell id="18" value="cl_learning_session&lt;br&gt;PK session_id&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;status(STARTED/PAUSED/COMPLETED)&lt;br&gt;started_at, ended_at, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="930" y="290" width="300" height="140" as="geometry"/>
</mxCell>
<mxCell id="19" value="cl_learning_progress&lt;br&gt;PK progress_id&lt;br&gt;FK session_id -&gt; cl_learning_session.session_id&lt;br&gt;FK lesson_id -&gt; cl_course_lesson.lesson_id&lt;br&gt;progress_pct, last_position_sec&lt;br&gt;mastery_level, tenant_id, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="1260" y="300" width="320" height="140" as="geometry"/>
</mxCell>
<mxCell id="20" value="cl_learning_event&lt;br&gt;PK event_id&lt;br&gt;FK session_id -&gt; cl_learning_session.session_id&lt;br&gt;event_type(start/pause/seek/finish)&lt;br&gt;event_time, payload_json, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="930" y="460" width="300" height="130" as="geometry"/>
</mxCell>
<mxCell id="21" value="设计方法与原因&lt;br&gt;1) 聚合根Course / LearningSession&lt;br&gt;2) 状态机Session状态驱动进度写入&lt;br&gt;3) 标签与知识点解耦,支持多维检索&lt;br&gt;4) 事件明细单表追加,兼顾审计与回放" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="40" y="470" width="840" height="120" as="geometry"/>
</mxCell>
<mxCell id="100" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="11" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="101" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="12" target="11" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="102" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="15" target="12" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="103" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="14" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="104" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="14" target="13" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="105" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="17" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="106" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="17" target="16" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="107" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="18" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="108" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="19" target="18" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="109" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="19" target="12" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="110" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="20" target="18" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>
<mxfile host="65bd71144e">
<diagram id="course-er" name="ER图">
<mxGraphModel dx="1312" dy="773" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="01 课程学习 - ER图含知识点掌握与图谱/向量同步关联)" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="360" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="课程主数据 + 学习过程数据分层;业务主库为 PostgreSQL知识检索能力由 Milvus + NebulaGraph/Neo4j 承担" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="1100" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="cl_course&lt;br&gt;PK course_id&lt;br&gt;title, subject_code, grade_code&lt;br&gt;difficulty_level, status&lt;br&gt;tenant_id, adcode, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="50" y="130" width="250" height="130" as="geometry"/>
</mxCell>
<mxCell id="12" value="cl_course_lesson&lt;br&gt;PK lesson_id&lt;br&gt;FK chapter_id -&gt; cl_course_chapter.chapter_id&lt;br&gt;lesson_no, lesson_title, duration_sec&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="650" y="300" width="270" height="130" as="geometry"/>
</mxCell>
<mxCell id="13" value="cl_knowledge_point&lt;br&gt;PK kp_id&lt;br&gt;kp_code, kp_name, subject_code, grade_code&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="1030" y="10" width="260" height="120" as="geometry"/>
</mxCell>
<mxCell id="14" value="cl_course_knowledge_rel&lt;br&gt;PK (course_id, kp_id)&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;weight, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1680" y="190" width="280" height="130" as="geometry"/>
</mxCell>
<mxCell id="15" value="cl_course_resource&lt;br&gt;PK resource_id&lt;br&gt;FK lesson_id -&gt; cl_course_lesson.lesson_id&lt;br&gt;resource_type(pdf/video/doc), resource_url&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="640" y="560" width="290" height="130" as="geometry"/>
</mxCell>
<mxCell id="16" value="cl_course_tag&lt;br&gt;PK tag_id&lt;br&gt;tag_name, tag_type&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="50" y="420" width="230" height="110" as="geometry"/>
</mxCell>
<mxCell id="17" value="cl_course_tag_rel&lt;br&gt;PK (course_id, tag_id)&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;FK tag_id -&gt; cl_course_tag.tag_id&lt;br&gt;tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="330" y="410" width="270" height="120" as="geometry"/>
</mxCell>
<mxCell id="18" value="cl_learning_session&lt;br&gt;PK session_id&lt;br&gt;FK user_id -&gt; tb_sys_user.user_id&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;status(STARTED/PAUSED/COMPLETED)&lt;br&gt;started_at, ended_at, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="950" y="450" width="300" height="140" as="geometry"/>
</mxCell>
<mxCell id="19" value="cl_learning_progress&lt;br&gt;PK progress_id&lt;br&gt;FK session_id -&gt; cl_learning_session.session_id&lt;br&gt;FK lesson_id -&gt; cl_course_lesson.lesson_id&lt;br&gt;progress_pct, last_position_sec&lt;br&gt;mastery_level, tenant_id, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="1880" y="295" width="320" height="140" as="geometry"/>
</mxCell>
<mxCell id="20" value="cl_learning_event&lt;br&gt;PK event_id&lt;br&gt;FK session_id -&gt; cl_learning_session.session_id&lt;br&gt;event_type(start/pause/seek/finish)&lt;br&gt;event_time, payload_json, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="950" y="750" width="300" height="130" as="geometry"/>
</mxCell>
<mxCell id="21" value="设计方法与原因&lt;br&gt;1) 聚合根Course / LearningSession&lt;br&gt;2) 状态机Session状态驱动进度写入&lt;br&gt;3) 标签与知识点解耦,支持多维检索&lt;br&gt;4) 事件明细单表追加,兼顾审计与回放&lt;br&gt;5) 知识点/学习画像变化可通过 ai.tb_ai_knowledge_sync_task 异步同步" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="10" y="850" width="840" height="120" as="geometry"/>
</mxCell>
<mxCell id="22" value="外部同步依赖ai&lt;br&gt;ai.tb_ai_knowledge_file&lt;br&gt;ai.tb_ai_knowledge_sync_task&lt;br&gt;ai.tb_ai_graph_entity / ai.tb_ai_graph_relation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="1320" y="760" width="330" height="150" as="geometry"/>
</mxCell>
<mxCell id="23" value="cl_kp_prerequisite_rel&lt;br&gt;PK (kp_id, pre_kp_id)&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;FK pre_kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1320" y="150" width="340" height="120" as="geometry"/>
</mxCell>
<mxCell id="24" value="cl_kp_material_rel&lt;br&gt;PK (kp_id, resource_id)&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;FK resource_id -&gt; cl_course_resource.resource_id&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1350" y="565" width="360" height="120" as="geometry"/>
</mxCell>
<mxCell id="25" value="cl_student_kp_mastery&lt;br&gt;PK mastery_id&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;student_id, mastery_level, mastery_score&lt;br&gt;last_practiced_at, tenant_id, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="1320" y="420" width="360" height="120" as="geometry"/>
</mxCell>
<mxCell id="100" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="10" target="11" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="101" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="11" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="102" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="12" target="15" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="103" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="10" target="14" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="104" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="13" target="14" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="105" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="11" target="17" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="106" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="16" target="17" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="107" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="10" target="18" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="108" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="18" target="19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="109" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="12" target="19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="110" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="18" target="20" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="112" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="13" target="23" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="113" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="13" target="24" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="114" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="15" target="24" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="115" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="13" target="25" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="116" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="19" target="25" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="cl_course_chapter&lt;br&gt;PK chapter_id&lt;br&gt;FK course_id -&gt; cl_course.course_id&lt;br&gt;chapter_no, chapter_title&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="340" y="240" width="250" height="130" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -8,6 +8,9 @@
<mxCell id="2" value="01 课程学习 - 对象类数据流图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="460" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="知识点/学习进度相关事件可入 ai 同步任务表,由调度器以 REST/JAR 模式驱动向 Milvus + NebulaGraph/Neo4j 同步" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=12;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="1300" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="Actor&lt;br&gt;学生" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="40" y="130" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="11" value="Actor&lt;br&gt;教师" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="40" y="240" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="12" value="Actor&lt;br&gt;机构管理员" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"><mxGeometry x="40" y="350" width="120" height="70" as="geometry"/></mxCell>
@@ -20,9 +23,12 @@
<mxCell id="41" value="Entity&lt;br&gt;KnowledgeIndexAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="800" y="210" width="220" height="70" as="geometry"/></mxCell>
<mxCell id="42" value="Entity&lt;br&gt;LearningSessionAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="800" y="310" width="230" height="70" as="geometry"/></mxCell>
<mxCell id="43" value="Entity&lt;br&gt;LearningProgressAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="800" y="410" width="240" height="70" as="geometry"/></mxCell>
<mxCell id="44" value="Entity&lt;br&gt;AiSyncTaskAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="800" y="510" width="240" height="70" as="geometry"/></mxCell>
<mxCell id="50" value="PostgreSQL(course/learning)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="170" width="240" height="90" as="geometry"/></mxCell>
<mxCell id="51" value="Redis(学习进度热点缓存)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="320" width="240" height="90" as="geometry"/></mxCell>
<mxCell id="60" value="设计方法与原因&lt;br&gt;1) CQRS课程目录读取与学习进度写入分离&lt;br&gt;2) 会话聚合驱动事件化进度更新&lt;br&gt;3) 热点进度走Redis减轻主库读压&lt;br&gt;4) 课程实体与知识点索引分离,支持后续多策略检索" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxCell id="52" value="PostgreSQL(ai sync task/config)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="470" width="260" height="90" as="geometry"/></mxCell>
<mxCell id="53" value="Milvus + NebulaGraph/Neo4j" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="620" width="260" height="90" as="geometry"/></mxCell>
<mxCell id="60" value="设计方法与原因&lt;br&gt;1) CQRS课程目录读取与学习进度写入分离&lt;br&gt;2) 会话聚合驱动事件化进度更新&lt;br&gt;3) 热点进度走Redis减轻主库读压&lt;br&gt;4) 课程实体与知识点索引分离,支持后续多策略检索&lt;br&gt;5) 同步调度支持 JAR 本地执行与 REST 远程触发" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1360" y="170" width="360" height="190" as="geometry"/>
</mxCell>
<mxCell id="100" value="浏览课程/课时" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="10" target="20" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
@@ -40,6 +46,9 @@
<mxCell id="112" value="持久化" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="42" target="50" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="113" value="持久化/缓存" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="43" target="50" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="114" value="热点缓存" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="43" target="51" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="115" value="同步任务入队" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=1;" parent="1" source="32" target="44" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="116" value="落库" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="44" target="52" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="117" value="REST/JAR调度执行" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=1;" parent="1" source="44" target="53" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>

View File

@@ -1,38 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-04-14T10:43:00.000Z" agent="Oz" version="24.7.17">
<diagram id="homework-er" name="ER图">
<mxfile host="app.diagrams.net" modified="2026-04-15T07:43:00.000Z" agent="Oz" version="24.7.17">
<diagram id="homework-er-v2" name="ER图">
<mxGraphModel dx="1800" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="02 习题与作业 - ER图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="400" height="30" as="geometry"/>
<mxCell id="2" value="02 习题与作业 - ER图question单schema10_create + 20_init" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="520" height="30" as="geometry"/>
</mxCell>
<mxCell id="10" value="hw_question_bank&lt;br&gt;PK bank_id&lt;br&gt;bank_name, subject_code, grade_code&lt;br&gt;status, tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="40" y="110" width="240" height="120" as="geometry"/></mxCell>
<mxCell id="11" value="hw_question_item&lt;br&gt;PK question_id&lt;br&gt;FK bank_id -&gt; hw_question_bank.bank_id&lt;br&gt;question_type, stem, difficulty&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="310" y="110" width="270" height="130" as="geometry"/></mxCell>
<mxCell id="12" value="hw_question_version&lt;br&gt;PK version_id&lt;br&gt;FK question_id -&gt; hw_question_item.question_id&lt;br&gt;version_no, answer_key, analysis&lt;br&gt;is_current, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="610" y="110" width="280" height="130" as="geometry"/></mxCell>
<mxCell id="13" value="hw_question_kp_rel&lt;br&gt;PK (question_id, kp_id)&lt;br&gt;FK question_id -&gt; hw_question_item.question_id&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;weight, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="920" y="110" width="280" height="130" as="geometry"/></mxCell>
<mxCell id="14" value="hw_paper&lt;br&gt;PK paper_id&lt;br&gt;paper_name, subject_code, total_score&lt;br&gt;tenant_id, created_by, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="40" y="290" width="240" height="120" as="geometry"/></mxCell>
<mxCell id="15" value="hw_paper_question&lt;br&gt;PK (paper_id, question_id)&lt;br&gt;FK paper_id -&gt; hw_paper.paper_id&lt;br&gt;FK question_id -&gt; hw_question_item.question_id&lt;br&gt;question_order, score, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="310" y="290" width="300" height="130" as="geometry"/></mxCell>
<mxCell id="16" value="hw_assignment&lt;br&gt;PK assignment_id&lt;br&gt;FK paper_id -&gt; hw_paper.paper_id&lt;br&gt;title, publish_time, deadline&lt;br&gt;status(DRAFT/PUBLISHED/CLOSED)&lt;br&gt;tenant_id, created_by" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="640" y="290" width="300" height="150" as="geometry"/></mxCell>
<mxCell id="17" value="hw_assignment_target&lt;br&gt;PK target_id&lt;br&gt;FK assignment_id -&gt; hw_assignment.assignment_id&lt;br&gt;target_type(class/student)&lt;br&gt;target_ref_id, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="970" y="290" width="280" height="130" as="geometry"/></mxCell>
<mxCell id="18" value="hw_submission&lt;br&gt;PK submission_id&lt;br&gt;FK assignment_id -&gt; hw_assignment.assignment_id&lt;br&gt;FK student_id -&gt; tb_sys_user.user_id&lt;br&gt;submit_time, used_seconds&lt;br&gt;status(SUBMITTED/RESUBMITTED)&lt;br&gt;tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"><mxGeometry x="1280" y="290" width="330" height="150" as="geometry"/></mxCell>
<mxCell id="19" value="hw_submission_answer&lt;br&gt;PK answer_id&lt;br&gt;FK submission_id -&gt; hw_submission.submission_id&lt;br&gt;FK question_id -&gt; hw_question_item.question_id&lt;br&gt;answer_content, answer_type, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"><mxGeometry x="1280" y="470" width="330" height="130" as="geometry"/></mxCell>
<mxCell id="20" value="hw_submission_attachment&lt;br&gt;PK attachment_id&lt;br&gt;FK submission_id -&gt; hw_submission.submission_id&lt;br&gt;file_type, object_key, file_hash&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"><mxGeometry x="920" y="470" width="330" height="130" as="geometry"/></mxCell>
<mxCell id="21" value="设计方法与原因&lt;br&gt;1) 题目版本化:保障历史作业可追溯&lt;br&gt;2) 试卷与作业分离:一份试卷可多次发布&lt;br&gt;3) 发布对象独立表:支持按班级/学员精细投放&lt;br&gt;4) 提交主表+答案明细:便于后续批改并行处理" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="40" y="470" width="840" height="130" as="geometry"/>
<mxCell id="3" value="说明:question 目录仅保留 2 个 SQL 文件10_create_question_tables.sql / 20_init_question_seed.sql" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=12;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="1100" height="24" as="geometry"/>
</mxCell>
<mxCell id="100" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="11" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="101" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="12" target="11" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="102" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="13" target="11" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="103" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="15" target="14" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="104" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="15" target="11" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="105" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="16" target="14" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="106" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="17" target="16" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="107" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="18" target="16" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="108" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="19" target="18" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="109" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="19" target="11" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="110" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="20" target="18" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="10" value="question.hw_question_bank&lt;br&gt;PK bank_id&lt;br&gt;bank_name, subject_code, grade_code, status&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="40" y="90" width="270" height="120" as="geometry"/>
</mxCell>
<mxCell id="11" value="question.hw_question_item&lt;br&gt;PK question_id&lt;br&gt;FK bank_id -&gt; hw_question_bank.bank_id&lt;br&gt;question_type, stem, stem_json, difficulty&lt;br&gt;answer_payload, analysis, scoring_rule_json&lt;br&gt;question_status, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="350" y="80" width="380" height="170" as="geometry"/>
</mxCell>
<mxCell id="12" value="question.hw_question_kp_rel&lt;br&gt;PK (question_id, kp_id)&lt;br&gt;FK question_id -&gt; hw_question_item.question_id&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;relation_type, confidence, graph_relation_id, source_table/source_pk&lt;br&gt;tenant_id, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="780" y="90" width="320" height="130" as="geometry"/>
</mxCell>
<mxCell id="21" value="外部同步依赖ai&lt;br&gt;ai.tb_ai_knowledge_file&lt;br&gt;ai.tb_ai_knowledge_sync_task&lt;br&gt;ai.tb_ai_graph_entity / ai.tb_ai_graph_relation&lt;br&gt;Target: Milvus + NebulaGraph/Neo4j" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="40" y="890" width="370" height="150" as="geometry"/>
</mxCell>
<mxCell id="13" value="question.hw_paper&lt;br&gt;PK paper_id&lt;br&gt;paper_name, subject_code, total_score&lt;br&gt;grading_policy_json&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="40" y="300" width="290" height="130" as="geometry"/>
</mxCell>
<mxCell id="14" value="question.hw_paper_question&lt;br&gt;PK (paper_id, question_id)&lt;br&gt;question_order, score, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="380" y="320" width="310" height="120" as="geometry"/>
</mxCell>
<mxCell id="15" value="question.hw_assignment&lt;br&gt;PK assignment_id&lt;br&gt;FK paper_id -&gt; hw_paper.paper_id&lt;br&gt;title, publish_time, deadline, status&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="740" y="300" width="340" height="140" as="geometry"/>
</mxCell>
<mxCell id="16" value="question.hw_assignment_target&lt;br&gt;PK target_id&lt;br&gt;FK assignment_id -&gt; hw_assignment.assignment_id&lt;br&gt;target_type, target_ref_id, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="1130" y="300" width="320" height="130" as="geometry"/>
</mxCell>
<mxCell id="17" value="question.hw_submission&lt;br&gt;PK submission_id&lt;br&gt;FK assignment_id -&gt; hw_assignment.assignment_id&lt;br&gt;FK student_id -&gt; tb_sys_user.user_id&lt;br&gt;submit_time, used_seconds, status, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="740" y="500" width="360" height="150" as="geometry"/>
</mxCell>
<mxCell id="18" value="question.hw_submission_answer&lt;br&gt;PK answer_id&lt;br&gt;FK submission_id -&gt; hw_submission.submission_id&lt;br&gt;FK question_id -&gt; hw_question_item.question_id&lt;br&gt;answer_type, answer_payload&lt;br&gt;file_id, file_type, tenant_id&lt;br&gt;UK (submission_id, question_id)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="1140" y="500" width="390" height="170" as="geometry"/>
</mxCell>
<mxCell id="19" value="upms.tb_sys_file&lt;br&gt;PK file_id&lt;br&gt;media_type, object_key, file_hash&lt;br&gt;uploaded_by, tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1140" y="730" width="320" height="120" as="geometry"/>
</mxCell>
<mxCell id="30" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="10" target="11" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="31" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="11" target="12" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="32" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="13" target="14" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="33" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="11" target="14" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="34" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="13" target="15" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="35" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="15" target="16" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="36" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="15" target="17" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="37" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="17" target="18" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="38" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="19" target="18" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>

View File

@@ -5,24 +5,30 @@
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="02 习题与作业 - 对象类数据流图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxCell id="2" value="02 习题与作业 - 对象类数据流图question单schema" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="500" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="question 仅 10/20 两个 SQL提交成功后除 MQ 事件外,同步写入 ai 任务表,通过 REST/JAR 调度同步到 Milvus + NebulaGraph/Neo4j" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=12;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="1400" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="Actor&lt;br&gt;教师" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="40" y="150" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="11" value="Actor&lt;br&gt;学生" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="40" y="300" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="20" value="Boundary&lt;br&gt;TeacherHomeworkBoundary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="230" y="130" width="220" height="90" as="geometry"/></mxCell>
<mxCell id="21" value="Boundary&lt;br&gt;StudentHomeworkBoundary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="230" y="280" width="220" height="90" as="geometry"/></mxCell>
<mxCell id="20" value="Boundary&lt;br&gt;TeacherQuestionBoundary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="230" y="130" width="220" height="90" as="geometry"/></mxCell>
<mxCell id="21" value="Boundary&lt;br&gt;StudentQuestionBoundary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="230" y="280" width="220" height="90" as="geometry"/></mxCell>
<mxCell id="30" value="Control&lt;br&gt;QuestionBankService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="90" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="31" value="Control&lt;br&gt;AssignmentPublishService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="200" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="32" value="Control&lt;br&gt;HomeworkQueryService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="310" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="32" value="Control&lt;br&gt;QuestionQueryService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="310" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="33" value="Control&lt;br&gt;SubmissionService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="420" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="40" value="Entity&lt;br&gt;QuestionBankAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="810" y="90" width="220" height="70" as="geometry"/></mxCell>
<mxCell id="41" value="Entity&lt;br&gt;AssignmentAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="810" y="200" width="220" height="70" as="geometry"/></mxCell>
<mxCell id="42" value="Entity&lt;br&gt;AssignmentTargetAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="810" y="300" width="240" height="70" as="geometry"/></mxCell>
<mxCell id="43" value="Entity&lt;br&gt;SubmissionAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="810" y="410" width="220" height="70" as="geometry"/></mxCell>
<mxCell id="50" value="PostgreSQL(question/homework)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="170" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="44" value="Entity&lt;br&gt;AiSyncTaskAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="810" y="520" width="240" height="70" as="geometry"/></mxCell>
<mxCell id="50" value="PostgreSQL(question)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="170" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="51" value="ObjectStorage(附件/图片/PDF)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="320" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="52" value="MQ(SubmissionCreatedEvent)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="450" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="53" value="PostgreSQL(ai sync task/config)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="580" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="54" value="Milvus + NebulaGraph/Neo4j" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="710" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="60" value="设计方法与原因&lt;br&gt;1) 发布链路与作答链路拆分,降低接口复杂度&lt;br&gt;2) 提交成功后发事件,解耦后续批改流程&lt;br&gt;3) 附件文件与答案结构分存,便于扩展多题型&lt;br&gt;4) 题库聚合与作业聚合分离,支持独立演进" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1380" y="200" width="330" height="190" as="geometry"/>
</mxCell>
@@ -41,6 +47,9 @@
<mxCell id="112" value="持久化" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="43" target="50" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="113" value="附件存储" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="43" target="51" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="114" value="异步事件" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="43" target="52" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="115" value="同步任务入队" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=1;" parent="1" source="33" target="44" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="116" value="落库" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="44" target="53" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="117" value="REST/JAR调度执行" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=1;" parent="1" source="44" target="54" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>

View File

@@ -1,35 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-04-14T10:44:00.000Z" agent="Oz" version="24.7.17">
<diagram id="grading-er" name="ER图">
<mxGraphModel dx="1800" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="03 批改与反馈 - ER图不含AI实现细节" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="560" height="30" as="geometry"/>
</mxCell>
<mxCell id="10" value="gd_grading_task&lt;br&gt;PK grading_task_id&lt;br&gt;FK submission_id -&gt; hw_submission.submission_id&lt;br&gt;status(PENDING/RUNNING/WAIT_REVIEW/DONE)&lt;br&gt;trigger_source, tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="40" y="120" width="330" height="140" as="geometry"/></mxCell>
<mxCell id="11" value="gd_grading_rule_set&lt;br&gt;PK rule_set_id&lt;br&gt;rule_version, subject_code, objective_policy&lt;br&gt;subjective_policy(manual_first)&lt;br&gt;tenant_id, enabled" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="400" y="120" width="320" height="130" as="geometry"/></mxCell>
<mxCell id="12" value="gd_objective_score&lt;br&gt;PK objective_score_id&lt;br&gt;FK grading_task_id -&gt; gd_grading_task.grading_task_id&lt;br&gt;FK answer_id -&gt; hw_submission_answer.answer_id&lt;br&gt;score, matched_rule, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="750" y="120" width="340" height="140" as="geometry"/></mxCell>
<mxCell id="13" value="gd_subjective_review&lt;br&gt;PK review_id&lt;br&gt;FK grading_task_id -&gt; gd_grading_task.grading_task_id&lt;br&gt;FK answer_id -&gt; hw_submission_answer.answer_id&lt;br&gt;reviewer_id, review_score, status, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"><mxGeometry x="1120" y="120" width="350" height="140" as="geometry"/></mxCell>
<mxCell id="14" value="gd_score_summary&lt;br&gt;PK summary_id&lt;br&gt;FK grading_task_id -&gt; gd_grading_task.grading_task_id&lt;br&gt;total_score, grade_level(A/B/C)&lt;br&gt;surpass_ratio, used_seconds, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="40" y="300" width="330" height="140" as="geometry"/></mxCell>
<mxCell id="15" value="gd_error_tag&lt;br&gt;PK error_tag_id&lt;br&gt;tag_code, tag_name&lt;br&gt;category(审题/计算/概念)&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="400" y="300" width="300" height="130" as="geometry"/></mxCell>
<mxCell id="16" value="gd_answer_error_rel&lt;br&gt;PK (answer_id, error_tag_id)&lt;br&gt;FK answer_id -&gt; hw_submission_answer.answer_id&lt;br&gt;FK error_tag_id -&gt; gd_error_tag.error_tag_id&lt;br&gt;confidence, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="730" y="300" width="330" height="130" as="geometry"/></mxCell>
<mxCell id="17" value="gd_wrong_question&lt;br&gt;PK wrong_question_id&lt;br&gt;FK student_id -&gt; tb_sys_user.user_id&lt;br&gt;FK question_id -&gt; hw_question_item.question_id&lt;br&gt;source_submission_id, mastery_status&lt;br&gt;review_count, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="1090" y="300" width="350" height="150" as="geometry"/></mxCell>
<mxCell id="18" value="gd_review_plan&lt;br&gt;PK review_plan_id&lt;br&gt;FK wrong_question_id -&gt; gd_wrong_question.wrong_question_id&lt;br&gt;plan_date, plan_stage(E1/E2/E3)&lt;br&gt;status, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="40" y="490" width="330" height="130" as="geometry"/></mxCell>
<mxCell id="19" value="gd_teacher_comment&lt;br&gt;PK comment_id&lt;br&gt;FK review_id -&gt; gd_subjective_review.review_id&lt;br&gt;comment_type(text/voice), content_ref&lt;br&gt;reviewer_id, tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1"><mxGeometry x="400" y="490" width="340" height="130" as="geometry"/></mxCell>
<mxCell id="20" value="设计方法与原因&lt;br&gt;1) 批改任务聚合承接状态机,保证流程一致性&lt;br&gt;2) 客观分与主观复核拆表,支持并行与补录&lt;br&gt;3) 错因标签与错题沉淀解耦,便于策略演进&lt;br&gt;4) 仅保留 GradingEnginePort 抽象不绑定AI实现" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
<mxGeometry x="780" y="500" width="660" height="130" as="geometry"/>
</mxCell>
<mxCell id="100" value="N:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="10" target="11" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="101" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="12" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="102" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="13" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="103" value="1:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="14" target="10" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="104" value="M:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="16" target="15" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="105" value="衍生" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="17" target="14" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="106" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="18" target="17" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="107" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="19" target="13" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
<mxfile host="65bd71144e">
<diagram id="grading-er-v2" name="ER图">
<mxGraphModel dx="1312" dy="773" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="03 批改与反馈 - ER图question单schema含知识点分析与AI同步" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="560" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="说明:批改与反馈表统一在 question/10_create_question_tables.sql 中定义" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=12;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="900" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="question.gd_grading_task&lt;br&gt;PK grading_task_id&lt;br&gt;FK submission_id -&gt; hw_submission.submission_id&lt;br&gt;paper_id, task_status, trigger_source&lt;br&gt;tenant_id, created_at, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="25" y="110" width="330" height="140" as="geometry"/>
</mxCell>
<mxCell id="11" value="question.gd_answer_grade&lt;br&gt;PK answer_grade_id&lt;br&gt;FK grading_task_id -&gt; gd_grading_task.grading_task_id&lt;br&gt;FK answer_id -&gt; hw_submission_answer.answer_id&lt;br&gt;grade_mode, grade_status, score, grader_id&lt;br&gt;evidence_json, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="420" y="90" width="360" height="150" as="geometry"/>
</mxCell>
<mxCell id="12" value="question.gd_score_summary&lt;br&gt;PK summary_id&lt;br&gt;UK grading_task_id -&gt; gd_grading_task.grading_task_id&lt;br&gt;total_score, grade_level, surpass_ratio, used_seconds&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="830" y="90" width="340" height="140" as="geometry"/>
</mxCell>
<mxCell id="13" value="question.gd_error_tag&lt;br&gt;PK error_tag_id&lt;br&gt;tag_code, tag_name, category&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1">
<mxGeometry x="40" y="300" width="300" height="120" as="geometry"/>
</mxCell>
<mxCell id="14" value="question.gd_answer_error_rel&lt;br&gt;PK (answer_id, error_tag_id)&lt;br&gt;FK answer_id -&gt; hw_submission_answer.answer_id&lt;br&gt;FK error_tag_id -&gt; gd_error_tag.error_tag_id&lt;br&gt;confidence, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="390" y="300" width="350" height="130" as="geometry"/>
</mxCell>
<mxCell id="15" value="question.gd_wrong_question&lt;br&gt;PK wrong_question_id&lt;br&gt;student_id, question_id, source_submission_id&lt;br&gt;mastery_status, review_count&lt;br&gt;tenant_id, created_at, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="790" y="290" width="350" height="140" as="geometry"/>
</mxCell>
<mxCell id="16" value="question.gd_review_plan&lt;br&gt;PK review_plan_id&lt;br&gt;FK wrong_question_id -&gt; gd_wrong_question.wrong_question_id&lt;br&gt;plan_date, plan_stage, plan_status&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1">
<mxGeometry x="1190" y="290" width="340" height="130" as="geometry"/>
</mxCell>
<mxCell id="17" value="question.gd_teacher_comment&lt;br&gt;PK comment_id&lt;br&gt;FK answer_grade_id -&gt; gd_answer_grade.answer_grade_id&lt;br&gt;reviewer_id, comment_type, content_ref&lt;br&gt;tenant_id, created_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" parent="1" vertex="1">
<mxGeometry x="420" y="480" width="350" height="130" as="geometry"/>
</mxCell>
<mxCell id="18" value="question.gd_explanation_submission&lt;br&gt;PK explanation_id&lt;br&gt;student_id, wrong_question_id, source_answer_id&lt;br&gt;audio_file_id, transcript_text, submission_status&lt;br&gt;tenant_id, created_at, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="40" y="500" width="340" height="150" as="geometry"/>
</mxCell>
<mxCell id="19" value="question.gd_explanation_assessment&lt;br&gt;PK assessment_id&lt;br&gt;UK explanation_id -&gt; gd_explanation_submission.explanation_id&lt;br&gt;evaluator_type, total_score, pass_status&lt;br&gt;improvement_suggestion, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="830" y="500" width="360" height="140" as="geometry"/>
</mxCell>
<mxCell id="20" value="question.gd_explanation_dimension_score&lt;br&gt;PK dimension_score_id&lt;br&gt;FK assessment_id -&gt; gd_explanation_assessment.assessment_id&lt;br&gt;dimension_code, dimension_name&lt;br&gt;score, weight, tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="1240" y="500" width="340" height="140" as="geometry"/>
</mxCell>
<mxCell id="23" value="question.gd_answer_kp_analysis&lt;br&gt;PK analysis_id&lt;br&gt;FK answer_id -&gt; hw_submission_answer.answer_id&lt;br&gt;FK kp_id -&gt; cl_knowledge_point.kp_id&lt;br&gt;correctness, mastery_score, confidence&lt;br&gt;evidence_json, tenant_id, updated_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="1240" y="90" width="360" height="160" as="geometry"/>
</mxCell>
<mxCell id="21" value="upms.tb_sys_message&lt;br&gt;PK message_id&lt;br&gt;message_type, biz_type, content_object_id&lt;br&gt;web_jump_url, tenant_id, send_at" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="40" y="710" width="330" height="120" as="geometry"/>
</mxCell>
<mxCell id="22" value="upms.tb_sys_message_recipient&lt;br&gt;PK (message_id, recipient_user_id)&lt;br&gt;FK message_id -&gt; tb_sys_message.message_id&lt;br&gt;read_status, read_at, clicked_at&lt;br&gt;tenant_id" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="430" y="710" width="360" height="120" as="geometry"/>
</mxCell>
<mxCell id="24" value="外部同步依赖ai&lt;br&gt;ai.tb_ai_knowledge_file&lt;br&gt;ai.tb_ai_knowledge_sync_task&lt;br&gt;ai.tb_ai_graph_entity / ai.tb_ai_graph_relation&lt;br&gt;Target: Milvus + NebulaGraph/Neo4j" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" parent="1" vertex="1">
<mxGeometry x="820" y="710" width="360" height="150" as="geometry"/>
</mxCell>
<mxCell id="30" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="10" target="11" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="31" value="1:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="10" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="32" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="13" target="14" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="33" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="12" target="15" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="34" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="15" target="16" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="35" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="11" target="17" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="36" value="1:1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="18" target="19" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="37" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="19" target="20" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="38" value="触发通知" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="11" target="21" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="39" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="21" target="22" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="40" value="1:N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;exitPerimeter=1;entryPerimeter=1;" parent="1" source="11" target="23" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -5,27 +5,36 @@
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="03 批改与反馈 - 对象类数据流图(规则批改 + 教师复核" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxCell id="2" value="03 批改与反馈 - 对象类数据流图(question单schema" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=22;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="20" y="20" width="700" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="批改结果会触发知识点分析同步任务;调度器支持 REST/JAR 两种模式JAR 本地执行 + REST 远程触发),目标为 Milvus + NebulaGraph/Neo4j" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=12;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="20" y="52" width="1400" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="Actor&lt;br&gt;学生" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" parent="1" vertex="1"><mxGeometry x="40" y="150" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="11" value="Actor&lt;br&gt;教师" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"><mxGeometry x="40" y="300" width="120" height="70" as="geometry"/></mxCell>
<mxCell id="20" value="Boundary&lt;br&gt;StudentResultBoundary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="230" y="140" width="220" height="90" as="geometry"/></mxCell>
<mxCell id="21" value="Boundary&lt;br&gt;TeacherReviewBoundary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="230" y="290" width="220" height="90" as="geometry"/></mxCell>
<mxCell id="22" value="Boundary&lt;br&gt;MessageInboxBoundary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="230" y="640" width="220" height="90" as="geometry"/></mxCell>
<mxCell id="30" value="Control&lt;br&gt;GradingDispatcher" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="80" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="31" value="Control&lt;br&gt;ObjectiveScoringService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="190" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="32" value="Control&lt;br&gt;ReviewWorkflowService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="300" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="33" value="Control&lt;br&gt;WrongBookService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="410" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="34" value="Control&lt;br&gt;ReviewPlannerService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="520" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="36" value="Control&lt;br&gt;SiteMessageService" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;" parent="1" vertex="1"><mxGeometry x="520" y="640" width="220" height="80" as="geometry"/></mxCell>
<mxCell id="35" value="Port&lt;br&gt;GradingEnginePort&lt;br&gt;(当前规则实现 RuleBasedEngine)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="780" y="80" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="40" value="Entity&lt;br&gt;GradingTaskAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="780" y="210" width="230" height="70" as="geometry"/></mxCell>
<mxCell id="41" value="Entity&lt;br&gt;ObjectiveScoreAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="780" y="310" width="230" height="70" as="geometry"/></mxCell>
<mxCell id="42" value="Entity&lt;br&gt;SubjectiveReviewAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="780" y="410" width="250" height="70" as="geometry"/></mxCell>
<mxCell id="43" value="Entity&lt;br&gt;WrongQuestionAggregate" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="780" y="510" width="240" height="70" as="geometry"/></mxCell>
<mxCell id="50" value="PostgreSQL(grading/wrongbook)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="230" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="44" value="Entity&lt;br&gt;SiteMessageAggregate&lt;br&gt;message_id, biz_type&lt;br&gt;content_object_id, web_jump_url" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1"><mxGeometry x="780" y="640" width="270" height="90" as="geometry"/></mxCell>
<mxCell id="50" value="PostgreSQL(question + ai sync task)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="230" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="51" value="Redis(结果缓存/待复核队列)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="370" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="52" value="MQ(GradingCompletedEvent)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="510" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="60" value="设计方法与原因&lt;br&gt;1) 批改调度、判分、复核、错题沉淀分控制器,职责清晰&lt;br&gt;2) 通过 Port 抽象引擎实现后续可无缝接入AI或外部服务&lt;br&gt;3) 结果完成后事件化通知,支撑复习计划与消息触达&lt;br&gt;4) 主流程先保障规则可用,再逐步增强智能能力" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxCell id="53" value="PostgreSQL(upms.site_message / recipient)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="650" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="54" value="Redis(未读计数缓存)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="770" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="55" value="Milvus + NebulaGraph/Neo4j" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1"><mxGeometry x="1080" y="890" width="250" height="90" as="geometry"/></mxCell>
<mxCell id="60" value="设计方法与原因&lt;br&gt;1) 批改调度、判分、复核、错题沉淀分控制器,职责清晰&lt;br&gt;2) 通过 Port 抽象引擎实现后续可无缝接入AI或外部服务&lt;br&gt;3) 主观题待审阅由站内信服务落库含content_object_id和web_jump_url&lt;br&gt;4) 教师通过收件箱点击跳转到对应审阅页,链路可追踪可回执" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1380" y="230" width="330" height="220" as="geometry"/>
</mxCell>
<mxCell id="100" value="查看批改结果" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="10" target="20" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
@@ -46,6 +55,16 @@
<mxCell id="115" value="持久化" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="43" target="50" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="116" value="待复核缓存" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="42" target="51" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="117" value="完成事件" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="34" target="52" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="118" value="查看站内信" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="11" target="22" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="119" value="待审阅通知触发" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="32" target="36" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="120" value="复习提醒通知触发" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="34" target="36" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="121" value="生成站内信" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="36" target="44" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="122" value="消息落库" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="44" target="53" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="123" value="未读计数缓存" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="44" target="54" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="124" value="已读/点击回执" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="22" target="36" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="125" value="点击消息跳转审阅页" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="22" target="21" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="126" value="返回跳转参数(messageId/objectId/url)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;endArrow=block;endFill=1;" parent="1" source="36" target="22" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="127" value="同步任务执行" style="edgeStyle=orthogonalEdgeStyle;rounded=0;jettySize=auto;html=1;dashed=1;endArrow=block;endFill=1;" parent="1" source="34" target="55" edge="1"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>

View File

@@ -0,0 +1,259 @@
<mxfile host="65bd71144e">
<diagram id="full-business-flow-v3" name="完整业务流程图">
<mxGraphModel dx="2300" dy="1500" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="5600" pageHeight="2300" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="AI智能学习系统完整业务流程图教学业务主线 + 学习闭环 + 管理审计)&#xa;版本:模块上色 + 数据实体标注schema.table+ 图谱/向量同步链路Milvus + NebulaGraph/Neo4j" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=20;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="30" y="20" width="1500" height="46" as="geometry"/>
</mxCell>
<mxCell id="90" value="模块色卡(与 SQL schema 对齐)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="2550" y="20" width="300" height="34" as="geometry"/>
</mxCell>
<mxCell id="91" value="course" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="2550" y="62" width="92" height="28" as="geometry"/>
</mxCell>
<mxCell id="92" value="homework" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="2650" y="62" width="92" height="28" as="geometry"/>
</mxCell>
<mxCell id="93" value="grading" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="2750" y="62" width="92" height="28" as="geometry"/>
</mxCell>
<mxCell id="94" value="ai" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="2850" y="62" width="92" height="28" as="geometry"/>
</mxCell>
<mxCell id="95" value="recommendation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="2950" y="62" width="120" height="28" as="geometry"/>
</mxCell>
<mxCell id="96" value="achievement" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="3080" y="62" width="110" height="28" as="geometry"/>
</mxCell>
<mxCell id="97" value="upms/运营" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="3200" y="62" width="110" height="28" as="geometry"/>
</mxCell>
<mxCell id="10" value="教师主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" vertex="1" parent="1">
<mxGeometry x="30" y="110" width="5400" height="220" as="geometry"/>
</mxCell>
<mxCell id="11" value="学生主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
<mxGeometry x="30" y="360" width="5400" height="260" as="geometry"/>
</mxCell>
<mxCell id="12" value="AI/系统主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
<mxGeometry x="30" y="660" width="5400" height="620" as="geometry"/>
</mxCell>
<mxCell id="13" value="管理运营主线" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=13;fontStyle=1;align=left;spacingLeft=10;" parent="1" vertex="1">
<mxGeometry x="30" y="1320" width="5400" height="300" as="geometry"/>
</mxCell>
<mxCell id="20" value="开始" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="70" y="470" width="110" height="64" as="geometry"/>
</mxCell>
<mxCell id="30" value="教师创建课程&#xa;course.cl_course" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="200" y="170" width="220" height="76" as="geometry"/>
</mxCell>
<mxCell id="31" value="课程绑定知识点&#xa;course.cl_course_knowledge_rel&#xa;course.cl_knowledge_point" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="450" y="160" width="250" height="96" as="geometry"/>
</mxCell>
<mxCell id="32" value="组卷生成 Paper&#xa;question.hw_paper&#xa;question.hw_paper_question" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="730" y="160" width="250" height="96" as="geometry"/>
</mxCell>
<mxCell id="33" value="发布作业到班级&#xa;question.hw_assignment&#xa;question.hw_assignment_target" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1010" y="160" width="250" height="96" as="geometry"/>
</mxCell>
<mxCell id="40" value="学生接收作业&#xa;upms.tb_school_class_member&#xa;question.hw_assignment_target" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1180" y="440" width="250" height="92" as="geometry"/>
</mxCell>
<mxCell id="41" value="学生答题并提交(文本/图片/音频)&#xa;question.hw_submission&#xa;question.hw_submission_answer / upms.tb_sys_file" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1460" y="430" width="320" height="110" as="geometry"/>
</mxCell>
<mxCell id="50" value="创建批改任务&#xa;question.gd_grading_task" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="1780" y="760" width="240" height="76" as="geometry"/>
</mxCell>
<mxCell id="51" value="AI-OCR 解析&#xa;ai.tb_ai_knowledge_file / ai.tb_ai_knowledge_sync_task" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2050" y="760" width="210" height="76" as="geometry"/>
</mxCell>
<mxCell id="52" value="AI-LLM 批改与错因标签&#xa;question.gd_answer_grade&#xa;question.gd_answer_error_rel / question.gd_error_tag&#xa;ai.tb_ai_knowledge_sync_task + ai.tb_ai_graph_entity + ai.tb_ai_graph_relation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2290" y="744" width="330" height="108" as="geometry"/>
</mxCell>
<mxCell id="53" value="主观题待复核?&#xa;question.gd_answer_grade.grade_mode" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2660" y="736" width="220" height="122" as="geometry"/>
</mxCell>
<mxCell id="54" value="教师复核/点评&#xa;question.gd_teacher_comment" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2860" y="170" width="220" height="76" as="geometry"/>
</mxCell>
<mxCell id="55" value="成绩汇总(总分/等级)&#xa;question.gd_score_summary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2920" y="760" width="240" height="76" as="geometry"/>
</mxCell>
<mxCell id="56" value="错题沉淀入错题本&#xa;question.gd_wrong_question" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3190" y="760" width="240" height="76" as="geometry"/>
</mxCell>
<mxCell id="57" value="错题状态流转(待学习/复习中/已掌握)&#xa;question.gd_wrong_question&#xa;question.gd_review_plan + ai.tb_ai_knowledge_sync_task" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3460" y="744" width="280" height="108" as="geometry"/>
</mxCell>
<mxCell id="60" value="AI生成举一反三变式题&#xa;ai.tb_ai_knowledge_sync_task&#xa;recommendation.rc_recommendation_item" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3190" y="430" width="280" height="110" as="geometry"/>
</mxCell>
<mxCell id="61" value="学生变式题练习&#xa;question.hw_submission&#xa;question.hw_submission_answer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3500" y="440" width="250" height="92" as="geometry"/>
</mxCell>
<mxCell id="62" value="是否掌握?&#xa;question.gd_wrong_question.mastery_status" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3780" y="428" width="200" height="110" as="geometry"/>
</mxCell>
<mxCell id="63" value="生成艾宾浩斯复习计划&#xa;question.gd_review_plan" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="4010" y="432" width="240" height="92" as="geometry"/>
</mxCell>
<mxCell id="64" value="到点提醒与复习反馈&#xa;upms.tb_sys_message&#xa;recommendation.rc_learning_loop_event" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="4250" y="900" width="260" height="92" as="geometry"/>
</mxCell>
<mxCell id="65" value="推荐召回与排序&#xa;recommendation.rc_recommendation_task&#xa;recommendation.rc_recommendation_item" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="4250" y="1000" width="260" height="92" as="geometry"/>
</mxCell>
<mxCell id="66" value="推送微课/专项训练/同类错题&#xa;recommendation.rc_recommendation_item" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="4250" y="1110" width="260" height="80" as="geometry"/>
</mxCell>
<mxCell id="67" value="学生反馈(曝光/点击/完成)&#xa;recommendation.rc_recommendation_feedback" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="4250" y="1210" width="260" height="80" as="geometry"/>
</mxCell>
<mxCell id="68" value="费曼讲解任务触发&#xa;recommendation.rc_learning_loop_event" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2920" y="430" width="240" height="92" as="geometry"/>
</mxCell>
<mxCell id="69" value="学生语音讲解提交&#xa;question.gd_explanation_submission&#xa;upms.tb_sys_file" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2920" y="540" width="240" height="92" as="geometry"/>
</mxCell>
<mxCell id="70" value="AI语音评测ASR/表达评分/建议)&#xa;question.gd_explanation_assessment&#xa;question.gd_explanation_dimension_score&#xa;ai.tb_ai_knowledge_sync_task" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3190" y="540" width="300" height="110" as="geometry"/>
</mxCell>
<mxCell id="71" value="画像与闭环引擎&#xa;recommendation.rc_student_profile_snapshot&#xa;recommendation.rc_student_profile_feature&#xa;recommendation.rc_learning_loop_case / rc_learning_loop_event" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3400" y="980" width="330" height="120" as="geometry"/>
</mxCell>
<mxCell id="72" value="规则模板/事件字典/阶段映射&#xa;achievement.ac_achievement_event_dict&#xa;achievement.ac_achievement_metric_def&#xa;achievement.ac_achievement_rule_template&#xa;recommendation.rc_loop_stage_achievement_map / rc_loop_metric_map" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=11;" parent="1" vertex="1">
<mxGeometry x="3400" y="1120" width="330" height="140" as="geometry"/>
</mxCell>
<mxCell id="73" value="成就计算与发放(激励数据=成就数据)&#xa;achievement.ac_user_achievement&#xa;achievement.ac_user_achievement_progress" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3770" y="980" width="310" height="104" as="geometry"/>
</mxCell>
<mxCell id="74" value="成长轨迹/指标更新&#xa;achievement.ac_user_achievement_daily_fact&#xa;recommendation.rc_learning_loop_effect_daily" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3770" y="1100" width="310" height="104" as="geometry"/>
</mxCell>
<mxCell id="80" value="机构学情与运营看板&#xa;question.gd_score_summary&#xa;recommendation.rc_recommendation_effect_daily&#xa;achievement.ac_user_achievement_daily_fact" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="2860" y="1390" width="300" height="116" as="geometry"/>
</mxCell>
<mxCell id="81" value="质量监控与异常预警&#xa;question.gd_grading_task&#xa;recommendation.rc_learning_loop_effect_daily" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3200" y="1400" width="300" height="100" as="geometry"/>
</mxCell>
<mxCell id="82" value="审计追溯与报表导出&#xa;ai.tb_ai_knowledge_file / ai.tb_ai_knowledge_sync_task&#xa;upms.tb_sys_message&#xa;recommendation.rc_learning_loop_event" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3540" y="1390" width="320" height="116" as="geometry"/>
</mxCell>
<mxCell id="83" value="结束&#xa;(进入下一轮学习循环)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="3890" y="1400" width="280" height="96" as="geometry"/>
</mxCell>
<mxCell id="100" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="20" target="30" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="101" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="30" target="31" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="102" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="31" target="32" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="103" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="32" target="33" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="104" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="33" target="40" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="105" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="40" target="41" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="106" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="41" target="50" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="107" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="50" target="51" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="108" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="51" target="52" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="109" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="52" target="53" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="110" value="是" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="53" target="54" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="111" value="否" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="53" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="112" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="54" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="113" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="55" target="56" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="114" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="56" target="57" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="115" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="56" target="60" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="116" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="60" target="61" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="117" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="61" target="62" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="118" value="否,继续练" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;dashed=1;" parent="1" source="62" target="60" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="119" value="是" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="62" target="63" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="120" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="63" target="64" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="121" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="64" target="65" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="122" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="65" target="66" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="123" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="66" target="67" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="124" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="57" target="68" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="125" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="68" target="69" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="126" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="69" target="70" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="127" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="67" target="71" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="128" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="70" target="71" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="129" value="配置读取" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="72" target="71" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="130" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="71" target="73" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="131" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="73" target="74" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="132" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="74" target="80" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="133" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="80" target="81" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="134" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="81" target="82" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="135" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;" parent="1" source="82" target="83" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,158 +1,130 @@
<?xml version='1.0' encoding='utf-8'?>
<mxfile host="app.diagrams.net" modified="2026-04-14T09:05:00.000Z" agent="Codex GPT-5" version="24.7.17">
<diagram id="multi-role-flow" name="数据流图(多角色)">
<mxGraphModel dx="1800" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="1200" math="0" shadow="0">
<mxfile host="app.diagrams.net" modified="2026-04-15T07:55:00.000Z" agent="Oz" version="24.7.17">
<diagram id="multi-role-flow-ai" name="数据流图(多角色)">
<mxGraphModel dx="1800" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2000" pageHeight="1300" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="2" value="多角色数据流图" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=24;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="30" y="20" width="220" height="30" as="geometry" />
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="多角色数据流图AI逻辑完整版" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=24;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="30" y="20" width="420" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="入口:微信小程序 / React 后台,处理链路Java 分布式服务 -&gt; Python AI 服务 -&gt; Redis / PostgreSQL" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="30" y="52" width="860" height="22" as="geometry" />
<mxCell id="3" value="入口:微信小程序/React 后台;主链路Java服务编排 -&gt; Python AI服务OCR/LLM/ASR -&gt; PostgreSQL/Redis/对象存储/Milvus/NebulaGraph(Neo4j);同步调度支持 REST/JAR" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" vertex="1" parent="1">
<mxGeometry x="30" y="52" width="1250" height="24" as="geometry"/>
</mxCell>
<mxCell id="10" value="学生" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=15;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="70" y="140" width="160" height="70" as="geometry" />
<mxGeometry x="60" y="120" width="150" height="65" as="geometry"/>
</mxCell>
<mxCell id="11" value="教师" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=15;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="70" y="350" width="160" height="70" as="geometry" />
<mxGeometry x="60" y="250" width="150" height="65" as="geometry"/>
</mxCell>
<mxCell id="12" value="机构管理员" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;fontSize=15;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="70" y="560" width="160" height="70" as="geometry" />
<mxGeometry x="60" y="380" width="150" height="65" as="geometry"/>
</mxCell>
<mxCell id="20" value="P1 接入与身份认证&lt;br&gt;微信小程序 / React -&gt; Java Gateway" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="80" width="290" height="80" as="geometry" />
<mxCell id="20" value="P1 接入与鉴权&#xa;Gateway + Auth + RBAC" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="280" y="90" width="260" height="78" as="geometry"/>
</mxCell>
<mxCell id="21" value="P2 作业 / 资料管理&lt;br&gt;Java 作业服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="205" width="290" height="80" as="geometry" />
<mxCell id="21" value="P2 作业/资料管理&#xa;布置、上传、提交、附件引用" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="280" y="200" width="260" height="78" as="geometry"/>
</mxCell>
<mxCell id="22" value="P3 AI批改与错因分析&lt;br&gt;Java 编排服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="330" width="290" height="80" as="geometry" />
<mxCell id="22" value="P3 批改编排服务&#xa;触发AI子流程、汇总得分/错因" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="280" y="310" width="260" height="78" as="geometry"/>
</mxCell>
<mxCell id="23" value="P4 错题本 / 复习计划&lt;br&gt;Java 复习服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="455" width="290" height="80" as="geometry" />
<mxCell id="23" value="P4 错题本复习计划&#xa;错题沉淀、复习节点评估" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="280" y="420" width="260" height="78" as="geometry"/>
</mxCell>
<mxCell id="24" value="P5 推荐与消息推送&lt;br&gt;Java 推荐服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="340" y="580" width="290" height="80" as="geometry" />
<mxCell id="24" value="P5 推荐与消息触达&#xa;推荐任务、反馈回流、站内信" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="280" y="530" width="260" height="78" as="geometry"/>
</mxCell>
<mxCell id="25" value="P6 Python AI处理服务&lt;br&gt;OCR / 批改 / 语音评测" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="760" y="250" width="300" height="80" as="geometry" />
<mxCell id="25" value="P6 讲解评估与教学干预&#xa;语音讲解评估、教师点评、学情回传" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="280" y="640" width="260" height="78" as="geometry"/>
</mxCell>
<mxCell id="26" value="P7 讲解评估 / 教学干预 / 运营监控&lt;br&gt;Java 教学与运营服务" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="760" y="470" width="330" height="90" as="geometry" />
<mxCell id="26" value="P7 画像与学习闭环引擎&#xa;画像快照、阶段事件、成就触发映射" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="280" y="750" width="260" height="78" as="geometry"/>
</mxCell>
<mxCell id="30" value="D1 PostgreSQL 用户 / 班级库&lt;br&gt;支持分库分表 / 分区" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="70" width="260" height="75" as="geometry" />
<mxCell id="40" value="AI-1 OCR解析服务&#xa;图片增强 / 版面解析 / 文本结构化" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="610" y="220" width="300" height="78" as="geometry"/>
</mxCell>
<mxCell id="31" value="D2 PostgreSQL 作业 / 批改库&lt;br&gt;支持分库分表 / 分区" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="190" width="260" height="75" as="geometry" />
<mxCell id="41" value="AI-2 LLM批改服务&#xa;客观判分 / 主观评估 / 错因标签" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="610" y="320" width="300" height="78" as="geometry"/>
</mxCell>
<mxCell id="32" value="D3 PostgreSQL 错题 / 复习库&lt;br&gt;支持分区归档" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="310" width="260" height="75" as="geometry" />
<mxCell id="42" value="AI-3 讲解评测服务&#xa;ASR转写 / 表达评分 / 改进建议" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="610" y="640" width="300" height="78" as="geometry"/>
</mxCell>
<mxCell id="33" value="D4 知识库 / 题库 / 对象存储" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="430" width="260" height="75" as="geometry" />
<mxCell id="43" value="AI-4 推荐策略服务&#xa;召回 / 排序 / 反馈学习" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="610" y="540" width="300" height="78" as="geometry"/>
</mxCell>
<mxCell id="34" value="D5 Redis Cluster&lt;br&gt;会话、缓存、热数据、推荐结果" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="550" width="260" height="75" as="geometry" />
<mxCell id="44" value="AI-5 模型与任务管理&#xa;model_config / task_log / retrieval_log&#xa;sync_scheduler_config / sync_dispatch_log" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="610" y="760" width="300" height="78" as="geometry"/>
</mxCell>
<mxCell id="35" value="D6 日志 / 审计 / BI" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="1180" y="670" width="260" height="75" as="geometry" />
<mxCell id="60" value="D1 PostgreSQL 用户/组织/班级库&#xa;upms/auth" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="90" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="40" value="登录 / 绑定" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="10" target="20">
<mxGeometry relative="1" as="geometry" />
<mxCell id="61" value="D2 PostgreSQL question题目/作业库&#xa;question.hw_*" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="200" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="41" value="登录 / 班级关系" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="11" target="20">
<mxGeometry relative="1" as="geometry" />
<mxCell id="62" value="D3 PostgreSQL question批改/错题/讲解库&#xa;question.gd_*" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="310" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="42" value="高权限登录" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="12" target="20">
<mxGeometry relative="1" as="geometry" />
<mxCell id="63" value="D4 PostgreSQL 推荐/画像/闭环库&#xa;recommendation" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="420" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="43" value="用户 / 班级 / 权限" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="20" target="30">
<mxGeometry relative="1" as="geometry" />
<mxCell id="64" value="D5 PostgreSQL 成就库&#xa;achievement激励=成就)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="530" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="44" value="会话 / Token" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="20" target="34">
<mxGeometry relative="1" as="geometry" />
<mxCell id="65" value="D6 Redis缓存层&#xa;会话/推荐缓存/未读计数" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="640" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="45" value="发布作业 / 资料" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="11" target="21">
<mxGeometry relative="1" as="geometry" />
<mxCell id="66" value="D7 对象存储&#xa;文件/课件/音频/附件" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="750" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="46" value="上传作业 / 答题" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="10" target="21">
<mxGeometry relative="1" as="geometry" />
<mxCell id="67" value="D8 Milvus + NebulaGraph/Neo4j&#xa;向量检索 / 知识图谱 / 同步任务结果" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="860" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="47" value="作业 / 资料 / 提交" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="21" target="31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="48" value="文件 / 课件" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="21" target="33">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="49" value="批改请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="21" target="22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="50" value="调用 Python AI" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="22" target="25">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="51" value="解析结果 / 错因 / 评分" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="25" target="22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="52" value="知识点 / 题库匹配" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="25" target="33">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="53" value="批改结果" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="22" target="31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="54" value="错题 / 薄弱项" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="22" target="23">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="55" value="批改结果 / 错因" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="22" target="10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="56" value="复核任务 / 学情" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="22" target="11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="57" value="错题 / 复习节点" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="23" target="32">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="58" value="画像 / 复习信号" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="23" target="24">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="59" value="错题本 / 复习计划" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="23" target="10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="60" value="微课 / 变式题召回" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="24" target="33">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="61" value="缓存推荐结果" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="24" target="34">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="62" value="推荐内容 / 提醒" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="24" target="10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="63" value="语音讲解提交" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="10" target="26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="64" value="点评 / 评分 / 学情查询" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="11" target="26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="65" value="机构监控 / 审核" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="12" target="26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="66" value="讲解评估调用" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="26" target="25">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="67" value="讲解记录 / 教学数据" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="26" target="31">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="68" value="报表 / 审计" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="26" target="35">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="69" value="班级学情 / 教学建议" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="26" target="11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="70" value="机构看板 / 质量报表" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="26" target="12">
<mxGeometry relative="1" as="geometry" />
<mxCell id="68" value="D9 站内信与审计BI&#xa;message / logs / reports" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#f8f9fa;strokeColor=#666666;fontSize=12;" vertex="1" parent="1">
<mxGeometry x="980" y="970" width="280" height="80" as="geometry"/>
</mxCell>
<mxCell id="80" value="登录/绑定" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="10" target="20"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="81" value="教学管理" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="11" target="20"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="82" value="运营管控" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="12" target="20"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="83" value="作业/资料请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="20" target="21"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="84" value="批改触发" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="21" target="22"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="85" value="错题沉淀" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="22" target="23"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="86" value="推荐触达" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="23" target="24"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="87" value="讲解评估" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="24" target="25"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="88" value="画像闭环" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="25" target="26"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="89" value="OCR请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="22" target="40"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="90" value="LLM批改请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="22" target="41"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="91" value="语音评测请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="25" target="42"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="92" value="推荐排序请求" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="24" target="43"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="93" value="任务记录/模型路由" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="40" target="44"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="94" value="任务记录/模型路由" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="41" target="44"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="95" value="任务记录/模型路由" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="42" target="44"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="96" value="任务记录/模型路由" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#d6b656;" edge="1" parent="1" source="43" target="44"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="100" value="用户/权限" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="20" target="60"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="101" value="作业/提交" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="21" target="61"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="102" value="批改/错题/讲解" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="22" target="62"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="103" value="错题/复习计划" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="23" target="62"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="104" value="推荐任务/反馈/画像" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="24" target="63"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="105" value="成就进度/发放" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="26" target="64"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="106" value="缓存读写" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="24" target="65"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="107" value="文件/知识资产" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="40" target="66"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="108" value="检索/图谱" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="43" target="66"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="109" value="消息/审计报表" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="24" target="68"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="110" value="AI日志/任务" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="44" target="68"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="114" value="同步调度REST/JAR" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;dashed=1;endArrow=block;endFill=1;strokeColor=#666666;" edge="1" parent="1" source="44" target="67"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="111" value="结果回传" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#82b366;" edge="1" parent="1" source="22" target="10"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="112" value="待复核/教学建议" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#6c8ebf;" edge="1" parent="1" source="25" target="11"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="113" value="质量看板" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;endFill=1;strokeColor=#b85450;" edge="1" parent="1" source="26" target="12"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
</mxfile>

View File

@@ -7,7 +7,7 @@
<mxCell id="2" value="AI智能学习系统系统架构" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=24;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="30" y="20" width="420" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="技术栈:微信小程序 + React + Java + Python(AI处理供 Java 调用) + Redis + PostgreSQL" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxCell id="3" value="技术栈:微信小程序 + React + Java + Python(AI处理供 Java 调用) + Redis + PostgreSQL + Milvus + NebulaGraph/Neo4j" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="30" y="52" width="760" height="22" as="geometry"/>
</mxCell>
<mxCell id="10" value="角色与前端接入层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
@@ -40,7 +40,7 @@
<mxCell id="34" value="错题 / 复习 / 推荐服务&lt;br&gt;错题本、艾宾浩斯计划、推荐触达" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="690" y="280" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="35" value="知识库与机构运营服务&lt;br&gt;知识库审核、运营看板、质量监控、报表导出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;" parent="1" vertex="1">
<mxCell id="35" value="知识库与机构运营服务&lt;br&gt;知识图谱/向量库管理、运营看板、质量监控、报表导出" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=14;" parent="1" vertex="1">
<mxGeometry x="940" y="280" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="36" value="分布式治理能力&lt;br&gt;服务注册发现 / 配置中心 / 链路追踪 / 灰度发布" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=14;" parent="1" vertex="1">
@@ -79,10 +79,10 @@
<mxCell id="63" value="对象存储&lt;br&gt;图片 / PDF / 音频 / 课件 / 讲解录音" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1100" y="650" width="200" height="85" as="geometry"/>
</mxCell>
<mxCell id="64" value="MQ / 任务调度&lt;br&gt;异步批改、推荐推送、报表任务" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxCell id="64" value="MQ / 任务调度&lt;br&gt;异步批改、推荐推送、图谱/向量同步REST/JAR" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1340" y="650" width="200" height="85" as="geometry"/>
</mxCell>
<mxCell id="65" value="日志 / 审计 / BI&lt;br&gt;操作日志、链路日志、运营分析" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxCell id="65" value="Milvus + NebulaGraph/Neo4j&lt;br&gt;向量检索、知识图谱、混合检索日志" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="1580" y="650" width="160" height="85" as="geometry"/>
</mxCell>
<mxCell id="70" value="外部能力" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">
@@ -210,7 +210,7 @@
<mxCell id="2" value="AI智能学习系统代码架构" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=24;fontStyle=1;" parent="1" vertex="1">
<mxGeometry x="30" y="20" width="420" height="30" as="geometry"/>
</mxCell>
<mxCell id="3" value="前端:微信小程序 + React后端Java 分布式服务AIPython 服务数据Redis + PostgreSQL" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxCell id="3" value="前端:微信小程序 + React后端Java 分布式服务AIPython 服务数据Redis + PostgreSQL + Milvus + NebulaGraph/Neo4j" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=13;fontColor=#666666;" parent="1" vertex="1">
<mxGeometry x="30" y="52" width="780" height="22" as="geometry"/>
</mxCell>
<mxCell id="10" value="前端应用层" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8f9fa;strokeColor=#b7c3d0;fontSize=16;fontStyle=1;align=left;spacingLeft=12;verticalAlign=top;" parent="1" vertex="1">

BIN
docs/flow_student.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

BIN
docs/flow_teacher.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -5,7 +5,7 @@
- [x] 根目录骨架已创建:`backend``frontend``app``init/pg`
- [x] `backend` Maven 多模块目录与基础 POM 已落地
- [x] `gateway``auth``upms``boot-dev``python-ai` 首版占位代码已创建
- [x] `init/pg` 已按模块拆分,并接入根目录 `sys_area.sql`
- [x] `init/pg` 已按模块拆分,并接入根目录 `tb_sys_area.sql`
- [x] 学校租户表命名已修正为 `tb_sys_tenant`
- [x] 跨包 DTO、Enums 已收口到对应 `api-*` 模块
- [x] Web 端已从 workspace 收敛为单一 React 项目
@@ -40,7 +40,7 @@
- `backend/apis``api-auth``api-upms``api-ai`
- `backend/gateway`统一入口、鉴权、路由、跨域、trace 透传
- `backend/auth`登录、token、当前用户
- `backend/upms`:用户、角色、权、菜单动态路由元数据、组织与区域基座
- `backend/upms`:用户、角色、菜单授权、菜单动态路由元数据、组织与区域基座
- `backend/ai-client`Java 调 Python 的适配层
- `backend/boot-dev`:本地聚合启动模块
- `backend/python-ai`:独立 Python AI 服务占位
@@ -76,9 +76,9 @@
- 区域是分库分表的核心路由维度:
- 以“省份区域”作为首要分片依据
- 业务表设计时必须显式保留区域路由字段
- `sys_area.sql` 视为区域基础数据来源约束:
- 首版必须预留 `sys_area` 基础表和初始化脚本接入位
- 区域编码、层级、父子关系以 `sys_area.sql` 为准
- `tb_sys_area.sql` 视为区域基础数据来源约束:
- 首版必须预留 `tb_sys_area` 基础表和初始化脚本接入位
- 区域编码、层级、父子关系以 `tb_sys_area.sql` 为准
- 首版数据模型明确区分两棵树:
- 区域树:省 / 市 / 区县
- 组织树:总校 / 分校 / 校区 / 部门
@@ -88,7 +88,6 @@
- `SysDept`
- `SysUser`
- `SysRole`
- `SysPermission`
- 所有租户级业务主表统一预留字段:
- `adcode`
- `tenant_id``school_id`
@@ -106,7 +105,7 @@
- 目录结构固定为类似:
- `init/pg/00_create_db.sql`
- `init/pg/01_create_schema.sql`
- `init/pg/sys/sys_area.sql`
- `init/pg/sys/tb_sys_area.sql`
- `init/pg/auth/*.sql`
- `init/pg/upms/*.sql`
- `init/pg/ai/*.sql`
@@ -114,12 +113,12 @@
- 每个模块维护自己的建表 SQL、索引 SQL、初始化数据 SQL
- 不把所有表混在一个超大 SQL 文件中
- 公共基础表单独归 `sys``common` 目录
- `sys_area.sql` 固定归属:
- `tb_sys_area.sql` 固定归属:
- 放在 `init/pg/sys/`
- 作为区域基础数据的首批初始化脚本
- 模块 SQL 的职责边界固定:
- `auth`登录、token、认证相关表
- `upms`:用户、角色、权限、菜单、学校租户、部门、区域引用关系
- `upms`:用户、角色、菜单、角色菜单授权、学校租户、部门、区域引用关系
- `ai`AI 调用记录、任务记录、模型配置占位
- 初始化脚本执行规则固定:
- 先执行库 / Schema 基础脚本
@@ -150,7 +149,7 @@
- `traceId`
- `upms` 首版接口能力必须覆盖:
- 当前用户信息
- 用户 / 角色 / 权基座
- 用户 / 角色 / 菜单授权基座
- 区域树查询
- 学校租户树查询
- 部门树查询
@@ -189,7 +188,7 @@
- PostgreSQL 与 Redis 本地联调配置可跑通
- SQL 初始化验证必须覆盖:
- `init/pg` 下脚本可按顺序执行
- `sys_area.sql` 可独立导入
- `tb_sys_area.sql` 可独立导入
- `auth``upms` 模块脚本可独立维护且组合执行无冲突
- 区域与租户模型最小验证必须覆盖:
- 区域树可查询
@@ -202,5 +201,5 @@
- 不使用 `Dubbo`
- 首版不做真实业务页面、不做学生端真实业务、不做真实 AI 推理
- `upms` 继续承担首版系统管理中心职责
- `sys_area.sql` 是必须接入的区域基础数据脚本,且归档在 `init/pg/sys/`
- `tb_sys_area.sql` 是必须接入的区域基础数据脚本,且归档在 `init/pg/sys/`
- 部门当前先按“年级、学科等组织维度”建模,不在首版细化更复杂教学组织规则

267
docs/系统.md Normal file
View File

@@ -0,0 +1,267 @@
---
# 产品名称优知源·AI错题费曼复习系统
**版本号**V1.1
**文档状态**:初稿
**最后更新日期**2026-03-24
## 1. 项目概述
### 1.1 项目背景
旨在利用AI技术解决批改作业时间占用率问题缩短作业批改时长和学生使用费曼学习点评时长同一班级学生作业和点评可同时批改比如之前一个学生5分钟*40学生老师需要占用200分钟批批改作业费曼评分10分钟*10学生需占用老师100分钟系统上线后可实现AI自动批改和点评批改+费曼评分时间可忽略不计,还可以帮助老师统计学生作业完成率,正确率,可以帮助老师用同样的时间管理更多学生
### 1.2 核心价值
+ **学生**错题自动成册费曼讲解结合AI分析提升知识吸收缩短作业批改等待时长。
+ **教师**:快速了解班级学情,智能发布作业,基于真实错题数据的堂前精准测验,减少批改作业和费曼点评占用工作时长。
---
## 2. 用户角色与功能
| 角色 | 终端 | 核心功能 |
| --- | --- | --- |
| **学生** | 微信小程序 | 1. 班级作业(包含查看作业,下载作业、上传作业图片等功能)<br/>2. AI批改包含学科作业校验防止学生乱传、作业题目批改、优知源题库识别<br/>3. AI错题本复习包含错题与作业匹配、历史错题记录<br/>4. 艾宾浩斯遗忘曲线和莱特纳盒子算法推送错题<br/>5. AI费曼讲解评分包含费曼模板、图片音频识别、评分体系 |
| **教师** | Web管理后台 | 1. AI班级看板<br/>2. AI学情跟踪<br/>3. AI深度学情分析<br/>4. AI作业计划日历管理<br/>5. AI温故知新 |
---
## 3. 业务流程图 (逻辑描述)
### 3.1 学生端核心流程
![画板](https://cdn.nlark.com/yuque/0/2026/jpeg/66756287/1775082261808-fcefcc75-af87-45d4-8c39-2bb99c27c7ee.jpeg)
1. **登录**:手机号一键登录/验证码登录 -> 识别身份。
2. **学生作业页面**:展示已报学科班级列表 + 小红点(待办计数,作业/错题)。
3. **班级页**
- **<font style="color:#000000;">学科作业:</font>**<font style="color:#000000;">下载当日作业 -> 上传 -></font>**AI判断是否为已报学科作业**<font style="color:#000000;">-> </font>**AI批改**<font style="color:#000000;"> -> 生成错题本并标记错题待办</font><font style="color:rgb(6, 10, 38);"> -> </font>**<font style="color:rgb(6, 10, 38);">学生针对错题进行重做</font>**<font style="color:#000000;"> -> 上传 </font><font style="color:rgb(6, 10, 38);"> -> AI二次批改</font><font style="color:#000000;"></font>
- **<font style="color:#000000;">温故知新</font>**<font style="color:#000000;"></font>班级页独立入口,根据艾宾浩斯遗忘曲线和莱特纳盒子算法推送历史错题复习任务<font style="color:rgb(6, 10, 38);"></font>
- **返回逻辑**:若有未完成待办(当日作业/错题),返回首页时保留小红点提示。
- **判断逻辑**<font style="color:rgb(6, 10, 38);"></font>若仍有错误:循环“重做”步骤,直到该题正确。
若**本次作业所有错题均重做正确** -> 触发**费曼讲解环节**(必须全部做对才能开始讲)。
4. **错题复习(费曼模式)**
- 进入错题 -> 展示费曼模版 -> 语音录入。
- **AI评分**
* < 85%提示错误原因需要重录错题待办按钮重新录入 / 先做下一题
如选择先做下一题则将本题置于错题本最后一题重新推送
* 85%正向鼓励提示如何更优秀 -> 自动进入下一题。
- **循环结束**:所有错题待办完成 -> 弹出鼓励语 -> 返回首页 -> **消除该学科小红点**
### 3.2 教师端核心流程
![画板](https://cdn.nlark.com/yuque/0/2026/jpeg/66756592/1774364810159-64049153-cd23-4297-96cb-ddea7bf80333.jpeg)
1. **登录**手机号登录Web端。
2. **首页**:展示分配班级/学科。
3. **班级详情页**
- **仪表盘**:昨日/今日完成率、正确率。
- **深度学情分析**:点击数据 -> 全班列表 -> 点击学生 -> 查看具体作业(图片+费曼语音)。
4. **训练计划(日历)**
- 颜色标识:绿(已预设)。
- 操作新建默认未来一周支持文档AI识别填充、查看、修改删除
5. **温故计划**
- 默认展示T+1日期的学生筛选结果
- 操作:对学生维度<font style="color:rgb(6, 10, 38);">温故知新推送错题</font>查看、修改(删除)。
---
## 4. 详细功能需求说明
### 4.1 学生小程序端
#### 4.1.1 登录与首页
+ **功能描述**
- 支持手机号获取验证码登录,自动关联后台用户信息。
- **首页布局**:卡片式展示“已报名学科班级”。
- **消息提示**:每个班级卡片右上角显示红色圆点及数字,代表“待处理作业数”或“待复习错题数”。
+ **交互逻辑**
- 若用户有未完成的“待办作业或待办错题”并点击返回上一页,系统需拦截或标记,确保小红点不消失,直到任务状态变更。
#### 4.1.2 班级作业模块
+ **待办作业列表**
- 列表一:展示老师布置的作业标题、截止日期。
- 列表二:温故知新系统根据艾宾浩斯遗忘曲线和莱特纳盒子算法,自动展示需要复习的历史错题并展示需要复习题数小红点。
- **开始作业**
* **下载作业**:点击按钮下载老师上传/温故知新的作业文件(图片/PDF/word
* **上传作业**调用摄像头拍照支持多图上传支持上传worePDF
拍照支持重拍图片和文件支持添加和删除提交后触发AI批改。
* 如从已上传作业且批改完成,则进入批改结果页面。
#### 4.1.3 AI批改与错题本
+ **后端逻辑**
- 接收图片 -> 大模型识别->本次作业校验->MCP优知源题库 -> 题目匹配->解题步骤分析 -> 判定对错。
- 模型优化通过意图识别优化、skill沉淀、MCP分层等手段持续优化模型识别准确性和性能
上传作业先校验是否为优智源发布作业,如非学生当日学科作业阻塞流程,提醒:“<font style="color:rgb(51, 51, 51);">该作业并非学生在优知所学习学科作业,请核对后重新上传。”</font>
若答案为空,则判断为错误。
- 若错误,自动存入“错题本”,标记知识点,标记待办,并统计所有错题待办数。
- <font style="color:#DF2A3F;">莱特盒子标记逻辑:
</font><font style="color:#DF2A3F;">预设box1、box2、box3、box4、box5
</font><font style="color:#DF2A3F;">新增错题记录错题时间进入box1
</font><font style="color:#DF2A3F;">复习时正确则题目盒子+1比如box1->box2
</font><font style="color:#DF2A3F;">复习时错误则题目盒子重置为box1
</font><font style="color:#DF2A3F;">复习时如题目在box5且回答正确则从错题本移除</font>
- 异常场景:处理中时,中途断网、中断操作(退出小程序,来点/短信),默认为未上传成功
+ **前端展示**
- 批改结果页:显示原题、学生作答、正确答案、**错因分析**。
- 按钮:“开始重做”“返回”:
[开始重做],点击后进入重做页,支持再次拍照上传解答过程。
[返回],返回待办作业列表,当前学科待办继续展示,待办数读取后端存值。
+ **二次批改**
- AI对比原题与新上传的答案。
- 前端展示:批改结果页:显示原题、学生作答、正确答案、**错因分析**。
若正确:标记该题为“已订正”。
- 若全部正确,页面提示:“已全部答对,我们开始复习吧!”并触发费曼解锁
+ **费曼解锁条件**
- 只有当本次作业下所有错题状态均为“**已订正**”时,页面底部[开始复习]按钮才变为可点击状态(高亮)[开始重做]同时置灰。
若有点错未订正,点击按钮提示:“请先完成所有错题的订正”。
- [开始复习],进入费曼讲解
[返回],返回待办作业列表,当前学科待办继续展示,待办数读取后端存值。
#### 4.1.4 费曼讲解核心模块(重点)
+ **页面布局**
- 顶部:当前错题题目简述。
- 中部:**费曼五步法填空模版**(支持语音转文字实时上屏,也支持纯语音提交):
1. 我要讲的概念是:`__________`
2. 它最核心的作用是:`__________`(一句话说清价值)
3. 举个学习中的例子:`__________`(用熟悉题目类比)
4. 常见的误解是:`__________`(指出易错点)
5. 我可以这样简化它:`__________`(用口诀/比喻总结)
- 底部:大尺寸“按住说话”按钮;小喇叭“试听”按钮,“提交评分”按钮。
+ **AI评分逻辑**
- **输入**:学生语音 + 原题知识点。
* **知识点讲清 (30%)**:是否准确描述了题目涉及的核心概念。
* **原理说明 (30%)**:能否一句话概括该知识点的作用。
* **逻辑思路 (30%)**:举例是否恰当,是否能用旧知解释新知。
* **语言表达 (10%)**:回答语言是否清晰,语速是否流畅。
* **总分计算**加权求和映射到0-100分。
- **处理**:分析讲解的逻辑性、准确性、通俗性。
- **异常场景**:处理中时,中途断网、中断操作(退出小程序,来点/短信),默认为未上传成功,需重新录入
- **输出与分支**
* **评分 < 85%**
+ 提示:“亲爱的伙伴讲解不够清晰未抓住核心请重试。”
AI总结根据评分标准显示对应维度的得分
+ 按钮组[重新录入] (高亮), [先做下一题] (次要)。
+ _注选择“先做下一题”则该题状态标记为“待重讲”小红点不消除。_
* **评分 85%**
+ 动效撒花/勋章动画文案:“太棒了你已经掌握了!”
+ 动作自动跳转至下一道错题
+ **结算页**
- 条件当前学科下所有待复习错题状态均为通过”。
- 展示全屏鼓励语海报
- 操作[返回主页] -> 触发该学科作业或温故知新的小红点消除逻辑。
---
#### 4.1.5 温故知新复习模式
+ **推送逻辑**
- 后台根据每道错题的“最后掌握时间”和“错误次数”计算下次复习日期。
- **莱特纳盒子机制**
* 盒子1T+1日新错题或常错题。
* 盒子23天后掌握度一般的题。
* 盒子37天后、盒子416天后、盒子535天后掌握度高的题。
- **艾宾浩斯节点**强制在遗忘临界点如第1、3、7、16、35天推送。
+ **复习流程**
- 学生点击“温故知新”待办 →→ 上传作业 →→ AI批改 →→ 正确/错误 →→ 系统更新该题的“盒子等级”和“下次复习时间”。
### 4.2 教师Web端
#### 4.2.1 登录与概览
+ **登录**:手机号 + 验证码/密码。
+ **主页**:网格布局展示所带班级及对应学科。
#### 4.2.2 班级数据仪表盘
+ **核心指标卡**
- 筛选条件,默认当天,日历可选择对应日期
作业情况:完成率 % | 正确率 %
(根据条件展示效果,完成率完成率=筛选日期区间内提交作业的学生人数/筛选日期区间内应提交作业的学生总人数×100%,正确率= 筛选日期区间内作业批改正确的总题数/筛选日期区间内作业批改的总题数×100%
+ **详细内容**
- **表格数据**:学生名称,取学生名称;完成度,筛选当天提交作业题数/筛选当天应提交题数*100%;第一次提交正确率,筛选当天第一次提交正确题数/筛选当天题数*100%;费曼学习平均评分,筛选当天费曼评分之和/次数;最后一次提交时间,筛选当天最后一次提交时间。
- **查看详情:**左侧展示作业原图/答题图,右侧/下方播放“费曼讲解”录音波形及转写文本。
- 导航:每层页面均有明确的 [返回] 按钮。
+ **深度学情仪表盘(新增核心模块)**
- **模块一:错题类型与概率分布(可按日,周,月时间筛选)**
* **图表**:柱状图展示错题类型(计算错误、概念混淆、审题不清、逻辑漏洞)。
* **数据**:各类型占比及出现概率。
* **<font style="color:rgb(6, 10, 38);">目的</font>**<font style="color:rgb(6, 10, 38);">:帮助老师判断是全班共性问题还是个别习惯问题。
</font><font style="color:rgb(6, 10, 38);">如计算错误 -< 题目列表 -< 学生列表-< 查看-< 具体单体分析报告</font>
- **模块二:知识点错题率(可按日,周,月时间筛选)**
* **图标**:柱状图按知识点罗列,显示错误概率大的前五个知识点。
* **交互**:点击某知识点,打开新页面查看具体是哪些题目错了。
* **目的**:帮助老师抓住高错误知识点
如知识点1 <font style="color:rgb(6, 10, 38);">-< 题目列表 -< 学生列表-< 查看-< 具体单体分析报告</font>
- **模块三:错题高频学生排名(可日,周,月按时间筛选)**
* **列表**:错题总数最多学生排名
展示默认展示10个学生可翻页
* **目的**:快速定位需要重点辅导的“困难户”。
如学生张三<font style="color:rgb(6, 10, 38);">-< 题目列表 -< 查看-< 具体单体分析报告</font>
- 从上述模块一二图表点跳转学生列表
+ 展示题目原图。
+ 展示全班学生列表。
- 对错,费曼评分,根据费曼评分最低倒序排列。
- **位置**简易仪表盘上方按钮独立Tab页**[深度学情分析]**。
#### 4.2.3 训练计划(日历视图)
+ **界面元素**
- **日历组件**
* 🟢 绿色:已预设作业且已完成批改。
- **作业日志列表**:位于日历下方,按创建时间倒序排列。
+ **操作功能**
- **新建计划**
* 默认时间范围当前日期下周一周7天。
* 表单:每一天为一行,支持上传文档。
* **AI辅助**上传文档后点击“AI识别”自动提取题目填入作业内容区域。
文档上传提取准确率为100%,行预览按钮,可预览转换后数据
* 校验:保存时检查天数 ≥ 1。
- **修改**
* 点击某日期的“修改”按钮,可修改**该日期后一天**的作业内容和截止日期等内容(防止误改已发布当天的紧急数据)。
* 按钮【确认】点击保存修改内容
- **返回**:各层级均有返回按钮,返回不做保存逻辑。
#### 4.2.4 温故计划
+ **界面元素**
- **筛选组件**默认筛选T+1日期。
- **学生列表**:位于日历下方,按入学录入系统时间排列。
+ **操作功能**
- **修改**
* 点击某学生的“修改”按钮,进入修改作业页面,可更换推送内容,整体修改。
* 按钮【确认】点击保存修改内容
* 替换掉的错题盒子等级继续保留
- **返回**:各层级均有返回按钮,返回不做保存逻辑。
---
## 5.原型图链接
### 5.1 小程序原型图链接
[https://modao.cc/proto/BEv3KNantcaveaK0xcJ28A/sharing?view_mode=read_only&screen=rbpVEb8VfOdcd8i85](https://modao.cc/proto/BEv3KNantcaveaK0xcJ28A/sharing?view_mode=read_only&screen=rbpVEb8VfOdcd8i85) #小程序-分享
### 5.2wep原型图链接
[https://modao.cc/proto/aDopc1kVtcaxlzyaS8cXEN/sharing?view_mode=read_only&screen=rbpVEbJqRCrED8gVy](https://modao.cc/proto/aDopc1kVtcaxlzyaS8cXEN/sharing?view_mode=read_only&screen=rbpVEbJqRCrED8gVy) #训练计划-分享
## 6. 待讨论项
1. 老师设置作业的形式需要讨论
1. 逐题录入上传word上传图片作业量级如何 录入形式影响C端批改作业、生成错题集的准确度
3/23会议讨论word和PDF格式文件
2. 是否要录入标准答案,什么形式需要讨论
3/23会议讨论word和PDF格式文件作业内包含标准答案
2. 学生端UI风格待确定优知源哪位老师来设计确定后九维用AI生成设计稿
3/23会议讨论优智源有专业设计3/24进入项目组
3. 费曼学习法学生上传 的是图片?语言?视频? 是一批题目的讲解还是一个题目? 视频费曼学习法业界还没有这样的产品比较费token准确度也不高探索难度极大需要持续优化
3/23会议讨论可以是语音如果是视频更好可以技术探索一下
4. 教师端作业相关的功能与静哥现在做的功能是什么关系?静哥是基础数据,需要给我们开放学生、班级、课程、作业等基础数据,我们做应用层开发
3/23会议讨论无太大影响除了班级学科信息有关联其他暂不用处理
5. 现在服务器数据库资源放在腾讯云上的大模型等AI产品都用阿里云千问的后续其他资源譬如文件服务器OSS、redis等资源用腾讯的还是阿里的需要维护2个平台需要大家决策
6. 重做的审批机制是不是和批改完全一致,是不是需要更侧重一些对比数据——比如字体,格式,步骤,防止学生潦草应对
7. 遗忘曲线模块,是否增加错题日限制——单日学生单科最大推送量,如果单日大量推送,学生能否做完,大量推送学生会不会产生厌学情绪
8. 仪表盘一的统计数据需要预置是否满足业务,如有不足可讨论增加,
9. 已超过截止时间,作业是否可以继续解答,如果做闭环,
建议方案1不可作答过了12点自动更新掉不做累计老师端核心指标增加仪表提示未做作业学生人工介入处理次日线下处理并上传ai批改。
好处,不给学生太大压力,及时跟进学情,督促学生,缺点,人工需要做的多一些
建议方案2可作答不做完作业待办小红点累计老师端深度学情分析增加仪表统计晚于截止时间作答学生
好处,无需人工介入,全程线上,缺点,不及时督促学生待办作业累积会有心理压力,且长期超过最晚时间作答可能影响孩子晚上睡眠,不利于好的作息习惯养成

View File

@@ -0,0 +1,81 @@
# 《功能清单》与《系统.md》差异分析
## 文档定位
- `AI智能学习系统功能清单.md`:已签约/已约定的合同范围基线Scope Baseline
- `系统.md`甲方最新发出的需求说明属于新增需求输入与流程细化输入Change Request Source
## 核心区分点
1. 功能清单用于界定合同交付边界。
2. 系统文档用于表达甲方最新业务想法,其中包含细化项、增项、以及与合同冲突项。
3. 后续评审必须以“合同基线 + 变更确认”双轨执行,避免范围漂移。
## 一、系统.md 相对合同的新增/细化点
### 1) 学生端流程门禁增强
- 增加“先订正完全部错题,才可解锁费曼复习”的强约束。
- 增加费曼评分阈值85分及未通过回流机制重录/后移)。
- 增加小红点保留与消除的精细规则(与任务状态强绑定)。
### 2) AI策略与状态规则更明确
- 新增莱特纳 box1~box5 的明确升降级逻辑与移除规则。
- 新增费曼评分维度与权重(知识点/原理/逻辑/表达)。
- 新增异常场景处理口径(断网、退出、来电等统一按失败处理)。
### 3) 教师端分析细节提升
- 新增深度学情三大模块:错因分布、知识点错题率、高频错题学生。
- 新增图表下钻链路(类型/知识点 -> 题目 -> 学生 -> 详情)。
### 4) 计划管理规则更具体
- 训练计划默认未来7天。
- 作业文档AI识别填充流程。
- 计划修改边界限制(防误改发布内容)。
## 二、合同功能清单中存在、但系统.md 未覆盖或弱化的内容
### 1) 机构端Web整体能力缺失
- 管理员高权限、教师/学员统一管理、权限配置、质量监控、机构报表等未展开。
### 2) 学生端扩展能力弱化
- AI变式题举一反三未成为主链路。
- 个性化推荐未形成明确功能闭环。
- 学习激励体系(勋章、打卡、韧性指标)未明确。
- 消息中心(已读/删除等)未明确。
### 3) 教师端管理与干预能力弱化
- 班级/学员管理未完整体现。
- 教师复核批改与讲解点评能力弱化。
- 教学建议与报表导出未作为明确交付项表达。
### 4) 支撑层能力表达不足
- 知识库标准化管理、推荐算法闭环在系统文档中不完整。
## 三、两文档冲突点(需优先决策)
1. **作业模式冲突**
合同含“机构作业 + 自主作业双模式”;系统文档强调“非本学科作业阻塞”,存在边界冲突。
2. **人工复核与纯AI闭环冲突**
合同包含教师复核/点评;系统文档更偏全自动闭环,需明确教师介入职责。
3. **交付范围冲突**
合同覆盖“学生+教师+机构+支撑层”;系统文档聚焦“学生+教师流程”,存在范围收缩/转移风险。
## 四、变更分类建议(用于评审)
### A. 可视为“细化不增项”
- 已有功能的页面流程细化、状态文案细化、评分展示细化(不新增系统能力)。
### B. 可能构成“新增范围”
- 深度学情下钻链路与分析维度扩展。
- 严格状态机与门禁规则落地(对前后端改造影响较大)。
- 费曼评分机制细化到权重与回流路径。
### C. 必须“变更单确认”
- 与合同冲突的作业模式口径。
- 是否保留教师复核机制。
- 是否把机构端/推荐/激励等合同项后置或拆期。
## 五、建议的会议确认清单
1. 本次交付是否继续以《功能清单》为合同唯一基线。
2. 《系统.md》哪些条目作为本期新增哪些进入二期。
3. 冲突条目采用哪一版口径(含验收标准与责任边界)。
4. 新增条目对应的工期、费用、里程碑是否同步调整。
## 结论
`系统.md` 不是合同替代文本,而是新增需求输入文档。
建议以《功能清单》作为验收主依据,对《系统.md》逐条走“细化/增项/冲突”分类并形成书面变更结论。

View File

@@ -1,4 +1,7 @@
CREATE SCHEMA IF NOT EXISTS sys;
CREATE SCHEMA IF NOT EXISTS auth;
CREATE SCHEMA IF NOT EXISTS upms;
CREATE SCHEMA IF NOT EXISTS ai;
CREATE SCHEMA IF NOT EXISTS course;
CREATE SCHEMA IF NOT EXISTS question;
CREATE SCHEMA IF NOT EXISTS achievement;
CREATE SCHEMA IF NOT EXISTS recommendation;

View File

@@ -1,7 +1,14 @@
\i /docker-entrypoint-initdb.d/upms/10_create_upms_tables.sql
\i /docker-entrypoint-initdb.d/auth/10_create_auth_tables.sql
\i /docker-entrypoint-initdb.d/ai/10_create_ai_tables.sql
\i /docker-entrypoint-initdb.d/course/10_create_course_tables.sql
\i /docker-entrypoint-initdb.d/question/10_create_question_tables.sql
\i /docker-entrypoint-initdb.d/achievement/10_create_achievement_tables.sql
\i /docker-entrypoint-initdb.d/recommendation/10_create_recommendation_tables.sql
\i /docker-entrypoint-initdb.d/sys/sys_area.sql
\i /docker-entrypoint-initdb.d/upms/20_init_upms_seed.sql
\i /docker-entrypoint-initdb.d/auth/20_init_auth_seed.sql
\i /docker-entrypoint-initdb.d/ai/20_init_ai_seed.sql
\i /docker-entrypoint-initdb.d/course/20_init_course_seed.sql
\i /docker-entrypoint-initdb.d/question/20_init_question_seed.sql
\i /docker-entrypoint-initdb.d/achievement/20_init_achievement_seed.sql

View File

@@ -0,0 +1,407 @@
DROP SCHEMA IF EXISTS achievement CASCADE;
CREATE SCHEMA IF NOT EXISTS achievement;
DROP TABLE IF EXISTS achievement.ac_achievement CASCADE;
CREATE TABLE IF NOT EXISTS achievement.ac_achievement (
achievement_id VARCHAR(64) PRIMARY KEY,
achievement_code VARCHAR(64) UNIQUE NOT NULL,
achievement_name VARCHAR(128) NOT NULL,
achievement_desc TEXT,
achievement_category VARCHAR(32) NOT NULL DEFAULT 'SCORE',
trigger_event VARCHAR(32) NOT NULL DEFAULT 'GRADE_PUBLISHED',
trigger_rule_json JSONB NOT NULL DEFAULT '{}'::JSONB,
badge_level VARCHAR(16) NOT NULL DEFAULT 'BRONZE',
badge_icon_url VARCHAR(512),
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
tenant_id VARCHAR(64) NOT NULL,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_ac_achievement_category
CHECK (achievement_category IN ('SCORE', 'PROGRESS', 'TEACHING', 'ENGAGEMENT', 'MANUAL')),
CONSTRAINT chk_ac_achievement_trigger_event
CHECK (trigger_event IN ('GRADE_PUBLISHED', 'WRONG_QUESTION_MASTERED', 'TASK_COMPLETED', 'REVIEW_COMPLETED', 'MANUAL')),
CONSTRAINT chk_ac_achievement_badge_level
CHECK (badge_level IN ('BRONZE', 'SILVER', 'GOLD', 'PLATINUM')),
CONSTRAINT chk_ac_achievement_status
CHECK (status IN ('ACTIVE', 'DISABLED')),
CONSTRAINT chk_ac_achievement_trigger_rule_json
CHECK (jsonb_typeof(trigger_rule_json) = 'object'),
CONSTRAINT fk_ac_achievement_created_by
FOREIGN KEY (created_by) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE achievement.ac_achievement IS '成就定义表';
COMMENT ON COLUMN achievement.ac_achievement.achievement_id IS '成就ID';
COMMENT ON COLUMN achievement.ac_achievement.achievement_code IS '成就编码';
COMMENT ON COLUMN achievement.ac_achievement.achievement_name IS '成就名称';
COMMENT ON COLUMN achievement.ac_achievement.achievement_desc IS '成就描述';
COMMENT ON COLUMN achievement.ac_achievement.achievement_category IS '成就类别SCORE/PROGRESS/TEACHING/ENGAGEMENT/MANUAL';
COMMENT ON COLUMN achievement.ac_achievement.trigger_event IS '触发事件GRADE_PUBLISHED/WRONG_QUESTION_MASTERED/TASK_COMPLETED/REVIEW_COMPLETED/MANUAL';
COMMENT ON COLUMN achievement.ac_achievement.trigger_rule_json IS '触发规则JSON可配置成绩阈值、次数阈值等';
COMMENT ON COLUMN achievement.ac_achievement.badge_level IS '徽章等级BRONZE/SILVER/GOLD/PLATINUM';
COMMENT ON COLUMN achievement.ac_achievement.badge_icon_url IS '徽章图标URL';
COMMENT ON COLUMN achievement.ac_achievement.status IS '状态ACTIVE/DISABLED';
COMMENT ON COLUMN achievement.ac_achievement.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_achievement.created_by IS '创建人ID';
COMMENT ON COLUMN achievement.ac_achievement.created_at IS '创建时间';
COMMENT ON COLUMN achievement.ac_achievement.updated_at IS '更新时间';
DROP TABLE IF EXISTS achievement.ac_achievement_role_rel CASCADE;
CREATE TABLE IF NOT EXISTS achievement.ac_achievement_role_rel (
achievement_id VARCHAR(64) NOT NULL,
role_id VARCHAR(64) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (achievement_id, role_id),
CONSTRAINT fk_ac_achievement_role_rel_achievement
FOREIGN KEY (achievement_id) REFERENCES achievement.ac_achievement(achievement_id) ON DELETE CASCADE,
CONSTRAINT fk_ac_achievement_role_rel_role
FOREIGN KEY (role_id) REFERENCES upms.tb_sys_role(role_id)
);
COMMENT ON TABLE achievement.ac_achievement_role_rel IS '成就-角色绑定表';
COMMENT ON COLUMN achievement.ac_achievement_role_rel.achievement_id IS '成就ID';
COMMENT ON COLUMN achievement.ac_achievement_role_rel.role_id IS '角色ID';
COMMENT ON COLUMN achievement.ac_achievement_role_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_achievement_role_rel.created_at IS '创建时间';
DROP TABLE IF EXISTS achievement.ac_user_achievement CASCADE;
CREATE TABLE IF NOT EXISTS achievement.ac_user_achievement (
user_achievement_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
achievement_id VARCHAR(64) NOT NULL,
role_id VARCHAR(64) NOT NULL,
summary_id VARCHAR(64),
grading_task_id VARCHAR(64),
score_snapshot NUMERIC(8,2),
grade_level VARCHAR(8),
surpass_ratio NUMERIC(5,2),
used_seconds INTEGER,
award_source VARCHAR(32) NOT NULL DEFAULT 'RULE_ENGINE',
award_reason VARCHAR(256),
evidence_json JSONB NOT NULL DEFAULT '{}'::JSONB,
achieved_count INTEGER NOT NULL DEFAULT 1,
first_achieved_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_achieved_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_ac_user_achievement_user_achievement_role
UNIQUE (user_id, achievement_id, role_id),
CONSTRAINT chk_ac_user_achievement_award_source
CHECK (award_source IN ('RULE_ENGINE', 'MANUAL', 'SYSTEM')),
CONSTRAINT chk_ac_user_achievement_evidence_json
CHECK (jsonb_typeof(evidence_json) = 'object'),
CONSTRAINT chk_ac_user_achievement_achieved_count
CHECK (achieved_count > 0),
CONSTRAINT fk_ac_user_achievement_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_ac_user_achievement_role_binding
FOREIGN KEY (achievement_id, role_id) REFERENCES achievement.ac_achievement_role_rel(achievement_id, role_id),
CONSTRAINT fk_ac_user_achievement_summary
FOREIGN KEY (summary_id) REFERENCES question.gd_score_summary(summary_id),
CONSTRAINT fk_ac_user_achievement_task
FOREIGN KEY (grading_task_id) REFERENCES question.gd_grading_task(grading_task_id)
);
COMMENT ON TABLE achievement.ac_user_achievement IS '用户成就记录表';
COMMENT ON COLUMN achievement.ac_user_achievement.user_achievement_id IS '用户成就记录ID';
COMMENT ON COLUMN achievement.ac_user_achievement.user_id IS '用户ID';
COMMENT ON COLUMN achievement.ac_user_achievement.achievement_id IS '成就ID';
COMMENT ON COLUMN achievement.ac_user_achievement.role_id IS '获奖时角色ID';
COMMENT ON COLUMN achievement.ac_user_achievement.summary_id IS '成绩汇总ID来源于question.gd_score_summary';
COMMENT ON COLUMN achievement.ac_user_achievement.grading_task_id IS '批改任务ID来源于question.gd_grading_task';
COMMENT ON COLUMN achievement.ac_user_achievement.score_snapshot IS '成绩快照分数';
COMMENT ON COLUMN achievement.ac_user_achievement.grade_level IS '成绩快照等级';
COMMENT ON COLUMN achievement.ac_user_achievement.surpass_ratio IS '成绩快照超越比例';
COMMENT ON COLUMN achievement.ac_user_achievement.used_seconds IS '成绩快照耗时(秒)';
COMMENT ON COLUMN achievement.ac_user_achievement.award_source IS '发放来源RULE_ENGINE/MANUAL/SYSTEM';
COMMENT ON COLUMN achievement.ac_user_achievement.award_reason IS '发放原因';
COMMENT ON COLUMN achievement.ac_user_achievement.evidence_json IS '发放证据JSON可存规则命中明细';
COMMENT ON COLUMN achievement.ac_user_achievement.achieved_count IS '累计达成次数';
COMMENT ON COLUMN achievement.ac_user_achievement.first_achieved_at IS '首次达成时间';
COMMENT ON COLUMN achievement.ac_user_achievement.last_achieved_at IS '最近达成时间';
COMMENT ON COLUMN achievement.ac_user_achievement.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_user_achievement.created_at IS '创建时间';
COMMENT ON COLUMN achievement.ac_user_achievement.updated_at IS '更新时间';
DROP TABLE IF EXISTS achievement.ac_user_achievement_progress CASCADE;
CREATE TABLE IF NOT EXISTS achievement.ac_user_achievement_progress (
progress_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
achievement_id VARCHAR(64) NOT NULL,
role_id VARCHAR(64) NOT NULL,
current_value NUMERIC(12,2) NOT NULL DEFAULT 0,
target_value NUMERIC(12,2) NOT NULL,
progress_rate NUMERIC(5,2) NOT NULL DEFAULT 0,
effort_index NUMERIC(6,2) NOT NULL DEFAULT 0,
improvement_rate NUMERIC(6,2) NOT NULL DEFAULT 0,
resilience_index NUMERIC(6,2) NOT NULL DEFAULT 0,
streak_days INTEGER NOT NULL DEFAULT 0,
last_checkin_date DATE,
progress_status VARCHAR(16) NOT NULL DEFAULT 'IN_PROGRESS',
progress_snapshot_json JSONB NOT NULL DEFAULT '{}'::JSONB,
last_evaluated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
achieved_at TIMESTAMP,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_ac_user_achievement_progress_user_achievement_role
UNIQUE (user_id, achievement_id, role_id),
CONSTRAINT chk_ac_user_achievement_progress_current
CHECK (current_value >= 0),
CONSTRAINT chk_ac_user_achievement_progress_target
CHECK (target_value > 0),
CONSTRAINT chk_ac_user_achievement_progress_rate
CHECK (progress_rate >= 0 AND progress_rate <= 100),
CONSTRAINT chk_ac_user_achievement_progress_effort_index
CHECK (effort_index >= 0 AND effort_index <= 100),
CONSTRAINT chk_ac_user_achievement_progress_improvement_rate
CHECK (improvement_rate >= -100 AND improvement_rate <= 1000),
CONSTRAINT chk_ac_user_achievement_progress_resilience_index
CHECK (resilience_index >= 0 AND resilience_index <= 100),
CONSTRAINT chk_ac_user_achievement_progress_streak_days
CHECK (streak_days >= 0),
CONSTRAINT chk_ac_user_achievement_progress_status
CHECK (progress_status IN ('IN_PROGRESS', 'ACHIEVED', 'EXPIRED')),
CONSTRAINT chk_ac_user_achievement_progress_snapshot_json
CHECK (jsonb_typeof(progress_snapshot_json) = 'object'),
CONSTRAINT chk_ac_user_achievement_progress_achieved_at
CHECK (
(progress_status = 'ACHIEVED' AND achieved_at IS NOT NULL)
OR (progress_status IN ('IN_PROGRESS', 'EXPIRED') AND achieved_at IS NULL)
),
CONSTRAINT fk_ac_user_achievement_progress_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_ac_user_achievement_progress_role_binding
FOREIGN KEY (achievement_id, role_id) REFERENCES achievement.ac_achievement_role_rel(achievement_id, role_id)
);
COMMENT ON TABLE achievement.ac_user_achievement_progress IS '用户成就进度表(含未达成记录)';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.progress_id IS '成就进度ID';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.user_id IS '用户ID';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.achievement_id IS '成就ID';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.role_id IS '角色ID学生/教师)';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.current_value IS '当前进度值';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.target_value IS '目标值';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.progress_rate IS '完成率0-100';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.effort_index IS '努力指数0-100';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.improvement_rate IS '进步率(可为负)';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.resilience_index IS '学习韧性指数0-100';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.streak_days IS '连续打卡天数';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.last_checkin_date IS '最近打卡日期';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.progress_status IS '进度状态IN_PROGRESS/ACHIEVED/EXPIRED';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.progress_snapshot_json IS '进度快照JSON可记录最近一次评估明细';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.last_evaluated_at IS '最近评估时间';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.achieved_at IS '达成时间';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.created_at IS '创建时间';
COMMENT ON COLUMN achievement.ac_user_achievement_progress.updated_at IS '更新时间';
DROP TABLE IF EXISTS achievement.ac_user_achievement_daily_fact CASCADE;
CREATE TABLE IF NOT EXISTS achievement.ac_user_achievement_daily_fact (
stat_date DATE NOT NULL,
user_id VARCHAR(64) NOT NULL,
role_id VARCHAR(64) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
gained_achievement_count INTEGER NOT NULL DEFAULT 0,
active_achievement_count INTEGER NOT NULL DEFAULT 0,
progress_delta NUMERIC(10,4) NOT NULL DEFAULT 0,
effort_index NUMERIC(6,2) NOT NULL DEFAULT 0,
improvement_rate NUMERIC(6,2) NOT NULL DEFAULT 0,
resilience_index NUMERIC(6,2) NOT NULL DEFAULT 0,
streak_days INTEGER NOT NULL DEFAULT 0,
learning_minutes INTEGER NOT NULL DEFAULT 0,
checkin_status VARCHAR(16) NOT NULL DEFAULT 'NONE',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (stat_date, user_id, role_id),
CONSTRAINT chk_ac_user_achievement_daily_fact_gained
CHECK (gained_achievement_count >= 0),
CONSTRAINT chk_ac_user_achievement_daily_fact_active
CHECK (active_achievement_count >= 0),
CONSTRAINT chk_ac_user_achievement_daily_fact_effort
CHECK (effort_index >= 0 AND effort_index <= 100),
CONSTRAINT chk_ac_user_achievement_daily_fact_improvement
CHECK (improvement_rate >= -100 AND improvement_rate <= 1000),
CONSTRAINT chk_ac_user_achievement_daily_fact_resilience
CHECK (resilience_index >= 0 AND resilience_index <= 100),
CONSTRAINT chk_ac_user_achievement_daily_fact_streak
CHECK (streak_days >= 0),
CONSTRAINT chk_ac_user_achievement_daily_fact_learning_minutes
CHECK (learning_minutes >= 0),
CONSTRAINT chk_ac_user_achievement_daily_fact_checkin
CHECK (checkin_status IN ('NONE', 'CHECKED_IN', 'BROKEN')),
CONSTRAINT fk_ac_user_achievement_daily_fact_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_ac_user_achievement_daily_fact_role
FOREIGN KEY (role_id) REFERENCES upms.tb_sys_role(role_id)
);
COMMENT ON TABLE achievement.ac_user_achievement_daily_fact IS '用户成就激励日事实表(激励数据即成就数据)';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.stat_date IS '统计日期';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.user_id IS '用户ID';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.role_id IS '角色ID';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.gained_achievement_count IS '当日新增成就数';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.active_achievement_count IS '当日活跃成就数';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.progress_delta IS '当日进度变化量';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.effort_index IS '当日努力指数0-100';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.improvement_rate IS '当日进步率(可为负)';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.resilience_index IS '当日学习韧性指数0-100';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.streak_days IS '当日连续打卡天数';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.learning_minutes IS '当日学习分钟数';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.checkin_status IS '打卡状态';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.created_at IS '创建时间';
COMMENT ON COLUMN achievement.ac_user_achievement_daily_fact.updated_at IS '更新时间';
DROP TABLE IF EXISTS achievement.ac_achievement_rule_template CASCADE;
DROP TABLE IF EXISTS achievement.ac_achievement_metric_def CASCADE;
DROP TABLE IF EXISTS achievement.ac_achievement_event_dict CASCADE;
CREATE TABLE IF NOT EXISTS achievement.ac_achievement_event_dict (
event_code VARCHAR(64) PRIMARY KEY,
event_name VARCHAR(128) NOT NULL,
event_domain VARCHAR(32) NOT NULL DEFAULT 'LEARNING_LOOP',
event_desc TEXT,
payload_schema_json JSONB NOT NULL DEFAULT '{}'::JSONB,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_ac_achievement_event_dict_domain
CHECK (event_domain IN ('LEARNING_LOOP', 'GRADING', 'REVIEW', 'CHECKIN', 'SYSTEM')),
CONSTRAINT chk_ac_achievement_event_dict_payload_json
CHECK (jsonb_typeof(payload_schema_json) = 'object')
);
COMMENT ON TABLE achievement.ac_achievement_event_dict IS '成就事件字典表(通用触发事件定义)';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.event_code IS '事件编码';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.event_name IS '事件名称';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.event_domain IS '事件域';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.event_desc IS '事件描述';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.payload_schema_json IS '事件载荷结构定义JSON';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.enabled IS '是否启用';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.created_at IS '创建时间';
COMMENT ON COLUMN achievement.ac_achievement_event_dict.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS achievement.ac_achievement_metric_def (
metric_id VARCHAR(64) PRIMARY KEY,
metric_code VARCHAR(64) NOT NULL,
metric_name VARCHAR(128) NOT NULL,
metric_type VARCHAR(16) NOT NULL DEFAULT 'NUM',
metric_unit VARCHAR(32),
source_domain VARCHAR(32) NOT NULL DEFAULT 'SYSTEM',
agg_method VARCHAR(16) NOT NULL DEFAULT 'LATEST',
default_value_num NUMERIC(18,6),
default_value_text VARCHAR(255),
expr_json JSONB NOT NULL DEFAULT '{}'::JSONB,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_ac_achievement_metric_def_tenant_code
UNIQUE (tenant_id, metric_code),
CONSTRAINT chk_ac_achievement_metric_def_type
CHECK (metric_type IN ('NUM', 'BOOL', 'TEXT', 'RATE', 'COUNT')),
CONSTRAINT chk_ac_achievement_metric_def_source_domain
CHECK (source_domain IN ('COURSE', 'HOMEWORK', 'GRADING', 'REVIEW', 'RECOMMENDATION', 'ACHIEVEMENT', 'SYSTEM')),
CONSTRAINT chk_ac_achievement_metric_def_agg_method
CHECK (agg_method IN ('LATEST', 'SUM', 'AVG', 'MAX', 'MIN', 'COUNT')),
CONSTRAINT chk_ac_achievement_metric_def_expr_json
CHECK (jsonb_typeof(expr_json) = 'object')
);
COMMENT ON TABLE achievement.ac_achievement_metric_def IS '成就指标定义表(通用指标注册)';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.metric_id IS '指标ID';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.metric_code IS '指标编码';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.metric_name IS '指标名称';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.metric_type IS '指标类型';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.metric_unit IS '指标单位';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.source_domain IS '指标来源域';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.agg_method IS '聚合方式';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.default_value_num IS '默认数值';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.default_value_text IS '默认文本值';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.expr_json IS '计算表达式JSON';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.enabled IS '是否启用';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.created_at IS '创建时间';
COMMENT ON COLUMN achievement.ac_achievement_metric_def.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS achievement.ac_achievement_rule_template (
template_id VARCHAR(64) PRIMARY KEY,
template_code VARCHAR(64) NOT NULL,
template_name VARCHAR(128) NOT NULL,
event_code VARCHAR(64) NOT NULL,
role_id VARCHAR(64),
achievement_category VARCHAR(32) NOT NULL DEFAULT 'PROGRESS',
rule_condition_json JSONB NOT NULL DEFAULT '{}'::JSONB,
reward_action_json JSONB NOT NULL DEFAULT '{}'::JSONB,
priority INTEGER NOT NULL DEFAULT 100,
template_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
valid_from TIMESTAMP,
valid_to TIMESTAMP,
tenant_id VARCHAR(64) NOT NULL,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_ac_achievement_rule_template_tenant_code
UNIQUE (tenant_id, template_code),
CONSTRAINT chk_ac_achievement_rule_template_category
CHECK (achievement_category IN ('SCORE', 'PROGRESS', 'TEACHING', 'ENGAGEMENT', 'MANUAL')),
CONSTRAINT chk_ac_achievement_rule_template_status
CHECK (template_status IN ('ACTIVE', 'INACTIVE', 'ARCHIVED')),
CONSTRAINT chk_ac_achievement_rule_template_condition_json
CHECK (jsonb_typeof(rule_condition_json) = 'object'),
CONSTRAINT chk_ac_achievement_rule_template_reward_json
CHECK (jsonb_typeof(reward_action_json) = 'object'),
CONSTRAINT chk_ac_achievement_rule_template_valid
CHECK (valid_to IS NULL OR valid_from IS NULL OR valid_to >= valid_from),
CONSTRAINT fk_ac_achievement_rule_template_event
FOREIGN KEY (event_code) REFERENCES achievement.ac_achievement_event_dict(event_code),
CONSTRAINT fk_ac_achievement_rule_template_role
FOREIGN KEY (role_id) REFERENCES upms.tb_sys_role(role_id),
CONSTRAINT fk_ac_achievement_rule_template_created_by
FOREIGN KEY (created_by) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE achievement.ac_achievement_rule_template IS '成就规则模板表(通用配置驱动)';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.template_id IS '模板ID';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.template_code IS '模板编码';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.template_name IS '模板名称';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.event_code IS '触发事件编码';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.role_id IS '适用角色ID';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.achievement_category IS '成就类别';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.rule_condition_json IS '规则条件JSON';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.reward_action_json IS '奖励动作JSON';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.priority IS '优先级';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.template_status IS '模板状态';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.valid_from IS '生效开始时间';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.valid_to IS '生效结束时间';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.tenant_id IS '租户ID';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.created_by IS '创建人';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.created_at IS '创建时间';
COMMENT ON COLUMN achievement.ac_achievement_rule_template.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_ac_achievement_tenant_status
ON achievement.ac_achievement(tenant_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ac_achievement_role_rel_role
ON achievement.ac_achievement_role_rel(role_id, tenant_id);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_user_role
ON achievement.ac_user_achievement(user_id, role_id, last_achieved_at DESC);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_tenant_time
ON achievement.ac_user_achievement(tenant_id, last_achieved_at DESC);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_summary
ON achievement.ac_user_achievement(summary_id);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_task
ON achievement.ac_user_achievement(grading_task_id);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_progress_user_status
ON achievement.ac_user_achievement_progress(user_id, role_id, progress_status, last_evaluated_at DESC);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_progress_tenant_status
ON achievement.ac_user_achievement_progress(tenant_id, progress_status, last_evaluated_at DESC);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_progress_achievement
ON achievement.ac_user_achievement_progress(achievement_id, role_id);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_progress_streak
ON achievement.ac_user_achievement_progress(user_id, role_id, streak_days DESC, last_evaluated_at DESC);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_daily_fact_tenant_date
ON achievement.ac_user_achievement_daily_fact(tenant_id, stat_date DESC);
CREATE INDEX IF NOT EXISTS idx_ac_user_achievement_daily_fact_user_date
ON achievement.ac_user_achievement_daily_fact(user_id, role_id, stat_date DESC);
CREATE INDEX IF NOT EXISTS idx_ac_achievement_event_dict_tenant_enabled
ON achievement.ac_achievement_event_dict(tenant_id, enabled, event_domain);
CREATE INDEX IF NOT EXISTS idx_ac_achievement_metric_def_tenant_enabled
ON achievement.ac_achievement_metric_def(tenant_id, enabled, source_domain);
CREATE INDEX IF NOT EXISTS idx_ac_achievement_rule_template_tenant_event
ON achievement.ac_achievement_rule_template(tenant_id, event_code, template_status, priority);

View File

@@ -0,0 +1,2 @@
-- 成就模块初始化数据占位
-- 按需补充学生成就、教师成就及角色绑定关系种子数据

View File

@@ -1,47 +1,215 @@
CREATE TABLE IF NOT EXISTS ai.tb_ai_model_config (
model_id VARCHAR(64) PRIMARY KEY,
model_name VARCHAR(128) NOT NULL,
provider VARCHAR(64) NOT NULL,
endpoint VARCHAR(255),
adcode VARCHAR(12),
tenant_id VARCHAR(64),
tenant_path VARCHAR(255),
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE ai.tb_ai_model_config IS 'AI模型配置表';
COMMENT ON COLUMN ai.tb_ai_model_config.model_id IS '模型ID';
COMMENT ON COLUMN ai.tb_ai_model_config.model_name IS '模型名称';
COMMENT ON COLUMN ai.tb_ai_model_config.provider IS '模型提供商';
COMMENT ON COLUMN ai.tb_ai_model_config.endpoint IS '模型服务地址';
COMMENT ON COLUMN ai.tb_ai_model_config.adcode IS '行政区划编码';
COMMENT ON COLUMN ai.tb_ai_model_config.tenant_id IS '租户ID';
COMMENT ON COLUMN ai.tb_ai_model_config.tenant_path IS '租户路径';
COMMENT ON COLUMN ai.tb_ai_model_config.dept_id IS '部门ID';
COMMENT ON COLUMN ai.tb_ai_model_config.dept_path IS '部门路径';
COMMENT ON COLUMN ai.tb_ai_model_config.created_at IS '创建时间';
DROP SCHEMA IF EXISTS ai CASCADE;
CREATE SCHEMA IF NOT EXISTS ai;
CREATE TABLE IF NOT EXISTS ai.tb_ai_task_log (
task_id VARCHAR(64) PRIMARY KEY,
task_type VARCHAR(64) NOT NULL,
task_status VARCHAR(32) NOT NULL,
adcode VARCHAR(12),
tenant_id VARCHAR(64),
tenant_path VARCHAR(255),
dept_id VARCHAR(64),
dept_path VARCHAR(255),
payload_json TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
DROP TABLE IF EXISTS ai.tb_ai_knowledge_file CASCADE;
CREATE TABLE IF NOT EXISTS ai.tb_ai_knowledge_file (
file_id VARCHAR(64) PRIMARY KEY,
file_code VARCHAR(64) UNIQUE NOT NULL,
file_name VARCHAR(255) NOT NULL,
source_file_id VARCHAR(64),
file_type VARCHAR(32) NOT NULL DEFAULT 'DOCUMENT',
file_status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
graph_sync_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
vector_sync_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
graph_doc_ref VARCHAR(128),
vector_doc_ref VARCHAR(128),
content_checksum VARCHAR(128),
metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB,
adcode VARCHAR(12),
tenant_id VARCHAR(64),
tenant_path VARCHAR(255),
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_ai_knowledge_file_type
CHECK (file_type IN ('DOCUMENT', 'PDF', 'IMAGE', 'OTHER')),
CONSTRAINT chk_ai_knowledge_file_status
CHECK (file_status IN ('ACTIVE', 'DISABLED')),
CONSTRAINT chk_ai_knowledge_file_graph_status
CHECK (graph_sync_status IN ('PENDING', 'SYNCED', 'FAILED')),
CONSTRAINT chk_ai_knowledge_file_vector_status
CHECK (vector_sync_status IN ('PENDING', 'SYNCED', 'FAILED')),
CONSTRAINT chk_ai_knowledge_file_metadata_json
CHECK (jsonb_typeof(metadata_json) = 'object'),
CONSTRAINT fk_ai_knowledge_file_source_file
FOREIGN KEY (source_file_id) REFERENCES upms.tb_sys_file(file_id)
);
COMMENT ON TABLE ai.tb_ai_task_log IS 'AI任务日志表';
COMMENT ON COLUMN ai.tb_ai_task_log.task_id IS '任务ID';
COMMENT ON COLUMN ai.tb_ai_task_log.task_type IS '任务类型';
COMMENT ON COLUMN ai.tb_ai_task_log.task_status IS '任务状态';
COMMENT ON COLUMN ai.tb_ai_task_log.adcode IS '行政区划编码';
COMMENT ON COLUMN ai.tb_ai_task_log.tenant_id IS '租户ID';
COMMENT ON COLUMN ai.tb_ai_task_log.tenant_path IS '租户路径';
COMMENT ON COLUMN ai.tb_ai_task_log.dept_id IS '部门ID';
COMMENT ON COLUMN ai.tb_ai_task_log.dept_path IS '部门路径';
COMMENT ON COLUMN ai.tb_ai_task_log.payload_json IS '任务载荷JSON';
COMMENT ON COLUMN ai.tb_ai_task_log.created_at IS '创建时间';
COMMENT ON TABLE ai.tb_ai_knowledge_file IS '知识文件主表(统一管理图谱和向量知识库入库对象)';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.file_id IS '知识文件ID';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.file_code IS '知识文件编码';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.file_name IS '知识文件名称';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.source_file_id IS '来源文件IDupms.tb_sys_file.file_id';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.file_type IS '文件类型';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.file_status IS '文件状态';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.graph_sync_status IS '图谱同步状态';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.vector_sync_status IS '向量库同步状态';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.graph_doc_ref IS '图谱侧文档/对象引用';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.vector_doc_ref IS '向量库侧文档/对象引用';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.content_checksum IS '内容校验值';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.metadata_json IS '文件元数据JSON';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.adcode IS '行政区划编码';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.tenant_id IS '租户ID';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.tenant_path IS '租户路径';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.dept_id IS '部门ID';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.dept_path IS '部门路径';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.created_at IS '创建时间';
COMMENT ON COLUMN ai.tb_ai_knowledge_file.updated_at IS '更新时间';
DROP TABLE IF EXISTS ai.tb_ai_knowledge_sync_task CASCADE;
CREATE TABLE IF NOT EXISTS ai.tb_ai_knowledge_sync_task (
task_id VARCHAR(64) PRIMARY KEY,
file_id VARCHAR(64) NOT NULL,
target_store VARCHAR(16) NOT NULL,
task_type VARCHAR(16) NOT NULL DEFAULT 'UPSERT',
task_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
retry_count INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
payload_json JSONB NOT NULL DEFAULT '{}'::JSONB,
adcode VARCHAR(12),
tenant_id VARCHAR(64),
tenant_path VARCHAR(255),
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_ai_knowledge_sync_task_store
CHECK (target_store IN ('GRAPH', 'VECTOR')),
CONSTRAINT chk_ai_knowledge_sync_task_type
CHECK (task_type IN ('UPSERT', 'DELETE', 'REBUILD')),
CONSTRAINT chk_ai_knowledge_sync_task_status
CHECK (task_status IN ('PENDING', 'RUNNING', 'SUCCESS', 'FAILED')),
CONSTRAINT chk_ai_knowledge_sync_task_retry
CHECK (retry_count >= 0),
CONSTRAINT chk_ai_knowledge_sync_task_payload_json
CHECK (jsonb_typeof(payload_json) = 'object'),
CONSTRAINT fk_ai_knowledge_sync_task_file
FOREIGN KEY (file_id) REFERENCES ai.tb_ai_knowledge_file(file_id) ON DELETE CASCADE
);
COMMENT ON TABLE ai.tb_ai_knowledge_sync_task IS '知识文件同步任务表(同步到图谱或向量知识库)';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.task_id IS '同步任务ID';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.file_id IS '知识文件ID';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.target_store IS '目标存储GRAPH/VECTOR';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.task_type IS '任务类型UPSERT/DELETE/REBUILD';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.task_status IS '任务状态';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.retry_count IS '重试次数';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.error_message IS '错误信息';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.payload_json IS '任务载荷JSON';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.adcode IS '行政区划编码';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.tenant_id IS '租户ID';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.tenant_path IS '租户路径';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.dept_id IS '部门ID';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.dept_path IS '部门路径';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.created_at IS '创建时间';
COMMENT ON COLUMN ai.tb_ai_knowledge_sync_task.updated_at IS '更新时间';
DROP TABLE IF EXISTS ai.tb_ai_graph_entity CASCADE;
CREATE TABLE IF NOT EXISTS ai.tb_ai_graph_entity (
entity_id VARCHAR(64) PRIMARY KEY,
file_id VARCHAR(64),
entity_type VARCHAR(64) NOT NULL,
entity_name VARCHAR(255) NOT NULL,
normalized_name VARCHAR(255),
graph_vertex_id VARCHAR(128),
entity_status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
sync_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
properties_json JSONB NOT NULL DEFAULT '{}'::JSONB,
adcode VARCHAR(12),
tenant_id VARCHAR(64),
tenant_path VARCHAR(255),
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_ai_graph_entity_file_name_type
UNIQUE (file_id, entity_name, entity_type),
CONSTRAINT chk_ai_graph_entity_status
CHECK (entity_status IN ('ACTIVE', 'DISABLED')),
CONSTRAINT chk_ai_graph_entity_sync_status
CHECK (sync_status IN ('PENDING', 'SYNCED', 'FAILED')),
CONSTRAINT chk_ai_graph_entity_properties_json
CHECK (jsonb_typeof(properties_json) = 'object'),
CONSTRAINT fk_ai_graph_entity_file
FOREIGN KEY (file_id) REFERENCES ai.tb_ai_knowledge_file(file_id) ON DELETE SET NULL
);
COMMENT ON TABLE ai.tb_ai_graph_entity IS '图谱实体表(用于实体绑定)';
COMMENT ON COLUMN ai.tb_ai_graph_entity.entity_id IS '实体ID';
COMMENT ON COLUMN ai.tb_ai_graph_entity.file_id IS '来源知识文件ID';
COMMENT ON COLUMN ai.tb_ai_graph_entity.entity_type IS '实体类型';
COMMENT ON COLUMN ai.tb_ai_graph_entity.entity_name IS '实体名称';
COMMENT ON COLUMN ai.tb_ai_graph_entity.normalized_name IS '标准化实体名称';
COMMENT ON COLUMN ai.tb_ai_graph_entity.graph_vertex_id IS '图数据库顶点ID';
COMMENT ON COLUMN ai.tb_ai_graph_entity.entity_status IS '实体状态';
COMMENT ON COLUMN ai.tb_ai_graph_entity.sync_status IS '同步状态';
COMMENT ON COLUMN ai.tb_ai_graph_entity.properties_json IS '实体属性JSON';
COMMENT ON COLUMN ai.tb_ai_graph_entity.adcode IS '行政区划编码';
COMMENT ON COLUMN ai.tb_ai_graph_entity.tenant_id IS '租户ID';
COMMENT ON COLUMN ai.tb_ai_graph_entity.tenant_path IS '租户路径';
COMMENT ON COLUMN ai.tb_ai_graph_entity.dept_id IS '部门ID';
COMMENT ON COLUMN ai.tb_ai_graph_entity.dept_path IS '部门路径';
COMMENT ON COLUMN ai.tb_ai_graph_entity.created_at IS '创建时间';
COMMENT ON COLUMN ai.tb_ai_graph_entity.updated_at IS '更新时间';
DROP TABLE IF EXISTS ai.tb_ai_graph_relation CASCADE;
CREATE TABLE IF NOT EXISTS ai.tb_ai_graph_relation (
relation_id VARCHAR(64) PRIMARY KEY,
src_entity_id VARCHAR(64) NOT NULL,
dst_entity_id VARCHAR(64) NOT NULL,
relation_type VARCHAR(64) NOT NULL,
relation_name VARCHAR(128),
graph_edge_id VARCHAR(128),
relation_weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
relation_status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
sync_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
properties_json JSONB NOT NULL DEFAULT '{}'::JSONB,
adcode VARCHAR(12),
tenant_id VARCHAR(64),
tenant_path VARCHAR(255),
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uk_ai_graph_relation_src_dst_type
UNIQUE (src_entity_id, dst_entity_id, relation_type),
CONSTRAINT chk_ai_graph_relation_src_dst
CHECK (src_entity_id <> dst_entity_id),
CONSTRAINT chk_ai_graph_relation_weight
CHECK (relation_weight > 0 AND relation_weight <= 1),
CONSTRAINT chk_ai_graph_relation_status
CHECK (relation_status IN ('ACTIVE', 'DISABLED')),
CONSTRAINT chk_ai_graph_relation_sync_status
CHECK (sync_status IN ('PENDING', 'SYNCED', 'FAILED')),
CONSTRAINT chk_ai_graph_relation_properties_json
CHECK (jsonb_typeof(properties_json) = 'object'),
CONSTRAINT fk_ai_graph_relation_src
FOREIGN KEY (src_entity_id) REFERENCES ai.tb_ai_graph_entity(entity_id) ON DELETE CASCADE,
CONSTRAINT fk_ai_graph_relation_dst
FOREIGN KEY (dst_entity_id) REFERENCES ai.tb_ai_graph_entity(entity_id) ON DELETE CASCADE
);
COMMENT ON TABLE ai.tb_ai_graph_relation IS '图谱实体关联表(用于实体关系绑定)';
COMMENT ON COLUMN ai.tb_ai_graph_relation.relation_id IS '关系ID';
COMMENT ON COLUMN ai.tb_ai_graph_relation.src_entity_id IS '源实体ID';
COMMENT ON COLUMN ai.tb_ai_graph_relation.dst_entity_id IS '目标实体ID';
COMMENT ON COLUMN ai.tb_ai_graph_relation.relation_type IS '关系类型';
COMMENT ON COLUMN ai.tb_ai_graph_relation.relation_name IS '关系名称';
COMMENT ON COLUMN ai.tb_ai_graph_relation.graph_edge_id IS '图数据库边ID';
COMMENT ON COLUMN ai.tb_ai_graph_relation.relation_weight IS '关系权重';
COMMENT ON COLUMN ai.tb_ai_graph_relation.relation_status IS '关系状态';
COMMENT ON COLUMN ai.tb_ai_graph_relation.sync_status IS '同步状态';
COMMENT ON COLUMN ai.tb_ai_graph_relation.properties_json IS '关系属性JSON';
COMMENT ON COLUMN ai.tb_ai_graph_relation.adcode IS '行政区划编码';
COMMENT ON COLUMN ai.tb_ai_graph_relation.tenant_id IS '租户ID';
COMMENT ON COLUMN ai.tb_ai_graph_relation.tenant_path IS '租户路径';
COMMENT ON COLUMN ai.tb_ai_graph_relation.dept_id IS '部门ID';
COMMENT ON COLUMN ai.tb_ai_graph_relation.dept_path IS '部门路径';
COMMENT ON COLUMN ai.tb_ai_graph_relation.created_at IS '创建时间';
COMMENT ON COLUMN ai.tb_ai_graph_relation.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_ai_knowledge_file_sync_status
ON ai.tb_ai_knowledge_file(graph_sync_status, vector_sync_status, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_ai_knowledge_sync_task_status
ON ai.tb_ai_knowledge_sync_task(task_status, target_store, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ai_graph_entity_name
ON ai.tb_ai_graph_entity(entity_type, entity_name);
CREATE INDEX IF NOT EXISTS idx_ai_graph_relation_src_dst
ON ai.tb_ai_graph_relation(src_entity_id, dst_entity_id, relation_type);

View File

@@ -1,6 +1,60 @@
INSERT INTO ai.tb_ai_model_config (
model_id, model_name, provider, endpoint, adcode, tenant_id, tenant_path, dept_id, dept_path
INSERT INTO ai.tb_ai_knowledge_file (
file_id, file_code, file_name, file_type, file_status,
graph_sync_status, vector_sync_status, metadata_json,
adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES (
'MODEL-MOCK-001', 'mock-python-ai', 'local', 'http://localhost:9000', '330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'
'KF-DEMO-001', 'K12_DEMO_KNOWLEDGE_FILE', 'K12示例知识文件', 'DOCUMENT', 'ACTIVE',
'PENDING', 'PENDING', '{"source":"seed","scenario":"knowledge_file_management"}'::JSONB,
'330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'
)
ON CONFLICT (model_id) DO NOTHING;
ON CONFLICT (file_id) DO NOTHING;
INSERT INTO ai.tb_ai_graph_entity (
entity_id, file_id, entity_type, entity_name, normalized_name, sync_status, properties_json,
adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES (
'GE-KP-001', 'KF-DEMO-001', 'KNOWLEDGE_POINT', '一次函数', '一次函数', 'PENDING',
'{"subject":"math","grade":"7"}'::JSONB,
'330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'
)
ON CONFLICT (entity_id) DO NOTHING;
INSERT INTO ai.tb_ai_graph_entity (
entity_id, file_id, entity_type, entity_name, normalized_name, sync_status, properties_json,
adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES (
'GE-QUESTION-001', 'KF-DEMO-001', 'QUESTION', '一次函数应用题', '一次函数应用题', 'PENDING',
'{"difficulty":"MEDIUM"}'::JSONB,
'330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'
)
ON CONFLICT (entity_id) DO NOTHING;
INSERT INTO ai.tb_ai_graph_relation (
relation_id, src_entity_id, dst_entity_id, relation_type, relation_name, relation_weight, sync_status, properties_json,
adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES (
'GR-001', 'GE-QUESTION-001', 'GE-KP-001', 'BELONGS_TO', '题目关联知识点', 1.0000, 'PENDING',
'{"source":"manual_binding"}'::JSONB,
'330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'
)
ON CONFLICT (relation_id) DO NOTHING;
INSERT INTO ai.tb_ai_knowledge_sync_task (
task_id, file_id, target_store, task_type, task_status, payload_json,
adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES (
'KST-GRAPH-001', 'KF-DEMO-001', 'GRAPH', 'UPSERT', 'PENDING',
'{"trigger":"seed","description":"sync to graph store"}'::JSONB,
'330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'
)
ON CONFLICT (task_id) DO NOTHING;
INSERT INTO ai.tb_ai_knowledge_sync_task (
task_id, file_id, target_store, task_type, task_status, payload_json,
adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES (
'KST-VECTOR-001', 'KF-DEMO-001', 'VECTOR', 'UPSERT', 'PENDING',
'{"trigger":"seed","description":"sync to vector store"}'::JSONB,
'330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'
)
ON CONFLICT (task_id) DO NOTHING;

View File

@@ -1,3 +1,7 @@
DROP SCHEMA IF EXISTS auth CASCADE;
CREATE SCHEMA IF NOT EXISTS auth;
DROP TABLE IF EXISTS auth.tb_auth_refresh_token CASCADE;
CREATE TABLE IF NOT EXISTS auth.tb_auth_refresh_token (
token_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
@@ -26,6 +30,7 @@ COMMENT ON COLUMN auth.tb_auth_refresh_token.expire_at IS '过期时间';
COMMENT ON COLUMN auth.tb_auth_refresh_token.revoked IS '是否撤销';
COMMENT ON COLUMN auth.tb_auth_refresh_token.created_at IS '创建时间';
DROP TABLE IF EXISTS auth.tb_auth_login_audit CASCADE;
CREATE TABLE IF NOT EXISTS auth.tb_auth_login_audit (
audit_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64),

View File

@@ -0,0 +1,512 @@
DROP SCHEMA IF EXISTS course CASCADE;
CREATE SCHEMA IF NOT EXISTS course;
DROP TABLE IF EXISTS course.cl_course CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_course (
course_id VARCHAR(64) PRIMARY KEY,
title VARCHAR(256) NOT NULL,
subject_code VARCHAR(32) NOT NULL,
grade_code VARCHAR(32) NOT NULL,
difficulty_level VARCHAR(16),
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE course.cl_course IS '课程主表';
COMMENT ON COLUMN course.cl_course.course_id IS '课程ID';
COMMENT ON COLUMN course.cl_course.title IS '课程标题';
COMMENT ON COLUMN course.cl_course.subject_code IS '学科编码';
COMMENT ON COLUMN course.cl_course.grade_code IS '年级编码';
COMMENT ON COLUMN course.cl_course.difficulty_level IS '难度等级';
COMMENT ON COLUMN course.cl_course.status IS '状态DRAFT/PUBLISHED/ARCHIVED';
COMMENT ON COLUMN course.cl_course.adcode IS '行政区划编码';
COMMENT ON COLUMN course.cl_course.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_course.tenant_path IS '租户路径';
COMMENT ON COLUMN course.cl_course.dept_id IS '部门ID';
COMMENT ON COLUMN course.cl_course.dept_path IS '部门路径';
COMMENT ON COLUMN course.cl_course.created_by IS '创建人ID';
COMMENT ON COLUMN course.cl_course.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_course_chapter CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_course_chapter (
chapter_id VARCHAR(64) PRIMARY KEY,
course_id VARCHAR(64) NOT NULL,
chapter_no INTEGER NOT NULL,
chapter_title VARCHAR(256) NOT NULL,
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_cl_course_chapter_course
FOREIGN KEY (course_id) REFERENCES course.cl_course(course_id)
);
COMMENT ON TABLE course.cl_course_chapter IS '课程章节表';
COMMENT ON COLUMN course.cl_course_chapter.chapter_id IS '章节ID';
COMMENT ON COLUMN course.cl_course_chapter.course_id IS '课程ID';
COMMENT ON COLUMN course.cl_course_chapter.chapter_no IS '章节序号';
COMMENT ON COLUMN course.cl_course_chapter.chapter_title IS '章节标题';
COMMENT ON COLUMN course.cl_course_chapter.adcode IS '行政区划编码';
COMMENT ON COLUMN course.cl_course_chapter.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_course_chapter.tenant_path IS '租户路径';
COMMENT ON COLUMN course.cl_course_chapter.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_course_node CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_course_node (
node_id VARCHAR(64) PRIMARY KEY,
chapter_id VARCHAR(64) NOT NULL,
node_no INTEGER NOT NULL,
node_title VARCHAR(256) NOT NULL,
node_type VARCHAR(32) NOT NULL DEFAULT 'LESSON',
class_type VARCHAR(32),
duration_sec INTEGER,
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_cl_course_node_type
CHECK (node_type IN ('LESSON', 'IN_CLASS_ACTIVITY', 'AFTER_CLASS_TASK', 'MATERIAL', 'OTHER')),
CONSTRAINT fk_cl_course_node_chapter
FOREIGN KEY (chapter_id) REFERENCES course.cl_course_chapter(chapter_id)
);
COMMENT ON TABLE course.cl_course_node IS '课程学习节点表';
COMMENT ON COLUMN course.cl_course_node.node_id IS '学习节点ID';
COMMENT ON COLUMN course.cl_course_node.chapter_id IS '章节ID';
COMMENT ON COLUMN course.cl_course_node.node_no IS '节点序号';
COMMENT ON COLUMN course.cl_course_node.node_title IS '节点标题';
COMMENT ON COLUMN course.cl_course_node.node_type IS '节点类型LESSON/IN_CLASS_ACTIVITY/AFTER_CLASS_TASK/MATERIAL/OTHER';
COMMENT ON COLUMN course.cl_course_node.class_type IS '课堂类型(如讲授/讨论/测验)';
COMMENT ON COLUMN course.cl_course_node.duration_sec IS '时长(秒)';
COMMENT ON COLUMN course.cl_course_node.adcode IS '行政区划编码';
COMMENT ON COLUMN course.cl_course_node.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_course_node.tenant_path IS '租户路径';
COMMENT ON COLUMN course.cl_course_node.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_knowledge_point CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_knowledge_point (
kp_id VARCHAR(64) PRIMARY KEY,
kp_code VARCHAR(64) NOT NULL,
kp_name VARCHAR(256) NOT NULL,
kp_alias VARCHAR(256),
kp_type VARCHAR(32) NOT NULL DEFAULT 'ATOMIC',
parent_kp_id VARCHAR(64),
kp_path VARCHAR(512),
kp_desc TEXT,
subject_code VARCHAR(32) NOT NULL,
grade_code VARCHAR(32) NOT NULL,
difficulty_level VARCHAR(16),
importance_level NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
graph_entity_id VARCHAR(64),
source_db VARCHAR(64) NOT NULL DEFAULT 'postgresql',
source_table VARCHAR(128) NOT NULL DEFAULT 'course.cl_knowledge_point',
source_pk VARCHAR(128),
metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB,
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_cl_knowledge_point_tenant_code
UNIQUE (tenant_id, kp_code),
CONSTRAINT uq_cl_knowledge_point_tenant_graph_entity
UNIQUE (tenant_id, graph_entity_id),
CONSTRAINT chk_cl_knowledge_point_type
CHECK (kp_type IN ('DOMAIN', 'TOPIC', 'ATOMIC')),
CONSTRAINT chk_cl_knowledge_point_importance
CHECK (importance_level > 0 AND importance_level <= 1),
CONSTRAINT chk_cl_knowledge_point_metadata_json
CHECK (jsonb_typeof(metadata_json) = 'object'),
CONSTRAINT fk_cl_knowledge_point_parent
FOREIGN KEY (parent_kp_id) REFERENCES course.cl_knowledge_point(kp_id) ON DELETE SET NULL
);
COMMENT ON TABLE course.cl_knowledge_point IS '知识点表';
COMMENT ON COLUMN course.cl_knowledge_point.kp_id IS '知识点ID';
COMMENT ON COLUMN course.cl_knowledge_point.kp_code IS '知识点编码';
COMMENT ON COLUMN course.cl_knowledge_point.kp_name IS '知识点名称';
COMMENT ON COLUMN course.cl_knowledge_point.kp_alias IS '知识点别名';
COMMENT ON COLUMN course.cl_knowledge_point.kp_type IS '知识点类型DOMAIN/TOPIC/ATOMIC';
COMMENT ON COLUMN course.cl_knowledge_point.parent_kp_id IS '父知识点ID';
COMMENT ON COLUMN course.cl_knowledge_point.kp_path IS '知识点树路径';
COMMENT ON COLUMN course.cl_knowledge_point.kp_desc IS '知识点描述';
COMMENT ON COLUMN course.cl_knowledge_point.subject_code IS '学科编码';
COMMENT ON COLUMN course.cl_knowledge_point.grade_code IS '年级编码';
COMMENT ON COLUMN course.cl_knowledge_point.difficulty_level IS '难度等级';
COMMENT ON COLUMN course.cl_knowledge_point.importance_level IS '重要性权重0,1]';
COMMENT ON COLUMN course.cl_knowledge_point.graph_entity_id IS '图谱实体IDNebula/Neo4j 映射)';
COMMENT ON COLUMN course.cl_knowledge_point.source_db IS '来源数据库';
COMMENT ON COLUMN course.cl_knowledge_point.source_table IS '来源表';
COMMENT ON COLUMN course.cl_knowledge_point.source_pk IS '来源主键';
COMMENT ON COLUMN course.cl_knowledge_point.metadata_json IS '扩展元数据JSON';
COMMENT ON COLUMN course.cl_knowledge_point.status IS '状态ACTIVE/DISABLED';
COMMENT ON COLUMN course.cl_knowledge_point.adcode IS '行政区划编码';
COMMENT ON COLUMN course.cl_knowledge_point.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_knowledge_point.tenant_path IS '租户路径';
COMMENT ON COLUMN course.cl_knowledge_point.dept_id IS '部门ID';
COMMENT ON COLUMN course.cl_knowledge_point.dept_path IS '部门路径';
COMMENT ON COLUMN course.cl_knowledge_point.created_at IS '创建时间';
COMMENT ON COLUMN course.cl_knowledge_point.updated_at IS '更新时间';
DROP TABLE IF EXISTS course.cl_chapter_kp_rel CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_chapter_kp_rel (
chapter_id VARCHAR(64) NOT NULL,
kp_id VARCHAR(64) NOT NULL,
relation_type VARCHAR(32) NOT NULL DEFAULT 'TEACHES',
weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
is_core BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (chapter_id, kp_id),
CONSTRAINT chk_cl_chapter_kp_rel_type
CHECK (relation_type IN ('TEACHES', 'REVIEWS', 'EXTENDS')),
CONSTRAINT chk_cl_chapter_kp_rel_weight
CHECK (weight > 0 AND weight <= 1),
CONSTRAINT fk_cl_chapter_kp_rel_chapter
FOREIGN KEY (chapter_id) REFERENCES course.cl_course_chapter(chapter_id),
CONSTRAINT fk_cl_chapter_kp_rel_kp
FOREIGN KEY (kp_id) REFERENCES course.cl_knowledge_point(kp_id)
);
COMMENT ON TABLE course.cl_chapter_kp_rel IS '章节-知识点关联表';
COMMENT ON COLUMN course.cl_chapter_kp_rel.chapter_id IS '章节ID';
COMMENT ON COLUMN course.cl_chapter_kp_rel.kp_id IS '知识点ID';
COMMENT ON COLUMN course.cl_chapter_kp_rel.relation_type IS '关系类型TEACHES/REVIEWS/EXTENDS';
COMMENT ON COLUMN course.cl_chapter_kp_rel.weight IS '关联权重0,1]';
COMMENT ON COLUMN course.cl_chapter_kp_rel.is_core IS '是否核心知识点';
COMMENT ON COLUMN course.cl_chapter_kp_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_chapter_kp_rel.created_at IS '创建时间';
COMMENT ON COLUMN course.cl_chapter_kp_rel.updated_at IS '更新时间';
DROP TABLE IF EXISTS course.cl_student_kp_mastery CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_student_kp_mastery (
mastery_id VARCHAR(64) PRIMARY KEY,
student_id VARCHAR(64) NOT NULL,
kp_id VARCHAR(64) NOT NULL,
mastery_score NUMERIC(6,4) NOT NULL DEFAULT 0,
mastery_level VARCHAR(16) NOT NULL DEFAULT 'UNKNOWN',
mastery_status VARCHAR(16) NOT NULL DEFAULT 'LEARNING',
evidence_json JSONB NOT NULL DEFAULT '{}'::JSONB,
last_source_type VARCHAR(32),
last_source_id VARCHAR(64),
last_assessed_at TIMESTAMP,
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_cl_student_kp_mastery_student_kp
UNIQUE (student_id, kp_id),
CONSTRAINT chk_cl_student_kp_mastery_score
CHECK (mastery_score >= 0 AND mastery_score <= 1),
CONSTRAINT chk_cl_student_kp_mastery_level
CHECK (mastery_level IN ('UNKNOWN', 'LOW', 'MEDIUM', 'HIGH', 'MASTERED')),
CONSTRAINT chk_cl_student_kp_mastery_status
CHECK (mastery_status IN ('LEARNING', 'STABLE', 'FORGOTTEN')),
CONSTRAINT chk_cl_student_kp_mastery_evidence_json
CHECK (jsonb_typeof(evidence_json) = 'object'),
CONSTRAINT fk_cl_student_kp_mastery_student
FOREIGN KEY (student_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_cl_student_kp_mastery_kp
FOREIGN KEY (kp_id) REFERENCES course.cl_knowledge_point(kp_id)
);
COMMENT ON TABLE course.cl_student_kp_mastery IS '学生知识点掌握度表';
COMMENT ON COLUMN course.cl_student_kp_mastery.mastery_id IS '掌握度记录ID';
COMMENT ON COLUMN course.cl_student_kp_mastery.student_id IS '学生ID';
COMMENT ON COLUMN course.cl_student_kp_mastery.kp_id IS '知识点ID';
COMMENT ON COLUMN course.cl_student_kp_mastery.mastery_score IS '掌握度分值0-1';
COMMENT ON COLUMN course.cl_student_kp_mastery.mastery_level IS '掌握等级';
COMMENT ON COLUMN course.cl_student_kp_mastery.mastery_status IS '掌握状态LEARNING/STABLE/FORGOTTEN';
COMMENT ON COLUMN course.cl_student_kp_mastery.evidence_json IS '证据数据JSON错题、批改、学习行为等';
COMMENT ON COLUMN course.cl_student_kp_mastery.last_source_type IS '最近更新来源类型';
COMMENT ON COLUMN course.cl_student_kp_mastery.last_source_id IS '最近更新来源ID';
COMMENT ON COLUMN course.cl_student_kp_mastery.last_assessed_at IS '最近评估时间';
COMMENT ON COLUMN course.cl_student_kp_mastery.adcode IS '行政区划编码';
COMMENT ON COLUMN course.cl_student_kp_mastery.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_student_kp_mastery.tenant_path IS '租户路径';
COMMENT ON COLUMN course.cl_student_kp_mastery.dept_id IS '部门ID';
COMMENT ON COLUMN course.cl_student_kp_mastery.dept_path IS '部门路径';
COMMENT ON COLUMN course.cl_student_kp_mastery.created_at IS '创建时间';
COMMENT ON COLUMN course.cl_student_kp_mastery.updated_at IS '更新时间';
DROP TABLE IF EXISTS course.cl_kp_material_rel CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_kp_material_rel (
rel_id VARCHAR(64) PRIMARY KEY,
kp_id VARCHAR(64) NOT NULL,
material_type VARCHAR(32) NOT NULL,
material_ref_id VARCHAR(64) NOT NULL,
material_uri VARCHAR(512),
weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
source_table VARCHAR(128),
source_pk VARCHAR(128),
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_cl_kp_material_rel_type
CHECK (material_type IN ('VECTOR_DOCUMENT', 'VECTOR_CHUNK', 'COURSE_RESOURCE', 'EXTERNAL_URI')),
CONSTRAINT chk_cl_kp_material_rel_weight
CHECK (weight > 0 AND weight <= 1),
CONSTRAINT fk_cl_kp_material_rel_kp
FOREIGN KEY (kp_id) REFERENCES course.cl_knowledge_point(kp_id)
);
COMMENT ON TABLE course.cl_kp_material_rel IS '知识点-学习资料关联表';
COMMENT ON COLUMN course.cl_kp_material_rel.rel_id IS '关联ID';
COMMENT ON COLUMN course.cl_kp_material_rel.kp_id IS '知识点ID';
COMMENT ON COLUMN course.cl_kp_material_rel.material_type IS '资料类型VECTOR_DOCUMENT/VECTOR_CHUNK/COURSE_RESOURCE/EXTERNAL_URI';
COMMENT ON COLUMN course.cl_kp_material_rel.material_ref_id IS '资料对象ID';
COMMENT ON COLUMN course.cl_kp_material_rel.material_uri IS '资料URI';
COMMENT ON COLUMN course.cl_kp_material_rel.weight IS '关联权重0,1]';
COMMENT ON COLUMN course.cl_kp_material_rel.source_table IS '来源表';
COMMENT ON COLUMN course.cl_kp_material_rel.source_pk IS '来源主键';
COMMENT ON COLUMN course.cl_kp_material_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_kp_material_rel.created_at IS '创建时间';
COMMENT ON COLUMN course.cl_kp_material_rel.updated_at IS '更新时间';
DROP TABLE IF EXISTS course.cl_course_knowledge_rel CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_course_knowledge_rel (
course_id VARCHAR(64) NOT NULL,
kp_id VARCHAR(64) NOT NULL,
weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (course_id, kp_id),
CONSTRAINT chk_cl_course_knowledge_rel_weight
CHECK (weight > 0 AND weight <= 1),
CONSTRAINT fk_cl_course_knowledge_rel_course
FOREIGN KEY (course_id) REFERENCES course.cl_course(course_id),
CONSTRAINT fk_cl_course_knowledge_rel_kp
FOREIGN KEY (kp_id) REFERENCES course.cl_knowledge_point(kp_id)
);
COMMENT ON TABLE course.cl_course_knowledge_rel IS '课程-知识点关联表';
COMMENT ON COLUMN course.cl_course_knowledge_rel.course_id IS '课程ID';
COMMENT ON COLUMN course.cl_course_knowledge_rel.kp_id IS '知识点ID';
COMMENT ON COLUMN course.cl_course_knowledge_rel.weight IS '关联权重0,1]';
COMMENT ON COLUMN course.cl_course_knowledge_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_course_knowledge_rel.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_course_tag CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_course_tag (
tag_id VARCHAR(64) PRIMARY KEY,
tag_name VARCHAR(128) NOT NULL,
tag_type VARCHAR(32) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE course.cl_course_tag IS '课程标签表';
COMMENT ON COLUMN course.cl_course_tag.tag_id IS '标签ID';
COMMENT ON COLUMN course.cl_course_tag.tag_name IS '标签名称';
COMMENT ON COLUMN course.cl_course_tag.tag_type IS '标签类型';
COMMENT ON COLUMN course.cl_course_tag.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_course_tag.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_course_tag_rel CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_course_tag_rel (
course_id VARCHAR(64) NOT NULL,
tag_id VARCHAR(64) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (course_id, tag_id),
CONSTRAINT fk_cl_course_tag_rel_course
FOREIGN KEY (course_id) REFERENCES course.cl_course(course_id),
CONSTRAINT fk_cl_course_tag_rel_tag
FOREIGN KEY (tag_id) REFERENCES course.cl_course_tag(tag_id)
);
COMMENT ON TABLE course.cl_course_tag_rel IS '课程-标签关联表';
COMMENT ON COLUMN course.cl_course_tag_rel.course_id IS '课程ID';
COMMENT ON COLUMN course.cl_course_tag_rel.tag_id IS '标签ID';
COMMENT ON COLUMN course.cl_course_tag_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_course_tag_rel.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_node_resource CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_node_resource (
resource_id VARCHAR(64) PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
resource_type VARCHAR(32) NOT NULL,
file_id VARCHAR(64),
resource_url VARCHAR(512),
resource_title VARCHAR(256),
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_cl_node_resource_type
CHECK (resource_type IN ('PDF', 'VIDEO', 'DOC', 'IMAGE', 'LINK', 'OTHER')),
CONSTRAINT chk_cl_node_resource_ref
CHECK (file_id IS NOT NULL OR resource_url IS NOT NULL),
CONSTRAINT fk_cl_node_resource_node
FOREIGN KEY (node_id) REFERENCES course.cl_course_node(node_id),
CONSTRAINT fk_cl_node_resource_file
FOREIGN KEY (file_id) REFERENCES upms.tb_sys_file(file_id)
);
COMMENT ON TABLE course.cl_node_resource IS '学习节点资源表';
COMMENT ON COLUMN course.cl_node_resource.resource_id IS '资源ID';
COMMENT ON COLUMN course.cl_node_resource.node_id IS '学习节点ID';
COMMENT ON COLUMN course.cl_node_resource.resource_type IS '资源类型PDF/VIDEO/DOC/IMAGE/LINK/OTHER';
COMMENT ON COLUMN course.cl_node_resource.file_id IS '资源文件ID';
COMMENT ON COLUMN course.cl_node_resource.resource_url IS '资源地址';
COMMENT ON COLUMN course.cl_node_resource.resource_title IS '资源标题';
COMMENT ON COLUMN course.cl_node_resource.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_node_resource.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_node_kp_rel CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_node_kp_rel (
node_id VARCHAR(64) NOT NULL,
kp_id VARCHAR(64) NOT NULL,
relation_type VARCHAR(32) NOT NULL DEFAULT 'TEACHES',
weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (node_id, kp_id),
CONSTRAINT chk_cl_node_kp_rel_type
CHECK (relation_type IN ('TEACHES', 'PRACTICES', 'REVIEWS')),
CONSTRAINT chk_cl_node_kp_rel_weight
CHECK (weight > 0 AND weight <= 1),
CONSTRAINT fk_cl_node_kp_rel_node
FOREIGN KEY (node_id) REFERENCES course.cl_course_node(node_id),
CONSTRAINT fk_cl_node_kp_rel_kp
FOREIGN KEY (kp_id) REFERENCES course.cl_knowledge_point(kp_id)
);
COMMENT ON TABLE course.cl_node_kp_rel IS '学习节点-知识点关联表';
COMMENT ON COLUMN course.cl_node_kp_rel.node_id IS '学习节点ID';
COMMENT ON COLUMN course.cl_node_kp_rel.kp_id IS '知识点ID';
COMMENT ON COLUMN course.cl_node_kp_rel.relation_type IS '关系类型TEACHES/PRACTICES/REVIEWS';
COMMENT ON COLUMN course.cl_node_kp_rel.weight IS '关联权重0,1]';
COMMENT ON COLUMN course.cl_node_kp_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_node_kp_rel.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_node_homework_rel CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_node_homework_rel (
node_id VARCHAR(64) NOT NULL,
assignment_id VARCHAR(64) NOT NULL,
relation_type VARCHAR(32) NOT NULL DEFAULT 'AFTER_CLASS',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (node_id, assignment_id),
CONSTRAINT chk_cl_node_homework_rel_type
CHECK (relation_type IN ('IN_CLASS', 'AFTER_CLASS')),
CONSTRAINT fk_cl_node_homework_rel_node
FOREIGN KEY (node_id) REFERENCES course.cl_course_node(node_id)
);
COMMENT ON TABLE course.cl_node_homework_rel IS '学习节点-作业关联表';
COMMENT ON COLUMN course.cl_node_homework_rel.node_id IS '学习节点ID';
COMMENT ON COLUMN course.cl_node_homework_rel.assignment_id IS '作业IDquestion.hw_assignment.assignment_id';
COMMENT ON COLUMN course.cl_node_homework_rel.relation_type IS '关系类型IN_CLASS/AFTER_CLASS';
COMMENT ON COLUMN course.cl_node_homework_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_node_homework_rel.created_at IS '创建时间';
DROP TABLE IF EXISTS course.cl_learning_session CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_learning_session (
session_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
course_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'STARTED',
started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ended_at TIMESTAMP,
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
CONSTRAINT fk_cl_learning_session_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_cl_learning_session_course
FOREIGN KEY (course_id) REFERENCES course.cl_course(course_id)
);
COMMENT ON TABLE course.cl_learning_session IS '学习会话表';
COMMENT ON COLUMN course.cl_learning_session.session_id IS '会话ID';
COMMENT ON COLUMN course.cl_learning_session.user_id IS '学员ID';
COMMENT ON COLUMN course.cl_learning_session.course_id IS '课程ID';
COMMENT ON COLUMN course.cl_learning_session.status IS '会话状态STARTED/PAUSED/COMPLETED';
COMMENT ON COLUMN course.cl_learning_session.started_at IS '开始时间';
COMMENT ON COLUMN course.cl_learning_session.ended_at IS '结束时间';
COMMENT ON COLUMN course.cl_learning_session.adcode IS '行政区划编码';
COMMENT ON COLUMN course.cl_learning_session.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_learning_session.tenant_path IS '租户路径';
COMMENT ON COLUMN course.cl_learning_session.dept_id IS '部门ID';
COMMENT ON COLUMN course.cl_learning_session.dept_path IS '部门路径';
DROP TABLE IF EXISTS course.cl_learning_progress CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_learning_progress (
progress_id VARCHAR(64) PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
node_id VARCHAR(64) NOT NULL,
progress_pct NUMERIC(5,2) NOT NULL DEFAULT 0,
last_position_sec INTEGER NOT NULL DEFAULT 0,
mastery_level VARCHAR(16),
tenant_id VARCHAR(64) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_cl_learning_progress_session
FOREIGN KEY (session_id) REFERENCES course.cl_learning_session(session_id),
CONSTRAINT fk_cl_learning_progress_node
FOREIGN KEY (node_id) REFERENCES course.cl_course_node(node_id)
);
COMMENT ON TABLE course.cl_learning_progress IS '学习进度表';
COMMENT ON COLUMN course.cl_learning_progress.progress_id IS '进度ID';
COMMENT ON COLUMN course.cl_learning_progress.session_id IS '学习会话ID';
COMMENT ON COLUMN course.cl_learning_progress.node_id IS '学习节点ID';
COMMENT ON COLUMN course.cl_learning_progress.progress_pct IS '进度百分比';
COMMENT ON COLUMN course.cl_learning_progress.last_position_sec IS '最后学习位置(秒)';
COMMENT ON COLUMN course.cl_learning_progress.mastery_level IS '掌握等级';
COMMENT ON COLUMN course.cl_learning_progress.tenant_id IS '租户ID';
COMMENT ON COLUMN course.cl_learning_progress.updated_at IS '更新时间';
DROP TABLE IF EXISTS course.cl_learning_event CASCADE;
CREATE TABLE IF NOT EXISTS course.cl_learning_event (
event_id VARCHAR(64) PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
event_type VARCHAR(32) NOT NULL,
event_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
payload_json JSONB,
tenant_id VARCHAR(64) NOT NULL,
CONSTRAINT fk_cl_learning_event_session
FOREIGN KEY (session_id) REFERENCES course.cl_learning_session(session_id)
);
COMMENT ON TABLE course.cl_learning_event IS '学习行为事件表';
COMMENT ON COLUMN course.cl_learning_event.event_id IS '事件ID';
COMMENT ON COLUMN course.cl_learning_event.session_id IS '会话ID';
COMMENT ON COLUMN course.cl_learning_event.event_type IS '事件类型start/pause/seek/finish';
COMMENT ON COLUMN course.cl_learning_event.event_time IS '事件时间';
COMMENT ON COLUMN course.cl_learning_event.payload_json IS '事件扩展信息';
COMMENT ON COLUMN course.cl_learning_event.tenant_id IS '租户ID';
CREATE INDEX IF NOT EXISTS idx_cl_course_tenant_subject_grade
ON course.cl_course(tenant_id, subject_code, grade_code);
CREATE INDEX IF NOT EXISTS idx_cl_course_chapter_course_no
ON course.cl_course_chapter(course_id, chapter_no);
CREATE INDEX IF NOT EXISTS idx_cl_course_node_chapter_no
ON course.cl_course_node(chapter_id, node_no);
CREATE INDEX IF NOT EXISTS idx_cl_knowledge_point_subject_grade
ON course.cl_knowledge_point(tenant_id, subject_code, grade_code);
CREATE INDEX IF NOT EXISTS idx_cl_knowledge_point_parent
ON course.cl_knowledge_point(tenant_id, parent_kp_id);
CREATE INDEX IF NOT EXISTS idx_cl_knowledge_point_graph_entity
ON course.cl_knowledge_point(tenant_id, graph_entity_id);
CREATE INDEX IF NOT EXISTS idx_cl_chapter_kp_rel_chapter
ON course.cl_chapter_kp_rel(tenant_id, chapter_id, relation_type);
CREATE INDEX IF NOT EXISTS idx_cl_chapter_kp_rel_kp
ON course.cl_chapter_kp_rel(tenant_id, kp_id, relation_type);
CREATE INDEX IF NOT EXISTS idx_cl_student_kp_mastery_student
ON course.cl_student_kp_mastery(tenant_id, student_id, mastery_score DESC);
CREATE INDEX IF NOT EXISTS idx_cl_student_kp_mastery_kp
ON course.cl_student_kp_mastery(tenant_id, kp_id, mastery_score DESC);
CREATE INDEX IF NOT EXISTS idx_cl_kp_material_rel_kp
ON course.cl_kp_material_rel(tenant_id, kp_id, material_type);
CREATE INDEX IF NOT EXISTS idx_cl_node_resource_node
ON course.cl_node_resource(node_id, resource_type);
CREATE INDEX IF NOT EXISTS idx_cl_node_kp_rel_node
ON course.cl_node_kp_rel(tenant_id, node_id, relation_type);
CREATE INDEX IF NOT EXISTS idx_cl_node_kp_rel_kp
ON course.cl_node_kp_rel(tenant_id, kp_id, relation_type);
CREATE INDEX IF NOT EXISTS idx_cl_node_homework_rel_node
ON course.cl_node_homework_rel(tenant_id, node_id, relation_type);
CREATE INDEX IF NOT EXISTS idx_cl_learning_session_user_course
ON course.cl_learning_session(tenant_id, user_id, course_id);
CREATE INDEX IF NOT EXISTS idx_cl_learning_progress_session_node
ON course.cl_learning_progress(session_id, node_id);
CREATE INDEX IF NOT EXISTS idx_cl_learning_event_session_time
ON course.cl_learning_event(session_id, event_time DESC);

View File

@@ -0,0 +1,2 @@
-- 课程学习模块初始化数据占位
-- 按需补充课程、章节、课时、知识点、标签等种子数据

View File

@@ -0,0 +1,729 @@
DROP SCHEMA IF EXISTS question CASCADE;
CREATE SCHEMA IF NOT EXISTS question;
DROP TABLE IF EXISTS question.hw_question_bank CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_question_bank (
bank_id VARCHAR(64) PRIMARY KEY,
bank_name VARCHAR(256) NOT NULL,
subject_code VARCHAR(32) NOT NULL,
grade_code VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE question.hw_question_bank IS '题库表';
COMMENT ON COLUMN question.hw_question_bank.bank_id IS '题库ID';
COMMENT ON COLUMN question.hw_question_bank.bank_name IS '题库名称';
COMMENT ON COLUMN question.hw_question_bank.subject_code IS '学科编码';
COMMENT ON COLUMN question.hw_question_bank.grade_code IS '年级编码';
COMMENT ON COLUMN question.hw_question_bank.status IS '状态';
COMMENT ON COLUMN question.hw_question_bank.adcode IS '行政区划编码';
COMMENT ON COLUMN question.hw_question_bank.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_question_bank.tenant_path IS '租户路径';
COMMENT ON COLUMN question.hw_question_bank.dept_id IS '部门ID';
COMMENT ON COLUMN question.hw_question_bank.dept_path IS '部门路径';
COMMENT ON COLUMN question.hw_question_bank.created_by IS '创建人ID';
COMMENT ON COLUMN question.hw_question_bank.created_at IS '创建时间';
DROP TABLE IF EXISTS question.hw_question_item CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_question_item (
question_id VARCHAR(64) PRIMARY KEY,
question_type VARCHAR(32) NOT NULL,
stem TEXT NOT NULL,
stem_json JSONB,
difficulty VARCHAR(16),
answer_payload JSONB,
analysis TEXT,
scoring_rule_json JSONB,
question_status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_hw_question_item_type
CHECK (question_type IN ('SINGLE_CHOICE', 'MULTIPLE_CHOICE', 'FILL_BLANK', 'JUDGE', 'SUBJECTIVE')),
CONSTRAINT chk_hw_question_item_answer_payload
CHECK (answer_payload IS NOT NULL AND jsonb_typeof(answer_payload) = 'object')
);
COMMENT ON TABLE question.hw_question_item IS '题目表(含标准答案)';
COMMENT ON COLUMN question.hw_question_item.question_id IS '题目ID';
COMMENT ON COLUMN question.hw_question_item.question_type IS '题型';
COMMENT ON COLUMN question.hw_question_item.stem IS '题干';
COMMENT ON COLUMN question.hw_question_item.stem_json IS '结构化题干JSON富文本/图片公式等)';
COMMENT ON COLUMN question.hw_question_item.difficulty IS '难度';
COMMENT ON COLUMN question.hw_question_item.answer_payload IS '统一标准答案载荷JSONB对应后端题型答案类型';
COMMENT ON COLUMN question.hw_question_item.analysis IS '题目解析';
COMMENT ON COLUMN question.hw_question_item.scoring_rule_json IS '评分规则JSON含主观题评分点';
COMMENT ON COLUMN question.hw_question_item.question_status IS '题目状态ACTIVE/DISABLED';
COMMENT ON COLUMN question.hw_question_item.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_question_item.created_at IS '创建时间';
COMMENT ON COLUMN question.hw_question_item.updated_at IS '更新时间';
DROP TABLE IF EXISTS question.hw_bank_question_rel CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_bank_question_rel (
bank_id VARCHAR(64) NOT NULL,
question_id VARCHAR(64) NOT NULL,
question_order INTEGER,
source_type VARCHAR(32) NOT NULL DEFAULT 'MANUAL',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (bank_id, question_id),
CONSTRAINT chk_hw_bank_question_rel_source_type
CHECK (source_type IN ('MANUAL', 'AI_GENERATED', 'IMPORTED', 'MIGRATED')),
CONSTRAINT fk_hw_bank_question_rel_bank
FOREIGN KEY (bank_id) REFERENCES question.hw_question_bank(bank_id),
CONSTRAINT fk_hw_bank_question_rel_question
FOREIGN KEY (question_id) REFERENCES question.hw_question_item(question_id)
);
COMMENT ON TABLE question.hw_bank_question_rel IS '题库-题目关联表';
COMMENT ON COLUMN question.hw_bank_question_rel.bank_id IS '题库ID';
COMMENT ON COLUMN question.hw_bank_question_rel.question_id IS '题目ID';
COMMENT ON COLUMN question.hw_bank_question_rel.question_order IS '题库内题目顺序';
COMMENT ON COLUMN question.hw_bank_question_rel.source_type IS '题目来源类型MANUAL/AI_GENERATED/IMPORTED/MIGRATED';
COMMENT ON COLUMN question.hw_bank_question_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_bank_question_rel.created_at IS '创建时间';
DROP TABLE IF EXISTS question.hw_question_kp_rel CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_question_kp_rel (
question_id VARCHAR(64) NOT NULL,
kp_id VARCHAR(64) NOT NULL,
relation_type VARCHAR(32) NOT NULL DEFAULT 'TESTS',
weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
confidence NUMERIC(5,4),
graph_relation_id VARCHAR(64),
source_table VARCHAR(128),
source_pk VARCHAR(128),
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (question_id, kp_id),
CONSTRAINT chk_hw_question_kp_rel_type
CHECK (relation_type IN ('TESTS', 'RELATED', 'SUPPORTS')),
CONSTRAINT chk_hw_question_kp_rel_weight
CHECK (weight > 0 AND weight <= 1),
CONSTRAINT chk_hw_question_kp_rel_confidence
CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 1)),
CONSTRAINT fk_hw_question_kp_rel_question
FOREIGN KEY (question_id) REFERENCES question.hw_question_item(question_id),
CONSTRAINT fk_hw_question_kp_rel_kp
FOREIGN KEY (kp_id) REFERENCES course.cl_knowledge_point(kp_id)
);
COMMENT ON TABLE question.hw_question_kp_rel IS '题目-知识点关联表';
COMMENT ON COLUMN question.hw_question_kp_rel.question_id IS '题目ID';
COMMENT ON COLUMN question.hw_question_kp_rel.kp_id IS '知识点ID';
COMMENT ON COLUMN question.hw_question_kp_rel.relation_type IS '关系类型TESTS/RELATED/SUPPORTS';
COMMENT ON COLUMN question.hw_question_kp_rel.weight IS '关联权重0,1]';
COMMENT ON COLUMN question.hw_question_kp_rel.confidence IS '抽取置信度';
COMMENT ON COLUMN question.hw_question_kp_rel.graph_relation_id IS '图谱关系ID';
COMMENT ON COLUMN question.hw_question_kp_rel.source_table IS '来源表';
COMMENT ON COLUMN question.hw_question_kp_rel.source_pk IS '来源主键';
COMMENT ON COLUMN question.hw_question_kp_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_question_kp_rel.created_at IS '创建时间';
COMMENT ON COLUMN question.hw_question_kp_rel.updated_at IS '更新时间';
DROP TABLE IF EXISTS question.hw_paper CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_paper (
paper_id VARCHAR(64) PRIMARY KEY,
paper_name VARCHAR(256) NOT NULL,
paper_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD',
subject_code VARCHAR(32) NOT NULL,
total_score NUMERIC(8,2) NOT NULL DEFAULT 100,
source_file_id VARCHAR(64),
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
grading_policy_json JSONB NOT NULL DEFAULT '{}'::JSONB,
CONSTRAINT chk_hw_paper_type
CHECK (paper_type IN ('STANDARD', 'EXTERNAL_UPLOAD')),
CONSTRAINT chk_hw_paper_grading_policy_json
CHECK (jsonb_typeof(grading_policy_json) = 'object'),
CONSTRAINT fk_hw_paper_source_file
FOREIGN KEY (source_file_id) REFERENCES upms.tb_sys_file(file_id)
);
COMMENT ON TABLE question.hw_paper IS '试卷表';
COMMENT ON COLUMN question.hw_paper.paper_id IS '试卷ID';
COMMENT ON COLUMN question.hw_paper.paper_name IS '试卷名称';
COMMENT ON COLUMN question.hw_paper.paper_type IS '试卷类型STANDARD/EXTERNAL_UPLOAD';
COMMENT ON COLUMN question.hw_paper.subject_code IS '学科编码';
COMMENT ON COLUMN question.hw_paper.total_score IS '总分';
COMMENT ON COLUMN question.hw_paper.source_file_id IS '来源文件ID课外上传作业解析来源';
COMMENT ON COLUMN question.hw_paper.adcode IS '行政区划编码';
COMMENT ON COLUMN question.hw_paper.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_paper.tenant_path IS '租户路径';
COMMENT ON COLUMN question.hw_paper.created_by IS '创建人';
COMMENT ON COLUMN question.hw_paper.created_at IS '创建时间';
COMMENT ON COLUMN question.hw_paper.grading_policy_json IS '试卷批改策略JSON单选/判断完全匹配、多选得分策略、填空匹配策略、简答评估模式)';
DROP TABLE IF EXISTS question.hw_paper_question CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_paper_question (
paper_id VARCHAR(64) NOT NULL,
question_id VARCHAR(64) NOT NULL,
question_order INTEGER NOT NULL,
score NUMERIC(8,2) NOT NULL DEFAULT 0,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (paper_id, question_id),
CONSTRAINT fk_hw_paper_question_paper
FOREIGN KEY (paper_id) REFERENCES question.hw_paper(paper_id),
CONSTRAINT fk_hw_paper_question_question
FOREIGN KEY (question_id) REFERENCES question.hw_question_item(question_id)
);
COMMENT ON TABLE question.hw_paper_question IS '试卷-题目关联表';
COMMENT ON COLUMN question.hw_paper_question.paper_id IS '试卷ID';
COMMENT ON COLUMN question.hw_paper_question.question_id IS '题目ID';
COMMENT ON COLUMN question.hw_paper_question.question_order IS '题目顺序';
COMMENT ON COLUMN question.hw_paper_question.score IS '题目分值';
COMMENT ON COLUMN question.hw_paper_question.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_paper_question.created_at IS '创建时间';
DROP TABLE IF EXISTS question.hw_assignment CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_assignment (
assignment_id VARCHAR(64) PRIMARY KEY,
paper_id VARCHAR(64) NOT NULL,
assignment_mode VARCHAR(32) NOT NULL DEFAULT 'PAPER',
title VARCHAR(256) NOT NULL,
publish_time TIMESTAMP,
deadline TIMESTAMP,
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_hw_assignment_mode
CHECK (assignment_mode IN ('PAPER', 'EXTERNAL_UPLOAD')),
CONSTRAINT fk_hw_assignment_paper
FOREIGN KEY (paper_id) REFERENCES question.hw_paper(paper_id)
);
COMMENT ON TABLE question.hw_assignment IS '作业表';
COMMENT ON COLUMN question.hw_assignment.assignment_id IS '作业ID';
COMMENT ON COLUMN question.hw_assignment.paper_id IS '试卷ID';
COMMENT ON COLUMN question.hw_assignment.assignment_mode IS '作业模式PAPER/EXTERNAL_UPLOAD';
COMMENT ON COLUMN question.hw_assignment.title IS '作业标题';
COMMENT ON COLUMN question.hw_assignment.publish_time IS '发布时间';
COMMENT ON COLUMN question.hw_assignment.deadline IS '截止时间';
COMMENT ON COLUMN question.hw_assignment.status IS '状态DRAFT/PUBLISHED/CLOSED';
COMMENT ON COLUMN question.hw_assignment.adcode IS '行政区划编码';
COMMENT ON COLUMN question.hw_assignment.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_assignment.tenant_path IS '租户路径';
COMMENT ON COLUMN question.hw_assignment.dept_id IS '部门ID';
COMMENT ON COLUMN question.hw_assignment.dept_path IS '部门路径';
COMMENT ON COLUMN question.hw_assignment.created_by IS '创建人';
COMMENT ON COLUMN question.hw_assignment.created_at IS '创建时间';
DROP TABLE IF EXISTS question.hw_assignment_target CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_assignment_target (
target_id VARCHAR(64) PRIMARY KEY,
assignment_id VARCHAR(64) NOT NULL,
target_type VARCHAR(32) NOT NULL,
target_ref_id VARCHAR(64) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_hw_assignment_target_assignment
FOREIGN KEY (assignment_id) REFERENCES question.hw_assignment(assignment_id)
);
COMMENT ON TABLE question.hw_assignment_target IS '作业投放对象表';
COMMENT ON COLUMN question.hw_assignment_target.target_id IS '投放记录ID';
COMMENT ON COLUMN question.hw_assignment_target.assignment_id IS '作业ID';
COMMENT ON COLUMN question.hw_assignment_target.target_type IS '投放类型CLASS/STUDENT';
COMMENT ON COLUMN question.hw_assignment_target.target_ref_id IS '投放对象ID';
COMMENT ON COLUMN question.hw_assignment_target.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_assignment_target.created_at IS '创建时间';
DROP TABLE IF EXISTS question.hw_submission CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_submission (
submission_id VARCHAR(64) PRIMARY KEY,
assignment_id VARCHAR(64) NOT NULL,
student_id VARCHAR(64) NOT NULL,
submission_mode VARCHAR(32) NOT NULL DEFAULT 'ASSIGNMENT',
origin_file_id VARCHAR(64),
submit_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
used_seconds INTEGER,
status VARCHAR(32) NOT NULL DEFAULT 'SUBMITTED',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_hw_submission_mode
CHECK (submission_mode IN ('ASSIGNMENT', 'EXTERNAL_UPLOAD')),
CONSTRAINT fk_hw_submission_assignment
FOREIGN KEY (assignment_id) REFERENCES question.hw_assignment(assignment_id),
CONSTRAINT fk_hw_submission_student
FOREIGN KEY (student_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_hw_submission_origin_file
FOREIGN KEY (origin_file_id) REFERENCES upms.tb_sys_file(file_id)
);
COMMENT ON TABLE question.hw_submission IS '作业提交表';
COMMENT ON COLUMN question.hw_submission.submission_id IS '提交ID';
COMMENT ON COLUMN question.hw_submission.assignment_id IS '作业ID';
COMMENT ON COLUMN question.hw_submission.student_id IS '学员ID';
COMMENT ON COLUMN question.hw_submission.submission_mode IS '提交模式ASSIGNMENT/EXTERNAL_UPLOAD';
COMMENT ON COLUMN question.hw_submission.origin_file_id IS '原始上传文件ID课外作业';
COMMENT ON COLUMN question.hw_submission.submit_time IS '提交时间';
COMMENT ON COLUMN question.hw_submission.used_seconds IS '作答耗时(秒)';
COMMENT ON COLUMN question.hw_submission.status IS '提交状态';
COMMENT ON COLUMN question.hw_submission.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_submission.created_at IS '创建时间';
DROP TABLE IF EXISTS question.hw_submission_answer CASCADE;
CREATE TABLE IF NOT EXISTS question.hw_submission_answer (
answer_id VARCHAR(64) PRIMARY KEY,
submission_id VARCHAR(64) NOT NULL,
question_id VARCHAR(64) NOT NULL,
answer_type VARCHAR(32) NOT NULL,
answer_payload JSONB,
file_id VARCHAR(64),
file_type VARCHAR(32),
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_hw_submission_answer_type
CHECK (answer_type IN ('SINGLE_CHOICE', 'MULTIPLE_CHOICE', 'FILL_BLANK', 'JUDGE', 'SUBJECTIVE')),
CONSTRAINT chk_hw_submission_answer_payload
CHECK (
answer_payload IS NOT NULL
OR file_id IS NOT NULL
),
CONSTRAINT chk_hw_submission_answer_payload_json
CHECK (answer_payload IS NULL OR jsonb_typeof(answer_payload) = 'object'),
CONSTRAINT chk_hw_submission_answer_file_type
CHECK (file_type IS NULL OR file_type IN ('IMAGE', 'AUDIO', 'VIDEO', 'DOCUMENT', 'OTHER')),
CONSTRAINT chk_hw_submission_answer_file_ref
CHECK (
(file_id IS NULL AND file_type IS NULL)
OR (file_id IS NOT NULL AND file_type IS NOT NULL)
),
CONSTRAINT uq_hw_submission_answer_submission_question
UNIQUE (submission_id, question_id),
CONSTRAINT fk_hw_submission_answer_submission
FOREIGN KEY (submission_id) REFERENCES question.hw_submission(submission_id),
CONSTRAINT fk_hw_submission_answer_question
FOREIGN KEY (question_id) REFERENCES question.hw_question_item(question_id),
CONSTRAINT fk_hw_submission_answer_file
FOREIGN KEY (file_id) REFERENCES upms.tb_sys_file(file_id)
);
COMMENT ON TABLE question.hw_submission_answer IS '提交答案明细表(一次提交中每题一行)';
COMMENT ON COLUMN question.hw_submission_answer.answer_id IS '答案ID';
COMMENT ON COLUMN question.hw_submission_answer.submission_id IS '提交ID';
COMMENT ON COLUMN question.hw_submission_answer.question_id IS '题目ID';
COMMENT ON COLUMN question.hw_submission_answer.answer_type IS '答案类型(与题型一致)';
COMMENT ON COLUMN question.hw_submission_answer.answer_payload IS '统一作答载荷JSONB对应后端答案类型';
COMMENT ON COLUMN question.hw_submission_answer.file_id IS '作答文件ID图片/音频等)';
COMMENT ON COLUMN question.hw_submission_answer.file_type IS '作答文件类型';
COMMENT ON COLUMN question.hw_submission_answer.tenant_id IS '租户ID';
COMMENT ON COLUMN question.hw_submission_answer.created_at IS '创建时间';
COMMENT ON COLUMN question.hw_submission_answer.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_hw_question_bank_tenant_subject_grade
ON question.hw_question_bank(tenant_id, subject_code, grade_code);
CREATE INDEX IF NOT EXISTS idx_hw_question_item_type
ON question.hw_question_item(question_type, difficulty);
CREATE INDEX IF NOT EXISTS idx_hw_question_item_tenant
ON question.hw_question_item(tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_hw_bank_question_rel_question
ON question.hw_bank_question_rel(question_id, bank_id);
CREATE INDEX IF NOT EXISTS idx_hw_bank_question_rel_tenant_bank
ON question.hw_bank_question_rel(tenant_id, bank_id, question_order);
CREATE INDEX IF NOT EXISTS idx_hw_question_kp_rel_tenant_question
ON question.hw_question_kp_rel(tenant_id, question_id);
CREATE INDEX IF NOT EXISTS idx_hw_question_kp_rel_tenant_kp
ON question.hw_question_kp_rel(tenant_id, kp_id);
CREATE INDEX IF NOT EXISTS idx_hw_question_kp_rel_graph_relation
ON question.hw_question_kp_rel(graph_relation_id);
CREATE INDEX IF NOT EXISTS idx_hw_assignment_tenant_status_deadline
ON question.hw_assignment(tenant_id, status, deadline);
CREATE INDEX IF NOT EXISTS idx_hw_assignment_mode
ON question.hw_assignment(assignment_mode, tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_hw_assignment_target_assignment
ON question.hw_assignment_target(assignment_id, target_type);
CREATE INDEX IF NOT EXISTS idx_hw_submission_assignment_student
ON question.hw_submission(assignment_id, student_id);
CREATE INDEX IF NOT EXISTS idx_hw_submission_mode
ON question.hw_submission(submission_mode, tenant_id, submit_time DESC);
CREATE INDEX IF NOT EXISTS idx_hw_submission_origin_file
ON question.hw_submission(origin_file_id);
CREATE INDEX IF NOT EXISTS idx_hw_submission_answer_submission
ON question.hw_submission_answer(submission_id);
CREATE INDEX IF NOT EXISTS idx_hw_submission_answer_question
ON question.hw_submission_answer(question_id);
CREATE INDEX IF NOT EXISTS idx_hw_submission_answer_file_id
ON question.hw_submission_answer(file_id);
DROP TABLE IF EXISTS question.gd_grading_task CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_grading_task (
grading_task_id VARCHAR(64) PRIMARY KEY,
submission_id VARCHAR(64) NOT NULL,
paper_id VARCHAR(64) NOT NULL,
task_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
trigger_source VARCHAR(32) NOT NULL DEFAULT 'SUBMISSION',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_gd_grading_task_submission
FOREIGN KEY (submission_id) REFERENCES question.hw_submission(submission_id),
CONSTRAINT fk_gd_grading_task_paper
FOREIGN KEY (paper_id) REFERENCES question.hw_paper(paper_id)
);
COMMENT ON TABLE question.gd_grading_task IS '批改任务表';
COMMENT ON COLUMN question.gd_grading_task.grading_task_id IS '批改任务ID';
COMMENT ON COLUMN question.gd_grading_task.submission_id IS '提交ID';
COMMENT ON COLUMN question.gd_grading_task.paper_id IS '试卷ID';
COMMENT ON COLUMN question.gd_grading_task.task_status IS '任务状态PENDING/RUNNING/WAIT_REVIEW/DONE';
COMMENT ON COLUMN question.gd_grading_task.trigger_source IS '触发来源';
COMMENT ON COLUMN question.gd_grading_task.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_grading_task.created_at IS '创建时间';
COMMENT ON COLUMN question.gd_grading_task.updated_at IS '更新时间';
DROP TABLE IF EXISTS question.gd_answer_grade CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_answer_grade (
answer_grade_id VARCHAR(64) PRIMARY KEY,
grading_task_id VARCHAR(64) NOT NULL,
answer_id VARCHAR(64) NOT NULL,
paper_id VARCHAR(64) NOT NULL,
grade_mode VARCHAR(32) NOT NULL DEFAULT 'AUTO',
grade_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
score NUMERIC(8,2),
grader_id VARCHAR(64),
grade_comment TEXT,
evidence_json JSONB,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_gd_answer_grade_mode
CHECK (grade_mode IN ('AUTO', 'MANUAL', 'AI', 'AI_REVIEW')),
CONSTRAINT chk_gd_answer_grade_status
CHECK (grade_status IN ('PENDING', 'DONE', 'REVIEWED')),
CONSTRAINT uq_gd_answer_grade_task_answer
UNIQUE (grading_task_id, answer_id),
CONSTRAINT fk_gd_answer_grade_task
FOREIGN KEY (grading_task_id) REFERENCES question.gd_grading_task(grading_task_id),
CONSTRAINT fk_gd_answer_grade_answer
FOREIGN KEY (answer_id) REFERENCES question.hw_submission_answer(answer_id),
CONSTRAINT fk_gd_answer_grade_paper
FOREIGN KEY (paper_id) REFERENCES question.hw_paper(paper_id),
CONSTRAINT fk_gd_answer_grade_grader
FOREIGN KEY (grader_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE question.gd_answer_grade IS '答案批改结果表(统一客观/主观)';
COMMENT ON COLUMN question.gd_answer_grade.answer_grade_id IS '答案批改结果ID';
COMMENT ON COLUMN question.gd_answer_grade.grading_task_id IS '批改任务ID';
COMMENT ON COLUMN question.gd_answer_grade.answer_id IS '答案ID';
COMMENT ON COLUMN question.gd_answer_grade.paper_id IS '试卷ID';
COMMENT ON COLUMN question.gd_answer_grade.grade_mode IS '批改模式AUTO/MANUAL/AI/AI_REVIEW';
COMMENT ON COLUMN question.gd_answer_grade.grade_status IS '批改状态PENDING/DONE/REVIEWED';
COMMENT ON COLUMN question.gd_answer_grade.score IS '最终得分';
COMMENT ON COLUMN question.gd_answer_grade.grader_id IS '评阅人ID';
COMMENT ON COLUMN question.gd_answer_grade.grade_comment IS '批改评语';
COMMENT ON COLUMN question.gd_answer_grade.evidence_json IS '判定依据JSON';
COMMENT ON COLUMN question.gd_answer_grade.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_answer_grade.created_at IS '创建时间';
COMMENT ON COLUMN question.gd_answer_grade.updated_at IS '更新时间';
DROP TABLE IF EXISTS question.gd_score_summary CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_score_summary (
summary_id VARCHAR(64) PRIMARY KEY,
grading_task_id VARCHAR(64) UNIQUE NOT NULL,
total_score NUMERIC(8,2) NOT NULL DEFAULT 0,
grade_level VARCHAR(8),
surpass_ratio NUMERIC(5,2),
used_seconds INTEGER,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_gd_score_summary_task
FOREIGN KEY (grading_task_id) REFERENCES question.gd_grading_task(grading_task_id)
);
COMMENT ON TABLE question.gd_score_summary IS '批改结果汇总表';
COMMENT ON COLUMN question.gd_score_summary.summary_id IS '汇总ID';
COMMENT ON COLUMN question.gd_score_summary.grading_task_id IS '批改任务ID';
COMMENT ON COLUMN question.gd_score_summary.total_score IS '总分';
COMMENT ON COLUMN question.gd_score_summary.grade_level IS '等级A/B/C';
COMMENT ON COLUMN question.gd_score_summary.surpass_ratio IS '超越比例';
COMMENT ON COLUMN question.gd_score_summary.used_seconds IS '作答耗时';
COMMENT ON COLUMN question.gd_score_summary.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_score_summary.created_at IS '创建时间';
DROP TABLE IF EXISTS question.gd_error_tag CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_error_tag (
error_tag_id VARCHAR(64) PRIMARY KEY,
tag_code VARCHAR(64) UNIQUE NOT NULL,
tag_name VARCHAR(128) NOT NULL,
category VARCHAR(32) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE question.gd_error_tag IS '错因标签表';
COMMENT ON COLUMN question.gd_error_tag.error_tag_id IS '标签ID';
COMMENT ON COLUMN question.gd_error_tag.tag_code IS '标签编码';
COMMENT ON COLUMN question.gd_error_tag.tag_name IS '标签名称';
COMMENT ON COLUMN question.gd_error_tag.category IS '分类(审题/计算/概念等)';
COMMENT ON COLUMN question.gd_error_tag.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_error_tag.created_at IS '创建时间';
DROP TABLE IF EXISTS question.gd_answer_error_rel CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_answer_error_rel (
answer_id VARCHAR(64) NOT NULL,
error_tag_id VARCHAR(64) NOT NULL,
confidence NUMERIC(5,4),
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (answer_id, error_tag_id),
CONSTRAINT fk_gd_answer_error_rel_answer
FOREIGN KEY (answer_id) REFERENCES question.hw_submission_answer(answer_id),
CONSTRAINT fk_gd_answer_error_rel_tag
FOREIGN KEY (error_tag_id) REFERENCES question.gd_error_tag(error_tag_id)
);
COMMENT ON TABLE question.gd_answer_error_rel IS '答案-错因标签关联表';
COMMENT ON COLUMN question.gd_answer_error_rel.answer_id IS '答案ID';
COMMENT ON COLUMN question.gd_answer_error_rel.error_tag_id IS '错因标签ID';
COMMENT ON COLUMN question.gd_answer_error_rel.confidence IS '置信度';
COMMENT ON COLUMN question.gd_answer_error_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_answer_error_rel.created_at IS '创建时间';
DROP TABLE IF EXISTS question.gd_answer_kp_analysis CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_answer_kp_analysis (
answer_id VARCHAR(64) NOT NULL,
kp_id VARCHAR(64) NOT NULL,
grading_task_id VARCHAR(64),
correctness VARCHAR(16) NOT NULL DEFAULT 'UNKNOWN',
mastery_score NUMERIC(6,4),
confidence NUMERIC(5,4),
evidence_json JSONB NOT NULL DEFAULT '{}'::JSONB,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (answer_id, kp_id),
CONSTRAINT chk_gd_answer_kp_analysis_correctness
CHECK (correctness IN ('CORRECT', 'PARTIAL', 'WRONG', 'UNKNOWN')),
CONSTRAINT chk_gd_answer_kp_analysis_mastery
CHECK (mastery_score IS NULL OR (mastery_score >= 0 AND mastery_score <= 1)),
CONSTRAINT chk_gd_answer_kp_analysis_confidence
CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 1)),
CONSTRAINT chk_gd_answer_kp_analysis_evidence_json
CHECK (jsonb_typeof(evidence_json) = 'object'),
CONSTRAINT fk_gd_answer_kp_analysis_answer
FOREIGN KEY (answer_id) REFERENCES question.hw_submission_answer(answer_id),
CONSTRAINT fk_gd_answer_kp_analysis_kp
FOREIGN KEY (kp_id) REFERENCES course.cl_knowledge_point(kp_id),
CONSTRAINT fk_gd_answer_kp_analysis_grading_task
FOREIGN KEY (grading_task_id) REFERENCES question.gd_grading_task(grading_task_id)
);
COMMENT ON TABLE question.gd_answer_kp_analysis IS '答案-知识点分析表';
COMMENT ON COLUMN question.gd_answer_kp_analysis.answer_id IS '答案ID';
COMMENT ON COLUMN question.gd_answer_kp_analysis.kp_id IS '知识点ID';
COMMENT ON COLUMN question.gd_answer_kp_analysis.grading_task_id IS '批改任务ID';
COMMENT ON COLUMN question.gd_answer_kp_analysis.correctness IS '正确性CORRECT/PARTIAL/WRONG/UNKNOWN';
COMMENT ON COLUMN question.gd_answer_kp_analysis.mastery_score IS '答案层知识点掌握度0-1';
COMMENT ON COLUMN question.gd_answer_kp_analysis.confidence IS '分析置信度';
COMMENT ON COLUMN question.gd_answer_kp_analysis.evidence_json IS '知识点分析依据JSON';
COMMENT ON COLUMN question.gd_answer_kp_analysis.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_answer_kp_analysis.created_at IS '创建时间';
COMMENT ON COLUMN question.gd_answer_kp_analysis.updated_at IS '更新时间';
DROP TABLE IF EXISTS question.gd_wrong_question CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_wrong_question (
wrong_question_id VARCHAR(64) PRIMARY KEY,
student_id VARCHAR(64) NOT NULL,
question_id VARCHAR(64) NOT NULL,
source_submission_id VARCHAR(64) NOT NULL,
mastery_status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
review_count INTEGER NOT NULL DEFAULT 0,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_gd_wrong_question_student
FOREIGN KEY (student_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_gd_wrong_question_question
FOREIGN KEY (question_id) REFERENCES question.hw_question_item(question_id),
CONSTRAINT fk_gd_wrong_question_submission
FOREIGN KEY (source_submission_id) REFERENCES question.hw_submission(submission_id)
);
COMMENT ON TABLE question.gd_wrong_question IS '错题沉淀表';
COMMENT ON COLUMN question.gd_wrong_question.wrong_question_id IS '错题记录ID';
COMMENT ON COLUMN question.gd_wrong_question.student_id IS '学员ID';
COMMENT ON COLUMN question.gd_wrong_question.question_id IS '题目ID';
COMMENT ON COLUMN question.gd_wrong_question.source_submission_id IS '来源提交ID';
COMMENT ON COLUMN question.gd_wrong_question.mastery_status IS '掌握状态';
COMMENT ON COLUMN question.gd_wrong_question.review_count IS '复习次数';
COMMENT ON COLUMN question.gd_wrong_question.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_wrong_question.created_at IS '创建时间';
COMMENT ON COLUMN question.gd_wrong_question.updated_at IS '更新时间';
DROP TABLE IF EXISTS question.gd_review_plan CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_review_plan (
review_plan_id VARCHAR(64) PRIMARY KEY,
wrong_question_id VARCHAR(64) NOT NULL,
plan_date DATE NOT NULL,
plan_stage VARCHAR(16) NOT NULL,
plan_status VARCHAR(32) NOT NULL DEFAULT 'TODO',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_gd_review_plan_wrong_question
FOREIGN KEY (wrong_question_id) REFERENCES question.gd_wrong_question(wrong_question_id)
);
COMMENT ON TABLE question.gd_review_plan IS '复习计划表';
COMMENT ON COLUMN question.gd_review_plan.review_plan_id IS '复习计划ID';
COMMENT ON COLUMN question.gd_review_plan.wrong_question_id IS '错题记录ID';
COMMENT ON COLUMN question.gd_review_plan.plan_date IS '计划日期';
COMMENT ON COLUMN question.gd_review_plan.plan_stage IS '阶段E1/E2/E3';
COMMENT ON COLUMN question.gd_review_plan.plan_status IS '计划状态';
COMMENT ON COLUMN question.gd_review_plan.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_review_plan.created_at IS '创建时间';
DROP TABLE IF EXISTS question.gd_teacher_comment CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_teacher_comment (
comment_id VARCHAR(64) PRIMARY KEY,
answer_grade_id VARCHAR(64) NOT NULL,
reviewer_id VARCHAR(64) NOT NULL,
comment_type VARCHAR(16) NOT NULL DEFAULT 'TEXT',
content_ref TEXT,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_gd_teacher_comment_answer_grade
FOREIGN KEY (answer_grade_id) REFERENCES question.gd_answer_grade(answer_grade_id),
CONSTRAINT fk_gd_teacher_comment_reviewer
FOREIGN KEY (reviewer_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE question.gd_teacher_comment IS '教师点评表';
COMMENT ON COLUMN question.gd_teacher_comment.comment_id IS '点评ID';
COMMENT ON COLUMN question.gd_teacher_comment.answer_grade_id IS '答案批改结果ID';
COMMENT ON COLUMN question.gd_teacher_comment.reviewer_id IS '教师ID';
COMMENT ON COLUMN question.gd_teacher_comment.comment_type IS '点评类型TEXT/VOICE';
COMMENT ON COLUMN question.gd_teacher_comment.content_ref IS '点评内容或资源引用';
COMMENT ON COLUMN question.gd_teacher_comment.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_teacher_comment.created_at IS '创建时间';
DROP TABLE IF EXISTS question.gd_explanation_dimension_score CASCADE;
DROP TABLE IF EXISTS question.gd_explanation_assessment CASCADE;
DROP TABLE IF EXISTS question.gd_explanation_submission CASCADE;
CREATE TABLE IF NOT EXISTS question.gd_explanation_submission (
explanation_id VARCHAR(64) PRIMARY KEY,
student_id VARCHAR(64) NOT NULL,
wrong_question_id VARCHAR(64),
source_submission_id VARCHAR(64),
source_answer_id VARCHAR(64),
audio_file_id VARCHAR(64) NOT NULL,
duration_seconds INTEGER,
transcript_text TEXT,
submission_status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_gd_explanation_submission_status
CHECK (submission_status IN ('SUBMITTED', 'EVALUATED', 'REJECTED')),
CONSTRAINT fk_gd_explanation_submission_student
FOREIGN KEY (student_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_gd_explanation_submission_wrong_question
FOREIGN KEY (wrong_question_id) REFERENCES question.gd_wrong_question(wrong_question_id),
CONSTRAINT fk_gd_explanation_submission_source_submission
FOREIGN KEY (source_submission_id) REFERENCES question.hw_submission(submission_id),
CONSTRAINT fk_gd_explanation_submission_source_answer
FOREIGN KEY (source_answer_id) REFERENCES question.hw_submission_answer(answer_id),
CONSTRAINT fk_gd_explanation_submission_audio_file
FOREIGN KEY (audio_file_id) REFERENCES upms.tb_sys_file(file_id)
);
COMMENT ON TABLE question.gd_explanation_submission IS '费曼讲解提交表';
COMMENT ON COLUMN question.gd_explanation_submission.explanation_id IS '讲解提交ID';
COMMENT ON COLUMN question.gd_explanation_submission.student_id IS '学员ID';
COMMENT ON COLUMN question.gd_explanation_submission.wrong_question_id IS '关联错题ID';
COMMENT ON COLUMN question.gd_explanation_submission.source_submission_id IS '来源作业提交ID';
COMMENT ON COLUMN question.gd_explanation_submission.source_answer_id IS '来源答案ID';
COMMENT ON COLUMN question.gd_explanation_submission.audio_file_id IS '讲解音频文件ID';
COMMENT ON COLUMN question.gd_explanation_submission.duration_seconds IS '时长(秒)';
COMMENT ON COLUMN question.gd_explanation_submission.transcript_text IS '转写文本';
COMMENT ON COLUMN question.gd_explanation_submission.submission_status IS '提交状态';
COMMENT ON COLUMN question.gd_explanation_submission.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_explanation_submission.created_at IS '创建时间';
COMMENT ON COLUMN question.gd_explanation_submission.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS question.gd_explanation_assessment (
assessment_id VARCHAR(64) PRIMARY KEY,
explanation_id VARCHAR(64) NOT NULL UNIQUE,
evaluator_type VARCHAR(16) NOT NULL DEFAULT 'AI',
evaluator_id VARCHAR(64),
total_score NUMERIC(8,2),
pass_status VARCHAR(16) NOT NULL DEFAULT 'PENDING',
improvement_suggestion TEXT,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_gd_explanation_assessment_evaluator_type
CHECK (evaluator_type IN ('AI', 'TEACHER', 'HYBRID')),
CONSTRAINT chk_gd_explanation_assessment_pass_status
CHECK (pass_status IN ('PENDING', 'PASSED', 'FAILED')),
CONSTRAINT fk_gd_explanation_assessment_explanation
FOREIGN KEY (explanation_id) REFERENCES question.gd_explanation_submission(explanation_id) ON DELETE CASCADE,
CONSTRAINT fk_gd_explanation_assessment_evaluator
FOREIGN KEY (evaluator_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE question.gd_explanation_assessment IS '费曼讲解评估汇总表';
COMMENT ON COLUMN question.gd_explanation_assessment.assessment_id IS '评估ID';
COMMENT ON COLUMN question.gd_explanation_assessment.explanation_id IS '讲解提交ID';
COMMENT ON COLUMN question.gd_explanation_assessment.evaluator_type IS '评估类型';
COMMENT ON COLUMN question.gd_explanation_assessment.evaluator_id IS '评估人ID';
COMMENT ON COLUMN question.gd_explanation_assessment.total_score IS '总分';
COMMENT ON COLUMN question.gd_explanation_assessment.pass_status IS '通过状态';
COMMENT ON COLUMN question.gd_explanation_assessment.improvement_suggestion IS '改进建议';
COMMENT ON COLUMN question.gd_explanation_assessment.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_explanation_assessment.created_at IS '创建时间';
COMMENT ON COLUMN question.gd_explanation_assessment.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS question.gd_explanation_dimension_score (
dimension_score_id VARCHAR(64) PRIMARY KEY,
assessment_id VARCHAR(64) NOT NULL,
dimension_code VARCHAR(32) NOT NULL,
dimension_name VARCHAR(64) NOT NULL,
score NUMERIC(8,2) NOT NULL DEFAULT 0,
weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
comment_text TEXT,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_gd_explanation_dimension_score_weight
CHECK (weight > 0 AND weight <= 1),
CONSTRAINT fk_gd_explanation_dimension_score_assessment
FOREIGN KEY (assessment_id) REFERENCES question.gd_explanation_assessment(assessment_id) ON DELETE CASCADE
);
COMMENT ON TABLE question.gd_explanation_dimension_score IS '费曼讲解维度评分表';
COMMENT ON COLUMN question.gd_explanation_dimension_score.dimension_score_id IS '维度评分ID';
COMMENT ON COLUMN question.gd_explanation_dimension_score.assessment_id IS '评估ID';
COMMENT ON COLUMN question.gd_explanation_dimension_score.dimension_code IS '维度编码';
COMMENT ON COLUMN question.gd_explanation_dimension_score.dimension_name IS '维度名称';
COMMENT ON COLUMN question.gd_explanation_dimension_score.score IS '维度得分';
COMMENT ON COLUMN question.gd_explanation_dimension_score.weight IS '维度权重';
COMMENT ON COLUMN question.gd_explanation_dimension_score.comment_text IS '维度评语';
COMMENT ON COLUMN question.gd_explanation_dimension_score.tenant_id IS '租户ID';
COMMENT ON COLUMN question.gd_explanation_dimension_score.created_at IS '创建时间';
CREATE INDEX IF NOT EXISTS idx_gd_grading_task_submission
ON question.gd_grading_task(submission_id, task_status);
CREATE INDEX IF NOT EXISTS idx_gd_grading_task_paper_status
ON question.gd_grading_task(paper_id, task_status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gd_answer_grade_task_status
ON question.gd_answer_grade(grading_task_id, grade_status);
CREATE INDEX IF NOT EXISTS idx_gd_answer_grade_paper_status
ON question.gd_answer_grade(paper_id, grade_status, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_gd_answer_error_rel_answer
ON question.gd_answer_error_rel(answer_id);
CREATE INDEX IF NOT EXISTS idx_gd_answer_kp_analysis_kp
ON question.gd_answer_kp_analysis(kp_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gd_answer_kp_analysis_task
ON question.gd_answer_kp_analysis(grading_task_id, kp_id);
CREATE INDEX IF NOT EXISTS idx_gd_wrong_question_student_status
ON question.gd_wrong_question(student_id, mastery_status);
CREATE INDEX IF NOT EXISTS idx_gd_review_plan_wrong_question_date
ON question.gd_review_plan(wrong_question_id, plan_date);
CREATE INDEX IF NOT EXISTS idx_gd_teacher_comment_answer_grade
ON question.gd_teacher_comment(answer_grade_id);
CREATE INDEX IF NOT EXISTS idx_gd_explanation_submission_student
ON question.gd_explanation_submission(student_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gd_explanation_submission_wrong_question
ON question.gd_explanation_submission(wrong_question_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gd_explanation_assessment_pass_status
ON question.gd_explanation_assessment(pass_status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gd_explanation_dimension_score_assessment
ON question.gd_explanation_dimension_score(assessment_id, dimension_code);

View File

@@ -0,0 +1,5 @@
-- 习题与作业模块初始化数据占位
-- 按需补充题库、题目、试卷、作业模板等种子数据
-- 批改与反馈模块初始化数据占位
-- 按需补充错因标签、统一答案批改结果、复习计划策略等种子数据

View File

@@ -0,0 +1,440 @@
DROP SCHEMA IF EXISTS recommendation CASCADE;
CREATE SCHEMA IF NOT EXISTS recommendation;
DROP TABLE IF EXISTS recommendation.rc_recommendation_task CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_recommendation_task (
task_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
scene_code VARCHAR(32) NOT NULL,
trigger_source VARCHAR(32) NOT NULL DEFAULT 'SCHEDULE',
strategy_version VARCHAR(32),
profile_snapshot_json JSONB NOT NULL DEFAULT '{}'::JSONB,
status VARCHAR(16) NOT NULL DEFAULT 'CREATED',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_rc_recommendation_task_status
CHECK (status IN ('CREATED', 'RUNNING', 'DONE', 'FAILED')),
CONSTRAINT chk_rc_recommendation_task_profile_json
CHECK (jsonb_typeof(profile_snapshot_json) = 'object'),
CONSTRAINT fk_rc_recommendation_task_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE recommendation.rc_recommendation_task IS '推荐任务表';
COMMENT ON COLUMN recommendation.rc_recommendation_task.task_id IS '推荐任务ID';
COMMENT ON COLUMN recommendation.rc_recommendation_task.user_id IS '推荐目标用户ID';
COMMENT ON COLUMN recommendation.rc_recommendation_task.scene_code IS '推荐场景编码HOME/WRONGBOOK/REVIEW等';
COMMENT ON COLUMN recommendation.rc_recommendation_task.trigger_source IS '触发来源SCHEDULE/EVENT/MANUAL';
COMMENT ON COLUMN recommendation.rc_recommendation_task.strategy_version IS '推荐策略版本';
COMMENT ON COLUMN recommendation.rc_recommendation_task.profile_snapshot_json IS '画像快照JSON';
COMMENT ON COLUMN recommendation.rc_recommendation_task.status IS '任务状态';
COMMENT ON COLUMN recommendation.rc_recommendation_task.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_recommendation_task.created_at IS '创建时间';
COMMENT ON COLUMN recommendation.rc_recommendation_task.updated_at IS '更新时间';
DROP TABLE IF EXISTS recommendation.rc_recommendation_item CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_recommendation_item (
item_id VARCHAR(64) PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
content_type VARCHAR(32) NOT NULL,
content_object_id VARCHAR(64) NOT NULL,
content_object_type VARCHAR(64) NOT NULL,
rank_no INTEGER NOT NULL,
score NUMERIC(10,6),
reason_codes VARCHAR(255),
metadata_json JSONB NOT NULL DEFAULT '{}'::JSONB,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_rc_recommendation_item_content_type
CHECK (content_type IN ('COURSE', 'QUESTION', 'PAPER', 'WRONG_QUESTION', 'REVIEW_PLAN', 'MIXED')),
CONSTRAINT chk_rc_recommendation_item_metadata_json
CHECK (jsonb_typeof(metadata_json) = 'object'),
CONSTRAINT uq_rc_recommendation_item_task_rank
UNIQUE (task_id, rank_no),
CONSTRAINT fk_rc_recommendation_item_task
FOREIGN KEY (task_id) REFERENCES recommendation.rc_recommendation_task(task_id) ON DELETE CASCADE
);
COMMENT ON TABLE recommendation.rc_recommendation_item IS '推荐结果明细表';
COMMENT ON COLUMN recommendation.rc_recommendation_item.item_id IS '推荐项ID';
COMMENT ON COLUMN recommendation.rc_recommendation_item.task_id IS '推荐任务ID';
COMMENT ON COLUMN recommendation.rc_recommendation_item.content_type IS '内容类型';
COMMENT ON COLUMN recommendation.rc_recommendation_item.content_object_id IS '内容对象ID';
COMMENT ON COLUMN recommendation.rc_recommendation_item.content_object_type IS '内容对象类型';
COMMENT ON COLUMN recommendation.rc_recommendation_item.rank_no IS '推荐位次';
COMMENT ON COLUMN recommendation.rc_recommendation_item.score IS '推荐分';
COMMENT ON COLUMN recommendation.rc_recommendation_item.reason_codes IS '推荐原因编码集合';
COMMENT ON COLUMN recommendation.rc_recommendation_item.metadata_json IS '扩展元数据JSON';
COMMENT ON COLUMN recommendation.rc_recommendation_item.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_recommendation_item.created_at IS '创建时间';
DROP TABLE IF EXISTS recommendation.rc_recommendation_feedback CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_recommendation_feedback (
feedback_id VARCHAR(64) PRIMARY KEY,
item_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
event_type VARCHAR(16) NOT NULL,
event_value NUMERIC(10,4),
event_detail_json JSONB NOT NULL DEFAULT '{}'::JSONB,
event_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_rc_recommendation_feedback_event_type
CHECK (event_type IN ('EXPOSE', 'CLICK', 'START', 'COMPLETE', 'DISLIKE', 'SKIP')),
CONSTRAINT chk_rc_recommendation_feedback_detail_json
CHECK (jsonb_typeof(event_detail_json) = 'object'),
CONSTRAINT fk_rc_recommendation_feedback_item
FOREIGN KEY (item_id) REFERENCES recommendation.rc_recommendation_item(item_id) ON DELETE CASCADE,
CONSTRAINT fk_rc_recommendation_feedback_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE recommendation.rc_recommendation_feedback IS '推荐反馈事件表';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.feedback_id IS '反馈事件ID';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.item_id IS '推荐项ID';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.user_id IS '用户ID';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.event_type IS '事件类型';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.event_value IS '事件值(可选)';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.event_detail_json IS '事件详情JSON';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.event_time IS '事件发生时间';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_recommendation_feedback.created_at IS '创建时间';
DROP TABLE IF EXISTS recommendation.rc_recommendation_effect_daily CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_recommendation_effect_daily (
stat_date DATE NOT NULL,
scene_code VARCHAR(32) NOT NULL,
strategy_version VARCHAR(32) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
expose_count BIGINT NOT NULL DEFAULT 0,
click_count BIGINT NOT NULL DEFAULT 0,
complete_count BIGINT NOT NULL DEFAULT 0,
dislike_count BIGINT NOT NULL DEFAULT 0,
ctr NUMERIC(10,6) NOT NULL DEFAULT 0,
complete_rate NUMERIC(10,6) NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (stat_date, scene_code, strategy_version, tenant_id)
);
COMMENT ON TABLE recommendation.rc_recommendation_effect_daily IS '推荐效果日统计表';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.stat_date IS '统计日期';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.scene_code IS '推荐场景编码';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.strategy_version IS '策略版本';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.expose_count IS '曝光次数';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.click_count IS '点击次数';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.complete_count IS '完成次数';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.dislike_count IS '不喜欢次数';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.ctr IS '点击率';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.complete_rate IS '完成率';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.created_at IS '创建时间';
COMMENT ON COLUMN recommendation.rc_recommendation_effect_daily.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_rc_recommendation_task_user_scene
ON recommendation.rc_recommendation_task(user_id, scene_code, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_rc_recommendation_task_tenant_status
ON recommendation.rc_recommendation_task(tenant_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_rc_recommendation_item_task
ON recommendation.rc_recommendation_item(task_id, rank_no);
CREATE INDEX IF NOT EXISTS idx_rc_recommendation_item_content
ON recommendation.rc_recommendation_item(content_object_type, content_object_id);
CREATE INDEX IF NOT EXISTS idx_rc_recommendation_feedback_item_event
ON recommendation.rc_recommendation_feedback(item_id, event_type, event_time DESC);
CREATE INDEX IF NOT EXISTS idx_rc_recommendation_feedback_user_event
ON recommendation.rc_recommendation_feedback(user_id, event_type, event_time DESC);
CREATE INDEX IF NOT EXISTS idx_rc_recommendation_effect_daily_tenant_date
ON recommendation.rc_recommendation_effect_daily(tenant_id, stat_date DESC);
DROP TABLE IF EXISTS recommendation.rc_student_profile_feature CASCADE;
DROP TABLE IF EXISTS recommendation.rc_student_profile_snapshot CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_student_profile_snapshot (
profile_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
profile_version VARCHAR(32) NOT NULL,
profile_date DATE NOT NULL,
feature_source_version VARCHAR(64),
profile_json JSONB NOT NULL DEFAULT '{}'::JSONB,
confidence_score NUMERIC(6,4),
profile_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_rc_student_profile_snapshot_user_version
UNIQUE (user_id, profile_version),
CONSTRAINT chk_rc_student_profile_snapshot_json
CHECK (jsonb_typeof(profile_json) = 'object'),
CONSTRAINT chk_rc_student_profile_snapshot_status
CHECK (profile_status IN ('ACTIVE', 'ARCHIVED')),
CONSTRAINT fk_rc_student_profile_snapshot_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE recommendation.rc_student_profile_snapshot IS '学员画像快照表';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.profile_id IS '画像ID';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.user_id IS '学员ID';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.profile_version IS '画像版本';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.profile_date IS '画像日期';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.feature_source_version IS '特征来源版本';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.profile_json IS '画像JSON';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.confidence_score IS '画像置信度';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.profile_status IS '画像状态';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.created_at IS '创建时间';
COMMENT ON COLUMN recommendation.rc_student_profile_snapshot.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS recommendation.rc_student_profile_feature (
feature_id VARCHAR(64) PRIMARY KEY,
profile_id VARCHAR(64) NOT NULL,
feature_group VARCHAR(32) NOT NULL,
feature_key VARCHAR(64) NOT NULL,
feature_value_num NUMERIC(18,6),
feature_value_text VARCHAR(512),
feature_value_json JSONB,
value_type VARCHAR(16) NOT NULL DEFAULT 'NUM',
source_domain VARCHAR(32) NOT NULL,
source_ref_id VARCHAR(64),
sample_time TIMESTAMP,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_rc_student_profile_feature_value_type
CHECK (value_type IN ('NUM', 'TEXT', 'JSON', 'BOOL')),
CONSTRAINT chk_rc_student_profile_feature_source_domain
CHECK (source_domain IN ('COURSE', 'HOMEWORK', 'GRADING', 'REVIEW', 'RECOMMENDATION', 'ACHIEVEMENT', 'SYSTEM')),
CONSTRAINT chk_rc_student_profile_feature_json
CHECK (feature_value_json IS NULL OR jsonb_typeof(feature_value_json) IN ('object', 'array', 'string', 'number', 'boolean')),
CONSTRAINT fk_rc_student_profile_feature_profile
FOREIGN KEY (profile_id) REFERENCES recommendation.rc_student_profile_snapshot(profile_id) ON DELETE CASCADE
);
COMMENT ON TABLE recommendation.rc_student_profile_feature IS '学员画像特征明细表';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.feature_id IS '特征ID';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.profile_id IS '画像ID';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.feature_group IS '特征组';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.feature_key IS '特征键';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.feature_value_num IS '数值特征值';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.feature_value_text IS '文本特征值';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.feature_value_json IS 'JSON特征值';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.value_type IS '值类型';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.source_domain IS '来源域';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.source_ref_id IS '来源对象ID';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.sample_time IS '采样时间';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_student_profile_feature.created_at IS '创建时间';
DROP TABLE IF EXISTS recommendation.rc_learning_loop_event CASCADE;
DROP TABLE IF EXISTS recommendation.rc_learning_loop_case CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_learning_loop_case (
loop_case_id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
wrong_question_id VARCHAR(64) NOT NULL,
review_plan_id VARCHAR(64),
profile_id VARCHAR(64),
recommendation_task_id VARCHAR(64),
recommendation_item_id VARCHAR(64),
feedback_id VARCHAR(64),
user_achievement_id VARCHAR(64),
loop_status VARCHAR(16) NOT NULL DEFAULT 'OPEN',
effect_score NUMERIC(10,4),
opened_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
closed_at TIMESTAMP,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_rc_learning_loop_case_status
CHECK (loop_status IN ('OPEN', 'IN_PROGRESS', 'CLOSED', 'FAILED')),
CONSTRAINT chk_rc_learning_loop_case_closed_at
CHECK (closed_at IS NULL OR closed_at >= opened_at),
CONSTRAINT fk_rc_learning_loop_case_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id),
CONSTRAINT fk_rc_learning_loop_case_wrong_question
FOREIGN KEY (wrong_question_id) REFERENCES question.gd_wrong_question(wrong_question_id),
CONSTRAINT fk_rc_learning_loop_case_review_plan
FOREIGN KEY (review_plan_id) REFERENCES question.gd_review_plan(review_plan_id),
CONSTRAINT fk_rc_learning_loop_case_profile
FOREIGN KEY (profile_id) REFERENCES recommendation.rc_student_profile_snapshot(profile_id),
CONSTRAINT fk_rc_learning_loop_case_recommendation_task
FOREIGN KEY (recommendation_task_id) REFERENCES recommendation.rc_recommendation_task(task_id),
CONSTRAINT fk_rc_learning_loop_case_recommendation_item
FOREIGN KEY (recommendation_item_id) REFERENCES recommendation.rc_recommendation_item(item_id),
CONSTRAINT fk_rc_learning_loop_case_feedback
FOREIGN KEY (feedback_id) REFERENCES recommendation.rc_recommendation_feedback(feedback_id),
CONSTRAINT fk_rc_learning_loop_case_user_achievement
FOREIGN KEY (user_achievement_id) REFERENCES achievement.ac_user_achievement(user_achievement_id)
);
COMMENT ON TABLE recommendation.rc_learning_loop_case IS '学习闭环主表(错题-复习-推荐-成就)';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.loop_case_id IS '闭环案例ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.user_id IS '用户ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.wrong_question_id IS '错题ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.review_plan_id IS '复习计划ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.profile_id IS '画像ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.recommendation_task_id IS '推荐任务ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.recommendation_item_id IS '推荐项ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.feedback_id IS '反馈事件ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.user_achievement_id IS '成就记录ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.loop_status IS '闭环状态';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.effect_score IS '闭环效果分';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.opened_at IS '闭环开启时间';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.closed_at IS '闭环关闭时间';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.created_at IS '创建时间';
COMMENT ON COLUMN recommendation.rc_learning_loop_case.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS recommendation.rc_learning_loop_event (
loop_event_id VARCHAR(64) PRIMARY KEY,
loop_case_id VARCHAR(64) NOT NULL,
stage_code VARCHAR(32) NOT NULL,
stage_status VARCHAR(16) NOT NULL DEFAULT 'DONE',
event_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
event_score NUMERIC(10,4),
event_detail_json JSONB NOT NULL DEFAULT '{}'::JSONB,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_rc_learning_loop_event_stage_code
CHECK (stage_code IN ('WRONG_CAPTURED', 'REVIEW_PLANNED', 'PROFILE_UPDATED', 'RECOMMENDED', 'EXPOSED', 'ENGAGED', 'REVIEW_COMPLETED', 'ACHIEVED')),
CONSTRAINT chk_rc_learning_loop_event_stage_status
CHECK (stage_status IN ('PENDING', 'DONE', 'FAILED', 'SKIPPED')),
CONSTRAINT chk_rc_learning_loop_event_detail_json
CHECK (jsonb_typeof(event_detail_json) = 'object'),
CONSTRAINT fk_rc_learning_loop_event_case
FOREIGN KEY (loop_case_id) REFERENCES recommendation.rc_learning_loop_case(loop_case_id) ON DELETE CASCADE
);
COMMENT ON TABLE recommendation.rc_learning_loop_event IS '学习闭环阶段事件表';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.loop_event_id IS '闭环事件ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.loop_case_id IS '闭环案例ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.stage_code IS '阶段编码';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.stage_status IS '阶段状态';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.event_time IS '事件时间';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.event_score IS '阶段得分';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.event_detail_json IS '事件详情JSON';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_event.created_at IS '创建时间';
DROP TABLE IF EXISTS recommendation.rc_learning_loop_effect_daily CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_learning_loop_effect_daily (
stat_date DATE NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
opened_case_count BIGINT NOT NULL DEFAULT 0,
closed_case_count BIGINT NOT NULL DEFAULT 0,
achieved_case_count BIGINT NOT NULL DEFAULT 0,
avg_effect_score NUMERIC(10,4) NOT NULL DEFAULT 0,
avg_close_hours NUMERIC(10,4) NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (stat_date, tenant_id)
);
COMMENT ON TABLE recommendation.rc_learning_loop_effect_daily IS '学习闭环效果日统计表';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.stat_date IS '统计日期';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.opened_case_count IS '开启案例数';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.closed_case_count IS '关闭案例数';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.achieved_case_count IS '达成成就案例数';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.avg_effect_score IS '平均效果分';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.avg_close_hours IS '平均闭环时长(小时)';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.created_at IS '创建时间';
COMMENT ON COLUMN recommendation.rc_learning_loop_effect_daily.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_rc_student_profile_snapshot_user_date
ON recommendation.rc_student_profile_snapshot(user_id, profile_date DESC);
CREATE INDEX IF NOT EXISTS idx_rc_student_profile_snapshot_tenant_version
ON recommendation.rc_student_profile_snapshot(tenant_id, profile_version, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_rc_student_profile_feature_profile_group
ON recommendation.rc_student_profile_feature(profile_id, feature_group, feature_key);
CREATE INDEX IF NOT EXISTS idx_rc_student_profile_feature_source
ON recommendation.rc_student_profile_feature(source_domain, source_ref_id);
CREATE INDEX IF NOT EXISTS idx_rc_learning_loop_case_user_status
ON recommendation.rc_learning_loop_case(user_id, loop_status, opened_at DESC);
CREATE INDEX IF NOT EXISTS idx_rc_learning_loop_case_wrong_question
ON recommendation.rc_learning_loop_case(wrong_question_id, opened_at DESC);
CREATE INDEX IF NOT EXISTS idx_rc_learning_loop_case_tenant_status
ON recommendation.rc_learning_loop_case(tenant_id, loop_status, opened_at DESC);
CREATE INDEX IF NOT EXISTS idx_rc_learning_loop_event_case_stage_time
ON recommendation.rc_learning_loop_event(loop_case_id, stage_code, event_time DESC);
CREATE INDEX IF NOT EXISTS idx_rc_learning_loop_event_tenant_time
ON recommendation.rc_learning_loop_event(tenant_id, event_time DESC);
CREATE INDEX IF NOT EXISTS idx_rc_learning_loop_effect_daily_tenant_date
ON recommendation.rc_learning_loop_effect_daily(tenant_id, stat_date DESC);
DROP TABLE IF EXISTS recommendation.rc_loop_metric_map CASCADE;
DROP TABLE IF EXISTS recommendation.rc_loop_stage_achievement_map CASCADE;
CREATE TABLE IF NOT EXISTS recommendation.rc_loop_stage_achievement_map (
stage_map_id VARCHAR(64) PRIMARY KEY,
stage_code VARCHAR(32) NOT NULL,
event_code VARCHAR(64) NOT NULL,
template_id VARCHAR(64),
metric_id VARCHAR(64),
award_source VARCHAR(32) NOT NULL DEFAULT 'RULE_ENGINE',
priority INTEGER NOT NULL DEFAULT 100,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
ext_json JSONB NOT NULL DEFAULT '{}'::JSONB,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_rc_loop_stage_achievement_map
UNIQUE (tenant_id, stage_code, event_code, priority),
CONSTRAINT chk_rc_loop_stage_achievement_map_stage_code
CHECK (stage_code IN ('WRONG_CAPTURED', 'REVIEW_PLANNED', 'PROFILE_UPDATED', 'RECOMMENDED', 'EXPOSED', 'ENGAGED', 'REVIEW_COMPLETED', 'ACHIEVED')),
CONSTRAINT chk_rc_loop_stage_achievement_map_award_source
CHECK (award_source IN ('RULE_ENGINE', 'MANUAL', 'SYSTEM')),
CONSTRAINT chk_rc_loop_stage_achievement_map_ext_json
CHECK (jsonb_typeof(ext_json) = 'object'),
CONSTRAINT fk_rc_loop_stage_achievement_map_event
FOREIGN KEY (event_code) REFERENCES achievement.ac_achievement_event_dict(event_code),
CONSTRAINT fk_rc_loop_stage_achievement_map_template
FOREIGN KEY (template_id) REFERENCES achievement.ac_achievement_rule_template(template_id),
CONSTRAINT fk_rc_loop_stage_achievement_map_metric
FOREIGN KEY (metric_id) REFERENCES achievement.ac_achievement_metric_def(metric_id)
);
COMMENT ON TABLE recommendation.rc_loop_stage_achievement_map IS '闭环阶段到成就事件映射表(通用配置)';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.stage_map_id IS '阶段映射ID';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.stage_code IS '闭环阶段编码';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.event_code IS '成就事件编码';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.template_id IS '成就规则模板ID';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.metric_id IS '成就指标ID';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.award_source IS '发放来源';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.priority IS '优先级';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.enabled IS '是否启用';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.ext_json IS '扩展配置JSON';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.created_at IS '创建时间';
COMMENT ON COLUMN recommendation.rc_loop_stage_achievement_map.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS recommendation.rc_loop_metric_map (
metric_map_id VARCHAR(64) PRIMARY KEY,
stage_code VARCHAR(32) NOT NULL,
metric_id VARCHAR(64) NOT NULL,
source_field VARCHAR(64) NOT NULL,
agg_method VARCHAR(16) NOT NULL DEFAULT 'LATEST',
weight NUMERIC(6,4) NOT NULL DEFAULT 1.0000,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
ext_json JSONB NOT NULL DEFAULT '{}'::JSONB,
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_rc_loop_metric_map
UNIQUE (tenant_id, stage_code, metric_id, source_field),
CONSTRAINT chk_rc_loop_metric_map_stage_code
CHECK (stage_code IN ('WRONG_CAPTURED', 'REVIEW_PLANNED', 'PROFILE_UPDATED', 'RECOMMENDED', 'EXPOSED', 'ENGAGED', 'REVIEW_COMPLETED', 'ACHIEVED')),
CONSTRAINT chk_rc_loop_metric_map_agg_method
CHECK (agg_method IN ('LATEST', 'SUM', 'AVG', 'MAX', 'MIN', 'COUNT')),
CONSTRAINT chk_rc_loop_metric_map_weight
CHECK (weight > 0 AND weight <= 1),
CONSTRAINT chk_rc_loop_metric_map_ext_json
CHECK (jsonb_typeof(ext_json) = 'object'),
CONSTRAINT fk_rc_loop_metric_map_metric
FOREIGN KEY (metric_id) REFERENCES achievement.ac_achievement_metric_def(metric_id)
);
COMMENT ON TABLE recommendation.rc_loop_metric_map IS '闭环指标映射表(通用配置)';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.metric_map_id IS '指标映射ID';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.stage_code IS '闭环阶段编码';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.metric_id IS '成就指标ID';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.source_field IS '来源字段名';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.agg_method IS '聚合方法';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.weight IS '权重';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.enabled IS '是否启用';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.ext_json IS '扩展配置JSON';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.tenant_id IS '租户ID';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.created_at IS '创建时间';
COMMENT ON COLUMN recommendation.rc_loop_metric_map.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_rc_loop_stage_achievement_map_tenant_stage
ON recommendation.rc_loop_stage_achievement_map(tenant_id, stage_code, enabled, priority);
CREATE INDEX IF NOT EXISTS idx_rc_loop_stage_achievement_map_event
ON recommendation.rc_loop_stage_achievement_map(event_code, tenant_id, enabled);
CREATE INDEX IF NOT EXISTS idx_rc_loop_metric_map_tenant_stage
ON recommendation.rc_loop_metric_map(tenant_id, stage_code, enabled);

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
-- 学校租户表
DROP TABLE IF EXISTS upms.tb_sys_tenant CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_tenant (
tenant_id VARCHAR(64) PRIMARY KEY,
parent_tenant_id VARCHAR(64),
@@ -19,6 +20,7 @@ COMMENT ON COLUMN upms.tb_sys_tenant.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_tenant.status IS '租户状态';
COMMENT ON COLUMN upms.tb_sys_tenant.created_at IS '创建时间';
DROP TABLE IF EXISTS upms.tb_sys_dept CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_dept (
dept_id VARCHAR(64) PRIMARY KEY,
parent_dept_id VARCHAR(64),
@@ -41,6 +43,10 @@ COMMENT ON COLUMN upms.tb_sys_dept.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_dept.dept_path IS '部门路径';
COMMENT ON COLUMN upms.tb_sys_dept.created_at IS '创建时间';
DROP TABLE IF EXISTS upms.tb_school_class_course_rel CASCADE;
DROP TABLE IF EXISTS upms.tb_school_class_member CASCADE;
DROP TABLE IF EXISTS upms.tb_school_class CASCADE;
DROP TABLE IF EXISTS upms.tb_sys_user CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_user (
user_id VARCHAR(64) PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
@@ -66,7 +72,132 @@ COMMENT ON COLUMN upms.tb_sys_user.dept_id IS '部门ID';
COMMENT ON COLUMN upms.tb_sys_user.dept_path IS '部门路径';
COMMENT ON COLUMN upms.tb_sys_user.status IS '用户状态';
COMMENT ON COLUMN upms.tb_sys_user.created_at IS '创建时间';
CREATE TABLE IF NOT EXISTS upms.tb_school_class (
class_id VARCHAR(64) PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
dept_id VARCHAR(64) NOT NULL,
class_code VARCHAR(64),
class_name VARCHAR(128) NOT NULL,
grade_code VARCHAR(32),
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
adcode VARCHAR(12),
tenant_path VARCHAR(255),
dept_path VARCHAR(255),
created_by VARCHAR(64),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_school_class_status
CHECK (status IN ('ACTIVE', 'INACTIVE', 'ARCHIVED')),
CONSTRAINT fk_school_class_dept
FOREIGN KEY (dept_id) REFERENCES upms.tb_sys_dept(dept_id),
CONSTRAINT fk_school_class_created_by
FOREIGN KEY (created_by) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE upms.tb_school_class IS '班级主表';
COMMENT ON COLUMN upms.tb_school_class.class_id IS '班级ID';
COMMENT ON COLUMN upms.tb_school_class.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_school_class.dept_id IS '所属部门ID';
COMMENT ON COLUMN upms.tb_school_class.class_code IS '班级编码';
COMMENT ON COLUMN upms.tb_school_class.class_name IS '班级名称';
COMMENT ON COLUMN upms.tb_school_class.grade_code IS '年级编码';
COMMENT ON COLUMN upms.tb_school_class.status IS '班级状态';
COMMENT ON COLUMN upms.tb_school_class.adcode IS '行政区划编码';
COMMENT ON COLUMN upms.tb_school_class.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_school_class.dept_path IS '部门路径';
COMMENT ON COLUMN upms.tb_school_class.created_by IS '创建人';
COMMENT ON COLUMN upms.tb_school_class.created_at IS '创建时间';
COMMENT ON COLUMN upms.tb_school_class.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS upms.tb_school_class_member (
class_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
member_role VARCHAR(16) NOT NULL DEFAULT 'STUDENT',
member_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
tenant_id VARCHAR(64) NOT NULL,
joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
left_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (class_id, user_id),
CONSTRAINT chk_school_class_member_role
CHECK (member_role IN ('STUDENT', 'HEAD_TEACHER', 'TEACHER', 'ASSISTANT')),
CONSTRAINT chk_school_class_member_status
CHECK (member_status IN ('ACTIVE', 'LEFT', 'SUSPENDED')),
CONSTRAINT chk_school_class_member_left_at
CHECK (left_at IS NULL OR left_at >= joined_at),
CONSTRAINT fk_school_class_member_class
FOREIGN KEY (class_id) REFERENCES upms.tb_school_class(class_id) ON DELETE CASCADE,
CONSTRAINT fk_school_class_member_user
FOREIGN KEY (user_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE upms.tb_school_class_member IS '班级成员表';
COMMENT ON COLUMN upms.tb_school_class_member.class_id IS '班级ID';
COMMENT ON COLUMN upms.tb_school_class_member.user_id IS '成员用户ID';
COMMENT ON COLUMN upms.tb_school_class_member.member_role IS '成员角色';
COMMENT ON COLUMN upms.tb_school_class_member.member_status IS '成员状态';
COMMENT ON COLUMN upms.tb_school_class_member.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_school_class_member.joined_at IS '加入时间';
COMMENT ON COLUMN upms.tb_school_class_member.left_at IS '离开时间';
COMMENT ON COLUMN upms.tb_school_class_member.created_at IS '创建时间';
COMMENT ON COLUMN upms.tb_school_class_member.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS upms.tb_school_class_course_rel (
class_id VARCHAR(64) NOT NULL,
course_id VARCHAR(64) NOT NULL,
relation_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
tenant_id VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (class_id, course_id),
CONSTRAINT chk_school_class_course_rel_status
CHECK (relation_status IN ('ACTIVE', 'INACTIVE')),
CONSTRAINT fk_school_class_course_rel_class
FOREIGN KEY (class_id) REFERENCES upms.tb_school_class(class_id) ON DELETE CASCADE
);
COMMENT ON TABLE upms.tb_school_class_course_rel IS '班级课程关联表';
COMMENT ON COLUMN upms.tb_school_class_course_rel.class_id IS '班级ID';
COMMENT ON COLUMN upms.tb_school_class_course_rel.course_id IS '课程ID';
COMMENT ON COLUMN upms.tb_school_class_course_rel.relation_status IS '关联状态';
COMMENT ON COLUMN upms.tb_school_class_course_rel.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_school_class_course_rel.created_at IS '创建时间';
COMMENT ON COLUMN upms.tb_school_class_course_rel.updated_at IS '更新时间';
DROP TABLE IF EXISTS upms.tb_sys_file CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_file (
file_id VARCHAR(64) PRIMARY KEY,
media_type VARCHAR(32) NOT NULL,
object_key VARCHAR(512) NOT NULL,
file_name VARCHAR(256),
mime_type VARCHAR(128),
file_size BIGINT,
file_hash VARCHAR(128),
duration_ms INTEGER,
uploaded_by VARCHAR(64),
adcode VARCHAR(12),
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_sys_file_media_type
CHECK (media_type IN ('IMAGE', 'AUDIO', 'VIDEO', 'DOCUMENT', 'OTHER')),
CONSTRAINT fk_sys_file_uploaded_by
FOREIGN KEY (uploaded_by) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE upms.tb_sys_file IS '系统文件资源表';
COMMENT ON COLUMN upms.tb_sys_file.file_id IS '文件ID';
COMMENT ON COLUMN upms.tb_sys_file.media_type IS '媒体类型';
COMMENT ON COLUMN upms.tb_sys_file.object_key IS '对象存储Key';
COMMENT ON COLUMN upms.tb_sys_file.file_name IS '文件名';
COMMENT ON COLUMN upms.tb_sys_file.mime_type IS 'MIME类型';
COMMENT ON COLUMN upms.tb_sys_file.file_size IS '文件大小(字节)';
COMMENT ON COLUMN upms.tb_sys_file.file_hash IS '文件哈希';
COMMENT ON COLUMN upms.tb_sys_file.duration_ms IS '音频/视频时长(毫秒)';
COMMENT ON COLUMN upms.tb_sys_file.uploaded_by IS '上传人ID';
COMMENT ON COLUMN upms.tb_sys_file.adcode IS '行政区划编码';
COMMENT ON COLUMN upms.tb_sys_file.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_sys_file.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_file.created_at IS '创建时间';
DROP TABLE IF EXISTS upms.tb_sys_role CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_role (
role_id VARCHAR(64) PRIMARY KEY,
role_code VARCHAR(64) UNIQUE NOT NULL,
@@ -89,28 +220,8 @@ COMMENT ON COLUMN upms.tb_sys_role.dept_id IS '部门ID';
COMMENT ON COLUMN upms.tb_sys_role.dept_path IS '部门路径';
COMMENT ON COLUMN upms.tb_sys_role.created_at IS '创建时间';
CREATE TABLE IF NOT EXISTS upms.tb_sys_permission (
permission_id VARCHAR(64) PRIMARY KEY,
permission_code VARCHAR(128) UNIQUE NOT NULL,
permission_name VARCHAR(128) NOT NULL,
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE upms.tb_sys_permission IS '权限表';
COMMENT ON COLUMN upms.tb_sys_permission.permission_id IS '权限ID';
COMMENT ON COLUMN upms.tb_sys_permission.permission_code IS '权限编码';
COMMENT ON COLUMN upms.tb_sys_permission.permission_name IS '权限名称';
COMMENT ON COLUMN upms.tb_sys_permission.adcode IS '行政区划编码';
COMMENT ON COLUMN upms.tb_sys_permission.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_sys_permission.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_permission.dept_id IS '部门ID';
COMMENT ON COLUMN upms.tb_sys_permission.dept_path IS '部门路径';
COMMENT ON COLUMN upms.tb_sys_permission.created_at IS '创建时间';
DROP TABLE IF EXISTS upms.tb_sys_menu CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_menu (
route_id VARCHAR(64) PRIMARY KEY,
parent_route_id VARCHAR(64),
@@ -146,8 +257,181 @@ COMMENT ON COLUMN upms.tb_sys_menu.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_menu.dept_id IS '部门ID';
COMMENT ON COLUMN upms.tb_sys_menu.dept_path IS '部门路径';
COMMENT ON COLUMN upms.tb_sys_menu.created_at IS '创建时间';
DROP TABLE IF EXISTS upms.tb_sys_role_menu CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_role_menu (
role_id VARCHAR(64) NOT NULL,
route_id VARCHAR(64) NOT NULL,
adcode VARCHAR(12) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255) NOT NULL,
dept_id VARCHAR(64),
dept_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (role_id, route_id)
);
COMMENT ON TABLE upms.tb_sys_role_menu IS '角色菜单授权表';
COMMENT ON COLUMN upms.tb_sys_role_menu.role_id IS '角色ID';
COMMENT ON COLUMN upms.tb_sys_role_menu.route_id IS '路由ID';
COMMENT ON COLUMN upms.tb_sys_role_menu.adcode IS '行政区划编码';
COMMENT ON COLUMN upms.tb_sys_role_menu.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_sys_role_menu.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_role_menu.dept_id IS '部门ID';
COMMENT ON COLUMN upms.tb_sys_role_menu.dept_path IS '部门路径';
COMMENT ON COLUMN upms.tb_sys_role_menu.created_at IS '创建时间';
DROP TABLE IF EXISTS upms.tb_sys_area CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_area (
id BIGINT PRIMARY KEY,
pid BIGINT NOT NULL,
name VARCHAR(128) NOT NULL,
letter VARCHAR(32),
adcode BIGINT NOT NULL,
location VARCHAR(255),
area_sort INTEGER,
area_status VARCHAR(8) NOT NULL DEFAULT '1',
area_type VARCHAR(8) NOT NULL DEFAULT '2',
hot VARCHAR(8) NOT NULL DEFAULT '0',
city_code VARCHAR(16),
create_by VARCHAR(64),
create_time TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
del_flag VARCHAR(8) NOT NULL DEFAULT '0'
);
COMMENT ON TABLE upms.tb_sys_area IS '行政区划表';
COMMENT ON COLUMN upms.tb_sys_area.id IS '主键ID';
COMMENT ON COLUMN upms.tb_sys_area.pid IS '父级行政区划编码';
COMMENT ON COLUMN upms.tb_sys_area.name IS '名称';
COMMENT ON COLUMN upms.tb_sys_area.letter IS '首字母';
COMMENT ON COLUMN upms.tb_sys_area.adcode IS '行政区划编码';
COMMENT ON COLUMN upms.tb_sys_area.location IS '经纬度';
COMMENT ON COLUMN upms.tb_sys_area.area_sort IS '排序';
COMMENT ON COLUMN upms.tb_sys_area.area_status IS '状态';
COMMENT ON COLUMN upms.tb_sys_area.area_type IS '层级类型';
COMMENT ON COLUMN upms.tb_sys_area.hot IS '是否热门';
COMMENT ON COLUMN upms.tb_sys_area.city_code IS '城市区号';
COMMENT ON COLUMN upms.tb_sys_area.create_by IS '创建人';
COMMENT ON COLUMN upms.tb_sys_area.create_time IS '创建时间';
COMMENT ON COLUMN upms.tb_sys_area.update_by IS '更新人';
COMMENT ON COLUMN upms.tb_sys_area.update_time IS '更新时间';
COMMENT ON COLUMN upms.tb_sys_area.del_flag IS '删除标记';
DROP TABLE IF EXISTS upms.tb_sys_message_recipient CASCADE;
DROP TABLE IF EXISTS upms.tb_sys_message CASCADE;
CREATE TABLE IF NOT EXISTS upms.tb_sys_message (
message_id VARCHAR(64) PRIMARY KEY,
message_type VARCHAR(32) NOT NULL DEFAULT 'INFO',
biz_type VARCHAR(64) NOT NULL,
title VARCHAR(256) NOT NULL,
content TEXT NOT NULL,
content_object_type VARCHAR(64) NOT NULL,
content_object_id VARCHAR(64) NOT NULL,
web_jump_url VARCHAR(512) NOT NULL,
send_channel VARCHAR(16) NOT NULL DEFAULT 'INBOX',
sender_user_id VARCHAR(64),
adcode VARCHAR(12),
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255),
message_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
send_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expire_at TIMESTAMP,
ext_json JSONB NOT NULL DEFAULT '{}'::JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_sys_message_type
CHECK (message_type IN ('TODO', 'INFO', 'ALERT', 'SYSTEM')),
CONSTRAINT chk_sys_message_channel
CHECK (send_channel IN ('INBOX')),
CONSTRAINT chk_sys_message_status
CHECK (message_status IN ('ACTIVE', 'CANCELLED', 'EXPIRED')),
CONSTRAINT chk_sys_message_url
CHECK (web_jump_url LIKE '/%'),
CONSTRAINT chk_sys_message_expire
CHECK (expire_at IS NULL OR expire_at >= send_at),
CONSTRAINT chk_sys_message_ext_json
CHECK (jsonb_typeof(ext_json) = 'object'),
CONSTRAINT fk_sys_message_sender
FOREIGN KEY (sender_user_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE upms.tb_sys_message IS '站内信消息主表';
COMMENT ON COLUMN upms.tb_sys_message.message_id IS '消息ID';
COMMENT ON COLUMN upms.tb_sys_message.message_type IS '消息类型TODO/INFO/ALERT/SYSTEM';
COMMENT ON COLUMN upms.tb_sys_message.biz_type IS '业务类型(如 SUBJECTIVE_REVIEW';
COMMENT ON COLUMN upms.tb_sys_message.title IS '消息标题';
COMMENT ON COLUMN upms.tb_sys_message.content IS '消息正文';
COMMENT ON COLUMN upms.tb_sys_message.content_object_type IS '内容对象类型(如 GRADING_TASK/TENANT';
COMMENT ON COLUMN upms.tb_sys_message.content_object_id IS '内容对象ID用于业务定位';
COMMENT ON COLUMN upms.tb_sys_message.web_jump_url IS 'Web跳转URL前端点击消息后跳转';
COMMENT ON COLUMN upms.tb_sys_message.send_channel IS '发送通道当前仅INBOX';
COMMENT ON COLUMN upms.tb_sys_message.sender_user_id IS '发送人ID系统消息可为空';
COMMENT ON COLUMN upms.tb_sys_message.adcode IS '行政区划编码';
COMMENT ON COLUMN upms.tb_sys_message.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_sys_message.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_message.message_status IS '消息状态ACTIVE/CANCELLED/EXPIRED';
COMMENT ON COLUMN upms.tb_sys_message.send_at IS '发送时间';
COMMENT ON COLUMN upms.tb_sys_message.expire_at IS '过期时间';
COMMENT ON COLUMN upms.tb_sys_message.ext_json IS '扩展字段JSON';
COMMENT ON COLUMN upms.tb_sys_message.created_at IS '创建时间';
COMMENT ON COLUMN upms.tb_sys_message.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS upms.tb_sys_message_recipient (
message_id VARCHAR(64) NOT NULL,
recipient_user_id VARCHAR(64) NOT NULL,
delivery_status VARCHAR(16) NOT NULL DEFAULT 'DELIVERED',
read_status VARCHAR(16) NOT NULL DEFAULT 'UNREAD',
read_at TIMESTAMP,
clicked_at TIMESTAMP,
read_source VARCHAR(16),
tenant_id VARCHAR(64) NOT NULL,
tenant_path VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (message_id, recipient_user_id),
CONSTRAINT chk_sys_message_recipient_delivery
CHECK (delivery_status IN ('DELIVERED', 'FAILED', 'RECALLED')),
CONSTRAINT chk_sys_message_recipient_read
CHECK (read_status IN ('UNREAD', 'READ', 'ARCHIVED')),
CONSTRAINT chk_sys_message_recipient_read_source
CHECK (read_source IS NULL OR read_source IN ('WEB', 'APP', 'MINI_PROGRAM')),
CONSTRAINT chk_sys_message_recipient_read_at
CHECK (
(read_status = 'UNREAD' AND read_at IS NULL)
OR (read_status IN ('READ', 'ARCHIVED') AND read_at IS NOT NULL)
),
CONSTRAINT chk_sys_message_recipient_click_at
CHECK (clicked_at IS NULL OR read_at IS NULL OR clicked_at >= read_at),
CONSTRAINT fk_sys_message_recipient_message
FOREIGN KEY (message_id) REFERENCES upms.tb_sys_message(message_id) ON DELETE CASCADE,
CONSTRAINT fk_sys_message_recipient_user
FOREIGN KEY (recipient_user_id) REFERENCES upms.tb_sys_user(user_id)
);
COMMENT ON TABLE upms.tb_sys_message_recipient IS '站内信收件人状态表';
COMMENT ON COLUMN upms.tb_sys_message_recipient.message_id IS '消息ID';
COMMENT ON COLUMN upms.tb_sys_message_recipient.recipient_user_id IS '收件人用户ID';
COMMENT ON COLUMN upms.tb_sys_message_recipient.delivery_status IS '投递状态DELIVERED/FAILED/RECALLED';
COMMENT ON COLUMN upms.tb_sys_message_recipient.read_status IS '读取状态UNREAD/READ/ARCHIVED';
COMMENT ON COLUMN upms.tb_sys_message_recipient.read_at IS '读取时间';
COMMENT ON COLUMN upms.tb_sys_message_recipient.clicked_at IS '点击跳转时间';
COMMENT ON COLUMN upms.tb_sys_message_recipient.read_source IS '读取来源WEB/APP/MINI_PROGRAM';
COMMENT ON COLUMN upms.tb_sys_message_recipient.tenant_id IS '租户ID';
COMMENT ON COLUMN upms.tb_sys_message_recipient.tenant_path IS '租户路径';
COMMENT ON COLUMN upms.tb_sys_message_recipient.created_at IS '创建时间';
COMMENT ON COLUMN upms.tb_sys_message_recipient.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_sys_tenant_adcode ON upms.tb_sys_tenant(adcode);
CREATE INDEX IF NOT EXISTS idx_dept_tenant ON upms.tb_sys_dept(tenant_id, dept_path);
CREATE INDEX IF NOT EXISTS idx_user_tenant ON upms.tb_sys_user(tenant_id, dept_id);
CREATE INDEX IF NOT EXISTS idx_school_class_tenant_dept ON upms.tb_school_class(tenant_id, dept_id, grade_code);
CREATE INDEX IF NOT EXISTS idx_school_class_member_tenant_user ON upms.tb_school_class_member(tenant_id, user_id, member_status);
CREATE INDEX IF NOT EXISTS idx_school_class_course_rel_tenant_course ON upms.tb_school_class_course_rel(tenant_id, course_id, relation_status);
CREATE INDEX IF NOT EXISTS idx_sys_file_tenant_media ON upms.tb_sys_file(tenant_id, media_type, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sys_file_uploaded_by ON upms.tb_sys_file(uploaded_by, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_route_tenant ON upms.tb_sys_menu(tenant_id, route_path);
CREATE INDEX IF NOT EXISTS idx_role_menu_tenant ON upms.tb_sys_role_menu(tenant_id, role_id);
CREATE INDEX IF NOT EXISTS idx_sys_area_pid ON upms.tb_sys_area(pid);
CREATE INDEX IF NOT EXISTS idx_sys_area_adcode ON upms.tb_sys_area(adcode);
CREATE INDEX IF NOT EXISTS idx_sys_message_tenant_biz ON upms.tb_sys_message(tenant_id, biz_type, send_at DESC);
CREATE INDEX IF NOT EXISTS idx_sys_message_object ON upms.tb_sys_message(content_object_type, content_object_id);
CREATE INDEX IF NOT EXISTS idx_sys_message_status_send_at ON upms.tb_sys_message(message_status, send_at DESC);
CREATE INDEX IF NOT EXISTS idx_sys_message_recipient_user_status ON upms.tb_sys_message_recipient(recipient_user_id, read_status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sys_message_recipient_tenant_user ON upms.tb_sys_message_recipient(tenant_id, recipient_user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sys_message_recipient_unread ON upms.tb_sys_message_recipient(recipient_user_id, created_at DESC) WHERE read_status = 'UNREAD';

View File

@@ -25,12 +25,6 @@ INSERT INTO upms.tb_sys_role (
('ROLE-ORG-ADMIN', 'ORG_ADMIN', '机构管理员', '330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/')
ON CONFLICT (role_id) DO NOTHING;
INSERT INTO upms.tb_sys_permission (
permission_id, permission_code, permission_name, adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES
('PERM-DASHBOARD', 'dashboard:view', '查看控制台', '330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'),
('PERM-TENANT', 'tenant:view', '查看租户组织', '330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/')
ON CONFLICT (permission_id) DO NOTHING;
INSERT INTO upms.tb_sys_menu (
route_id, parent_route_id, route_path, route_name, component_key, layout_type, title, icon, permission_code,
@@ -41,3 +35,84 @@ INSERT INTO upms.tb_sys_menu (
('ROUTE-TENANT', NULL, '/tenant', 'tenant-management', 'tenant', 'SIDEBAR', '租户组织', 'building-2', 'tenant:view', FALSE,
'330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/')
ON CONFLICT (route_id) DO NOTHING;
INSERT INTO upms.tb_sys_role_menu (
role_id, route_id, adcode, tenant_id, tenant_path, dept_id, dept_path
) VALUES
('ROLE-ORG-ADMIN', 'ROUTE-DASHBOARD', '330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/'),
('ROLE-ORG-ADMIN', 'ROUTE-TENANT', '330100', 'SCH-HQ', '/SCH-HQ/', 'DEPT-HQ-ADMIN', '/DEPT-HQ/DEPT-HQ-ADMIN/')
ON CONFLICT (role_id, route_id) DO NOTHING;
INSERT INTO upms.tb_sys_message (
message_id, message_type, biz_type, title, content, content_object_type, content_object_id, web_jump_url,
send_channel, sender_user_id, adcode, tenant_id, tenant_path, message_status, send_at, ext_json
) VALUES
(
'MSG-20260415001',
'TODO',
'SUBJECTIVE_REVIEW',
'有 1 份主观题待审阅',
'高一数学《函数综合训练》有主观题待你审阅,请尽快处理。',
'GRADING_TASK',
'GD-TASK-20260415001',
'/grading/review?gradingTaskId=GD-TASK-20260415001&messageId=MSG-20260415001',
'INBOX',
NULL,
'330100',
'SCH-HQ',
'/SCH-HQ/',
'ACTIVE',
CURRENT_TIMESTAMP,
'{"priority":"high","source":"grading"}'::JSONB
),
(
'MSG-20260415002',
'INFO',
'TENANT_NOTICE',
'杭州分校租户信息有更新',
'杭州分校基础资料已更新,可进入租户组织页查看详情。',
'TENANT',
'SCH-ZJ-HZ-01',
'/tenant?tenantId=SCH-ZJ-HZ-01&messageId=MSG-20260415002',
'INBOX',
NULL,
'330100',
'SCH-HQ',
'/SCH-HQ/',
'ACTIVE',
CURRENT_TIMESTAMP,
'{"priority":"normal","source":"upms"}'::JSONB
)
ON CONFLICT (message_id) DO NOTHING;
INSERT INTO upms.tb_sys_message_recipient (
message_id, recipient_user_id, delivery_status, read_status, read_at, clicked_at, read_source,
tenant_id, tenant_path, created_at, updated_at
) VALUES
(
'MSG-20260415001',
'U10001',
'DELIVERED',
'UNREAD',
NULL,
NULL,
NULL,
'SCH-HQ',
'/SCH-HQ/',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
'MSG-20260415002',
'U10001',
'DELIVERED',
'READ',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
'WEB',
'SCH-HQ',
'/SCH-HQ/',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT (message_id, recipient_user_id) DO NOTHING;