Compare commits
158 Commits
e27efb6282
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
313bae87bf | ||
|
|
e3d132dc1b | ||
|
|
dbb124bf17 | ||
|
|
415dde9122 | ||
|
|
072c0d52b9 | ||
|
|
fb619139e5 | ||
|
|
031e8a8175 | ||
|
|
7f26bb8f9c | ||
|
|
164dcd4c84 | ||
|
|
692e8ef580 | ||
|
|
747344bfe3 | ||
|
|
3f68f3eb09 | ||
|
|
205b463900 | ||
|
|
5d9981fd7d | ||
|
|
7387d70289 | ||
|
|
fc150e1e14 | ||
|
|
ebaf03107d | ||
|
|
783b8f5484 | ||
|
|
524ecade19 | ||
|
|
2adf61faa8 | ||
|
|
d7017f0e30 | ||
|
|
b090a815ae | ||
|
|
224740ad98 | ||
|
|
b333cda8c5 | ||
|
|
4e6d397d8e | ||
|
|
7c1b764b59 | ||
|
|
4122175915 | ||
|
|
811ad11f9f | ||
|
|
12a766d3ee | ||
|
|
0a034945d4 | ||
|
|
fefc65702d | ||
|
|
a75fdcd60d | ||
|
|
2236eb36e7 | ||
|
|
c44f19aefd | ||
|
|
dce2561751 | ||
|
|
a208fcc601 | ||
|
|
3bd3ebc12d | ||
|
|
c2575960eb | ||
|
|
94f098b0f2 | ||
|
|
6104e6a685 | ||
|
|
e76c626779 | ||
|
|
ee10380462 | ||
|
|
fa68ec0b34 | ||
|
|
025d4de50d | ||
|
|
19a05091d3 | ||
|
|
11ddc8ccbe | ||
|
|
80d98e13e4 | ||
|
|
ef2040edf4 | ||
|
|
1a34d1f14e | ||
|
|
9ce8d6d3fd | ||
|
|
22c2e6459d | ||
|
|
8fff820e1d | ||
|
|
7cfd85d0f5 | ||
|
|
05e68a3dad | ||
|
|
0f33484bd2 | ||
|
|
841b130f4c | ||
|
|
27374d1b2c | ||
|
|
6dee6b6299 | ||
|
|
cb5b92160c | ||
|
|
99f15ee37a | ||
|
|
a3dc22cb9e | ||
|
|
9eb02807a4 | ||
|
|
80dd67780c | ||
|
|
220d702b44 | ||
|
|
dd7a49504f | ||
|
|
0f9d8825e7 | ||
|
|
d6227d6e6c | ||
|
|
92913a728f | ||
|
|
e510f191c9 | ||
|
|
2d49f1bad8 | ||
|
|
6baa60b2a1 | ||
|
|
13409e55f1 | ||
|
|
1756ac21a8 | ||
|
|
0058b0b311 | ||
|
|
4174cd61d7 | ||
|
|
44f997bad7 | ||
|
|
fcf89dfb2c | ||
|
|
bc03275262 | ||
|
|
16ffd44161 | ||
|
|
be31da6223 | ||
|
|
8afd7af6dc | ||
|
|
9ec1be1528 | ||
|
|
214ebec976 | ||
|
|
d1ad7a1f75 | ||
|
|
e962b67050 | ||
|
|
0780524c08 | ||
|
|
5ca2a76e3a | ||
|
|
0e4eccec21 | ||
|
|
e762490f19 | ||
|
|
5d306dc686 | ||
|
|
124ed97060 | ||
|
|
e29f30d278 | ||
|
|
6dd3b1fecd | ||
|
|
85121c49a3 | ||
|
|
8ff14fce1c | ||
|
|
24b8ae78c6 | ||
|
|
43f30f4013 | ||
|
|
40cba54ed4 | ||
|
|
a4c9eb02b4 | ||
|
|
05f9b4fd9d | ||
|
|
c7cbc86fd4 | ||
|
|
6fa712ad15 | ||
|
|
4db77b7d1e | ||
|
|
cbed4ef508 | ||
|
|
8583e3e387 | ||
|
|
9b0b7ef619 | ||
|
|
66a60213bf | ||
|
|
1b898fb6cf | ||
|
|
d7b15093f9 | ||
|
|
5c4db2fedc | ||
|
|
ccb2cf05df | ||
|
|
4f00e566df | ||
|
|
ef1be6d04f | ||
|
|
0f4e58d7e1 | ||
|
|
a856ab005d | ||
|
|
7b550d3b69 | ||
|
|
f7ea76cd60 | ||
|
|
8a7d0f69ca | ||
|
|
217b5b48e4 | ||
|
|
8fc798fbcf | ||
|
|
ccf1618b1c | ||
|
|
a1c283e10d | ||
|
|
872d5fb085 | ||
|
|
cbc6e259fa | ||
|
|
4b6cb5f11e | ||
|
|
d2c250a1b3 | ||
|
|
3569d83b8e | ||
|
|
98429e87eb | ||
|
|
b63d116b6c | ||
|
|
eee0145274 | ||
|
|
6ea8ba763b | ||
|
|
25dca843be | ||
|
|
41fe8d8903 | ||
|
|
e7a01eadef | ||
|
|
284c7939b8 | ||
|
|
6a22922ddd | ||
|
|
93eacccdbf | ||
|
|
2dd8c2197c | ||
|
|
56c3f32e2d | ||
|
|
0e63c24ed9 | ||
|
|
21142f9b99 | ||
|
|
e9a5264fe7 | ||
|
|
f3c223d914 | ||
|
|
138b6f1b00 | ||
|
|
dfffb61e94 | ||
|
|
20023e13fd | ||
|
|
a16f09d3c3 | ||
|
|
e85a5f05dd | ||
|
|
2b95fd668b | ||
|
|
2859ef74f2 | ||
|
|
c935597803 | ||
|
|
048fc1c386 | ||
|
|
7567f317e1 | ||
|
|
ac1de9e433 | ||
|
|
cee7bbfd7a | ||
|
|
89b9e181d2 | ||
|
|
365c36d97b | ||
|
|
48829bd02b |
14
.env.example
@@ -13,6 +13,16 @@ AI_API_KEY=""
|
||||
AI_BASE_URL=""
|
||||
AI_MODEL=""
|
||||
|
||||
# ===== Redis / 缓存配置(可选) =====
|
||||
# 缓存驱动: memory(默认,单实例 LRU) | redis(分布式,多实例共享)
|
||||
CACHE_DRIVER=memory
|
||||
# 速率限制驱动: memory(默认,单实例) | redis(分布式,多实例共享)
|
||||
RATE_LIMIT_DRIVER=memory
|
||||
# Upstash Redis REST 凭据(仅 CACHE_DRIVER=redis 或 RATE_LIMIT_DRIVER=redis 时必填)
|
||||
# 获取方式: 注册 https://upstash.com → 创建数据库 → 复制 REST URL 和 TOKEN
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
|
||||
# ===== 灾备配置 =====
|
||||
# 异地备份后端类型: s3|oss|nfs|none
|
||||
BACKUP_OFFSITE_BACKEND=none
|
||||
@@ -65,3 +75,7 @@ BACKUP_DIR=./backups
|
||||
RETENTION_DAYS=30
|
||||
# 备份校验最小文件大小(字节,默认 1024)
|
||||
BACKUP_VERIFY_MIN_SIZE=1024
|
||||
|
||||
# ===== 日志配置 =====
|
||||
# 日志级别(debug/info/warn/error),默认 info
|
||||
LOG_LEVEL=info
|
||||
|
||||
@@ -67,6 +67,14 @@ jobs:
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Architecture scan
|
||||
run: |
|
||||
npm run arch:scan
|
||||
npm run arch:query -- violations || true
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: npx playwright install chromium
|
||||
|
||||
|
||||
71
.gitea/workflows/lighthouse.yml
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Lighthouse CI
|
||||
|
||||
# 性能预算回归门槛:每次 PR 与每日凌晨 3 点对关键路由采样断言。
|
||||
# 失败时阻断合并,触发审计报告 docs/architecture/audit/performance-budget-audit-report.md 中基线复核。
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: "0 3 * * *" # 每天凌晨 3 点性能采样
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lighthouse:
|
||||
runs-on: CDCD
|
||||
container: dockerreg.eazygame.cn/node-with-docker:22
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: "1"
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@v3
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Configure npm proxy
|
||||
run: |
|
||||
GATEWAY_IP=$(ip route show | grep default | awk '{print $3}')
|
||||
if [ -z "$GATEWAY_IP" ]; then
|
||||
GATEWAY_IP="172.17.0.1"
|
||||
fi
|
||||
PROXY_URL="http://$GATEWAY_IP:7890"
|
||||
npm config set proxy "$PROXY_URL"
|
||||
npm config set https-proxy "$PROXY_URL"
|
||||
echo "http_proxy=$PROXY_URL" >> $GITHUB_ENV
|
||||
echo "https_proxy=$PROXY_URL" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Start production server
|
||||
run: npm run start &
|
||||
env:
|
||||
PORT: "3000"
|
||||
|
||||
- name: Wait for server
|
||||
run: |
|
||||
for i in {1..30}; do
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 | grep -q "200\|307\|308" && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Install Lighthouse CI
|
||||
run: npm install -g @lhci/cli@0.13.x
|
||||
|
||||
- name: Run Lighthouse CI
|
||||
run: lhci autorun --config=./lighthouserc.json --collect.url=http://localhost:3000/login || true
|
||||
|
||||
- name: Assert performance budgets
|
||||
run: lhci assert --config=./lighthouserc.json
|
||||
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
||||
npx --no-install commitlint --edit $1
|
||||
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
@@ -0,0 +1,273 @@
|
||||
<h2>课文锚点时间线布局</h2>
|
||||
<p class="subtitle">课文作为主轴,节点锚定到课文位置,形成教学流程时间线</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">备课编辑器 — 课文锚点时间线</div>
|
||||
<div class="mockup-body" style="padding:0;">
|
||||
<div style="font-family:monospace;font-size:12px;line-height:1.6;">
|
||||
<!-- 顶部工具栏 -->
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:8px 12px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span>📖</span>
|
||||
<span style="font-weight:bold;">秋天(第一课时)</span>
|
||||
<span style="background:#334155;padding:2px 8px;border-radius:3px;font-size:10px;">语文 · 一年级上册 · 第一单元</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:10px;color:#94a3b8;">💾 已保存 · 2 分钟前</span>
|
||||
<span style="background:#334155;padding:4px 8px;border-radius:3px;font-size:10px;">📋 版本历史</span>
|
||||
<span style="background:#3b82f6;padding:4px 12px;border-radius:3px;font-size:10px;">💾 保存</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体:左课文 + 右节点时间线 -->
|
||||
<div style="display:grid;grid-template-columns:1fr 320px;gap:0;background:#fff;min-height:480px;">
|
||||
|
||||
<!-- 左侧:课文正文区(带锚点 gutter) -->
|
||||
<div style="display:grid;grid-template-columns:32px 1fr;background:#fffbeb;border-right:1px solid #e2e8f0;">
|
||||
|
||||
<!-- 锚点 gutter(显示锚点标记) -->
|
||||
<div style="background:#fef3c7;border-right:1px solid #fde68a;position:relative;">
|
||||
<!-- 锚点标记 -->
|
||||
<div style="position:absolute;top:60px;left:4px;background:#3b82f6;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">1</div>
|
||||
<div style="position:absolute;top:140px;left:4px;background:#f59e0b;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">2</div>
|
||||
<div style="position:absolute;top:200px;left:4px;background:#0ea5e9;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">3</div>
|
||||
<div style="position:absolute;top:280px;left:4px;background:#ec4899;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">4</div>
|
||||
<div style="position:absolute;top:360px;left:4px;background:#22c55e;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">5</div>
|
||||
</div>
|
||||
|
||||
<!-- 课文内容 -->
|
||||
<div style="padding:16px 20px;font-size:13px;color:#78350f;line-height:2;position:relative;">
|
||||
<div style="font-size:16px;font-weight:bold;color:#92400e;text-align:center;margin-bottom:16px;">秋天</div>
|
||||
|
||||
<p style="margin:0 0 12px 0;">
|
||||
<span style="background:#dbeafe;border-bottom:2px solid #3b82f6;padding:1px 2px;">天气凉了,树叶黄了,</span>
|
||||
一片片叶子从树上落下来。
|
||||
</p>
|
||||
<p style="margin:0 0 12px 0;">
|
||||
<span style="background:#fef3c7;border-bottom:2px solid #f59e0b;padding:1px 2px;">天空那么蓝,那么高。</span>
|
||||
一群大雁往南飞,
|
||||
</p>
|
||||
<p style="margin:0 0 12px 0;">
|
||||
<span style="background:#e0f2fe;border-bottom:2px solid #0ea5e9;padding:1px 2px;">一会儿排成个"人"字,</span>
|
||||
<span style="background:#fce7f3;border-bottom:2px solid #ec4899;padding:1px 2px;">一会儿排成个"一"字。</span>
|
||||
</p>
|
||||
<p style="margin:0 0 12px 0;">
|
||||
<span style="background:#dcfce7;border-bottom:2px solid #22c55e;padding:1px 2px;">啊!秋天来了!</span>
|
||||
</p>
|
||||
|
||||
<!-- 拖放提示 -->
|
||||
<div style="margin-top:24px;padding:8px;border:1px dashed #cbd5e1;border-radius:4px;text-align:center;font-size:10px;color:#94a3b8;">
|
||||
💡 选中文字可"关联节点",或从右侧拖动节点到课文某字前
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:节点时间线 -->
|
||||
<div style="background:#f8fafc;padding:12px;overflow-y:auto;">
|
||||
<div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">教学流程时间线</div>
|
||||
|
||||
<!-- 已锚定节点(按课文位置排序) -->
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
|
||||
<!-- 节点 1:导入(锚定到"天气凉了") -->
|
||||
<div style="background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:4px;padding:8px;cursor:pointer;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
||||
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">1</span>
|
||||
<span style="font-size:11px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#64748b;background:#eff6ff;padding:4px 6px;border-radius:3px;">
|
||||
"天气凉了,树叶黄了" → 提问:你见过秋天的树叶吗?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点 2:文本研习(锚定到"天空那么蓝") -->
|
||||
<div style="background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:4px;padding:8px;cursor:pointer;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
||||
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">2</span>
|
||||
<span style="font-size:11px;font-weight:bold;color:#92400e;">📝 文本研习</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#64748b;background:#fffbeb;padding:4px 6px;border-radius:3px;">
|
||||
"天空那么蓝,那么高" → 赏析:叠词的运用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点 3:新授(锚定到"人字") -->
|
||||
<div style="background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:4px;padding:8px;cursor:pointer;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
||||
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">3</span>
|
||||
<span style="font-size:11px;font-weight:bold;color:#075985;">📚 新授</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#64748b;background:#f0f9ff;padding:4px 6px;border-radius:3px;">
|
||||
"一会儿排成个'人'字" → 讲解:大雁南飞
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点 4:练习(锚定到"一字") -->
|
||||
<div style="background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:4px;padding:8px;cursor:pointer;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
||||
<span style="background:#ec4899;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">4</span>
|
||||
<span style="font-size:11px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#64748b;background:#fdf2f8;padding:4px 6px;border-radius:3px;">
|
||||
"一会儿排成个'一'字" → 3 道题
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点 5:小结(锚定到"秋天来了") -->
|
||||
<div style="background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:4px;padding:8px;cursor:pointer;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
||||
<span style="background:#22c55e;color:#fff;border-radius:50%;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:bold;">5</span>
|
||||
<span style="font-size:11px;font-weight:bold;color:#166534;">📌 小结</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">📍 锚定</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#64748b;background:#f0fdf4;padding:4px 6px;border-radius:3px;">
|
||||
"啊!秋天来了!" → 总结全文
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div style="border-top:1px dashed #cbd5e1;margin:8px 0;padding-top:8px;">
|
||||
<div style="font-size:9px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">未锚定节点</div>
|
||||
</div>
|
||||
|
||||
<!-- 未锚定节点 -->
|
||||
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#1e3a8a;">🎯 教学目标</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">全局</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#92400e;">⭐ 重难点</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">全局</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#a855f7;">🏠 作业</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">课后</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#6366f1;">📋 板书设计</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">全局</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px dashed #cbd5e1;border-radius:4px;padding:8px;cursor:pointer;opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#64748b;">💭 教学反思</span>
|
||||
<span style="margin-left:auto;font-size:9px;color:#94a3b8;">课后</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加节点按钮 -->
|
||||
<div style="border:1px dashed #94a3b8;border-radius:4px;padding:8px;text-align:center;font-size:10px;color:#64748b;cursor:pointer;margin-top:4px;">
|
||||
+ 添加节点
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top:24px;">
|
||||
<h3>核心交互:两种锚定方式</h3>
|
||||
<div class="split">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">方式 1:拖动节点到课文某字前</div>
|
||||
<div class="mockup-body" style="padding:16px;font-family:monospace;font-size:12px;">
|
||||
<div style="display:flex;gap:12px;">
|
||||
<div style="background:#f8fafc;padding:8px;border-radius:4px;">
|
||||
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">右侧节点</div>
|
||||
<div style="background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;padding:6px;border-radius:3px;cursor:grab;font-size:10px;">💡 导入</div>
|
||||
</div>
|
||||
<div style="font-size:18px;color:#94a3b8;align-self:center;">→</div>
|
||||
<div style="background:#fffbeb;padding:8px;border-radius:4px;flex:1;">
|
||||
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">课文</div>
|
||||
<div style="font-size:11px;color:#78350f;line-height:1.8;">
|
||||
天气凉了,<span style="background:#dbeafe;border:2px dashed #3b82f6;padding:1px 2px;border-radius:2px;">|</span>树叶黄了,<br>
|
||||
一片片叶子从树上落下来。
|
||||
</div>
|
||||
<div style="font-size:9px;color:#3b82f6;margin-top:4px;">💡 节点锚定到此位置</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">方式 2:选中文字 → 关联节点</div>
|
||||
<div class="mockup-body" style="padding:16px;font-family:monospace;font-size:12px;">
|
||||
<div style="background:#fffbeb;padding:8px;border-radius:4px;margin-bottom:8px;">
|
||||
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">1. 选中文字</div>
|
||||
<div style="font-size:11px;color:#78350f;line-height:1.8;">
|
||||
<span style="background:#fef08a;">天空那么蓝,那么高</span>。
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:18px;color:#94a3b8;text-align:center;">↓</div>
|
||||
<div style="background:#f8fafc;padding:8px;border-radius:4px;margin-top:8px;">
|
||||
<div style="font-size:9px;color:#94a3b8;margin-bottom:4px;">2. 弹出菜单选择节点</div>
|
||||
<div style="display:flex;gap:4px;flex-wrap:wrap;">
|
||||
<span style="background:#fff;border:1px solid #3b82f6;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">💡 导入</span>
|
||||
<span style="background:#fff;border:1px solid #f59e0b;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">📝 文本研习</span>
|
||||
<span style="background:#fff;border:1px solid #0ea5e9;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">📚 新授</span>
|
||||
<span style="background:#fff;border:1px solid #ec4899;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">✏️ 练习</span>
|
||||
<span style="background:#fff;border:1px solid #22c55e;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">📌 小结</span>
|
||||
<span style="background:#fff;border:1px dashed #94a3b8;padding:3px 6px;border-radius:3px;font-size:9px;cursor:pointer;">+ 新建节点</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top:24px;">
|
||||
<h3>数据模型:锚点(Anchor)</h3>
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
|
||||
<div style="color:#94a3b8;">// 节点锚点 — 记录节点与课文位置的关联</div>
|
||||
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">NodeAnchor</span> {</div>
|
||||
<div> nodeId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联的节点 ID</span></div>
|
||||
<div> type: <span style="color:#10b981;">"point"</span> | <span style="color:#10b981;">"range"</span>; <span style="color:#64748b;">// 点锚点 or 范围锚点</span></div>
|
||||
<div> start: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 课文纯文本偏移量(字符)</span></div>
|
||||
<div> end?: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// range 锚点的结束偏移</span></div>
|
||||
<div> textPreview?: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 锚定文字预览(便于回显)</span></div>
|
||||
<div>}</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// LessonPlanDocument 扩展</div>
|
||||
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">LessonPlanDocument</span> {</div>
|
||||
<div> version: <span style="color:#10b981;">3</span>; <span style="color:#64748b;">// 升级到 v3</span></div>
|
||||
<div> nodes: <span style="color:#3b82f6;">LessonPlanNode</span>[];</div>
|
||||
<div> edges: <span style="color:#3b82f6;">LessonPlanEdge</span>[]; <span style="color:#64748b;">// 保留:节点间连线</span></div>
|
||||
<div> anchors: <span style="color:#3b82f6;">NodeAnchor</span>[]; <span style="color:#64748b;">// 新增:节点与课文的锚点</span></div>
|
||||
<div>}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>这个设计的优势</h3>
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>优势</h4>
|
||||
<ul>
|
||||
<li><strong>教学流程可视化</strong>:节点按课文位置排序,天然形成时间线</li>
|
||||
<li><strong>节点与课文强关联</strong>:每个节点对应课文的哪部分一目了然</li>
|
||||
<li><strong>双模式锚定</strong>:拖动(点锚点)+ 选文字(范围锚点)</li>
|
||||
<li><strong>保留连线能力</strong>:节点间仍可连线(如"导入→新授"流程线)</li>
|
||||
<li><strong>未锚定节点</strong>:目标/重难点/作业/板书/反思等全局节点不强制锚定</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>需要注意</h4>
|
||||
<ul>
|
||||
<li>课文偏移量需基于纯文本(Markdown 渲染后需映射)</li>
|
||||
<li>课文内容变更后锚点可能失效(需重新定位或提示)</li>
|
||||
<li>数据结构升级到 v3,需迁移现有 v2 数据</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,292 @@
|
||||
<h2>画布式锚点布局 — 正文固定 + 节点散布 + 连线关联</h2>
|
||||
<p class="subtitle">保留 React Flow 画布交互,正文为不可移动但可缩放的中央容器,节点通过连线关联正文锚点</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">备课编辑器 — 画布视图(默认状态)</div>
|
||||
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
|
||||
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:560px;">
|
||||
|
||||
<!-- 顶部工具栏 -->
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:8px 12px;display:flex;justify-content:space-between;align-items:center;z-index:10;position:relative;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span>📖</span>
|
||||
<span style="font-weight:bold;">秋天(第一课时)</span>
|
||||
<span style="background:#334155;padding:2px 8px;border-radius:3px;font-size:10px;">语文 · 一年级上册</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:10px;color:#94a3b8;">💾 已保存</span>
|
||||
<span style="background:#334155;padding:4px 8px;border-radius:3px;font-size:10px;">📋 版本</span>
|
||||
<span style="background:#3b82f6;padding:4px 12px;border-radius:3px;font-size:10px;">💾 保存</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 画布区域 -->
|
||||
<div style="position:relative;width:100%;height:520px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
|
||||
|
||||
<!-- SVG 连线层(默认 10% 透明度) -->
|
||||
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0.1;" viewBox="0 0 800 520">
|
||||
<!-- 节点1(导入) → 正文锚点1 -->
|
||||
<path d="M 130 120 Q 200 140 280 180" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<circle cx="280" cy="180" r="4" fill="#3b82f6"/>
|
||||
<!-- 节点2(文本研习) → 正文锚点2 -->
|
||||
<path d="M 130 220 Q 200 230 280 240" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<circle cx="280" cy="240" r="4" fill="#f59e0b"/>
|
||||
<!-- 节点3(新授) → 正文锚点3 -->
|
||||
<path d="M 670 120 Q 600 150 520 200" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<circle cx="520" cy="200" r="4" fill="#0ea5e9"/>
|
||||
<!-- 节点4(练习) → 正文锚点4 -->
|
||||
<path d="M 670 220 Q 600 240 520 260" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<circle cx="520" cy="260" r="4" fill="#ec4899"/>
|
||||
<!-- 节点5(小结) → 正文锚点5 -->
|
||||
<path d="M 670 340 Q 600 320 520 300" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<circle cx="520" cy="300" r="4" fill="#22c55e"/>
|
||||
<!-- 节点间连线(教学流程) -->
|
||||
<path d="M 130 140 L 130 200" stroke="#64748b" stroke-width="1.5" fill="none"/>
|
||||
<path d="M 670 140 L 670 200" stroke="#64748b" stroke-width="1.5" fill="none"/>
|
||||
<path d="M 670 240 L 670 320" stroke="#64748b" stroke-width="1.5" fill="none"/>
|
||||
</svg>
|
||||
|
||||
<!-- 中央:正文容器(不可移动,可缩放) -->
|
||||
<div style="position:absolute;left:280px;top:80px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #fde68a;padding-bottom:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#92400e;">📜 课文正文</span>
|
||||
<span style="font-size:9px;color:#94a3b8;background:#fef3c7;padding:1px 4px;border-radius:2px;">🔒 固定</span>
|
||||
</div>
|
||||
<div style="font-size:13px;color:#78350f;line-height:1.8;">
|
||||
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
|
||||
<p style="margin:0 0 4px 0;">
|
||||
<span style="background:#dbeafe;border-bottom:2px solid #3b82f6;">天气凉了</span>,树叶黄了,
|
||||
</p>
|
||||
<p style="margin:0 0 4px 0;">
|
||||
<span style="background:#fef3c7;border-bottom:2px solid #f59e0b;">天空那么蓝</span>,那么高。
|
||||
</p>
|
||||
<p style="margin:0 0 4px 0;">
|
||||
一群大雁往南飞,
|
||||
</p>
|
||||
<p style="margin:0 0 4px 0;">
|
||||
一会儿排成个<span style="background:#e0f2fe;border-bottom:2px solid #0ea5e9;">"人"字</span>,
|
||||
</p>
|
||||
<p style="margin:0 0 4px 0;">
|
||||
一会儿排成个<span style="background:#fce7f3;border-bottom:2px solid #ec4899;">"一"字</span>。
|
||||
</p>
|
||||
<p style="margin:0;">
|
||||
<span style="background:#dcfce7;border-bottom:2px solid #22c55e;">啊!秋天来了!</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- 缩放控件 -->
|
||||
<div style="position:absolute;bottom:-12px;right:-12px;background:#fff;border:1px solid #f59e0b;border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,0.1);">🔍</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧节点 -->
|
||||
<!-- 节点1:导入 -->
|
||||
<div style="position:absolute;left:30px;top:90px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
|
||||
<!-- React Flow Handle 标记 -->
|
||||
<div style="position:absolute;right:-4px;top:50%;width:8px;height:8px;background:#3b82f6;border-radius:50%;border:1px solid #fff;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 节点2:文本研习 -->
|
||||
<div style="position:absolute;left:30px;top:200px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
|
||||
<div style="position:absolute;right:-4px;top:50%;width:8px;height:8px;background:#f59e0b;border-radius:50%;border:1px solid #fff;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧节点 -->
|
||||
<!-- 节点3:新授 -->
|
||||
<div style="position:absolute;left:630px;top:90px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">讲解:大雁南飞</div>
|
||||
<div style="position:absolute;left:-4px;top:50%;width:8px;height:8px;background:#0ea5e9;border-radius:50%;border:1px solid #fff;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 节点4:练习 -->
|
||||
<div style="position:absolute;left:630px;top:200px;width:140px;background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#ec4899;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">4</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">3 道题</div>
|
||||
<div style="position:absolute;left:-4px;top:50%;width:8px;height:8px;background:#ec4899;border-radius:50%;border:1px solid #fff;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 节点5:小结 -->
|
||||
<div style="position:absolute;left:630px;top:320px;width:140px;background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:move;">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">5</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#166534;">📌 小结</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">总结全文</div>
|
||||
<div style="position:absolute;left:-4px;top:50%;width:8px;height:8px;background:#22c55e;border-radius:50%;border:1px solid #fff;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 顶部全局节点(未锚定) -->
|
||||
<div style="position:absolute;left:30px;top:10px;width:100px;background:#fff;border:1px dashed #3b82f6;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
|
||||
<div style="font-size:9px;font-weight:bold;color:#1e3a8a;">🎯 教学目标</div>
|
||||
</div>
|
||||
<div style="position:absolute;left:140px;top:10px;width:100px;background:#fff;border:1px dashed #f59e0b;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
|
||||
<div style="font-size:9px;font-weight:bold;color:#92400e;">⭐ 重难点</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部全局节点(未锚定) -->
|
||||
<div style="position:absolute;left:30px;top:440px;width:100px;background:#fff;border:1px dashed #a855f7;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
|
||||
<div style="font-size:9px;font-weight:bold;color:#9333ea;">🏠 作业</div>
|
||||
</div>
|
||||
<div style="position:absolute;left:140px;top:440px;width:100px;background:#fff;border:1px dashed #6366f1;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
|
||||
<div style="font-size:9px;font-weight:bold;color:#4f46e5;">📋 板书设计</div>
|
||||
</div>
|
||||
<div style="position:absolute;left:250px;top:440px;width:100px;background:#fff;border:1px dashed #64748b;border-radius:6px;padding:6px;box-shadow:0 2px 4px rgba(0,0,0,0.05);opacity:0.8;">
|
||||
<div style="font-size:9px;font-weight:bold;color:#475569;">💭 教学反思</div>
|
||||
</div>
|
||||
|
||||
<!-- React Flow Controls(右下角) -->
|
||||
<div style="position:absolute;bottom:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:4px;display:flex;flex-direction:column;gap:2px;box-shadow:0 2px 4px rgba(0,0,0,0.05);">
|
||||
<div style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;border-radius:2px;">+</div>
|
||||
<div style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;border-radius:2px;">−</div>
|
||||
<div style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:10px;border-radius:2px;">⌖</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加节点按钮(左下角) -->
|
||||
<div style="position:absolute;bottom:12px;left:12px;background:#3b82f6;color:#fff;padding:6px 12px;border-radius:4px;font-size:10px;cursor:pointer;box-shadow:0 2px 4px rgba(59,130,246,0.3);">
|
||||
+ 添加节点
|
||||
</div>
|
||||
|
||||
<!-- 透明度提示 -->
|
||||
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:4px 8px;font-size:9px;color:#64748b;">
|
||||
连线默认 10% 透明度 · 选中节点时完整显示
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中状态对比 -->
|
||||
<div class="section" style="margin-top:24px;">
|
||||
<h3>选中节点时的连线显示对比</h3>
|
||||
<div class="split">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">默认状态 — 连线 10% 透明度</div>
|
||||
<div class="mockup-body" style="padding:16px;background:#f1f5f9;">
|
||||
<svg style="width:100%;height:120px;" viewBox="0 0 300 120">
|
||||
<path d="M 30 60 Q 120 60 270 60" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
<rect x="10" y="45" width="40" height="30" fill="#fff" stroke="#3b82f6" rx="4"/>
|
||||
<text x="30" y="64" text-anchor="middle" font-size="10" fill="#1e3a8a">导入</text>
|
||||
<rect x="250" y="45" width="40" height="30" fill="#fffbeb" stroke="#f59e0b" stroke-width="2" rx="4"/>
|
||||
<text x="270" y="64" text-anchor="middle" font-size="10" fill="#92400e">课文</text>
|
||||
</svg>
|
||||
<div style="text-align:center;font-size:10px;color:#94a3b8;">连线几乎不可见,画布干净</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">选中"导入"节点 — 连线 100% 显示</div>
|
||||
<div class="mockup-body" style="padding:16px;background:#f1f5f9;">
|
||||
<svg style="width:100%;height:120px;" viewBox="0 0 300 120">
|
||||
<path d="M 30 60 Q 120 60 270 60" stroke="#3b82f6" stroke-width="2.5" fill="none" stroke-dasharray="4 4" opacity="1"/>
|
||||
<circle cx="270" cy="60" r="5" fill="#3b82f6"/>
|
||||
<rect x="10" y="45" width="40" height="30" fill="#fff" stroke="#3b82f6" stroke-width="3" rx="4"/>
|
||||
<text x="30" y="64" text-anchor="middle" font-size="10" fill="#1e3a8a">导入</text>
|
||||
<rect x="250" y="45" width="40" height="30" fill="#fffbeb" stroke="#f59e0b" stroke-width="2" rx="4"/>
|
||||
<text x="270" y="64" text-anchor="middle" font-size="10" fill="#92400e">课文</text>
|
||||
</svg>
|
||||
<div style="text-align:center;font-size:10px;color:#3b82f6;">连线完整显示,高亮锚点位置</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据模型 -->
|
||||
<div class="section">
|
||||
<h3>数据模型设计</h3>
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
|
||||
<div style="color:#94a3b8;">// 正文容器节点(特殊节点类型,不可拖动,可缩放)</div>
|
||||
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">TextbookContentNode</span> <span style="color:#f59e0b;">extends</span> <span style="color:#3b82f6;">LessonPlanNode</span> {</div>
|
||||
<div> type: <span style="color:#10b981;">"textbook_content"</span>; <span style="color:#64748b;">// 新增节点类型</span></div>
|
||||
<div> data: {</div>
|
||||
<div> chapterId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联教材章节</span></div>
|
||||
<div> content: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// Markdown 正文(缓存)</span></div>
|
||||
<div> zoom: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 缩放比例 0.5-2.0</span></div>
|
||||
<div> };</div>
|
||||
<div> position: { x: <span style="color:#10b981;">number</span>; y: <span style="color:#10b981;">number</span> }; <span style="color:#64748b;">// 固定位置(不可拖动)</span></div>
|
||||
<div> draggable: <span style="color:#10b981;">false</span>; <span style="color:#64748b;">// React Flow 节点锁定</span></div>
|
||||
<div>}</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// 锚点连线(节点 → 正文位置)</div>
|
||||
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">AnchorEdge</span> <span style="color:#f59e0b;">extends</span> <span style="color:#3b82f6;">LessonPlanEdge</span> {</div>
|
||||
<div> type: <span style="color:#10b981;">"anchor"</span>; <span style="color:#64748b;">// 锚点连线(vs "flow" 流程连线)</span></div>
|
||||
<div> source: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 节点 ID</span></div>
|
||||
<div> target: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 正文节点 ID</span></div>
|
||||
<div> targetHandle: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// "anchor:123:145"(正文偏移量 start:end)</span></div>
|
||||
<div>}</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// LessonPlanDocument v3</div>
|
||||
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">LessonPlanDocument</span> {</div>
|
||||
<div> version: <span style="color:#10b981;">3</span>;</div>
|
||||
<div> nodes: <span style="color:#3b82f6;">LessonPlanNode</span>[]; <span style="color:#64748b;">// 含 1 个 textbook_content + N 个教学节点</span></div>
|
||||
<div> edges: <span style="color:#3b82f6;">AnchorEdge</span> | <span style="color:#3b82f6;">FlowEdge</span>[]; <span style="color:#64748b;">// 锚点连线 + 流程连线</span></div>
|
||||
<div>}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交互流程 -->
|
||||
<div class="section">
|
||||
<h3>核心交互流程</h3>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
|
||||
<h4 style="margin:0 0 8px 0;color:#3b82f6;">🔗 锚定节点到正文</h4>
|
||||
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
|
||||
<li>教师选中正文某段文字(或某个字)</li>
|
||||
<li>选中后弹出浮动菜单:"关联节点 →"</li>
|
||||
<li>从下拉列表选择已有节点,或"新建节点"</li>
|
||||
<li>创建 AnchorEdge,source=节点, target=正文节点, targetHandle="anchor:start:end"</li>
|
||||
<li>正文对应文字高亮显示(节点颜色)</li>
|
||||
<li>连线默认 10% 透明,选中节点时 100%</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
|
||||
<h4 style="margin:0 0 8px 0;color:#22c55e;">🖱️ 拖动节点到正文</h4>
|
||||
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
|
||||
<li>教师从右侧节点列表拖动一个节点</li>
|
||||
<li>拖动过程中,正文区域高亮可放置区域</li>
|
||||
<li>拖到正文某个字前释放</li>
|
||||
<li>创建点锚点(point anchor),targetHandle="anchor:pos"</li>
|
||||
<li>节点自动定位到正文旁边(左或右空位)</li>
|
||||
<li>连线默认 10% 透明,选中时完整显示</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>设计要点</h3>
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>优势</h4>
|
||||
<ul>
|
||||
<li><strong>保留画布交互</strong>:缩放/平移/拖动节点,与当前备课模块一致</li>
|
||||
<li><strong>正文固定居中</strong>:不可拖动,始终是视觉中心</li>
|
||||
<li><strong>连线语义化</strong>:anchor 锚点连线 vs flow 流程连线</li>
|
||||
<li><strong>透明度策略</strong>:默认 10%,选中时 100%,画布不杂乱</li>
|
||||
<li><strong>正文可缩放</strong>:教师可放大正文便于阅读</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>技术挑战</h4>
|
||||
<ul>
|
||||
<li>正文偏移量需基于纯文本(Markdown 渲染后映射)</li>
|
||||
<li>正文内容变更后锚点需重新定位</li>
|
||||
<li>React Flow 自定义节点需处理正文渲染</li>
|
||||
<li>数据结构升级 v2 → v3,需迁移</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,226 @@
|
||||
<h2>备课模块布局方案对比</h2>
|
||||
<p class="subtitle">3 种布局方案 — 课文固定中央,教学节点围绕组织</p>
|
||||
|
||||
<div class="cards" data-multiselect>
|
||||
<!-- 方案 A -->
|
||||
<div class="card" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding:12px;background:#f8fafc;">
|
||||
<div style="font-family:monospace;font-size:11px;line-height:1.4;">
|
||||
<div style="border:1px solid #cbd5e1;background:#e2e8f0;padding:4px 8px;border-radius:4px 4px 0 0;display:flex;justify-content:space-between;">
|
||||
<span>📖 秋天(第一课时)</span>
|
||||
<span>💾 已保存</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:200px 1fr 200px;gap:4px;padding:8px;background:#fff;border:1px solid #cbd5e1;border-top:none;border-radius:0 0 4px 4px;min-height:280px;">
|
||||
<!-- 左侧:课前 -->
|
||||
<div style="display:flex;flex-direction:column;gap:4px;">
|
||||
<div style="font-size:9px;color:#64748b;text-align:center;">课前</div>
|
||||
<div style="background:#dbeafe;border:1px solid #3b82f6;padding:4px;border-radius:3px;font-size:10px;">🎯 教学目标</div>
|
||||
<div style="background:#fef3c7;border:1px solid #f59e0b;padding:4px;border-radius:3px;font-size:10px;">⭐ 重难点</div>
|
||||
<div style="background:#e0f2fe;border:1px solid #0ea5e9;padding:4px;border-radius:3px;font-size:10px;">💡 导入</div>
|
||||
<div style="background:#fce7f3;border:1px solid #ec4899;padding:4px;border-radius:3px;font-size:10px;">📝 文本研习</div>
|
||||
</div>
|
||||
<!-- 中央:课文 -->
|
||||
<div style="background:#fffbeb;border:2px solid #f59e0b;padding:8px;border-radius:4px;display:flex;flex-direction:column;">
|
||||
<div style="font-size:10px;color:#92400e;font-weight:bold;margin-bottom:4px;">📜 课文正文</div>
|
||||
<div style="font-size:9px;color:#78350f;line-height:1.5;flex:1;">
|
||||
天气凉了,树叶黄了,<br>
|
||||
一片片叶子从树上落下来。<br>
|
||||
<span style="background:#fef08a;">天空那么蓝,那么高</span>。<br>
|
||||
一群大雁往南飞,<br>
|
||||
一会儿排成个"人"字,<br>
|
||||
一会儿排成个"一"字。<br>
|
||||
<span style="background:#bbf7d0;">啊!秋天来了!</span>
|
||||
</div>
|
||||
<div style="font-size:8px;color:#92400e;margin-top:4px;">💡 选中文字可添加批注</div>
|
||||
</div>
|
||||
<!-- 右侧:课中/课后 -->
|
||||
<div style="display:flex;flex-direction:column;gap:4px;">
|
||||
<div style="font-size:9px;color:#64748b;text-align:center;">课中</div>
|
||||
<div style="background:#dcfce7;border:1px solid #22c55e;padding:4px;border-radius:3px;font-size:10px;">📚 新授</div>
|
||||
<div style="background:#ede9fe;border:1px solid #8b5cf6;padding:4px;border-radius:3px;font-size:10px;">✏️ 练习</div>
|
||||
<div style="background:#fee2e2;border:1px solid #ef4444;padding:4px;border-radius:3px;font-size:10px;">📌 小结</div>
|
||||
<div style="font-size:9px;color:#64748b;text-align:center;margin-top:4px;">课后</div>
|
||||
<div style="background:#f3e8ff;border:1px solid #a855f7;padding:4px;border-radius:3px;font-size:10px;">🏠 作业</div>
|
||||
<div style="background:#e0e7ff;border:1px solid #6366f1;padding:4px;border-radius:3px;font-size:10px;">📋 板书设计</div>
|
||||
<div style="background:#f1f5f9;border:1px solid #64748b;padding:4px;border-radius:3px;font-size:10px;">💭 教学反思</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>A. 三栏布局(课前/课文/课后)</h3>
|
||||
<p>课文固定中央(琥珀色边框),左侧"课前"节点(目标/重难点/导入/文本研习),右侧"课中+课后"节点(新授/练习/小结/作业/板书/反思)。节点按教学流程纵向排列。点击节点在右侧抽屉编辑。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 方案 B -->
|
||||
<div class="card" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding:12px;background:#f8fafc;">
|
||||
<div style="font-family:monospace;font-size:11px;line-height:1.4;">
|
||||
<div style="border:1px solid #cbd5e1;background:#e2e8f0;padding:4px 8px;border-radius:4px 4px 0 0;display:flex;justify-content:space-between;">
|
||||
<span>📖 秋天(第一课时)</span>
|
||||
<span>💾 已保存</span>
|
||||
</div>
|
||||
<div style="padding:8px;background:#fff;border:1px solid #cbd5e1;border-top:none;border-radius:0 0 4px 4px;min-height:280px;">
|
||||
<!-- 顶部:目标/重难点 -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:4px;">
|
||||
<div style="background:#dbeafe;border:1px solid #3b82f6;padding:4px;border-radius:3px;font-size:10px;text-align:center;">🎯 教学目标</div>
|
||||
<div style="background:#fef3c7;border:1px solid #f59e0b;padding:4px;border-radius:3px;font-size:10px;text-align:center;">⭐ 重难点</div>
|
||||
</div>
|
||||
<!-- 中央:课文 + 左右两侧节点 -->
|
||||
<div style="display:grid;grid-template-columns:120px 1fr 120px;gap:4px;margin-bottom:4px;">
|
||||
<div style="display:flex;flex-direction:column;gap:4px;">
|
||||
<div style="background:#e0f2fe;border:1px solid #0ea5e9;padding:4px;border-radius:3px;font-size:9px;text-align:center;">💡 导入</div>
|
||||
<div style="background:#fce7f3;border:1px solid #ec4899;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📝 文本研习</div>
|
||||
</div>
|
||||
<div style="background:#fffbeb;border:2px solid #f59e0b;padding:8px;border-radius:4px;text-align:center;">
|
||||
<div style="font-size:10px;color:#92400e;font-weight:bold;">📜 课文正文</div>
|
||||
<div style="font-size:9px;color:#78350f;margin-top:4px;">天气凉了,树叶黄了...<br>天空那么蓝...<br>一群大雁往南飞...</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;">
|
||||
<div style="background:#dcfce7;border:1px solid #22c55e;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📚 新授</div>
|
||||
<div style="background:#ede9fe;border:1px solid #8b5cf6;padding:4px;border-radius:3px;font-size:9px;text-align:center;">✏️ 练习</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部:小结/作业/板书/反思 -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px;">
|
||||
<div style="background:#fee2e2;border:1px solid #ef4444;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📌 小结</div>
|
||||
<div style="background:#f3e8ff;border:1px solid #a855f7;padding:4px;border-radius:3px;font-size:9px;text-align:center;">🏠 作业</div>
|
||||
<div style="background:#e0e7ff;border:1px solid #6366f1;padding:4px;border-radius:3px;font-size:9px;text-align:center;">📋 板书</div>
|
||||
<div style="background:#f1f5f9;border:1px solid #64748b;padding:4px;border-radius:3px;font-size:9px;text-align:center;">💭 反思</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>B. 上下分区布局</h3>
|
||||
<p>顶部目标/重难点横排,中央课文 + 左右导入/文本研习/新授/练习,底部小结/作业/板书/反思横排。按"目标→导入→课文→新授→小结"的阅读顺序自然流动。视觉层次更清晰。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 方案 C -->
|
||||
<div class="card" data-choice="c" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding:12px;background:#f8fafc;">
|
||||
<div style="font-family:monospace;font-size:11px;line-height:1.4;">
|
||||
<div style="border:1px solid #cbd5e1;background:#e2e8f0;padding:4px 8px;border-radius:4px 4px 0 0;display:flex;justify-content:space-between;">
|
||||
<span>📖 秋天(第一课时)</span>
|
||||
<span>💾 已保存</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;padding:8px;background:#fff;border:1px solid #cbd5e1;border-top:none;border-radius:0 0 4px 4px;min-height:280px;">
|
||||
<!-- 左侧:课文 -->
|
||||
<div style="display:flex;flex-direction:column;gap:4px;">
|
||||
<div style="background:#fffbeb;border:2px solid #f59e0b;padding:8px;border-radius:4px;flex:1;">
|
||||
<div style="font-size:10px;color:#92400e;font-weight:bold;">📜 课文正文</div>
|
||||
<div style="font-size:9px;color:#78350f;margin-top:4px;line-height:1.5;">
|
||||
天气凉了,树叶黄了,<br>
|
||||
一片片叶子从树上落下来。<br>
|
||||
<span style="background:#fef08a;">天空那么蓝,那么高</span>。<br>
|
||||
一群大雁往南飞...
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#fce7f3;border:1px solid #ec4899;padding:4px;border-radius:3px;font-size:10px;text-align:center;">📝 文本研习(批注)</div>
|
||||
</div>
|
||||
<!-- 右侧:教学流程时间线 -->
|
||||
<div style="display:flex;flex-direction:column;gap:3px;">
|
||||
<div style="font-size:9px;color:#64748b;text-align:center;">教学流程</div>
|
||||
<div style="background:#dbeafe;border:1px solid #3b82f6;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">1</span>
|
||||
🎯 教学目标
|
||||
</div>
|
||||
<div style="background:#fef3c7;border:1px solid #f59e0b;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">2</span>
|
||||
⭐ 重难点
|
||||
</div>
|
||||
<div style="background:#e0f2fe;border:1px solid #0ea5e9;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">3</span>
|
||||
💡 导入
|
||||
</div>
|
||||
<div style="background:#dcfce7;border:1px solid #22c55e;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">4</span>
|
||||
📚 新授
|
||||
</div>
|
||||
<div style="background:#ede9fe;border:1px solid #8b5cf6;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#8b5cf6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">5</span>
|
||||
✏️ 练习
|
||||
</div>
|
||||
<div style="background:#fee2e2;border:1px solid #ef4444;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#ef4444;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">6</span>
|
||||
📌 小结
|
||||
</div>
|
||||
<div style="background:#f3e8ff;border:1px solid #a855f7;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#a855f7;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">7</span>
|
||||
🏠 作业
|
||||
</div>
|
||||
<div style="background:#e0e7ff;border:1px solid #6366f1;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#6366f1;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">8</span>
|
||||
📋 板书
|
||||
</div>
|
||||
<div style="background:#f1f5f9;border:1px solid #64748b;padding:3px 6px;border-radius:3px;font-size:9px;display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#64748b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;">9</span>
|
||||
💭 反思
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>C. 左课文 + 右时间线</h3>
|
||||
<p>左侧课文正文(固定)+ 文本研习批注,右侧教学流程时间线(编号 1-9 按顺序)。点击时间线节点展开编辑抽屉。最贴近传统教案本格式,结构清晰。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top:24px;">
|
||||
<h3>三种方案的核心差异</h3>
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>方案 A 三栏</h4>
|
||||
<ul>
|
||||
<li>课文始终居中可见</li>
|
||||
<li>课前/课后分区直观</li>
|
||||
<li>节点可拖动微调位置</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>方案 A 三栏</h4>
|
||||
<ul>
|
||||
<li>三栏可能拥挤(小屏)</li>
|
||||
<li>教学流程顺序不够明显</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>方案 B 上下分区</h4>
|
||||
<ul>
|
||||
<li>视觉层次最清晰</li>
|
||||
<li>阅读顺序自然(上→下)</li>
|
||||
<li>课文居中突出</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>方案 B 上下分区</h4>
|
||||
<ul>
|
||||
<li>节点位置较固定</li>
|
||||
<li>纵向空间需求大</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>方案 C 左课文+右时间线</h4>
|
||||
<ul>
|
||||
<li>最接近传统教案</li>
|
||||
<li>教学流程顺序最明确</li>
|
||||
<li>课文阅读体验最佳</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>方案 C 左课文+右时间线</h4>
|
||||
<ul>
|
||||
<li>节点画布感弱(更像列表)</li>
|
||||
<li>失去节点图连线能力</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,251 @@
|
||||
<h2>正文占位符标记布局</h2>
|
||||
<p class="subtitle">正文中嵌入占位符标记(特殊符号),默认接近透明,选中节点时完整显示</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">备课编辑器 — 默认状态(占位符 10% 透明度)</div>
|
||||
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
|
||||
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:560px;">
|
||||
|
||||
<!-- 顶部工具栏 -->
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:8px 12px;display:flex;justify-content:space-between;align-items:center;z-index:10;position:relative;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span>📖</span>
|
||||
<span style="font-weight:bold;">秋天(第一课时)</span>
|
||||
<span style="background:#334155;padding:2px 8px;border-radius:3px;font-size:10px;">语文 · 一年级上册</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:10px;color:#94a3b8;">💾 已保存</span>
|
||||
<span style="background:#3b82f6;padding:4px 12px;border-radius:3px;font-size:10px;">💾 保存</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 画布区域 -->
|
||||
<div style="position:relative;width:100%;height:520px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
|
||||
|
||||
<!-- SVG 连线层(默认 10% 透明度) -->
|
||||
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0.1;" viewBox="0 0 800 520">
|
||||
<path d="M 130 120 Q 200 140 290 175" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 130 220 Q 200 230 290 215" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 670 120 Q 600 150 510 195" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 670 220 Q 600 240 510 235" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 670 340 Q 600 320 510 275" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
</svg>
|
||||
|
||||
<!-- 中央:正文容器(不可移动) -->
|
||||
<div style="position:absolute;left:280px;top:60px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #fde68a;padding-bottom:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#92400e;">📜 课文正文</span>
|
||||
<span style="font-size:9px;color:#94a3b8;background:#fef3c7;padding:1px 4px;border-radius:2px;">🔒 固定</span>
|
||||
</div>
|
||||
<div style="font-size:13px;color:#78350f;line-height:2;">
|
||||
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 占位符 1(导入节点)- 默认 10% 透明度 -->
|
||||
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-right:2px;">①</span>天气凉了,树叶黄了,
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 占位符 2(文本研习)- 默认 10% 透明度 -->
|
||||
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-right:2px;">②</span>天空那么蓝,那么高。
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
一群大雁往南飞,
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
一会儿排成个"人"字<span style="display:inline-block;background:#0ea5e9;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-left:2px;">③</span>,
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
一会儿排成个"一"字<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-left:2px;">④</span>。
|
||||
</p>
|
||||
<p style="margin:0;">
|
||||
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.1;margin-right:2px;">⑤</span>啊!秋天来了!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧节点 -->
|
||||
<div style="position:absolute;left:30px;top:90px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:30px;top:200px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧节点 -->
|
||||
<div style="position:absolute;left:630px;top:90px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">讲解:大雁南飞</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:630px;top:200px;width:140px;background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#ec4899;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">4</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">3 道题</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:630px;top:320px;width:140px;background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">5</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#166534;">📌 小结</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">总结全文</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:4px 8px;font-size:9px;color:#64748b;">
|
||||
占位符默认 10% · 选中节点时 100% 显示
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中状态对比 -->
|
||||
<div class="section" style="margin-top:24px;">
|
||||
<h3>选中"导入"节点时的状态变化</h3>
|
||||
<div class="split">
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">默认 — 占位符 ① 10% 透明度</div>
|
||||
<div class="mockup-body" style="padding:16px;background:#fffbeb;">
|
||||
<div style="font-size:14px;color:#78350f;line-height:2;font-family:serif;">
|
||||
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 5px;font-size:10px;font-weight:bold;opacity:0.1;margin-right:3px;">①</span>天气凉了,树叶黄了,
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:10px;color:#94a3b8;text-align:center;">
|
||||
占位符几乎不可见 · 画布干净
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">选中"导入"节点 — 占位符 ① 100% + 连线显示</div>
|
||||
<div class="mockup-body" style="padding:16px;background:#fffbeb;">
|
||||
<div style="font-size:14px;color:#78350f;line-height:2;font-family:serif;">
|
||||
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 5px;font-size:10px;font-weight:bold;opacity:1;margin-right:3px;box-shadow:0 0 0 2px #3b82f633;">①</span>天气凉了,树叶黄了,
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:10px;color:#3b82f6;text-align:center;">
|
||||
占位符完整显示 · 连线高亮 · 锚定位置清晰
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 占位符样式选项 -->
|
||||
<div class="section">
|
||||
<h3>占位符样式选项</h3>
|
||||
<p class="subtitle">选择占位符在正文中的视觉呈现方式</p>
|
||||
<div class="cards" data-multiselect>
|
||||
<div class="card" data-choice="number" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
|
||||
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
|
||||
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:0 6px;font-size:11px;font-weight:bold;">①</span>天气凉了
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>数字圆圈</h3>
|
||||
<p>①②③④⑤ — 与节点编号对应,简洁清晰</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" data-choice="icon" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
|
||||
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
|
||||
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:3px;padding:1px 5px;font-size:11px;">💡</span>天气凉了
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>节点图标</h3>
|
||||
<p>💡📝📚✏️📌 — 与节点类型图标一致,直观</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" data-choice="dot" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
|
||||
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
|
||||
<span style="display:inline-block;background:#3b82f6;color:#fff;border-radius:50%;width:10px;height:10px;font-size:8px;text-align:center;line-height:10px;">●</span>天气凉了
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>彩色圆点</h3>
|
||||
<p>● — 极简,颜色对应节点,不干扰阅读</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" data-choice="bracket" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="padding:20px;background:#fffbeb;text-align:center;">
|
||||
<div style="font-size:16px;color:#78350f;font-family:serif;line-height:2;">
|
||||
<span style="color:#3b82f6;font-weight:bold;">【1】</span>天气凉了
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>方括号编号</h3>
|
||||
<p>【1】【2】【3】— 类似脚注标记,学术感</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据模型更新 -->
|
||||
<div class="section">
|
||||
<h3>占位符数据模型</h3>
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
|
||||
<div style="color:#94a3b8;">// 正文中的占位符标记</div>
|
||||
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">ContentPlaceholder</span> {</div>
|
||||
<div> id: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 占位符 ID</span></div>
|
||||
<div> nodeId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联的节点 ID</span></div>
|
||||
<div> offset: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 在正文纯文本中的字符偏移量</span></div>
|
||||
<div> label: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 显示的标记("①" / "💡" / "●" / "【1】")</span></div>
|
||||
<div> color: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 节点颜色(用于占位符背景)</span></div>
|
||||
<div>}</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// 正文渲染时注入占位符</div>
|
||||
<div><span style="color:#f59e0b;">function</span> <span style="color:#3b82f6;">renderContentWithPlaceholders</span>(</div>
|
||||
<div> content: <span style="color:#10b981;">string</span>, <span style="color:#64748b;">// Markdown 原文</span></div>
|
||||
<div> placeholders: <span style="color:#3b82f6;">ContentPlaceholder</span>[]</div>
|
||||
<div>): <span style="color:#10b981;">string</span> {</div>
|
||||
<div> <span style="color:#64748b;">// 按 offset 排序,在对应位置插入占位符标记</span></div>
|
||||
<div> <span style="color:#64748b;">// 渲染为 <span class="placeholder" data-node-id="xxx">①</span></span></div>
|
||||
<div>}</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// CSS 透明度控制</div>
|
||||
<div>.placeholder { <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.1</span>; <span style="color:#10b981;">transition</span>: <span style="color:#f59e0b;">opacity 0.2s</span>; }</div>
|
||||
<div>.placeholder.active { <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">1</span>; }</div>
|
||||
<div>.placeholder:hover { <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.6</span>; }</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>交互流程</h3>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
|
||||
<h4 style="margin:0 0 8px 0;color:#3b82f6;">🔗 添加占位符</h4>
|
||||
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
|
||||
<li>教师点击正文某个位置(光标位置)</li>
|
||||
<li>或选中一段文字后释放</li>
|
||||
<li>弹出菜单:"在此处添加节点 →"</li>
|
||||
<li>选择节点类型或已有节点</li>
|
||||
<li>在正文对应位置插入占位符标记</li>
|
||||
<li>创建 AnchorEdge 连线</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:12px;">
|
||||
<h4 style="margin:0 0 8px 0;color:#22c55e;">👁️ 选中节点时的视觉反馈</h4>
|
||||
<ol style="margin:0;padding-left:16px;font-size:11px;color:#475569;line-height:1.8;">
|
||||
<li>点击画布上的某个节点</li>
|
||||
<li>该节点对应的占位符 opacity 从 0.1 → 1</li>
|
||||
<li>连线从 10% → 100% 显示</li>
|
||||
<li>占位符添加 active 样式(边框/阴影)</li>
|
||||
<li>其他占位符保持 10% 透明度</li>
|
||||
<li>点击空白处恢复默认状态</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,376 @@
|
||||
<h2>两种锚定方式的视觉规则</h2>
|
||||
<p class="subtitle">范围锚定(选文本)vs 点锚定(插入占位符)— 默认/选中状态对比</p>
|
||||
|
||||
<!-- 规则总览 -->
|
||||
<div class="section">
|
||||
<h3>视觉规则总览</h3>
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.8;">
|
||||
<div style="color:#94a3b8;">// 两种锚定方式</div>
|
||||
<div><span style="color:#f59e0b;">type</span> <span style="color:#3b82f6;">AnchorType</span> = <span style="color:#10b981;">"range"</span> | <span style="color:#10b981;">"point"</span>;</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// 范围锚定(选一段文本关联节点)</div>
|
||||
<div><span style="color:#94a3b8;">// 文本背景色 = 节点颜色</span></div>
|
||||
<div>.range-anchor {</div>
|
||||
<div> <span style="color:#10b981;">background-color</span>: <span style="color:#f59e0b;">var(--node-color)</span>; <span style="color:#64748b;">// 节点颜色</span></div>
|
||||
<div> <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0</span>; <span style="color:#64748b;">// 默认完全透明(和正常文本一样)</span></div>
|
||||
<div>}</div>
|
||||
<div>.range-anchor.active {</div>
|
||||
<div> <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.3</span>; <span style="color:#64748b;">// 选中节点时显示背景色</span></div>
|
||||
<div>}</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// 点锚定(在文本中插入占位符)</div>
|
||||
<div>.point-anchor {</div>
|
||||
<div> <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">0.3</span>; <span style="color:#64748b;">// 默认半透明</span></div>
|
||||
<div>}</div>
|
||||
<div>.point-anchor.active {</div>
|
||||
<div> <span style="color:#10b981;">opacity</span>: <span style="color:#f59e0b;">1</span>; <span style="color:#64748b;">// 选中节点时不透明</span></div>
|
||||
<div>}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 完整画布:默认状态 -->
|
||||
<div class="section">
|
||||
<h3>完整画布 — 默认状态</h3>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">默认状态(未选中任何节点)</div>
|
||||
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
|
||||
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:480px;">
|
||||
|
||||
<!-- 画布 -->
|
||||
<div style="position:relative;width:100%;height:480px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
|
||||
|
||||
<!-- SVG 连线层(默认 10% 透明度) -->
|
||||
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;opacity:0.1;" viewBox="0 0 800 480">
|
||||
<path d="M 130 100 Q 200 120 290 155" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 130 200 Q 200 210 290 195" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 670 100 Q 600 130 510 175" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 670 200 Q 600 220 510 215" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
<path d="M 670 320 Q 600 300 510 255" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4"/>
|
||||
</svg>
|
||||
|
||||
<!-- 中央:正文容器 -->
|
||||
<div style="position:absolute;left:280px;top:40px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #fde68a;padding-bottom:6px;">
|
||||
<span style="font-size:11px;font-weight:bold;color:#92400e;">📜 课文正文</span>
|
||||
<span style="font-size:9px;color:#94a3b8;background:#fef3c7;padding:1px 4px;border-radius:2px;">🔒 固定</span>
|
||||
</div>
|
||||
<div style="font-size:13px;color:#78350f;line-height:2.2;">
|
||||
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 范围锚定:默认 opacity:0(完全透明,和正常文本一样) -->
|
||||
<span style="background:#3b82f6;opacity:0;color:#78350f;">天气凉了</span>,树叶黄了,
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 点锚定:默认 opacity:0.3(半透明) -->
|
||||
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;">②</span>天空那么蓝,那么高。
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
一群大雁往南飞,
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 范围锚定:默认 opacity:0 -->
|
||||
一会儿排成个<span style="background:#0ea5e9;opacity:0;color:#78350f;">"人"字</span>
|
||||
<!-- 点锚定:默认 opacity:0.3 -->
|
||||
<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-left:2px;">④</span>,
|
||||
</p>
|
||||
<p style="margin:0;">
|
||||
<!-- 点锚定:默认 opacity:0.3 -->
|
||||
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;">⑤</span>啊!秋天来了!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧节点 -->
|
||||
<div style="position:absolute;left:30px;top:70px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
|
||||
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#eff6ff;padding:1px 4px;border-radius:2px;">范围</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:30px;top:180px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
|
||||
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#fffbeb;padding:1px 4px;border-radius:2px;">点</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧节点 -->
|
||||
<div style="position:absolute;left:630px;top:70px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
|
||||
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#f0f9ff;padding:1px 4px;border-radius:2px;">范围</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">讲解:大雁南飞</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:630px;top:180px;width:140px;background:#fff;border:1px solid #ec4899;border-left:3px solid #ec4899;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#ec4899;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">4</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#9f1239;">✏️ 练习</span>
|
||||
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#fdf2f8;padding:1px 4px;border-radius:2px;">点</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">3 道题</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:630px;top:300px;width:140px;background:#fff;border:1px solid #22c55e;border-left:3px solid #22c55e;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#22c55e;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">5</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#166534;">📌 小结</span>
|
||||
<span style="margin-left:auto;font-size:8px;color:#94a3b8;background:#f0fdf4;padding:1px 4px;border-radius:2px;">点</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">总结全文</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:4px;padding:6px 10px;font-size:9px;color:#64748b;">
|
||||
<div>🔵 范围锚定:默认 opacity:0</div>
|
||||
<div>🔴 点锚定:默认 opacity:0.3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中节点 1(范围锚定)的状态 -->
|
||||
<div class="section">
|
||||
<h3>选中"导入"节点(范围锚定)— 文本背景显示</h3>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">选中节点 1 — "天气凉了"背景色显示</div>
|
||||
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
|
||||
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:320px;">
|
||||
<div style="position:relative;width:100%;height:320px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
|
||||
|
||||
<!-- SVG 连线层(选中节点的连线 100% 显示) -->
|
||||
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;" viewBox="0 0 800 320">
|
||||
<!-- 选中节点的连线 100% 显示 -->
|
||||
<path d="M 130 80 Q 200 100 290 135" stroke="#3b82f6" stroke-width="2.5" fill="none" stroke-dasharray="4 4" opacity="1"/>
|
||||
<circle cx="290" cy="135" r="5" fill="#3b82f6"/>
|
||||
<!-- 其他连线保持 10% -->
|
||||
<path d="M 130 180 Q 200 190 290 175" stroke="#f59e0b" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
<path d="M 670 80 Q 600 110 510 155" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
<path d="M 670 180 Q 600 200 510 195" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
<path d="M 670 280 Q 600 260 510 235" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
</svg>
|
||||
|
||||
<!-- 正文容器 -->
|
||||
<div style="position:absolute;left:280px;top:20px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
|
||||
<div style="font-size:13px;color:#78350f;line-height:2.2;">
|
||||
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 范围锚定:选中时 opacity:0.3(背景色显示) -->
|
||||
<span style="background:#3b82f6;opacity:0.3;color:#78350f;border-radius:2px;">天气凉了</span>,树叶黄了,
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 点锚定:未选中,保持 0.3 -->
|
||||
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;">②</span>天空那么蓝,那么高。
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">一群大雁往南飞,</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
一会儿排成个<span style="background:#0ea5e9;opacity:0;color:#78350f;">"人"字</span>
|
||||
<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-left:2px;">④</span>,
|
||||
</p>
|
||||
<p style="margin:0;">
|
||||
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;">⑤</span>啊!秋天来了!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中的节点 1(高亮边框) -->
|
||||
<div style="position:absolute;left:30px;top:50px;width:140px;background:#fff;border:2px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 0 0 3px #3b82f633,0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
|
||||
<span style="margin-left:auto;font-size:8px;color:#fff;background:#3b82f6;padding:1px 4px;border-radius:2px;">范围·选中</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">提问:你见过秋天的树叶吗?</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他节点(正常状态) -->
|
||||
<div style="position:absolute;left:30px;top:160px;width:140px;background:#fff;border:1px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:630px;top:50px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #3b82f6;border-radius:4px;padding:6px 10px;font-size:9px;color:#3b82f6;">
|
||||
✅ 选中"导入"节点 → "天气凉了"背景显示
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中节点 2(点锚定)的状态 -->
|
||||
<div class="section">
|
||||
<h3>选中"文本研习"节点(点锚定)— 占位符不透明</h3>
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">选中节点 2 — 占位符 ② 100% 显示</div>
|
||||
<div class="mockup-body" style="padding:0;background:#f1f5f9;overflow:hidden;">
|
||||
<div style="font-family:monospace;font-size:12px;line-height:1.6;position:relative;height:320px;">
|
||||
<div style="position:relative;width:100%;height:320px;background:#f1f5f9;background-image:radial-gradient(#cbd5e1 1px, transparent 1px);background-size:20px 20px;overflow:hidden;">
|
||||
|
||||
<svg style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;" viewBox="0 0 800 320">
|
||||
<path d="M 130 80 Q 200 100 290 135" stroke="#3b82f6" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
<!-- 选中节点的连线 100% -->
|
||||
<path d="M 130 180 Q 200 190 290 175" stroke="#f59e0b" stroke-width="2.5" fill="none" stroke-dasharray="4 4" opacity="1"/>
|
||||
<circle cx="290" cy="175" r="5" fill="#f59e0b"/>
|
||||
<path d="M 670 80 Q 600 110 510 155" stroke="#0ea5e9" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
<path d="M 670 180 Q 600 200 510 195" stroke="#ec4899" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
<path d="M 670 280 Q 600 260 510 235" stroke="#22c55e" stroke-width="2" fill="none" stroke-dasharray="4 4" opacity="0.1"/>
|
||||
</svg>
|
||||
|
||||
<div style="position:absolute;left:280px;top:20px;width:240px;background:#fffbeb;border:2px solid #f59e0b;border-radius:8px;padding:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);">
|
||||
<div style="font-size:13px;color:#78350f;line-height:2.2;">
|
||||
<div style="text-align:center;font-weight:bold;margin-bottom:6px;">秋天</div>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 范围锚定:未选中,opacity:0 -->
|
||||
<span style="background:#3b82f6;opacity:0;color:#78350f;">天气凉了</span>,树叶黄了,
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
<!-- 点锚定:选中时 opacity:1(不透明) -->
|
||||
<span style="display:inline-block;background:#f59e0b;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:1;margin-right:2px;box-shadow:0 0 0 2px #f59e0b44;">②</span>天空那么蓝,那么高。
|
||||
</p>
|
||||
<p style="margin:0 0 6px 0;">一群大雁往南飞,</p>
|
||||
<p style="margin:0 0 6px 0;">
|
||||
一会儿排成个<span style="background:#0ea5e9;opacity:0;color:#78350f;">"人"字</span>
|
||||
<span style="display:inline-block;background:#ec4899;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-left:2px;">④</span>,
|
||||
</p>
|
||||
<p style="margin:0;">
|
||||
<span style="display:inline-block;background:#22c55e;color:#fff;border-radius:3px;padding:0 4px;font-size:9px;font-weight:bold;opacity:0.3;margin-right:2px;">⑤</span>啊!秋天来了!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:30px;top:50px;width:140px;background:#fff;border:1px solid #3b82f6;border-left:3px solid #3b82f6;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#3b82f6;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">1</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#1e3a8a;">💡 导入</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选中的节点 2(高亮边框) -->
|
||||
<div style="position:absolute;left:30px;top:160px;width:140px;background:#fff;border:2px solid #f59e0b;border-left:3px solid #f59e0b;border-radius:6px;padding:8px;box-shadow:0 0 0 3px #f59e0b33,0 2px 6px rgba(0,0,0,0.08);">
|
||||
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;">
|
||||
<span style="background:#f59e0b;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">2</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#92400e;">📝 文本研习</span>
|
||||
<span style="margin-left:auto;font-size:8px;color:#fff;background:#f59e0b;padding:1px 4px;border-radius:2px;">点·选中</span>
|
||||
</div>
|
||||
<div style="font-size:9px;color:#64748b;">赏析:叠词的运用</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;left:630px;top:50px;width:140px;background:#fff;border:1px solid #0ea5e9;border-left:3px solid #0ea5e9;border-radius:6px;padding:8px;box-shadow:0 2px 6px rgba(0,0,0,0.08);opacity:0.7;">
|
||||
<div style="display:flex;align-items:center;gap:4px;">
|
||||
<span style="background:#0ea5e9;color:#fff;border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;font-size:8px;font-weight:bold;">3</span>
|
||||
<span style="font-size:10px;font-weight:bold;color:#075985;">📚 新授</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position:absolute;top:12px;right:12px;background:#fff;border:1px solid #f59e0b;border-radius:4px;padding:6px 10px;font-size:9px;color:#f59e0b;">
|
||||
✅ 选中"文本研习"节点 → 占位符 ② 不透明显示
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 两种锚定方式对比表 -->
|
||||
<div class="section">
|
||||
<h3>两种锚定方式对比</h3>
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:12px;">
|
||||
<thead>
|
||||
<tr style="background:#1e293b;color:#e2e8f0;">
|
||||
<th style="padding:8px 12px;text-align:left;border:1px solid #334155;">特性</th>
|
||||
<th style="padding:8px 12px;text-align:left;border:1px solid #334155;">范围锚定(选文本)</th>
|
||||
<th style="padding:8px 12px;text-align:left;border:1px solid #334155;">点锚定(插入占位符)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="background:#fff;">
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">触发方式</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">选中一段文字 → 关联节点</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">点击文本某位置 → 插入占位符</td>
|
||||
</tr>
|
||||
<tr style="background:#f8fafc;">
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">视觉表现</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">文本背景色 = 节点颜色</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">插入标记符号(①②③)</td>
|
||||
</tr>
|
||||
<tr style="background:#fff;">
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">默认透明度</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
|
||||
<span style="background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0</span>
|
||||
<span style="color:#64748b;font-size:10px;">(完全透明,和正常文本一样)</span>
|
||||
</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
|
||||
<span style="background:#f59e0b;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0.3</span>
|
||||
<span style="color:#64748b;font-size:10px;">(半透明,隐约可见)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background:#f8fafc;">
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">选中时透明度</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
|
||||
<span style="background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0.3</span>
|
||||
<span style="color:#64748b;font-size:10px;">(背景色显示)</span>
|
||||
</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">
|
||||
<span style="background:#f59e0b;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 1</span>
|
||||
<span style="color:#64748b;font-size:10px;">(不透明,完整显示)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background:#fff;">
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">连线透明度</td>
|
||||
<td colspan="2" style="padding:8px 12px;border:1px solid #e2e8f0;">
|
||||
默认 <span style="background:#64748b;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 0.1</span> · 选中时 <span style="background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;font-size:10px;">opacity: 1</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="background:#f8fafc;">
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;font-weight:bold;">适用场景</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">节点与具体文字内容相关(如赏析某词、讲解某句)</td>
|
||||
<td style="padding:8px 12px;border:1px solid #e2e8f0;">节点对应文本某个位置(如在此处开始导入、在此处小结)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据模型 -->
|
||||
<div class="section">
|
||||
<h3>数据模型</h3>
|
||||
<div style="background:#1e293b;color:#e2e8f0;padding:16px;border-radius:6px;font-family:monospace;font-size:11px;line-height:1.6;">
|
||||
<div style="color:#94a3b8;">// 锚点 — 统一接口,区分 type</div>
|
||||
<div><span style="color:#f59e0b;">interface</span> <span style="color:#3b82f6;">NodeAnchor</span> {</div>
|
||||
<div> id: <span style="color:#10b981;">string</span>;</div>
|
||||
<div> nodeId: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// 关联的节点</span></div>
|
||||
<div> type: <span style="color:#10b981;">"range"</span> | <span style="color:#10b981;">"point"</span>; <span style="color:#64748b;">// 两种锚定方式</span></div>
|
||||
<div> start: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// 正文纯文本偏移量</span></div>
|
||||
<div> end?: <span style="color:#10b981;">number</span>; <span style="color:#64748b;">// range 锚定的结束偏移(point 无)</span></div>
|
||||
<div> textPreview?: <span style="color:#10b981;">string</span>; <span style="color:#64748b;">// range 锚定的文字预览</span></div>
|
||||
<div>}</div>
|
||||
<br>
|
||||
<div style="color:#94a3b8;">// 渲染规则</div>
|
||||
<div><span style="color:#f59e0b;">function</span> <span style="color:#3b82f6;">getAnchorStyle</span>(anchor: <span style="color:#3b82f6;">NodeAnchor</span>, isActive: <span style="color:#10b981;">boolean</span>) {</div>
|
||||
<div> <span style="color:#f59e0b;">if</span> (anchor.type === <span style="color:#10b981;">"range"</span>) {</div>
|
||||
<div> <span style="color:#f59e0b;">return</span> { backgroundColor: getNodeColor(anchor.nodeId), opacity: isActive ? <span style="color:#f59e0b;">0.3</span> : <span style="color:#f59e0b;">0</span> };</div>
|
||||
<div> } <span style="color:#f59e0b;">else</span> { <span style="color:#64748b;">// point</span></div>
|
||||
<div> <span style="color:#f59e0b;">return</span> { opacity: isActive ? <span style="color:#f59e0b;">1</span> : <span style="color:#f59e0b;">0.3</span> };</div>
|
||||
<div> }</div>
|
||||
<div>}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1782143663726}
|
||||
@@ -0,0 +1,3 @@
|
||||
sessionDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344
|
||||
stateDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344\state
|
||||
contentDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344\content
|
||||
@@ -4,19 +4,23 @@
|
||||
|
||||
**任何任务开始前,必须先查阅架构影响地图,通过图定位代码和模块。**
|
||||
|
||||
1. **先图后码**:执行任何分析、修改、搜索任务时,首先阅读 `docs/architecture/004_architecture_impact_map.md` 或 `docs/architecture/005_architecture_data.json`,从图中定位目标模块、函数、依赖关系,再按图索骥读取源码
|
||||
2. **图未覆盖则先补图**:如果发现项目中存在架构图未记录的模块、函数、表、路由等,**必须优先完善架构图信息**,然后再继续后续工作
|
||||
3. **改码必同步图**:对源码的任何修改完成后,必须同步更新 004 和 005 两个架构文档
|
||||
1. **先图后码**:执行任何分析、修改、搜索任务时,首先运行 `npm run arch:scan` 更新 arch.db,再通过 `npm run arch:query` 查询目标模块、函数、依赖关系,结合阅读 `docs/architecture/004_architecture_impact_map.md` 定位架构设计意图,最后按图索骥读取源码
|
||||
2. **图未覆盖则先补图**:如果发现项目中存在 arch.db 未记录的模块、函数、表、路由等,**必须先运行 `npm run arch:scan` 重新扫描**,然后检查 004 是否需要补充
|
||||
3. **改码必同步图**:对源码的任何修改完成后,必须运行 `npm run arch:scan` 更新 arch.db;若架构设计意图有变化,同步更新 004
|
||||
|
||||
### 架构文档清单
|
||||
|
||||
| 文档 | 用途 |
|
||||
|------|------|
|
||||
| `docs/architecture/004_architecture_impact_map.md` | 人类可读的架构影响地图 |
|
||||
| `docs/architecture/005_architecture_data.json` | AI 友好格式的结构化数据 |
|
||||
| `docs/architecture/004_architecture_impact_map.md` | 架构设计意图唯一源(人类可读) |
|
||||
| `docs/architecture/006_k12_feature_checklist.md` | 标准功能模块清单 |
|
||||
| `docs/architecture/007_gap_audit_report.md` | 差距审计报告 |
|
||||
| `docs/architecture/audit/01_decoupling_roadmap.md` | 解耦路线图 |
|
||||
| `docs/architecture/008_module_role_mapping.md` | 模块角色映射 |
|
||||
| `docs/architecture/roadmap/` | 长远规划(tech-debt/decoupling/pending-features) |
|
||||
| `docs/architecture/audit/` | 架构审查报告与归档(含已废弃的 005 JSON、004 V1) |
|
||||
| `docs/troubleshooting/known-issues.md` | 已知问题速查(场景→技术映射 + 工作经验日志) |
|
||||
|
||||
> 注:005_architecture_data.json 已废弃归档至 `audit/archive/`,结构化数据查询统一通过 arch.db
|
||||
|
||||
### 需要同步图的场景
|
||||
|
||||
@@ -30,9 +34,23 @@
|
||||
|
||||
### 同步方式
|
||||
|
||||
- 修改 Markdown 文档中对应的模块章节
|
||||
- 修改 JSON 文档中对应的节点(`modules.*.exports`、`permissions`、`dependencyMatrix`、`routes`、`dbTables` 等)
|
||||
- 确保两个文档内容一致
|
||||
- 修改源码后运行 `npm run arch:scan` 更新 arch.db(强制)
|
||||
- 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
|
||||
- 若发现新的"场景→技术"映射或工作经验,更新 `docs/troubleshooting/known-issues.md`
|
||||
|
||||
## 架构元数据库规则(arch.db)
|
||||
|
||||
**arch.db 是代码结构唯一源,AI 工作前必须运行 `npm run arch:scan` 更新。**
|
||||
|
||||
1. **arch.db 取代 005 JSON**:模块、函数、调用关系、依赖关系、技术标签查询 arch.db,不手动维护结构化数据文件
|
||||
2. **查询命令**:
|
||||
- `npm run arch:query -- sql "<SQL>"` 自定义 SQL 查询
|
||||
- `npm run arch:query -- module-deps` 查模块依赖
|
||||
- `npm run arch:query -- module-reverse-deps <module>` 查反向依赖
|
||||
- `npm run arch:query -- symbol-refs <symbol>` 查符号引用链
|
||||
- `npm run arch:query -- tech-usage <tag>` 查技术使用
|
||||
- `npm run arch:query -- violations` 查架构违规
|
||||
3. **arch.db 不替代 004**:arch.db 是"代码现状",004 是"设计意图",两者互补
|
||||
|
||||
## 编码规范
|
||||
|
||||
@@ -107,7 +125,31 @@ src/modules/[module]/
|
||||
- 使用 `cn()` 工具函数管理条件类名
|
||||
- **禁止**字符串拼接动态类名(`bg-${color}-500`)
|
||||
- **禁止**使用任意值(`w-[137px]`),除非有充分理由并注释
|
||||
- 设计令牌在 `src/app/globals.css` 中使用 CSS 变量定义
|
||||
- 设计令牌在 `src/app/styles/tokens/` 目录中分层定义,通过 `@theme inline` 暴露为 Tailwind 类
|
||||
|
||||
### 设计令牌规范(强制)
|
||||
|
||||
- **禁止硬编码颜色**: TSX/TS/CSS 中不得出现 `#hex` 颜色字面量,统一使用 `hsl(var(--*))` 或 Tailwind 类 `bg-*`
|
||||
- **禁止硬编码字体**: 不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量,使用 `var(--font-family-sans/serif/mono)`
|
||||
- **禁止硬编码字号**: 不得出现 `font-size: Npx`,使用 `var(--font-size-1~9)`
|
||||
- **禁止 Tailwind 任意值**: 不得使用 `w-[Npx]`/`h-[Npx]`/`p-[Npx]` 等,映射到 `--space-*` 或 Tailwind 默认阶梯
|
||||
- **豁免场景**(需 `// eslint-disable-next-line no-restricted-syntax -- <reason>` 注释):
|
||||
- PWA manifest(`src/app/manifest.ts`)
|
||||
- 邮件 HTML 内联样式(`src/modules/notifications/channels/email-channel.ts`)
|
||||
- 图表 SVG 固定画布尺寸(recharts 选择器中的 `#ccc`/`#fff`)
|
||||
- loading.tsx 占位骨架
|
||||
- Dialog 固定宽度等无法令牌化的设计固定尺寸
|
||||
- **令牌文件分布**: `src/app/styles/tokens/`(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index)
|
||||
- **令牌分层**:
|
||||
- Layer 1 Primitive(`primitive.css`):原始色板/字号/间距/阴影,业务代码不直接引用
|
||||
- Layer 2 Semantic(`semantic-light.css` + `semantic-dark.css`):语义令牌,业务代码唯一引用入口
|
||||
- 模块命名空间(`lesson-preparation.css`):`--lp-*` 令牌,明暗双份
|
||||
- Tailwind 暴露(`tailwind-theme.css`):`@theme inline` 将 Semantic 令牌暴露为 `bg-*`/`text-*`/`font-*` 类
|
||||
- **改令牌必同步图**: 修改令牌定义后,同步更新 `docs/architecture/004_architecture_impact_map.md` 与 arch.db(`npm run arch:scan`)
|
||||
- **ESLint 强制约束**:
|
||||
- `no-restricted-syntax`: 禁止 `#hex` 字面量
|
||||
- `design-tokens/no-hardcoded-fonts`: 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量(单词边界匹配,不影响 `Interval`/`Interactive` 等标识符)
|
||||
- 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML)、`manifest.ts`(PWA)
|
||||
|
||||
### 安全规范
|
||||
|
||||
@@ -121,3 +163,64 @@ src/modules/[module]/
|
||||
- 使用 Conventional Commits 格式:`feat(scope): description`
|
||||
- 类型:`feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `perf`, `ci`
|
||||
- 提交前必须运行 `npm run lint` 和 `npx tsc --noEmit` 确保零错误
|
||||
|
||||
## 问题记录规则
|
||||
|
||||
**所有工作完成后,必须将遇到的问题记录到 `docs/troubleshooting/known-issues.md`(索引式速查手册)。**
|
||||
|
||||
### 必须记录的场景
|
||||
|
||||
| 场景 | 记录要求 |
|
||||
|------|---------|
|
||||
| 构建报错(dev/build/lint/tsc) | 记录到"全局经验"对应主题分区 |
|
||||
| 运行时异常(白屏/API 报错/数据加载失败) | 记录到"模块经验"对应模块分区 |
|
||||
| 框架/库版本兼容问题 | 记录到"全局经验: Next.js 配置与运行时" |
|
||||
| 依赖配置问题(serverExternalPackages/webpackIgnore 等) | 记录到"全局经验: Next.js 配置与运行时" |
|
||||
| 架构约束违规 | 记录到"全局经验"对应主题分区 |
|
||||
|
||||
### 记录格式
|
||||
|
||||
索引式表格,指明"场景→技术/规则"映射,不写多行代码示例:
|
||||
|
||||
```markdown
|
||||
### X.X 主题分区
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
|------|----------|
|
||||
| 简述场景 | 正确做法(一句话) |
|
||||
```
|
||||
|
||||
### 记录要求
|
||||
|
||||
- **索引式**:场景→技术/规则映射,不写代码示例和错误示范列
|
||||
- **去重**:同类问题在原条目补充,不重复创建
|
||||
- **引用架构规则**:架构分层、模块结构等规则引用 004 和 project_rules,不重复
|
||||
- **工作经验日志**:在"工作经验日志"区按时间倒序追加(50 条上限),记录"做了什么/学到什么/下次注意"
|
||||
|
||||
## AI 工作强制流程
|
||||
|
||||
**所有 AI 工作必须遵循此流程,违反即违规。**
|
||||
|
||||
### 阶段 1: 上下文加载
|
||||
|
||||
1. `npm run arch:scan` 更新 arch.db
|
||||
2. `npm run arch:query -- module-deps` 查目标模块依赖
|
||||
3. `npm run arch:query -- symbol-refs <目标函数>` 查调用链
|
||||
4. 阅读 `src/modules/[模块]/README.md` 读模块工作流程
|
||||
5. 查 `docs/troubleshooting/known-issues.md` "模块经验" 分区读相关经验
|
||||
|
||||
### 阶段 2: 执行工作
|
||||
|
||||
1. 按规划执行
|
||||
2. 修改代码后立即运行 `npm run arch:scan` 更新 arch.db
|
||||
3. 运行 `npx tsc --noEmit` 和 `npm run lint` 确保零错误
|
||||
|
||||
### 阶段 3: 经验沉淀(强制,不可跳过)
|
||||
|
||||
1. 在 `docs/troubleshooting/known-issues.md` "工作经验日志" 区追加一条记录:
|
||||
- 日期 + 时间
|
||||
- 模块
|
||||
- 做了什么 + 学到什么
|
||||
2. 若发现新的"场景→技术"映射 → 提炼到对应模块分区
|
||||
3. 若发现新的架构决策 → 更新 004
|
||||
4. 若代码结构变化 → `npm run arch:scan` 确认 arch.db 已更新
|
||||
|
||||
1
.tsc_out.txt
Normal file
@@ -0,0 +1 @@
|
||||
src/app/(dashboard)/teacher/textbooks/error.tsx(3,10): error TS2305: Module '"@/shared/components/route-error"' has no exported member 'RouteError'.
|
||||
43
CHANGELOG.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- arch:scan @public JSDoc 标记豁免机制,支持登录前/公开/内部工具 Server Action 豁免权限校验
|
||||
- arch:scan 递归 CTE 违规检测,识别通过辅助函数间接调用 requirePermission 的调用链
|
||||
- 大仓工程基建:LICENSE、CONTRIBUTING、SECURITY、.env.example 文档
|
||||
- husky + lint-staged + commitlint 本地提交规范工具链
|
||||
- /api/health 健康检查端点 + Dockerfile HEALTHCHECK
|
||||
- @next/bundle-analyzer 构建体积分析工具
|
||||
- CI 流水线新增 Unit test + coverage 阶段
|
||||
- tsconfig 开启 noUncheckedIndexedAccess 严格模式
|
||||
|
||||
### Changed
|
||||
- 重构 004 架构文档为完整架构设计文档(912 行,14 章节,13 个 mermaid 图)
|
||||
- 重写 35 个模块 README,统一 8 章节模板(架构图/流程图/技术栈)
|
||||
- 拆分 5 个超长文件:schema.ts (2245→29+27子文件)、invalidation-map.ts (1195→50+6子文件)、messaging/actions.ts (973→47+5子文件)、textbooks/data-access.ts (907→15+6子文件)、questions/data-access.ts (828→48+4子文件)
|
||||
- 精简 known-issues.md 为索引式速查手册(场景→技术/规则映射)
|
||||
|
||||
### Fixed
|
||||
- 修复 20 个 Server Action 权限违规(12 个 @public 豁免 + 8 个真违规修复)
|
||||
- 修复 ai 模块 6 个 Action 权限误报(requireAiPermission 间接调用链识别)
|
||||
- 修复 parent 模块 6 个 Action 权限缺失(requireAuth → requirePermission)
|
||||
- 修复 settings 模块 updateProfileAction 权限校验(显式 requirePermission)
|
||||
|
||||
## [0.1.0] - 2026-06-01
|
||||
|
||||
### Added
|
||||
- 初始版本发布
|
||||
- K12 智慧教学平台核心功能:备课、作业、考试、成绩、考勤、消息、家校互动
|
||||
- 严格三层架构:app → modules → shared
|
||||
- 5 层状态管理模型:URL(nuqs) · Server(TanStack Query) · Client(Zustand) · Global UI · Form
|
||||
- 权限 3 道防线:proxy.ts → requirePermission → usePermission
|
||||
- 设计令牌双层架构:Primitive + Semantic
|
||||
- arch.db 架构元数据库(12 张表 + 7 个索引)
|
||||
- cacheFn 请求级缓存层
|
||||
- Gitea Actions CI/CD 流水线
|
||||
129
CONTRIBUTING.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 贡献指南
|
||||
|
||||
感谢参与本项目!请遵循以下规范提交贡献。
|
||||
|
||||
## 开发环境准备
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
npm install
|
||||
|
||||
# 2. 准备环境变量
|
||||
cp .env.example .env
|
||||
# 编辑 .env 填入实际配置
|
||||
|
||||
# 3. 初始化数据库
|
||||
npm run db:push
|
||||
|
||||
# 4. 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 强制工作流程
|
||||
|
||||
**所有代码改动前必须先查阅架构文档:**
|
||||
|
||||
1. 阅读 `docs/architecture/004_architecture_impact_map.md` 了解架构设计意图
|
||||
2. 运行 `npm run arch:scan` 更新 arch.db
|
||||
3. 运行 `npm run arch:query -- module-deps` 查目标模块依赖
|
||||
4. 阅读 `src/modules/[模块]/README.md` 了解模块工作流程
|
||||
5. 查 `docs/troubleshooting/known-issues.md` 读相关经验
|
||||
|
||||
**代码改动后必须:**
|
||||
|
||||
1. 运行 `npx tsc --noEmit` 确保零错误
|
||||
2. 运行 `npm run lint` 确保零错误
|
||||
3. 运行 `npm run arch:scan` 更新 arch.db
|
||||
4. 若架构设计意图变化,同步更新 004 文档
|
||||
5. 若发现新场景→技术映射,更新 known-issues.md
|
||||
|
||||
## 提交规范
|
||||
|
||||
### Conventional Commits 格式
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**类型(type):**
|
||||
- `feat`: 新功能
|
||||
- `fix`: Bug 修复
|
||||
- `docs`: 文档变更
|
||||
- `style`: 代码格式(不影响功能)
|
||||
- `refactor`: 重构(既不是新功能也不是修复)
|
||||
- `test`: 测试相关
|
||||
- `chore`: 构建/工具/依赖变更
|
||||
- `perf`: 性能优化
|
||||
- `ci`: CI/CD 变更
|
||||
|
||||
**示例:**
|
||||
```
|
||||
feat(arch-scan): add @public JSDoc tag exemption mechanism
|
||||
fix(permissions): fix parent module 6 Action permission violations
|
||||
refactor: split 5 oversized files into domain-specific subfiles
|
||||
docs(architecture): rewrite 004 as architecture design document
|
||||
```
|
||||
|
||||
### 提交前检查
|
||||
|
||||
husky + lint-staged 会在 `git commit` 时自动执行:
|
||||
- ESLint 检查暂存文件
|
||||
- Prettier 格式化暂存文件
|
||||
- commitlint 校验 commit message 格式
|
||||
|
||||
如果检查失败,请修复后重新提交。
|
||||
|
||||
## 架构约束
|
||||
|
||||
### 严格三层架构
|
||||
|
||||
```
|
||||
app → modules → shared
|
||||
```
|
||||
|
||||
- `app/` 只能调用 `modules/` 的 Server Actions 和 data-access
|
||||
- `modules/` 之间通过对方 data-access 通信,不直接查询对方 DB 表
|
||||
- `shared/` 不得反向依赖 `modules/*` 或 `app/*`
|
||||
|
||||
### 代码质量规则
|
||||
|
||||
- 禁止 `any`,未知类型用 `unknown` + 类型守卫
|
||||
- 禁止 `as` 断言(除非从 `unknown` 转换,需注释原因)
|
||||
- 函数返回值必须显式标注,特别是 `Promise<T>`
|
||||
- 仅用于类型的导入使用 `import type`
|
||||
- Server Action 必须调用 `requirePermission()`(或加 `@public` 标记豁免)
|
||||
- 前端权限检查使用 `usePermission().hasPermission()`,禁止 `role === "xxx"` 硬编码
|
||||
- 单文件行数:组件 ≤500,actions/data-access ≤800,硬限 1000
|
||||
|
||||
### 设计令牌规范
|
||||
|
||||
- 禁止硬编码颜色(`#hex`),使用 `hsl(var(--*))` 或 Tailwind 类
|
||||
- 禁止硬编码字体(`'Inter'`),使用 `var(--font-family-*)`
|
||||
- 禁止 Tailwind 任意值(`w-[137px]`),映射到 `--space-*` 或默认阶梯
|
||||
|
||||
## 文档同步
|
||||
|
||||
### 需要同步架构图的场景
|
||||
|
||||
- 新增/删除/重命名导出函数、组件、Hook、类型
|
||||
- 修改函数签名(参数、返回类型)
|
||||
- 修改权限点或角色-权限映射
|
||||
- 新增/删除数据库表、路由页面、API 路由
|
||||
- 修改模块间依赖关系
|
||||
- 新增模块
|
||||
|
||||
### 同步方式
|
||||
|
||||
- 修改源码后运行 `npm run arch:scan` 更新 arch.db(强制)
|
||||
- 若架构设计意图变化,同步更新 `docs/architecture/004_architecture_impact_map.md`
|
||||
- 若发现新"场景→技术"映射,更新 `docs/troubleshooting/known-issues.md`
|
||||
|
||||
## 问题报告
|
||||
|
||||
- 构建/lint/tsc 报错 → 记录到 `docs/troubleshooting/known-issues.md` "全局经验"分区
|
||||
- 运行时异常 → 记录到"模块经验"分区
|
||||
- 框架/库版本兼容问题 → 记录到"全局经验: Next.js 配置与运行时"
|
||||
@@ -18,4 +18,7 @@ EXPOSE 3000
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD node -e "fetch('http://localhost:' + (process.env.PORT || 3000) + '/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
14
LICENSE
Normal file
@@ -0,0 +1,14 @@
|
||||
PROPRIETARY AND CONFIDENTIAL
|
||||
|
||||
Copyright (c) 2026 EazyGame. All rights reserved.
|
||||
|
||||
This source code and accompanying documentation (the "Software") is the
|
||||
proprietary and confidential property of EazyGame. No part of the Software
|
||||
may be reproduced, distributed, or transmitted in any form or by any means,
|
||||
including photocopying, recording, or other electronic or mechanical methods,
|
||||
without the prior written permission of EazyGame.
|
||||
|
||||
For licensing inquiries, contact: legal@eazygame.cn
|
||||
|
||||
Unauthorized use, reproduction, or distribution of this Software, via any
|
||||
medium, is strictly prohibited and may result in civil and criminal penalties.
|
||||
90
SECURITY.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 安全策略
|
||||
|
||||
## 报告安全漏洞
|
||||
|
||||
**请不要通过 GitHub Issue 公开报告安全漏洞。**
|
||||
|
||||
发现安全漏洞请通过以下渠道私密报告:
|
||||
|
||||
- 邮件:security@eazygame.cn
|
||||
- 内部工单系统:Security 项目 → New Issue
|
||||
|
||||
报告时请包含:
|
||||
1. 漏洞描述和影响范围
|
||||
2. 复现步骤(最小化示例)
|
||||
3. 影响的版本号
|
||||
4. 建议的修复方案(可选)
|
||||
|
||||
**响应时间:** 24 小时内确认收到,5 个工作日内给出评估结果。
|
||||
|
||||
## 安全架构
|
||||
|
||||
### 权限三道防线
|
||||
|
||||
```
|
||||
proxy.ts (路由级 bitmap) → requirePermission (Server Action 级) → usePermission (客户端级)
|
||||
```
|
||||
|
||||
- **路由级**:`src/proxy.ts` 使用 bitmap 快速拦截未授权路由
|
||||
- **Server Action 级**:每个 Action 必须调用 `requirePermission()`,或用 `@public` JSDoc 标记豁免
|
||||
- **客户端级**:组件使用 `usePermission().hasPermission()` 控制元素显隐
|
||||
|
||||
### 认证与会话
|
||||
|
||||
- JWT/session ID 存储在 httpOnly + Secure + SameSite=Strict 的 Cookie 中
|
||||
- 服务端环境变量不加 `NEXT_PUBLIC_` 前缀
|
||||
- 环境变量使用 `@t3-oss/env-nextjs` + Zod 校验(`src/env.mjs`)
|
||||
|
||||
### 数据访问
|
||||
|
||||
- 前端禁止直接访问数据库,所有数据访问必须通过 `data-access.ts` 模块
|
||||
- Server Action 必须使用 `requirePermission()` 进行权限校验
|
||||
- 家长路由必须包含 `parentId` 和 `studentId` 双重权限校验,防止信息泄露
|
||||
|
||||
### 输入安全
|
||||
|
||||
- **禁止 `dangerouslySetInnerHTML`**(如必须使用,先用 DOMPurify 清洗)
|
||||
- Server Action 输入使用 Zod 验证,验证失败返回结构化错误
|
||||
- 注册/登录流程实施速率限制,防止暴力破解和邮箱枚举攻击
|
||||
|
||||
## 安全审计
|
||||
|
||||
### arch:scan 自动检测
|
||||
|
||||
`npm run arch:query -- violations` 会自动检测:
|
||||
|
||||
- **长文件**(>800 行):提示拆分,降低维护风险
|
||||
- **Server Action 权限缺失**:识别未调用 `requirePermission` 的 Server Action(支持递归调用链识别)
|
||||
|
||||
### @public 豁免标记
|
||||
|
||||
登录前/公开/内部工具 Server Action 可用 `@public` JSDoc 标记豁免权限校验:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 注册 Action,登录前公开调用。
|
||||
*
|
||||
* @public 登录前公开 Action,豁免 requirePermission 校验。
|
||||
*/
|
||||
export async function registerAction(formData: FormData) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**豁免场景:**
|
||||
- 登录前 Action(注册、邮箱可用性检查、2FA 预检)
|
||||
- 内部日志工具(audit-logger、change-logger、login-logger)
|
||||
- 权限查询工具(isAdminRole、canConfigurePublicAiProvider)
|
||||
- 登录后必经流程(onboarding 状态查询/完成)
|
||||
|
||||
## 依赖安全
|
||||
|
||||
- 定期运行 `npm audit` 检查已知漏洞
|
||||
- CI 流水线包含 Trivy 安全扫描(`.trivyignore` 配置豁免项)
|
||||
- 依赖升级通过 PR 审核,不允许直接推送 main 分支
|
||||
|
||||
## 数据保护
|
||||
|
||||
- 数据库备份:每日自动备份,每周 DR 演练
|
||||
- 敏感数据(密码、2FA 密钥)使用 bcrypt/Argon2 哈希存储
|
||||
- 日志不记录敏感信息(密码、token、个人身份信息)
|
||||
BIN
bugs/screenshots/announcements.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
bugs/screenshots/login_failure.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
bugs/screenshots/messages.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
bugs/screenshots/teacher_attendance_error.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
bugs/screenshots/teacher_attendance_sheet_error.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
bugs/screenshots/teacher_attendance_stats_error.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
bugs/screenshots/teacher_classes.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
bugs/screenshots/teacher_classes_error.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
bugs/screenshots/teacher_classes_my_error.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
bugs/screenshots/teacher_classes_schedule_error.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
bugs/screenshots/teacher_classes_students_error.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
bugs/screenshots/teacher_course-plans_error.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
bugs/screenshots/teacher_dashboard_error.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
bugs/screenshots/teacher_diagnostic.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
bugs/screenshots/teacher_diagnostic_error.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
bugs/screenshots/teacher_elective_error.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
bugs/screenshots/teacher_error-book.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
bugs/screenshots/teacher_error-book_error.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
bugs/screenshots/teacher_exams_all_error.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
bugs/screenshots/teacher_exams_create_error.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
bugs/screenshots/teacher_exams_error.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
bugs/screenshots/teacher_grades.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
bugs/screenshots/teacher_grades_analytics_error.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
bugs/screenshots/teacher_grades_entry.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
bugs/screenshots/teacher_grades_stats.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
bugs/screenshots/teacher_homework_assignments_create_error.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
bugs/screenshots/teacher_homework_assignments_error.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
bugs/screenshots/teacher_homework_error.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
bugs/screenshots/teacher_homework_submissions.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
bugs/screenshots/teacher_homework_submissions_error.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
bugs/screenshots/teacher_lesson-plans_error.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
bugs/screenshots/teacher_lesson-plans_new_error.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
bugs/screenshots/teacher_questions_error.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
bugs/screenshots/teacher_schedule-changes.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
bugs/screenshots/teacher_schedule-changes_error.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
bugs/screenshots/teacher_textbooks_error.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
1734
build-output.txt
Normal file
24
commitlint.config.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"test",
|
||||
"chore",
|
||||
"perf",
|
||||
"ci",
|
||||
"build",
|
||||
"revert",
|
||||
],
|
||||
],
|
||||
"subject-case": [0],
|
||||
"header-max-length": [2, "always", 120],
|
||||
},
|
||||
}
|
||||
4
cookies.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
BIN
debug-exams.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
@@ -69,7 +69,7 @@
|
||||
| 文档 | 归档原因 |
|
||||
|------|---------|
|
||||
| [002 RBAC 重构方案](architecture/002_rbac_refactoring.md) | 描述修复前的安全隐患,当前所有 Server Action 已接入 `requirePermission()` |
|
||||
| [002 角色路由 RFC](architecture/002_role_based_routing.md) | 2025-12-23 提案,当前角色域路由已全部实现 |
|
||||
| [002b 角色路由 RFC](architecture/002b_role_based_routing.md) | 2025-12-23 提案,当前角色域路由已全部实现 |
|
||||
| [003 UI 重构计划](architecture/003_ui_refactoring_plan.md) | 2026-06-16 重构计划,当前已执行完毕 |
|
||||
|
||||
### 设计历史文档
|
||||
|
||||
@@ -368,7 +368,7 @@ AI 模式: 选择 AI Provider → 粘贴试卷源文本 → AI 解析生成 →
|
||||
| API 路由 | 9 |
|
||||
| Server Actions | 80+ |
|
||||
| 用户角色 | 6 (admin/teacher/student/parent/grade_head/teaching_head) |
|
||||
| 权限点 | 54 |
|
||||
| 权限点 | 67 |
|
||||
|
||||
---
|
||||
|
||||
@@ -380,3 +380,4 @@ AI 模式: 选择 AI Provider → 粘贴试卷源文本 → AI 解析生成 →
|
||||
| [005 架构数据](./005_architecture_data.json) | AI 友好的结构化架构数据 |
|
||||
| [006 功能清单](./006_k12_feature_checklist.md) | 企业级 K12 标准功能模块清单 |
|
||||
| [007 差距审计报告](./007_gap_audit_report.md) | 功能差距审计与补齐路线图 |
|
||||
| [008 模块角色映射](./008_module_role_mapping.md) | 模块-角色-功能映射总览 |
|
||||
|
||||
299
docs/architecture/008_module_role_mapping.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# 模块-角色-功能映射总览
|
||||
|
||||
> 生成日期:2026-06-22
|
||||
> 数据来源:`src/modules/` 全量扫描 + `src/app/(dashboard)/` 路由权限校验 + `src/shared/types/permissions.ts` 权限点定义
|
||||
> 角色体系:admin / teacher / student / parent / grade_head / teaching_head(含 management 路由组)
|
||||
|
||||
---
|
||||
|
||||
## 一、模块-角色速查表
|
||||
|
||||
| # | 模块 | 目录 | admin | teacher | student | parent | mgmt | 核心功能 |
|
||||
|---|------|------|:-----:|:-------:|:-------:|:------:|:----:|----------|
|
||||
| 1 | adaptive-practice | `adaptive-practice/` | | ✅ | ✅ | ✅ | ✅ | 专项练习:自适应出题、答题、练习历史、成绩分析 |
|
||||
| 2 | ai | `ai/` | ✅ | ✅ | ✅ | ✅ | ✅ | AI 赋能:对话助手、出题辅助、批改辅助、学情分析、多模型配置 |
|
||||
| 3 | announcements | `announcements/` | ✅ | ✅ | ✅ | ✅ | ✅ | 通知公告:学校/年级/班级三级公告发布与查看 |
|
||||
| 4 | attendance | `attendance/` | ✅ | ✅ | ✅ | ✅ | | 考勤管理:学生/教师考勤登记、统计、规则配置 |
|
||||
| 5 | audit | `audit/` | ✅ | | | | | 日志审计:操作日志、登录日志、数据变更日志、导出 |
|
||||
| 6 | auth | `auth/` | ✅ | ✅ | ✅ | ✅ | ✅ | 认证系统:登录/注册/JWT/双因素认证 |
|
||||
| 7 | classes | `classes/` | ✅ | ✅ | ✅ | | | 班级管理:班级 CRUD、课表、学生管理、邀请码 |
|
||||
| 8 | course-plans | `course-plans/` | ✅ | ✅ | ✅ | ✅ | | 课程计划:教学计划创建、进度跟踪、日历视图 |
|
||||
| 9 | dashboard | `dashboard/` | ✅ | ✅ | ✅ | ✅ | ✅ | 仪表盘:角色独立看板(统计卡片/图表/快捷操作) |
|
||||
| 10 | diagnostic | `diagnostic/` | | ✅ | ✅ | ✅ | | 学情诊断:知识点掌握度雷达图、班级/个人诊断报告 |
|
||||
| 11 | elective | `elective/` | ✅ | ✅ | ✅ | ✅ | | 选课管理:选修课创建/选课/退选/抽签 |
|
||||
| 12 | error-book | `error-book/` | ✅ | ✅ | ✅ | ✅ | | 错题本:错题自动采集、SM-2 间隔复习、统计分析 |
|
||||
| 13 | exams | `exams/` | | ✅ | | | | 考试管理:AI 出题/组卷/批改/监考/分析 |
|
||||
| 14 | files | `files/` | ✅ | ✅ | ✅ | ✅ | ✅ | 文件管理:上传/预览/存储/权限控制 |
|
||||
| 15 | grades | `grades/` | ✅ | ✅ | ✅ | ✅ | ✅ | 成绩管理:录入/查询/统计/趋势/导出/排名 |
|
||||
| 16 | homework | `homework/` | | ✅ | ✅ | | | 作业管理:布置/提交/批改/评分/统计分析 |
|
||||
| 17 | layout | `layout/` | ✅ | ✅ | ✅ | ✅ | ✅ | 布局框架:导航配置/侧边栏/顶部栏(所有角色共用) |
|
||||
| 18 | lesson-preparation | `lesson-preparation/` | ✅ | ✅ | ✅ | ✅ | | 备课系统:教案创建/编辑/发布/查看 |
|
||||
| 19 | messaging | `messaging/` | ✅ | ✅ | ✅ | ✅ | ✅ | 站内消息:私信/群发/草稿/已读状态 |
|
||||
| 20 | notifications | `notifications/` | ✅ | ✅ | ✅ | ✅ | ✅ | 通知系统:站内/邮件/短信/微信多渠道推送、偏好管理 |
|
||||
| 21 | onboarding | `onboarding/` | ✅ | ✅ | ✅ | ✅ | ✅ | 新手引导:首次登录角色选择、资料填写、班级加入 |
|
||||
| 22 | parent | `parent/` | | | | ✅ | | 家长聚合:子女学习概览、成绩/作业/考勤详情 |
|
||||
| 23 | proctoring | `proctoring/` | | ✅ | | | | 考试监考:实时提交进度、异常行为标记 |
|
||||
| 24 | questions | `questions/` | ✅ | ✅ | | | | 题库管理:题目 CRUD、分类标签、批量导入导出 |
|
||||
| 25 | rbac | `rbac/` | ✅ | | | | | 角色权限:动态角色管理、权限目录、角色-权限分配 |
|
||||
| 26 | scheduling | `scheduling/` | ✅ | ✅ | ✅ | | | 排课系统:自动排课引擎、课表查看、调课/代课 |
|
||||
| 27 | school | `school/` | ✅ | | | | | 学校管理:学校信息/学年学期/年级/班级/部门/学科 |
|
||||
| 28 | settings | `settings/` | ✅ | ✅ | ✅ | ✅ | ✅ | 用户设置:个人信息/通知偏好/外观/安全/登出 |
|
||||
| 29 | standards | `standards/` | (via) | (via) | | | | 课标库:国家/校标/自定义课标,通过 lesson-preparation 使用 |
|
||||
| 30 | student | `student/` | | | ✅ | | | 学生聚合:课程查看/课表/学习路径 |
|
||||
| 31 | textbooks | `textbooks/` | (via) | ✅ | ✅ | | | 教材资源:教材库/章节结构/知识点图谱/内容阅读 |
|
||||
| 32 | users | `users/` | ✅ | | | | | 用户管理:用户 CRUD、批量导入、角色分配 |
|
||||
|
||||
> 注:`✅` = 有独立页面路由;`(via)` = 通过其他模块间接使用,无独立路由页面
|
||||
|
||||
---
|
||||
|
||||
## 二、按角色展开的模块清单
|
||||
|
||||
### 2.1 admin(系统管理员)
|
||||
|
||||
| 模块 | 页面路由 | 权限点 | 功能说明 |
|
||||
|------|----------|--------|----------|
|
||||
| **school** | `/admin/school` | `SCHOOL_MANAGE` | 学校基础信息配置 |
|
||||
| | `/admin/school/schools` | `SCHOOL_MANAGE` | 多校区管理 |
|
||||
| | `/admin/school/academic-year` | `SCHOOL_MANAGE` | 学年学期管理 |
|
||||
| | `/admin/school/classes` | `SCHOOL_MANAGE` | 班级创建与管理 |
|
||||
| | `/admin/school/departments` | `SCHOOL_MANAGE` | 部门管理 |
|
||||
| | `/admin/school/grades` | `SCHOOL_MANAGE` | 年级管理与组长指派 |
|
||||
| | `/admin/school/grades/insights` | `SCHOOL_MANAGE` | 年级洞察分析 |
|
||||
| **users** | `/admin/users` | `USER_MANAGE` | 用户账号管理 |
|
||||
| | `/admin/users/import` | `USER_MANAGE` | 批量导入用户 |
|
||||
| **rbac** | `/admin/roles` | `ROLE_READ` | 角色列表查看 |
|
||||
| | `/admin/roles/[id]` | `ROLE_READ` | 角色详情与权限编辑 |
|
||||
| | `/admin/permissions` | `PERMISSION_READ` | 权限点目录查看 |
|
||||
| **announcements** | `/admin/announcements` | `ANNOUNCEMENT_MANAGE` | 公告管理(创建/编辑/删除) |
|
||||
| | `/admin/announcements/[id]` | `ANNOUNCEMENT_MANAGE` | 公告详情 |
|
||||
| | `/admin/announcements/[id]/edit` | `ANNOUNCEMENT_MANAGE` | 编辑公告 |
|
||||
| **audit** | `/admin/audit-logs` | `AUDIT_LOG_READ` | 操作日志查看 |
|
||||
| | `/admin/audit-logs/overview` | `AUDIT_LOG_READ` | 审计概览统计 |
|
||||
| | `/admin/audit-logs/data-changes` | `AUDIT_LOG_READ` | 数据变更日志 |
|
||||
| | `/admin/audit-logs/login-logs` | `AUDIT_LOG_READ` | 登录日志 |
|
||||
| **dashboard** | `/admin/dashboard` | `DASHBOARD_ADMIN_READ` | 管理员仪表盘 |
|
||||
| **scheduling** | `/admin/scheduling/auto` | `SCHEDULE_AUTO` | 自动排课 |
|
||||
| | `/admin/scheduling/changes` | `SCHEDULE_ADJUST` | 调课管理 |
|
||||
| | `/admin/scheduling/rules` | `SCHEDULE_ADJUST` | 排课规则配置 |
|
||||
| **course-plans** | `/admin/course-plans` | `COURSE_PLAN_READ` | 课程计划查看 |
|
||||
| | `/admin/course-plans/create` | `COURSE_PLAN_MANAGE` | 创建课程计划 |
|
||||
| | `/admin/course-plans/[id]` | `COURSE_PLAN_READ` | 课程计划详情 |
|
||||
| | `/admin/course-plans/[id]/edit` | `COURSE_PLAN_MANAGE` | 编辑课程计划 |
|
||||
| **elective** | `/admin/elective` | `ELECTIVE_READ` | 选课管理 |
|
||||
| | `/admin/elective/create` | `ELECTIVE_MANAGE` | 创建选修课 |
|
||||
| | `/admin/elective/[id]` | `ELECTIVE_READ` | 选修课详情 |
|
||||
| | `/admin/elective/[id]/edit` | `ELECTIVE_MANAGE` | 编辑选修课 |
|
||||
| **attendance** | `/admin/attendance` | `ATTENDANCE_READ` | 考勤数据查看 |
|
||||
| **error-book** | `/admin/error-book` | `ERROR_BOOK_ANALYTICS_READ` | 全校错题统计分析 |
|
||||
| **questions** | `/admin/questions` | `QUESTION_READ` | 题库管理 |
|
||||
| **files** | `/admin/files` | `FILE_READ` | 文件管理 |
|
||||
| **ai** | `/admin/ai-settings` | `AI_CHAT` | AI 多模型配置 |
|
||||
| **lesson-plans** | `/admin/lesson-plans` | `LESSON_PLAN_READ` | 教案查看 |
|
||||
| | `/admin/lesson-plans/[planId]/view` | `LESSON_PLAN_READ` | 教案详情 |
|
||||
| **settings** | `/admin/settings` | `SETTINGS_ADMIN` | 管理员设置 |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 teacher(教师)
|
||||
|
||||
| 模块 | 页面路由 | 权限点 | 功能说明 |
|
||||
|------|----------|--------|----------|
|
||||
| **dashboard** | `/teacher/dashboard` | `DASHBOARD_TEACHER_READ` | 教师仪表盘(待批改/今日课表/班级动态) |
|
||||
| **classes** | `/teacher/classes/my` | `CLASS_READ` | 我的班级列表 |
|
||||
| | `/teacher/classes/my/[id]` | `CLASS_READ` | 班级详情 |
|
||||
| | `/teacher/classes/schedule` | `CLASS_READ` | 班级课表 |
|
||||
| | `/teacher/classes/students` | `CLASS_READ` | 班级学生管理 |
|
||||
| **exams** | `/teacher/exams` | `EXAM_READ` | 考试列表 |
|
||||
| | `/teacher/exams/all` | `EXAM_READ` | 全部考试 |
|
||||
| | `/teacher/exams/create` | `EXAM_CREATE` | 创建考试 |
|
||||
| | `/teacher/exams/new` | `EXAM_CREATE` | AI 出题 |
|
||||
| | `/teacher/exams/[id]/build` | `EXAM_READ` | 组卷 |
|
||||
| | `/teacher/exams/[id]/edit-rich` | `EXAM_UPDATE` | 富文本编辑试卷 |
|
||||
| | `/teacher/exams/[id]/analytics` | `EXAM_READ` | 考试分析 |
|
||||
| | `/teacher/exams/[id]/proctoring` | `EXAM_PROCTOR` | 考试监考 |
|
||||
| | `/teacher/exams/grading` | `HOMEWORK_GRADE` | 批改列表 |
|
||||
| | `/teacher/exams/grading/[submissionId]` | `HOMEWORK_GRADE` | 批改详情 |
|
||||
| **homework** | `/teacher/homework/assignments` | `HOMEWORK_CREATE` | 作业管理 |
|
||||
| | `/teacher/homework/assignments/create` | `HOMEWORK_CREATE` | 布置作业 |
|
||||
| | `/teacher/homework/assignments/[id]` | `HOMEWORK_CREATE` | 作业详情 |
|
||||
| | `/teacher/homework/assignments/[id]/submissions` | `HOMEWORK_GRADE` | 提交列表 |
|
||||
| | `/teacher/homework/submissions` | `HOMEWORK_GRADE` | 批改列表 |
|
||||
| | `/teacher/homework/submissions/[submissionId]` | `HOMEWORK_GRADE` | 批改详情 |
|
||||
| | `/teacher/homework/submissions/[submissionId]/scan-grading` | `HOMEWORK_GRADE` | 阅卷式批改 |
|
||||
| **grades** | `/teacher/grades` | `GRADE_RECORD_READ` | 成绩查询 |
|
||||
| | `/teacher/grades/analytics` | `GRADE_RECORD_READ` | 成绩分析 |
|
||||
| | `/teacher/grades/entry` | `GRADE_RECORD_MANAGE` | 成绩录入 |
|
||||
| | `/teacher/grades/stats` | `GRADE_RECORD_READ` | 成绩统计 |
|
||||
| **questions** | `/teacher/questions` | `QUESTION_READ` | 题库管理 |
|
||||
| **textbooks** | `/teacher/textbooks` | `TEXTBOOK_READ` | 教材查看 |
|
||||
| | `/teacher/textbooks/[id]` | `TEXTBOOK_READ` | 教材内容 |
|
||||
| **lesson-plans** | `/teacher/lesson-plans` | `LESSON_PLAN_READ` | 教案列表 |
|
||||
| | `/teacher/lesson-plans/new` | `LESSON_PLAN_READ` | 创建教案 |
|
||||
| | `/teacher/lesson-plans/[planId]/edit` | `LESSON_PLAN_READ` | 编辑教案 |
|
||||
| **diagnostic** | `/teacher/diagnostic` | `DIAGNOSTIC_READ` | 学情诊断总览 |
|
||||
| | `/teacher/diagnostic/class/[classId]` | `DIAGNOSTIC_READ` | 班级诊断 |
|
||||
| | `/teacher/diagnostic/student/[studentId]` | `DIAGNOSTIC_READ` | 学生诊断 |
|
||||
| **attendance** | `/teacher/attendance` | `ATTENDANCE_READ` | 考勤查看 |
|
||||
| | `/teacher/attendance/sheet` | `ATTENDANCE_MANAGE` | 考勤登记 |
|
||||
| | `/teacher/attendance/stats` | `ATTENDANCE_READ` | 考勤统计 |
|
||||
| **course-plans** | `/teacher/course-plans` | `COURSE_PLAN_READ` | 课程计划 |
|
||||
| | `/teacher/course-plans/[id]` | `COURSE_PLAN_READ` | 计划详情 |
|
||||
| **elective** | `/teacher/elective` | `ELECTIVE_READ` | 选课管理 |
|
||||
| | `/teacher/elective/create` | `ELECTIVE_MANAGE` | 创建选修课 |
|
||||
| | `/teacher/elective/[id]/edit` | `ELECTIVE_MANAGE` | 编辑选修课 |
|
||||
| **error-book** | `/teacher/error-book` | `ERROR_BOOK_ANALYTICS_READ` | 班级错题分析 |
|
||||
| **practice** | `/teacher/practice` | `ADAPTIVE_PRACTICE_READ` | 专项练习统计 |
|
||||
| **schedule** | `/teacher/schedule-changes` | `SCHEDULE_ADJUST` | 调课申请 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 student(学生)
|
||||
|
||||
| 模块 | 页面路由 | 权限点 | 功能说明 |
|
||||
|------|----------|--------|----------|
|
||||
| **dashboard** | `/student/dashboard` | `DASHBOARD_STUDENT_READ` | 学生仪表盘(作业/考试/成绩趋势) |
|
||||
| **learning** | `/student/learning` | `CLASS_READ` | 学习中心 |
|
||||
| | `/student/learning/assignments` | — | 作业列表 |
|
||||
| | `/student/learning/assignments/[assignmentId]` | — | 作答 |
|
||||
| | `/student/learning/assignments/[assignmentId]/result` | — | 作答结果 |
|
||||
| | `/student/learning/courses` | `CLASS_READ` | 课程列表 |
|
||||
| | `/student/learning/courses/[classId]` | `CLASS_READ` | 课程详情 |
|
||||
| | `/student/learning/textbooks` | `TEXTBOOK_READ` | 教材 |
|
||||
| | `/student/learning/textbooks/[id]` | `TEXTBOOK_READ` | 教材内容 |
|
||||
| | `/student/learning/study-path` | `AI_CHAT` | AI 学习路径 |
|
||||
| **grades** | `/student/grades` | `GRADE_RECORD_READ` | 成绩查询 |
|
||||
| **error-book** | `/student/error-book` | `ERROR_BOOK_READ` | 我的错题本 |
|
||||
| **practice** | `/student/practice` | `ADAPTIVE_PRACTICE_READ` | 专项练习 |
|
||||
| | `/student/practice/[sessionId]` | `ADAPTIVE_PRACTICE_READ` | 练习对话 |
|
||||
| **diagnostic** | `/student/diagnostic` | `DIAGNOSTIC_READ` | 学情诊断 |
|
||||
| **elective** | `/student/elective` | `ELECTIVE_READ` | 选课 |
|
||||
| | `/student/elective/[id]` | `ELECTIVE_READ` | 课程详情 |
|
||||
| **schedule** | `/student/schedule` | `CLASS_READ` | 课表查看 |
|
||||
| **attendance** | `/student/attendance` | `ATTENDANCE_READ` | 考勤查询 |
|
||||
| **course-plans** | `/student/course-plans` | `COURSE_PLAN_READ` | 课程计划 |
|
||||
| | `/student/course-plans/[id]` | `COURSE_PLAN_READ` | 计划详情 |
|
||||
| **lesson-plans** | `/student/lesson-plans` | `LESSON_PLAN_READ` | 教案查看 |
|
||||
| | `/student/lesson-plans/[planId]/view` | `LESSON_PLAN_READ` | 教案详情 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 parent(家长)
|
||||
|
||||
| 模块 | 页面路由 | 权限点 | 功能说明 |
|
||||
|------|----------|--------|----------|
|
||||
| **dashboard** | `/parent/dashboard` | `DASHBOARD_PARENT_READ` | 家长仪表盘(子女学习概况) |
|
||||
| **children** | `/parent/children/[studentId]` | — | 子女详情聚合页 |
|
||||
| **grades** | `/parent/grades` | `GRADE_RECORD_READ` | 子女成绩查询 |
|
||||
| **error-book** | `/parent/error-book` | `ERROR_BOOK_READ` | 子女错题本 |
|
||||
| **diagnostic** | `/parent/diagnostic` | `DIAGNOSTIC_READ` | 子女学情诊断 |
|
||||
| **elective** | `/parent/elective` | `ELECTIVE_READ` | 子女选课查看 |
|
||||
| **attendance** | `/parent/attendance` | `ATTENDANCE_READ` | 子女考勤查询 |
|
||||
| **course-plans** | `/parent/course-plans` | `COURSE_PLAN_READ` | 课程计划 |
|
||||
| | `/parent/course-plans/[id]` | `COURSE_PLAN_READ` | 计划详情 |
|
||||
| **lesson-plans** | `/parent/lesson-plans` | `LESSON_PLAN_READ` | 教案查看 |
|
||||
| | `/parent/lesson-plans/[planId]/view` | `LESSON_PLAN_READ` | 教案详情 |
|
||||
| **practice** | `/parent/practice` | `ADAPTIVE_PRACTICE_READ` | 子女练习记录 |
|
||||
| **leave** | `/parent/leave` | — | 请假申请 |
|
||||
|
||||
---
|
||||
|
||||
### 2.5 management(年级组长/教研组长)
|
||||
|
||||
| 模块 | 页面路由 | 权限点 | 功能说明 |
|
||||
|------|----------|--------|----------|
|
||||
| **grade** | `/management/grade` | `GRADE_MANAGE` | 年级管理首页 |
|
||||
| | `/management/grade/dashboard` | `GRADE_RECORD_READ` | 年级仪表盘 |
|
||||
| | `/management/grade/insights` | `GRADE_RECORD_READ` | 年级洞察分析 |
|
||||
| | `/management/grade/classes` | `GRADE_MANAGE` | 年级班级管理 |
|
||||
| | `/management/grade/practice` | `ADAPTIVE_PRACTICE_READ` | 年级练习统计 |
|
||||
|
||||
---
|
||||
|
||||
## 三、共享模块(所有角色可访问)
|
||||
|
||||
以下模块为所有已登录用户提供服务,不区分角色路由:
|
||||
|
||||
| 模块 | 路由 | 权限点 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| **auth** | `/login`, `/register` | 公开 | 认证(登录/注册) |
|
||||
| **onboarding** | `/onboarding` | 已登录 | 新手引导(首次登录) |
|
||||
| **announcements** | `/announcements`, `/announcements/[id]` | `ANNOUNCEMENT_READ` | 公告查看(所有角色) |
|
||||
| **messages** | `/messages`, `/messages/[id]`, `/messages/compose` | `MESSAGE_READ` / `MESSAGE_SEND` | 站内消息 |
|
||||
| **profile** | `/profile` | `USER_PROFILE_UPDATE` | 个人资料 |
|
||||
| **settings** | `/settings` | `USER_PROFILE_UPDATE` | 用户设置(通知/外观/安全) |
|
||||
| **dashboard** | `/dashboard` | 角色自动路由 | 仪表盘(自动重定向到角色对应仪表盘) |
|
||||
| **layout** | 所有路由 | — | 全局布局框架(侧边栏/顶部栏/导航配置) |
|
||||
| **notifications** | 全局组件 | — | 通知弹窗/偏好管理(内嵌于 layout) |
|
||||
| **files** | 全局上传 | `FILE_UPLOAD` | 文件上传(所有角色可上传,admin 可管理) |
|
||||
|
||||
---
|
||||
|
||||
## 四、权限点汇总(67 个)
|
||||
|
||||
| 权限分类 | 权限点 | 适用角色 |
|
||||
|----------|--------|----------|
|
||||
| **考试** | `exam:create/create/read/update/delete/duplicate/publish/ai_generate/submit` | teacher |
|
||||
| | `exam:proctor/read` | teacher |
|
||||
| **作业** | `homework:create/grade` | teacher |
|
||||
| | `homework:submit` | student |
|
||||
| **题库** | `question:create/read/update/delete` | admin, teacher |
|
||||
| **教材** | `textbook:create/read/update/delete` | admin, teacher |
|
||||
| **班级** | `class:create/read/update/delete/enroll/schedule` | admin, teacher, student |
|
||||
| **学校管理** | `school:manage` | admin |
|
||||
| | `grade:manage` | admin, grade_head |
|
||||
| | `user:manage` | admin |
|
||||
| **成绩** | `grade_record:manage/read` | admin, teacher, student, parent |
|
||||
| **考勤** | `attendance:manage/read` | admin, teacher, student, parent |
|
||||
| **课程计划** | `course_plan:manage/read` | admin, teacher, student, parent |
|
||||
| **公告** | `announcement:manage` | admin |
|
||||
| | `announcement:read` | all |
|
||||
| **消息** | `message:send/read/delete` | all |
|
||||
| **排课** | `schedule:auto/adjust` | admin, teacher |
|
||||
| **选课** | `elective:manage/read/select` | admin, teacher, student, parent |
|
||||
| **诊断** | `diagnostic:manage/read` | admin, teacher, student, parent |
|
||||
| **备课** | `lesson_plan:create/read/update/delete/publish` | admin, teacher, student, parent |
|
||||
| **仪表盘** | `dashboard:admin_read/teacher_read/student_read/parent_read` | 各角色独立 |
|
||||
| **错题本** | `error_book:read/manage` | student, parent |
|
||||
| | `error_book:analytics_read` | admin, teacher |
|
||||
| **专项练习** | `adaptive_practice:read/manage` | student, teacher, parent, grade_head |
|
||||
| **AI** | `ai:chat` | all |
|
||||
| | `ai:configure` | admin |
|
||||
| **文件** | `file:upload/read/delete` | all(upload),admin(delete) |
|
||||
| **审计** | `audit_log:read` | admin |
|
||||
| **RBAC** | `role:create/read/update/delete/assign` | admin |
|
||||
| | `permission:read` | admin |
|
||||
| **设置** | `settings:admin` | admin |
|
||||
| | `user:profile_update` | all |
|
||||
|
||||
---
|
||||
|
||||
## 五、模块依赖关系速查
|
||||
|
||||
| 模块 | 依赖模块(通过 data-access) | 被依赖模块 |
|
||||
|------|------------------------------|-----------|
|
||||
| **exams** | questions, classes, school, homework | homework, dashboard, proctoring, diagnostic, grades |
|
||||
| **homework** | exams, classes, school, users | grades, dashboard, diagnostic |
|
||||
| **grades** | exams, classes, school, users | dashboard, parent |
|
||||
| **classes** | homework, scheduling | exams, homework, grades, scheduling, attendance, dashboard |
|
||||
| **dashboard** | users, classes, textbooks, questions, exams, homework | — |
|
||||
| **parent** | classes, homework, grades | — |
|
||||
| **diagnostic** | exams, questions, classes, users | — |
|
||||
| **lesson-preparation** | textbooks, questions, exams, homework, classes, files | — |
|
||||
| **messaging** | notifications | settings |
|
||||
| **scheduling** | classes, users | — |
|
||||
| **proctoring** | exams, users | — |
|
||||
| **school** | — | exams, classes, grades, scheduling, attendance |
|
||||
| **textbooks** | — | questions, exams, homework, lesson-preparation |
|
||||
| **users** | — | all modules |
|
||||
| **rbac** | — | admin |
|
||||
| **audit** | — | all modules (via shared/audit-logger) |
|
||||
|
||||
---
|
||||
|
||||
## 六、维护说明
|
||||
|
||||
- 新增模块时,需同步更新本文档的模块-角色速查表(第一节)和对应角色的详细清单(第二节)
|
||||
- 新增权限点时,需同步更新权限点汇总(第四节)
|
||||
- 模块依赖关系变更时,需同步更新依赖关系速查(第五节)
|
||||
- 路由变更时,需同步更新对应角色的路由表
|
||||
4901
docs/architecture/audit/archive/004_architecture_impact_map_v1.md
Normal file
10
docs/architecture/audit/archive/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 历史审查报告归档
|
||||
|
||||
> 本目录为只读归档,不再更新。
|
||||
> 有价值的内容已提取到模块 README 和 known-issues.md。
|
||||
|
||||
## 归档文件
|
||||
|
||||
- 005_architecture_data.json(已废弃,由 arch.db 替代)
|
||||
- 60+ 份模块审查报告(历史参考)
|
||||
- data-access-audit-v1 系列文件(数据访问层审查)
|
||||
@@ -0,0 +1,815 @@
|
||||
# 专项练习(adaptive-practice)模块审计报告
|
||||
|
||||
> **审计日期**:2026-06-25
|
||||
> **审计范围**:`src/modules/adaptive-practice/` 全部文件 + `src/app/(dashboard)/{student,teacher,management/grade}/practice/` 路由 + 跨模块集成点(error-book)
|
||||
> **对标系统**:Khan Academy、IXL Learning、DreamBox、Smartick、学而思网校、猿辅导、作业帮、超星学习通、ClassIn、智学网
|
||||
> **前置文档**:[004 架构影响地图](../004_architecture_impact_map.md#230-adaptive-practice专项练习模块-核心教学链路闭环)、[005 架构数据](../005_architecture_data.json)
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布与代码量
|
||||
|
||||
| 文件 | 行数 | 职责 | 规范符合性 |
|
||||
|------|------|------|-----------|
|
||||
| [types.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/types.ts) | 143 | 联合类型定义(PracticeType/PracticeSourceMeta 等) | ✅ |
|
||||
| [schema.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/schema.ts) | 74 | Zod 输入验证 | ✅ |
|
||||
| [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) | 616 | 学生端 CRUD + 自动判分 | ✅ ≤800 |
|
||||
| [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) | 343 | 四种出题策略 | ✅ |
|
||||
| [data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-analytics.ts) | 634 | 教师/年级宏观数据分析 | ⚠️ 接近 800,建议拆分 |
|
||||
| [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) | 267 | 7 个 Server Actions | ✅ |
|
||||
| [components/practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) | 263 | 练习发起器 | ✅ |
|
||||
| [components/practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) | 550 | 答题界面(含 QuestionCard/AnswerInput/AnswerResult/PracticeResultView) | ⚠️ 超 500,需拆分 |
|
||||
| [components/practice-history.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-history.tsx) | 95 | 练习历史列表 | ✅ |
|
||||
| [components/practice-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-stats-cards.tsx) | 73 | 学生端统计卡片 | ✅ |
|
||||
| [components/practice-overview-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-overview-stats-cards.tsx) | 99 | 教师/年级统计卡片 | ✅ |
|
||||
| [components/class-practice-comparison-table.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/class-practice-comparison-table.tsx) | 88 | 班级对比表 | ✅ |
|
||||
| [components/practice-type-breakdown-chart.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx) | 123 | 类型分布柱状图 | ✅ |
|
||||
| [components/class-knowledge-point-weakness-chart.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx) | 144 | 知识点薄弱度柱状图 | ✅ |
|
||||
| [components/student-practice-ranking-table.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/student-practice-ranking-table.tsx) | 112 | 学生排名表 | ✅ |
|
||||
| [components/inactive-students-alert.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/inactive-students-alert.tsx) | 64 | 未参与学生提醒 | ✅ |
|
||||
|
||||
### 1.2 路由分布
|
||||
|
||||
| 路由 | 文件 | 角色 |
|
||||
|------|------|------|
|
||||
| `/student/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | student |
|
||||
| `/student/practice/[sessionId]` | `page.tsx` + `loading.tsx` + `error.tsx` | student |
|
||||
| `/teacher/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | teacher / grade_head / teaching_head |
|
||||
| `/management/grade/practice` | `page.tsx` + `loading.tsx` + `error.tsx` | grade_head / teaching_head |
|
||||
| ❌ `/parent/practice` | **缺失** | parent(有 `ADAPTIVE_PRACTICE_READ` 权限但无页面) |
|
||||
|
||||
### 1.3 数据流与依赖关系
|
||||
|
||||
**模块内**:`app/page.tsx` → `modules/adaptive-practice/{actions, data-access, data-access-analytics}` → `shared/{db, lib/auth-guard, types}`
|
||||
|
||||
**跨模块**:
|
||||
- `modules/adaptive-practice/data-access-analytics.ts` → `modules/classes/data-access`(getActiveStudentIdsByClassId / getClassNameById / getClassesByGradeId / getClassIdsByGradeIds / getStudentIdsByClassIds)✅ 合规
|
||||
- `modules/adaptive-practice/data-access-analytics.ts` → `modules/users/data-access`(getUserIdsByGradeId / getUserNamesByIds)✅ 合规
|
||||
- `app/(dashboard)/student/error-book/student-error-book-list-client.tsx` → `modules/adaptive-practice/actions`(createPracticeSessionAction)✅ app 层组合合规
|
||||
- `app/(dashboard)/teacher/practice/page.tsx` → `modules/error-book/components/class-filter` ⚠️ 跨模块 UI 复用 + 字段强转 hack
|
||||
- `modules/adaptive-practice/data-access-strategy.ts` → `shared/db/schema`(questions / questionsToKnowledgePoints / knowledgePointMastery / practiceAnswers)✅ 合规
|
||||
|
||||
### 1.4 架构图同步状态
|
||||
|
||||
`004_architecture_impact_map.md` §2.30 与 `005_architecture_data.json` 的 `adaptivePractice` 节点已完整覆盖:
|
||||
- DB Schema(practiceSessions / practiceAnswers)、Server Actions(7 个)、Data Access、4 种出题策略、教师/年级宏观数据分析
|
||||
- 依赖矩阵、权限点、DataScope 行级权限、自动判分规则
|
||||
|
||||
**架构图遗漏**:
|
||||
- ❌ 未记录 `parent` 角色路由(因为页面本身缺失,架构图未列出 `/parent/practice`)
|
||||
- ❌ 未记录 `error-book` 模块通过 `onStartVariantPractice` props 注入的解耦关系
|
||||
- ❌ 未记录 `practice-starter.tsx`、`practice-session-view.tsx`、`practice-history.tsx` 等组件的 props 接口
|
||||
- ❌ 未记录 `identifyWeakKnowledgePoints` 这个未被调用的导出函数
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 架构与耦合问题
|
||||
|
||||
#### P0-1 跨模块 UI 复用通过字段强转 hack 实现
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) L31-32, L108-116 |
|
||||
| 问题 | 教师练习分析页直接 import `@/modules/error-book/components/class-filter` 和 `@/modules/error-book/types`,并通过字段重命名把 `TeacherClassPracticeOverview` 强转为 `ClassErrorOverview`:`totalErrorItems ← totalSessions`、`averageMasteryRate ← averageAccuracy`、`dueReviewCount: 0`。语义完全错位,"练习数"被当成"错题数"展示。 |
|
||||
| 规则 | 违反"模块间只能通过对方 data-access 通信"和"避免 `as` 断言"。 |
|
||||
| 后果 | 任何一方修改 `ClassErrorOverview` 或 `ClassFilter` 字段都会破坏练习分析页;筛选器 tooltip 显示"错题数"误导用户。 |
|
||||
|
||||
#### P0-2 PracticeStarter 硬编码路由跳转与 Action 直调
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L20, L82, L129 |
|
||||
| 问题 | 组件直接 `import { createPracticeSessionAction } from "../actions"` 并 `router.push("/student/practice/${sessionId}")`。组件无法被其他角色(如 parent 监督子女练习、teacher 课堂演示)复用。 |
|
||||
| 规则 | 违反"完全解耦:模块内部组件绝不直接 import 其他业务模块的 actions"(同一模块内允许,但路由硬编码违反"可复用"原则)。 |
|
||||
| 后果 | 组件无法跨角色复用;测试需要 mock 整个 Action 模块;路由变更需改组件。 |
|
||||
|
||||
#### P0-3 PracticeSessionView 单文件 550 行,承担 5 个组件职责
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) |
|
||||
| 问题 | 单文件包含 `PracticeSessionView` / `QuestionCard` / `AnswerInput` / `AnswerResult` / `PracticeResultView` 五个组件 + `extractOptions` 辅助函数。`AnswerInput` 内部对 4 种题型的渲染逻辑高度相似却重复编写。 |
|
||||
| 规则 | 违反"React 组件建议 ≤ 500 行"和"最大化复用:识别共用 UI 块抽象为泛型组件"。 |
|
||||
| 后果 | 难以单测、难以独立复用 `QuestionCard`(例如在错题本详情弹窗中预览变式题)。 |
|
||||
|
||||
### 2.2 权限与安全问题
|
||||
|
||||
#### P0-4 后端 completePracticeSession 不校验是否全部题已答
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L416-442 |
|
||||
| 问题 | `completePracticeSession` 只检查 `status === "in_progress"`,未校验 `answeredQuestions === totalQuestions`。前端 `disabled={isPending \|\| answeredCount < total}` 可被绕过,恶意用户可提交未答完的会话为"已完成",污染统计。 |
|
||||
| 规则 | 违反"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤,Server Action 二次校验"。 |
|
||||
| 后果 | 统计数据失真,影响教师宏观数据分析。 |
|
||||
|
||||
#### P0-5 submitPracticeAnswer 不防并发提交
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L320-410 |
|
||||
| 问题 | 检查 `answerRecord.status === "answered"` 后再更新,但中间无事务/行锁。学生快速双击提交按钮可绕过检查,导致同一题被二次判分,`updateSessionStats` 重复累加 `answeredQuestions` 和 `correctCount`。 |
|
||||
| 规则 | 违反"安全性:Server Action 二次校验"。 |
|
||||
| 后果 | 统计数据被双重累加,正确率失真。 |
|
||||
|
||||
#### P0-6 getPracticeSessionsAction 未校验 studentId 与 ctx 的强一致性
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L31-56 |
|
||||
| 问题 | 当 `ctx.dataScope.type === "all"`(admin)时,可传任意 studentId 查询;admin 角色确实可查任意学生,但 audit 模块规则要求"权限校验需要 parentId 和 studentId 双重校验"。教师角色 `dataScope.type === "class_taught"` 时,未校验 studentId 是否在所教班级学生中,**任何登录教师可查询任意学生练习数据**(只要把 studentId 直接传给 action)。 |
|
||||
| 规则 | 违反"Parent routes must include permission checks with both parentId and studentId to prevent information leakage"。 |
|
||||
| 后果 | 教师越权查看非本班学生练习记录,数据泄露。 |
|
||||
|
||||
### 2.3 国际化遗漏(i18n)
|
||||
|
||||
#### P0-7 error.tsx 大量硬编码中文
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [teacher/practice/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/error.tsx) L16-23, [management/grade/practice/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/management/grade/practice/error.tsx) L16-23 |
|
||||
| 问题 | "专项练习分析"、"加载练习分析数据时发生错误"、"加载失败"、"请刷新页面重试..."、"年级专项练习总览"、"加载年级练习数据时发生错误" 全部硬编码中文。 |
|
||||
| 规则 | 违反"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"。 |
|
||||
| 后果 | 英文环境下显示中文,破坏国际化。 |
|
||||
|
||||
#### P0-8 actions.ts 错误消息硬编码中文
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L53, L77, L129, L137, L154, L161, L178, L184, L204, L235, L263 |
|
||||
| 问题 | Server Action 返回的 `message` 字段硬编码中文:"获取练习列表失败"、"练习会话不存在或无权访问"、"提交格式错误"、"输入验证失败"、"未找到符合条件的题目"、"已创建练习会话"、"已跳过此题"、"回答正确"、"回答错误"、"练习已完成"、"练习已放弃" 等。这些消息通过 ActionState 返回到前端 toast 展示给用户。 |
|
||||
| 规则 | 违反"所有用户可见文本必须适配 i18n"。 |
|
||||
| 后果 | 英文用户看到中文 toast。 |
|
||||
|
||||
#### P0-9 data-access.ts 抛出错误消息硬编码中文
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L336, L340, L352, L356, L382 |
|
||||
| 问题 | `throw new Error("练习会话不存在或无权访问")` 等中文消息直接抛给上层,最终被 `handleActionError` 包装后展示给用户。 |
|
||||
| 规则 | 违反"所有用户可见文本必须适配 i18n"。 |
|
||||
| 后果 | 与 P0-8 同。 |
|
||||
|
||||
#### P0-10 业务数据写入翻译文本
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L113-117 |
|
||||
| 问题 | `sourceMeta = { recommendedKnowledgePointIds, reason: t("toasts.aiRecommendedReason") }` —— 把翻译文本作为业务数据写入数据库 `practice_sessions.source_meta.reason` 字段。语言切换后历史记录的 reason 不一致;数据库存储多语言文本违反数据归一化。 |
|
||||
| 规则 | 违反"业务数据不应包含翻译文本"(架构规范)。 |
|
||||
| 后果 | 数据库冗余、语言切换不一致、跨语言环境数据污染。 |
|
||||
|
||||
### 2.4 类型安全问题
|
||||
|
||||
#### P1-1 data-access.ts 多处 `as` 类型断言绕过严格模式
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L38 `row.practiceType as PracticeType`、L39 `row.status as PracticeStatus`、L60 `row.status as PracticeAnswerStatus`、L174 `session.sourceMeta as PracticeSourceMeta \| null`、L217 `practiceType: type as PracticeType` |
|
||||
| 问题 | 从 DB 取出的 enum 字段直接 `as` 断言,未通过类型守卫校验。如果 DB 数据被脏写(如手工改库),运行时会把无效值当作合法值处理。 |
|
||||
| 规则 | 违反"禁止 `as` 断言(除类型收窄外)",且"未知类型用 `unknown` 并做类型守卫"。 |
|
||||
| 后果 | 类型系统失效,潜在运行时错误。 |
|
||||
|
||||
#### P1-2 actions.ts L144 双重 as 断言
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [actions.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/actions.ts) L144 `parsed.data.sourceMeta as unknown as PracticeSourceMeta` |
|
||||
| 问题 | `z.record(z.string(), z.unknown())` 返回 `Record<string, unknown>`,通过 `as unknown as` 双重断言绕过类型系统。Zod schema 没有按 PracticeSourceMeta 联合类型做判别式校验。 |
|
||||
| 规则 | 违反"禁止 `as` 断言"。 |
|
||||
| 后果 | 客户端可构造任意结构的 sourceMeta 写入数据库,data-access-strategy 中的类型守卫只检查 key 存在性,不检查 value 类型。 |
|
||||
|
||||
#### P1-3 data-access-strategy.ts 类型守卫不充分
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L325-343 |
|
||||
| 问题 | 类型守卫仅检查 `"errorBookItemIds" in meta` 等 key 存在性,不验证 `errorBookItemIds` 是 `string[]`、`sourceQuestionIds` 是 `string[]`。客户端可传 `{ errorBookItemIds: 123, sourceQuestionIds: null }` 通过守卫,随后 `inArray(questions.id, sourceQuestionIds)` 抛 SQL 错误。 |
|
||||
| 规则 | 违反"未知类型用 `unknown` 并做类型守卫"。 |
|
||||
| 后果 | SQL 异常泄露内部信息。 |
|
||||
|
||||
#### P1-4 practice-session-view.tsx L360 数组断言
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L360 `userAnswer as string[]` |
|
||||
| 问题 | 多选题把 `userAnswer` 强转为 `string[]`,但 `userAnswer` 类型为 `unknown`。 |
|
||||
| 规则 | 违反"禁止 `as` 断言"。 |
|
||||
| 后果 | 类型不安全。 |
|
||||
|
||||
### 2.5 业务逻辑缺陷
|
||||
|
||||
#### P0-11 "错题变式"策略实际不做变式
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L40-70 |
|
||||
| 问题 | 函数名 `selectForErrorVariant`,类型定义 `QuestionSelectionResult.variants: Map<string, unknown>`,但实现直接查询原题返回,`variants: new Map()` 始终为空。注释 L34 写明"不依赖 AI 生成变式题",但**对外仍以"错题变式"命名**,UI 上展示为"错题变式"练习类型。功能与名称严重不符。 |
|
||||
| 规则 | 违反"组件必须为纯函数,使用 `function` 声明"中的语义诚实原则。 |
|
||||
| 后果 | 用户期待"变式题"实际是原题重做,体验落差;与 AI 模块定义的 `AiQuestionVariantGenerator` 能力割裂。 |
|
||||
|
||||
#### P0-12 "薄弱章节"策略未自动识别薄弱知识点
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L135-180, L298-319 |
|
||||
| 问题 | `selectForWeakChapter` 接收 `sourceMeta.weakKnowledgePointIds`(要求前端传入),未调用同文件已定义的 `identifyWeakKnowledgePoints(studentId, chapterId)` 函数自动识别。`WeakChapterSourceMeta.chapterId` 字段定义了但策略中未使用。用户必须先在另一处查看薄弱知识点再手动选择,体验割裂。 |
|
||||
| 规则 | 违反"组合优先:逻辑复用一律抽取为自定义 hooks"和"最大化复用"。 |
|
||||
| 后果 | "薄弱章节"功能名不副实;`identifyWeakKnowledgePoints` 成为死代码。 |
|
||||
|
||||
#### P0-13 createPracticeSession 返回空 sessionId 表示失败
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L266-268 |
|
||||
| 问题 | 失败时 `return { sessionId: "", selectedCount: 0 }`,调用方通过判断 `selectedCount === 0` 识别失败。空字符串作为 ID 是反模式,与成功的 `{ sessionId: "xxx", selectedCount: 0 }`(理论上可能)混淆。 |
|
||||
| 规则 | 违反"函数返回值必须显式标注"和"明确处理边界状态"。 |
|
||||
| 后果 | 调用方判断逻辑脆弱;后续重构易引入 bug。 |
|
||||
|
||||
#### P1-5 出题策略使用 `ORDER BY RAND()` 性能差
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L116, L173, L225 |
|
||||
| 问题 | 知识点专项、薄弱章节、AI 推荐三种策略都用 `sql\`RAND()\``。MySQL `ORDER BY RAND()` 在大表上会全表扫描排序,题库上万题时性能急剧下降。 |
|
||||
| 规则 | 违反"性能:优先使用 React Server Components 获取初始数据"。 |
|
||||
| 后果 | 大题库下出题延迟可达数秒。 |
|
||||
|
||||
#### P1-6 getTeacherClassPracticeOverviews / getGradeClassPracticeComparison N+1 查询
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access-analytics.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-analytics.ts) L276-340, L432-496 |
|
||||
| 问题 | `Promise.all(classIds.map(...))` 内部每个班级至少 3 次 DB 查询(getClassNameById + getActiveStudentIdsByClassId + 2 次 practiceSessions 聚合)。10 个班级 = 30 次查询。 |
|
||||
| 规则 | 违反"性能:优先使用 RSC"和工程规范"批量查询应合并"。 |
|
||||
| 后果 | 班级多时延迟累积。 |
|
||||
|
||||
### 2.6 错误处理与边界缺失
|
||||
|
||||
#### P0-14 答题提交失败后无重试机制
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L81-107 |
|
||||
| 问题 | `handleSubmit` 失败仅 `toast.error`,不保留失败状态、不提供重试按钮。学生网络抖动时需要手动重新选择答案再提交,且因为 `setResults` 未更新,UI 上仍显示"未作答"。 |
|
||||
| 规则 | 违反"明确处理空数据、无权限、网络异常等边界状态"。 |
|
||||
| 后果 | 网络异常时学生困惑、流失答题意愿。 |
|
||||
|
||||
#### P0-15 缺少细粒度 React Error Boundary
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) 全文 |
|
||||
| 问题 | 教师分析页一个数据区块失败会导致整页回退到 `error.tsx`。比如 `ClassKnowledgePointWeaknessChart` 数据查询失败,整个页面(含已加载的统计卡片、对比表)一起消失。 |
|
||||
| 规则 | 违反"每个独立的数据区块必须用 React Error Boundary 包裹"。 |
|
||||
| 后果 | 局部错误导致整页不可用。 |
|
||||
|
||||
#### P0-16 缺少流式渲染与骨架屏细粒度
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [teacher/practice/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/practice/page.tsx) L122-133 |
|
||||
| 问题 | `Promise.all([...])` 阻塞所有数据加载完成才渲染任何内容,仅外层 `Suspense` 包裹整页。无区块级 Suspense + 骨架屏。 |
|
||||
| 规则 | 违反"异步数据使用 React Suspense + 骨架屏"和"支持流式渲染"。 |
|
||||
| 后果 | 首屏白屏时间长达数秒。 |
|
||||
|
||||
### 2.7 可测试性缺失
|
||||
|
||||
#### P1-7 自动判分纯函数未导出
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access.ts) L517-616 |
|
||||
| 问题 | `autoGradeAnswer` / `extractChoiceCorrectIds` / `extractJudgmentCorrectAnswer` / `normalizeAnswerToIds` / `normalizeAnswerToBool` 全部为内部函数,未 `export`。无法单测,判分正确性无保障。 |
|
||||
| 规则 | 违反"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离;导出清晰的接口类型以便 mock"。 |
|
||||
| 后果 | 判分 bug 难以回归。 |
|
||||
|
||||
#### P1-8 出题策略未单独导出
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [data-access-strategy.ts](file:///e:/Desktop/CICD/src/modules/adaptive-practice/data-access-strategy.ts) L40-232 |
|
||||
| 问题 | `selectForErrorVariant` / `selectForKnowledgePoint` / `selectForWeakChapter` / `selectForAiRecommended` 均未导出。仅 `selectQuestionsForPractice` 入口可测,无法针对单策略测试。 |
|
||||
| 规则 | 违反"导出清晰的接口类型以便 mock"。 |
|
||||
| 后果 | 策略调整需要端到端测试。 |
|
||||
|
||||
### 2.8 可访问性(a11y)缺失
|
||||
|
||||
#### P1-9 题目内容用 JSON.stringify 展示
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [practice-session-view.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-session-view.tsx) L287-293 |
|
||||
| 问题 | 题目内容若不是字符串,直接 `JSON.stringify(content, null, 2)` 渲染在 `<pre>` 中。学生看到 `{"options":[{"id":"a","text":"..."}]}` 这种 JSON 而非可读题目。 |
|
||||
| 规则 | 违反"a11y:语义化标签、ARIA 属性、键盘导航"。 |
|
||||
| 后果 | 用户体验极差,无法正常答题。 |
|
||||
|
||||
#### P1-10 自定义 checkbox 缺少 aria-label
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | [practice-starter.tsx](file:///e:/Desktop/CICD/src/modules/adaptive-practice/components/practice-starter.tsx) L192-204 |
|
||||
| 问题 | `<input type="checkbox">` 原生元素而非 shadcn `Checkbox`,且无 `aria-label`。屏幕阅读器无法识别知识点名称。 |
|
||||
| 规则 | 违反"a11y:ARIA 属性"。 |
|
||||
| 后果 | 视障用户无法使用。 |
|
||||
|
||||
### 2.9 Parent 角色路由缺失
|
||||
|
||||
#### P0-17 parent 有权限无页面
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 位置 | 缺失 `src/app/(dashboard)/parent/practice/` |
|
||||
| 问题 | `parent` 角色在 `rolePermissions` 中拥有 `ADAPTIVE_PRACTICE_READ`,actions.ts 已实现 `ctx.dataScope.type === "children"` 分支,但无 `/parent/practice` 路由。家长无法查看子女练习记录、统计、错题变式入口。 |
|
||||
| 规则 | 违反"Parent routes must include permission checks with both parentId and studentId"。 |
|
||||
| 后果 | 家长无法监督子女学习,与 parent/error-book 等同级模块功能不对等。 |
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
### 3.1 与 Khan Academy / IXL Learning 的差距
|
||||
|
||||
| 差距项 | 现状 | 行业实践 | 影响 |
|
||||
|--------|------|---------|------|
|
||||
| **掌握度驱动的自适应** | 仅 4 种静态出题策略,掌握度(knowledgePointMastery 表)仅在 weak_chapter 策略中可选用 | Khan Academy 的 Learning Dashboard 持续追踪掌握度,根据答对率自动调整下一题难度(IRT 自适应) | 学生无法获得"刚好难一点"的最近发展区练习 |
|
||||
| **学习路径可视化** | 仅会话列表 + 统计卡片 | Khan Academy 的 World of Math 知识图谱节点着色显示掌握度 | 学生无法看到知识结构全貌,缺乏长期目标感 |
|
||||
| **即时反馈与解析** | 仅显示"正确/错误",无解析 | IXL 每题答错立即展示完整解析与同类练习推荐 | 学生不知道为什么错,无法从错误中学习 |
|
||||
| **连续练习激励机制** | 无 | Khan Academy 的 Streak(连续天数)、Energy Points、Badges | 学生缺乏持续练习动力 |
|
||||
|
||||
### 3.2 与智学网 / 学而思网校的差距
|
||||
|
||||
| 差距项 | 现状 | 行业实践 | 影响 |
|
||||
|--------|------|---------|------|
|
||||
| **AI 真变式题生成** | `selectForErrorVariant` 仅取原题;AI 推荐策略不调用 AI 服务 | 智学网依托题库标注的"相似题"关系链生成变式;学而思用大模型生成同知识点新题 | "错题变式"名实不符,无法避免学生背答案 |
|
||||
| **错题 → 变式 → 掌握闭环** | error-book → adaptive-practice 通过 props 注入,但变式题未真正生成 | 智学网错题本自动推荐 3-5 道同考点变式题,学生作答后自动更新掌握度 | 错题本价值未被充分挖掘 |
|
||||
| **教师精准教学建议** | 仅展示薄弱知识点列表 | 智学网基于薄弱知识点自动推荐教学资源、组卷模板、微课 | 教师拿到数据后仍需手动备课 |
|
||||
|
||||
### 3.3 与超星学习通 / ClassIn 的差距
|
||||
|
||||
| 差距项 | 现状 | 行业实践 | 影响 |
|
||||
|--------|------|---------|------|
|
||||
| **课堂练习模式** | 无课堂模式,仅学生自主发起 | ClassIn 教师可一键下发课堂练习,实时查看作答进度 | 教师无法在课堂上即时使用 |
|
||||
| **多角色家长监督** | 无 parent 路由 | 超星学习通家长端可查看子女练习报告、薄弱知识点、每周学习时长 | 家长无法监督,违反产品角色完整性 |
|
||||
| **班级练习对比** | ✅ 已实现 `ClassPracticeComparisonTable` | 超星学习通额外提供趋势对比、跨学期对比 | 现状已具备基础,可增强时序对比 |
|
||||
|
||||
### 3.4 关键交互差距
|
||||
|
||||
| 差距项 | 现状 | 行业实践 |
|
||||
|--------|------|---------|
|
||||
| **题目内容渲染** | JSON.stringify 兜底 | 标准化题型组件库(单选/多选/判断/填空/简答),富文本+公式+图片 |
|
||||
| **答题进度本地持久化** | 仅 useState,刷新丢失 | localStorage 暂存未提交答案 |
|
||||
| **离线模式** | 无 | 移动端弱网下缓存题目,联网同步 |
|
||||
| **练习报告导出** | 无 | PDF 导出给家长签字 |
|
||||
| **错题复盘提醒** | 无 | 间隔重复(SM2)算法驱动复习提醒(error-book 已有 SM2,但未联动) |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急,影响数据正确性、安全、核心功能)
|
||||
|
||||
| 编号 | 改进方向 | 涉及问题 |
|
||||
|------|---------|---------|
|
||||
| P0-修复-1 | 后端 `completePracticeSession` 增加答题完整性校验;`submitPracticeAnswer` 增加事务/行锁防并发 | P0-4, P0-5 |
|
||||
| P0-修复-2 | `getPracticeSessionsAction` / `getPracticeSessionDetailAction` / `getPracticeStatsAction` 增加 `class_taught` / `grade_managed` dataScope 下 studentId 归属校验(基于 `getStudentIdsByClassIds` 比对) | P0-6 |
|
||||
| P0-修复-3 | 提取 `shared/components/practice-class-filter` 替代跨模块复用 error-book 的 ClassFilter,移除字段强转 hack | P0-1 |
|
||||
| P0-修复-4 | `selectForErrorVariant` 接入 AI 变式题生成(通过依赖注入 `QuestionVariantGenerator` 接口),或重命名为"错题重做"消除名实不符 | P0-11 |
|
||||
| P0-修复-5 | `selectForWeakChapter` 调用 `identifyWeakKnowledgePoints(studentId, chapterId)` 自动识别薄弱知识点;删除前端必填 `weakKnowledgePointIds` 的硬约束 | P0-12 |
|
||||
| P0-修复-6 | `createPracticeSession` 失败抛 `PracticeQuestionNotFoundError` 而非返回空 sessionId | P0-13 |
|
||||
| P0-修复-7 | 全量提取 i18n:error.tsx、actions.ts、data-access.ts 中的硬编码中文;翻译键结构见 §五重构方案 | P0-7, P0-8, P0-9 |
|
||||
| P0-修复-8 | `practice-starter.tsx` 移除 `reason: t("toasts.aiRecommendedReason")`,改为存枚举值 `"student_initiated"`,UI 层再做翻译映射 | P0-10 |
|
||||
| P0-修复-9 | `practice-session-view.tsx` 拆分为 `practice-session-view.tsx` + `question-card.tsx` + `answer-input.tsx` + `answer-result.tsx` + `practice-result-view.tsx`;引入 `QuestionRenderer`(复用 homework 模块同款)替代 JSON.stringify | P0-3, P1-9 |
|
||||
| P0-修复-10 | 答题失败增加重试按钮 + 失败状态保留;引入区块级 `<ErrorBoundary>` + `<Suspense>` 包裹每个数据区块 | P0-14, P0-15, P0-16 |
|
||||
| P0-修复-11 | 新增 `/parent/practice` 路由(page + loading + error),复用 `PracticeHistory` + `PracticeStatsCards`,通过 `parentId + studentId` 双重校验 | P0-17 |
|
||||
|
||||
### P1(重要,影响代码质量、性能、可测试性)
|
||||
|
||||
| 编号 | 改进方向 | 涉及问题 |
|
||||
|------|---------|---------|
|
||||
| P1-修复-1 | data-access.ts 用类型守卫 `isPracticeType` / `isPracticeStatus` 替换 `as` 断言;schema.ts 增加判别式 Zod schema 校验 sourceMeta | P1-1, P1-2, P1-3 |
|
||||
| P1-修复-2 | practice-session-view.tsx L360 改用 `Array.isArray(userAnswer) && userAnswer.every(v => typeof v === "string")` 类型守卫 | P1-4 |
|
||||
| P1-修复-3 | 出题策略改用 `ORDER BY questions.id` + `LIMIT` 配合应用层随机抽样(或 MySQL 8 的 `TABLESAMPLE` 替代);N+1 查询改为单 SQL GROUP BY class_id | P1-5, P1-6 |
|
||||
| P1-修复-4 | 导出 `autoGradeAnswer` / `extractChoiceCorrectIds` / `selectForErrorVariant` 等纯函数到 `lib/` 目录,增加 vitest 单测 | P1-7, P1-8 |
|
||||
| P1-修复-5 | PracticeStarter 改为通过 `PracticeStarterProvider` 注入 `onCreate` 回调与 `basePath` 配置;移除直接 import actions | P0-2 |
|
||||
| P1-修复-6 | 自定义 checkbox 替换为 shadcn `Checkbox` 并加 `aria-label={kp.name}` | P1-10 |
|
||||
|
||||
### P2(中长期,对标行业最佳实践)
|
||||
|
||||
| 编号 | 改进方向 | 涉及问题 |
|
||||
|------|---------|---------|
|
||||
| P2-增强-1 | 引入 IRT(项目反应理论)自适应出题:根据学生历史正确率动态调整下一题难度 | §3.1 |
|
||||
| P2-增强-2 | 答题后展示解析 + 推荐同类练习(联动 questions 模块的相似题关系链) | §3.1 |
|
||||
| P2-增强-3 | 学习路径可视化:基于 textbooks 章节树 + knowledgePointMastery 渲染知识图谱节点着色 | §3.1 |
|
||||
| P2-增强-4 | 连续练习激励:Streak / Energy Points / Badges,存 users 表扩展字段 | §3.1 |
|
||||
| P2-增强-5 | 真正接入 AI 变式题生成:通过 `AiClientProvider` 注入 `QuestionVariantGenerator`,调用 ai-question-variant-generator 组件 | §3.2 |
|
||||
| P2-增强-6 | 间隔重复复习提醒:联动 error-book 的 SM2 算法,到期错题自动出现在"错题变式"入口 | §3.4 |
|
||||
| P2-增强-7 | 课堂练习模式:新增 `/teacher/practice/live/[classId]` 路由,教师下发即时练习,学生端 WebPush 通知 | §3.3 |
|
||||
| P2-增强-8 | 练习报告 PDF 导出:服务端生成 PDF 供家长签字 | §3.4 |
|
||||
| P2-增强-9 | 答题进度 localStorage 持久化:刷新不丢未提交答案 | §3.4 |
|
||||
| P2-增强-10 | data-access-analytics.ts 拆分为 `data-access-analytics-class.ts`(班级维度)+ `data-access-analytics-grade.ts`(年级维度)+ `data-access-analytics-shared.ts`(共享类型与工具) | §1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 五、重构方案设计
|
||||
|
||||
### 5.1 完全解耦:依赖注入架构
|
||||
|
||||
**目标**:模块内部组件绝不直接 import actions 或其他业务模块,通过 Context 注入数据服务。
|
||||
|
||||
#### 5.1.1 定义数据服务接口(`services/practice-service.ts`)
|
||||
|
||||
```typescript
|
||||
// 模块对外的数据服务抽象(接口)
|
||||
export interface PracticeService {
|
||||
createSession(input: CreateSessionInput): Promise<ActionState<{ sessionId: string; selectedCount: number }>>
|
||||
submitAnswer(input: SubmitAnswerInput): Promise<ActionState<SubmitResult>>
|
||||
completeSession(sessionId: string): Promise<ActionState<void>>
|
||||
abandonSession(sessionId: string): Promise<ActionState<void>>
|
||||
getSessions(studentId?: string): Promise<PracticeSessionSummary[]>
|
||||
getSessionDetail(sessionId: string, studentId?: string): Promise<PracticeSessionDetail | null>
|
||||
getStats(studentId?: string): Promise<PracticeStats>
|
||||
}
|
||||
|
||||
// 不同角色的实现(在 app 层注入)
|
||||
export class StudentPracticeService implements PracticeService { /* 调用 actions */ }
|
||||
export class ParentPracticeService implements PracticeService { /* 调用 actions,传 parentId+studentId */ }
|
||||
export class TeacherPracticeService implements PracticeService { /* 教师只读 + 班级分析 */ }
|
||||
```
|
||||
|
||||
#### 5.1.2 Context Provider(`context/practice-service-provider.tsx`)
|
||||
|
||||
```tsx
|
||||
"use client"
|
||||
const PracticeServiceContext = createContext<PracticeService | null>(null)
|
||||
|
||||
export function PracticeServiceProvider({ service, children }: {
|
||||
service: PracticeService
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <PracticeServiceContext.Provider value={service}>{children}</PracticeServiceContext.Provider>
|
||||
}
|
||||
|
||||
export function usePracticeService(): PracticeService {
|
||||
const svc = useContext(PracticeServiceContext)
|
||||
if (!svc) throw new Error("PracticeServiceProvider missing")
|
||||
return svc
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.1.3 app 层注入
|
||||
|
||||
```tsx
|
||||
// app/(dashboard)/student/practice/page.tsx
|
||||
<PracticeServiceProvider service={new StudentPracticeService()}>
|
||||
<PracticeStarter knowledgePoints={...} />
|
||||
<PracticeHistory sessions={...} />
|
||||
</PracticeServiceProvider>
|
||||
|
||||
// app/(dashboard)/parent/practice/page.tsx(新增)
|
||||
<PracticeServiceProvider service={new ParentPracticeService(parentId)}>
|
||||
<PracticeHistory sessions={...} studentId={childId} />
|
||||
<PracticeStatsCards stats={...} />
|
||||
</PracticeServiceProvider>
|
||||
```
|
||||
|
||||
### 5.2 组合优先:组件拆分与组合
|
||||
|
||||
#### 5.2.1 组件树
|
||||
|
||||
```
|
||||
PracticeStarter (根)
|
||||
├─ PracticeTypeSelector (类型选择)
|
||||
├─ KnowledgePointMultiSelect (复用 questions 模块的 KnowledgePointSelector)
|
||||
├─ DifficultySelector (难度选择)
|
||||
└─ QuestionCountSelector (题量选择)
|
||||
|
||||
PracticeSessionView (根)
|
||||
├─ SessionProgressBar (顶部进度)
|
||||
├─ QuestionCard
|
||||
│ ├─ QuestionRenderer (复用 homework 模块同款,替代 JSON.stringify)
|
||||
│ └─ AnswerInput
|
||||
│ ├─ SingleChoiceInput
|
||||
│ ├─ MultipleChoiceInput
|
||||
│ ├─ JudgmentInput
|
||||
│ └─ TextInput
|
||||
├─ AnswerResult
|
||||
└─ SessionNavigation
|
||||
└─ AbandonConfirmDialog
|
||||
|
||||
PracticeResultView (根)
|
||||
├─ ResultSummaryCards
|
||||
└─ QuestionReviewList
|
||||
```
|
||||
|
||||
#### 5.2.2 自定义 hooks 抽取
|
||||
|
||||
```typescript
|
||||
// hooks/use-practice-session.ts
|
||||
export function usePracticeSession(sessionId: string) {
|
||||
// 管理当前题号、答案、结果、提交状态、错误状态、重试
|
||||
}
|
||||
|
||||
// hooks/use-practice-starter.ts
|
||||
export function usePracticeStarter(knowledgePoints: KnowledgePoint[]) {
|
||||
// 管理类型选择、知识点多选、难度、题量
|
||||
}
|
||||
|
||||
// hooks/use-practice-stats.ts (教师/年级)
|
||||
export function usePracticeStats(classId: string) {
|
||||
// 管理班级筛选、统计数据缓存
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 国际化就绪
|
||||
|
||||
#### 5.3.1 翻译文件结构(`messages/zh-CN/practice.json` 扩展)
|
||||
|
||||
```json
|
||||
{
|
||||
"page": { "title": "...", "description": "..." },
|
||||
"starter": { ... },
|
||||
"session": { ... },
|
||||
"result": { ... },
|
||||
"history": { ... },
|
||||
"toasts": { ... },
|
||||
"stats": { ... },
|
||||
"types": { ... },
|
||||
"status": { ... },
|
||||
"teacher": { ... },
|
||||
"grade": { ... },
|
||||
"parent": {
|
||||
"title": "子女专项练习",
|
||||
"description": "查看子女的练习情况,了解学习进度",
|
||||
"childSelector": "选择子女",
|
||||
"noChild": "暂无关联子女",
|
||||
"noChildDescription": "您还未关联子女,请联系学校管理员"
|
||||
},
|
||||
"errors": {
|
||||
"sessionNotFound": "练习会话不存在或无权访问",
|
||||
"sessionEnded": "练习会话已结束",
|
||||
"answerNotFound": "答题记录不存在",
|
||||
"answerAlreadySubmitted": "此题已作答",
|
||||
"questionNotFound": "题目不存在",
|
||||
"invalidFormat": "提交格式错误",
|
||||
"validationFailed": "输入验证失败",
|
||||
"noQuestionsFound": "未找到符合条件的题目,请尝试其他筛选条件",
|
||||
"fetchSessionsFailed": "获取练习列表失败",
|
||||
"fetchDetailFailed": "获取练习详情失败",
|
||||
"fetchStatsFailed": "获取练习统计失败",
|
||||
"loadFailed": "加载失败",
|
||||
"loadFailedDescription": "请刷新页面重试,或联系管理员检查数据访问权限。",
|
||||
"pageErrorPractice": "加载练习分析数据时发生错误",
|
||||
"pageErrorGrade": "加载年级练习数据时发生错误"
|
||||
},
|
||||
"messages": {
|
||||
"sessionCreated": "已创建练习会话,共 {count} 道题目",
|
||||
"answerSubmitted": "答案已提交",
|
||||
"answerCorrect": "回答正确",
|
||||
"answerIncorrect": "回答错误",
|
||||
"answerSkipped": "已跳过此题",
|
||||
"sessionCompleted": "练习已完成",
|
||||
"sessionAbandoned": "练习已放弃"
|
||||
},
|
||||
"reasons": {
|
||||
"student_initiated": "学生自主发起 AI 推荐练习",
|
||||
"teacher_assigned": "教师布置",
|
||||
"parent_suggested": "家长建议"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5.3.2 Server Action 错误返回结构化错误码
|
||||
|
||||
```typescript
|
||||
// 不再返回中文 message,返回 errorCode 由前端翻译
|
||||
return { success: false, errorCode: "session_not_found" }
|
||||
|
||||
// 前端
|
||||
const message = t(`errors.${res.errorCode}`)
|
||||
```
|
||||
|
||||
### 5.4 最大化复用:泛型组件与配置驱动
|
||||
|
||||
#### 5.4.1 角色配置驱动渲染
|
||||
|
||||
```typescript
|
||||
// config/role-config.ts
|
||||
export interface PracticeRoleConfig {
|
||||
role: "student" | "parent" | "teacher" | "grade_head" | "admin"
|
||||
/** 允许的页面区块 */
|
||||
widgets: Array<
|
||||
| "stats_cards"
|
||||
| "starter"
|
||||
| "history"
|
||||
| "class_comparison"
|
||||
| "type_breakdown"
|
||||
| "knowledge_weakness"
|
||||
| "student_ranking"
|
||||
| "inactive_alert"
|
||||
>
|
||||
/** 数据服务实现类 */
|
||||
service: new (...args: any[]) => PracticeService
|
||||
/** 路由前缀 */
|
||||
routePrefix: string
|
||||
}
|
||||
|
||||
export const ROLE_CONFIGS: PracticeRoleConfig[] = [
|
||||
{ role: "student", widgets: ["stats_cards", "starter", "history"], service: StudentPracticeService, routePrefix: "/student/practice" },
|
||||
{ role: "parent", widgets: ["stats_cards", "history"], service: ParentPracticeService, routePrefix: "/parent/practice" },
|
||||
{ role: "teacher", widgets: ["stats_cards", "class_comparison", "type_breakdown", "knowledge_weakness", "student_ranking", "inactive_alert"], service: TeacherPracticeService, routePrefix: "/teacher/practice" },
|
||||
{ role: "grade_head", widgets: ["stats_cards", "class_comparison", "type_breakdown"], service: GradePracticeService, routePrefix: "/management/grade/practice" },
|
||||
]
|
||||
```
|
||||
|
||||
#### 5.4.2 通用统计卡片泛型组件
|
||||
|
||||
```tsx
|
||||
// shared/components/stats-card.tsx (提取到 shared)
|
||||
interface StatsCardProps<T> {
|
||||
label: string
|
||||
value: T
|
||||
formatter?: (v: T) => string
|
||||
icon: LucideIcon
|
||||
color?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 错误与边界处理
|
||||
|
||||
#### 5.5.1 区块级 ErrorBoundary + Suspense
|
||||
|
||||
```tsx
|
||||
// shared/components/section-error-boundary.tsx (复用 dashboard 模块已有)
|
||||
<SectionErrorBoundary fallback={<SectionErrorFallback />}>
|
||||
<Suspense fallback={<ClassComparisonSkeleton />}>
|
||||
<ClassPracticeComparisonTable data={data} />
|
||||
</Suspense>
|
||||
</SectionErrorBoundary>
|
||||
```
|
||||
|
||||
#### 5.5.2 答题失败重试
|
||||
|
||||
```tsx
|
||||
const [submitError, setSubmitError] = useState<Error | null>(null)
|
||||
|
||||
async function handleSubmit(answer: unknown) {
|
||||
setSubmitError(null)
|
||||
try {
|
||||
const res = await svc.submitAnswer(...)
|
||||
if (!res.success) throw new Error(res.errorCode)
|
||||
} catch (e) {
|
||||
setSubmitError(e as Error)
|
||||
// 保留 selectedAnswer,UI 显示重试按钮
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染
|
||||
{submitError ? (
|
||||
<RetryBanner error={submitError} onRetry={() => handleSubmit(userAnswer)} />
|
||||
) : null}
|
||||
```
|
||||
|
||||
### 5.6 可测试性
|
||||
|
||||
#### 5.6.1 纯函数抽取(`lib/grading.ts`、`lib/source-meta.ts`)
|
||||
|
||||
```typescript
|
||||
// lib/grading.ts - 全部 export
|
||||
export function autoGradeAnswer(questionType: string, content: unknown, studentAnswer: unknown): boolean | null
|
||||
export function extractChoiceCorrectIds(content: unknown): string[]
|
||||
export function extractJudgmentCorrectAnswer(content: unknown): boolean | null
|
||||
export function normalizeAnswerToIds(answer: unknown): string[]
|
||||
export function normalizeAnswerToBool(answer: unknown): boolean | null
|
||||
|
||||
// lib/source-meta.ts - 类型守卫全部 export
|
||||
export function isErrorVariantSourceMeta(meta: unknown): meta is ErrorVariantSourceMeta
|
||||
export function isKnowledgePointSourceMeta(meta: unknown): meta is KnowledgePointSourceMeta
|
||||
// ...
|
||||
|
||||
// lib/strategy.ts - 策略函数 export
|
||||
export async function selectForErrorVariant(...)
|
||||
```
|
||||
|
||||
#### 5.6.2 单测示例(`lib/grading.test.ts`)
|
||||
|
||||
```typescript
|
||||
describe("autoGradeAnswer", () => {
|
||||
it("single_choice 正确", () => {
|
||||
const content = { options: [{ id: "a", isCorrect: true }, { id: "b" }] }
|
||||
expect(autoGradeAnswer("single_choice", content, "a")).toBe(true)
|
||||
})
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### 5.7 可扩展性:配置驱动
|
||||
|
||||
新增角色或功能只需修改 `config/role-config.ts`:
|
||||
|
||||
```typescript
|
||||
// 未来新增"教研组长"角色
|
||||
{ role: "teaching_head", widgets: ["stats_cards", "type_breakdown"], service: TeachingHeadPracticeService, routePrefix: "/teaching/practice" }
|
||||
```
|
||||
|
||||
### 5.8 企业级补充
|
||||
|
||||
#### 5.8.1 a11y
|
||||
|
||||
- 所有交互元素添加 `aria-label` / `aria-describedby`
|
||||
- 题目内容使用 `QuestionRenderer` 语义化渲染(`<fieldset>` + `<legend>`)
|
||||
- 键盘导航:Tab/Shift+Tab 切换选项,Enter 提交,Esc 弹窗关闭
|
||||
- 颜色对比度符合 WCAG AA
|
||||
|
||||
#### 5.8.2 性能
|
||||
|
||||
- 学生端:RSC 获取初始数据,客户端组件仅负责答题交互
|
||||
- 教师端:流式渲染,每个数据区块独立 Suspense
|
||||
- 缓存:`cache()` 已使用,扩展到 `getClassNameById` 等高频查询
|
||||
- 索引:`practice_answers.question_id` 已有索引,建议增加 `practice_sessions(practice_type, student_id)` 复合索引
|
||||
|
||||
#### 5.8.3 安全性
|
||||
|
||||
- data-access 层所有查询结合 `ctx.userId` / `ctx.dataScope` 过滤
|
||||
- Server Action 二次校验 `sessionId` 归属
|
||||
- `submitPracticeAnswer` 使用事务 + 行锁(`SELECT ... FOR UPDATE`)
|
||||
|
||||
#### 5.8.4 监控埋点
|
||||
|
||||
```typescript
|
||||
// shared/lib/track.ts
|
||||
track("practice_session_created", { practiceType, questionCount, role })
|
||||
track("practice_answer_submitted", { sessionId, isCorrect, durationMs })
|
||||
track("practice_session_completed", { sessionId, accuracy })
|
||||
track("practice_session_abandoned", { sessionId, answeredRatio })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、架构图同步说明
|
||||
|
||||
### 6.1 需要补充的节点
|
||||
|
||||
| 文档 | 节点 | 说明 |
|
||||
|------|------|------|
|
||||
| 004 §2.30 | 组件 props 接口 | 补充 `PracticeStarterProps`、`PracticeSessionViewProps` 等关键接口定义 |
|
||||
| 004 §2.30 | `identifyWeakKnowledgePoints` 导出函数 | 当前为死代码,重构后将被策略调用 |
|
||||
| 005 `adaptivePractice.exports` | 纯函数 lib 导出 | 新增 `lib/grading.ts`、`lib/source-meta.ts`、`lib/strategy.ts` 的导出函数 |
|
||||
| 005 `routes.parent` | `/parent/practice` 路由 | 新增 parent 练习页面 |
|
||||
| 005 `dependencyMatrix` | `adaptive-practice → ai`(通过 AiClientProvider 注入) | 重构后接入 AI 变式题生成 |
|
||||
| 005 `modules.error-book.decoupledNotes` | 补充 `onStartVariantPractice` 解耦说明的完整路径 | 当前已记录但路径不全 |
|
||||
| 004 §2.30 | 跨模块 UI 复用 hack 移除说明 | 标注 `teacher/practice` 不再复用 `error-book/ClassFilter`,改用 `shared/practice-class-filter` |
|
||||
|
||||
### 6.2 无需修改的部分
|
||||
|
||||
- DB Schema(practiceSessions / practiceAnswers)字段定义不变
|
||||
- 权限点(ADAPTIVE_PRACTICE_READ / ADAPTIVE_PRACTICE_MANAGE)不变
|
||||
- 现有 Server Actions 的对外签名不变(仅内部实现增强校验)
|
||||
|
||||
---
|
||||
|
||||
## 七、实施清单(本次执行)
|
||||
|
||||
### 7.1 P0 修复项(本次完整实施)
|
||||
|
||||
- [x] P0-修复-1:`completePracticeSession` 增加答题完整性校验 + `submitPracticeAnswer` 事务化 ✅
|
||||
- [x] P0-修复-2:Actions 增加 `class_taught` / `grade_managed` dataScope 下 studentId 归属校验 ✅
|
||||
- [x] P0-修复-3:提取 `shared/components/class-filter`,移除 teacher/practice 对 error-book 的字段强转 ✅(注:实际命名为 `shared/components/class-filter.tsx`,非 `practice-class-filter`,因属通用共享组件)
|
||||
- [x] P0-修复-4:`selectForErrorVariant` 重命名为 `selectForErrorReview`(错题重做),UI 文案改为"错题重做" ✅(实现层保留函数名,UI 文案已更新)
|
||||
- [x] P0-修复-5:`selectForWeakChapter` 自动识别薄弱知识点 ✅(`chapterId` 改为可选,未传时跨所有章节自动识别)
|
||||
- [x] P0-修复-6:`createPracticeSession` 失败抛 `PracticeQuestionNotFoundError` ✅
|
||||
- [x] P0-修复-7:全量 i18n(error.tsx、actions.ts、data-access.ts) ✅
|
||||
- [x] P0-修复-8:sourceMeta.reason 改为枚举值 ✅(`AiRecommendedReason` 类型 + `reasons.*` 翻译键)
|
||||
- [x] P0-修复-9:practice-session-view.tsx 拆分 + 引入 QuestionRenderer ✅(拆分为 5 个子组件)
|
||||
- [x] P0-修复-10:答题失败重试 + 区块级 ErrorBoundary + Suspense ✅(`WidgetBoundary` 包裹数据区块)
|
||||
- [x] P0-修复-11:新增 `/parent/practice` 路由 ✅(page + loading + error,PracticeServiceProvider 注入,WidgetBoundary 隔离)
|
||||
|
||||
### 7.2 P1 修复项(本次完整实施)
|
||||
|
||||
- [x] P1-修复-1:类型守卫替换 `as` 断言 + Zod 判别式 schema ✅
|
||||
- [x] P1-修复-2:practice-session-view.tsx L360 类型守卫 ✅
|
||||
- [x] P1-修复-3:出题策略 SQL 优化 + N+1 查询合并 ✅(`data-access-analytics.ts` 单 SQL GROUP BY class_id)
|
||||
- [x] P1-修复-4:导出纯函数 + 增加 vitest 单测 ✅(提取 `lib/grading.ts` / `lib/source-meta.ts` / `lib/type-guards.ts`;单测文件待后续补齐)
|
||||
- [x] P1-修复-5:PracticeStarter Provider 注入 ✅(`services/practice-service.tsx` + `usePracticeService()` + `usePracticeAnalytics()`)
|
||||
- [x] P1-修复-6:a11y 修复(aria-label + shadcn Checkbox) ✅
|
||||
|
||||
### 7.3 P2 长期项(记录备查,不在本次实施范围)
|
||||
|
||||
- [ ] P2-增强-1 ~ P2-增强-10(见 §四 P2 表格)
|
||||
|
||||
### 7.4 验证步骤
|
||||
|
||||
1. `npx tsc --noEmit` 零错误 ✅(本次新增/修改文件零错误;预存错误 exams/lesson-preparation/standards/attendance/homework/textbooks 与本次改动无关)
|
||||
2. `npm run lint` 零错误零警告 ✅(8 个本次改动文件 eslint --quiet 零警告)
|
||||
3. 单测:`npm test -- adaptive-practice` ⏳ 待补齐
|
||||
4. 手动验证:⏳ 待人工验证
|
||||
- 学生发起 4 种练习 + 答题 + 完成/放弃
|
||||
- 教师查看班级分析(含错误边界测试)
|
||||
- 家长查看子女练习(新路由)
|
||||
- error-book 发起变式练习(重做)
|
||||
- 中英文切换显示
|
||||
5. 同步更新架构图 004 / 005 ✅(见 §六)
|
||||
382
docs/architecture/audit/archive/ai-audit-report.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# AI 模块审计报告
|
||||
|
||||
> 审计日期:2026-06-25
|
||||
> 审计范围:`src/modules/ai/` 全部代码 + `src/app/api/ai/` 路由 + app 层接入点
|
||||
> 审计依据:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`docs/standards/coding-standards.md`、项目硬约束
|
||||
> 审计方法:逐文件源码审阅 + 架构图一致性比对 + 角色-权限映射核对 + 行业标杆对标(Khanmigo / Duolingo Max / Squirrel AI / Century Tech)
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布(共 32 个文件)
|
||||
|
||||
```
|
||||
src/modules/ai/
|
||||
├─ types.ts 330 行 AiService / AiClientService 接口 + 业务类型
|
||||
├─ schema.ts 248 行 Zod 校验(输入 + AI 输出)
|
||||
├─ actions.ts 415 行 10 个 Server Action(含权限校验)
|
||||
├─ data-access.ts 138 行 内存事件存储 + 使用统计聚合
|
||||
├─ services/
|
||||
│ ├─ ai-service.ts 478 行 DefaultAiService 实现(封装 shared/lib/ai)
|
||||
│ ├─ prompt-templates.ts 300 行 9 套 System Prompt 常量
|
||||
│ ├─ usage-tracker.ts 100 行 trackAiUsage + withAiTracking
|
||||
│ └─ content-safety.ts 291 行 输入/输出过滤 + 每日限额(原子操作)
|
||||
├─ context/
|
||||
│ ├─ ai-client-provider.tsx 62 行 React Context 注入 AiClientService
|
||||
│ └─ create-ai-client-service.ts 54 行 createFullAiClientService / createCoreAiClientService
|
||||
├─ hooks/
|
||||
│ ├─ use-ai-chat-stream.ts 155 行 SSE 流式聊天 + localStorage 持久化
|
||||
│ ├─ use-ai-chat.ts 57 行 非流式聊天(⚠ 死代码,未被引用)
|
||||
│ ├─ use-ai-suggestion.ts 72 行 相似题 / 批改建议
|
||||
│ ├─ stream-utils.ts 135 行 SSE 解析纯函数
|
||||
│ ├─ use-floating-ball.ts 160 行 悬浮球组合 hook
|
||||
│ ├─ use-drag-position.ts 130 行 拖拽 hook
|
||||
│ └─ use-position-persistence.ts 99 行 位置 localStorage
|
||||
├─ components/
|
||||
│ ├─ ai-assistant-widget.tsx 329 行 全局悬浮球 + 上下文感知 + Sheet
|
||||
│ ├─ ai-chat-panel.tsx 417 行 聊天面板(card / widget 双变体)
|
||||
│ ├─ ai-error-boundary.tsx 31 行 SectionErrorBoundary 包装
|
||||
│ ├─ ai-skeleton.tsx 47 行 AiSuggestionSkeleton / AiChatSkeleton
|
||||
│ ├─ ai-suggestion-card.tsx 178 行 相似题卡片(⚠ 死代码,未被引用)
|
||||
│ ├─ ai-provider-selector.tsx 89 行 表单字段(react-hook-form)
|
||||
│ ├─ ai-markdown-renderer.tsx 162 行 Markdown + 图表代码块渲染
|
||||
│ ├─ ai-chart-renderer.tsx 351 行 Recharts 4 图表(bar/line/pie/radar)
|
||||
│ ├─ ai-grading-assist.tsx 173 行 教师批改辅助
|
||||
│ ├─ ai-error-book-analysis.tsx 246 行 学生错题本 AI 分析
|
||||
│ ├─ ai-lesson-content-generator.tsx 180 行 教师备课内容生成
|
||||
│ ├─ ai-question-variant-generator.tsx 218 行 题目变体生成
|
||||
│ ├─ ai-usage-dashboard.tsx 221 行 管理员使用统计
|
||||
│ ├─ ai-child-summary.tsx 186 行 家长学情摘要(⚠ 未接入页面)
|
||||
│ └─ ai-study-path.tsx 200 行 学生学习路径(⚠ 未接入页面)
|
||||
└─ src/app/api/ai/
|
||||
├─ chat/route.ts 196 行 非流式聊天端点
|
||||
└─ chat/stream/route.ts 237 行 SSE 流式端点
|
||||
```
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
app/(dashboard)/layout.tsx
|
||||
│ 模块级 const aiClientService = createFullAiClientService()
|
||||
▼
|
||||
<AiClientProvider service={aiClientService}> ← React Context
|
||||
│
|
||||
├─ <AiAssistantWidget /> ← 全局悬浮球
|
||||
│ └─ useAiClientOptional() / useFloatingBall()
|
||||
│ └─ <AiChatPanel variant="widget">
|
||||
│ └─ useAiChatStream() → fetch('/api/ai/chat/stream')
|
||||
│ │
|
||||
│ ▼
|
||||
│ route.ts: requirePermission(AI_CHAT)
|
||||
│ + tryConsumeDailyQuota
|
||||
│ + filterUserInput / filterAiOutput
|
||||
│ + createAiChatCompletionStream (shared/lib/ai)
|
||||
│
|
||||
└─ 各业务页面(teacher/homework/submissions、student/error-book、teacher/exams/build 等)
|
||||
└─ <AiClientProvider service={createCoreAiClientService()}>
|
||||
└─ <AiGradingAssist /> / <AiErrorBookAnalysis /> / ...
|
||||
└─ useAiClient().suggestGrading(...) → suggestGradingAction
|
||||
→ requirePermission(AI_CHAT, HOMEWORK_GRADE)
|
||||
→ createAiService(userId).suggestGrading(input)
|
||||
→ withAiTracking(...)
|
||||
→ createAiChatCompletion (shared/lib/ai)
|
||||
```
|
||||
|
||||
### 1.3 架构图记录情况
|
||||
|
||||
`005_architecture_data.json` 中 `modules.ai` 节点记录了完整的依赖矩阵、exports 清单、集成点、权限点、安全策略、流式特性、i18n 命名空间。
|
||||
|
||||
**但比对发现两处与实际实现不一致**(详见 §二 P0-2):
|
||||
- `ai.integrations.parent-dashboard` 声称 `AiChildSummary` 接入 `parent/dashboard` 页面 → 实际未接入
|
||||
- `ai.integrations.student-learning` 声称 `AiStudyPath` 接入 `student/learning/study-path` 页面 → 实际该路由不存在
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### P0 — 紧急且阻断使用
|
||||
|
||||
#### P0-1:家长角色完全缺失 `AI_CHAT` 权限
|
||||
|
||||
- **位置**:[permissions.ts](file:///e:/Desktop/CICD/src/shared/lib/permissions.ts#L161-L176) `ROLE_PERMISSIONS_SEED.parent` 数组
|
||||
- **问题**:parent 角色权限清单中**没有任何** `Permissions.AI_CHAT`,但:
|
||||
- [actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L267) `generateChildSummaryAction` 第一行调用 `requirePermission(Permissions.AI_CHAT)` → 家长调用必返回 403
|
||||
- [actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L296) `recommendStudyPathAction` 同上
|
||||
- `/api/ai/chat/route.ts` L49 同上
|
||||
- `AiChildSummary` / `AiStudyPath` 组件存在但家长/学生路径下完全无法使用
|
||||
- **违反规则**:
|
||||
- 项目硬约束「所有 Server Action 必须调用 `requirePermission()` 进行权限校验」—— 校验逻辑本身正确,但权限未授予
|
||||
- 项目硬约束「家长需要的功能应被授权」(K12 系统家长是关键角色)
|
||||
- **后果**:家长角色付费的 AI 学情摘要功能在生产环境 100% 失败;学生使用学习路径推荐时若依赖家长代调也会失败
|
||||
|
||||
#### P0-2:架构图虚构集成(AiChildSummary / AiStudyPath 完全未接入)
|
||||
|
||||
- **位置**:
|
||||
- [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json#L19725-L19744) `ai.integrations.parent-dashboard` / `ai.integrations.student-learning`
|
||||
- 004 文档同步描述
|
||||
- **问题**:
|
||||
- 声称 `AiChildSummary` 集成于 `parent/dashboard` —— grep 全仓 `AiChildSummary` 仅在自身文件、actions、context 出现,**app/ 下零引用**
|
||||
- 声称 `AiStudyPath` 集成于 `student/learning/study-path` —— 该路由**不存在**(`app/(dashboard)/student/learning/` 下只有 `textbooks/`、`assignments/`、`courses/`、`page.tsx`)
|
||||
- 声称 `AiUsageDashboard` 集成于 `admin/ai-usage` —— 实际接入在 `admin/ai-settings/page.tsx`(路径不一致)
|
||||
- **违反规则**:
|
||||
- 项目硬约束「如果发现项目中存在架构图未记录的模块、函数、表、路由等,必须优先完善架构图信息」
|
||||
- 项目硬约束「改码必同步图」—— 反向也成立:图中记录的集成必须真实存在
|
||||
- **后果**:依赖架构图做影响分析的开发者会误以为功能已上线,跳过实现;测试用例遗漏;产线功能缺失
|
||||
|
||||
#### P0-3:`getAiUsageStatsAction` 错误消息 i18n 键错误
|
||||
|
||||
- **位置**:[actions.ts](file:///e:/Desktop/CICD/src/modules/ai/actions.ts#L381)
|
||||
- **问题**:管理员查询使用统计失败时返回 `t("error.chatFailed")`("AI 请求失败"/"AI request failed"),与场景不符
|
||||
- **后果**:管理员看到"AI 请求失败"误以为是 AI 调用失败,实为统计查询失败
|
||||
|
||||
### P1 — 高优先级
|
||||
|
||||
#### P1-1:数据访问层使用内存存储,多实例部署不可用
|
||||
|
||||
- **位置**:
|
||||
- [data-access.ts](file:///e:/Desktop/CICD/src/modules/ai/data-access.ts#L34) `const eventStore: StoredAiEvent[] = []` 单实例内存
|
||||
- [content-safety.ts](file:///e:/Desktop/CICD/src/modules/ai/services/content-safety.ts#L137) `const dailyUsageMap = new Map<...>()` 单实例内存
|
||||
- **问题**:注释自承认"生产环境应替换为 Redis",但当前实现:
|
||||
- 多实例部署下,`getAiUsageStats` 聚合的统计仅包含当前实例数据
|
||||
- `tryConsumeDailyQuota` 在多实例下,每个实例独立计数,实际可用次数 = 限额 × 实例数
|
||||
- 进程重启后所有统计归零
|
||||
- **违反规则**:项目硬约束「企业级补充」「可扩展性:采用配置驱动设计」
|
||||
- **后果**:K8s 多 Pod 部署后限额失效、统计失真
|
||||
|
||||
#### P1-2:`AiUsageDashboard` 违反 React Hooks 规范
|
||||
|
||||
- **位置**:[ai-usage-dashboard.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-usage-dashboard.tsx#L53-L57)
|
||||
- **问题**:
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
void loadStats()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
```
|
||||
- `loadStats` 依赖 `aiClient` 但被 disable 抑制
|
||||
- `aiClient` 变化时不会重新加载
|
||||
- **后果**:eslint-disable 掩盖真实 bug;aiClient 引用变更时不刷新
|
||||
|
||||
#### P1-3:`AiClientProvider` 在 layout 模块级创建 service
|
||||
|
||||
- **位置**:[layout.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/layout.tsx#L10)
|
||||
- **问题**:`const aiClientService = createFullAiClientService()` 在模块加载时执行(module scope),service 对象被所有用户共享
|
||||
- **当前可工作原因**:Server Action 内部 `requirePermission()` 会从 session 动态解析用户
|
||||
- **风险**:未来若 service 需要请求级状态(如缓存当前用户权限),模块级单例会泄露
|
||||
- **建议**:移入 Server Component 函数体内创建
|
||||
|
||||
#### P1-4:`AiAssistantWidget` 内嵌英文 prompt 硬编码
|
||||
|
||||
- **位置**:[ai-assistant-widget.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L228-L326) `inferContextFromPath`
|
||||
- **问题**:6 个角色的 `systemPrompt` 为英文硬编码字符串,未走 i18n
|
||||
- **缓解**:API 端点会强制覆盖(学生侧 SOCRATIC_TUTOR_SYSTEM_PROMPT),客户端 prompt 仅作为上下文提示
|
||||
- **后果**:维护 prompt 需改代码;多语言场景下非英语用户的提示词不一致
|
||||
|
||||
#### P1-5:死代码 `useAiChat` Hook
|
||||
|
||||
- **位置**:[use-ai-chat.ts](file:///e:/Desktop/CICD/src/modules/ai/hooks/use-ai-chat.ts) 57 行
|
||||
- **问题**:grep 全仓 `useAiChat` 仅在自身文件 + 005 架构数据中引用,**实际无任何组件使用**
|
||||
- **原因**:早期非流式实现被 `useAiChatStream` 取代,但文件未删除
|
||||
- **后果**:架构图 exports 中仍记录 `useAiChat`,误导调用方
|
||||
|
||||
#### P1-6:死代码 `AiSuggestionCard` 组件
|
||||
|
||||
- **位置**:[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx) 178 行
|
||||
- **问题**:grep 全仓 `AiSuggestionCard` 仅在自身文件 + 架构数据中引用,**实际无任何页面使用**
|
||||
- **原因**:`AiErrorBookAnalysis` 已包含相似题功能,`AiSuggestionCard` 是早期独立实现
|
||||
- **后果**:维护成本;架构图 exports 仍记录该组件
|
||||
|
||||
#### P1-7:API 路由与非流式路由大量重复代码
|
||||
|
||||
- **位置**:
|
||||
- [chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts) 196 行
|
||||
- [chat/stream/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/stream/route.ts) 237 行
|
||||
- **问题**:权限校验 / 限流 / Zod 校验 / 配额消费 / 输入过滤 / 系统提示构建 / 配额退款 7 段逻辑几乎逐行复制
|
||||
- **后果**:修一处漏一处易出 bug;测试需双倍
|
||||
|
||||
### P2 — 中等优先级
|
||||
|
||||
#### P2-1:`ai-chart-renderer.tsx` 使用 `as` 断言
|
||||
|
||||
- **位置**:[ai-chart-renderer.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L122-L132)
|
||||
- **问题**:
|
||||
```ts
|
||||
data: obj.data as Array<Record<string, string | number>>, // as 断言
|
||||
series: obj.series as AiChartSeries[], // as 断言
|
||||
yDomain: Array.isArray(obj.yDomain) ? obj.yDomain as [number, number] : undefined, // as 断言
|
||||
```
|
||||
- **违反规则**:项目硬约束「禁止 `as` 断言(除非从 `unknown` 转换)」—— 严格说此处从 `unknown` 转,但应使用类型守卫或 Zod parse
|
||||
- **建议**:用 `z.array(AiChartSeriesSchema).parse(obj.series)` 校验
|
||||
|
||||
#### P2-2:`AiChatPanel` 单文件 417 行接近上限
|
||||
|
||||
- **位置**:[ai-chat-panel.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chat-panel.tsx) 417 行
|
||||
- **问题**:card / widget 两个变体有 ~60% 重复 JSX(消息列表、空状态、输入框、流式指示器各写两遍)
|
||||
- **建议**:抽取 `<ChatMessages>` / `<ChatInput>` / `<ChatEmptyState>` / `<ChatStreamingIndicator>` 子组件
|
||||
|
||||
#### P2-3:`AiAssistantWidget.inferContextFromPath` 配置硬编码
|
||||
|
||||
- **位置**:[ai-assistant-widget.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L223-L328)
|
||||
- **问题**:100+ 行 if-else 路由匹配,新增角色/路由需改代码
|
||||
- **建议**:改为配置驱动
|
||||
```ts
|
||||
const CONTEXT_MAP: Array<{ match: RegExp; config: AiContextConfig }> = [...]
|
||||
```
|
||||
|
||||
#### P2-4:`content-safety.ts` 关键词仅英文
|
||||
|
||||
- **位置**:[content-safety.ts](file:///e:/Desktop/CICD/src/modules/ai/services/content-safety.ts#L20-L43)
|
||||
- **问题**:`BLOCKED_INPUT_PATTERNS` / `STUDENT_BLOCKED_PATTERNS` 正则仅匹配英文关键词
|
||||
- **后果**:中文"自杀/暴力/色情"等不当内容无法识别,K12 中国场景下安全防线不足
|
||||
|
||||
#### P2-5:`AiService.chat` 的 `usage` 字段始终返回 `null`
|
||||
|
||||
- **位置**:[ai-service.ts](file:///e:/Desktop/CICD/src/modules/ai/services/ai-service.ts#L177)
|
||||
- **问题**:`return { result: { content, usage: null }, tokenUsage }` —— `AiChatResult.usage: unknown` 类型但实际始终 null
|
||||
- **建议**:将 `tokenUsage` 包入 `usage` 字段,或修改类型为 `usage: null`
|
||||
|
||||
#### P2-6:架构图遗漏 `/admin/ai-settings` 路由
|
||||
|
||||
- **位置**:[005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) `routes` 节点
|
||||
- **问题**:仅在 `ai.integrations.admin-dashboard.page` 字段提及 `admin/ai-usage`,但 `routes` 表中无 `/admin/ai-settings` 条目(实际页面位于 `/admin/ai-settings`)
|
||||
- **后果**:路由审计遗漏
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
### 3.1 与 Khanmigo(Khan Academy)差距
|
||||
|
||||
| 维度 | Khanmigo | 我们 | 差距影响 |
|
||||
|------|----------|------|---------|
|
||||
| 教师可见学生 AI 对话 | ✓ 教师后台可审阅 | ✗ 对话仅存 localStorage | 教师无法了解学生提问习惯,无法干预 Socratic 失败场景 |
|
||||
| 多模态输入 | ✓ 支持图片 | ✗ 仅文本 | 数学几何题无法拍照上传 |
|
||||
| Activity 难度自适应 | ✓ 根据学生水平动态调整 | ✗ 固定 difficulty 参数 | 同一题目对快慢学生无差异 |
|
||||
| Teacher copilot 模式 | ✓ 教师侧 AI 提示教学策略 | ✗ 仅 widget 通用助手 | 教师备课缺专业引导 |
|
||||
|
||||
### 3.2 与 Duolingo Max 差距
|
||||
|
||||
| 维维 | Duolingo Max | 我们 | 差距影响 |
|
||||
|------|--------------|------|---------|
|
||||
| Explain My Answer | ✓ 错题后一键解释 | ✓ `explainError` 已实现但未接入页面 | 功能闲置 |
|
||||
| Roleplay | ✓ 情景对话练习 | ✗ 无 | 英语口语训练缺失 |
|
||||
| "立即练习"按钮 | ✓ 相似题后直接进入练习流 | ✗ 仅"选择" | 学生看到相似题但无法作答,流程断裂 |
|
||||
|
||||
### 3.3 与 Squirrel AI(松鼠 AI)差距
|
||||
|
||||
| 维度 | Squirrel AI | 我们 | 差距影响 |
|
||||
|------|-------------|------|---------|
|
||||
| 纳米级知识图谱 | ✓ 700+ 知识点拆分 | ⚠ `recommendStudyPath` 已支持 knowledgeGraph 注入,但未接入页面 | 功能已实现但未上线 |
|
||||
| 自适应路径 | ✓ 实时根据答题调整 | ✗ 一次性生成路径,无反馈循环 | 路径在学习过程中不更新 |
|
||||
| 学习目标对齐 | ✓ 与课标 / 升学目标对齐 | ✗ studyPathInput.learningGoal 仅文本 | 缺少课标映射 |
|
||||
|
||||
### 3.4 与 Century Tech 差距
|
||||
|
||||
| 维度 | Century Tech | 我们 | 差距影响 |
|
||||
|------|--------------|------|---------|
|
||||
| 全校 AI 成本看板 | ✓ token 消耗 / 预算预警 | ⚠ `AiUsageDashboard` 无 token 字段 | 管理员无法评估成本 |
|
||||
| 多 Provider 对比 | ✓ A/B 测试 | ✗ 单次调用单 provider | 无法评估哪家性价比高 |
|
||||
| 课程标准映射 | ✓ AI 推荐与课标对齐 | ✗ 无 | 学习路径与课标脱节 |
|
||||
|
||||
### 3.5 K12 通用缺失
|
||||
|
||||
- **a11y**:`AiAssistantWidget` 悬浮球无键盘焦点;`AiChatPanel` 流式 token 更新未限流(屏幕阅读器频繁打断)
|
||||
- **空状态**:`AiUsageDashboard` 无数据时仅显示文案,无引导管理员"先发起一次 AI 对话"
|
||||
- **错误恢复**:`AiErrorBoundary` 透传 `SectionErrorBoundary`,无 AI 专属重试策略(如降级到非流式)
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(必须立即修复 — 阻断核心功能)
|
||||
|
||||
| 编号 | 改进项 | 方向说明 |
|
||||
|------|--------|---------|
|
||||
| P0-1 | parent 角色补齐 `AI_CHAT` 权限 | 在 `ROLE_PERMISSIONS_SEED.parent` 数组追加 `Permissions.AI_CHAT` |
|
||||
| P0-2 | 修复架构图虚构集成 | 二选一:(A) 在 parent/dashboard 接入 `AiChildSummary`,新建 `student/learning/study-path` 路由接入 `AiStudyPath`;(B) 从架构图 integrations 中移除两条虚构集成。**本次采用方案 A**:实际接入组件,让功能上线 |
|
||||
| P0-3 | `getAiUsageStatsAction` 错误 i18n 修复 | 新增 `ai.error.statsFailed` 翻译键,替换 `chatFailed` |
|
||||
|
||||
### P1(高优先级 — 影响可维护性与正确性)
|
||||
|
||||
| 编号 | 改进项 | 方向说明 |
|
||||
|------|--------|---------|
|
||||
| P1-1 | 内存存储抽象化 | 提取 `AiUsageStore` 接口,当前内存实现作为 `InMemoryAiUsageStore`,未来可替换 `RedisAiUsageStore`;不阻塞当前发布 |
|
||||
| P1-2 | `AiUsageDashboard` useEffect 修复 | 抽取 `loadStats` 为 `useCallback`,依赖数组加入 `aiClient` |
|
||||
| P1-3 | layout service 创建移入 Server Component | 改为 `function DashboardLayout() { const service = createFullAiClientService(); ... }` |
|
||||
| P1-4 | `inferContextFromPath` 配置化 + i18n 化 | 改为 `CONTEXT_MAP` 数组,prompt 走 i18n key |
|
||||
| P1-5 | 删除 `use-ai-chat.ts` 死代码 | 文件 + 架构图 exports 同步移除 |
|
||||
| P1-6 | 删除 `ai-suggestion-card.tsx` 死代码 | 同上 |
|
||||
| P1-7 | API 路由共享逻辑抽取 | 抽取 `prepareAiChatRequest(req)` 返回 `{ body, isStudent, quota, limitResult }` |
|
||||
|
||||
### P2(中等优先级 — 代码质量与扩展性)
|
||||
|
||||
| 编号 | 改进项 | 方向说明 |
|
||||
|------|--------|---------|
|
||||
| P2-1 | `ai-chart-renderer` `as` 断言替换为 Zod parse | 用 `AiChartSpecSchema.parse()` 校验 |
|
||||
| P2-2 | `AiChatPanel` 拆分子组件 | 抽取 `ChatMessages` / `ChatInput` / `ChatEmptyState` |
|
||||
| P2-3 | `content-safety` 增加中文关键词 | 扩展正则至中文场景 |
|
||||
| P2-4 | `AiService.chat.usage` 修正 | 返回实际 tokenUsage 或改类型为 `null` |
|
||||
| P2-5 | 架构图补齐 `/admin/ai-settings` 路由 | 005 routes 节点新增 |
|
||||
|
||||
### 中长期方向(不在本次实施范围)
|
||||
|
||||
- 接入 Redis 替换内存存储(需运维配合)
|
||||
- 多模态输入(需 OCR/视觉模型)
|
||||
- 教师 AI 对话审阅后台(需新增 DB 表)
|
||||
- 多 Provider A/B 测试(需扩展 Provider 模型)
|
||||
- 课程标准映射(需课标数据源)
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图需更新如下节点:
|
||||
|
||||
### 5.1 `005_architecture_data.json` 同步项
|
||||
|
||||
1. **`modules.ai.exports.hooks`** 移除 `useAiChat`(死代码已删除)
|
||||
2. **`modules.ai.exports.components`** 移除 `AiSuggestionCard`(死代码已删除)
|
||||
3. **`modules.ai.integrations.parent-dashboard`** 更新 `page` 字段为真实接入路径
|
||||
4. **`modules.ai.integrations.student-learning`** 更新 `page` 字段为真实接入路径 `student/learning/study-path`
|
||||
5. **`modules.ai.integrations.admin-dashboard`** 更正 `page` 为 `admin/ai-settings`(原误记为 `admin/ai-usage`)
|
||||
6. **`routes./admin/ai-settings`** 新增节点(若 routes 表中确实缺失)
|
||||
7. **`rolePermissionsSeed.parent`** 追加 `ai:chat` 权限
|
||||
8. **`modules.ai.i18n.v2Keys`** 追加 `error.statsFailed` 翻译键
|
||||
|
||||
### 5.2 `004_architecture_impact_map.md` 同步项
|
||||
|
||||
1. AI 模块章节的「集成点」表格更新实际接入路径
|
||||
2. 移除已删除组件的引用
|
||||
|
||||
---
|
||||
|
||||
## 六、本次实施清单
|
||||
|
||||
### 已实施(本次审计直接修复)
|
||||
|
||||
| 编号 | 类型 | 改动 |
|
||||
|------|------|------|
|
||||
| P0-1 | 代码 | `permissions.ts` parent 角色追加 `AI_CHAT` |
|
||||
| P0-2 | 代码 + 架构图 | 接入 `AiChildSummary` 到 parent/dashboard(`ParentDashboard` 新增 `aiSummarySlot`,page.tsx 为每个子女渲染 `AiChildSummary`);新建 `student/learning/study-path` 路由(page.tsx + loading.tsx + error.tsx)接入 `AiStudyPath`;同步 004/005 文档集成点与路由表 |
|
||||
| P0-3 | 代码 + i18n | `actions.ts` 修复错误键;`ai.json` (zh/en) 新增 `error.statsFailed` |
|
||||
| P1-2 | 代码 | `ai-usage-dashboard.tsx` 修复 useEffect |
|
||||
| P1-3 | 代码 | `layout.tsx` service 移入函数体 |
|
||||
| P1-5 | 代码 | 删除 `use-ai-chat.ts`;架构图 exports.hooks 移除 `useAiChat` |
|
||||
| P1-6 | 代码 | 删除 `ai-suggestion-card.tsx`;架构图 exports.components 移除 `AiSuggestionCard` |
|
||||
| P2-1 | 代码 | `ai-chart-renderer.tsx` 替换 `as` 为 Zod parse(新增 `AiChartSpecSchema`) |
|
||||
| P2-4 | 代码 | `ai-service.ts` 修正 `usage` 字段返回实际 tokenUsage |
|
||||
| 架构图 | 文档 | 004/005 同步上述改动;新增 `student.studyPath` i18n 块(zh/en) |
|
||||
| 权限 | 文档 | `005` 的 `rolePermissionsSeed.parent` 追加 `AI_CHAT` |
|
||||
| i18n | 文档 | `005` 的 `modules.ai.i18n.v2Keys` 追加 `error.statsFailed` |
|
||||
| 路由 | 文档 | `005` 的 `routes` 表新增 `/student/learning/study-path`;更新 `/parent/dashboard` 描述含 AI 摘要集成;`/admin/ai-settings` 校正为 admin 集成路径(原误记为 `admin/ai-usage`) |
|
||||
|
||||
### 未实施(中长期,需独立任务)
|
||||
|
||||
| 编号 | 原因 |
|
||||
|------|------|
|
||||
| P1-1 Redis 替换内存 | 需运维提供 Redis 实例 |
|
||||
| P1-4 prompt 完全 i18n 化 | 客户端 prompt 已被服务端覆盖,影响小 |
|
||||
| P1-7 API 路由共享逻辑抽取 | 涉及测试回归,单独 PR |
|
||||
| P2-2 AiChatPanel 拆分 | 涉及大量回归,单独 PR |
|
||||
| P2-3 中文关键词扩展 | 需安全策略评审 |
|
||||
390
docs/architecture/audit/archive/ai-module-deep-audit-report.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# AI 模块审计报告 V3 — 架构解耦与全角色对标
|
||||
|
||||
> 审计范围:基于 V1(`ai-module-audit-report.md`)与 V2(`ai-module-audit-report-v2.md`)已完成实现,进行第三轮深度架构审计。
|
||||
> 审计日期:2026-06-24
|
||||
> 审计方法:全文件逐行扫描 + 三层架构合规性矩阵 + 跨模块依赖图 + K12 行业标杆对标(Khanmigo / Duolingo Max / Squirrel AI / Century Tech / MagicSchool AI)
|
||||
> 审计依据:`docs/standards/coding-standards.md`、`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、项目规则
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布(30 个文件)
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| **types** | `modules/ai/types.ts` | 295 | AiService/AiClientService 接口 + 8 个业务场景类型 |
|
||||
| **schema** | `modules/ai/schema.ts` | 227 | Zod 验证(8 输入 + 8 输出) |
|
||||
| **actions** | `modules/ai/actions.ts` | 381 | 9 个 Server Actions(含权限校验) |
|
||||
| **data-access** | `modules/ai/data-access.ts` | 138 | AI 事件内存存储 + 统计聚合 |
|
||||
| **services** | `modules/ai/services/ai-service.ts` | 439 | DefaultAiService 实现(8 方法) |
|
||||
| **services** | `modules/ai/services/prompt-templates.ts` | 277 | 10 个系统提示词模板 |
|
||||
| **services** | `modules/ai/services/usage-tracker.ts` | 99 | AI 使用量埋点 |
|
||||
| **services** | `modules/ai/services/content-safety.ts` | 291 | 内容安全过滤(输入/输出/配额/Socratic) |
|
||||
| **context** | `modules/ai/context/ai-client-provider.tsx` | 62 | React Context Provider + Hooks |
|
||||
| **hooks** | `modules/ai/hooks/use-ai-chat-stream.ts` | 155 | 流式 AI 对话 Hook |
|
||||
| **hooks** | `modules/ai/hooks/use-ai-chat.ts` | 57 | 非流式 AI 对话 Hook |
|
||||
| **hooks** | `modules/ai/hooks/use-ai-suggestion.ts` | 72 | AI 建议 Hook |
|
||||
| **hooks** | `modules/ai/hooks/use-floating-ball.ts` | 243 | 悬浮球拖拽 Hook |
|
||||
| **hooks** | `modules/ai/hooks/stream-utils.ts` | 135 | SSE 流解析工具 |
|
||||
| **components** | `modules/ai/components/ai-chat-panel.tsx` | 417 | AI 对话面板 |
|
||||
| **components** | `modules/ai/components/ai-assistant-widget.tsx` | 329 | 全局 AI 助手悬浮球 |
|
||||
| **components** | `modules/ai/components/ai-markdown-renderer.tsx` | 143 | Markdown 渲染器 |
|
||||
| **components** | `modules/ai/components/ai-chart-renderer.tsx` | 329 | 图表渲染器 |
|
||||
| **components** | `modules/ai/components/ai-error-boundary.tsx` | 88 | AI 错误边界 |
|
||||
| **components** | `modules/ai/components/ai-skeleton.tsx` | 47 | 骨架屏 |
|
||||
| **components** | `modules/ai/components/ai-provider-selector.tsx` | 89 | 服务商选择器 |
|
||||
| **components** | `modules/ai/components/ai-grading-assist.tsx` | 173 | AI 批改辅助 |
|
||||
| **components** | `modules/ai/components/ai-error-book-analysis.tsx` | 246 | 错题本 AI 分析 |
|
||||
| **components** | `modules/ai/components/ai-lesson-content-generator.tsx` | 180 | 备课内容生成器 |
|
||||
| **components** | `modules/ai/components/ai-question-variant-generator.tsx` | 218 | 题目变体生成器 |
|
||||
| **components** | `modules/ai/components/ai-child-summary.tsx` | 186 | 家长学情摘要 |
|
||||
| **components** | `modules/ai/components/ai-usage-dashboard.tsx` | 221 | 管理员使用统计 |
|
||||
| **components** | `modules/ai/components/ai-study-path.tsx` | 200 | 学生学习路径 |
|
||||
| **components** | `modules/ai/components/ai-suggestion-card.tsx` | 164 | 相似题建议卡片 |
|
||||
| **api** | `app/api/ai/chat/route.ts` | 48 | 非流式聊天端点 |
|
||||
| **api** | `app/api/ai/chat/stream/route.ts` | 237 | SSE 流式端点 |
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
客户端组件
|
||||
└─▶ useAiClient() / useAiClientOptional()
|
||||
└─▶ AiClientProvider (app/layout.tsx 或子页面注入)
|
||||
└─▶ Server Actions (modules/ai/actions.ts)
|
||||
└─▶ createAiService(userId) → DefaultAiService
|
||||
└─▶ createAiChatCompletion (shared/lib/ai)
|
||||
└─▶ OpenAI SDK + ai_providers 表
|
||||
|
||||
SSE 流式端点(独立路径):
|
||||
app/api/ai/chat/stream/route.ts
|
||||
└─▶ requirePermission(AI_CHAT)
|
||||
└─▶ content-safety (filterUserInput / tryConsumeDailyQuota)
|
||||
└─▶ createAiChatCompletionStream (shared/lib/ai)
|
||||
└─▶ content-safety (filterAiOutput / validateSocraticOutput)
|
||||
```
|
||||
|
||||
### 1.3 架构图记录情况
|
||||
|
||||
- `004_architecture_impact_map.md` 第 2.29 节完整记录了 AI 模块(V2/V3/V4 变更)
|
||||
- `005_architecture_data.json` 包含 `modules.ai` 节点(exports/dependencies/integrations/safety/streaming/i18n)
|
||||
- **结论:架构图对 AI 模块的记录基本完整**,但未记录跨模块违规依赖(见 2.1.1)。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 架构分层问题
|
||||
|
||||
#### 问题 2.1.1:5 处跨模块直接依赖 `shared/lib/ai`(绕过 modules/ai)
|
||||
|
||||
- **位置**:
|
||||
- [ai-suggest.ts:5](file:///e:/Desktop/CICD/src/modules/lesson-preparation/ai-suggest.ts#L5) — `import { createAiChatCompletion } from "@/shared/lib/ai"`
|
||||
- [settings/actions.ts:14](file:///e:/Desktop/CICD/src/modules/settings/actions.ts#L14) — `import { encryptAiApiKey, getAiErrorMessage, testAiProviderById, testAiProviderConfig } from "@/shared/lib/ai"`
|
||||
- [exams/actions.ts:987](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L987) — 动态 `import("@/shared/lib/ai")`
|
||||
- [exams/ai-pipeline/request.ts:11](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/request.ts#L11) — `import { createAiChatCompletion, getAiErrorMessage } from "@/shared/lib/ai"`
|
||||
- [exams/ai-pipeline/parse.ts:11](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/parse.ts#L11) — `import { createAiChatCompletion } from "@/shared/lib/ai"`
|
||||
- **原因**:AI 模块在 V2 重构后才形成独立模块,但这些历史调用点未同步迁移。
|
||||
- **后果**:绕过 `modules/ai` 的内容安全过滤、每日配额、Socratic 模式、使用量埋点等保护机制;AI 调用无法统一治理。
|
||||
- **违反规则**:`项目规则 → 架构分层规则 → 模块间只能通过对方 data-access 通信`;`项目规则 → Server Action 规范 → 每个 Action 必须调用 requirePermission()`。
|
||||
|
||||
#### 问题 2.1.2:非流式聊天端点绕过 modules/ai
|
||||
|
||||
- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
|
||||
- **现状**:直接调用 `shared/lib/ai` 的 `createAiChatCompletion`,未走 `aiChatAction`,缺失内容安全过滤、每日配额、Socratic 模式等保护。
|
||||
- **后果**:与非流式端点(`/api/ai/chat/stream`)形成安全策略不一致;学生可通过非流式端点绕过 Socratic 模式获取直接答案。
|
||||
- **违反规则**:`项目规则 → 安全规范`;`项目规则 → Server Action 规范`。
|
||||
|
||||
#### 问题 2.1.3:4 个子页面重复创建 AiClientService
|
||||
|
||||
- **位置**:
|
||||
- [teacher/lesson-plans/[planId]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx)
|
||||
- [teacher/homework/submissions/[submissionId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/homework/submissions/[submissionId]/page.tsx)
|
||||
- [student/error-book/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/error-book/page.tsx)
|
||||
- [teacher/exams/[id]/build/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/[id]/build/page.tsx)
|
||||
- **现状**:每个页面各自创建只含 6 个 Action 的 `AiClientService`,覆盖 layout.tsx 的全局 Provider(含 9 个 Action)。
|
||||
- **后果**:代码重复;`generateChildSummary`、`recommendStudyPath`、`getAiUsageStats` 在这些页面内为 `undefined`,若未来组件调用将运行时错误。
|
||||
- **违反规则**:`项目规则 → 工程约定 → 最大化复用`。
|
||||
|
||||
### 2.2 权限问题
|
||||
|
||||
#### 问题 2.2.1:非流式聊天端点未走 requirePermission 体系
|
||||
|
||||
- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
|
||||
- **现状**:虽然调用了 `requirePermission(Permissions.AI_CHAT)`,但绕过了 `modules/ai/actions.ts` 的 `aiChatAction`,导致内容安全过滤、每日配额、Socratic 模式等保护机制缺失。
|
||||
- **后果**:权限校验通过但安全策略不一致。
|
||||
- **违反规则**:`项目规则 → Server Action 规范 → 每个 Action 必须调用 requirePermission()`(虽调用但绕过 Action 编排层)。
|
||||
|
||||
### 2.3 国际化问题
|
||||
|
||||
#### 问题 2.3.1:ai-chart-renderer.tsx 硬编码中文
|
||||
|
||||
- **位置**:[ai-chart-renderer.tsx:147](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L147)
|
||||
- **代码**:`图表数据格式错误,无法渲染`
|
||||
- **后果**:无法切换语言。
|
||||
- **违反规则**:`项目规则 → 所有用户可见文本必须适配 i18n`。
|
||||
|
||||
#### 问题 2.3.2:ai-assistant-widget.tsx systemPrompt 硬编码英文
|
||||
|
||||
- **位置**:[ai-assistant-widget.tsx:230-326](file:///e:/Desktop/CICD/src/modules/ai/components/ai-assistant-widget.tsx#L230-L326)
|
||||
- **现状**:`inferContextFromPath` 函数中所有 `systemPrompt` 和 `contextMessage` 为硬编码英文。
|
||||
- **判定**:systemPrompt 是发送给 AI 的指令(非用户可见文本),可保留英文(模型兼容性最佳);但 `contextMessage` 显示在 UI 中,应 i18n 化。
|
||||
- **后果**:contextMessage 无法国际化。
|
||||
- **违反规则**:`项目规则 → 所有用户可见文本必须适配 i18n`。
|
||||
|
||||
### 2.4 类型安全问题
|
||||
|
||||
#### 问题 2.4.1:ai-markdown-renderer.tsx 使用 as 断言
|
||||
|
||||
- **位置**:[ai-markdown-renderer.tsx:92](file:///e:/Desktop/CICD/src/modules/ai/components/ai-markdown-renderer.tsx#L92)
|
||||
- **代码**:`lang.slice(CHART_LANG_PREFIX.length) as AiChartType`
|
||||
- **后果**:若 lang 不在 AiChartType 枚举内,类型不安全。
|
||||
- **违反规则**:`项目规则 → TypeScript 规则 → 禁止 as 断言`。
|
||||
|
||||
#### 问题 2.4.2:ai-provider-selector.tsx 使用 as 断言
|
||||
|
||||
- **位置**:[ai-provider-selector.tsx:66](file:///e:/Desktop/CICD/src/modules/ai/components/ai-provider-selector.tsx#L66)
|
||||
- **代码**:`field.value as string`
|
||||
- **后果**:react-hook-form 的 field.value 类型应为泛型,此处强转。
|
||||
- **违反规则**:`项目规则 → TypeScript 规则 → 禁止 as 断言`。
|
||||
|
||||
#### 问题 2.4.3:ai-chart-renderer.tsx 使用 as 断言
|
||||
|
||||
- **位置**:[ai-chart-renderer.tsx:117](file:///e:/Desktop/CICD/src/modules/ai/components/ai-chart-renderer.tsx#L117)
|
||||
- **代码**:`JSON.parse(data) as AiChartSpec`
|
||||
- **后果**:JSON 结构不可信,直接断言可能运行时错误。
|
||||
- **违反规则**:`项目规则 → TypeScript 规则 → 禁止 as 断言`。
|
||||
|
||||
### 2.5 错误处理问题
|
||||
|
||||
#### 问题 2.5.1:ai-suggestion-card.tsx 未包裹 Error Boundary
|
||||
|
||||
- **位置**:[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx)
|
||||
- **现状**:组件内部有 try/catch 处理异步错误,但未用 `AiErrorBoundary` 包裹渲染期错误。
|
||||
- **后果**:若 `result.data` 结构异常或 `questions.map` 渲染时抛错,整页崩溃。
|
||||
- **违反规则**:`审计要求 → 每个独立数据区块必须用 React Error Boundary 包裹`。
|
||||
|
||||
#### 问题 2.5.2:非流式聊天端点错误处理不完整
|
||||
|
||||
- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
|
||||
- **现状**:`getStatusFromError` 基于错误消息字符串匹配状态码,脆弱且不可靠。
|
||||
- **后果**:错误分类不准确。
|
||||
- **违反规则**:`项目规则 → 错误处理`。
|
||||
|
||||
### 2.6 文件大小问题
|
||||
|
||||
#### 问题 2.6.1:use-floating-ball.ts 超出 Hook 行数限制
|
||||
|
||||
- **位置**:[use-floating-ball.ts](file:///e:/Desktop/CICD/src/modules/ai/hooks/use-floating-ball.ts)
|
||||
- **现状**:243 行,超出 Hook 文件 80 行建议上限 163 行。
|
||||
- **后果**:职责过多(位置加载/保存、拖拽事件、边缘吸附、半隐藏),难以维护和测试。
|
||||
- **违反规则**:`项目规则 → 单文件行数 → 自定义 Hook:建议 ≤ 80 行`。
|
||||
|
||||
### 2.7 可复用性问题
|
||||
|
||||
#### 问题 2.7.1:4 个子页面重复创建 AiClientService
|
||||
|
||||
- 见问题 2.1.3。
|
||||
|
||||
#### 问题 2.7.2:ai-suggestion-card.tsx 未被使用
|
||||
|
||||
- **位置**:[ai-suggestion-card.tsx](file:///e:/Desktop/CICD/src/modules/ai/components/ai-suggestion-card.tsx)
|
||||
- **现状**:组件已实现但无任何引用。
|
||||
- **后果**:死代码,维护负担。
|
||||
- **违反规则**:`审计要求 → 最大化复用`(应集成到错题本等页面)。
|
||||
|
||||
### 2.8 监控问题
|
||||
|
||||
#### 问题 2.8.1:非流式聊天端点无使用量埋点
|
||||
|
||||
- **位置**:[app/api/ai/chat/route.ts](file:///e:/Desktop/CICD/src/app/api/ai/chat/route.ts)
|
||||
- **现状**:未调用 `trackAiUsage`,调用数据不入统计。
|
||||
- **后果**:管理员仪表盘数据不完整。
|
||||
- **违反规则**:`审计要求 → 监控`。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
### 3.1 与 Khanmigo(Khan Academy)的差距
|
||||
|
||||
| 功能 | Khanmigo | 本项目 | 差距 |
|
||||
|------|----------|--------|------|
|
||||
| 学生 Socratic 模式 | ✅ 强制引导式 | ✅ 已实现 | 无 |
|
||||
| 教师备课助手 | ✅ 课程计划生成 | ✅ 已实现 | 无 |
|
||||
| 多语言支持 | ✅ 20+ 语言 | ⚠️ 仅中英文 | 缺少多语言 Prompt |
|
||||
| 学生情绪识别 | ✅ 检测挫败感 | ❌ 未实现 | 缺少情绪分析 |
|
||||
| 家长沟通建议 | ✅ 家庭教育指导 | ✅ 已实现 | 无 |
|
||||
|
||||
### 3.2 与 Duolingo Max 的差距
|
||||
|
||||
| 功能 | Duolingo Max | 本项目 | 差距 |
|
||||
|------|-------------|--------|------|
|
||||
| 解释我的答案 | ✅ AI 解释错误原因 | ❌ 未实现 | 缺少错题 AI 解释 |
|
||||
| 角色扮演练习 | ✅ 情景对话练习 | ❌ 未实现 | 缺少口语/情景练习 |
|
||||
| 个性化复习 | ✅ 基于遗忘曲线 | ⚠️ 仅 SM2 算法 | AI 未参与复习规划 |
|
||||
|
||||
### 3.3 与 Squirrel AI 的差距
|
||||
|
||||
| 功能 | Squirrel AI | 本项目 | 差距 |
|
||||
|------|-------------|--------|------|
|
||||
| 纳米级知识图谱 | ✅ 10000+ 知识点 | ⚠️ V3 已集成 | 知识图谱粒度较粗 |
|
||||
| 自适应学习路径 | ✅ 实时调整 | ✅ V3 已实现 | 无 |
|
||||
| 多模态学习 | ✅ 视频+图文+音频 | ❌ 仅文本 | 缺少多模态 |
|
||||
| 学习风格识别 | ✅ VARK 模型 | ❌ 未实现 | 缺少学习风格分析 |
|
||||
|
||||
### 3.4 与 MagicSchool AI 的差距
|
||||
|
||||
| 功能 | MagicSchool AI | 本项目 | 差距 |
|
||||
|------|----------------|--------|------|
|
||||
| 50+ AI 工具 | ✅ 丰富工具集 | ⚠️ 8 个能力 | 工具数量不足 |
|
||||
| IEP 生成 | ✅ 特殊教育计划 | ❌ 未实现 | 缺少 IEP |
|
||||
| 家校沟通模板 | ✅ 邮件/通知模板 | ❌ 未实现 | 缺少沟通模板 |
|
||||
| 跨学科项目设计 | ✅ PBL 项目设计 | ❌ 未实现 | 缺少 PBL |
|
||||
|
||||
### 3.5 关键差距总结
|
||||
|
||||
1. **AI 能力数量不足**:仅 8 个能力,行业平均 15-20 个
|
||||
2. **多模态缺失**:仅支持文本,缺少图像/语音/视频输入
|
||||
3. **学习风格识别缺失**:未识别学生 VARK 学习风格
|
||||
4. **情绪识别缺失**:未检测学生挫败感/兴奋度
|
||||
5. **IEP/PBL 缺失**:未支持特殊教育和项目式学习
|
||||
6. **跨模块数据联动不足**:AI 未与考勤、行为、心理等数据联动分析
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急 — 安全/架构合规)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P0-1 | 5 处跨模块直接依赖 shared/lib/ai | 迁移至通过 modules/ai 的 data-access 或 actions 调用 |
|
||||
| P0-2 | 非流式聊天端点绕过 modules/ai | 重构为调用 aiChatAction 或迁移安全策略 |
|
||||
| P0-3 | ai-chart-renderer.tsx 硬编码中文 | 迁移至 i18n |
|
||||
| P0-4 | 3 处 as 断言 | 改用类型守卫函数 |
|
||||
|
||||
### P1(重要 — 规范/复用)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P1-1 | 4 个子页面重复创建 AiClientService | 提取 createAiClientService 工厂函数 |
|
||||
| P1-2 | use-floating-ball.ts 超出行数限制 | 拆分为 use-drag-position + use-edge-snap |
|
||||
| P1-3 | ai-suggestion-card.tsx 未包裹 Error Boundary | 集成时用 AiErrorBoundary 包裹 |
|
||||
| P1-4 | ai-assistant-widget.tsx contextMessage 硬编码 | 迁移至 i18n |
|
||||
| P1-5 | 非流式端点无使用量埋点 | 集成 trackAiUsage |
|
||||
|
||||
### P2(增强 — 行业对标)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P2-1 | 缺少错题 AI 解释 | 新增 explainErrorAction |
|
||||
| P2-2 | 缺少学习风格识别 | 新增 VARK 评估 |
|
||||
| P2-3 | 缺少 IEP 生成 | 新增特殊教育模块 |
|
||||
| P2-4 | 缺少家校沟通模板 | 新增沟通模板生成 |
|
||||
| P2-5 | 缺少多模态输入 | 支持图像/语音输入 |
|
||||
| P2-6 | 缺少情绪识别 | 集成情绪分析 API |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
### 5.1 需补充的节点
|
||||
|
||||
1. **跨模块违规依赖**:在 `005_architecture_data.json` 的 `dependencyMatrix` 中标注 `lesson-preparation`、`settings`、`exams` 对 `shared/lib/ai` 的违规依赖(应改为通过 `modules/ai`)。
|
||||
2. **非流式端点安全策略缺失**:在 `004_architecture_impact_map.md` 的 AI 模块安全机制章节补充非流式端点的安全策略差距。
|
||||
3. **ai-suggestion-card.tsx 未使用**:在文件清单中标注该组件为"已实现未集成"。
|
||||
|
||||
### 5.2 需修改的节点
|
||||
|
||||
1. **V4 超时优化**:已在 V4 中记录(`shared/lib/ai/client.ts` 按场景分离超时)。
|
||||
2. **V4 规范修复**:已在 V4 中记录(ai-service.ts 消除 as 断言、actions.ts 消除非空断言、ai-assistant-widget.tsx i18n 修复)。
|
||||
|
||||
---
|
||||
|
||||
## 六、实施记录
|
||||
|
||||
### 6.1 P0 改进实施
|
||||
|
||||
#### P0-1:迁移跨模块依赖(部分)
|
||||
|
||||
**已实施**:
|
||||
- 修复 `ai-chart-renderer.tsx` 硬编码中文(P0-3)
|
||||
- 修复 `ai-markdown-renderer.tsx` as 断言(P0-4 部分)
|
||||
- 修复 `ai-provider-selector.tsx` as 断言(P0-4 部分)
|
||||
- 修复 `ai-chart-renderer.tsx` as 断言(P0-4 部分)
|
||||
|
||||
**未实施(需中长期计划)**:
|
||||
- 5 处跨模块直接依赖 shared/lib/ai 的迁移(涉及 exams/lesson-preparation/settings 三个模块的重构,影响范围大,需单独排期)
|
||||
|
||||
#### P0-2:非流式聊天端点安全策略重构(已实施)
|
||||
|
||||
**实施**:重构 `app/api/ai/chat/route.ts`,与流式端点 `/api/ai/chat/stream` 安全策略完全对齐:
|
||||
- Zod 校验输入(`AiChatInputSchema`,限制消息数 50/长度 8000)
|
||||
- `tryConsumeDailyQuota` 原子化每日限额(防 TOCTOU 竞态)
|
||||
- `filterUserInput` 输入安全过滤
|
||||
- `filterAiOutput` 输出安全过滤
|
||||
- 学生侧 Socratic 模式(服务端强制 `SOCRATIC_TUTOR_SYSTEM_PROMPT`,忽略客户端 systemPrompt)
|
||||
- `validateSocraticOutput` 苏格拉底式输出校验
|
||||
- 过滤/失败时 `refundDailyQuota`(不惩罚用户)
|
||||
- `trackEvent` 使用量埋点(成功/失败均记录)
|
||||
|
||||
#### P0-3:ai-chart-renderer.tsx 硬编码中文(已实施)
|
||||
|
||||
**实施**:添加 i18n 键 `ai.chart.parseError`,替换硬编码中文。
|
||||
|
||||
#### P0-4:as 断言修复(已实施)
|
||||
|
||||
**实施**:
|
||||
- `ai-markdown-renderer.tsx`:改用类型守卫函数 `isAiChartType`
|
||||
- `ai-provider-selector.tsx`:改用 `String(field.value ?? "")`
|
||||
- `ai-chart-renderer.tsx`:改用 Zod schema 校验
|
||||
|
||||
### 6.2 P1 改进实施
|
||||
|
||||
#### P1-1:提取 createAiClientService 工厂(已实施)
|
||||
|
||||
**实施**:新建 `modules/ai/context/create-ai-client-service.ts`,导出 `createFullAiClientService`(含全部 9 个 Action)和 `createCoreAiClientService`(仅 6 个常用 Action)两个工厂函数。`app/(dashboard)/layout.tsx` 使用前者,4 个子页面(error-book、lesson-plans、homework、exams)使用后者,消除重复代码。
|
||||
|
||||
#### P1-2:拆分 use-floating-ball.ts(已实施)
|
||||
|
||||
**实施**:将 243 行的 `use-floating-ball.ts` 拆分为 3 个文件:
|
||||
- `use-position-persistence.ts`:Position 类型、常量、clamp/load/save 纯函数、位置状态 Hook(localStorage + resize 校正)
|
||||
- `use-drag-position.ts`:拖拽状态 + pointer 事件处理 Hook(通过回调委托业务逻辑)
|
||||
- `use-floating-ball.ts`:主组合 Hook(边缘吸附 + 半隐藏 + hovered 状态 + show/resetPosition)
|
||||
|
||||
#### P1-3:ai-suggestion-card.tsx Error Boundary(已实施)
|
||||
|
||||
**实施**:将原组件重命名为 `AiSuggestionCardInner`,新建 `AiSuggestionCard` 包装器用 `AiErrorBoundary` 包裹内部组件,保持公开 API 不变。
|
||||
|
||||
#### P1-4:ai-assistant-widget.tsx contextMessage i18n(已实施)
|
||||
|
||||
**实施**:在 `en/ai.json` 和 `zh-CN/ai.json` 中添加 `chat.contextMessage.*` 翻译键(7 个场景:teacherGrading/teacherLesson/teacherExam/studentErrorBook/studentHomework/parent/admin),替换 `inferContextFromPath` 中 7 处硬编码英文。
|
||||
|
||||
#### P1-5:非流式端点使用量埋点(已实施)
|
||||
|
||||
**实施**:在 `app/api/ai/chat/route.ts` 中集成 `trackEvent`(事件名 `ai.chat`),成功时记录 durationMs/tokenCount,失败时记录 errorMessage/durationMs。与流式端点(`ai.chat_stream`)对齐。
|
||||
|
||||
### 6.3 P2 改进(中长期计划)
|
||||
|
||||
#### P2-1:错题 AI 解释(已实施)
|
||||
|
||||
**实施**:
|
||||
- 新增 `ExplainErrorInput`/`ExplainErrorResult` 类型(`modules/ai/types.ts`)
|
||||
- 新增 `ExplainErrorInputSchema`/`ExplainErrorResultSchema` Zod 校验(`modules/ai/schema.ts`)
|
||||
- 新增 `EXPLAIN_ERROR_SYSTEM_PROMPT` 提示词(`modules/ai/services/prompt-templates.ts`)
|
||||
- 新增 `explainError` 服务方法(`modules/ai/services/ai-service.ts`)
|
||||
- 新增 `explainErrorAction` Server Action(`modules/ai/actions.ts`,权限:AI_CHAT + ERROR_BOOK_READ)
|
||||
- 更新 `AiService`/`AiClientService` 接口,添加 `explainError` 方法
|
||||
- 更新 `createFullAiClientService` 工厂函数包含 `explainError`
|
||||
- 更新 `AiCapability` 类型添加 `"explain-error"`
|
||||
- 更新 `AiUsageEvent`/`AI_EVENT_MAP` 添加 `explain_error` 埋点
|
||||
- 更新 i18n 翻译键 `capability.explainError`(en/zh-CN)
|
||||
- 更新架构文档 004/005
|
||||
|
||||
**未实施(需后续排期)**:
|
||||
- P2-2:VARK 学习风格评估(需新增评估模块 + DB 表)
|
||||
- P2-3:IEP 特殊教育计划生成(需新增特殊教育模块)
|
||||
- P2-4:家校沟通模板生成(需新增模板管理模块)
|
||||
- P2-5:多模态输入支持(需接入图像/语音 API)
|
||||
- P2-6:情绪识别(需接入情绪分析 API)
|
||||
392
docs/architecture/audit/archive/announcements-audit-report.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# 公告(announcements)模块审计报告
|
||||
|
||||
> 审查日期:2026-06-25
|
||||
> 审查范围:`src/modules/announcements/**`、`src/app/(dashboard)/announcements/**`、`src/app/(dashboard)/admin/announcements/**`、`src/shared/i18n/messages/{zh-CN,en}/announcements.json`
|
||||
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.16、`docs/architecture/005_architecture_data.json#announcements`
|
||||
> 关联报告:`announcements-messages-audit-report.md`(合并版,2026-06-22,已不再维护,本文为公告模块的独立深度审计)
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
| 层 | 路径 | 文件 | 行数 | 说明 |
|
||||
|----|------|------|------|------|
|
||||
| 路由 · 用户端 | `src/app/(dashboard)/announcements/page.tsx` | 1 | 36 | 列表页(所有非管理角色共用) |
|
||||
| 路由 · 用户端 | `src/app/(dashboard)/announcements/[id]/page.tsx` | 1 | 41 | 详情页(只读) |
|
||||
| 路由 · 用户端 | `src/app/(dashboard)/announcements/{loading,error,[id]/error}.tsx` | 3 | 60 | 骨架屏 + 错误边界 |
|
||||
| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/page.tsx` | 1 | 45 | 管理列表页 |
|
||||
| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/[id]/page.tsx` | 1 | 47 | 编辑页(直接渲染表单,无详情视图) |
|
||||
| 路由 · 管理端 | `src/app/(dashboard)/admin/announcements/{loading,error,[id]/error}.tsx` | 3 | 64 | 骨架屏 + 错误边界 |
|
||||
| 模块 | `src/modules/announcements/actions.ts` | 1 | 403 | 9 个 Server Action + 通知编排 + 埋点 |
|
||||
| 模块 | `src/modules/announcements/data-access.ts` | 1 | 413 | CRUD + 发布/归档 + 置顶/已读 + 3 个页面编排函数 |
|
||||
| 模块 | `src/modules/announcements/types.ts` | 1 | 75 | 类型定义 |
|
||||
| 模块 | `src/modules/announcements/schema.ts` | 1 | 95 | Zod 校验 + `refineAudience` 条件校验 |
|
||||
| 模块 · 组件 | `src/modules/announcements/components/announcement-list.tsx` | 1 | 126 | 列表(纯服务端过滤) |
|
||||
| 模块 · 组件 | `src/modules/announcements/components/announcement-card.tsx` | 1 | 125 | 卡片 + 置顶切换 |
|
||||
| 模块 · 组件 | `src/modules/announcements/components/announcement-detail.tsx` | 1 | 267 | 详情 + 管理操作 + 自动已读 |
|
||||
| 模块 · 组件 | `src/modules/announcements/components/announcement-form.tsx` | 1 | 230 | 创建/编辑表单 |
|
||||
| 模块 · 组件 | `src/modules/announcements/components/admin-announcements-view.tsx` | 1 | 67 | 管理端视图(列表 + 创建 Dialog) |
|
||||
| i18n | `src/shared/i18n/messages/{zh-CN,en}/announcements.json` | 2 | 103/103 | 11 命名空间翻译字典 |
|
||||
| 测试 | — | 0 | 0 | **零测试文件** |
|
||||
|
||||
文件大小均在规范内(组件 ≤500 行,actions/data-access ≤800 行)。
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
[Route] /announcements/page.tsx
|
||||
└─▶ announcements/data-access.getUserAnnouncementsPageData(userId, dataScope)
|
||||
├─▶ resolveAudience(userId, dataScope) // 内部函数
|
||||
│ └─▶ classes/data-access.{getClassGradeId | getStudentActiveClassId | getStudentActiveGradeId}
|
||||
└─▶ getAnnouncements({ status: "published", audience })
|
||||
|
||||
[Route] /announcements/[id]/page.tsx ⚠️ 未做受众/状态过滤
|
||||
├─▶ announcements/data-access.getAnnouncementById(id)
|
||||
└─▶ announcements/data-access.isAnnouncementReadByUser(id, userId)
|
||||
|
||||
[Route] /admin/announcements/page.tsx
|
||||
└─▶ announcements/data-access.getAdminAnnouncementsPageData(status)
|
||||
├─▶ getAnnouncements({ status })
|
||||
├─▶ school/data-access.getGrades()
|
||||
└─▶ classes/data-access.getAdminClasses()
|
||||
|
||||
[Route] /admin/announcements/[id]/page.tsx
|
||||
└─▶ announcements/data-access.getEditAnnouncementPageData(id)
|
||||
├─▶ getAnnouncementById(id)
|
||||
└─▶ school/data-access.getGrades()
|
||||
|
||||
[Action] createAnnouncementAction / updateAnnouncementAction / publishAnnouncementAction
|
||||
└─▶ notifyAnnouncementPublished(announcement)
|
||||
├─▶ resolveTargetUserIds(announcement) // ⚠️ 纯业务逻辑在 actions.ts
|
||||
│ ├─▶ users/data-access.{getAllUserIds | getUserIdsByGradeId}
|
||||
│ └─▶ classes/data-access.{getStudentIdsByClassId | getTeacherIdsByClassIds}
|
||||
└─▶ notifications.sendBatchNotifications(payloads)
|
||||
```
|
||||
|
||||
### 1.3 架构图记录情况
|
||||
|
||||
`004_architecture_impact_map.md` §2.16 对 announcements 模块的记录较为完整:
|
||||
- ✅ 导出函数(9 个 Action + 14 个 data-access 函数)记录准确
|
||||
- ✅ 依赖关系(`shared/*`、`@/auth`、`school`、`classes`、`users`、`notifications`)记录准确
|
||||
- ✅ 已修复问题清单(P1-2/P1-5/P1-6/V2-P0-2/V2-P1-1/V2-P1-4/V2-P2-13d/V3-P0-2)记录详实
|
||||
- ✅ 文件清单与组件清单行数准确
|
||||
|
||||
**但架构图存在以下遗漏/不一致**(详见第五章):
|
||||
1. 未记录 `AnnouncementDetail` 组件中 `canManage=true` 分支为**死代码**(无任何页面使用)
|
||||
2. 未记录 `getAnnouncementReadStatusAction` 为**死代码**(无任何调用方)
|
||||
3. 未记录 `/announcements/[id]` 路由层存在的**安全越权风险**(无受众过滤)
|
||||
4. 未记录 `resolveAudience` 内部函数的**多孩子/多年级数据截断 Bug**
|
||||
5. 未记录 actions 返回的英文字符串未走 i18n 的问题
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 【P0 · 安全越权】用户端详情页无受众/状态过滤
|
||||
|
||||
- **位置**:[src/app/(dashboard)/announcements/[id]/page.tsx:26-27](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/[id]/page.tsx)
|
||||
- **问题**:详情页直接调用 `getAnnouncementById(id)`,未传入 `audience` 也未校验 `status === "published"`。
|
||||
- **后果**:任意持有 `ANNOUNCEMENT_READ` 权限的登录用户,只要知道/猜到公告 ID(cuid2),即可读取:
|
||||
- 草稿(`status="draft"`)公告——提前泄露未发布内容
|
||||
- 已归档(`status="archived"`)公告——绕过归档语义
|
||||
- 其他年级/班级的定向公告——跨班级信息泄露(如某班处分通知被外班学生读到)
|
||||
- **违反规则**:项目规则"安全性:所有敏感数据查询必须在 data-access 层结合当前用户权限过滤" + "Parent routes must include permission checks with both `parentId` and `studentId` to prevent information leakage"。
|
||||
- **根因**:`getAnnouncementById` 设计为通用读取函数,未提供"按受众过滤"重载;路由层也未在读取后做二次校验。
|
||||
|
||||
### 2.2 【P0 · 数据截断】`resolveAudience` 仅取首个 gradeId / classId / childId
|
||||
|
||||
- **位置**:[src/modules/announcements/data-access.ts:351-395](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts)
|
||||
- **问题**:
|
||||
```ts
|
||||
if (dataScope.type === "grade_managed") {
|
||||
const gradeId = dataScope.gradeIds[0] // ⚠️ 仅取第一个
|
||||
}
|
||||
if (dataScope.type === "class_members" || dataScope.type === "class_taught") {
|
||||
const classId = dataScope.classIds[0] // ⚠️ 仅取第一个
|
||||
}
|
||||
if (dataScope.type === "children") {
|
||||
const childId = dataScope.childrenIds[0] // ⚠️ 仅取第一个孩子
|
||||
}
|
||||
```
|
||||
- **后果**:
|
||||
- **家长**有多个孩子在不同班级/年级时,只能看到第一个孩子的定向公告,第二个孩子的班主任通知完全不可见——直接违反 K12 家长端核心诉求。
|
||||
- **年级主任**管理多个年级时,只能看到第一个年级的公告。
|
||||
- **教师**任课多个班级时,只能看到第一个班级的公告。
|
||||
- **违反规则**:项目规则"Parent routes must include permission checks with both `parentId` and `studentId`" 与"data-access 层结合当前用户权限过滤"。
|
||||
- **根因**:`getAnnouncements` 的 `audience` 参数设计为单值 `{ gradeId?, classId? }`,不支持多值;`resolveAudience` 为迁就该签名做了截断。
|
||||
|
||||
### 2.3 【P0 · 越权写】置顶/已读 Action 缺少资源所有权二次校验
|
||||
|
||||
- **位置**:[src/modules/announcements/actions.ts:335-376](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)
|
||||
- **问题**:
|
||||
- `toggleAnnouncementPinAction` 仅校验 `ANNOUNCEMENT_MANAGE`,未校验公告是否存在、未校验调用者是否为该公告作者或管理员范围。
|
||||
- `markAnnouncementAsReadAction` 仅校验 `ANNOUNCEMENT_READ`,未校验该公告是否对当前用户可见(即未结合 2.1 的受众过滤)。任意用户可对任意公告 ID(包括草稿、他人班级公告)写入已读记录,污染 `announcement_reads` 表。
|
||||
- **后果**:数据库完整性被破坏;统计 `readCount` 失真;为后续基于已读率的分析埋下错误数据。
|
||||
- **违反规则**:项目规则"Server Action 二次校验"。
|
||||
- **根因**:Action 层信任了 `requirePermission` 的角色校验,未做资源级(resource-level)授权。
|
||||
|
||||
### 2.4 【P1 · i18n 违规】Actions 返回英文硬编码消息
|
||||
|
||||
- **位置**:[src/modules/announcements/actions.ts](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts) 全文
|
||||
- **问题**:所有 Action 返回的 `ActionState.message` 均为英文字符串:
|
||||
- `"Announcement created"` / `"Announcement updated"` / `"Announcement deleted"`
|
||||
- `"Announcement published"` / `"Announcement archived"`
|
||||
- `"Announcement not found"` / `"Invalid form data"` / `"Unexpected error"`
|
||||
- `"Pin status toggled"` / `"Announcement marked as read"`
|
||||
- 这些 message 通过 `toast.success(res.message)` / `toast.error(res.message)` 直接展示给用户(见 [announcement-detail.tsx:80,100,115](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx) 与 [announcement-form.tsx:80,88](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx))。
|
||||
- **后果**:中文用户在创建/发布/删除公告后看到英文 Toast,i18n 字典中已定义的 `messages.created` / `messages.updated` 等翻译键完全未使用。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n" + "Server Action 返回值统一采用 `ActionState<T>` 类型"(隐含 message 应可本地化)。
|
||||
- **根因**:Actions 在 try 块内同步返回字符串,未通过 `getTranslations("announcements")` 获取本地化文案;i18n 字典定义了键但 Action 未消费。
|
||||
|
||||
### 2.5 【P1 · 死代码】`AnnouncementDetail` 管理分支与 `getAnnouncementReadStatusAction` 无调用方
|
||||
|
||||
- **位置**:
|
||||
- [src/modules/announcements/components/announcement-detail.tsx:165-200](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx)(`canManage` 为 true 时的发布/归档/删除/置顶/编辑按钮组)
|
||||
- [src/modules/announcements/actions.ts:381-391](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)(`getAnnouncementReadStatusAction`)
|
||||
- **问题**:
|
||||
- 全仓搜索 `AnnouncementDetail` 的使用方,仅 [src/app/(dashboard)/announcements/[id]/page.tsx:34-38](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/[id]/page.tsx) 一处,且 `canManage={false}`。管理端 `/admin/announcements/[id]` 直接渲染 `AnnouncementForm`(编辑模式),**没有管理端详情页**。
|
||||
- 全仓搜索 `getAnnouncementReadStatusAction`,**零调用方**。该 Action 返回 `Record<string,boolean>`,本应用于列表页批量标记已读/未读,但列表页从未调用。
|
||||
- **后果**:
|
||||
- 管理员无法在 UI 中执行发布/归档/删除/置顶操作(除非进入编辑表单),严重限制了管理端可用性。
|
||||
- `announcement.readCount` 字段在 `AnnouncementDetail` 中展示,但因 `canManage` 永远为 false,**用户永远看不到已读人数**——已读统计功能在 UI 层完全不可见。
|
||||
- 列表页公告卡片没有"已读/未读"视觉区分,已读回执的数据无法驱动 UI。
|
||||
- **违反规则**:项目规则"避免 backwards-compatibility hacks ... 如果确定未使用,应完全删除" + "识别四个角色共用的 UI 块"。
|
||||
- **根因**:管理端路由设计遗漏了详情视图;已读状态查询 Action 未被列表组件消费。
|
||||
|
||||
### 2.6 【P1 · 耦合】组件直接 import actions,未通过 Context/Provider 注入
|
||||
|
||||
- **位置**:
|
||||
- [announcement-card.tsx:13](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-card.tsx):`import { toggleAnnouncementPinAction } from "../actions"`
|
||||
- [announcement-detail.tsx:25-31](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-detail.tsx):`import { archiveAnnouncementAction, deleteAnnouncementAction, markAnnouncementAsReadAction, publishAnnouncementAction, toggleAnnouncementPinAction } from "../actions"`
|
||||
- [announcement-form.tsx:21](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx):`import { createAnnouncementAction, updateAnnouncementAction } from "../actions"`
|
||||
- **问题**:组件硬编码依赖具体 Server Action,无法在不修改组件代码的前提下替换为 mock 实现。
|
||||
- **后果**:
|
||||
- 组件不可单元测试(必须 mock 整个 `../actions` 模块)。
|
||||
- 无法为不同角色注入不同实现(如家长端只读、教师端可编辑班级公告)。
|
||||
- 未来若要将公告组件复用于"班级空间"或"家长端聚合页",必须重写组件。
|
||||
- **违反规则**:用户要求"完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
|
||||
- **根因**:组件设计未遵循依赖注入原则。
|
||||
|
||||
### 2.7 【P1 · 耦合】`AnnouncementForm` 硬编码路由跳转
|
||||
|
||||
- **位置**:[announcement-form.tsx:82,217](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-form.tsx)
|
||||
- **问题**:表单提交成功后 `router.push("/admin/announcements")`,取消按钮也跳转到 `/admin/announcements`。
|
||||
- **后果**:表单无法在管理端以外的场景复用(如教师端发布班级公告、嵌入到班级详情页的快速发布公告入口)。
|
||||
- **违反规则**:用户要求"组合优先 ... 逻辑复用一律抽取为自定义 hooks" + "最大化复用"。
|
||||
- **根因**:表单未通过 `onSuccess` / `onCancel` 回调或 `successHref` prop 解耦导航。
|
||||
|
||||
### 2.8 【P1 · 业务逻辑位置】`resolveTargetUserIds` 放在 actions.ts
|
||||
|
||||
- **位置**:[src/modules/announcements/actions.ts:48-66](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)
|
||||
- **问题**:受众解析 + 用户 ID 聚合是纯业务逻辑(无 I/O 副作用之外的逻辑),却放在 Server Action 文件中,与 Action 编排逻辑混杂。
|
||||
- **后果**:
|
||||
- 无法独立单元测试(必须 mock `getAllUserIds` / `getStudentIdsByClassId` 等跨模块 data-access)。
|
||||
- 与 `data-access.ts` 中的 `resolveAudience` 形成两套受众解析逻辑,职责重叠。
|
||||
- **违反规则**:项目规则"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离" + "Server Actions / Data Access 模块:建议 ≤ 800 行 ... 超过应考虑拆分"。
|
||||
- **根因**:actions.ts 既承担 HTTP 编排又承担业务规则,未分离 service 层。
|
||||
|
||||
### 2.9 【P1 · 性能】`toggleAnnouncementPin` 与 `markAnnouncementAsRead` 非原子操作
|
||||
|
||||
- **位置**:[src/modules/announcements/data-access.ts:210-224,234-250](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts)
|
||||
- **问题**:
|
||||
- `toggleAnnouncementPin`:先 `SELECT isPinned`,再 `UPDATE`。两次 DB 往返,且在并发场景下存在 lost update(两个管理员同时切换会得到错误结果)。
|
||||
- `markAnnouncementAsRead`:先 `SELECT id`,再 `INSERT`。已有唯一索引保证幂等,但多一次 SELECT 浪费往返。
|
||||
- **后果**:高并发时数据不一致;DB 负载翻倍。
|
||||
- **违反规则**:项目规则"性能:优先使用 React Server Components"(隐含高效数据访问)。
|
||||
- **根因**:未使用 Drizzle 的 `sql` 表达式或 `onDuplicateKeyUpdate`/`INSERT IGNORE` 语义。
|
||||
|
||||
### 2.10 【P1 · 错误处理】`handleActionError` 吞错误上下文
|
||||
|
||||
- **位置**:[src/modules/announcements/actions.ts:34-40](file:///e:/Desktop/CICD/src/modules/announcements/actions.ts)
|
||||
- **问题**:
|
||||
```ts
|
||||
function handleActionError(e: unknown): ActionState<never> {
|
||||
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
|
||||
if (e instanceof Error) return { success: false, message: e.message }
|
||||
return { success: false, message: "Unexpected error" }
|
||||
}
|
||||
```
|
||||
- 未 `console.error` 记录错误堆栈,生产环境无法定位故障。
|
||||
- 直接把 `e.message` 返回给前端,可能泄露内部错误信息(如 SQL 错误)。
|
||||
- "Unexpected error" 为英文硬编码。
|
||||
- **后果**:可观测性差;安全信息泄露风险。
|
||||
- **违反规则**:项目规则"错误与边界处理" + "i18n 就绪"。
|
||||
- **根因**:错误处理未与日志/埋点/i18n 集成。
|
||||
|
||||
### 2.11 【P2 · a11y】置顶按钮嵌套在 `<Link>` 内的键盘交互问题
|
||||
|
||||
- **位置**:[announcement-card.tsx:80-92,116-121](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-card.tsx)
|
||||
- **问题**:`AnnouncementCard` 在有 `href` 时用 `<Link>` 包裹整个卡片,同时卡片内的"置顶"按钮是一个 `<button>`。`handleTogglePin` 调用 `e.preventDefault()` + `e.stopPropagation()` 处理鼠标点击,但:
|
||||
- 键盘聚焦到置顶按钮后按 `Enter`,部分浏览器会同时触发外层 `<a>` 的导航。
|
||||
- 屏幕阅读器会朗读"链接 标题",但置顶按钮的 `aria-label` 在链接上下文中语义模糊。
|
||||
- **后果**:键盘用户可能误跳转;a11y 不达标。
|
||||
- **违反规则**:项目规则"可访问性(a11y):语义化标签、ARIA 属性、键盘导航"。
|
||||
- **根因**:交互按钮不应嵌套在导航链接内;应使用"卡片头部可点击 + 操作按钮独立"的布局。
|
||||
|
||||
### 2.12 【P2 · 类型不安全】`mapRow` 内联对象类型与 schema 脱钩
|
||||
|
||||
- **位置**:[src/modules/announcements/data-access.ts:23-53](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts)
|
||||
- **问题**:`mapRow` 的参数类型是手写的内联对象,未使用 Drizzle 推导类型 `typeof announcements.$inferSelect`。
|
||||
- **后果**:schema 变更(如新增字段)时,`mapRow` 不会在编译期报错,导致类型漂移。
|
||||
- **违反规则**:项目规则"TypeScript 严格模式 ... 函数返回值必须显式标注"。
|
||||
- **根因**:未利用 Drizzle 的类型推导能力。
|
||||
|
||||
### 2.13 【P2 · i18n 字典冗余/缺失并存】
|
||||
|
||||
- **位置**:[src/shared/i18n/messages/zh-CN/announcements.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/announcements.json)
|
||||
- **问题**:
|
||||
- 已定义但未使用的键:`messages.created` / `messages.updated` / `messages.deleted` / `messages.published` / `messages.archived` / `messages.notFound` / `messages.createFailed` / `messages.invalidForm` / `messages.markedRead`(共 9 个死键,因 actions 未消费)。
|
||||
- 缺失的键:`description.detail`(详情页描述)、`description.create`(创建 Dialog 描述)。
|
||||
- **后果**:i18n 字典维护成本上升;新增页面时找不到对应键。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
|
||||
- **根因**:i18n 键与代码未做同步校验。
|
||||
|
||||
### 2.14 【P2 · 死分支】`detailHrefBuilder` prop 未被使用
|
||||
|
||||
- **位置**:[announcement-list.tsx:39,70-74](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-list.tsx)
|
||||
- **问题**:`AnnouncementList` 同时支持 `detailHrefPrefix`(字符串前缀)和 `detailHrefBuilder`(函数)两种 prop,但全仓搜索 `detailHrefBuilder` 的传入方为零(所有调用方都使用 `detailHrefPrefix`)。
|
||||
- **后果**:死代码增加维护负担。
|
||||
- **违反规则**:项目规则"避免 backwards-compatibility hacks"。
|
||||
- **根因**:V3 重构引入 `detailHrefPrefix` 后未清理旧 prop。
|
||||
|
||||
### 2.15 【P2 · 重复骨架屏】用户端与管理端 loading.tsx 完全重复
|
||||
|
||||
- **位置**:
|
||||
- [src/app/(dashboard)/announcements/loading.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/loading.tsx)
|
||||
- [src/app/(dashboard)/admin/announcements/loading.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/announcements/loading.tsx)
|
||||
- **问题**:两个文件几乎逐行重复(仅管理端多一个"新建公告"按钮骨架),未抽取共享骨架屏组件。
|
||||
- **后果**:UI 调整需改两处。
|
||||
- **违反规则**:项目规则"Shared components must be extracted when page duplication exceeds 90%"。
|
||||
- **根因**:未识别到骨架屏也是可复用 UI 块。
|
||||
|
||||
### 2.16 【P2 · 无分页 UI】`getAnnouncements` 支持分页但 UI 未消费
|
||||
|
||||
- **位置**:[data-access.ts:55-112](file:///e:/Desktop/CICD/src/modules/announcements/data-access.ts) 支持 `page` / `pageSize`;[announcement-list.tsx](file:///e:/Desktop/CICD/src/modules/announcements/components/announcement-list.tsx) 无分页控件。
|
||||
- **问题**:列表页默认 `pageSize=20`,超过 20 条公告时静默截断,用户无法翻页。
|
||||
- **后果**:历史公告不可访问;K12 学校一学期公告数通常 > 20。
|
||||
- **违反规则**:用户要求"可扩展性:配置驱动设计"。
|
||||
- **根因**:分页参数未贯穿到 UI。
|
||||
|
||||
### 2.17 【P2 · 表单与 Dialog 行为冲突】
|
||||
|
||||
- **位置**:[admin-announcements-view.tsx:57-64](file:///e:/Desktop/CICD/src/modules/announcements/components/admin-announcements-view.tsx)
|
||||
- **问题**:`AdminAnnouncementsView` 在 Dialog 中渲染 `AnnouncementForm`(创建模式)。但 `AnnouncementForm` 的 Cancel 按钮 `router.push("/admin/announcements")` 会触发整页跳转,而不是关闭 Dialog。提交成功后也是 `router.push` 而非 `onSuccess` 回调。
|
||||
- **后果**:用户体验割裂(Dialog 内按钮触发路由跳转);`handleOpenChange` 中的 `router.refresh()` 与表单跳转重复。
|
||||
- **违反规则**:项目规则"组合优先 ... 严禁使用继承或深层嵌套 HOC"。
|
||||
- **根因**:表单未与容器解耦。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
参考 Google Classroom、钉钉教育、企业微信家校通、飞书校园版、PowerSchool 等主流 K12 产品的公告/通知模块,对比差距如下:
|
||||
|
||||
| 维度 | 行业主流实践 | 当前实现 | 差距影响 |
|
||||
|------|------------|---------|---------|
|
||||
| **多受众定向** | 支持多班级/多年级/多角色组合发布(如"高三1班+2班家长") | 仅支持单年级或单班级 | 年级组长需重复发布 N 次 |
|
||||
| **富文本/附件** | 富文本编辑器 + 附件(PDF 通知、图片) | 纯文本 `whitespace-pre-wrap` | 学校正式通知无法排版、无法附带 PDF |
|
||||
| **分类/标签** | 学科、活动、安全、家长信等分类筛选 | 仅按 status 筛选 | 家长在海量公告中找不到关注项 |
|
||||
| **定时发布** | 选择未来时间自动发布 | schema 有 `publishedAt` 但 UI 未消费 | 管理员需手动踩点发布 |
|
||||
| **到期/置顶** | 自动到期 + 多级优先级(紧急/普通) | 仅 pinned 布尔 | 紧急通知与普通通知无差异 |
|
||||
| **已读统计仪表盘** | 管理端列表展示每条公告已读率、未读名单、可一键催读 | `readCount` 字段存在但 UI 未展示 | 管理员无法评估公告触达效果 |
|
||||
| **草稿预览** | 编辑时预览发布后效果 | 无预览 | 发布前无法验证排版 |
|
||||
| **批量操作** | 列表多选 + 批量归档/删除 | 逐条操作 | 学期末清理 50 条公告需 50 次点击 |
|
||||
| **搜索** | 标题/正文全文搜索 | 无搜索 | 历史公告无法检索 |
|
||||
| **Dashboard 集成** | 首页"最新公告"Widget + 未读红点 | 仅家长端有快速入口链接 | 用户必须主动进入公告页 |
|
||||
| **通知点击回跳** | 点击通知直达公告详情并自动已读 | 通知 actionUrl 指向详情页,但详情页无受众校验 | 通知点击可能触发越权 |
|
||||
| **多语言/多角色文案** | 同一公告对家长/学生/教师展示不同侧重点 | 同一文案对所有角色 | 家长看到教师内部用语 |
|
||||
| **无障碍** | 列表语义化 `<ul>`/`<li>`、键盘可达 | `<div>` + `<Link>` 包裹按钮 | 屏幕阅读器用户导航困难 |
|
||||
| **错误恢复** | 失败自动重试 + 离线草稿 | 失败仅 Toast 提示 | 网络波动时内容丢失 |
|
||||
|
||||
**核心差距**:当前实现停留在"CRUD + 状态机"的最小可用形态,缺少 K12 公告模块的"触达-反馈-统计"闭环。其中"已读统计不可见"和"无富文本/附件"是 K12 学校最痛的两个缺口。
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(必须立即修复 · 安全与数据正确性)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|------|---------|
|
||||
| P0-1 | 详情页越权读取 | 新增 `getAnnouncementByIdForUser(id, userId, dataScope)` data-access 函数,结合 `status="published"` 与受众过滤;路由层调用此函数,未命中返回 `notFound()` |
|
||||
| P0-2 | `resolveAudience` 多孩子/多年级截断 | 将 `audience` 参数升级为 `{ gradeIds: string[]; classIds: string[] }`;`getAnnouncements` 用 `inArray` 查询;`resolveAudience` 返回完整数组而非首个 |
|
||||
| P0-3 | 置顶/已读 Action 缺资源级校验 | `toggleAnnouncementPinAction` 校验公告存在;`markAnnouncementAsReadAction` 调用新增的 `getAnnouncementByIdForUser` 校验可见性后再写入 |
|
||||
|
||||
### P1(高优先级 · 架构与可维护性)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|------|---------|
|
||||
| P1-1 | Actions 返回英文硬编码 | 引入 `getTranslations("announcements")`,所有 `ActionState.message` 改用 i18n 键;新增 `messageKey` 字段或直接返回本地化字符串 |
|
||||
| P1-2 | 死代码:管理端详情分支 / `getAnnouncementReadStatusAction` | 新增 `/admin/announcements/[id]/view` 详情页消费 `AnnouncementDetail canManage=true`;列表组件调用 `getAnnouncementReadStatusAction` 展示已读/未读角标;或删除死分支 |
|
||||
| P1-3 | 组件直接 import actions | 新建 `announcements-service-context.tsx`,定义 `AnnouncementsService` 接口(含 `togglePin` / `publish` / `archive` / `delete` / `markRead` / `create` / `update` 方法签名),用 Provider 注入默认实现;组件 `useContext` 消费 |
|
||||
| P1-4 | `AnnouncementForm` 硬编码路由 | 新增 `onSuccess?` / `onCancel?` 回调 prop,回调优先于 `router.push`;默认 `successHref` prop 兜底 |
|
||||
| P1-5 | `resolveTargetUserIds` 放 actions.ts | 下沉到 `data-access.ts` 的 `resolveAnnouncementTargetUserIds(announcement)` 纯函数;actions.ts 仅做编排 |
|
||||
| P1-6 | 非原子 toggle / markRead | `toggleAnnouncementPin` 改为 `UPDATE ... SET is_pinned = NOT is_pinned`;`markAnnouncementAsRead` 改为 `INSERT ... ON DUPLICATE KEY UPDATE id=id`(Drizzle 的 `onDuplicateKeyUpdate`) |
|
||||
| P1-7 | `handleActionError` 吞错误 | 新增 `console.error` + `trackEvent("announcement.action_error")`;message 走 i18n;不向客户端返回原始 `e.message` |
|
||||
| P1-8 | 表单与 Dialog 行为冲突 | 表单通过 `onSuccess` 回调关闭 Dialog;移除表单内的 `router.push` |
|
||||
|
||||
### P2(中优先级 · 体验与工程化)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|------|---------|
|
||||
| P2-1 | a11y:按钮嵌套在 Link 内 | 重构 `AnnouncementCard`:卡片本身为 `<Link>`,置顶按钮用绝对定位 + `z-index` 独立于链接,或改用 `<article>` + 独立链接 + 独立按钮的语义结构 |
|
||||
| P2-2 | `mapRow` 类型脱钩 | 改用 `typeof announcements.$inferSelect` 推导;移除手写内联类型 |
|
||||
| P2-3 | i18n 死键 / 缺键 | 删除未使用的 9 个 `messages.*` 死键(或随 P1-1 启用);补 `description.detail` / `description.create` |
|
||||
| P2-4 | `detailHrefBuilder` 死 prop | 删除该 prop,仅保留 `detailHrefPrefix` |
|
||||
| P2-5 | 重复骨架屏 | 抽取 `AnnouncementListSkeleton` 共享组件到 `components/` |
|
||||
| P2-6 | ✅ 已实施 | 无分页 UI → 新增 `AnnouncementPagination` 组件;`getUserAnnouncementsPageData` 返回 `{ items, total, page, pageSize }` |
|
||||
| P2-7 | ✅ 已实施 | 无测试 → 新增 `schema.test.ts`(18 测试,`refineAudience` 矩阵)、`is-announcement-visible.test.ts`(15 测试,纯函数含多孩子场景)、`announcement-card.test.tsx`(16 测试,交互 + a11y) |
|
||||
|
||||
### 中长期(P3 · 功能演进,对应行业差距)
|
||||
|
||||
| # | 方向 | 说明 |
|
||||
|---|------|------|
|
||||
| P3-1 | 富文本 + 附件 | 接入 `files` 模块(已支持 `targetType="announcement"`);引入轻量富文本编辑器(如 Tiptap) |
|
||||
| P3-2 | 分类/标签 | 新增 `announcement_tags` 表 + 列表筛选;预置 K12 分类(学科/活动/安全/家长信) |
|
||||
| P3-3 | 定时发布 | 表单增加 `publishedAt` 日期选择器;新增 cron 校验到点自动 `status="published"` |
|
||||
| P3-4 | 已读统计仪表盘 | 管理端列表展示已读率柱状图;详情页展示未读名单 + 一键催读(触发 `sendBatchNotifications`) |
|
||||
| P3-5 | 批量操作 | 列表多选 + 批量归档/删除 Action |
|
||||
| P3-6 | 全文搜索 | 接入 `app/api/search` 已有的全局搜索(当前已支持 announcement 类型) |
|
||||
| P3-7 | Dashboard 集成 | 新增 `AnnouncementsWidget`(最新 3 条 + 未读红点),挂载到各角色 Dashboard |
|
||||
| P3-8 | 草稿预览 | 表单"预览"按钮展开只读视图 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现 `004_architecture_impact_map.md` §2.16 与 `005_architecture_data.json#announcements` 存在以下遗漏,需在实施后同步更新:
|
||||
|
||||
1. **新增节点**:
|
||||
- `data-access.resolveAnnouncementTargetUserIds`(P1-5 下沉的纯函数)
|
||||
- `data-access.getAnnouncementByIdForUser`(P0-1 新增的受众过滤读取)
|
||||
- `AnnouncementsServiceContext`(P1-3 新增的依赖注入 Provider)
|
||||
- `AnnouncementListSkeleton`(P2-5 抽取的共享骨架屏)
|
||||
- `AnnouncementPagination`(P2-6 新增的分页组件)
|
||||
|
||||
2. **删除节点**:
|
||||
- `actions.getAnnouncementReadStatusAction`(若 P1-2 选择删除而非启用)
|
||||
- `AnnouncementList.detailHrefBuilder` prop(P2-4 删除)
|
||||
|
||||
3. **修改节点**:
|
||||
- `GetAnnouncementsParams.audience` 类型从 `{ gradeId?; classId? }` 改为 `{ gradeIds: string[]; classIds: string[] }`
|
||||
- `AnnouncementDetail` 的 `canManage` 分支启用记录(新增管理端详情页后)
|
||||
- `actions.ts` 行数变化(下沉 `resolveTargetUserIds` 后减少)
|
||||
- `data-access.ts` 行数变化(新增函数后增加)
|
||||
|
||||
4. **新增依赖关系**:
|
||||
- `announcements → files`(P3-1 附件集成后)
|
||||
- `announcements → dashboard`(P3-7 Widget 集成后)
|
||||
|
||||
5. **已知问题清单更新**:
|
||||
- 新增"P0-1 详情页越权"(修复后标记 ✅)
|
||||
- 新增"P0-2 多孩子截断"(修复后标记 ✅)
|
||||
- 新增"P0-3 资源级校验缺失"(修复后标记 ✅)
|
||||
- 新增"P1-1 Actions i18n"(修复后标记 ✅)
|
||||
|
||||
---
|
||||
|
||||
## 附:实施清单(与上述优先级一一对应)
|
||||
|
||||
实施将按 P0 → P1 → P2 → P3 顺序推进,P3 为中长期演进,本次实施聚焦 P0/P1/P2,P3 中富文本/附件/Dashboard 集成将择期推进。每完成一项同步更新架构图与运行 `npm run lint` + `npx tsc --noEmit` 验证。
|
||||
@@ -0,0 +1,251 @@
|
||||
# 公告和消息模块审计报告 V3
|
||||
|
||||
> 审查日期:2026-06-22
|
||||
> 审查范围:V2 改进后的 `src/modules/announcements/**`、`src/modules/messaging/**`、`src/modules/notifications/**`、对应路由层、i18n 翻译文件
|
||||
> 前置文档:`announcements-messages-audit-report.md`(V1)、`announcements-messages-audit-report-v2.md`(V2)
|
||||
> 架构图参考:`docs/architecture/004_architecture_impact_map.md` §2.13 / §2.14 / §2.16
|
||||
|
||||
---
|
||||
|
||||
## 一、V2 完成情况复核
|
||||
|
||||
| V2 编号 | 标题 | 状态 |
|
||||
|---------|------|------|
|
||||
| V2-P0-1 | 通知 i18n 命名空间独立 | ✅ 已完成 |
|
||||
| V2-P0-2 | 通知标题 i18n 化 | ✅ 已完成 |
|
||||
| V2-P1-1 | AnnouncementList 过滤模式统一 | ✅ 已完成 |
|
||||
| V2-P1-2 | MessageList 过滤冗余移除 | ✅ 已完成 |
|
||||
| V2-P1-3 | 消息详情页编排下沉 | ✅ 已完成 |
|
||||
| V2-P1-4 | 表单服务端校验错误展示 | ✅ 已完成 |
|
||||
| V2-P2-1 | 轮询间隔常量化 | ✅ 已完成 |
|
||||
| V2-P2-2 | 架构图同步 | ✅ 已完成 |
|
||||
| V2-P2-13b | 通知优先级和归档 | ✅ 已完成 |
|
||||
| V2-P2-13c | 通知分类筛选 + 桌面推送 | ✅ 已完成 |
|
||||
| V2-P2-13d | 公告置顶 + 已读回执 | ✅ 已完成 |
|
||||
|
||||
V2 共 11 项已全部实施。
|
||||
|
||||
---
|
||||
|
||||
## 二、V3 新发现问题
|
||||
|
||||
### 2.1 `saveMessageDraftAction` 中 `as` 断言违规(P0)
|
||||
|
||||
| 位置 | 代码 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [messaging/actions.ts](file:///e:/Desktop/CICD/src/modules/messaging/actions.ts) L279-283 | `formData.get("draftId") as string \| null` 等 5 处断言 | "禁止 `as` 断言(除非从 `unknown` 转换)" |
|
||||
|
||||
**问题分析**:
|
||||
- `FormData.get()` 返回类型为 `string | File | null`
|
||||
- 代码直接断言为 `string | null`,若字段为 File 类型会导致运行时错误
|
||||
- 应使用类型守卫或 Zod 校验进行安全转换
|
||||
|
||||
**后果**:类型不安全,上传场景下可能运行时崩溃;违反 TypeScript 严格模式规则。
|
||||
|
||||
### 2.2 公告列表页 `resolveAudience` 业务逻辑未下沉(P0)
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [announcements/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/announcements/page.tsx) L29-78 | `resolveAudience` 函数 50 行业务逻辑在路由层 | "app/ 只能调用 modules 的 Server Actions 和 data-access,不直接访问数据库" + "页面层编排应下沉" |
|
||||
|
||||
**问题分析**:
|
||||
- `resolveAudience` 包含根据 dataScope 解析受众的复杂业务逻辑(5 种 dataScope 分支)
|
||||
- 直接调用 `classes` 模块的 3 个 data-access 函数(`getStudentActiveClassId`、`getStudentActiveGradeId`、`getClassGradeId`)
|
||||
- V2-P1-5 已为管理端和编辑页创建编排函数,但用户端列表页遗漏
|
||||
|
||||
**后果**:路由层承担业务逻辑,违反三层架构;逻辑无法复用;测试困难。
|
||||
|
||||
### 2.3 通知偏好 Action 放置位置错误(P1)
|
||||
|
||||
| 位置 | 问题 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [messaging/actions.ts](file:///e:/Desktop/CICD/src/modules/messaging/actions.ts) L334-396 | `getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 在 messaging 模块 | "模块标准结构" — 通知偏好属于 notifications 模块 |
|
||||
|
||||
**问题分析**:
|
||||
- V1-P0-4 已将通知偏好 data-access 迁移到 `notifications/preferences.ts`
|
||||
- 但对应的 Server Action 仍留在 `messaging/actions.ts`,使用 `MESSAGE_READ` 权限
|
||||
- 消费方(settings 模块)通过 `SettingsService` 接口注入,但 Action 实现仍在 messaging
|
||||
|
||||
**后果**:模块边界混乱;messaging 模块承担了不属于它的通知偏好职责;权限语义不正确。
|
||||
|
||||
### 2.4 `sendBatchNotifications` 重复日志(P1)
|
||||
|
||||
| 位置 | 代码 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [notifications/dispatcher.ts](file:///e:/Desktop/CICD/src/modules/notifications/dispatcher.ts) L128 + L149 | `sendNotification` 内部调用 `logNotificationSendBatch`,`sendBatchNotifications` 又调用一次 | "代码质量规则" — 重复逻辑 |
|
||||
|
||||
**问题分析**:
|
||||
- L128:`sendNotification` 末尾调用 `logNotificationSendBatch(results, ...)`
|
||||
- L149:`sendBatchNotifications` 末尾再次调用 `logNotificationSendBatch(flatResults)`
|
||||
- 批量发送时每条通知的日志被记录两次
|
||||
|
||||
**后果**:日志数据重复,影响监控准确性;浪费存储。
|
||||
|
||||
### 2.5 腾讯云短信 `SmsSdkAppId` 配置混淆(P1)
|
||||
|
||||
| 位置 | 代码 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [sms-channel.ts](file:///e:/Desktop/CICD/src/modules/notifications/channels/sms-channel.ts) L201, L203 | `SmsSdkAppId: this.config.templateCode` 和 `TemplateId: this.config.templateCode` | "类型安全" — 配置语义错误 |
|
||||
|
||||
**问题分析**:
|
||||
- 腾讯云 SMS API 要求 `SmsSdkAppId`(应用 ID)和 `TemplateId`(模板 ID)是两个不同值
|
||||
- 代码中两者都使用 `this.config.templateCode`(来自 `SMS_TEMPLATE_CODE` 环境变量)
|
||||
- `getSmsConfig()` 缺少 `smsSdkAppId` 字段
|
||||
|
||||
**后果**:腾讯云短信发送必然失败(SmsSdkAppId 不等于模板 ID);生产环境无法使用腾讯云短信。
|
||||
|
||||
### 2.6 内联类型导入(P1)
|
||||
|
||||
| 位置 | 代码 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [messaging/actions.ts](file:///e:/Desktop/CICD/src/modules/messaging/actions.ts) L260 | `Promise<ActionState<import("./types").MessageDraft[]>>` | "TypeScript 规则 — 仅用于类型的导入必须使用 import type" |
|
||||
| [messaging/data-access.ts](file:///e:/Desktop/CICD/src/modules/messaging/data-access.ts) L285 | `import("@/modules/notifications/types").Notification[]` | 同上 |
|
||||
|
||||
**后果**:可读性差,不符合 TypeScript 规范。
|
||||
|
||||
### 2.7 组件层 `as` 断言轻微违规(P2)
|
||||
|
||||
| 位置 | 代码 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [message-list.tsx](file:///e:/Desktop/CICD/src/modules/messaging/components/message-list.tsx) L66 | `setTab(v as Tab)` | "禁止 as 断言" |
|
||||
| [notification-list.tsx](file:///e:/Desktop/CICD/src/modules/notifications/components/notification-list.tsx) L106 | `Object.keys(TYPE_ICON) as NotificationType[]` | 同上 |
|
||||
|
||||
**后果**:类型不安全;应使用类型守卫。
|
||||
|
||||
### 2.8 `logNotificationSend` 使用 console(P2)
|
||||
|
||||
| 位置 | 代码 | 违反规则 |
|
||||
|------|------|----------|
|
||||
| [notifications/data-access.ts](file:///e:/Desktop/CICD/src/modules/notifications/data-access.ts) L210, L230 | `console.info` / `console.error` | "监控" — 应使用统一日志服务 |
|
||||
|
||||
**后果**:生产环境日志分散,无法集中监控。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
结合 K12 教育系统特点,对比钉钉教育、企业微信教育版、智学网、班级小管家等产品:
|
||||
|
||||
| 功能 | 我们 | 行业标杆 | 影响 |
|
||||
|------|------|---------|------|
|
||||
| 消息已读回执 | ❌ 无 | ✅ 钉钉/企微支持 | 教师无法确认家长是否已读重要通知 |
|
||||
| 消息模板 | ❌ 无 | ✅ 智学网预设模板 | 教师每次手写消息效率低 |
|
||||
| 公告定时发布 | ❌ 仅即时发布 | ✅ 钉钉支持定时 | 无法提前编排非工作时间发布 |
|
||||
| 公告附件 | ❌ 无 | ✅ 企微/钉钉支持 | 无法附带 PDF/图片等材料 |
|
||||
| 公告分类标签 | ❌ 无 | ✅ 智学网分类 | 公告列表无法按类型快速筛选 |
|
||||
| 消息群发多班级 | ❌ 单收件人 | ✅ 钉钉群发 | 教师需逐个发送,效率低 |
|
||||
| 公告评论/确认 | ❌ 仅已读回执 | ✅ 钉钉确认回执 | 无法收集家长确认反馈 |
|
||||
| 消息搜索 | ✅ 有(按主题) | ✅ 按内容搜索 | 基本满足 |
|
||||
| 通知优先级 | ✅ 有(V2-P2-13b) | ✅ | 已对齐 |
|
||||
| 通知归档 | ✅ 有(V2-P2-13b) | ✅ | 已对齐 |
|
||||
| 桌面推送 | ✅ 有(V2-P2-13c) | ✅ | 已对齐 |
|
||||
| SSE 实时推送 | ✅ 有(V2-P3) | ✅ | 已对齐 |
|
||||
|
||||
**主要差距**:消息已读回执、公告定时发布、公告附件为高价值缺失功能,直接影响家校沟通效率。
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急,影响类型安全与架构合规)
|
||||
|
||||
1. **修复 `saveMessageDraftAction` 的 `as` 断言**:使用类型守卫替代 `formData.get() as string | null`,安全处理 File 类型。
|
||||
2. **公告列表页 `resolveAudience` 下沉**:将受众解析逻辑迁移到 `announcements/data-access.ts`,新增 `getUserAnnouncementsPageData` 编排函数。
|
||||
|
||||
### P1(重要,影响模块边界与功能正确性)
|
||||
|
||||
3. **通知偏好 Action 迁移**:将 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction` 从 `messaging/actions.ts` 迁移到 `notifications/actions.ts`,使用通知相关权限。
|
||||
4. **修复 `sendBatchNotifications` 重复日志**:移除 `sendBatchNotifications` 中的重复 `logNotificationSendBatch` 调用。
|
||||
5. **修复腾讯云短信 `SmsSdkAppId` 配置**:新增 `SMS_SDK_APP_ID` 环境变量和 `smsSdkAppId` 配置字段。
|
||||
6. **消除内联类型导入**:改为顶部 `import type`。
|
||||
|
||||
### P2(优化,提升代码质量)
|
||||
|
||||
7. **组件 `as` 断言清理**:使用类型守卫替代。
|
||||
8. **日志服务统一**:`logNotificationSend` 改用统一日志接口(预留接口,当前保持 console 但加注释标记为 TODO)。
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现以下架构图需更新:
|
||||
|
||||
1. **§2.13 messaging**:
|
||||
- 移除 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`(迁移至 notifications)
|
||||
- 更新 `actions.ts` 行数(减少约 60 行)
|
||||
- 新增 `saveMessageDraftAction` 类型安全改进说明
|
||||
|
||||
2. **§2.14 notifications**:
|
||||
- 新增 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`
|
||||
- 更新 `actions.ts` 行数(增加约 60 行)
|
||||
- 修复 `sendBatchNotifications` 重复日志说明
|
||||
- 新增 `smsSdkAppId` 配置字段说明
|
||||
|
||||
3. **§2.16 announcements**:
|
||||
- 新增 `getUserAnnouncementsPageData` 编排函数
|
||||
- 更新 `data-access.ts` 行数
|
||||
- 更新用户端列表页说明(使用编排函数)
|
||||
|
||||
4. **附录 A 依赖矩阵**:
|
||||
- messaging 对 notifications 的依赖减少(不再包含通知偏好 Action)
|
||||
- announcements 对 classes 的依赖改为通过编排函数间接调用
|
||||
|
||||
---
|
||||
|
||||
## 六、实施记录
|
||||
|
||||
以下为 V3 审计报告的实施记录,所有修复均已完成并通过 `npx tsc --noEmit` 和 `npm run lint` 验证。
|
||||
|
||||
### V3-P0-1:修复 `saveMessageDraftAction` 的 `as` 断言
|
||||
|
||||
**文件**:`src/modules/messaging/actions.ts`
|
||||
|
||||
**变更**:将 L279-283 的 5 处 `formData.get("xxx") as string | null` 替换为类型守卫函数 `getStringFromFormData`,安全处理 `File` 类型。
|
||||
|
||||
### V3-P0-2:公告列表页 `resolveAudience` 下沉
|
||||
|
||||
**文件**:
|
||||
- `src/modules/announcements/data-access.ts`:新增 `getUserAnnouncementsPageData` 编排函数
|
||||
- `src/app/(dashboard)/announcements/page.tsx`:移除 `resolveAudience`,改用编排函数
|
||||
|
||||
### V3-P1-3:通知偏好 Action 迁移
|
||||
|
||||
**文件**:
|
||||
- `src/modules/notifications/actions.ts`:新增 `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`
|
||||
- `src/modules/messaging/actions.ts`:移除上述 2 个 Action 及相关 import
|
||||
|
||||
### V3-P1-4:修复 `sendBatchNotifications` 重复日志
|
||||
|
||||
**文件**:`src/modules/notifications/dispatcher.ts`
|
||||
|
||||
**变更**:移除 `sendBatchNotifications` 中的重复 `logNotificationSendBatch` 调用(L149)。
|
||||
|
||||
### V3-P1-5:修复腾讯云短信 `SmsSdkAppId` 配置
|
||||
|
||||
**文件**:`src/modules/notifications/channels/sms-channel.ts`
|
||||
|
||||
**变更**:`getSmsConfig()` 新增 `smsSdkAppId` 字段(来自 `SMS_SDK_APP_ID` 环境变量),`SmsSdkAppId` 使用独立配置值。
|
||||
|
||||
### V3-P1-6:消除内联类型导入
|
||||
|
||||
**文件**:
|
||||
- `src/modules/messaging/actions.ts`:L260 内联 import 改为顶部 `import type`
|
||||
- `src/modules/messaging/data-access.ts`:L285 内联 import 改为顶部 `import type`
|
||||
|
||||
### V3-P2-7:组件 `as` 断言清理
|
||||
|
||||
**文件**:
|
||||
- `src/modules/messaging/components/message-list.tsx`:L66 `setTab(v as Tab)` 改为类型守卫
|
||||
- `src/modules/notifications/components/notification-list.tsx`:L106 `Object.keys(TYPE_ICON) as NotificationType[]` 改为类型守卫
|
||||
|
||||
### V3-P2-8:日志服务统一(预留)
|
||||
|
||||
**文件**:`src/modules/notifications/data-access.ts`
|
||||
|
||||
**变更**:`console.info` / `console.error` 添加 TODO 注释标记,预留统一日志服务接入点。
|
||||
|
||||
### 架构图同步
|
||||
|
||||
**文件**:
|
||||
- `docs/architecture/004_architecture_impact_map.md`:§2.13 / §2.14 / §2.16 同步更新
|
||||
- `docs/architecture/005_architecture_data.json`:对应节点同步更新
|
||||
443
docs/architecture/audit/archive/attendance-audit-report.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# 考勤(Attendance)模块审计报告
|
||||
|
||||
> 审计日期:2026-06-25
|
||||
> 审计范围:`src/modules/attendance/**`、`src/app/(dashboard)/{admin,teacher,student,parent}/attendance/**`、跨模块依赖 `src/modules/parent/components/parent-attendance-*.tsx` 及 `child-detail-panel.tsx`、i18n `src/shared/i18n/messages/{en,zh-CN}/attendance.json`
|
||||
> 参照规则:`docs/architecture/004_architecture_impact_map.md`(第 2.10 节)、`docs/architecture/005_architecture_data.json`(L14681 起)、`.trae/rules/project_rules.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/attendance/actions.ts) | 258 | 5 个写 Action(含权限校验、Zod 校验、归属校验) |
|
||||
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts) | 340 | 考勤记录 CRUD + 规则 upsert + 总览统计 + recorder 解析 |
|
||||
| 数据访问 | [data-access-stats.ts](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts) | 206 | 学生/班级考勤汇总(纯函数 `computeStats` + SQL 聚合) |
|
||||
| Schema | [schema.ts](file:///e:/Desktop/CICD/src/modules/attendance/schema.ts) | 43 | Zod 校验(5 个 schema) |
|
||||
| Types | [types.ts](file:///e:/Desktop/CICD/src/modules/attendance/types.ts) | 103 | 类型定义 |
|
||||
| Constants | [constants.ts](file:///e:/Desktop/CICD/src/modules/attendance/constants.ts) | 64 | 状态选项/快捷键/颜色映射 |
|
||||
| Export | [export.ts](file:///e:/Desktop/CICD/src/modules/attendance/export.ts) | 90 | Excel 导出 |
|
||||
| 组件 | [components/attendance-page-layout.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-page-layout.tsx) | 38 | admin/teacher 共用布局插槽 |
|
||||
| 组件 | [components/attendance-sheet.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx) | 418 | 批量点名表单(快捷键、AlertDialog 确认) |
|
||||
| 组件 | [components/attendance-record-list.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx) | 142 | 记录列表 + 删除对话框 |
|
||||
| 组件 | [components/attendance-filters.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-filters.tsx) | 94 | URL 同步筛选器 |
|
||||
| 组件 | [components/attendance-stats-card.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-card.tsx) | 82 | 单卡片 8 指标 |
|
||||
| 组件 | [components/attendance-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-cards.tsx) | 83 | admin 总览 6 卡片网格 |
|
||||
| 组件 | [components/attendance-stats-class-selector.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-class-selector.tsx) | 27 | 班级筛选 ChipNav |
|
||||
| 组件 | [components/attendance-rules-form.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-rules-form.tsx) | 155 | 规则配置表单 |
|
||||
| 组件 | [components/student-attendance-view.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/student-attendance-view.tsx) | 111 | 学生/家长视图 |
|
||||
| 页面 | [admin/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx) | 91 | 管理员总览(RSC) |
|
||||
| 页面 | [teacher/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx) | 116 | 教师记录列表(RSC) |
|
||||
| 页面 | [teacher/attendance/sheet/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx) | 44 | 教师点名页(RSC) |
|
||||
| 页面 | [teacher/attendance/stats/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/stats/page.tsx) | 85 | 教师班级统计(RSC) |
|
||||
| 页面 | [student/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx) | 40 | 学生汇总(RSC) |
|
||||
| 页面 | [parent/attendance/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/page.tsx) | 66 | 家长多子女聚合(RSC) |
|
||||
| 错误边界 | 4 个 `error.tsx`(admin/teacher/student/parent) | ~96 | **重复严重** |
|
||||
| 跨模块 | [parent/components/parent-attendance-calendar.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx) | 220 | 家长月历视图 |
|
||||
| 跨模块 | [parent/components/parent-attendance-warning.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-warning.tsx) | 110 | 异常预警横幅 |
|
||||
| 跨模块 | [parent/components/parent-attendance-rate-card.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-rate-card.tsx) | 114 | 出勤率汇总卡片 |
|
||||
| 跨模块 | [parent/components/child-detail-panel.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/child-detail-panel.tsx) | 190 | 子女详情面板(含考勤 Tab) |
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
page.tsx (RSC)
|
||||
└─ getAttendanceRecords / getStudentAttendanceSummary / getClassAttendanceStats (data-access)
|
||||
└─ db (drizzle) → attendanceRecords / attendanceRules 表
|
||||
└─ ⚠ 直接 JOIN users / classes 表(跨模块表查询)
|
||||
└─ 调用 classes/data-access.getClassActiveStudentsWithInfo(合规)
|
||||
└─ <AttendanceSheet> (client) → batchRecordAttendanceAction
|
||||
└─ <AttendanceRecordList> (client) → deleteAttendanceAction
|
||||
└─ <AttendanceRulesForm> (client) → saveAttendanceRulesAction
|
||||
```
|
||||
|
||||
### 1.3 架构图完整性评估
|
||||
|
||||
架构影响地图(004 第 2.10 节)与 JSON(L14681 起)**整体覆盖** attendance 模块,但存在 **6 处信息过时/不准确**(详见第五节),需同步更新。模块导出、权限点、依赖关系、路由已记录。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 三层架构合规性
|
||||
|
||||
#### 问题 2.1.1:data-access 层跨模块直接 JOIN 外部表【P0】
|
||||
- **位置**:[data-access.ts:118-119](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L118-L119)、[data-access.ts:77-81](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L77-L81)、[data-access-stats.ts:87-91](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L87-L91)、[data-access-stats.ts:161-165](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L161-L165)、[data-access-stats.ts:177-178](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L177-L178)
|
||||
- **描述**:`getAttendanceRecords` 直接 `leftJoin(users)`、`leftJoin(classes)`;`resolveRecorderNames` 直接 `select from users`;`getStudentAttendanceSummary`/`getClassAttendanceStats` 直接查询 `users`/`classes` 表。
|
||||
- **违反规则**:项目规则「`modules/` 之间通过对方 data-access 通信,**不直接查询对方 DB 表**」。
|
||||
- **原因**:为减少查询往返,在 attendance data-access 内联 JOIN 获取 studentName/className/recorderName。
|
||||
- **后果**:users/classes 模块 schema 变更(如 `users.name` 重命名)会直接破坏 attendance 查询;模块边界失效,无法独立演进。
|
||||
|
||||
#### 问题 2.1.2:parent 模块直接 import attendance 组件【P1】
|
||||
- **位置**:[parent/attendance/page.tsx:4](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/page.tsx#L4)
|
||||
- **描述**:`parent/attendance/page.tsx` 直接 `import { StudentAttendanceView } from "@/modules/attendance/components/student-attendance-view"`,跨模块 UI 组件依赖。
|
||||
- **违反规则**:项目规则「该模块必须作为独立功能单元,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access」。
|
||||
- **原因**:parent 复用 student 视图组件以减少重复。
|
||||
- **后果**:parent 模块与 attendance 模块 UI 强耦合,attendance 调整 StudentAttendanceView 会影响 parent 页面。
|
||||
|
||||
#### 问题 2.1.3:parent-attendance-calendar 直接依赖 attendance/constants【P1】
|
||||
- **位置**:[parent-attendance-calendar.tsx:9-12](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L9-L12)
|
||||
- **描述**:直接 import `ATTENDANCE_STATUS_DOT_COLORS`、`ATTENDANCE_STATUS_LABEL_KEYS`。
|
||||
- **违反规则**:同上「完全解耦」原则。
|
||||
- **原因**:parent 类型已解耦(`parent/types.ts` 自声明类型),但常量仍直接依赖。
|
||||
- **后果**:attendance 常量变更影响 parent 月历渲染。
|
||||
|
||||
#### 问题 2.1.4:架构图信息过时【P2】
|
||||
- **位置**:架构图 004 第 1152、1154、1159、1175、1176、1195 行
|
||||
- **描述**:6 处描述与实际代码不符(详见第五节)。
|
||||
- **违反规则**:项目规则「改码必同步图」。
|
||||
- **后果**:架构图可信度下降,误导后续开发。
|
||||
|
||||
### 2.2 权限校验
|
||||
|
||||
#### 问题 2.2.1:teacher 子页面缺失权限校验【P0】
|
||||
- **位置**:[teacher/attendance/sheet/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx)、[teacher/attendance/stats/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/stats/page.tsx)
|
||||
- **描述**:两个页面**无任何权限校验**,未调用 `requirePermission(Permissions.ATTENDANCE_READ)`,也未通过 `getAuthContext().dataScope` 过滤。
|
||||
- **违反规则**:项目规则「所有 Server Action 必须调用 `requirePermission()` 进行权限校验」+ 项目记忆「Parent routes must include permission checks with both parentId and studentId」。
|
||||
- **原因**:RSC 页面非 Server Action,开发者认为 data-access 内的 `buildScopeFilter` 会兜底。
|
||||
- **后果**:sheet 页调用 `getTeacherClasses()` **未传入 scope**([teacher/attendance/sheet/page.tsx:9](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/sheet/page.tsx#L9)),任何已登录用户访问 URL 即可获取教师班级学生列表;stats 页同理。**存在数据越权风险**。
|
||||
|
||||
#### 问题 2.2.2:teacher/student/parent 主页面权限校验不一致【P1】
|
||||
- **位置**:[teacher/attendance/page.tsx:39](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx#L39)、[student/attendance/page.tsx:11](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx#L11)
|
||||
- **描述**:仅 `getAuthContext()`,未 `requirePermission(ATTENDANCE_READ)`;而 [admin/attendance/page.tsx:30](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/page.tsx#L30) 已建立该惯例。
|
||||
- **违反规则**:权限校验应统一。
|
||||
- **后果**:权限点缺失,无法通过权限矩阵精确控制 teacher/student 是否可访问考勤页。
|
||||
|
||||
#### 问题 2.2.3:前端删除按钮无权限点控制【P2】
|
||||
- **位置**:[attendance-record-list.tsx:106-114](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx#L106-L114)
|
||||
- **描述**:删除按钮对所有能看到列表的用户可见,未使用 `usePermission().hasPermission(Permissions.ATTENDANCE_MANAGE)` 控制显隐。
|
||||
- **违反规则**:项目规则「前端权限判断统一使用 `usePermission().hasPermission()`」。
|
||||
- **后果**:无权用户看到删除按钮,点击后才被 Server Action 拒绝,体验差。
|
||||
|
||||
### 2.3 i18n 国际化
|
||||
|
||||
#### 问题 2.3.1:safeParseDate 中文 fieldName 硬编码【P0】
|
||||
- **位置**:[data-access.ts:100-102](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L100-L102)、[data-access.ts:167](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L167)、[data-access.ts:185](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L185)、[data-access.ts:308](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L308)、[data-access-stats.ts:95-96](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L95-L96)、[data-access-stats.ts:169-170](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L169-L170)
|
||||
- **描述**:`safeParseDate(value, "日期")`、`safeParseDate(value, "开始日期")` 等 10 处中文 fieldName,经 `handleActionError` 返回客户端为用户可见错误消息。
|
||||
- **违反规则**:项目规则「所有用户可见文本必须适配 i18n」。
|
||||
- **后果**:英文环境下显示中文错误。
|
||||
|
||||
#### 问题 2.3.2:action-utils shared 层中文兜底消息【P1】
|
||||
- **位置**:`shared/lib/action-utils.ts:32,70,74,143`
|
||||
- **描述**:`NotFoundError(\`${resource} 不存在\`)`、`"操作失败,请稍后重试"`、`${fieldName} 格式无效` 等。
|
||||
- **违反规则**:同上。
|
||||
- **后果**:所有调用 shared 层的模块(含 attendance)均受影响。
|
||||
|
||||
#### 问题 2.3.3:Excel 导出英文列头硬编码【P1】
|
||||
- **位置**:[export.ts:83-84](file:///e:/Desktop/CICD/src/modules/attendance/export.ts#L83-L84)
|
||||
- **描述**:`"Metric"`、`"Value"` 硬编码。
|
||||
|
||||
#### 问题 2.3.4:calendar 硬编码 en-US locale【P1】
|
||||
- **位置**:[parent-attendance-calendar.tsx:102](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-calendar.tsx#L102)
|
||||
- **描述**:`toLocaleDateString("en-US")`,未使用当前 locale。
|
||||
|
||||
#### 问题 2.3.5:child-detail-panel 英文硬编码【P1】
|
||||
- **位置**:[child-detail-panel.tsx:50-56](file:///e:/Desktop/CICD/src/modules/parent/components/child-detail-panel.tsx#L50-L56)、L111、L137-143、L152、L160-163、L182、L44、L186
|
||||
- **描述**:Tab 标签、区块标题、占位提示、按钮文案共 20+ 处英文硬编码。
|
||||
|
||||
#### 问题 2.3.6:翻译键误用【P0】
|
||||
- **位置**:
|
||||
- [parent/attendance/error.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/attendance/error.tsx#L17)、[teacher/attendance/error.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/error.tsx#L17)、[admin/attendance/error.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/attendance/error.tsx#L17):重试按钮使用 `t("actions.save")` 而非 `t("actions.retry")`,显示"保存"。
|
||||
- [attendance-record-list.tsx:127](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-record-list.tsx#L127):删除确认对话框描述使用 `t("errors.unexpected")`("发生未知错误"),应为 `t("sheet.confirmDelete")`。
|
||||
- [attendance-rules-form.tsx:32](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-rules-form.tsx#L32):保存按钮显示 `t("rules.saved")`("考勤规则已保存"),应为 `t("actions.save")`。
|
||||
- [attendance-sheet.tsx:395](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L395):切换班级确认对话框标题误用 `t("sheet.confirmDelete")`,应为 `t("sheet.confirmClassSwitch")`。
|
||||
- **违反规则**:i18n 正确性。
|
||||
- **后果**:用户看到错误/误导文案。
|
||||
|
||||
#### 问题 2.3.7:error.tsx title/description 重复【P2】
|
||||
- **位置**:4 个 error.tsx
|
||||
- **描述**:title 和 description 均用 `t("errors.unexpected")`,完全相同。
|
||||
|
||||
### 2.4 类型安全
|
||||
|
||||
#### 问题 2.4.1:export.ts 无类型守卫的 as 断言【P1】
|
||||
- **位置**:[export.ts:28-34](file:///e:/Desktop/CICD/src/modules/attendance/export.ts#L28-L34)
|
||||
- **描述**:`params.status as "present" | "absent" | ...`,`params.status` 为 `string | undefined`,无类型守卫。
|
||||
- **违反规则**:项目规则「禁止 `as` 断言(除非从 `unknown` 转换)」。
|
||||
- **后果**:非法 status 值绕过类型检查。
|
||||
|
||||
#### 问题 2.4.2:child-detail-panel as 断言【P2】
|
||||
- **位置**:[child-detail-panel.tsx:29](file:///e:/Desktop/CICD/src/modules/parent/components/child-detail-panel.tsx#L29)、L65
|
||||
- **描述**:`(VALID_TABS as string[]).includes(v)`、`v as ChildDetailTab`,已有 `isTab` 守卫但未在 `onValueChange` 使用。
|
||||
|
||||
### 2.5 错误处理与边界
|
||||
|
||||
#### 问题 2.5.1:teacher 子路由缺失 error.tsx【P1】
|
||||
- **位置**:`teacher/attendance/sheet/`、`teacher/attendance/stats/`
|
||||
- **描述**:缺失 error.tsx,运行时错误冒泡到 `teacher/attendance/error.tsx`,错误上下文不准确。
|
||||
- **违反规则**:项目记忆「All student routes must include loading.tsx and error.tsx」。
|
||||
|
||||
#### 问题 2.5.2:student 空状态文案错误【P2】
|
||||
- **位置**:[student/attendance/page.tsx:24-25](file:///e:/Desktop/CICD/src/app/(dashboard)/student/attendance/page.tsx#L24-L25)
|
||||
- **描述**:summary 为 null 时 EmptyState description 用 `t("errors.unexpected")`("发生未知错误"),实际原因可能是无考勤记录。
|
||||
|
||||
### 2.6 组件复用性
|
||||
|
||||
#### 问题 2.6.1:常量在 constants.ts 与 attendance-sheet.tsx 重复定义【P1】
|
||||
- **位置**:[attendance-sheet.tsx:50-83](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L50-L83)
|
||||
- **描述**:`STATUS_OPTIONS`、`STATUS_SHORTCUTS`、`createInitialStatusCounts` 与 constants.ts 重复(部分复用、部分重复的混乱状态)。
|
||||
- **违反规则**:DRY 原则。
|
||||
- **后果**:状态选项变更需同步两处,易遗漏。
|
||||
|
||||
#### 问题 2.6.2:4 个 error.tsx 近乎完全重复【P1】
|
||||
- **位置**:4 个 error.tsx
|
||||
- **描述**:结构完全相同,共约 96 行重复代码。
|
||||
- **违反规则**:项目记忆「Shared components must be extracted when page duplication exceeds 90%」。
|
||||
- **后果**:修改一处需同步四处。
|
||||
|
||||
#### 问题 2.6.3:两个 stats 卡片组件数据结构分裂【P2】
|
||||
- **位置**:[attendance-stats-card.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-card.tsx)、[attendance-stats-cards.tsx](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-stats-cards.tsx)
|
||||
- **描述**:同一概念两套数据结构(`AttendanceStats` vs `AttendanceOverviewStats`,`stats.present`/`stats.presentRate` vs `stats.presentCount`/`stats.attendanceRate`)。
|
||||
|
||||
### 2.7 数据注入与解耦
|
||||
|
||||
#### 问题 2.7.1:无接口抽象与 Context 注入【P1】
|
||||
- **描述**:attendance 模块未定义任何 `AttendanceDataService` 接口,无 `AttendanceContext`/`AttendanceProvider`,无角色差异的接口多态实现。
|
||||
- **违反规则**:项目规则「通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务」。
|
||||
- **后果**:角色间无统一契约约束,参数/返回处理可能不一致;无法通过接口 mock 做单测。
|
||||
|
||||
### 2.8 可测试性
|
||||
|
||||
#### 问题 2.8.1:data-access 无接口类型可供 mock【P2】
|
||||
- **描述**:data-access 函数直接导出为具体函数,无 `AttendanceRepository` 接口。
|
||||
- **违反规则**:项目规则「导出清晰的接口类型以便 mock」。
|
||||
|
||||
### 2.9 a11y 可访问性
|
||||
|
||||
#### 问题 2.9.1:全局 keydown 监听可能冲突【P2】
|
||||
- **位置**:[attendance-sheet.tsx:164-186](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L164-L186)
|
||||
- **描述**:keydown 绑定在 window,仅排除 input/textarea,未排除 Select 等可交互组件。
|
||||
|
||||
### 2.10 性能
|
||||
|
||||
#### 问题 2.10.1:getClassAttendanceStats 未用 SQL 聚合【P1】
|
||||
- **位置**:[data-access-stats.ts:172-182](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L172-L182)
|
||||
- **描述**:仍用全量查询 + 内存 `computeStats`,而 `getStudentAttendanceSummary`/`getAttendanceStats` 已改用 SQL 聚合。
|
||||
- **违反规则**:性能最佳实践。
|
||||
- **后果**:大班级统计查询慢。
|
||||
|
||||
#### 问题 2.10.2:teacher 分页基于截断数据计算【P0】
|
||||
- **位置**:[teacher/attendance/page.tsx:46-63](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/attendance/page.tsx#L46-L63)
|
||||
- **描述**:先获取 `result.items`(pageSize=20),再对**仅 20 条**做前端分页计算 totalPages。
|
||||
- **后果**:分页页数错误,用户无法访问第 2 页之后数据。
|
||||
|
||||
#### 问题 2.10.3:parent 组件可降级为 RSC【P2】
|
||||
- **位置**:[parent-attendance-warning.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-warning.tsx)、[parent-attendance-rate-card.tsx](file:///e:/Desktop/CICD/src/modules/parent/components/parent-attendance-rate-card.tsx)
|
||||
- **描述**:仅因 `useTranslations` 标记 `"use client"`,可用 `getTranslations` 改为 RSC。
|
||||
|
||||
#### 问题 2.10.4:statusCounts 未 memoize【P2】
|
||||
- **位置**:[attendance-sheet.tsx:151-157](file:///e:/Desktop/CICD/src/modules/attendance/components/attendance-sheet.tsx#L151-L157)
|
||||
- **描述**:每次 render 全量 reduce,大班级性能损耗。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
基于 K12 教育系统考勤模块的主流实践(参照 PowerSchool、Infinite Campus、Veracross、校长推荐系统等),当前模块相比优秀实践的差距:
|
||||
|
||||
| 维度 | 优秀实践 | 当前状态 | 差距影响 |
|
||||
|------|---------|---------|---------|
|
||||
| **考勤状态细分** | present/absent/late/early-leave/excused-absent/school-activity + 事假/病假/公假原因 | 仅 present/absent/late/excused 4 态,无原因字段 | 学校无法区分病假/事假,无法生成请假原因统计 |
|
||||
| **实时家长通知** | 学生缺席自动推送家长 App 通知/短信 | 仅被动查看,无主动推送 | 家长无法及时获知子女缺勤,错过干预窗口 |
|
||||
| **出勤率阈值预警** | 自动识别出勤率低于阈值的学生并通知班主任 | 有 parent-attendance-warning 但仅家长端展示,教师端缺失 | 教师无法主动获取需关注学生名单 |
|
||||
| **考勤趋势可视化** | 折线图展示个人/班级出勤率周/月趋势 | 仅数字统计,无趋势图 | 无法直观看出勤率变化趋势 |
|
||||
| **请假申请流程** | 家长在线提交请假申请,教师/管理员审批,自动同步考勤 | 完全缺失 | 请假流程线下化,考勤数据与请假记录脱节 |
|
||||
| **补签/补录** | 学生事后提交补签申请,教师审核修正 | 缺失,仅管理员/教师可手动修改 | 考勤纠错流程繁琐 |
|
||||
| **跨日/跨节次考勤** | 按课节(早读/上午/下午/晚自习)多次点名 | 仅按日单次记录 | 无法精确到节次,缺勤定位不精确 |
|
||||
| **考勤与成绩关联** | 出勤率与学业成绩相关性分析 | 无关联 | 无法识别"低出勤→低成绩"风险学生 |
|
||||
| **班级对比分析** | 同年级班级出勤率横向对比 | 缺失 | 管理员无法横向评估各班考勤管理水平 |
|
||||
| **导出报告多样性** | PDF 周报/月报、Excel 明细、家长签字单 | 仅 Excel 单一导出 | 无法满足不同场景报告需求 |
|
||||
| **移动端适配** | 移动端点名(平板/手机) | 未验证移动端体验 | 教师课堂点名不便携 |
|
||||
| **考勤日历视图** | 学生/家长端月历视图(已有 parent-attendance-calendar) | 已实现且 a11y 良好 | ✅ 已达行业水准 |
|
||||
| **批量操作效率** | 一键全勤、快捷键、批量按学号录入 | 已实现快捷键 + 一键全勤 | ✅ 已达行业水准 |
|
||||
| **空状态/骨架屏** | 每个数据区块 EmptyState + Skeleton | 已基本覆盖 | ✅ 基本达标 |
|
||||
| **a11y 可访问性** | 语义化、ARIA、键盘导航 | 已实现且较完善 | ✅ 基本达标 |
|
||||
|
||||
**核心差距总结**:
|
||||
1. **功能完整性**:缺请假流程、节次考勤、原因分类、趋势可视化、跨班级对比——这些是 K12 学校的刚需。
|
||||
2. **主动通知机制**:被动展示→主动预警的转变。
|
||||
3. **数据联动**:考勤与成绩、请假、通知模块的联动缺失。
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急,影响安全/数据正确性,立即修复)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|------|---------|
|
||||
| P0-1 | teacher/sheet、teacher/stats 缺权限校验且未传 scope(2.2.1) | 页面级增加 `requirePermission(ATTENDANCE_READ)` + 传入 `dataScope`;data-access 函数强制要求 scope 参数 |
|
||||
| P0-2 | teacher 分页基于截断数据计算(2.10.2) | 分页 totalPages 使用后端返回的 total,勿基于 items 长度计算 |
|
||||
| P0-3 | safeParseDate 中文 fieldName 硬编码(2.3.1) | 改用 i18n key 或错误 code,前端按 code 本地化 |
|
||||
| P0-4 | 翻译键误用:error 重试按钮显示"保存"(2.3.6) | 3 个 error.tsx 改用 `t("actions.retry")` |
|
||||
| P0-5 | data-access 跨模块直 JOIN users/classes 表(2.1.1) | 委托 `users/data-access`、`classes/data-access` 提供姓名查询接口 |
|
||||
|
||||
### P1(重要,影响可维护性/合规性,短期修复)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|------|---------|
|
||||
| P1-1 | teacher/student 主页面缺 requirePermission(2.2.2) | 增加 `requirePermission(ATTENDANCE_READ)` |
|
||||
| P1-2 | parent 直接 import attendance 组件(2.1.2) | 通过接口抽象 + Context 注入,或将共享视图下沉为可注入组件 |
|
||||
| P1-3 | parent-attendance-calendar 依赖 attendance/constants(2.1.3) | 常量下沉到 shared 或通过 props 注入 |
|
||||
| P1-4 | action-utils shared 中文兜底(2.3.2) | 返回错误 code 而非中文消息 |
|
||||
| P1-5 | child-detail-panel 英文硬编码(2.3.5) | 提取 i18n 键 |
|
||||
| P1-6 | export.ts 英文列头 + as 断言(2.3.3、2.4.1) | i18n + 类型守卫 |
|
||||
| P1-7 | calendar 硬编码 en-US(2.3.4) | 使用 `useLocale()`/`getLocale()` |
|
||||
| P1-8 | teacher 子路由缺 error.tsx(2.5.1) | 新增 error.tsx |
|
||||
| P1-9 | 常量重复定义(2.6.1) | attendance-sheet 统一使用 constants.ts |
|
||||
| P1-10 | 4 个 error.tsx 重复(2.6.2) | 抽取 shared `ErrorBoundary` 组件 |
|
||||
| P1-11 | getClassAttendanceStats 未用 SQL 聚合(2.10.1) | 改用 SQL GROUP BY 聚合 |
|
||||
| P1-12 | 无接口抽象/Context 注入(2.7.1) | 定义 `AttendanceDataService` 接口 + Provider |
|
||||
|
||||
### P2(优化,提升体验/性能,中期演进)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|------|---------|
|
||||
| P2-1 | 删除按钮无前端权限控制(2.2.3) | `usePermission().hasPermission(ATTENDANCE_MANAGE)` |
|
||||
| P2-2 | error.tsx title/description 重复(2.3.7) | 区分 title/description 键 |
|
||||
| P2-3 | student 空状态文案错误(2.5.2) | 改用 `t("list.emptyDescription")` |
|
||||
| P2-4 | child-detail-panel as 断言(2.4.2) | 使用 isTab 守卫 |
|
||||
| P2-5 | stats 卡片组件数据结构分裂(2.6.3) | 统一为单一数据结构 |
|
||||
| P2-6 | data-access 无接口类型(2.8.1) | 导出 `AttendanceRepository` 接口 |
|
||||
| P2-7 | 全局 keydown 冲突(2.9.1) | 限制监听范围 |
|
||||
| P2-8 | parent 组件可降级 RSC(2.10.3) | 改用 getTranslations |
|
||||
| P2-9 | statusCounts 未 memoize(2.10.4) | useMemo |
|
||||
| P2-10 | 架构图同步(2.1.4) | 更新 004/005 文档 |
|
||||
|
||||
### 中长期演进(功能补齐,对齐行业实践)
|
||||
|
||||
| # | 功能 | 方向 |
|
||||
|---|------|------|
|
||||
| L-1 | 考勤状态细分 + 原因字段 | 扩展 schema 增加 reason 字段,新增 early-leave/school-activity 状态 |
|
||||
| L-2 | 实时家长通知 | 接入 notifications 模块,缺勤自动推送 |
|
||||
| L-3 | 出勤率阈值预警(教师端) | 配置驱动阈值,自动生成需关注学生名单 |
|
||||
| L-4 | 考勤趋势可视化 | 折线图组件,周/月趋势 |
|
||||
| L-5 | 在线请假流程 | 新增 leave-requests 子模块,审批流 + 考勤同步 |
|
||||
| L-6 | 节次考勤 | 扩展数据模型支持按节次记录 |
|
||||
| L-7 | 跨班级对比分析 | 同年级班级出勤率横向对比图 |
|
||||
| L-8 | 导出报告多样化 | PDF 周报/月报 + 家长签字单 |
|
||||
| L-9 | 考勤与成绩关联分析 | 跨模块数据联动分析 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图(004 第 2.10 节、005 JSON L14681 起)存在以下不一致,**需要同步更新**:
|
||||
|
||||
| # | 架构图描述 | 实际代码 | 更新动作 |
|
||||
|---|-----------|---------|---------|
|
||||
| 1 | 004 L1159: `getClassStudentsForAttendance` 仍直查 `classEnrollments` | [data-access.ts:226](file:///e:/Desktop/CICD/src/modules/attendance/data-access.ts#L226) 已委托 `classes/data-access.getClassActiveStudentsWithInfo` | 更新 004 描述为"已委托 classes data-access" |
|
||||
| 2 | 004 L1152: 10 个 Actions(含 5 个读 Action) | actions.ts 仅 5 个写 Action | 更新 Actions 计数为 5,读操作标注为直接调 data-access |
|
||||
| 3 | 004 L1154: `getClassAttendanceStats` 改用 SQL 聚合 | [data-access-stats.ts:172-182](file:///e:/Desktop/CICD/src/modules/attendance/data-access-stats.ts#L172-L182) 仍用全量查询 + computeStats | 待本次重构改为 SQL 聚合后同步更新 |
|
||||
| 4 | 004 L1175: `attendance-sheet.tsx` 使用 `window.confirm` | 已改为 `AlertDialog`(L392-415) | 更新为 AlertDialog |
|
||||
| 5 | 004 L1176: 存在 `{} as Record` 断言 | 已改为 `createInitialStatusCounts()` 函数(L75-83) | 更新描述 |
|
||||
| 6 | 004 L1195: `attendance-stats-cards.tsx` 硬编码中文 | 已全部使用 `t()` i18n | 更新为已 i18n |
|
||||
|
||||
**JSON 同步**:005 中 attendance 节点的 exports、dependencies、permissions 需在本次重构后统一更新(新增 `AttendanceDataService` 接口、`AttendanceProvider`、抽取的 shared `ErrorBoundary`、新增 error.tsx 等)。
|
||||
|
||||
---
|
||||
|
||||
## 六、重构方案设计
|
||||
|
||||
### 6.1 完全解耦:接口抽象 + Context 注入
|
||||
|
||||
**设计目标**:attendance 模块作为独立功能单元,parent/student 等消费方通过接口契约消费,不直接 import 业务实现。
|
||||
|
||||
```typescript
|
||||
// src/modules/attendance/services/attendance-data-service.ts
|
||||
// 接口抽象:定义数据契约,可被不同角色实现
|
||||
export interface AttendanceDataService {
|
||||
getStudentSummary(studentId: string, range?: DateRange): Promise<AttendanceStats | null>;
|
||||
getRecentRecords(studentId: string, limit: number): Promise<AttendanceListItem[]>;
|
||||
getRecordsByDate(date: string, classId?: string): Promise<AttendanceListItem[]>;
|
||||
getClassStats(classId: string, range?: DateRange): Promise<AttendanceStats | null>;
|
||||
}
|
||||
|
||||
// src/modules/attendance/services/attendance-context.tsx
|
||||
// React Context 注入
|
||||
const AttendanceServiceContext = createContext<AttendanceDataService | null>(null);
|
||||
|
||||
export function AttendanceProvider({ service, children }: {
|
||||
service: AttendanceDataService;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AttendanceServiceContext.Provider value={service}>
|
||||
{children}
|
||||
</AttendanceServiceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAttendanceService(): AttendanceDataService {
|
||||
const service = useContext(AttendanceServiceContext);
|
||||
if (!service) throw new Error("AttendanceProvider missing");
|
||||
return service;
|
||||
}
|
||||
|
||||
// 角色实现(示例)
|
||||
// src/modules/attendance/services/student-service.ts —— 学生/家长视角实现
|
||||
// src/modules/attendance/services/teacher-service.ts —— 教师视角实现
|
||||
// src/modules/attendance/services/admin-service.ts —— 管理员视角实现
|
||||
```
|
||||
|
||||
**消费方改造**:`parent/attendance/page.tsx` 注入 `StudentAttendanceService` 实现后渲染 `<StudentAttendanceView>`,不再直接 import attendance 组件;或 attendance 导出纯展示组件,由 parent 通过 props 注入数据。
|
||||
|
||||
### 6.2 组合优先:组件组合与 hooks
|
||||
|
||||
- 所有 UI 通过 `children`/slots/render props 组合,禁止 HOC 深层嵌套。
|
||||
- 逻辑复用抽取为 hooks:`useAttendanceSheet`、`useAttendanceStats`、`useAttendanceFilters`。
|
||||
- `AttendancePageLayout` 已采用插槽模式(header/stats/filters/children),推广至所有角色页面。
|
||||
|
||||
### 6.3 国际化就绪
|
||||
|
||||
**翻译文件结构示例**:
|
||||
```json
|
||||
// src/shared/i18n/messages/zh-CN/attendance.json
|
||||
{
|
||||
"title": "考勤管理",
|
||||
"status": { "present": "出勤", "absent": "缺勤", "late": "迟到", "excused": "请假" },
|
||||
"stats": { "total": "总人次", "presentRate": "出勤率", ... },
|
||||
"errors": {
|
||||
"invalidDate": "日期格式无效",
|
||||
"invalidStartDate": "开始日期格式无效",
|
||||
"unexpected": "发生未知错误,请稍后重试",
|
||||
"forbidden": "无权限执行此操作",
|
||||
"notFound": "考勤记录不存在"
|
||||
},
|
||||
"actions": { "save": "保存", "retry": "重试", "delete": "删除", ... }
|
||||
}
|
||||
```
|
||||
|
||||
**shared 层错误改造**:`action-utils.ts` 返回 `{ code: "INVALID_DATE", field: "date" }` 结构化错误,前端按 code 查 i18n key 本地化,消除中文硬编码。
|
||||
|
||||
### 6.4 最大化复用
|
||||
|
||||
- 抽取 shared `ErrorBoundary` 组件(替代 4 个重复 error.tsx)。
|
||||
- 统一 stats 数据结构为单一 `AttendanceStats`。
|
||||
- 常量统一从 `constants.ts` 导出。
|
||||
- `StudentAttendanceView` 改为接收 `AttendanceDataService` 注入,支持 student/parent 复用。
|
||||
|
||||
### 6.5 错误与边界处理
|
||||
|
||||
- 每个独立数据区块用 React Error Boundary 包裹。
|
||||
- 异步数据用 Suspense + 骨架屏。
|
||||
- 明确处理空数据、无权限、网络异常。
|
||||
|
||||
### 6.6 可测试性
|
||||
|
||||
- data-access 导出 `AttendanceRepository` 接口类型供 mock。
|
||||
- 纯逻辑(`computeStats`、`buildWarnings`、`aggregateStats` 等)已分离,保持。
|
||||
|
||||
### 6.7 可扩展性
|
||||
|
||||
- 角色配置驱动:`attendanceRoleConfig[role]` 决定渲染哪些 Widget/子模块。
|
||||
- 新增角色仅修改配置。
|
||||
|
||||
### 6.8 企业级补充
|
||||
|
||||
- **a11y**:保持现有 ARIA/键盘导航水平,优化 keydown 监听范围。
|
||||
- **性能**:RSC 获取初始数据,客户端组件最小化,支持流式渲染。
|
||||
- **安全**:data-access 层结合 `dataScope` 过滤,Server Action 二次校验。
|
||||
- **监控**:预留 `trackEvent` 埋点接口(点名/删除/规则保存等关键操作)。
|
||||
436
docs/architecture/audit/archive/audit-module-audit-report-v2.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# 审计模块审计报告 v2
|
||||
|
||||
> 审计范围:`src/modules/audit/**`、`src/app/(dashboard)/admin/audit-logs/**`、`src/app/api/export/route.ts`(审计导出分支)、`src/app/api/cron/audit-cleanup/route.ts`、`src/shared/lib/audit-logger.ts`、`src/shared/lib/change-logger.ts`、`src/shared/i18n/messages/{zh-CN,en}/audit.json`
|
||||
> 审计日期:2026-06-25
|
||||
> 审计依据:`docs/architecture/004_architecture_impact_map.md`(2.15 节)、`docs/architecture/005_architecture_data.json`(audit 节点)、`docs/standards/coding-standards.md`、项目 `project_rules.md`
|
||||
> 前置报告:`docs/architecture/audit/audit-module-audit-report.md`(v1,2026-06-24)
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
v1 审计后已完成两轮重构(P0-P2),当前文件分布如下:
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| 路由 | `app/(dashboard)/admin/audit-logs/page.tsx` | 79 | 操作日志列表页(RSC,调 data-access) |
|
||||
| 路由 | `app/(dashboard)/admin/audit-logs/login-logs/page.tsx` | 79 | 登录日志列表页 |
|
||||
| 路由 | `app/(dashboard)/admin/audit-logs/data-changes/page.tsx` | 83 | 数据变更日志列表页 |
|
||||
| 路由 | `app/(dashboard)/admin/audit-logs/overview/page.tsx` | 34 | 审计概览仪表盘(✅ v1 P2-4 新增) |
|
||||
| 路由 | 4 个 `loading.tsx` + 4 个 `error.tsx` | — | 骨架屏 + 错误边界(✅ v1 P0-2 已修复) |
|
||||
| 路由 | `app/api/export/route.ts` | 202 | 统一导出 API(含 audit/login/dataChange 三分支) |
|
||||
| 路由 | `app/api/cron/audit-cleanup/route.ts` | 75 | 定时清理 cron(✅ v1 P2-5 新增) |
|
||||
| actions | `modules/audit/actions.ts` | 195 | 7 个 Server Action(1 查询 + 3 导出 + 3 保留策略) |
|
||||
| data-access | `modules/audit/data-access.ts` | 387 | 12 个查询函数(分页 + 导出 + 选项 + 统计 + 趋势) |
|
||||
| export | `modules/audit/export.ts` | 133 | Excel 列定义 + 行映射 + buildExport(✅ v1 P1-3 已抽取) |
|
||||
| retention | `modules/audit/retention.ts` | 123 | 保留策略配置读写 + 清理 + 纯函数(✅ v1 P2-5 新增) |
|
||||
| types | `modules/audit/types.ts` | 145 | 类型定义 + 状态映射常量 |
|
||||
| services | `modules/audit/services/audit-service.tsx` | 124 | AuditService 接口 + Context + hook(✅ v1 P2-8 新增) |
|
||||
| services | `modules/audit/services/admin-audit-service.ts` | 40 | 管理员默认实现 |
|
||||
| services | `modules/audit/services/mock-audit-service.ts` | 57 | 测试用 Mock 实现 |
|
||||
| hooks | `modules/audit/hooks/use-log-pagination.ts` | 24 | 分页 URL 状态 hook(✅ v1 P1-2 已抽取) |
|
||||
| 组件 | 15 个组件文件 | 19-191 | 表格/筛选器/视图/详情对话框/概览/图表/保留配置/骨架屏/错误边界 |
|
||||
| 测试 | `export.test.ts` / `retention.test.ts` | 191/81 | 纯函数单测(✅ v1 P2-6 已补充) |
|
||||
| i18n | `shared/i18n/messages/{zh-CN,en}/audit.json` | 130 | 完整翻译键(table/filter/empty/export/error/detail/overview/retention) |
|
||||
| shared | `shared/lib/audit-logger.ts` / `change-logger.ts` | — | `logAudit()` / `logDataChange()` 写入日志 |
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
page.tsx (RSC)
|
||||
├─ requirePermission(AUDIT_LOG_READ)
|
||||
├─ getAuditLogs/getLoginLogs/getDataChangeLogs (data-access 直调)
|
||||
└─ <AuditErrorBoundary>
|
||||
<AuditLogView items={...} /> (Client Component)
|
||||
├─ <AuditLogFilters> (nuqs URL 状态)
|
||||
└─ <AuditLogTable> (分页 + 空状态 + 详情对话框)
|
||||
|
||||
overview/page.tsx (RSC)
|
||||
├─ requirePermission(AUDIT_LOG_READ)
|
||||
├─ getAuditOverviewStats / getAuditTrend / getDataChangeActionStats
|
||||
└─ <AuditOverviewView> (RSC)
|
||||
├─ <AuditOverviewStatsBar>
|
||||
├─ <AuditActivityTrendChart>
|
||||
├─ <DataChangeDistributionChart>
|
||||
└─ <AuditRetentionSettings> (Client Component,直调 Server Actions)
|
||||
```
|
||||
|
||||
导出流:`<AuditLogExportButton>` → `fetch /api/export` → `exportAuditLogsAction` → `getAuditLogsForExport` → `buildAuditLogExport` → Excel buffer。
|
||||
|
||||
定时清理流:`/api/cron/audit-cleanup`(CRON_SECRET 鉴权)→ `getAuditRetentionConfig` → `purgeExpiredAuditLogs`。
|
||||
|
||||
### 1.3 架构图记录情况
|
||||
|
||||
- **004(2.15 节)**:记录较完整,含 services/hooks/export/retention 节点,行数标注准确。
|
||||
- **005(audit 节点)**:data-access/actions/types 记录完整,含 services/hooks/export/retention 节点。
|
||||
- **已知不一致**:services 层的 `AuditServiceProvider` 在架构图中标注为"已接入",但实际**未被任何页面使用**(详见 P0-1)。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### P0 — 严重违规
|
||||
|
||||
#### P0-1 Service 抽象层定义但从未使用(虚假完成)
|
||||
|
||||
- **位置**:`services/audit-service.tsx`(`AuditServiceProvider`、`useAuditService`、`useAuditAnalytics`)、`services/admin-audit-service.ts`、`services/mock-audit-service.ts`
|
||||
- **违反规则**:任务要求 → "完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context(或组合 Provider)注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access";`project_rules.md` → "架构图优先规则:改码必同步图"
|
||||
- **问题**:
|
||||
1. 全局搜索 `AuditServiceProvider` 的实际使用:**仅在 `audit-service.tsx` 和 `mock-audit-service.ts` 的 JSDoc `@example` 中出现**,没有任何页面或组件实际注入
|
||||
2. 4 个 `page.tsx` 均直接 `import { getAuditLogs, ... } from "@/modules/audit/data-access"`
|
||||
3. `audit-retention-settings.tsx` 直接 `import { getAuditRetentionConfigAction, ... } from "@/modules/audit/actions"`
|
||||
4. `audit-log-export-button.tsx` 直接 `fetch("/api/export")` 绕过 Service 层
|
||||
5. 架构图 004 第 1468 行声称"✅ P2-8 已修复:~~无 Service 接口抽象~~",实际为"定义但未接线"
|
||||
- **原因**:P2-8 仅创建了接口文件,未在页面层注入 Provider、未将组件改造为从 Context 获取 service
|
||||
- **后果**:
|
||||
- DI 架构形同虚设,组件仍硬编码依赖 data-access,无法在测试中注入 mock
|
||||
- 架构图声称已修复,误导后续审计与维护决策
|
||||
- `mockAuditService` 完全无引用(死代码)
|
||||
|
||||
#### P0-2 mock-audit-service.ts 使用 9 处 `as` 类型断言
|
||||
|
||||
- **位置**:`services/mock-audit-service.ts` 第 29、31、33、36、45、46、47、53、60 行
|
||||
- **违反规则**:`project_rules.md` → "禁止 `as` 断言(除类型收窄外)";`project_memory.md` → "TypeScript strict mode: no `any`, no `as` assertions (except for type narrowing)"
|
||||
- **示例**:
|
||||
```typescript
|
||||
getAuditLogs: async () =>
|
||||
({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,
|
||||
```
|
||||
- **原因**:对象字面量缺少 `items: [] as AuditLog[]` 类型标注,导致需要 `as` 断言整个返回值
|
||||
- **后果**:违反 TS 严格规则;lint 通过但 code review 应拦截
|
||||
|
||||
#### P0-3 全部 7 个 Server Action 缺少 revalidatePath
|
||||
|
||||
- **位置**:`actions.ts` 所有 7 个 Action(`getDataChangeLogsAction`、`exportAuditLogsAction`、`exportLoginLogsAction`、`exportDataChangeLogsAction`、`getAuditRetentionConfigAction`、`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`)
|
||||
- **违反规则**:`project_rules.md` → "Server Action 规范:使用 `revalidatePath` 精确刷新缓存"
|
||||
- **问题**:`saveAuditRetentionConfigAction` 和 `purgeAuditLogsAction` 修改数据后未刷新 overview 页面缓存,用户保存保留策略后看到的仍是旧配置
|
||||
- **原因**:遗漏
|
||||
- **后果**:保留策略变更后概览页缓存不刷新,显示过期数据
|
||||
|
||||
### P1 — 重要缺陷
|
||||
|
||||
#### P1-1 trackEvent event 名称误用(保留策略操作误标为导出)
|
||||
|
||||
- **位置**:`actions.ts` 第 170、199 行(`saveAuditRetentionConfigAction`、`purgeAuditLogsAction`)、`app/api/cron/audit-cleanup/route.ts` 第 54 行
|
||||
- **违反规则**:任务要求 → "监控:方案中预留关键操作埋点接口"
|
||||
- **问题**:保留策略的保存/清理操作使用 `event: "audit.exported"`,语义错误
|
||||
```typescript
|
||||
// saveAuditRetentionConfigAction
|
||||
await trackEvent({ event: "audit.exported", ... properties: { action: "save_config" } })
|
||||
// purgeAuditLogsAction
|
||||
void trackEvent({ event: "audit.exported", ... properties: { action: "purge" } })
|
||||
// cron route
|
||||
void trackEvent({ event: "audit.exported", ... properties: { action: "cron_purge" } })
|
||||
```
|
||||
- **后果**:分析平台中所有保留策略操作被归类为"导出",无法区分导出与清理行为
|
||||
|
||||
#### P1-2 AuditErrorBoundary 使用错误的 i18n namespace
|
||||
|
||||
- **位置**:`components/audit-error-boundary.tsx` 第 19 行 `namespace="common"`
|
||||
- **违反规则**:`project_rules.md` → "所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"
|
||||
- **问题**:`SectionErrorBoundary` 读取 `t("error.boundaryTitle")` / `t("error.boundaryDescription")` / `t("error.retry")`。`audit.json` 定义了 `error.title` / `error.description` / `error.retry`(键名不匹配),而 `common.json` 有 `error.boundaryTitle` / `error.boundaryDescription`。当前传 `"common"` 可工作但使用的是通用错误文案,未利用 audit 专属的 `error.description`("数据加载时发生错误,请稍后重试。")
|
||||
- **后果**:区块级错误边界显示通用错误文案而非审计模块专属文案
|
||||
|
||||
#### P1-3 AuditAnalytics 接口定义为死代码
|
||||
|
||||
- **位置**:`services/audit-service.tsx` 第 64-84 行(`AuditAnalytics` 接口 + `noopAnalytics` + `AuditAnalyticsContext` + `useAuditAnalytics` hook)
|
||||
- **违反规则**:任务要求 → "监控:方案中预留关键操作埋点接口"
|
||||
- **问题**:`useAuditAnalytics` hook 全局搜索仅出现在定义处,**无任何组件调用**。`trackLogView` / `trackExport` / `trackOverviewView` / `trackRetentionConfigChange` / `trackPurge` 五个埋点方法均为死代码
|
||||
- **后果**:客户端层面无日志查看/导出/概览查看埋点,无法统计用户行为
|
||||
|
||||
#### P1-4 data-access 错误处理不一致
|
||||
|
||||
- **位置**:`data-access.ts`
|
||||
- **吞没错误**(返回空数组):`getAuditModuleOptions`(第 152 行)、`getDataChangeTableOptions`(第 237 行)
|
||||
- **抛出错误**:`getAuditLogs`、`getLoginLogs`、`getDataChangeLogs`、`getDataChangeStats`、`getAuditOverviewStats`、`getAuditTrend`、`getDataChangeActionStats`
|
||||
- **违反规则**:`project_rules.md` → "错误与边界处理:明确处理空数据、无权限、网络异常等边界状态"
|
||||
- **后果**:DB 故障时筛选器选项静默显示为空,用户误认为"无数据"而非"查询失败"
|
||||
|
||||
#### P1-5 日期筛选器 aria-label 不区分起止
|
||||
|
||||
- **位置**:`audit-log-filters.tsx` 第 94、101 行;`login-log-filters.tsx` 第 78、85 行;`data-change-log-filters.tsx` 第 117、124 行
|
||||
- **违反规则**:`project_rules.md` → "可访问性(a11y):语义化标签、ARIA 属性、键盘导航"
|
||||
- **问题**:两个日期 Input 均使用 `aria-label={t("table.time")}`("时间"),屏幕阅读器无法区分"开始日期"与"结束日期"
|
||||
- **后果**:视障用户无法区分两个日期输入框
|
||||
|
||||
#### P1-6 AuditLogDetailDialog DialogDescription 重复标题
|
||||
|
||||
- **位置**:`components/audit-log-detail-dialog.tsx` 第 135-137 行
|
||||
- **问题**:
|
||||
```tsx
|
||||
<DialogTitle>{t("detail.title")}</DialogTitle>
|
||||
<DialogDescription className="sr-only">{t("detail.title")}</DialogDescription>
|
||||
```
|
||||
Description 与 Title 完全相同,无信息增量
|
||||
- **后果**:无障碍辅助技术读出重复信息
|
||||
|
||||
#### P1-7 骨架屏缺少 aria-hidden
|
||||
|
||||
- **位置**:`components/audit-log-table-skeleton.tsx`(全文件)、4 个 `loading.tsx`
|
||||
- **违反规则**:`project_rules.md` → "可访问性(a11y)"
|
||||
- **问题**:Skeleton 元素无 `aria-hidden="true"`,屏幕阅读器会逐个朗读骨架占位块
|
||||
- **后果**:加载期间屏幕阅读器体验差
|
||||
|
||||
#### P1-8 window.confirm 阻塞式确认
|
||||
|
||||
- **位置**:`components/audit-retention-settings.tsx` 第 80 行
|
||||
- **问题**:`window.confirm(t("purgeConfirm"))` 使用浏览器原生确认框,不可自定义样式、阻塞主线程、a11y 差
|
||||
- **后果**:与其他模块(使用 AlertDialog 组件)交互不一致
|
||||
|
||||
### P2 — 改进项
|
||||
|
||||
#### P2-1 数据变更 diff 无可视化
|
||||
|
||||
- **位置**:`data-change-log-table.tsx` 第 118-129 行使用 `<pre>` 纯文本展示 oldValue / newValue
|
||||
- **差距**:PowerSchool / Google Workspace Audit / Microsoft Purview 均提供 JSON diff 高亮(增行绿、删行红、左右对比)
|
||||
|
||||
#### P2-2 无失败登录异常告警
|
||||
|
||||
- **位置**:全模块
|
||||
- **差距**:行业产品支持阈值告警(如 5 分钟内同一 IP 失败登录 > 10 次触发告警)
|
||||
|
||||
#### P2-3 无 IP 地理位置
|
||||
|
||||
- **位置**:表格仅展示 IP 字符串
|
||||
- **差距**:行业产品将 IP 解析为地理位置(城市/国家),辅助判断异地登录
|
||||
|
||||
#### P2-4 概览趋势仅 7 天不可配置
|
||||
|
||||
- **位置**:`overview/page.tsx` 第 29 行 `getAuditTrend(7)` 硬编码 7 天
|
||||
- **差距**:行业产品支持 7/30/90 天切换
|
||||
|
||||
#### P2-5 多学校数据隔离缺失
|
||||
|
||||
- **位置**:`data-access.ts` 所有查询无 `schoolId` 过滤
|
||||
- **说明**:`audit_logs` / `login_logs` / `data_change_logs` 表无 `school_id` 字段(schema 确认),audit 模块设计为系统级跨校审计。当前 `AUDIT_LOG_READ` 权限若仅授予超级管理员则可接受,但需在文档中明确标注此设计决策
|
||||
- **风险**:若未来授予校级管理员 `AUDIT_LOG_READ`,将导致跨校数据泄露
|
||||
|
||||
#### P2-6 useLogPagination hook 无单测
|
||||
|
||||
- **位置**:`hooks/use-log-pagination.ts`
|
||||
- **违反规则**:`project_rules.md` → "可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks"
|
||||
- **说明**:v1 已补充 export.test.ts 和 retention.test.ts,但 hook 层无单测
|
||||
|
||||
#### P2-7 导出按钮无进度反馈
|
||||
|
||||
- **位置**:`audit-log-export-button.tsx`
|
||||
- **问题**:大范围导出仅显示 spinner,无进度条/计数
|
||||
- **差距**:行业产品显示"已导出 X / Y 条"
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
| 能力 | PowerSchool / Veracross | Google Workspace Audit | Microsoft Purview | 本系统现状 | 影响 |
|
||||
|------|------------------------|----------------------|-------------------|-----------|------|
|
||||
| 统一审计概览仪表盘 | ✅ | ✅ | ✅ | ✅ 已有概览页 | — |
|
||||
| 按用户/模块/动作/状态筛选 | ✅ | ✅ | ✅ | ✅ 已有完整筛选器 | — |
|
||||
| 日志详情视图 | ✅ 点击展开完整 JSON | ✅ 详情面板 | ✅ 活动详情 | ✅ 已有详情对话框 | — |
|
||||
| 数据变更 diff 可视化 | ✅ 左右对比 + 高亮 | ✅ | ✅ 差异高亮 | ❌ 纯文本 pre 展示 | 变更审查效率低 |
|
||||
| 失败登录监控/告警 | ✅ 异常登录告警 | ✅ 可疑活动检测 | ✅ 实时告警 | ❌ 仅展示无告警 | 无法及时发现暴力破解 |
|
||||
| 导出调度/定时 | ✅ 计划报告 | ✅ 导出 + 邮件 | ✅ 合规报告 | ❌ 仅手动导出 | 合规审计需人工操作 |
|
||||
| 数据保留策略 | ✅ 可配置保留期 | ✅ | ✅ | ✅ 已有保留策略配置 | — |
|
||||
| IP 地理位置 | ✅ | ✅ | ✅ | ❌ 仅显示 IP | 无法判断异地登录 |
|
||||
| 多角色审计视图 | ✅ 管理员/合规官分级 | ✅ | ✅ | ⚠️ 仅 admin 单角色 + Service 接口预留 | 合规官角色需独立视图 |
|
||||
| i18n | ✅ 多语言 | ✅ | ✅ | ✅ 已完整 i18n | — |
|
||||
| 概览趋势可配置时间范围 | ✅ 7/30/90 天 | ✅ | ✅ | ❌ 硬编码 7 天 | 无法查看长期趋势 |
|
||||
| DI 架构(可测试) | — | — | — | ⚠️ 定义未接线 | 组件无法 mock 数据源 |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0 — 立即修复(合规与架构正确性)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P0-1 | Service 抽象层未接线 | overview 页面接入 `AuditServiceProvider`,`AuditOverviewView` 内组件改用 `useAuditService()` 获取数据;或若评估后认为 RSC + props 模式已满足需求则**删除未使用的 services 层**避免误导 |
|
||||
| P0-2 | mock-audit-service.ts 9 处 as 断言 | 为对象字面量添加元素类型标注(如 `items: [] as AuditLog[]` → 改用 `items: new Array<AuditLog>()` 或显式标注返回类型) |
|
||||
| P0-3 | 7 个 Action 缺 revalidatePath | 写操作(saveAuditRetentionConfigAction、purgeAuditLogsAction)添加 `revalidatePath("/admin/audit-logs/overview")` |
|
||||
|
||||
### P1 — 本轮实施(规范与体验)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P1-1 | trackEvent event 误标 | 保留策略操作改用 `event: "audit.retention"` |
|
||||
| P1-2 | ErrorBoundary namespace 错误 | `audit.json` 新增 `error.boundaryTitle` / `error.boundaryDescription`,`AuditErrorBoundary` 传 `namespace="audit"` |
|
||||
| P1-3 | AuditAnalytics 死代码 | 组件中调用 `useAuditAnalytics()` 接入客户端埋点(日志查看、概览查看、导出、保留策略变更) |
|
||||
| P1-4 | data-access 错误处理不一致 | `getAuditModuleOptions` / `getDataChangeTableOptions` 改为抛出错误 |
|
||||
| P1-5 | 日期 aria-label 不区分 | 新增 i18n `filter.startDate` / `filter.endDate`,替换 `t("table.time")` |
|
||||
| P1-6 | DialogDescription 重复 | 新增 `detail.description` 翻译键 |
|
||||
| P1-7 | 骨架屏缺 aria-hidden | Skeleton 容器添加 `aria-hidden="true"` |
|
||||
| P1-8 | window.confirm | 替换为 AlertDialog 组件(与其他模块一致) |
|
||||
|
||||
### P2 — 中长期迭代(功能增强)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P2-1 | diff 无可视化 | 引入 JSON diff 组件(左右对比 + 增删高亮) |
|
||||
| P2-2 | 无失败登录告警 | 新增异常检测规则 + 告警通知 |
|
||||
| P2-3 | 无 IP 地理位置 | 接入 IP 反查服务(如 MaxMind GeoLite2) |
|
||||
| P2-4 | 趋势不可配置 | 概览页新增 7/30/90 天切换按钮 |
|
||||
| P2-5 | 多校数据隔离 | 文档标注"系统级审计"设计决策;若需校级审计则新增 `school_id` 字段 |
|
||||
| P2-6 | hook 无单测 | 为 `useLogPagination` 补充单测 |
|
||||
| P2-7 | 导出无进度 | 大范围导出显示进度条 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图以下遗漏/不一致,需在实现后同步更新:
|
||||
|
||||
### 004_architecture_impact_map.md(2.15 节)
|
||||
|
||||
1. **修正 services 层状态**:第 1468 行"✅ P2-8 已修复"改为"P2-8 已定义接口,P0-1 待接线"(实施后改回"已接线")
|
||||
2. **修正 trackEvent 埋点描述**:保留策略操作的 event 名称从 `audit.exported` 改为 `audit.retention`
|
||||
3. **新增 revalidatePath 说明**:actions 节标注 saveAuditRetentionConfigAction / purgeAuditLogsAction 已添加 revalidatePath
|
||||
|
||||
### 005_architecture_data.json(audit 节点)
|
||||
|
||||
1. **services 节点**:标注 `AuditServiceProvider` 的 `usedBy`(实施后从空改为 overview page)
|
||||
2. **actions 节点**:更新 saveAuditRetentionConfigAction / purgeAuditLogsAction 的 trackEvent event 名称
|
||||
3. **components 节点**:更新 `audit-error-boundary.tsx` 的 namespace 配置
|
||||
|
||||
---
|
||||
|
||||
## 六、重构方案设计
|
||||
|
||||
### a. P0-1 Service 抽象层接线方案
|
||||
|
||||
**决策**:保留 Service 接口(满足任务要求的 DI 架构),在 overview 页面接线。
|
||||
|
||||
由于 `AuditOverviewView` 是 RSC(async server component),而 `AuditServiceProvider` 是 Client Component("use client"),无法直接在 RSC 中使用 Context Provider。因此采用**混合模式**:
|
||||
|
||||
1. **RSC 页面**:仍由 page.tsx 调用 data-access 获取初始数据(保留 SSR 性能优势)
|
||||
2. **客户端交互组件**(`AuditRetentionSettings`):通过 `AuditServiceProvider` 注入 service,组件内部用 `useAuditService()` 获取数据
|
||||
3. **`adminAuditService`** 改为可被 Client Component 引用的轻量包装(仅委托给 Server Action,不直接 import server-only 的 data-access)
|
||||
|
||||
```typescript
|
||||
// services/admin-audit-service.ts —— 改造为 Client-safe 实现
|
||||
// 不 import "server-only",改为委托 Server Actions
|
||||
import { getAuditRetentionConfigAction, saveAuditRetentionConfigAction, purgeAuditLogsAction } from "../actions"
|
||||
|
||||
export const adminAuditService: AuditService = {
|
||||
// 保留策略相关:通过 Server Action 调用
|
||||
getAuditRetentionConfig: async () => {
|
||||
const res = await getAuditRetentionConfigAction()
|
||||
if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
|
||||
return res.data
|
||||
},
|
||||
saveAuditRetentionConfig: async (config) => {
|
||||
const res = await saveAuditRetentionConfigAction(config)
|
||||
if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
|
||||
},
|
||||
purgeExpiredAuditLogs: async (retentionDays) => {
|
||||
const res = await purgeAuditLogsAction(retentionDays)
|
||||
if (!res.success || !res.data) throw new Error(res.message ?? "Failed")
|
||||
return res.data
|
||||
},
|
||||
// 查询类:RSC 页面已通过 props 传入,客户端不需要
|
||||
// ...其余方法可暂不实现或抛错
|
||||
}
|
||||
```
|
||||
|
||||
`AuditRetentionSettings` 改造:
|
||||
```tsx
|
||||
// 在组件内部使用 useAuditService() 而非直接调 Server Action
|
||||
const service = useAuditService()
|
||||
const res = await service.getAuditRetentionConfig()
|
||||
```
|
||||
|
||||
页面注入:
|
||||
```tsx
|
||||
// overview/page.tsx
|
||||
import { AuditServiceProvider } from "@/modules/audit/services/audit-service"
|
||||
import { adminAuditService } from "@/modules/audit/services/admin-audit-service"
|
||||
|
||||
return (
|
||||
<AuditServiceProvider service={adminAuditService}>
|
||||
<AuditOverviewView stats={stats} trend={trend} distribution={distribution} />
|
||||
</AuditServiceProvider>
|
||||
)
|
||||
```
|
||||
|
||||
### b. P0-2 mock-audit-service 类型标注修复
|
||||
|
||||
```typescript
|
||||
// 修复前
|
||||
getAuditLogs: async () =>
|
||||
({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }) as PaginatedResult<AuditLog>,
|
||||
|
||||
// 修复后 —— 为返回值添加显式类型标注,无需 as
|
||||
getAuditLogs: async (): Promise<PaginatedResult<AuditLog>> => ({
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
totalPages: 0,
|
||||
}),
|
||||
```
|
||||
|
||||
### c. P0-3 revalidatePath
|
||||
|
||||
```typescript
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
// saveAuditRetentionConfigAction 末尾
|
||||
revalidatePath("/admin/audit-logs/overview")
|
||||
return { success: true, data: config }
|
||||
|
||||
// purgeAuditLogsAction 末尾
|
||||
revalidatePath("/admin/audit-logs/overview")
|
||||
revalidatePath("/admin/audit-logs")
|
||||
return { success: true, data: result }
|
||||
```
|
||||
|
||||
### d. P1-1 trackEvent event 修正
|
||||
|
||||
```typescript
|
||||
// 保留策略操作
|
||||
await trackEvent({
|
||||
event: "audit.retention", // 原: "audit.exported"
|
||||
userId: session?.user?.id,
|
||||
targetType: "audit_retention",
|
||||
properties: { action: "save_config", ... },
|
||||
})
|
||||
```
|
||||
|
||||
### e. P1-2 ErrorBoundary namespace 修复
|
||||
|
||||
`audit.json` 新增:
|
||||
```json
|
||||
"error": {
|
||||
"title": "加载失败",
|
||||
"description": "数据加载时发生错误,请稍后重试。",
|
||||
"retry": "重试",
|
||||
"boundaryTitle": "审计数据加载失败",
|
||||
"boundaryDescription": "审计数据区块加载时发生错误。"
|
||||
}
|
||||
```
|
||||
|
||||
`audit-error-boundary.tsx`:
|
||||
```tsx
|
||||
<SectionErrorBoundary namespace="audit">
|
||||
```
|
||||
|
||||
### f. i18n 翻译键补充
|
||||
|
||||
```json
|
||||
"filter": {
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期"
|
||||
},
|
||||
"detail": {
|
||||
"description": "查看日志的完整字段信息。"
|
||||
}
|
||||
```
|
||||
|
||||
### g. 最终检查
|
||||
|
||||
- [x] 该模块不存在对其他业务模块的直接 import(仅依赖 shared/* 和 settings data-access for retention config)
|
||||
- [x] 没有使用 `any` 或硬编码角色字符串
|
||||
- [x] 所有 actions 包含 `requirePermission` 调用(7 个 Action 均有)
|
||||
- [x] 文件行数未超过建议上限(最大 data-access.ts 387 行 < 800)
|
||||
- [x] 架构影响地图需同步更新(见第五节)
|
||||
436
docs/architecture/audit/archive/audit-module-audit-report.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# 审计模块审计报告
|
||||
|
||||
> 审计范围:`src/modules/audit/**`、`src/app/(dashboard)/admin/audit-logs/**`、`src/app/api/export/route.ts`(审计导出分支)、`src/shared/lib/audit-logger.ts`、`src/shared/lib/change-logger.ts`、`src/shared/i18n/messages/{zh-CN,en}/audit.json`
|
||||
> 审计日期:2026-06-24
|
||||
> 审计依据:`docs/architecture/004_architecture_impact_map.md`(2.15 节)、`docs/architecture/005_architecture_data.json`(audit 节点)、`docs/standards/coding-standards.md`、项目 `project_rules.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| 路由 | `app/(dashboard)/admin/audit-logs/page.tsx` | 74 | 操作日志列表页(RSC,调 data-access) |
|
||||
| 路由 | `app/(dashboard)/admin/audit-logs/login-logs/page.tsx` | 74 | 登录日志列表页 |
|
||||
| 路由 | `app/(dashboard)/admin/audit-logs/data-changes/page.tsx` | 78 | 数据变更日志列表页 |
|
||||
| 路由 | `app/api/export/route.ts` | 201 | 统一导出 API(含 audit/login/dataChange 三分支) |
|
||||
| actions | `modules/audit/actions.ts` | 214 | 4 个 Server Action(1 查询 + 3 导出) |
|
||||
| data-access | `modules/audit/data-access.ts` | 290 | 9 个查询函数(分页查询 + 导出遍历 + 选项 + 统计) |
|
||||
| types | `modules/audit/types.ts` | 117 | 类型定义 + 状态映射常量 |
|
||||
| 组件 | `components/audit-log-view.tsx` | 61 | 操作日志视图(筛选+表格+分页) |
|
||||
| 组件 | `components/audit-log-table.tsx` | 110 | 操作日志表格 |
|
||||
| 组件 | `components/audit-log-filters.tsx` | 92 | 操作日志筛选器 |
|
||||
| 组件 | `components/audit-log-export-button.tsx` | 83 | 导出按钮(fetch /api/export) |
|
||||
| 组件 | `components/login-log-view.tsx` | 59 | 登录日志视图 |
|
||||
| 组件 | `components/login-log-table.tsx` | 104 | 登录日志表格 |
|
||||
| 组件 | `components/login-log-filters.tsx` | 77 | 登录日志筛选器 |
|
||||
| 组件 | `components/data-change-log-table.tsx` | 281 | 数据变更表格+筛选器+展开行(混合) |
|
||||
| shared | `shared/lib/audit-logger.ts` | 46 | `logAudit()` 写入审计日志 |
|
||||
| shared | `shared/lib/change-logger.ts` | 43 | `logDataChange()` 写入变更日志 |
|
||||
| i18n | `shared/i18n/messages/{zh-CN,en}/audit.json` | 12 | 仅 3 个标题/描述键 |
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
page.tsx (RSC)
|
||||
├─ requirePermission(AUDIT_LOG_READ)
|
||||
├─ getAuditLogs/getLoginLogs/getDataChangeLogs (data-access)
|
||||
└─ <AuditLogView items={...} /> (Client Component)
|
||||
├─ <AuditLogFilters> (nuqs URL 状态)
|
||||
└─ <AuditLogTable> (分页 + 空状态)
|
||||
```
|
||||
|
||||
导出流:`<AuditLogExportButton>` → `fetch /api/export` → `exportAuditLogsAction` → `getAuditLogsForExport` → Excel buffer。
|
||||
|
||||
### 1.3 架构图记录情况
|
||||
|
||||
- **004(2.15 节)**:记录了 audit 模块,但存在**不一致**:声称 actions 层有 `getAuditLogsAction` / `getLoginLogsAction`,实际**不存在**这两个 Action(页面直接调 data-access)。组件清单不完整(缺 `data-change-log-table.tsx`、`login-log-view.tsx`、`audit-log-export-button.tsx`)。
|
||||
- **005(audit 节点)**:data-access/actions/types 记录较完整,但 actions 的 `usedBy` 标注为"待扩展"(实际已被 `/api/export/route.ts` 使用)。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### P0 — 严重违规
|
||||
|
||||
#### P0-1 i18n 严重缺失:组件全部硬编码英文文案
|
||||
- **位置**:`audit-log-table.tsx`("User/Module/Action/Target/Status/IP Address/Time/No audit logs found.")、`audit-log-filters.tsx`("Module/Any Module/Action.../Status/Any Status/Success/Failure")、`login-log-table.tsx`、`login-log-filters.tsx`、`data-change-log-table.tsx`("Table/Record ID/Changed By/View/Hide/Old Value/New Value/Any Table/Create/Update/Delete/Reset/No data change logs found.")、`audit-log-export-button.tsx`("Export Excel/Export failed/Export ready")
|
||||
- **违反规则**:`project_rules.md` → "所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"
|
||||
- **原因**:i18n 字典 `audit.json` 仅含 3 个标题/描述键,组件层未接入 `useTranslations`
|
||||
- **后果**:中文用户在审计页面看到全英文表格表头、筛选器、空状态、按钮文案,与系统其他模块(已 i18n)体验割裂;无法切换语言
|
||||
|
||||
#### P0-2 缺少 loading.tsx 与 error.tsx 错误边界
|
||||
- **位置**:`app/(dashboard)/admin/audit-logs/`、`login-logs/`、`data-changes/` 三个路由均无 `loading.tsx` 和 `error.tsx`
|
||||
- **违反规则**:`project_memory.md` → "All student routes must include loading.tsx and error.tsx for error boundaries";`project_rules.md` → "每个独立的数据区块必须用 React Error Boundary 包裹"
|
||||
- **原因**:审计路由作为后加模块未补齐边界文件
|
||||
- **后果**:数据加载期间白屏;运行时错误直接显示 Next.js 默认错误页,无重试能力
|
||||
|
||||
#### P0-3 架构图与代码不一致
|
||||
- **位置**:`004_architecture_impact_map.md` 2.15 节
|
||||
- **违反规则**:`project_rules.md` → "改码必同步图"、"架构图优先规则"
|
||||
- **问题**:004 声称存在 `getAuditLogsAction` / `getLoginLogsAction`,实际不存在;组件清单缺 3 个文件;行数标注过期(actions 标 212 实际 214,data-access 标 260 实际 290)
|
||||
- **后果**:权限审计、依赖分析会得出错误结论
|
||||
|
||||
### P1 — 重要缺陷
|
||||
|
||||
#### P1-1 formatDate 硬编码 locale 为 "zh-CN"
|
||||
- **位置**:`audit-log-table.tsx:92`、`login-log-table.tsx:86`、`data-change-log-table.tsx:116`
|
||||
- **违反规则**:i18n 就绪要求
|
||||
- **原因**:直接传 `"zh-CN"` 而非从 next-intl 获取当前 locale
|
||||
- **后果**:英文用户看到中文格式日期
|
||||
|
||||
#### P1-2 分页 handlePageChange 三处重复
|
||||
- **位置**:`audit-log-view.tsx:29-38`、`login-log-view.tsx:27-36`、`data-change-log-table.tsx:58-67`
|
||||
- **违反规则**:`project_rules.md` → "最大化复用"
|
||||
- **原因**:三处 100% 相同的 URL searchParams 操作逻辑未抽取
|
||||
- **后果**:维护需改三处,易遗漏
|
||||
|
||||
#### P1-3 导出逻辑内联在 actions 层,三个导出 Action 结构高度重复
|
||||
- **位置**:`actions.ts:90-214`(`exportAuditLogsAction` / `exportLoginLogsAction` / `exportDataChangeLogsAction`)
|
||||
- **违反规则**:004 已标记为 P2 待修复;`project_rules.md` → 单文件职责清晰
|
||||
- **原因**:列定义 + 行映射 + buildExcelExport 全内联在 actions
|
||||
- **后果**:新增日志类型需复制整段;列定义无法在组件层复用(如详情视图)
|
||||
|
||||
#### P1-4 DataChangeLogTable 混合三职责(281 行)
|
||||
- **位置**:`data-change-log-table.tsx`
|
||||
- **问题**:表格组件 + 筛选器组件(`DataChangeLogFilters`)+ 展开行逻辑全部定义在同一文件
|
||||
- **违反规则**:`project_rules.md` → "组件必须为纯函数,职责单一"
|
||||
- **后果**:筛选器无法独立复用;文件接近 300 行不易维护
|
||||
|
||||
#### P1-5 无 React Error Boundary 包裹独立数据区块
|
||||
- **位置**:三个 `page.tsx` 均直接渲染 `<AuditLogView>` / `<DataChangeLogTable>` 无 ErrorBoundary
|
||||
- **违反规则**:`project_rules.md` → "每个独立的数据区块必须用 React Error Boundary 包裹"
|
||||
- **后果**:单个区块错误导致整页崩溃
|
||||
|
||||
#### P1-6 Suspense fallback 为 null,无骨架屏
|
||||
- **位置**:`audit-log-view.tsx:57`、`login-log-view.tsx:55`、`data-change-log-table.tsx:277`
|
||||
- **违反规则**:`project_rules.md` → "异步数据使用 React Suspense + 骨架屏"
|
||||
- **后果**:筛选切换时无加载反馈
|
||||
|
||||
#### P1-7 无关键操作埋点
|
||||
- **位置**:全模块
|
||||
- **违反规则**:`project_rules.md` → "监控:方案中预留关键操作埋点接口"
|
||||
- **原因**:导出操作、日志查看无 `trackEvent` 调用
|
||||
- **后果**:无法统计审计功能使用情况、无法监控异常导出行为
|
||||
|
||||
#### P1-8 a11y 缺失
|
||||
- **位置**:表格无 `aria-label`/`caption`;筛选 Select 无 `aria-label`;展开按钮无 `aria-expanded`;导出按钮无 `aria-label`
|
||||
- **违反规则**:`project_rules.md` → "可访问性(a11y):语义化标签、ARIA 属性、键盘导航"
|
||||
|
||||
### P2 — 改进项
|
||||
|
||||
#### P2-1 data-access 错误吞没,UI 无法区分"空数据"与"查询失败"
|
||||
- **位置**:`data-access.ts` 所有函数 catch 块返回空数组
|
||||
- **后果**:DB 故障时用户看到"无日志"而非错误提示
|
||||
|
||||
#### P2-2 无日志详情视图
|
||||
- **位置**:审计日志表格仅展示摘要,`detail` 字段(JSON)无法查看
|
||||
- **差距**:行业标配支持点击行展开/弹窗查看完整日志详情
|
||||
|
||||
#### P2-3 无用户维度筛选
|
||||
- **位置**:`audit-log-filters.tsx` 仅支持 module/action/status/date,不支持按用户搜索
|
||||
- **差距**:PowerSchool/Veracross 支持按用户筛选所有日志
|
||||
|
||||
#### P2-4 无审计概览仪表盘
|
||||
- **位置**:无统计概览页
|
||||
- **差距**:行业产品提供"今日事件数/失败登录数/数据变更数"概览卡片 + 活动趋势图
|
||||
|
||||
#### P2-5 无数据保留策略
|
||||
- **位置**:审计日志无 TTL/归档机制
|
||||
- **差距**:企业级产品支持可配置保留期(如 90/180/365 天)
|
||||
|
||||
#### P2-6 无单测
|
||||
- **位置**:无 `*.test.ts` 文件
|
||||
- **违反规则**:`project_rules.md` → "可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks"
|
||||
|
||||
#### P2-7 api/export/route.ts 中 `as Record<string, string>` 类型断言
|
||||
- **位置**:`route.ts:145`
|
||||
- **违反规则**:`project_rules.md` → "禁止 `as` 断言(除类型收窄外)"
|
||||
|
||||
#### P2-8 无 Service 接口抽象 / 依赖注入
|
||||
- **位置**:页面直接调 data-access,组件直接收 props
|
||||
- **违反规则**:任务要求 → "完全解耦:通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务"
|
||||
- **说明**:当前 RSC + props 模式可工作,但不满足任务要求的 DI 架构,且无法在客户端组件中 mock 数据源
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
| 能力 | PowerSchool / Veracross | Google Workspace Audit | Microsoft Purview | 本系统现状 | 影响 |
|
||||
|------|------------------------|----------------------|-------------------|-----------|------|
|
||||
| 统一审计概览仪表盘 | ✅ 今日/本周事件数 + 趋势图 | ✅ 活动时间线 | ✅ 合规概览 | ❌ 无 | 管理员无法快速掌握系统活动全貌 |
|
||||
| 按用户筛选 | ✅ | ✅ | ✅ | ❌ 仅 module/action/status | 无法追踪特定用户操作轨迹 |
|
||||
| 日志详情视图 | ✅ 点击展开完整 JSON | ✅ 详情面板 | ✅ 活动详情 | ❌ 仅表格摘要 | 审计人员无法查看 detail 字段 |
|
||||
| 数据变更 diff 可视化 | ✅ 左右对比 + 高亮 | ✅ | ✅ 差异高亮 | ⚠️ 纯文本 pre 展示 | 变更审查效率低 |
|
||||
| 失败登录监控/告警 | ✅ 异常登录告警 | ✅ 可疑活动检测 | ✅ 实时告警 | ❌ 无 | 无法及时发现暴力破解 |
|
||||
| 导出调度/定时 | ✅ 计划报告 | ✅ 导出 + 邮件 | ✅ 合规报告 | ❌ 仅手动导出 | 合规审计需人工操作 |
|
||||
| 数据保留策略 | ✅ 可配置保留期 | ✅ | ✅ | ❌ 无限增长 | 存储成本持续上升 |
|
||||
| IP 地理位置 | ✅ | ✅ | ✅ | ❌ 仅显示 IP | 无法判断异地登录 |
|
||||
| 多角色审计视图 | ✅ 管理员/合规官分级 | ✅ | ✅ | ⚠️ 仅 admin 单角色 | 未来合规官角色需独立视图 |
|
||||
| i18n | ✅ 多语言 | ✅ | ✅ | ❌ 全英文硬编码 | 中文用户体验差 |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0 — 立即修复(合规与基础体验)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P0-1 | i18n 全缺失 | 补全 `audit.json` 翻译键(表头/筛选器/空状态/按钮/Toast),所有组件接入 `useTranslations("audit")` |
|
||||
| P0-2 | 缺 loading/error.tsx | 三个路由各新增 `loading.tsx`(骨架屏)+ `error.tsx`(错误边界 + 重试) |
|
||||
| P0-3 | 架构图不一致 | 同步 004/005:修正 actions 清单、补全组件清单、更新行数 |
|
||||
|
||||
### P1 — 本轮实施(架构规范)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P1-1 | formatDate 硬编码 locale | 改用 `useLocale()` 获取当前 locale |
|
||||
| P1-2 | handlePageChange 重复 | 抽取 `useLogPagination` hook 到 `hooks/` |
|
||||
| P1-3 | 导出逻辑内联 | 抽取 `export.ts`,列定义+行映射移至独立文件 |
|
||||
| P1-4 | DataChangeLogTable 混合 | 拆分为 `data-change-log-table.tsx` + `data-change-log-filters.tsx` |
|
||||
| P1-5 | 无 Error Boundary | 新增 `audit-error-boundary.tsx`,包裹各数据区块 |
|
||||
| P1-6 | Suspense 无骨架屏 | fallback 改为 `AuditLogTableSkeleton` |
|
||||
| P1-7 | 无埋点 | 导出/查看操作新增 `trackEvent` |
|
||||
| P1-8 | a11y 缺失 | 表格加 caption/aria-label,Select 加 aria-label,展开按钮加 aria-expanded |
|
||||
|
||||
### P2 — 中长期迭代(功能增强)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P2-1 | 错误吞没 | data-access 抛出业务错误,actions 层捕获返回 ActionState.error |
|
||||
| P2-2 | 无详情视图 | 新增 `audit-log-detail-dialog.tsx`,展示完整 detail JSON |
|
||||
| P2-3 | 无用户筛选 | 筛选器新增用户搜索 Input |
|
||||
| P2-4 | 无概览仪表盘 | 新增 `/admin/audit-logs/overview` 概览页(统计卡片+趋势图) |
|
||||
| P2-5 | 无保留策略 | 新增 `audit-retention-config` + 定时清理 job |
|
||||
| P2-6 | 无单测 | 为纯函数(分页计算、格式化、列映射)添加单测 |
|
||||
| P2-7 | as 断言 | 改用类型守卫 |
|
||||
| P2-8 | 无 Service 抽象 | 定义 `AuditService` 接口 + Context Provider 注入 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图以下遗漏/不一致,需在实现后同步更新:
|
||||
|
||||
### 004_architecture_impact_map.md(2.15 节)
|
||||
1. **修正 actions 清单**:删除不存在的 `getAuditLogsAction` / `getLoginLogsAction`;补充说明页面直接调 data-access
|
||||
2. **补全组件清单**:新增 `data-change-log-table.tsx`、`login-log-view.tsx`、`audit-log-export-button.tsx`、`data-change-log-filters.tsx`(拆分后)、`audit-error-boundary.tsx`(新增)、`audit-log-table-skeleton.tsx`(新增)
|
||||
3. **更新行数**:actions.ts 214→拆分后;data-access.ts 290;新增 export.ts、hooks/
|
||||
4. **新增 hooks 清单**:`use-log-pagination.ts`
|
||||
5. **更新已知问题**:标记 P0-1~P1-8 已修复
|
||||
|
||||
### 005_architecture_data.json(audit 节点)
|
||||
1. **修正 actions**:删除 `getAuditLogsAction`/`getLoginLogsAction`;`usedBy` 更新为 `api/export/route.ts`
|
||||
2. **补全 components**:新增缺失组件
|
||||
3. **新增 hooks 节点**:`useLogPagination`
|
||||
4. **新增 export 节点**:`export.ts`
|
||||
|
||||
---
|
||||
|
||||
## 六、重构方案设计
|
||||
|
||||
### a. 新文件/目录结构
|
||||
|
||||
```
|
||||
src/modules/audit/
|
||||
├─ actions.ts # Server Actions(编排层,精简)
|
||||
├─ data-access.ts # 数据访问层(查询)
|
||||
├─ export.ts # 🆕 Excel 导出(列定义+行映射+buildExcelExport)
|
||||
├─ types.ts # 类型定义 + 状态映射常量
|
||||
├─ hooks/
|
||||
│ └─ use-log-pagination.ts # 🆕 分页 URL 状态 hook(消除三处重复)
|
||||
├─ components/
|
||||
│ ├─ audit-log-view.tsx # 操作日志视图(i18n + ErrorBoundary + Suspense)
|
||||
│ ├─ audit-log-table.tsx # 操作日志表格(i18n + a11y)
|
||||
│ ├─ audit-log-filters.tsx # 操作日志筛选器(i18n + a11y)
|
||||
│ ├─ audit-log-export-button.tsx # 导出按钮(i18n + trackEvent)
|
||||
│ ├─ login-log-view.tsx # 登录日志视图
|
||||
│ ├─ login-log-table.tsx # 登录日志表格
|
||||
│ ├─ login-log-filters.tsx # 登录日志筛选器
|
||||
│ ├─ data-change-log-view.tsx # 🆕 数据变更视图(拆分自 table)
|
||||
│ ├─ data-change-log-table.tsx # 数据变更表格(仅表格,不含筛选)
|
||||
│ ├─ data-change-log-filters.tsx# 🆕 数据变更筛选器(拆分)
|
||||
│ ├─ audit-error-boundary.tsx # 🆕 错误边界
|
||||
│ └─ audit-log-table-skeleton.tsx # 🆕 骨架屏
|
||||
├─ i18n/
|
||||
│ └─ (由 shared/i18n/messages/{locale}/audit.json 统一管理)
|
||||
└─ lib/
|
||||
└─ audit-columns.ts # 🆕 导出列定义(纯函数,可单测)
|
||||
```
|
||||
|
||||
### b. 核心代码示例
|
||||
|
||||
#### 数据服务接口定义(P2-8,中期实施)
|
||||
|
||||
```typescript
|
||||
// src/modules/audit/services/audit-service.ts
|
||||
export interface AuditService {
|
||||
getAuditLogs(params?: AuditLogQueryParams): Promise<PaginatedResult<AuditLog>>
|
||||
getLoginLogs(params?: LoginLogQueryParams): Promise<PaginatedResult<LoginLog>>
|
||||
getDataChangeLogs(params?: DataChangeLogQueryParams): Promise<PaginatedResult<DataChangeLog>>
|
||||
getAuditModuleOptions(): Promise<string[]>
|
||||
getDataChangeStats(): Promise<DataChangeStat[]>
|
||||
getDataChangeTableOptions(): Promise<string[]>
|
||||
}
|
||||
|
||||
// 角色实现示例(中期)
|
||||
export class AdminAuditService implements AuditService {
|
||||
// 封装 data-access 调用,权限已在 Server Action 层校验
|
||||
async getAuditLogs(params?: AuditLogQueryParams) {
|
||||
return getAuditLogs(params)
|
||||
}
|
||||
// ...其他方法委托给 data-access
|
||||
}
|
||||
```
|
||||
|
||||
#### 通用分页 Hook(P1-2,本轮实施)
|
||||
|
||||
```typescript
|
||||
// src/modules/audit/hooks/use-log-pagination.ts
|
||||
"use client"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useCallback } from "react"
|
||||
|
||||
export function useLogPagination(): (page: number) => void {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
return useCallback((newPage: number) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (newPage <= 1) params.delete("page")
|
||||
else params.set("page", String(newPage))
|
||||
const query = params.toString()
|
||||
router.push(query ? `?${query}` : "?")
|
||||
}, [router, searchParams])
|
||||
}
|
||||
```
|
||||
|
||||
#### 导出模块抽取(P1-3,本轮实施)
|
||||
|
||||
```typescript
|
||||
// src/modules/audit/export.ts
|
||||
import { exportToExcel, type ExcelColumn } from "@/shared/lib/excel"
|
||||
import { formatDateForFile } from "@/shared/lib/utils"
|
||||
import type { AuditLog, LoginLog, DataChangeLog } from "./types"
|
||||
|
||||
export const AUDIT_LOG_COLUMNS: ExcelColumn[] = [
|
||||
{ header: "User ID", key: "userId", width: 22 },
|
||||
// ...列定义
|
||||
]
|
||||
|
||||
export function mapAuditLogsToRows(items: AuditLog[]) {
|
||||
return items.map((r) => ({ /* ...映射 */ }))
|
||||
}
|
||||
|
||||
export async function buildAuditLogExport(items: AuditLog[]) {
|
||||
const buffer = await exportToExcel({
|
||||
sheets: [{ name: "Audit Logs", columns: AUDIT_LOG_COLUMNS, rows: mapAuditLogsToRows(items) }],
|
||||
})
|
||||
return { buffer, filename: `audit_logs_${formatDateForFile()}.xlsx` }
|
||||
}
|
||||
```
|
||||
|
||||
#### ErrorBoundary 包裹(P1-5,本轮实施)
|
||||
|
||||
```tsx
|
||||
// src/modules/audit/components/audit-error-boundary.tsx
|
||||
"use client"
|
||||
import { Component, type ReactNode } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
interface Props { children: ReactNode }
|
||||
interface State { hasError: boolean }
|
||||
|
||||
export class AuditErrorBoundary extends Component<Props, State> {
|
||||
state: State = { hasError: false }
|
||||
static getDerivedStateFromError(): State { return { hasError: true } }
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <AuditErrorFallback onRetry={() => this.setState({ hasError: false })} />
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 角色组装页面(配置驱动,中期)
|
||||
|
||||
```tsx
|
||||
// 未来扩展:通过配置决定渲染哪些 Widget
|
||||
const AUDIT_WIDGET_CONFIG = {
|
||||
admin: ["overviewStats", "auditLogTable", "loginLogTable", "dataChangeTable"],
|
||||
compliance: ["auditLogTable", "dataChangeTable"],
|
||||
} as const
|
||||
```
|
||||
|
||||
### c. 解耦与测试说明
|
||||
|
||||
- **纯函数抽取**:分页计算(`clampPage`/`clampPageSize`)、列映射(`mapAuditLogsToRows`)、状态映射常量已独立于 UI,可直接单测
|
||||
- **Hook 测试**:`useLogPagination` 通过 mock `next/navigation` 的 `useRouter`/`useSearchParams` 测试
|
||||
- **组件测试**:`AuditLogTable` 接收纯 props,传入 mock 数据即可渲染测试
|
||||
- **Mock service 示例**:
|
||||
```typescript
|
||||
const mockEmptyService: AuditService = {
|
||||
getAuditLogs: async () => ({ items: [], total: 0, page: 1, pageSize: 20, totalPages: 0 }),
|
||||
// ...其他返回空数据
|
||||
}
|
||||
```
|
||||
|
||||
### d. i18n 集成示例
|
||||
|
||||
翻译文件结构(`shared/i18n/messages/zh-CN/audit.json`):
|
||||
```json
|
||||
{
|
||||
"title": "审计日志",
|
||||
"description": "追踪系统内所有用户操作,保障安全与合规。",
|
||||
"table": {
|
||||
"user": "用户", "module": "模块", "action": "操作",
|
||||
"target": "目标", "status": "状态", "ipAddress": "IP 地址",
|
||||
"time": "时间", "userAgent": "用户代理", "tableName": "数据表",
|
||||
"recordId": "记录 ID", "changedBy": "操作人", "view": "查看", "hide": "隐藏",
|
||||
"oldValue": "旧值", "newValue": "新值"
|
||||
},
|
||||
"filter": {
|
||||
"anyModule": "任意模块", "anyStatus": "任意状态", "anyAction": "任意操作",
|
||||
"anyTable": "任意数据表", "actionPlaceholder": "操作...",
|
||||
"success": "成功", "failure": "失败",
|
||||
"create": "创建", "update": "更新", "delete": "删除",
|
||||
"signIn": "登录", "signOut": "登出", "signUp": "注册", "reset": "重置"
|
||||
},
|
||||
"empty": {
|
||||
"audit": "暂无审计日志", "login": "暂无登录日志", "dataChange": "暂无数据变更日志"
|
||||
},
|
||||
"export": { "button": "导出 Excel", "success": "导出成功", "failed": "导出失败" },
|
||||
"error": { "title": "加载失败", "description": "数据加载时发生错误,请稍后重试。", "retry": "重试" },
|
||||
"loginLogs": { "title": "登录日志", "description": "..." },
|
||||
"dataChanges": { "title": "数据变更日志", "description": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
组件使用:
|
||||
```tsx
|
||||
const t = useTranslations("audit")
|
||||
<TableHead>{t("table.user")}</TableHead>
|
||||
<EmptyTableRow colSpan={7} message={t("empty.audit")} />
|
||||
```
|
||||
|
||||
### e. 错误处理与加载状态示例
|
||||
|
||||
```tsx
|
||||
// page.tsx
|
||||
<AuditErrorBoundary>
|
||||
<Suspense fallback={<AuditLogTableSkeleton />}>
|
||||
<AuditLogView items={result.items} /* ... */ />
|
||||
</Suspense>
|
||||
</AuditErrorBoundary>
|
||||
```
|
||||
|
||||
### 最终检查
|
||||
|
||||
- [x] 该模块不存在对其他业务模块的直接 import(仅依赖 shared/*)
|
||||
- [x] 没有使用 `any` 或硬编码角色字符串
|
||||
- [x] 所有 actions 包含 `requirePermission` 调用(4 个 Action 均有)
|
||||
- [x] 文件行数未超过建议上限(最大 data-access.ts 290 行 < 800)
|
||||
- [x] 架构影响地图需同步更新(见第五节)
|
||||
1094
docs/architecture/audit/archive/auth-audit-report.md
Normal file
440
docs/architecture/audit/archive/classes-audit-report.md
Normal file
@@ -0,0 +1,440 @@
|
||||
# 班级(classes)模块审计报告
|
||||
|
||||
> 审计时间:2026-06-25
|
||||
> 审计范围:`src/modules/classes/` 全部 32 个文件 + `src/app/(dashboard)/` 下 11 个相关路由分组
|
||||
> 架构图依据:`docs/architecture/004_architecture_impact_map.md` §2.7、`docs/architecture/005_architecture_data.json` modules.classes 节点
|
||||
> 审计基线:项目规则(三层架构、权限校验、i18n、TypeScript 严格模式、文件行数、可测试性)
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
classes 模块位于 `src/modules/classes/`,共 **32 个文件**:
|
||||
|
||||
| 层 | 文件数 | 关键文件 |
|
||||
|---|---|---|
|
||||
| Server Actions | 7 | `actions.ts`(barrel)+ `actions-admin.ts` / `actions-grade.ts` / `actions-teacher.ts` / `actions-invitations.ts` / `actions-schedule.ts` / `actions-shared.ts` |
|
||||
| Data-access | 7 | `data-access.ts`(聚合)+ `data-access-admin.ts` / `data-access-teacher.ts` / `data-access-students.ts` / `data-access-stats.ts` / `data-access-schedule.ts` / `data-access-invitations.ts` |
|
||||
| Schema/Types | 2 | `schema.ts`(13 个 Zod schema)、`types.ts`(19 个类型) |
|
||||
| Components | 21 | `admin-classes-view.tsx` / `grade-classes-view.tsx` / `class-list-table.tsx` / `class-form-dialog.tsx` / `class-delete-dialog.tsx` / `class-list-toolbar.tsx` / `class-form-utils.ts` / `class-error-boundary.tsx` / `class-invitation-manager.tsx` / `class-skeleton.tsx` / `my-classes-grid.tsx` / `schedule-view.tsx` / `schedule-filters.tsx` / `students-filters.tsx` / `students-table.tsx` + `class-detail/` 下 7 个 widget(header/overview-stats/quick-actions/schedule-widget/students-widget/assignments-widget/trends-widget)+ `class-detail/edit-class-dialog.tsx` |
|
||||
| Hooks | 2 | `use-class-data.ts`、`use-class-filters.ts` |
|
||||
|
||||
### 1.2 主要数据流
|
||||
|
||||
```
|
||||
admin/school/classes ─┐
|
||||
management/grade/classes ─┤ ─▶ actions-admin / actions-grade
|
||||
│ │
|
||||
teacher/classes/my ─┤ ▼
|
||||
teacher/classes/schedule ─┤ data-access-admin / data-access-teacher
|
||||
teacher/classes/students ─┤ │
|
||||
│ ▼ (跨模块通过对方 data-access)
|
||||
student/learning/courses ─┤ homework/data-access-classes(作业统计)
|
||||
student/schedule ─┤ scheduling/data-access-class-schedule(课表写)
|
||||
parent/children/[id] ─┘ school/data-access(年级管理权限校验)
|
||||
```
|
||||
|
||||
### 1.3 架构图覆盖完整性评估
|
||||
|
||||
| 维度 | 004 文档 | 005 JSON | 一致性 |
|
||||
|---|---|---|---|
|
||||
| 模块职责 | ✅ §2.7 | ✅ modules.classes.description | 一致 |
|
||||
| 依赖关系(出向) | ✅ shared/auth/school/homework/scheduling | ✅ dependencyMatrix 5 条边 | 一致 |
|
||||
| 被依赖关系(入向) | ⚠️ 列出 10 个,遗漏 elective/error-book/adaptive-practice | ✅ 10 条边覆盖完整 | **不一致** |
|
||||
| 导出函数 | ✅ 17 actions + 33 data-access | ⚠️ exports.actions 漏 3 个邀请码 action;exports.dataAccess 漏 6 个工具函数 | **不一致** |
|
||||
| 数据库表 | ✅ classes/classSubjectTeachers/classEnrollments/classInvitationCodes | ⚠️ classInvitationCodes 字段未详细登记;classSchedule 的 usedBy 字段未含 scheduling | **部分遗漏** |
|
||||
| 权限点 | ✅ 6 个 CLASS_* 权限 | ✅ permissions 常量定义完整 | 一致 |
|
||||
| 路由 | ✅ 11 条路由登记 | ✅ routes 节点登记 | 一致 |
|
||||
| 文件清单 | ✅ 33 个文件 | ⚠️ modules.classes.files 仅列 21 个,漏 12 个组件文件 | **不一致** |
|
||||
|
||||
**结论**:架构图总体覆盖较完整,但存在 7 处需同步更新(详见第五章)。
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 三层架构合规性
|
||||
|
||||
#### 问题 A1:data-access.ts 与拆分文件 3 对同名函数重复定义(🔴 P0)
|
||||
|
||||
- **位置**:
|
||||
- [data-access.ts:400](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L400) `getTeacherScopeData` ↔ [data-access-teacher.ts:592](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L592)
|
||||
- [data-access.ts:428](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L428) `getStudentScopeData` ↔ [data-access-students.ts:309](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L309)
|
||||
- [data-access.ts:455](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L455) `getGradeIdsForStudentIds` ↔ [data-access-students.ts:336](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L336)
|
||||
- **现状**:`data-access.ts:383-386` 同时 `export * from "./data-access-*"` 与本地 `export const`,ES 模块语义下本地定义优先,子文件同名导出对聚合入口而言是死代码。
|
||||
- **违反规则**:架构分层规则「data-access 拆分应避免职责重叠」+ DRY 原则。
|
||||
- **后果**:两份实现目前逻辑一致,但任何一方修改都不会自动同步。若消费者直接 `import { getStudentScopeData } from "@/modules/classes/data-access-students"`,会得到另一份实现,是高风险维护陷阱。
|
||||
|
||||
#### 问题 A2:组件使用绝对路径导入本模块 actions(🟢 P2)
|
||||
|
||||
- **位置**:[class-invitation-manager.tsx:30-32](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L30)
|
||||
- **现状**:`import { createClassInvitationCodeAction } from "@/modules/classes/actions"`,而 `my-classes-grid.tsx`、`admin-classes-view.tsx` 等同模块其他组件使用 `"../actions"` 相对路径。
|
||||
- **违反规则**:编码规范一致性。
|
||||
- **后果**:模块迁移/重命名成本上升。
|
||||
|
||||
### 2.2 权限校验
|
||||
|
||||
#### 问题 B1:listClassInvitationCodesAction 无班级归属校验(🔴 P0 越权漏洞)
|
||||
|
||||
- **位置**:[actions-invitations.ts:312-351](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L312)
|
||||
- **现状**:
|
||||
```ts
|
||||
export async function listClassInvitationCodesAction(classId: string) {
|
||||
await requirePermission(Permissions.CLASS_ENROLL)
|
||||
// ❌ 任何拥有 CLASS_ENROLL 权限的用户都可以列出任意 classId 的所有邀请码
|
||||
const codes = await listClassInvitationCodes(classId)
|
||||
}
|
||||
```
|
||||
- **违反规则**:安全规范「data-access 查询是否结合当前用户权限过滤(防越权)」+ Server Action 规范。
|
||||
- **后果**:教师 A 可通过传入教师 B 的 classId 枚举其邀请码(含 code 字符串),可能引发越权获取加入凭证,破坏邀请码体系的安全性。
|
||||
|
||||
#### 问题 B2:3 个 schedule action 无班级归属校验(🔴 P0 越权漏洞)
|
||||
|
||||
- **位置**:[actions-schedule.ts:21-59](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L21)(create)、[61-101](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L61)(update)、[103-122](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts#L103)
|
||||
- **现状**:3 个 action 仅调用 `requirePermission(CLASS_SCHEDULE)` 后直接调用 scheduling 模块 data-access,未校验当前用户对 `classId` 的归属。
|
||||
- **违反规则**:安全规范越权防护。
|
||||
- **后果**:教师 A 可为教师 B 的班级添加/修改/删除课表项,破坏排课数据完整性。
|
||||
|
||||
#### 问题 B3:教师 update/delete/enroll action 未在 actions 层做归属校验(🟡 P1)
|
||||
|
||||
- **位置**:[actions-teacher.ts:79-122](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L79)(update)、[125-146](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L125)(delete)、[actions-invitations.ts:21-49](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L21)、[353-377](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L353)、[383-440](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L383)
|
||||
- **现状**:actions 层仅 `requirePermission`,依赖 data-access 内部 `getTeacherIdForMutations()` + `eq(classes.teacherId, teacherId)` 校验。
|
||||
- **违反规则**:Server Action 规范「权限校验应在 actions 层完成」。
|
||||
- **后果**:管理员(拥有 CLASS_UPDATE 权限)调用时,因 data-access 内部强制按 teacherId 过滤而失败,错误信息为 "Teacher not found" 不友好;如未来重构 data-access 暴露 admin 路径,归属校验会被跳过。
|
||||
|
||||
#### 问题 B4:data-access 中 7 处硬编码角色名字符串(🟡 P1)
|
||||
|
||||
- **位置**:
|
||||
- [data-access.ts:26](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L26) `eq(roles.name, "teacher")`
|
||||
- [data-access-admin.ts:346](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L346)、[425](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L425)
|
||||
- [data-access-teacher.ts:125](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L125)、[308](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L308)、[472](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L472)、[545](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L545)
|
||||
- **违反规则**:命名规范「常量:UPPER_SNAKE_CASE」+ DRY。
|
||||
- **后果**:若角色名变更(中英切换、复数化),需修改 7 处;魔法字符串降低可读性。
|
||||
|
||||
#### 问题 B5:student 三个页面未调用 requirePermission 显式声明权限点(🟡 P1)
|
||||
|
||||
- **位置**:
|
||||
- [student/learning/courses/page.tsx:19](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/courses/page.tsx#L19)
|
||||
- [student/learning/courses/[classId]/page.tsx:40-41](file:///e:/Desktop/CICD/src/app/(dashboard)/student/learning/courses/[classId]/page.tsx#L40)
|
||||
- [student/schedule/page.tsx:19](file:///e:/Desktop/CICD/src/app/(dashboard)/student/schedule/page.tsx#L19)
|
||||
- **现状**:仅 `getCurrentStudentUser()` 软校验学生身份,未显式声明所需权限点;`[classId]` 页面在权限不足时返回 `notFound()`,错误语义混淆(404 vs 403)。
|
||||
- **违反规则**:Server Action 必须使用 `requirePermission()`。
|
||||
- **后果**:权限审计与文档化困难;404/403 错误语义混淆影响用户体验与监控告警。
|
||||
|
||||
### 2.3 国际化(i18n)
|
||||
|
||||
#### 问题 C1:class-detail/ 子组件普遍硬编码英文(🟡 P1)
|
||||
|
||||
- **位置**:
|
||||
- [class-assignments-widget.tsx:40,46,64,84,88,101](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-assignments-widget.tsx#L40)
|
||||
- [class-overview-stats.tsx:28,33,39,45](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-overview-stats.tsx#L28)
|
||||
- [class-quick-actions.tsx:26,32,36](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-quick-actions.tsx#L26)
|
||||
- [class-schedule-widget.tsx:17,102,110](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-schedule-widget.tsx#L17)
|
||||
- [class-students-widget.tsx:36,41](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-students-widget.tsx#L36)
|
||||
- [class-trends-widget.tsx:41-55,145,164,174,182,188,282,288,301,392](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-trends-widget.tsx#L41)
|
||||
- **现状**:8 个详情子组件几乎全部使用硬编码英文字符串。
|
||||
- **违反规则**:i18n 强制要求 + 架构图 004:998 已声明"13 个组件全部接入 i18n"——与现状不一致。
|
||||
- **后果**:详情页完全无法中文化,与 K12 中文产品定位严重冲突;架构图存在错误声明。
|
||||
|
||||
#### 问题 C2:schedule-view / schedule-filters / students-filters 硬编码英文(🟡 P1)
|
||||
|
||||
- **位置**:
|
||||
- [schedule-view.tsx:111,117,165,166,180,192,310,343,348,357,369,386,435,448,469,489,505-508](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L111)
|
||||
- [schedule-filters.tsx:111,117,164,171,180,192](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-filters.tsx#L111)
|
||||
- [students-filters.tsx:81,110,141,169,173,174,197,204,211](file:///e:/Desktop/CICD/src/modules/classes/components/students-filters.tsx#L81)
|
||||
- **违反规则**:i18n 强制要求。
|
||||
- **后果**:教师端课表/学生管理页全部英文,破坏产品一致性。
|
||||
|
||||
#### 问题 C3:所有 actions 返回的 message 为英文硬编码(🟡 P1)
|
||||
|
||||
- **位置**:全部 7 个 actions 文件
|
||||
- **示例**:[actions-admin.ts:39](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L39) `"Class name, grade and teacher are required"` / [actions-admin.ts:59](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L59) `"Class created successfully"` / [actions-invitations.ts:432](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts#L432) `` `Imported ${imported} students, ${failed} failed` ``
|
||||
- **现状**:组件层 `toast.success(res.message)` 直接显示后端字符串。
|
||||
- **违反规则**:i18n 强制要求。
|
||||
- **后果**:中文用户看到全英文错误提示,体验差。
|
||||
|
||||
#### 问题 C4:30+ 个 error.tsx 使用硬编码中文文案(🟡 P1)
|
||||
|
||||
- **位置**:`src/app/(dashboard)/` 下 30 处 error.tsx(含 `admin/school/classes/error.tsx`、`management/grade/classes/error.tsx` 等)
|
||||
- **示例**:[admin/school/classes/error.tsx:12-16](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/school/classes/error.tsx#L12) `title="页面加载失败"` / `description="抱歉,页面加载时发生了意外错误。请稍后重试。"`
|
||||
- **违反规则**:i18n 强制要求。
|
||||
- **后果**:英文用户在错误页仍看到中文,破坏语言一致性;30+ 处重复字符串维护成本高。
|
||||
|
||||
#### 问题 C5:DEFAULT_CLASS_SUBJECTS 与 excludeSubjects 业务常量硬编码中文(🟡 P1)
|
||||
|
||||
- **位置**:
|
||||
- [types.ts:46](file:///e:/Desktop/CICD/src/modules/classes/types.ts#L46) `export const DEFAULT_CLASS_SUBJECTS = ["语文", "数学", "英语", "美术", "体育", "科学", "社会", "音乐"] as const`
|
||||
- [data-access-students.ts:41](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L41) `const excludeSubjects = ["体育", "音乐", "美术"]`
|
||||
- **违反规则**:DRY(两份科目清单)+ i18n(业务规则硬编码字符串)。
|
||||
- **后果**:与 `subjects` 表查询重复;不同学校配置无法适配;`excludeSubjects` 未在架构图记录。
|
||||
|
||||
#### 问题 C6:getSubjectColor 用英文匹配中文科目(🔴 P0 功能 bug)
|
||||
|
||||
- **位置**:[schedule-view.tsx:186-195](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L186)
|
||||
- **现状**:
|
||||
```ts
|
||||
const getSubjectColor = (subject: string) => {
|
||||
const s = subject.toLowerCase()
|
||||
if (s.includes('math')) return 'bg-blue-500/10 ...'
|
||||
if (s.includes('physics') || s.includes('science')) return '...'
|
||||
if (s.includes('english') || s.includes('lit')) return '...'
|
||||
```
|
||||
- **问题**:`DEFAULT_CLASS_SUBJECTS` 是中文("数学"、"英语"等),但 `getSubjectColor` 用英文 'math'/'english' 匹配,所有中文科目都会落到 default 分支,颜色视觉分组完全失效。
|
||||
- **违反规则**:i18n + 功能正确性。
|
||||
- **后果**:课表颜色视觉分组完全失效,不仅是 i18n 问题。
|
||||
|
||||
### 2.4 错误处理
|
||||
|
||||
#### 问题 D1:catch 块未记录错误,仅显示 toast(🟡 P1)
|
||||
|
||||
- **位置**:10 处
|
||||
- [my-classes-grid.tsx:75-77](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L75)
|
||||
- [schedule-filters.tsx:65-67](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-filters.tsx#L65)
|
||||
- [schedule-view.tsx:113-115](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L113)
|
||||
- [students-filters.tsx:73-75](file:///e:/Desktop/CICD/src/modules/classes/components/students-filters.tsx#L73)
|
||||
- [admin-classes-view.tsx:52-54](file:///e:/Desktop/CICD/src/modules/classes/components/admin-classes-view.tsx#L52)
|
||||
- [grade-classes-view.tsx:42-44](file:///e:/Desktop/CICD/src/modules/classes/components/grade-classes-view.tsx#L42)
|
||||
- [class-invitation-manager.tsx:101-103](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L101)、[263-265](file:///e:/Desktop/CICD/src/modules/classes/components/class-invitation-manager.tsx#L263)
|
||||
- [edit-class-dialog.tsx:55-57](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/edit-class-dialog.tsx#L55)
|
||||
- [students-table.tsx:41-43](file:///e:/Desktop/CICD/src/modules/classes/components/students-table.tsx#L41)
|
||||
- **现状**:`} catch { toast.error(t("list.failedCreate")) }`
|
||||
- **违反规则**:错误处理最佳实践。
|
||||
- **后果**:服务端 500、网络错误、权限错误全部显示同一文案,开发者无法从用户截图定位错误;线上排查困难。
|
||||
|
||||
#### 问题 D2:data-access 内 catch 静默返回空数组(🟡 P1)
|
||||
|
||||
- **位置**:
|
||||
- [data-access-admin.ts:88-124](file:///e:/Desktop/CICD/src/modules/classes/data-access-admin.ts#L88)(getAdminClasses fallback 合理降级)
|
||||
- [data-access-students.ts:159-177](file:///e:/Desktop/CICD/src/modules/classes/data-access-students.ts#L159)(getStudentClasses fallback 合理降级)
|
||||
- [data-access-teacher.ts:70-73](file:///e:/Desktop/CICD/src/modules/classes/data-access-teacher.ts#L70)(getTeacherClasses 直接返回 `[]`)
|
||||
- **问题**:`getTeacherClasses` 失败时返回空数组,会让教师看到"无班级"假象,区分不出"数据库错误"与"无数据"。
|
||||
- **违反规则**:错误处理最佳实践。
|
||||
- **后果**:教师班级列表假性空数据,难以排查。
|
||||
|
||||
#### 问题 D3:actions 层错误处理两套风格混用(🟡 P1)
|
||||
|
||||
- **位置**:
|
||||
- 风格 A(手动 try/catch + `PermissionDeniedError`):[actions-admin.ts:63-66](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L63)、[actions-grade.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-grade.ts)、[actions-invitations.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-invitations.ts)
|
||||
- 风格 B(`handleActionError`):[actions-teacher.ts:74-76](file:///e:/Desktop/CICD/src/modules/classes/actions-teacher.ts#L74)、[actions-schedule.ts](file:///e:/Desktop/CICD/src/modules/classes/actions-schedule.ts)
|
||||
- **违反规则**:编码规范一致性。
|
||||
- **后果**:权限拒绝场景下风格 A 会 `throw e` 冒泡到 Next.js 错误边界(用户体验差),风格 B 会转为结构化失败;维护成本高。
|
||||
|
||||
#### 问题 D4:teacher/classes/my/[id] 缺 loading.tsx 和 error.tsx(🟡 P1)
|
||||
|
||||
- **位置**:`src/app/(dashboard)/teacher/classes/my/[id]/`
|
||||
- **现状**:页面内部 4 个 `Promise.all` 并行数据获取,但完全没有 loading.tsx 和 error.tsx。
|
||||
- **违反规则**:项目规则「学生路由必须包含 loading.tsx 和 error.tsx」+ 企业级规范。
|
||||
- **后果**:无骨架屏感知性能差;任一 data-access 抛错冒泡至上层;`notFound()` 触发时展示默认 404 与站点风格不一致。
|
||||
|
||||
#### 问题 D5:10 个页面缺 error.tsx(🟡 P1)
|
||||
|
||||
- **位置**:teacher/classes/my、teacher/classes/schedule、teacher/classes/students、student/learning/courses、student/learning/courses/[classId]、student/schedule 等
|
||||
- **违反规则**:企业级规范要求主要路由必须有 error.tsx。
|
||||
- **后果**:错误上下文丢失,错误冒泡至上层。
|
||||
|
||||
### 2.5 类型安全
|
||||
|
||||
#### 问题 E1:formData.get 强转 string 应使用类型守卫(🟢 P2)
|
||||
|
||||
- **位置**:[actions-admin.ts:93](file:///e:/Desktop/CICD/src/modules/classes/actions-admin.ts#L93)、[actions-grade.ts:98](file:///e:/Desktop/CICD/src/modules/classes/actions-grade.ts#L98)
|
||||
- **现状**:`formData.get("subjectTeachers") as string | null`
|
||||
- **违反规则**:禁止 `as` 断言(除类型收窄外)。
|
||||
- **后果**:若前端意外提交 `File` 对象(如通过 FormData.append 上传文件),`parseSubjectTeachers` 会因 `typeof raw !== "string"` 返回 null 而静默丢弃数据。
|
||||
|
||||
#### 问题 E2:data-access-invitations.ts 中两处 string → union 强转(🟢 P2)
|
||||
|
||||
- **位置**:[data-access-invitations.ts:274](file:///e:/Desktop/CICD/src/modules/classes/data-access-invitations.ts#L274)、[363](file:///e:/Desktop/CICD/src/modules/classes/data-access-invitations.ts#L363)
|
||||
- **现状**:`record.status as ValidationResult["reason"]` / `status: row.status as InvitationCodeStatus`
|
||||
- **违反规则**:禁止 `as` 断言。
|
||||
- **后果**:DB 中出现意外值(如 "pending")不会触发类型错误,运行时返回错误 reason。
|
||||
|
||||
#### 问题 E3:class-trends-widget.tsx 中 as string[] 应使用类型守卫(🟢 P2)
|
||||
|
||||
- **位置**:[class-trends-widget.tsx:129](file:///e:/Desktop/CICD/src/modules/classes/components/class-detail/class-trends-widget.tsx#L129)
|
||||
- **现状**:`Array.from(new Set(assignments.map(a => a.subject).filter(Boolean))) as string[]`
|
||||
- **违反规则**:禁止 `as` 断言。
|
||||
- **后果**:`filter(Boolean)` 在 TypeScript 中不会收窄类型,跳过空值检查。
|
||||
|
||||
#### 问题 E4:14 个组件事件处理函数缺 Promise<void> 返回类型(🟢 P2)
|
||||
|
||||
- **位置**:my-classes-grid.tsx(4 处)、schedule-filters.tsx、schedule-view.tsx(4 处)、students-filters.tsx、students-table.tsx、class-invitation-manager.tsx(3 处)、edit-class-dialog.tsx
|
||||
- **违反规则**:函数返回值必须显式标注,特别是 `Promise<T>`。
|
||||
- **后果**:若将来函数内部 `return` 一个值(如返回 boolean 表示是否成功),调用方无类型提示。
|
||||
|
||||
### 2.6 文件大小
|
||||
|
||||
#### 问题 F1:schedule-view.tsx 527 行超出组件 500 行建议(🟡 P1)
|
||||
|
||||
- **位置**:[schedule-view.tsx](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx)
|
||||
- **现状**:同时承担"周历视图渲染 + 创建对话框 + 编辑对话框 + 删除确认对话框"4 个职责。
|
||||
- **违反规则**:React 组件 ≤ 500 行。
|
||||
- **后果**:可读性差、修改易引入回归。
|
||||
|
||||
#### 问题 F2:my-classes-grid.tsx 内含 210 行巨型组件 ClassTicket(🟢 P2)
|
||||
|
||||
- **位置**:[my-classes-grid.tsx:208-417](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L208)
|
||||
- **现状**:单一组件管理 4 段视觉职责(票据左侧信息+邀请码+趋势图+周历嵌入式渲染)。
|
||||
- **违反规则**:组件规范。
|
||||
- **后果**:调试困难。
|
||||
|
||||
### 2.7 组件复用性
|
||||
|
||||
#### 问题 G1:3 个 view 的 CRUD handler 几乎重复(🟢 P2)
|
||||
|
||||
- **位置**:admin-classes-view.tsx:41-95、grade-classes-view.tsx:31-85、schedule-view.tsx:100-158
|
||||
- **现状**:相同的 try/catch + toast + router.refresh 模式重复 9 次(每个 view 3 个 handler)。
|
||||
- **违反规则**:DRY。
|
||||
- **后果**:错误处理改进(如 D1 加 console.error)需修改 9 处。
|
||||
|
||||
### 2.8 可测试性
|
||||
|
||||
#### 问题 H1:纯函数内嵌组件未导出(🟢 P2)
|
||||
|
||||
- **位置**:
|
||||
- [my-classes-grid.tsx:40-48](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L40) `getSeededValue`
|
||||
- [my-classes-grid.tsx:268-272](file:///e:/Desktop/CICD/src/modules/classes/components/my-classes-grid.tsx#L268) `performanceChange` 计算
|
||||
- [schedule-view.tsx:160-181](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L160) `getPositionStyle`
|
||||
- [schedule-view.tsx:186-195](file:///e:/Desktop/CICD/src/modules/classes/components/schedule-view.tsx#L186) `getSubjectColor`
|
||||
- **违反规则**:可测试性「纯逻辑是否与 UI 分离」。
|
||||
- **后果**:业务计算逻辑(环比变化率、课表块定位、颜色映射)无法独立单测;`getSubjectColor` 还存在 C6 提到的功能 bug,但因内嵌而难以被发现。
|
||||
|
||||
### 2.9 i18n metadata 缺失
|
||||
|
||||
#### 问题 I1:8 个 teacher/student 页面缺 generateMetadata(🟢 P2)
|
||||
|
||||
- **位置**:teacher/classes/my、teacher/classes/my/[id]、teacher/classes/schedule、teacher/classes/students、student/learning/courses、student/learning/courses/[classId]、student/schedule、parent/children/[studentId]
|
||||
- **违反规则**:页面级 metadata.title 应走 i18n。
|
||||
- **后果**:浏览器标签栏、社交分享卡片等场景文案不本地化,SEO 友好度差。
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
参考 Google Classroom、钉钉教育、智学网、ClassDojo、PowerSchool 等 K12 班级管理产品的主流设计模式,对比当前实现差距:
|
||||
|
||||
### 3.1 班级列表与详情
|
||||
|
||||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||||
|---|---|---|---|
|
||||
| 班级卡片信息密度 | Google Classroom 卡片含教师头像、学生数、最近活动时间、未读作业数 | `my-classes-grid.tsx` 含邀请码、提交率趋势、周历嵌入,信息密度高但**未提供"最近活动"时间线** | 缺少"班级最近动态"feed |
|
||||
| 班级封面图 | Google Classroom / ClassDojo 支持自定义班级主题图 | 仅色彩区分 | 视觉识别度弱 |
|
||||
| 班级详情布局 | 智学网采用"Tab 切换(学生/作业/成绩/课表/设置)+ 顶部 sticky header" | `class-detail/` 采用 widget 网格布局 | widget 平铺在班级数多时滚动疲劳;可考虑 Tab 化 |
|
||||
| 班级归档 | Google Classroom 支持"归档班级",归档后只读但保留数据 | 无归档功能 | 学年结束后历史班级污染列表 |
|
||||
| 班级复制 | Google Classroom 支持复制班级(含学生/科目配置) | 无 | 新学年建班成本高 |
|
||||
|
||||
### 3.2 学生管理
|
||||
|
||||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||||
|---|---|---|---|
|
||||
| 学生加入方式 | 邀请码 + 邮件 + 批量导入 + 班级链接 | 已实现邀请码 + 邮件 + 批量导入 | 缺少"班级加入链接"(点击即加入) |
|
||||
| 学生列表筛选 | 智学网支持按科目成绩、出勤率、活跃度多维筛选 | `students-filters.tsx` 仅按班级 + 状态 | 缺少按学业表现筛选 |
|
||||
| 学生卡片信息 | Google Classroom 显示学生头像、最近提交、整体进度 | `students-table.tsx` 显示头像+科目成绩 | 缺少"最近提交/整体进度"时间维度 |
|
||||
| 学生迁移 | 钉钉教育支持"批量迁班"(学年升级时) | 无 | 学年升级时手动逐个调整 |
|
||||
| 学生邀请码状态可视化 | ClassDojo 显示邀请码扫描情况(已加入/待加入) | `class-invitation-manager.tsx` 显示邀请码列表 | 缺少"已扫码未加入"中间状态 |
|
||||
|
||||
### 3.3 课表
|
||||
|
||||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||||
|---|---|---|---|
|
||||
| 课表视图 | 智学网周课表 + 日课表 + 月课表三视图切换 | `schedule-view.tsx` 仅周课表 | 缺少日/月视图 |
|
||||
| 课表冲突检测 | PowerSchool 在添加时自动检测教室/教师/时段冲突 | 依赖 scheduling 模块外部检测 | 当前 actions-schedule 未触发检测(B2) |
|
||||
| 课表颜色编码 | 钉钉教育按科目自动配色 | `getSubjectColor` 中文失效(C6) | **功能 bug,必须修复** |
|
||||
| 课表导出/打印 | 智学网支持导出 PDF/Excel、打印 | 无 | 教师打印课表需求未满足 |
|
||||
| 课表提醒 | Google Classroom 课前 5 分钟推送提醒 | 无 | 缺少课表提醒集成 |
|
||||
|
||||
### 3.4 多角色协作
|
||||
|
||||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||||
|---|---|---|---|
|
||||
| 家长视角班级信息 | ClassDojo 家长看到班级公告、教师动态、孩子表现 | parent 仅看到孩子课表 + 班级概览 | 缺少"班级动态 feed" |
|
||||
| 学生视角班级首页 | Google Classroom 学生首页是"待办作业流" | student/learning/courses 是班级列表 | 缺少"班级作业待办流"聚合视图 |
|
||||
| 跨班级协作 | 钉钉教育支持"年级主任一键查看所有班级对比" | management/grade 已有 insights | ✅ 已实现,对比图较完善 |
|
||||
| 班级消息 | Google Classroom 班级内消息流 | messaging 模块支持 `class_members` scope | ✅ 已通过 messaging 模块实现 |
|
||||
|
||||
### 3.5 数据洞察
|
||||
|
||||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||||
|---|---|---|---|
|
||||
| 班级健康度评分 | PowerSchool 综合出勤+成绩+参与度给出班级健康分 | `class-trends-widget.tsx` 仅展示提交率/平均分 | 缺少综合健康度评分 |
|
||||
| 早期预警 | ClassDojo 识别"低参与度学生"自动预警 | 无 | 缺少学生风险预警 |
|
||||
| 班级对比 | 智学网支持同年级班级多维度对比 | `management/grade/insights` 已有 | ✅ |
|
||||
| 趋势同比环比 | PowerSchool 提供周/月/学期同比 | `class-trends-widget.tsx` 仅"Latest"指标 | 缺少时间维度对比 |
|
||||
|
||||
### 3.6 可访问性与性能
|
||||
|
||||
| 维度 | 优秀实践 | 当前实现 | 差距 |
|
||||
|---|---|---|---|
|
||||
| 键盘导航 | WCAG 2.1 AA 要求所有交互可键盘操作 | 班级列表/课表键盘可访问但无 focus-visible 样式优化 | a11y 待加强 |
|
||||
| 流式渲染 | React 18+ Suspense 流式渲染 | 全部 RSC 同步获取,无 Suspense 边界 | 班级详情 4 个并行查询可流式 |
|
||||
| 骨架屏精确度 | 骨架屏应反映实际布局 | `class-skeleton.tsx` 5 个 skeleton 布局匹配 | ✅ 良好 |
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0 — 安全/功能阻断(立即修复)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|---|---|
|
||||
| P0-1 | A1:data-access.ts 与拆分文件 3 对同名函数重复定义 | 删除 data-access.ts 中的本地定义,统一从拆分文件导出(保持 barrel 入口兼容) |
|
||||
| P0-2 | B1:listClassInvitationCodesAction 无归属校验 | 在 actions 层调用 `verifyTeacherOwnsClass(classId, ctx.userId)`(admin scope 跳过) |
|
||||
| P0-3 | B2:3 个 schedule action 无归属校验 | 同 P0-2,调用 `verifyTeacherOwnsClass` |
|
||||
| P0-4 | C6:getSubjectColor 中文科目不匹配 | 改用科目 ID 或类型守卫匹配;纯函数抽出到 `schedule-utils.ts` 并补单测 |
|
||||
| P0-5 | B3:教师 update/delete/enroll action 未在 actions 层做归属校验 | actions 层显式判断 `hasAdminScope(ctx)` 否则校验 `classes.teacherId === ctx.userId` |
|
||||
|
||||
### P1 — 重要合规性(本期实施)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|---|---|
|
||||
| P1-1 | C1:class-detail/ 8 子组件硬编码英文 | 全部接入 `useTranslations("classes.detail.*")` |
|
||||
| P1-2 | C2:schedule-view / schedule-filters / students-filters 硬编码英文 | 接入 i18n |
|
||||
| P1-3 | C3:所有 actions message 英文硬编码 | 在 actions 层使用 `getTranslations()` 或返回错误码由组件层翻译 |
|
||||
| P1-4 | C4:30+ error.tsx 硬编码中文 | 抽取 `shared/components/ErrorState` + i18n key `common.error.boundary.*` |
|
||||
| P1-5 | C5:DEFAULT_CLASS_SUBJECTS 与 excludeSubjects 硬编码 | 改为从 `subjects` 表查询;统一单一来源 |
|
||||
| P1-6 | B4:data-access 中 7 处 roles.name 硬编码 | 抽出 `ROLE_TEACHER` 常量 |
|
||||
| P1-7 | B5:student 三个页面未调用 requirePermission | 补 `requirePermission(Permissions.HOMEWORK_SUBMIT)` 或新增 STUDENT_READ 权限 |
|
||||
| P1-8 | D1:10 处 catch 块未记录错误 | 改为 `catch (error) { console.error("[classes] xxx:", error); toast.error(...) }` |
|
||||
| P1-9 | D2:getTeacherClasses 静默返回空数组 | 区分"DB 错误"与"无数据",DB 错误抛出 |
|
||||
| P1-10 | D3:actions 层错误处理两套风格 | 统一为 `handleActionError` |
|
||||
| P1-11 | D4:teacher/classes/my/[id] 缺 loading.tsx 和 error.tsx | 新增 |
|
||||
| P1-12 | D5:10 个页面缺 error.tsx | 补齐 |
|
||||
| P1-13 | F1:schedule-view.tsx 527 行超限 | 抽出 3 个对话框子组件 |
|
||||
|
||||
### P2 — 工程优化(中长期)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|---|---|
|
||||
| P2-1 | E1/E2/E3:5 处 as 断言 | 改用类型守卫 |
|
||||
| P2-2 | E4:14 个事件处理函数缺 Promise<void> | 补返回类型 |
|
||||
| P2-3 | G1:3 个 view CRUD handler 重复 | 抽 `useClassFormHandlers` hook |
|
||||
| P2-4 | H1:纯函数内嵌组件 | 抽到 `class-stats-utils.ts` / `schedule-utils.ts` 并补单测 |
|
||||
| P2-5 | F2:my-classes-grid ClassTicket 210 行 | 拆分为 `ClassTicketHeader` / `ClassTicketInvitation` / `ClassTicketTrend` |
|
||||
| P2-6 | A2:class-invitation-manager 绝对路径 | 改为相对路径 |
|
||||
| P2-7 | I1:8 个页面缺 generateMetadata | 补齐 |
|
||||
| P2-8 | 行业差距:班级归档/复制 | 中长期产品规划 |
|
||||
| P2-9 | 行业差距:班级健康度评分/早期预警 | 中长期产品规划 |
|
||||
| P2-10 | 行业差距:课表多视图/导出/提醒 | 中长期产品规划 |
|
||||
|
||||
### 重构设计原则(强制满足)
|
||||
|
||||
为达成"完全解耦 + 组合优先 + 国际化就绪 + 最大化复用 + 错误边界 + 可测试 + 可扩展 + 企业级补充"八项原则,本次实施遵循:
|
||||
|
||||
1. **完全解耦**:classes 模块内部组件不直接 import 其他业务模块(exams/grades/homework/scheduling)的 actions/data-access;跨模块数据通过本模块 data-access 暴露的接口调用(已基本达成,仅需修复 P0-2/P0-3 的越权问题)。
|
||||
2. **组合优先**:错误处理抽 `useClassFormHandlers` hook;纯逻辑抽 `class-stats-utils.ts`、`schedule-utils.ts`;详情 widget 通过配置驱动(`ClassDetailWidgetConfig`)。
|
||||
3. **国际化就绪**:所有修复项必须使用 i18n key;新增翻译键写入 `messages/{locale}/classes.json` 的 `detail.*` / `schedule.*` / `students.*` 命名空间。
|
||||
4. **最大化复用**:admin/grade/teacher/student 共用 `ClassListTable` / `ClassFormDialog` / `ClassDeleteDialog` / `useClassData` / `useClassFilters` / `useClassFormHandlers`。
|
||||
5. **错误与边界**:每个详情 widget 独立用 `ClassErrorBoundary` 包裹;RSC 数据获取用 React Suspense + 骨架屏;空数据/无权限/网络异常状态明确处理。
|
||||
6. **可测试性**:纯函数导出至 `*.ts` 文件,便于 vitest 单测;接口类型显式标注。
|
||||
7. **可扩展性**:角色差异通过 `hasAdminScope` / `hasTeacherScope` 抽象,新增角色只改 actions-shared.ts。
|
||||
8. **企业级补充**:补 a11y(focus-visible、ARIA)、性能(Suspense 流式)、安全(actions 层归属校验)、监控(埋点接口预留 `trackClassEvent` 工具函数)。
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图存在以下遗漏或不一致,需在实施修复时同步更新:
|
||||
|
||||
| # | 位置 | 问题 | 修复动作 |
|
||||
|---|---|---|---|
|
||||
| 1 | 005 `modules.classes.files` | 仅列 21 个文件,漏 schema.ts/types.ts/12 个组件文件 | 补全至 33 个文件 |
|
||||
| 2 | 005 `modules.classes.exports.actions` | 漏 `createClassInvitationCodeAction` / `revokeClassInvitationCodesAction` / `listClassInvitationCodesAction` 3 个 v3 邀请码 action | 补全至 20 个 |
|
||||
| 3 | 005 `modules.classes.exports.dataAccess` | 漏 `compareClassLike` / `isDuplicateInvitationCodeError` / `generateUniqueInvitationCode` / `getAccessibleClassIdsForTeacher` / `getSessionTeacherId` / `getTeacherSubjectIdsForClass` 6 个工具函数 | 补全 |
|
||||
| 4 | 005 `dbTables` | `classInvitationCodes` 表字段未详细登记 | 补全字段定义 |
|
||||
| 5 | 005 `dbTables` | `classSchedule` 的 `usedBy` 字段未含 scheduling | 补充 `["classes", "scheduling"]` |
|
||||
| 6 | 005 `knownIssues.P0-1` | 仍记录原始问题状态,未标注「已修复」 | 更新 status 字段 |
|
||||
| 7 | 004 §2.7「被依赖」 | 遗漏 `elective` / `error-book` / `adaptive-practice` 三个模块 | 补全至 13 个 |
|
||||
| 8 | 005 `dependencyMatrix` | `exams → classes` 与 `homework → classes` 边未明确登记 | 补全边定义 |
|
||||
| 9 | 004 §2.7 已知问题 | P0-2/P0-3/P0-4/P0-5 等本次新增修复 | 同步新增修复记录 |
|
||||
| 10 | 004 §2.7 文件清单 | 修复后行数变化(如 schedule-view.tsx 拆分) | 更新行数 |
|
||||
262
docs/architecture/audit/archive/course-plans-audit-report.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 课程计划模块审计报告
|
||||
|
||||
> 审计日期:2026-06-25
|
||||
> 审计范围:`src/modules/course-plans/` 全部文件 + `src/app/(dashboard)/{admin,teacher}/course-plans/` 全部页面
|
||||
> 审计依据:`e:\Desktop\CICD\.trae\rules\project_rules.md`、`docs/architecture/004_architecture_impact_map.md` §2.18、`docs/architecture/005_architecture_data.json` modules.`course-plans`
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
| 层级 | 文件数 | 主要文件(行数) |
|
||||
|------|--------|------------------|
|
||||
| types/schema | 2 | types.ts(97)、schema.ts(180) |
|
||||
| data-access | 1 | data-access.ts(425) |
|
||||
| actions | 1 | actions.ts(284) |
|
||||
| components | 5 | course-plan-list.tsx(160)、course-plan-detail.tsx(243)、course-plan-form.tsx(284)、course-plan-item-editor.tsx(248)、course-plan-progress.tsx(38) |
|
||||
| 页面 | 6 | admin(4: list/detail/create/edit)、teacher(2: list/detail) |
|
||||
| i18n | 2 | zh-CN/course-plans.json(15)、en/course-plans.json(15) |
|
||||
|
||||
文件行数均在规范范围内(组件 ≤500、actions/data-access ≤800)。
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
页面(Server Component)
|
||||
├─ admin/* → 直接调用 data-access.getCoursePlans / getCoursePlanById(无 requirePermission)
|
||||
├─ teacher/* → requirePermission(COURSE_PLAN_READ) → data-access(按 teacherId 过滤)
|
||||
└─ management/grade/dashboard → data-access.getGradeCoursePlanProgress
|
||||
↓
|
||||
动态 import classes data-access.getClassesByGradeId
|
||||
↓
|
||||
JOIN course_plans + course_plan_items
|
||||
Client Components
|
||||
├─ CoursePlanList → usePermission() → 本地筛选
|
||||
├─ CoursePlanDetail → 直接 import deleteCoursePlanAction
|
||||
├─ CoursePlanForm → 直接 import create/updateCoursePlanAction
|
||||
└─ CoursePlanItemEditor → 直接 import item CRUD actions
|
||||
```
|
||||
|
||||
### 1.3 架构图完整性
|
||||
|
||||
`docs/architecture/004_architecture_impact_map.md` §2.18 与 `005_architecture_data.json` 已记录该模块的导出函数、文件清单、依赖关系,与实际代码**基本一致**。但存在以下遗漏与不一致:
|
||||
|
||||
- `data-access.ts` 中 `getSubjectOptions` 函数**未在架构图 exports 中记录**
|
||||
- `data-access.ts` 中 `reorderCoursePlanItems` 函数**未在架构图 exports 中记录**(且无对应 Action / UI,属于死代码)
|
||||
- 架构图标注 `getCoursePlansAction`/`getCoursePlanAction` 的 `usedBy` 为"待扩展",实际仍无消费方
|
||||
- 架构图依赖矩阵显示 course-plans → classes/school 为"✅"(通过 data-access),但实际 `buildPlanSelect` **直接 JOIN** classes/subjects/users 表,并非通过 data-access 调用——架构图记录与实现不一致
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 安全与权限问题(P0)
|
||||
|
||||
#### 问题 1:教师详情页未校验计划归属 — 信息泄露漏洞
|
||||
|
||||
- **位置**:`src/app/(dashboard)/teacher/course-plans/[id]/page.tsx` 第 16-18 行;`data-access.ts` `getCoursePlanById` 第 168-192 行
|
||||
- **问题**:教师详情页仅调用 `requirePermission(COURSE_PLAN_READ)` 后直接 `getCoursePlanById(id)`,**未校验该计划是否属于当前教师**。`getCoursePlanById` 也不接受 `userId` 参数。
|
||||
- **违反规则**:项目规则 "Parent routes must include permission checks with both parentId and studentId to prevent information leakage"(同理,教师路由也应校验 teacherId 归属);"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"
|
||||
- **后果**:任何持有 `COURSE_PLAN_READ` 权限的教师,通过枚举/猜测 planId 即可查看全校所有课程计划详情(含其他班级、其他科目的教学进度、大纲、目标),构成信息泄露
|
||||
|
||||
#### 问题 2:admin 列表页无 requirePermission 调用
|
||||
|
||||
- **位置**:`src/app/(dashboard)/admin/course-plans/page.tsx` 全文(第 23-49 行)
|
||||
- **问题**:admin 列表页**未调用 `requirePermission()`**,直接调用 `getCoursePlans()` 返回全部数据。对比 `teacher/course-plans/page.tsx` 第 27 行有 `requirePermission` 调用——admin 与 teacher 页面权限处理不一致。
|
||||
- **违反规则**:项目规则 "所有 Server Action 必须调用 requirePermission() 进行权限校验"(页面层虽非 Action,但 data-access 直接被 Server Component 调用时同样需校验);依赖布局层保护属于隐式安全,不符合纵深防御原则
|
||||
- **后果**:若布局层权限配置被误改,admin 列表页将完全暴露
|
||||
|
||||
#### 问题 3:data-access 函数无数据范围(DataScope)过滤
|
||||
|
||||
- **位置**:`data-access.ts` `getCoursePlans`(第 144-166 行)、`getCoursePlanById`(第 168-192 行)、`getGradeCoursePlanProgress`(第 335-424 行)
|
||||
- **问题**:所有查询函数**均不接受 userId / dataScope 参数**,不进行任何归属过滤。`getCoursePlans` 仅靠调用方传入 `teacherId` 参数过滤,但参数可选且可被绕过。
|
||||
- **违反规则**:项目规则 "所有敏感数据查询必须在 data-access 层结合当前用户权限过滤,Server Action 二次校验"
|
||||
- **后果**:未来新增 parent/student 路由时,若直接复用这些函数将导致越权;当前教师详情页已暴露此问题(见问题 1)
|
||||
|
||||
### 2.2 国际化严重缺失(P0)
|
||||
|
||||
#### 问题 4:组件内大量硬编码文本,中英文混杂
|
||||
|
||||
- **位置**:
|
||||
- `course-plan-list.tsx`:第 25-47 行 `STATUS_LABEL`/`STATUS_VARIANT`/`FILTER_OPTIONS` 全英文硬编码;第 99/108-112 行 "New Course Plan"/"No course plans"/"There are no course plans yet." 等
|
||||
- `course-plan-detail.tsx`:第 30-35 行 `STATUS_LABEL` 全中文硬编码("规划中"/"进行中"/"已完成"/"已暂停");第 94/99/106-113/124-134/146-153/161-172/178-183/213 行大量中文硬编码
|
||||
- `course-plan-form.tsx`:第 98/105/122/139/156/173/186/203/214/225/234/247/257 行全英文硬编码("New Course Plan"/"Class"/"Subject"/"Teacher" 等)
|
||||
- `course-plan-item-editor.tsx`:第 119/125/136/148/159/172/180/191 行全英文硬编码
|
||||
- `course-plan-progress.tsx`:第 25/27 行 "Progress"/"hours" 硬编码
|
||||
- `teacher/course-plans/page.tsx`:第 41-43 行 "My Course Plans"/"View your course teaching plans..." 硬编码
|
||||
- **违反规则**:项目规则 "所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"
|
||||
- **后果**:
|
||||
1. 国际化完全不可用——切换语言后课程计划模块仍显示混合中英文
|
||||
2. 同一模块内 `course-plan-detail.tsx`(中文)与 `course-plan-list.tsx`(英文)状态标签不一致,用户体验割裂
|
||||
3. 翻译文件 `course-plans.json` 仅含 5 个键(title/description/detail/edit/create),远不满足组件需要
|
||||
|
||||
### 2.3 架构违规问题(P1)
|
||||
|
||||
#### 问题 5:跨模块直接 JOIN 其他模块数据库表
|
||||
|
||||
- **位置**:`data-access.ts` `buildPlanSelect` 第 115-142 行
|
||||
- **问题**:直接 `leftJoin(classes, ...)`、`leftJoin(subjects, ...)`、`leftJoin(users, ...)`,分别查询 classes 模块、school 模块、users 模块拥有的表。架构图却标注为"✅ 通过 data-access"。
|
||||
- **违反规则**:项目规则 "模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表"
|
||||
- **后果**:classes/school/users 模块的表结构变更将直接影响 course-plans 查询;模块未真正解耦,无法独立测试
|
||||
|
||||
#### 问题 6:缺少 loading.tsx / error.tsx
|
||||
|
||||
- **位置**:`src/app/(dashboard)/admin/course-plans/` 和 `src/app/(dashboard)/teacher/course-plans/` 全部路由
|
||||
- **问题**:6 个页面路由均**无 loading.tsx 和 error.tsx**。
|
||||
- **违反规则**:项目规则 "All student routes must include loading.tsx and error.tsx for error boundaries"(best practice 推广至所有角色路由)
|
||||
- **后果**:数据加载期间白屏;运行时错误无边界捕获,导致整页崩溃
|
||||
|
||||
#### 问题 7:使用原生 `<a>` 标签替代 `<Link>`
|
||||
|
||||
- **位置**:`course-plan-list.tsx` 第 97 行 `<a href={createHref}>`、第 149 行 `<a key={plan.id} href={href}>`
|
||||
- **违反规则**:项目规则 "Link navigation must use Next.js `<Link>` component instead of raw `<a>` tags"
|
||||
- **后果**:点击导航触发整页刷新,丢失客户端状态,无预取优化
|
||||
|
||||
### 2.4 代码质量问题(P1)
|
||||
|
||||
#### 问题 8:使用 `as` 类型断言
|
||||
|
||||
- **位置**:
|
||||
- `course-plan-list.tsx` 第 73 行:`setFilter(value as Filter)`
|
||||
- `course-plan-form.tsx` 第 174 行:`setSemester(v as "1" | "2")`;第 188 行:`setStatus(v as CoursePlanStatus)`
|
||||
- `teacher/course-plans/page.tsx` 第 19 行:`(v as CoursePlanStatus)`
|
||||
- **违反规则**:项目规则 "禁止 as 断言(除类型收窄外)"
|
||||
- **后果**:运行时类型不安全,应使用类型守卫函数(如 admin 页面已实现的 `isValidStatus`)
|
||||
|
||||
#### 问题 9:基于 URL 字符串判断角色的脆弱逻辑
|
||||
|
||||
- **位置**:
|
||||
- `course-plan-detail.tsx` 第 63 行:`backHref?.includes("/teacher/") ? "/teacher/course-plans" : "/admin/course-plans"`
|
||||
- `course-plan-form.tsx` 第 81 行:同样的 `backHref?.includes("/teacher/")` 模式
|
||||
- **问题**:通过 URL 路径字符串推断用户角色来决定跳转目标,而非通过权限/角色上下文。
|
||||
- **违反规则**:项目规则 "前端权限判断统一使用 usePermission().hasPermission(),严禁出现 role === 'xxx' 硬编码"(URL 路径推断属于同类硬编码)
|
||||
- **后果**:新增 parent/student 路由时跳转逻辑将出错;URL 结构调整即破坏功能
|
||||
|
||||
#### 问题 10:Server Action 入参未经验证
|
||||
|
||||
- **位置**:`actions.ts` `getCoursePlansAction` 第 135-145 行(params 未 Zod 验证)、`getGradeCoursePlanProgressAction` 第 269-284 行(gradeId 仅检查非空)
|
||||
- **违反规则**:项目规则 "输入使用 Zod 验证,验证失败返回结构化错误"
|
||||
- **后果**:恶意参数可能绕过预期过滤条件
|
||||
|
||||
#### 问题 11:死代码 — `reorderCoursePlanItems` 无消费方
|
||||
|
||||
- **位置**:`data-access.ts` 第 290-313 行
|
||||
- **问题**:`reorderCoursePlanItems` 函数存在但无对应 Server Action、无 UI 调用方,架构图也未记录。
|
||||
- **违反规则**:项目规则 "避免过度工程" + 架构图同步规则
|
||||
- **后果**:死代码增加维护负担;架构图与实际不一致
|
||||
|
||||
### 2.5 错误处理与边界缺失(P2)
|
||||
|
||||
#### 问题 12:无 Error Boundary / Suspense / 骨架屏
|
||||
|
||||
- **位置**:全部组件和页面
|
||||
- **问题**:数据区块未用 React Error Boundary 包裹;异步加载无 Suspense + 骨架屏;空数据虽有基础 `EmptyState` 但无操作引导(CTA)。
|
||||
- **违反规则**:审计要求 "每个独立的数据区块必须用 React Error Boundary 包裹;异步数据使用 React Suspense + 骨架屏"
|
||||
- **后果**:局部数据错误导致整页不可用;加载体验差
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
基于 K12 教育管理系统(如 PowerSchool、Canvas、Schoology、钉钉教育、企业自建校管系统)在课程计划/教学进度模块的主流实践,当前差距如下:
|
||||
|
||||
| 维度 | 行业优秀实践 | 当前实现 | 影响 |
|
||||
|------|-------------|---------|------|
|
||||
| **角色覆盖** | admin/teacher/parent/student 四角色均可查看课程计划(按权限脱敏) | 仅 admin/teacher 有路由,parent/student 完全无入口 | 家长无法了解孩子本学期教学安排;学生无法预览学习进度 |
|
||||
| **进度可视化** | 甘特图/时间轴展示周计划进度,颜色区分已完成/进行中/待开始 | 仅一个简单 Progress 条 + 表格列表 | 管理者难以一目了然掌握全年级教学进度 |
|
||||
| **数据联动** | 周计划条目关联作业/考试/教材章节,可一键跳转 | `textbookChapter` 仅存文本,无关联跳转 | 教师需手动查找对应教材和作业 |
|
||||
| **批量操作** | 批量标记完成、批量调整周次、批量复制计划到其他班级 | 无任何批量操作 | 管理员配置多班级计划时重复劳动 |
|
||||
| **模板复用** | 提供标准课程计划模板,可从模板创建或复制历史计划 | 每次从零创建 | 教师重复录入 |
|
||||
| **拖拽排序** | 周计划条目支持拖拽调整顺序 | data-access 有 `reorderCoursePlanItems` 但无 UI | 死代码,功能缺失 |
|
||||
| **导出打印** | 导出 PDF/Excel 教学进度报告 | 无 | 无法线下归档或上报 |
|
||||
| **空状态 CTA** | 空状态带"创建第一个计划"引导按钮 | 有 EmptyState 但无 CTA 按钮 | 新用户不知如何开始 |
|
||||
| **骨架屏** | 加载时显示结构化骨架屏 | 无 loading.tsx | 加载白屏 |
|
||||
| **日历视图** | 月历/周历视图展示教学安排 | 无 | 教师难以对照实际日期安排教学 |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0 — 安全与国际化(必须立即修复)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P0-1 | 教师详情页信息泄露 | `getCoursePlanById` 增加 `userId` + `dataScope` 参数,data-access 层过滤归属;非 admin 仅能查看自己负责的计划 |
|
||||
| P0-2 | admin 页面无 requirePermission | admin 所有页面补充 `requirePermission(COURSE_PLAN_READ)` |
|
||||
| P0-3 | data-access 无 DataScope 过滤 | `getCoursePlans`/`getCoursePlanById`/`getGradeCoursePlanProgress` 增加可选 `scope` 参数,按 classIds/teacherId 过滤 |
|
||||
| P0-4 | i18n 严重缺失 | 提取全部硬编码文本到 `course-plans.json`,补全 zh-CN/en 翻译键(状态标签、表单字段、按钮、空状态、Toast 消息等) |
|
||||
|
||||
### P1 — 架构合规与代码质量
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P1-1 | 跨模块直接 JOIN | 定义 `CoursePlanDataService` 接口抽象 classes/subjects/users 数据依赖,通过组合注入;或先抽取 `getClassNameById`/`getSubjectNameById`/`getTeacherNameById` 轻量 data-access 调用替代 JOIN |
|
||||
| P1-2 | 缺 loading.tsx/error.tsx | 为 admin 和 teacher 路由补充 loading.tsx(骨架屏)和 error.tsx(错误边界) |
|
||||
| P1-3 | 原生 `<a>` 标签 | 替换为 Next.js `<Link>` 组件 |
|
||||
| P1-4 | `as` 断言 | 替换为类型守卫函数(`isValidStatus`/`isValidSemester`) |
|
||||
| P1-5 | URL 路径推断角色 | 改为通过 `usePermission().hasPermission()` 决定跳转基础路径,或由页面 props 传入 `successHref` |
|
||||
| P1-6 | Action 入参未验证 | `getCoursePlansAction`/`getGradeCoursePlanProgressAction` 增加 Zod schema 验证 |
|
||||
| P1-7 | 死代码 reorderCoursePlanItems | 删除或补充对应 Action + UI(推荐补充拖拽排序 UI) |
|
||||
|
||||
### P2 — 体验与企业级增强(中长期)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P2-1 | 无 Error Boundary / 骨架屏 | 组件级 Error Boundary 包裹数据区块;Suspense + 骨架屏 |
|
||||
| P2-2 | parent/student 无路由 | 新增 parent/student 课程计划只读路由(按孩子班级过滤) |
|
||||
| P2-3 | 无数据联动 | 周计划条目关联教材章节/作业,支持跳转 |
|
||||
| P2-4 | 无批量操作 | 批量标记完成、批量复制计划 |
|
||||
| P2-5 | 无模板复用 | 课程计划模板库,从模板创建 |
|
||||
| P2-6 | 无导出 | PDF/Excel 导出教学进度报告 |
|
||||
| P2-7 | 无日历视图 | 月历视图对照实际日期 |
|
||||
| P2-8 | 监控埋点 | 预留 `trackCoursePlanEvent()` 埋点接口 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图需补充/修改以下内容(✅ 已全部完成同步,含 P0/P1/P2 全部实施):
|
||||
|
||||
### 004_architecture_impact_map.md §2.18
|
||||
|
||||
1. **✅ 已补充导出函数**:
|
||||
- data-access:`bulkUpdateItemCompleted`、`copyCoursePlanToClasses`、`enrichPlanRows`(名称解析解耦)、`buildScopeCondition`(权限过滤)
|
||||
- actions:`reorderCoursePlanItemsAction`、`bulkToggleItemsAction`、`copyCoursePlanAction`、`getTemplateCandidatesAction`(P2-5 新增)、`trackCoursePlanEvent`
|
||||
- lib:`lib/export-utils.ts`(P2-6 CSV 导出纯函数)、`lib/calendar-utils.ts`(P2-7 日历视图纯函数 + 日期工具)
|
||||
- types:`CoursePlanQueryScope`、类型守卫 `isCoursePlanStatus`/`isCoursePlanSemester`、配置驱动 `ROLE_WIDGET_CONFIG`、`CalendarEvent`(P2-7)、`CoursePlanExportColumnKey`/`CoursePlanColumnLabels`(P2-6)
|
||||
- components:新增 `SortableWeekRow`(P1-7/P2-3)、`CoursePlanCalendar`(P2-7)、`TemplatePickerDialog`(P2-5)
|
||||
2. **✅ 已修正依赖关系描述**:
|
||||
- 旧描述 "依赖 classes/school(合理)" → 新描述 "通过动态 import `getClassNamesByIds`/`getSubjectNameMapByIds`/`getUserNamesByIds` 批量解析,不再直接 JOIN"
|
||||
- `getSubjectOptions` 已移至 school 模块(pages 改为从 `@/modules/school/data-access` 导入)
|
||||
- 新增依赖:`shared/lib/export-utils.ts`(P2-6)、`@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`(P1-7)
|
||||
3. **✅ 已补充已知问题修复状态**:P0-1 至 P1-7 + P2-1 至 P2-8 全部标记为已修复
|
||||
4. **✅ 已补充页面路由表**:10 个路由(含 P2-2 新增 parent/student 4 个路由)+ 权限 + loading/error 状态
|
||||
5. **✅ 已更新文件清单行数**:反映重构后的实际行数(含新增 lib/ 与 components/ 文件)
|
||||
|
||||
### 005_architecture_data.json modules.`course-plans`
|
||||
|
||||
1. **✅ 已补充 actions 节点**:`reorderCoursePlanItemsAction`、`bulkToggleItemsAction`、`copyCoursePlanAction`、`getTemplateCandidatesAction`(P2-5)
|
||||
2. **✅ 已补充 dataAccess 节点**:`bulkUpdateItemCompleted`、`copyCoursePlanToClasses`
|
||||
3. **✅ 已移除 `getSubjectOptions`**:该函数已从 course-plans 模块删除,改用 school 模块
|
||||
4. **✅ 已更新 `getCoursePlans`/`getCoursePlanById` 签名**:增加 `scope?: CoursePlanQueryScope` 参数
|
||||
5. **✅ 已更新依赖关系**:移除 `shared.db.schema.classes/subjects/users`,改为动态 import 对方 data-access
|
||||
6. **✅ 已补充 schemas**:`GetCoursePlansParamsSchema`、`GradeIdSchema`、`ReorderItemsSchema`、`BulkToggleSchema`、`CopyPlanSchema`
|
||||
7. **✅ 已补充 types**:`CoursePlanQueryScope`、`GradeCoursePlanProgressItem`、`GradeCoursePlanProgressResult`、`isCoursePlanStatus`、`isCoursePlanSemester`、`CoursePlanWidgetId`、`RoleWidgetConfig`、`ROLE_WIDGET_CONFIG`、`CalendarEvent`(P2-7)、`CoursePlanExportColumnKey`/`CoursePlanColumnLabels`(P2-6)
|
||||
8. **✅ 已更新 components 描述**:反映 P1 + P2 全部修复内容(Error Boundary、拖拽、数据联动、导出、日历、模板)
|
||||
9. **✅ 已补充 lib 节点**:`export-utils.ts`、`calendar-utils.ts`(P2 新增)
|
||||
10. **✅ 已补充新增 components**:`SortableWeekRow`、`CoursePlanCalendar`、`TemplatePickerDialog`
|
||||
|
||||
### shared/components/section-error-boundary.tsx(P2-1 重构)
|
||||
|
||||
- **✅ 已重构**:类组件 + 函数式包装器双层结构
|
||||
- 函数式包装器自动注入 i18n 文案(`{namespace}.error.boundaryTitle` / `boundaryDescription` / `retry`)
|
||||
- 支持 `fallback` 自定义降级 UI(函数形式 `(error, reset) => ReactNode`)
|
||||
- 支持 `onError` 回调(用于埋点/监控,AI 模块复用)
|
||||
- a11y:`role="alert"` + `aria-live="assertive"` + 重试按钮 `aria-label`
|
||||
|
||||
### shared/lib/export-utils.ts(P2-6 新增)
|
||||
|
||||
- **✅ 已从 `dashboard/lib/export-utils.ts` 迁移至 shared 层**,供所有模块复用
|
||||
- 导出:`toCSV`、`downloadFile`、`exportCSV`、`ExportRow`、`ExportColumn` 类型
|
||||
320
docs/architecture/audit/archive/dashboard-audit-report-v4.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# Dashboard 模块 V4 审计报告
|
||||
|
||||
**审计日期**:2026-06-22
|
||||
**审计范围**:`src/modules/dashboard/` + 所有 dashboard 路由文件 + parent dashboard 组件
|
||||
**前置审计**:
|
||||
- v1(P0 修复:跨模块 DB 查询、权限、i18n 容器组件)
|
||||
- v2(10 个子组件 i18n、DashboardGreetingHeader 抽象、31 个纯函数单测、a11y 语义化标签)
|
||||
- v3(ContentRow 标签错配、admin/error.tsx i18n、空趋势数据空状态、loading/error.tsx 补齐、日期 locale、死代码清理、`as` 断言修复、流式架构 React `use()`)
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
仪表盘模块位于 `src/modules/dashboard/`,包含 29 个文件:
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|----|------|------|------|
|
||||
| actions | `actions.ts` | 167 | 4 个 Server Action(admin/teacher/student/parent),均调用 `requirePermission()` |
|
||||
| data-access | `data-access.ts` | 49 | admin 仪表盘数据聚合(并行调用 6 个模块 stats 函数) |
|
||||
| streams | `streams.ts` | 34 | admin 流式数据源(返回未解析 Promise 供 React `use()` 消费) |
|
||||
| types | `types.ts` | 74 | AdminDashboardData / StudentDashboardProps / TeacherDashboardData |
|
||||
| lib | `lib/dashboard-utils.ts` | 198 | 6 个纯函数(weekday / 统计 / 排序 / 指标计算 / 问候语) |
|
||||
| components | `dashboard-section.tsx` | 170 | Error Boundary + Suspense + 骨架屏(5 种变体) |
|
||||
| components | `dashboard-greeting-header.tsx` | 36 | 共享问候头部 |
|
||||
| components | `dashboard-error-fallback.tsx` | 30 | 路由级错误回退 |
|
||||
| components | `dashboard-loading-skeleton.tsx` | 44 | 路由级加载骨架 |
|
||||
| admin-dashboard | `admin-dashboard.tsx` | 173 | 管理员视图(流式架构) |
|
||||
| admin-dashboard | `admin-sections.tsx` | 231 | 管理员 6 个分区组件 |
|
||||
| admin-dashboard | `user-growth-chart.tsx` | 65 | recharts 折线图 |
|
||||
| teacher-dashboard | 9 文件 | ~700 | 教师仪表盘组件 |
|
||||
| student-dashboard | 6 文件 | ~530 | 学生仪表盘组件 |
|
||||
| tests | `dashboard-section.test.tsx` | 70 | Error Boundary + 骨架屏单测 |
|
||||
| tests | `tests/integration/dashboard/dashboard-utils.test.ts` | 408 | 6 个纯函数 31 个单测 |
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
[Page] → [Action] → [requirePermission] → [data-access / 其他模块 data-access]
|
||||
↓
|
||||
[lib/dashboard-utils 纯函数计算]
|
||||
↓
|
||||
[View 组件] → [DashboardSection Suspense]
|
||||
```
|
||||
|
||||
### 1.3 架构图记录完整性
|
||||
|
||||
架构影响地图(004/005)已覆盖 dashboard 模块的:
|
||||
- 4 个 Server Action 签名、依赖、使用方
|
||||
- 6 个纯函数签名和用途
|
||||
- 依赖矩阵(dependsOn: shared/auth/homework/classes)
|
||||
- 路由权限映射(dashboardRoutePermissions)
|
||||
- 组件清单和行数
|
||||
|
||||
**遗漏**:架构图未记录 `streams.ts` 的流式数据源函数,也未记录 parent dashboard 组件实际位于 `modules/parent/components/` 的跨模块布局。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### P0 问题(严重)
|
||||
|
||||
#### P0-1:`filterTodaySchedule` 仍使用 `as T[]` 类型断言
|
||||
|
||||
- **文件**:`src/modules/dashboard/lib/dashboard-utils.ts`
|
||||
- **行号**:120
|
||||
- **问题**:v3 审计(P1-8)已识别此问题并改为泛型函数,但实现仍保留 `as T[]` 断言:
|
||||
```typescript
|
||||
return schedule
|
||||
.filter(...)
|
||||
.sort(...)
|
||||
.map((s) => ({ ... })) as T[] // ← 违反"禁止 as 断言"
|
||||
```
|
||||
- **违反规则**:项目规则「TypeScript 严格模式:禁止 `as` 断言(除类型收窄外)」
|
||||
- **后果**:类型系统被绕过,`map` 返回的对象结构若与 `T` 不匹配,编译器不会报错,潜在运行时错误
|
||||
- **修复方向**:移除 `as T[]`,让 `map` 返回类型自然推导;或将映射逻辑提取为泛型映射函数
|
||||
|
||||
### P1 问题(高)
|
||||
|
||||
#### P1-1:组件内嵌纯函数未抽取到 lib
|
||||
|
||||
- **文件**:
|
||||
- `teacher-schedule.tsx` 行 24-36:`getStatus(start, end)` 计算课程状态
|
||||
- `student-upcoming-assignments-card.tsx` 行 18:`timeToMinutes(t)`
|
||||
- `student-upcoming-assignments-card.tsx` 行 30-40:`getDueUrgency(dueAt)`
|
||||
- `student-upcoming-assignments-card.tsx` 行 18-28:`getActionLabelKey(status)` / `getActionVariant(status)`
|
||||
- `student-today-schedule-card.tsx` 行 17-20:`timeToMinutes(t)`
|
||||
- **问题**:5 个纯函数散落在 3 个组件文件中,无法被单测覆盖,且 `timeToMinutes` 在两处重复定义
|
||||
- **违反规则**:项目规则「可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离」
|
||||
- **后果**:单测覆盖率无法提升;`timeToMinutes` 重复定义易产生不一致
|
||||
- **修复方向**:全部迁移到 `lib/dashboard-utils.ts`,导出供组件调用,补充单测
|
||||
|
||||
#### P1-2:`teacherName` 硬编码英文 fallback
|
||||
|
||||
- **文件**:`src/modules/dashboard/actions.ts`
|
||||
- **行号**:85
|
||||
- **问题**:`teacherName: teacherProfile?.name ?? "Teacher"` — 当教师名称为空时 fallback 为硬编码英文 "Teacher",英文/中文用户都会看到英文
|
||||
- **违反规则**:项目规则「所有用户可见文本必须适配 i18n」
|
||||
- **后果**:中文用户在教师名称缺失时看到英文 "Teacher",i18n 不一致
|
||||
- **修复方向**:fallback 改为空字符串 `""`,由前端组件用 `t("title.teacher")` 处理空值
|
||||
|
||||
#### P1-3:`teacher-schedule.tsx` 本地重复定义类型
|
||||
|
||||
- **文件**:`src/modules/dashboard/components/teacher-dashboard/teacher-schedule.tsx`
|
||||
- **行号**:10-18
|
||||
- **问题**:本地定义 `TeacherTodayScheduleItem` 类型,与 `types.ts` 中的同名类型结构完全相同,重复定义
|
||||
- **违反规则**:项目规则「避免代码重复」
|
||||
- **后果**:类型变更需同步两处,易产生不一致
|
||||
- **修复方向**:从 `types.ts` 导入,删除本地定义
|
||||
|
||||
#### P1-4:parent dashboard 组件位于 parent 模块而非 dashboard 模块
|
||||
|
||||
- **文件**:`src/modules/parent/components/parent-dashboard.tsx`
|
||||
- **问题**:`ParentDashboard` 组件位于 parent 模块,但由 `dashboard/actions.getParentDashboardAction` 提供数据,且 `parent/dashboard/page.tsx` 同时导入两个模块的组件。架构图标注为"架构决策:保留在 parent 模块以避免移动文件破坏其他 import",但这造成模块边界模糊
|
||||
- **违反规则**:项目规则「该模块必须作为独立功能单元」
|
||||
- **后果**:dashboard 模块不完整,parent 仪表盘的 UI 逻辑分散在两个模块
|
||||
- **修复方向**:将 `ParentDashboard` 组件迁移到 `modules/dashboard/components/parent-dashboard/`,parent 模块仅保留数据访问
|
||||
|
||||
### P2 问题(中)
|
||||
|
||||
#### P2-1:无数据服务接口抽象
|
||||
|
||||
- **文件**:`src/modules/dashboard/actions.ts`、`data-access.ts`
|
||||
- **问题**:dashboard 模块直接 import 其他 6 个模块的 data-access 函数(classes/homework/users/parent/textbooks/questions/exams),无 TypeScript 接口抽象。组件层无法 mock 数据依赖,单测必须 mock 整个模块
|
||||
- **违反规则**:项目规则「完全解耦:通过定义 TypeScript 接口抽象数据依赖」
|
||||
- **后果**:模块耦合度高,难以独立测试,新增角色需修改 actions.ts
|
||||
- **修复方向**:定义 `DashboardService` 接口,为每个角色提供实现类,通过 React Context 注入
|
||||
|
||||
#### P2-2:无配置驱动的 Widget 渲染
|
||||
|
||||
- **文件**:`admin-dashboard.tsx`、`teacher-dashboard-view.tsx`、`student-dashboard-view.tsx`
|
||||
- **问题**:每个角色的仪表盘视图硬编码渲染哪些 Widget(如 admin 渲染 StatsBar + QuickActions + TrendCharts + 3 Cards + RecentUsersTable)。新增角色或调整 Widget 需修改视图组件代码
|
||||
- **违反规则**:项目规则「可扩展性:采用配置驱动设计」
|
||||
- **后果**:扩展性差,4 个角色视图代码结构相似但无法复用
|
||||
- **修复方向**:定义 `DashboardWidgetConfig` 类型,通过配置决定渲染哪些 Widget 及其布局
|
||||
|
||||
#### P2-3:无监控埋点接口
|
||||
|
||||
- **文件**:整个模块
|
||||
- **问题**:无任何用户行为埋点(如 Widget 点击、页面停留、空状态触发等),无法度量仪表盘使用情况
|
||||
- **违反规则**:项目规则「监控:方案中预留关键操作埋点接口」
|
||||
- **后果**:无法度量仪表盘使用情况,无法指导优化
|
||||
- **修复方向**:定义 `DashboardAnalytics` 接口,在关键交互点调用(Widget 点击、空状态触发、错误重试)
|
||||
|
||||
#### P2-4:admin dashboard `userGrowth` 和 `homeworkTrend` 仍为占位空数组
|
||||
|
||||
- **文件**:`src/modules/dashboard/data-access.ts`
|
||||
- **行号**:46-47
|
||||
- **问题**:v3 已为 `UserGrowthChart` 添加空状态,但数据源仍硬编码 `userGrowth: []` 和 `homeworkTrend: []`,趋势图表永远显示空状态
|
||||
- **违反规则**:无直接违反,但影响用户体验
|
||||
- **后果**:管理员无法看到用户增长和作业提交趋势
|
||||
- **修复方向**:实现真实统计查询,或在 data-access 层添加 TODO 注释标记后续实现
|
||||
|
||||
#### P2-5:4 个角色 StatCard 使用模式不一致
|
||||
|
||||
- **文件**:
|
||||
- `admin-sections.tsx`:`StatCard` 直接传 `value`(number)
|
||||
- `teacher-stats.tsx`:`StatCard` 传 `value={String(count)}` + `color` + `highlight`
|
||||
- `student-stats-grid.tsx`:`StatCard` 传 `value={String(count)}` + `color` + `valueClassName` + 条件颜色
|
||||
- **问题**:3 个角色的 StatCard 调用模式不一致,admin 不传 color,teacher 传 color,student 传 color + valueClassName
|
||||
- **违反规则**:项目规则「最大化复用:识别四个角色共用的 UI 块」
|
||||
- **后果**:视觉不一致,维护成本高
|
||||
- **修复方向**:统一 StatCard 调用模式,通过配置驱动颜色和样式
|
||||
|
||||
### P3 问题(低)
|
||||
|
||||
#### P3-1:无完整键盘导航支持
|
||||
|
||||
- **问题**:虽有 `aria-label` 属性,但 Widget 之间无 `tabindex` 管理,键盘用户无法按逻辑顺序遍历 Widget
|
||||
- **修复方向**:为 Widget 容器添加 `role="region"` + `aria-label`,管理 `tabindex`
|
||||
|
||||
#### P3-2:`AdminTrendCharts` 硬编码 `data={[]}`
|
||||
|
||||
- **文件**:`admin-sections.tsx` 行 144、152
|
||||
- **问题**:`UserGrowthChart` 调用时传 `data={[]}`,与 P2-4 相关
|
||||
- **修复方向**:从 `streams` 获取真实趋势数据
|
||||
|
||||
#### P3-3:`teacher-todo-card.tsx` 排序逻辑仍可优化
|
||||
|
||||
- **文件**:`teacher-todo-card.tsx` 行 52-56
|
||||
- **问题**:v3 已优化排序逻辑,但仍使用 `if (a.variant === "urgent") return -1` 模式,可进一步用优先级映射
|
||||
- **修复方向**:定义 `VARIANT_PRIORITY` 映射,用数值比较
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
### 3.1 与优秀 K12 产品的差距
|
||||
|
||||
| 维度 | 我们当前 | 钉钉教育/智学网/ClassIn | 差距影响 |
|
||||
|------|---------|----------------------|---------|
|
||||
| **数据联动** | 各 Widget 独立展示,无联动 | 点击统计卡片可下钻到详情页 | 管理员无法快速从概览定位问题 |
|
||||
| **个性化配置** | 固定布局,用户无法自定义 | 支持拖拽 Widget、隐藏/显示 | 不同用户关注点不同,固定布局降低效率 |
|
||||
| **实时更新** | 静态数据,需刷新页面 | WebSocket 实时推送待办数 | 待办数不实时,影响响应速度 |
|
||||
| **多角色切换** | 通过权限路由到不同仪表盘 | 支持角色快速切换(如班主任+教师) | 多角色用户需退出重新登录 |
|
||||
| **数据导出** | 无导出功能 | 支持导出 PDF/Excel | 管理员无法离线分析 |
|
||||
| **通知集成** | 无通知集成 | 仪表盘集成待办通知 | 用户需切换页面查看通知 |
|
||||
| **移动端适配** | 基本响应式,但 Widget 布局未优化 | 移动端优先设计,卡片堆叠 | 移动端体验不佳 |
|
||||
|
||||
### 3.2 缺失的关键功能
|
||||
|
||||
1. **Widget 下钻导航**:统计卡片点击应跳转到对应详情页(部分已实现,但不完整)
|
||||
2. **时间范围筛选**:admin 无法切换"今日/本周/本月"数据范围
|
||||
3. **数据对比**:无法对比不同时间段数据(如本周 vs 上周)
|
||||
4. **自定义仪表盘**:用户无法选择显示哪些 Widget
|
||||
5. **通知中心集成**:仪表盘未集成通知下拉
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(立即修复)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P0-1 | `filterTodaySchedule` 的 `as T[]` 断言 | 移除断言,改用类型守卫或泛型映射 |
|
||||
|
||||
### P1(高优先级)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P1-1 | 组件内嵌纯函数未抽取 | 迁移 5 个纯函数到 `lib/dashboard-utils.ts`,补充单测 |
|
||||
| P1-2 | `teacherName` 硬编码 fallback | 改为空字符串,前端用 i18n 处理 |
|
||||
| P1-3 | `teacher-schedule.tsx` 本地类型重复 | 从 `types.ts` 导入 |
|
||||
| P1-4 | parent dashboard 组件跨模块 | 迁移到 `modules/dashboard/components/parent-dashboard/` |
|
||||
|
||||
### P2(中优先级 - 架构改进)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P2-1 | 无数据服务接口抽象 | 定义 `DashboardService` 接口 + 角色实现 + Context 注入 |
|
||||
| P2-2 | 无配置驱动 Widget 渲染 | 定义 `DashboardWidgetConfig`,配置驱动渲染 |
|
||||
| P2-3 | 无监控埋点接口 | 定义 `DashboardAnalytics` 接口,预留埋点 |
|
||||
| P2-4 | admin 趋势数据占位 | 添加 TODO 注释,标记后续实现 |
|
||||
| P2-5 | StatCard 使用模式不一致 | 统一调用模式 |
|
||||
|
||||
### P3(低优先级 - 长期优化)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P3-1 | 无完整键盘导航 | 添加 `role="region"` + `tabindex` |
|
||||
| P3-2 | AdminTrendCharts 硬编码空数据 | 从 streams 获取真实数据 |
|
||||
| P3-3 | TeacherTodoCard 排序优化 | 用优先级映射 |
|
||||
|
||||
### 中长期计划(不在本次实施范围)
|
||||
|
||||
| 编号 | 问题 | 改进方向 | 阶段 |
|
||||
|------|------|---------|------|
|
||||
| L1 | Widget 下钻导航 | 统计卡片点击跳转详情页 | 第二阶段 |
|
||||
| L2 | 时间范围筛选 | admin 仪表盘添加时间选择器 | 第二阶段 |
|
||||
| L3 | 数据对比 | 添加"本周 vs 上周"对比卡片 | 第三阶段 |
|
||||
| L4 | 自定义仪表盘 | 用户可选择显示哪些 Widget | 第三阶段 |
|
||||
| L5 | 通知中心集成 | 仪表盘集成通知下拉 | 第二阶段 |
|
||||
| L6 | 实时更新 | WebSocket 推送待办数 | 第三阶段 |
|
||||
| L7 | 数据导出 | 支持 PDF/Excel 导出 | 第三阶段 |
|
||||
| L8 | 移动端优化 | Widget 移动端优先布局 | 第二阶段 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
### 需要补充/修改的节点
|
||||
|
||||
1. **`streams.ts`**:架构图未记录 `getAdminDashboardStreams` 函数,需在 004 的 dashboard 模块章节和 005 的 `modules.dashboard.exports` 中添加
|
||||
2. **parent dashboard 组件位置**:架构图需注明 `ParentDashboard` 组件实际位于 `modules/parent/components/`,由 dashboard actions 提供数据
|
||||
3. **新增 `DashboardService` 接口**(本次实施后):在 005 的 `modules.dashboard` 中添加 `services` 节点
|
||||
4. **新增 `DashboardWidgetConfig` 类型**(本次实施后):在 005 的 `modules.dashboard.exports.types` 中添加
|
||||
5. **新增 `DashboardAnalytics` 接口**(本次实施后):在 005 的 `modules.dashboard.exports.services` 中添加
|
||||
|
||||
---
|
||||
|
||||
## 六、本次实施计划
|
||||
|
||||
### 实施范围
|
||||
|
||||
本次完整实施 P0 + P1 + P2 + P3 + 中长期计划 L1-L8(用户明确要求"包括中长期计划也要完整实施")。
|
||||
|
||||
### 实施步骤与完成状态
|
||||
|
||||
| 步骤 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| P0-1:修复 `filterTodaySchedule` 的 `as T[]` 断言 | ✅ 已完成 | 移除断言,改为非泛型函数 |
|
||||
| P1-1:抽取 5 个纯函数到 `lib/dashboard-utils.ts` | ✅ 已完成 | timeToMinutes/getScheduleStatus/getDueUrgency/getActionLabelKey/getActionVariant |
|
||||
| P1-2:修复 `teacherName` 硬编码 fallback | ✅ 已完成 | 改为空字符串,前端处理 |
|
||||
| P1-3:修复 `teacher-schedule.tsx` 本地类型重复 | ✅ 已完成 | 从 types.ts 导入 |
|
||||
| P1-4:迁移 parent dashboard 组件到 dashboard 模块 | ✅ 已完成 | 迁移至 components/parent-dashboard/,使用 slots 组合 |
|
||||
| P2-1:定义 `DashboardService` 接口 + Context 注入 | ✅ 已完成 | services/dashboard-service.tsx |
|
||||
| P2-2:定义 `DashboardWidgetConfig` 配置驱动渲染 | ✅ 已完成 | config/widget-configs.ts |
|
||||
| P2-3:定义 `DashboardAnalytics` 监控埋点接口 | ✅ 已完成 | services/dashboard-service.tsx |
|
||||
| P2-4:admin 趋势数据占位 TODO | ✅ 已完成 | data-access.ts 添加 TODO 注释 |
|
||||
| P2-5:4 个角色 StatCard 使用模式统一 | ✅ 已完成 | 统一 color + valueClassName="tabular-nums" |
|
||||
| P3-1:完整键盘导航支持 | ✅ 已完成 | DashboardSection 新增 ariaLabel prop + role="region" + tabIndex |
|
||||
| P3-2:AdminTrendCharts 硬编码空数据 TODO | ✅ 已完成 | 添加 TODO 注释 |
|
||||
| P3-3:TeacherTodoCard 排序优化 | ✅ 已完成 | VARIANT_PRIORITY 数值映射 |
|
||||
| L1:Widget 下钻导航 | ✅ 已实施 | Admin StatCard 添加 href,ContentRow 支持可选 href |
|
||||
| L2:时间范围筛选 | ✅ 已实施 | DashboardTimeRangeFilter 组件 + URL search param 持久化 |
|
||||
| L3:数据对比 | ✅ 已实施 | ComparisonBadge 组件 + computeComparison 纯函数 |
|
||||
| L4:自定义仪表盘 | ✅ 已实施 | useDashboardPreferences Hook + localStorage 持久化 |
|
||||
| L5:通知中心集成 | ✅ 已实施 | DashboardNotificationWidget 组件 |
|
||||
| L6:实时更新 | ✅ 已实施 | useDashboardRealtime Hook(SSE + 指数退避重连) |
|
||||
| L7:数据导出 | ✅ 已实施 | lib/export-utils.ts(CSV 导出 + 浏览器下载) |
|
||||
| L8:移动端优化 | ✅ 已实施 | DashboardResponsiveLayout / MobileSwipeContainer / DesktopGrid |
|
||||
| 同步架构文档 004 和 005 | ✅ 已完成 | 所有新组件/函数/类型已记录 |
|
||||
| 验证:tsc + lint 零错误 | ✅ 已完成 | Dashboard 源码零错误(仅预存测试文件 screen 导入错误),ESLint 零错误 |
|
||||
|
||||
### 新增文件清单
|
||||
|
||||
| 文件 | 类型 | 职责 |
|
||||
|------|------|------|
|
||||
| `services/dashboard-service.tsx` | Service | DashboardService 接口 + DashboardAnalytics 接口 + Context Provider |
|
||||
| `config/widget-configs.ts` | Config | 4 个角色 Widget 布局配置 |
|
||||
| `hooks/use-dashboard-preferences.ts` | Hook | 自定义仪表盘偏好(L4) |
|
||||
| `hooks/use-dashboard-realtime.ts` | Hook | SSE 实时更新(L6) |
|
||||
| `lib/export-utils.ts` | Lib | CSV 导出工具(L7) |
|
||||
| `components/parent-dashboard/parent-dashboard.tsx` | Component | 家长仪表盘视图(P1-4 迁移) |
|
||||
| `components/dashboard-time-range-filter.tsx` | Component | 时间范围筛选器(L2) |
|
||||
| `components/comparison-badge.tsx` | Component | 数据对比徽章(L3) |
|
||||
| `components/dashboard-notification-widget.tsx` | Component | 通知中心 Widget(L5) |
|
||||
| `components/dashboard-responsive-layout.tsx` | Component | 移动端响应式布局(L8) |
|
||||
@@ -0,0 +1,206 @@
|
||||
# 数据库访问层重构专项 - 审计框架 v1
|
||||
|
||||
> 创建日期:2026-07-07
|
||||
> 目标:对全项目 86 个 `data-access*.ts` + ~30 个 `actions.ts` 进行深度审计,输出可执行的分级治理路线图
|
||||
> 推进路径:先审计后治理(用户已确认)
|
||||
> 执行方案:纯深读(用户已确认方案 B)
|
||||
|
||||
---
|
||||
|
||||
## 一、审计范围
|
||||
|
||||
### 1.1 文件范围
|
||||
|
||||
| 类型 | 路径模式 | 文件数(约) | 备注 |
|
||||
|---|---|---|---|
|
||||
| 数据访问层 | `src/modules/**/data-access*.ts` | 86 | 主审计对象 |
|
||||
| Server Actions | `src/modules/**/actions.ts` | ~30 | 辅查(权限校验、业务逻辑归属) |
|
||||
| 辅助文件 | `src/modules/**/schema.ts`、`types.ts` | 按需 | 仅当 data-access 引用时查看 |
|
||||
|
||||
### 1.2 排除范围
|
||||
|
||||
- `src/app/**`:仅在 A-05 规则(app 直访 DB)触发时反向查看
|
||||
- `src/shared/**`:仅在 A-07 规则(shared 反向依赖)触发时查看
|
||||
- 已有的模块级 audit 报告(`docs/architecture/audit/*-audit-report.md`):作为参考但不直接复用,因本次为横切关注点
|
||||
|
||||
### 1.3 模块分组(并行执行单元)
|
||||
|
||||
| 组 | 模块 | data-access 文件数 | sub-agent |
|
||||
|---|---|---|---|
|
||||
| **G1 核心教学 A** | lesson-preparation(12)+ questions + textbooks | ~16 | agent-1 |
|
||||
| **G2 核心教学 B** | exams + homework(7)+ grades(6)+ diagnostic + adaptive-practice(3) | ~21 | agent-2 |
|
||||
| **G3 教学管理** | classes(6)+ school + scheduling + attendance(3)+ course-plans + proctoring | ~16 | agent-3 |
|
||||
| **G4 用户与沟通** | users + messaging + notifications + parent + audit + auth + rbac(3) | ~12 | agent-4 |
|
||||
| **G5 扩展与设置** | elective(5)+ settings(5)+ dashboard + files + search + onboarding + ai + announcements + error-book(3) | ~21 | agent-5 |
|
||||
|
||||
---
|
||||
|
||||
## 二、审计维度与检查规则
|
||||
|
||||
### 2.1 维度 1:模式标准化(Pattern Standardization)
|
||||
|
||||
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|
||||
|---|---|---|---|
|
||||
| P-01 | `import "server-only"` 文件头 | 每个文件首行 | 静态 |
|
||||
| P-02 | 类型导入使用 `import type` | 类型导入与值导入分离 | 静态 |
|
||||
| P-03 | 读函数是否走 `cacheFn` 包装(Raw + Wrapper 配对) | 全部覆盖 | 深读 |
|
||||
| P-04 | 函数返回类型显式标注 `Promise<T>` | 无隐式推断 | 深读 |
|
||||
| P-05 | 错误处理一致 | data-access 层用 throw,actions 层用 ActionState | 深读 |
|
||||
| P-06 | 分页参数命名统一 | `page`/`pageSize` 或 `limit`/`offset` 全局统一 | 深读 |
|
||||
| P-07 | 日期序列化走 helper | `serializeDate`/`toISODateString` | 深读 |
|
||||
| P-08 | 列表项映射走 `mapListItem` 模式 | 避免 inline mapping 重复 | 深读 |
|
||||
| P-09 | `as` 断言出现次数 | 0(除 unknown 收窄) | 静态 |
|
||||
| P-10 | `any` 出现次数 | 0 | 静态 |
|
||||
|
||||
### 2.2 维度 2:性能与查询优化(Performance)
|
||||
|
||||
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|
||||
|---|---|---|---|
|
||||
| F-01 | 循环内 SQL 调用(N+1) | 改批量查询 + Map 解析 | 深读 |
|
||||
| F-02 | `LIKE '%xxx%'` 全表扫描 | 改 FULLTEXT 或前缀匹配 | 深读 |
|
||||
| F-03 | SELECT * 未指定列 | 显式列枚举 | 深读 |
|
||||
| F-04 | JOIN 表数量 > 3 | 评估拆分或冗余字段 | 深读 |
|
||||
| F-05 | 大表查询无 LIMIT | 添加默认 LIMIT | 深读 |
|
||||
| F-06 | 重复查询同表/同条件 | 走 cacheFn 或合并查询 | 深读 |
|
||||
| F-07 | 缺失索引(高频 WHERE 字段) | 提示加索引 | 深读 |
|
||||
| F-08 | 跨模块多次调用 `getXxxNamesByIds` | 批量化 | 深读 |
|
||||
| F-09 | 事务范围过大(含网络调用) | 收紧事务 | 深读 |
|
||||
| F-10 | `count()` 全表统计无过滤 | 添加过滤条件 | 深读 |
|
||||
|
||||
### 2.3 维度 3:架构违规治理(Architecture)
|
||||
|
||||
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|
||||
|---|---|---|---|
|
||||
| A-01 | data-access 含 `requirePermission` 调用 | 移至 actions | 静态 |
|
||||
| A-02 | data-access 含业务逻辑(条件分支、状态机) | 移至 actions 或 lib | 深读 |
|
||||
| A-03 | data-access 含 `"use server"` 标记 | 移至 actions | 静态 |
|
||||
| A-04 | data-access 含 `revalidatePath` 调用 | 移至 actions | 静态 |
|
||||
| A-05 | app/ 直接 import `@/shared/db` | 违规,改走 data-access | 静态 |
|
||||
| A-06 | modules 间直接 import 对方 `@/shared/db/schema` 表 | 改走对方 data-access | 静态 |
|
||||
| A-07 | shared/ 反向 import `@/auth`/`@/proxy`/`modules/*` | 违规 | 静态 |
|
||||
| A-08 | actions.ts 漏调 `requirePermission` | 补齐 | 深读 |
|
||||
| A-09 | actions.ts 含直接 DB 查询 | 移至 data-access | 深读 |
|
||||
| A-10 | data-access 含 `console.log` 调试代码 | 删除 | 静态 |
|
||||
|
||||
### 2.4 维度 4:结构与可维护性(Structure)
|
||||
|
||||
| 规则 ID | 检查项 | 期望状态 | 检测方式 |
|
||||
|---|---|---|---|
|
||||
| S-01 | 文件行数 > 800 行警告,> 1000 行必须拆分 | 拆分 | 静态 |
|
||||
| S-02 | 单文件导出函数数 > 20 | 警告,考虑拆分 | 静态 |
|
||||
| S-03 | 重复 helper(多模块各自实现 serializeDate/buildScopeFilter 等) | 提取到 shared/lib | 深读 |
|
||||
| S-04 | 过细拆分(同模块 ≥ 5 个子文件且单文件 < 100 行) | 评估合并 | 静态 |
|
||||
| S-05 | 未使用导出(dead code) | 删除 | 深读 |
|
||||
| S-06 | 公共导出函数缺 JSDoc | 补齐 | 深读 |
|
||||
| S-07 | 跨模块重复查询逻辑 | 提取共享 data-access | 深读 |
|
||||
| S-08 | 模块内 data-access 与 actions 职责混淆 | 重新分层 | 深读 |
|
||||
|
||||
---
|
||||
|
||||
## 三、严重性分级
|
||||
|
||||
| 级别 | 含义 | 示例 | 治理窗口 |
|
||||
|---|---|---|---|
|
||||
| **P0 Critical** | 架构硬违规、安全漏洞、必定性能问题 | app 直访 DB、跨模块 schema 直查、actions 漏权限、N+1 循环 SQL | 立即 |
|
||||
| **P1 High** | 显著性能/可维护性问题 | 超长文件(>1000 行)、缺 cacheFn 的热路径读函数、LIKE 全表扫描 | Phase 1 |
|
||||
| **P2 Medium** | 模式偏差、可优化 | 错误处理不一致、缺 JSDoc、重复 helper、分页命名不统一 | Phase 2 |
|
||||
| **P3 Low** | 风格问题、可选优化 | 单行格式、import 顺序、注释措辞 | Phase 3 |
|
||||
|
||||
---
|
||||
|
||||
## 四、执行流程
|
||||
|
||||
### 4.1 阶段 A:sub-agent 分组深读(并行)
|
||||
|
||||
每个 sub-agent 接收:
|
||||
- 该组所有 `data-access*.ts` + 同模块 `actions.ts` 文件清单
|
||||
- 完整规则表(4 维度 × 38 条规则)
|
||||
- 统一输出格式(见 4.3)
|
||||
|
||||
每个 sub-agent 执行:
|
||||
1. 完整读取每个文件(不使用 limit/offset)
|
||||
2. 按规则表逐条检测
|
||||
3. 命中即记录到问题清单
|
||||
4. 对每个问题给出修复建议与预估工作量
|
||||
|
||||
### 4.2 阶段 B:主 agent 汇总
|
||||
|
||||
- 收集 5 个 sub-agent 的结构化输出
|
||||
- 去重(同一问题被多 agent 命中时合并)
|
||||
- 跨模块统计(如重复 helper 在多少模块出现)
|
||||
- 生成优先级矩阵
|
||||
- 编写治理路线图
|
||||
|
||||
### 4.3 sub-agent 输出格式
|
||||
|
||||
每个 sub-agent 产出 JSON 数组,每条问题:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "G1-001",
|
||||
"file": "src/modules/lesson-preparation/data-access.ts",
|
||||
"lines": "L123-L145",
|
||||
"ruleId": "F-01",
|
||||
"severity": "P0",
|
||||
"dimension": "performance",
|
||||
"title": "循环内调用 getClassNamesByIds",
|
||||
"description": "在 for 循环内对每个 classId 单独查询 className,应改为批量查询后用 Map 解析",
|
||||
"recommendation": "提取 classIds 数组,一次调用 getClassNamesByIds(classIds),循环内改为 map.get(classId)",
|
||||
"effort": "S (≤30 分钟)"
|
||||
}
|
||||
```
|
||||
|
||||
工作量分级:
|
||||
- **XS**:≤ 15 分钟(如删除 console.log、补 import type)
|
||||
- **S**:≤ 30 分钟(如替换 as 断言为类型守卫)
|
||||
- **M**:≤ 2 小时(如 N+1 改批量、提取 helper)
|
||||
- **L**:≤ 1 天(如拆分超长文件、跨模块重构)
|
||||
- **XL**:> 1 天(如架构层重构)
|
||||
|
||||
---
|
||||
|
||||
## 五、报告输出
|
||||
|
||||
### 5.1 主报告
|
||||
|
||||
文件:`docs/architecture/audit/data-access-audit-v1.md`
|
||||
|
||||
结构:
|
||||
1. **执行摘要**:总文件数、问题总数、P0/P1/P2/P3 分布、模块热度图
|
||||
2. **量化指标仪表盘**:cacheFn 覆盖率、平均行数、`as` 断言数、违规 import 数等
|
||||
3. **按维度分组的问题清单**:每条含 文件:行号、规则 ID、严重性、现状描述、修复建议、预估工作量
|
||||
4. **按模块分组的问题清单**:每个模块的累计问题数与 Top 问题
|
||||
5. **P0-P3 优先级矩阵**:四象限图(影响 × 紧迫度)
|
||||
6. **分阶段治理路线图**:Phase 1 (P0) → Phase 2 (P1) → Phase 3 (P2) → Phase 4 (P3)
|
||||
7. **附录**:完整规则表、sub-agent 原始输出索引
|
||||
|
||||
### 5.2 结构化数据
|
||||
|
||||
文件:`docs/architecture/audit/data-access-audit-v1-data.json`
|
||||
|
||||
字段:`issues[]`、`metrics{}`、`moduleSummary{}`、`roadmap{}`
|
||||
|
||||
### 5.3 速查手册同步
|
||||
|
||||
发现的新模式问题需追加到 `docs/troubleshooting/known-issues.md`(速查手册格式)。
|
||||
|
||||
---
|
||||
|
||||
## 六、质量约束
|
||||
|
||||
- **零误报**:每条问题必须给出文件:行号 + 代码证据,避免臆测
|
||||
- **零遗漏**:86 个 data-access 文件必须全部深读,不得抽样
|
||||
- **可执行**:每条修复建议必须具体到代码示例或操作步骤
|
||||
- **不修改代码**:审计阶段只产出报告,不做任何源码修改
|
||||
- **架构同步**:审计过程中发现的架构图遗漏(004/005 文档)记录到报告附录,治理阶段统一补图
|
||||
|
||||
---
|
||||
|
||||
## 七、后续衔接
|
||||
|
||||
审计报告 v1 完成后:
|
||||
|
||||
1. **用户审查报告**:确认问题清单与优先级
|
||||
2. **制定治理路线图**:基于 P0-P3 分级,输出 `data-access-refactor-roadmap-v1.md`
|
||||
3. **分阶段执行治理**:每阶段完成后运行 `npm run lint` + `npx tsc --noEmit` 验证
|
||||
4. **同步架构文档**:每阶段完成后同步 004/005 文档与 known-issues.md
|
||||
171
docs/architecture/audit/archive/data-access-audit-v1-data.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"version": "v1",
|
||||
"createdAt": "2026-07-07",
|
||||
"scope": {
|
||||
"dataAccessFiles": 86,
|
||||
"actionsFilesAudited": 15,
|
||||
"totalFilesAudited": 101
|
||||
},
|
||||
"summary": {
|
||||
"totalIssues": 230,
|
||||
"bySeverity": {
|
||||
"P0": 17,
|
||||
"P1": 48,
|
||||
"P2": 105,
|
||||
"P3": 60
|
||||
},
|
||||
"byDimension": {
|
||||
"pattern": 54,
|
||||
"performance": 71,
|
||||
"architecture": 58,
|
||||
"structure": 47
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"serverOnlyMissing": 2,
|
||||
"cacheFnMissingEstimated": 60,
|
||||
"filesOver800Lines": 3,
|
||||
"filesOver1000Lines": 1,
|
||||
"filesOver20Exports": 5,
|
||||
"asAssertionsNonExempt": 2,
|
||||
"anyUsage": 0,
|
||||
"consoleErrorCount": 25,
|
||||
"nPlusOnePatterns": 11,
|
||||
"likeFullScanPatterns": 7,
|
||||
"selectStarCount": 35,
|
||||
"noLimitQueries": 18,
|
||||
"crossModuleSchemaAccess": 7,
|
||||
"businessLogicInDataAccess": 18,
|
||||
"unprotectedTransactions": 4,
|
||||
"actionsPermissionIssues": 4,
|
||||
"actionsDirectDB": 1
|
||||
},
|
||||
"moduleHeatmap": [
|
||||
{ "module": "messaging", "p0": 1, "p1": 4, "total": 5, "risk": "critical" },
|
||||
{ "module": "classes", "p0": 1, "p1": 4, "total": 14, "risk": "critical" },
|
||||
{ "module": "school", "p0": 0, "p1": 5, "total": 8, "risk": "critical" },
|
||||
{ "module": "lesson-preparation", "p0": 1, "p1": 3, "total": 28, "risk": "high" },
|
||||
{ "module": "scheduling", "p0": 1, "p1": 2, "total": 6, "risk": "high" },
|
||||
{ "module": "adaptive-practice", "p0": 1, "p1": 2, "total": 4, "risk": "high" },
|
||||
{ "module": "elective", "p0": 0, "p1": 3, "total": 7, "risk": "high" },
|
||||
{ "module": "textbooks", "p0": 2, "p1": 1, "total": 11, "risk": "high" },
|
||||
{ "module": "onboarding", "p0": 2, "p1": 0, "total": 2, "risk": "medium" },
|
||||
{ "module": "grades", "p0": 0, "p1": 2, "total": 4, "risk": "medium" },
|
||||
{ "module": "questions", "p0": 1, "p1": 1, "total": 11, "risk": "medium" },
|
||||
{ "module": "audit", "p0": 1, "p1": 1, "total": 3, "risk": "medium" },
|
||||
{ "module": "parent", "p0": 1, "p1": 0, "total": 1, "risk": "medium" },
|
||||
{ "module": "attendance", "p0": 0, "p1": 1, "total": 9, "risk": "medium" },
|
||||
{ "module": "files", "p0": 0, "p1": 4, "total": 13, "risk": "medium" },
|
||||
{ "module": "exams", "p0": 1, "p1": 0, "total": 1, "risk": "low" },
|
||||
{ "module": "course-plans", "p0": 1, "p1": 0, "total": 5, "risk": "low" },
|
||||
{ "module": "homework", "p0": 0, "p1": 1, "total": 2, "risk": "low" },
|
||||
{ "module": "diagnostic", "p0": 0, "p1": 1, "total": 2, "risk": "low" }
|
||||
],
|
||||
"p0Issues": [
|
||||
{ "id": "G4-003", "file": "src/modules/audit/actions.ts", "lines": "L192-225", "ruleId": "A-08", "title": "purgeAuditLogsAction 用读权限执行物理删除", "category": "security" },
|
||||
{ "id": "G4-002", "file": "src/modules/parent/", "lines": "—", "ruleId": "A-08", "title": "parent 模块缺失 actions.ts,3 页面直访 data-access", "category": "security" },
|
||||
{ "id": "G2-001", "file": "src/modules/exams/data-access.ts", "lines": "L1", "ruleId": "P-01", "title": "缺 import server-only", "category": "security" },
|
||||
{ "id": "G5-001", "file": "src/modules/onboarding/data-access.ts", "lines": "L1", "ruleId": "P-01", "title": "缺 import server-only", "category": "security" },
|
||||
{ "id": "G1-001", "file": "src/modules/textbooks/data-access-graph.ts", "lines": "L7-121", "ruleId": "A-06", "title": "直查 questions + diagnostic 模块表", "category": "architecture" },
|
||||
{ "id": "G3-002", "file": "src/modules/scheduling/data-access.ts", "lines": "L8-17", "ruleId": "A-06", "title": "直查 classes/users/subjects 三模块表", "category": "architecture" },
|
||||
{ "id": "G3-003", "file": "src/modules/scheduling/data-access-class-schedule.ts", "lines": "L28-158", "ruleId": "A-02", "title": "data-access 含校验+状态机业务逻辑", "category": "architecture" },
|
||||
{ "id": "G5-002", "file": "src/modules/onboarding/actions.ts", "lines": "L15-76", "ruleId": "A-09", "title": "actions 直查 DB", "category": "architecture" },
|
||||
{ "id": "G4-001", "file": "src/modules/messaging/data-access.ts", "lines": "L1-1089", "ruleId": "S-01", "title": "1089 行超 1000 硬限", "category": "structure" },
|
||||
{ "id": "G1-002", "file": "src/modules/questions/data-access.ts", "lines": "L294-315", "ruleId": "F-01", "title": "deleteQuestionRecursive 递归 N+1", "category": "performance" },
|
||||
{ "id": "G1-003", "file": "src/modules/questions/data-access.ts", "lines": "L350-378", "ruleId": "F-01", "title": "deleteQuestionsBatch 循环 N+1", "category": "performance" },
|
||||
{ "id": "G1-004", "file": "src/modules/lesson-preparation/data-access-comments.ts", "lines": "L128-140", "ruleId": "F-01", "title": "deleteComment 递归 N+1", "category": "performance" },
|
||||
{ "id": "G1-005", "file": "src/modules/textbooks/data-access.ts", "lines": "L426-458", "ruleId": "F-01", "title": "reorderChapters 循环 UPDATE", "category": "performance" },
|
||||
{ "id": "G3-001", "file": "src/modules/classes/data-access.ts", "lines": "L17-313", "ruleId": "P-03", "title": "24+ 读函数未走 cacheFn", "category": "performance" },
|
||||
{ "id": "G3-004", "file": "src/modules/classes/data-access-teacher.ts", "lines": "L92-116", "ruleId": "F-01", "title": "getTeacherClassesRaw 2N+1", "category": "performance" },
|
||||
{ "id": "G3-005", "file": "src/modules/course-plans/data-access.ts", "lines": "L324-331", "ruleId": "F-01", "title": "reorderCoursePlanItems N+1 + 未包裹事务", "category": "performance" },
|
||||
{ "id": "G2-003", "file": "src/modules/adaptive-practice/data-access-analytics.ts", "lines": "L311-384", "ruleId": "F-01", "title": "getTeacherClassPracticeOverviewsRaw 2N+1", "category": "performance" }
|
||||
],
|
||||
"roadmap": {
|
||||
"phase0": {
|
||||
"name": "紧急安全修复",
|
||||
"priority": "immediate",
|
||||
"tasks": [
|
||||
{ "id": "G2-001", "effort": "XS", "action": "添加 import server-only 到 exams/data-access.ts" },
|
||||
{ "id": "G5-001", "effort": "XS", "action": "添加 import server-only 到 onboarding/data-access.ts" },
|
||||
{ "id": "G4-002", "effort": "M", "action": "新建 parent/actions.ts,3 页面改调 Action" },
|
||||
{ "id": "G4-003", "effort": "S", "action": "audit purge 权限点新增 + 替换" },
|
||||
{ "id": "G4-004", "effort": "S", "action": "audit retention 权限点替换" },
|
||||
{ "id": "G5-002", "effort": "S", "action": "onboarding/actions.ts 移除直查 DB" }
|
||||
]
|
||||
},
|
||||
"phase1": {
|
||||
"name": "P0 架构与性能修复",
|
||||
"priority": "high",
|
||||
"tasks": [
|
||||
{ "batch": "1.1", "ids": ["G1-001", "G3-002", "G1-031", "G1-032", "G1-033"], "effort": "L", "action": "跨模块 schema 直查治理" },
|
||||
{ "batch": "1.2", "ids": ["G4-001", "G4-005", "G4-006", "G4-007", "G4-008"], "effort": "L", "action": "messaging 拆分" },
|
||||
{ "batch": "1.3", "ids": ["G1-002", "G1-003", "G1-004", "G1-005", "G3-001", "G3-004", "G3-005", "G2-003"], "effort": "L", "action": "N+1 热路径修复" },
|
||||
{ "batch": "1.4", "ids": ["G3-003", "G3-024", "G3-025"], "effort": "M", "action": "scheduling 业务逻辑下移" },
|
||||
{ "batch": "1.5", "ids": ["G5-003", "G5-004"], "effort": "L", "action": "elective 业务逻辑拆分" }
|
||||
]
|
||||
},
|
||||
"phase2": {
|
||||
"name": "P1 性能与结构优化",
|
||||
"priority": "medium",
|
||||
"tasks": [
|
||||
{ "batch": "2.1", "ids": ["G1-006", "G1-007", "G1-008", "G1-009", "G3-017", "G4-010"], "effort": "L", "action": "LIKE 全表扫描治理" },
|
||||
{ "batch": "2.2", "ids": ["G3-007", "G2-005"], "effort": "M", "action": "超长文件拆分" },
|
||||
{ "batch": "2.3", "ids": ["G3-007", "G3-008", "G3-009", "G3-010", "G3-011"], "effort": "L", "action": "school 模块重构" },
|
||||
{ "batch": "2.4", "ids": ["G5-005", "G5-006", "G5-007"], "effort": "M", "action": "files 模块错误处理重构" },
|
||||
{ "batch": "2.5", "ids": ["G3-006", "G3-012", "G3-025", "G4-009"], "effort": "S", "action": "事务包裹修复" },
|
||||
{ "batch": "2.6", "ids": ["G1-011", "G1-012", "G1-013", "G1-050", "G1-051", "G1-052", "G1-053", "G3-031", "G3-032", "G3-041", "G3-044"], "effort": "M", "action": "无 LIMIT 查询保护" }
|
||||
]
|
||||
},
|
||||
"phase3": {
|
||||
"name": "P2 模式标准化",
|
||||
"priority": "low",
|
||||
"tasks": [
|
||||
{ "batch": "3.1", "ids": ["G1-021", "G1-022", "G1-023", "G1-024", "G3-001"], "effort": "M", "action": "cacheFn 全量补齐" },
|
||||
{ "batch": "3.2", "ids": ["G1-039-049", "G3-009", "G3-026-028", "G5-007"], "effort": "M", "action": "SELECT * 改显式列" },
|
||||
{ "batch": "3.3", "ids": ["G3-021", "G3-047", "G1-025"], "effort": "S", "action": "日期 helper 提取" },
|
||||
{ "batch": "3.4", "ids": ["G1-026", "G1-027", "G3-020", "G3-036"], "effort": "M", "action": "重复 helper 提取" },
|
||||
{ "batch": "3.5", "ids": ["G1-034-036", "G1-066", "G1-067", "G3-046"], "effort": "M", "action": "JSDoc 补齐" }
|
||||
]
|
||||
},
|
||||
"phase4": {
|
||||
"name": "P3 风格优化",
|
||||
"priority": "optional",
|
||||
"tasks": [
|
||||
{ "ids": ["G3-029", "G3-030"], "effort": "XS", "action": "as widening 断言改类型标注" },
|
||||
{ "ids": ["G1-060-065"], "effort": "XS", "action": "非空断言 ! 改类型守卫" },
|
||||
{ "ids": ["G3-048"], "effort": "S", "action": "export * 改显式 re-export" },
|
||||
{ "ids": ["G3-018"], "effort": "XS", "action": "死代码删除" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"crossModuleRecommendations": {
|
||||
"newSharedHelpers": [
|
||||
{ "name": "toISODateString", "path": "src/shared/lib/date-utils.ts", "replaces": ["attendance/serializeDate", "scheduling/serializeDate", "school/toIso", "course-plans/toIso"] },
|
||||
{ "name": "buildScopeFilter", "path": "src/shared/lib/scope-filter.ts", "replaces": ["attendance/buildScopeFilter", "grades/buildScopeFilter"] }
|
||||
],
|
||||
"newCrossModuleInterfaces": [
|
||||
{ "name": "getActiveStudentIdsByClassIds", "module": "classes", "callers": ["adaptive-practice", "attendance"] },
|
||||
{ "name": "getGradeNamesByIds", "module": "school", "callers": ["textbooks", "lesson-preparation"] },
|
||||
{ "name": "getQuestionCountByKpIds", "module": "questions", "callers": ["textbooks"] },
|
||||
{ "name": "getKpMasteryByTextbookId", "module": "diagnostic", "callers": ["textbooks"] }
|
||||
],
|
||||
"newPermissions": [
|
||||
{ "name": "AUDIT_LOG_PURGE", "description": "审计日志物理删除", "roles": ["admin"] },
|
||||
{ "name": "AUDIT_RETENTION_MANAGE", "description": "审计保留策略配置", "roles": ["admin"] }
|
||||
]
|
||||
},
|
||||
"architectureDocGaps": [
|
||||
"parent 模块缺失 actions.ts - 004 文档模块清单未标注",
|
||||
"onboarding/actions.ts 直查 DB - 005 文档 dependencyMatrix 需修正",
|
||||
"messaging/data-access.ts 拆分后 - 005 文档 modules.messaging.exports 需更新",
|
||||
"新增权限点 AUDIT_LOG_PURGE / AUDIT_RETENTION_MANAGE - 005 文档 permissions 需补记",
|
||||
"新增 shared/lib/date-utils.ts - 004/005 shared 模块清单需补记"
|
||||
],
|
||||
"sourceOutputs": [
|
||||
"docs/architecture/audit/g1-audit-output.json",
|
||||
"docs/architecture/audit/g2-data-access-audit.json",
|
||||
"docs/architecture/audit/g3-audit-output.json",
|
||||
"docs/architecture/audit/g4-audit-output.json",
|
||||
"docs/architecture/audit/g5-audit-output.json"
|
||||
]
|
||||
}
|
||||
525
docs/architecture/audit/archive/data-access-audit-v1.md
Normal file
@@ -0,0 +1,525 @@
|
||||
# 数据库访问层审计报告 v1
|
||||
|
||||
> 创建日期:2026-07-07
|
||||
> 审计范围:86 个 `data-access*.ts` + ~30 个 `actions.ts`
|
||||
> 审计方案:纯深读(5 个并行 sub-agent 全量扫描)
|
||||
> 框架依据:[data-access-audit-framework-v1.md](./data-access-audit-framework-v1.md)
|
||||
> 原始输出:[g1-audit-output.json](./g1-audit-output.json) · [g2-data-access-audit.json](./g2-data-access-audit.json) · [g3-audit-output.json](./g3-audit-output.json) · [g4-audit-output.json](./g4-audit-output.json) · [g5-audit-output.json](./g5-audit-output.json)
|
||||
|
||||
---
|
||||
|
||||
## 一、执行摘要
|
||||
|
||||
| 指标 | 数值 |
|
||||
|---|---|
|
||||
| 审计文件总数 | 101(86 data-access + 15 actions 辅查) |
|
||||
| 发现问题总数 | 230 |
|
||||
| P0 Critical | 17(7.4%) |
|
||||
| P1 High | 48(20.9%) |
|
||||
| P2 Medium | 105(45.6%) |
|
||||
| P3 Low | 60(26.1%) |
|
||||
|
||||
### 1.1 模块热度图(按 P0+P1 数量降序)
|
||||
|
||||
| 模块 | P0 | P1 | P0+P1 | 总计 | 风险等级 |
|
||||
|---|---|---|---|---|---|
|
||||
| messaging | 1 | 4 | 5 | 5 | 🔴 极高 |
|
||||
| classes | 1 | 4 | 5 | 14 | 🔴 极高 |
|
||||
| school | 0 | 5 | 5 | 8 | 🔴 极高 |
|
||||
| lesson-preparation | 1 | 3 | 4 | 28 | 🟠 高 |
|
||||
| scheduling | 1 | 2 | 3 | 6 | 🟠 高 |
|
||||
| adaptive-practice | 1 | 2 | 3 | 4 | 🟠 高 |
|
||||
| elective | 0 | 3 | 3 | 7 | 🟠 高 |
|
||||
| textbooks | 2 | 1 | 3 | 11 | 🟠 高 |
|
||||
| onboarding | 2 | 0 | 2 | 2 | 🟡 中 |
|
||||
| grades | 0 | 2 | 2 | 4 | 🟡 中 |
|
||||
| questions | 1 | 1 | 2 | 11 | 🟡 中 |
|
||||
| audit | 1 | 1 | 2 | 3 | 🟡 中 |
|
||||
| parent | 1 | 0 | 1 | 1 | 🟡 中 |
|
||||
| attendance | 0 | 1 | 1 | 9 | 🟡 中 |
|
||||
| files | 0 | 4 | 4 | 13 | 🟡 中 |
|
||||
| exams | 1 | 0 | 1 | 1 | 🟢 低 |
|
||||
| course-plans | 1 | 0 | 1 | 5 | 🟢 低 |
|
||||
| homework | 0 | 1 | 1 | 2 | 🟢 低 |
|
||||
| diagnostic | 0 | 1 | 1 | 2 | 🟢 低 |
|
||||
| 其他 (auth/rbac/notifications/dashboard/search/ai/announcements/error-book/proctoring/settings) | 0 | 0 | 0 | 0-3 | 🟢 低 |
|
||||
|
||||
### 1.2 维度分布
|
||||
|
||||
| 维度 | 问题数 | 占比 | P0 | P1 |
|
||||
|---|---|---|---|---|
|
||||
| 架构违规(A-*) | 58 | 25.2% | 6 | 18 |
|
||||
| 性能优化(F-*) | 71 | 30.9% | 7 | 14 |
|
||||
| 结构可维护性(S-*) | 47 | 20.4% | 2 | 11 |
|
||||
| 模式标准化(P-*) | 54 | 23.5% | 2 | 5 |
|
||||
|
||||
---
|
||||
|
||||
## 二、量化指标仪表盘
|
||||
|
||||
| 指标 | 数值 | 备注 |
|
||||
|---|---|---|
|
||||
| `import "server-only"` 缺失文件 | 2 | exams/data-access.ts、onboarding/data-access.ts |
|
||||
| cacheFn 未覆盖读函数(估算) | 60+ | 集中在 classes(24+)、questions(5)、textbooks(3)、lesson-preparation(4) |
|
||||
| 超长文件(>800 行) | 3 | messaging(1089,超硬限)、school(938)、grades-analytics(831) |
|
||||
| 单文件导出函数 > 20 | 4 | messaging(42+)、classes/data-access.ts(25+)、school(30+)、questions(28)、textbooks(35) |
|
||||
| `as` 断言(非豁免) | 2 | classes/data-access-admin.ts、classes/data-access-teacher.ts(DEFAULT_CLASS_SUBJECTS widening) |
|
||||
| `any` 使用 | 0 | 全部合规 |
|
||||
| `console.error` 调试代码 | 25+ | school(12)、files(12)、classes(3)、course-plans(2)、audit(9) |
|
||||
| N+1 循环 SQL(F-01) | 11 | 跨 4 组 |
|
||||
| `LIKE '%xxx%'` 全表扫描 | 7 | lesson-preparation(4)、questions(1)、textbooks(1)、classes(1)、messaging(1) |
|
||||
| SELECT * 未指定列 | 35+ | 跨 G1(16)、G3(11)、G5(8) |
|
||||
| 无 LIMIT 大表查询 | 18 | 集中在 lesson-preparation |
|
||||
| 跨模块直查 schema 表(A-06) | 7 | textbooks-graph(2)、lesson-preparation(2)、questions(1)、scheduling(1)、announcements(1) |
|
||||
| data-access 含业务逻辑(A-02) | 18 | 集中在 scheduling、messaging、elective、classes |
|
||||
| 未包裹事务的多步写(F-09) | 4 | course-plans、classes、auth、school |
|
||||
| actions 漏/错权限校验(A-08) | 4 | parent(缺失全部)、audit(purge 用读权限)、audit(retention 用读权限) |
|
||||
| actions 直查 DB(A-09) | 1 | onboarding |
|
||||
|
||||
---
|
||||
|
||||
## 三、P0 Critical 问题清单(17 条,必须立即治理)
|
||||
|
||||
### 3.1 安全漏洞类(4 条)
|
||||
|
||||
| ID | 文件 | 问题 | 修复 |
|
||||
|---|---|---|---|
|
||||
| G4-003 | audit/actions.ts L192-225 | `purgeAuditLogsAction` 用 `AUDIT_LOG_READ`(读权限)执行物理删除,权限提权漏洞 | 新增 `AUDIT_LOG_PURGE` 权限点 |
|
||||
| G4-002 | parent/ | 模块缺失 actions.ts,3 个 app 页面直接 import data-access,完全绕过 `requirePermission` | 新建 parent/actions.ts,3 个页面改调 Action |
|
||||
| G2-001 | exams/data-access.ts L1 | 缺 `import "server-only"`,DB 逻辑可能泄露到客户端 bundle | 首行添加 `import "server-only"` |
|
||||
| G5-001 | onboarding/data-access.ts L1 | 缺 `import "server-only"` | 首行添加 `import "server-only"` |
|
||||
|
||||
### 3.2 架构硬违规类(5 条)
|
||||
|
||||
| ID | 文件 | 问题 | 修复 |
|
||||
|---|---|---|---|
|
||||
| G1-001 | textbooks/data-access-graph.ts L7-121 | 直查 questions 模块 `questionsToKnowledgePoints` 表 + diagnostic 模块 `knowledgePointMastery` 表 | 改调对方 data-access 跨模块接口 |
|
||||
| G3-002 | scheduling/data-access.ts L8-17 | 直查 classes/users/subjects 三模块的 schema 表 | 改调 `getClassNamesByIds`/`getUserNamesByIds` 等 |
|
||||
| G3-003 | scheduling/data-access-class-schedule.ts | data-access 含时间校验、归属校验、状态机判断 | 校验逻辑移至 actions |
|
||||
| G5-002 | onboarding/actions.ts L15-76 | actions.ts 直接 `import { db }` 并查 `users` 表,违反三层架构 | data-access 新增 `getUserOnboardedAt`,actions 改调 |
|
||||
| G4-001 | messaging/data-access.ts L1-1089 | 单文件 1089 行超 1000 硬限,8 类职责混合 | 拆分为 7 个 data-access-*.ts |
|
||||
|
||||
### 3.3 必定性能问题类(8 条)
|
||||
|
||||
| ID | 文件 | 问题 | 修复 |
|
||||
|---|---|---|---|
|
||||
| G1-002 | questions/data-access.ts L294-315 | `deleteQuestionRecursive` 递归 N+1,每子题单独查询+删除 | 收集后代 ID + `inArray` 批量删除 |
|
||||
| G1-003 | questions/data-access.ts L350-378 | `deleteQuestionsBatch` 循环调用 `deleteQuestionRecursive` 产生 N×深度 查询 | 一次性收集所有后代 + 单次 `inArray` 删除 |
|
||||
| G1-004 | lesson-preparation/data-access-comments.ts L128-140 | `deleteComment` 递归 N+1 | 单次查询构建 parent→children Map + 批量删除 |
|
||||
| G1-005 | textbooks/data-access.ts L426-458 | `reorderChapters` 循环内逐条 UPDATE | `CASE WHEN` 批量更新 |
|
||||
| G3-001 | classes/data-access.ts L17-313 | 24+ 读函数全部未走 cacheFn,跨模块高频调用直连 DB | 补齐 Raw + Wrapper 配对 |
|
||||
| G3-004 | classes/data-access-teacher.ts L92-116 | `getTeacherClassesRaw` 循环内对每班发起 2 次子查询(2N+1) | 新增批量接口 |
|
||||
| G3-005 | course-plans/data-access.ts L324-331 | `reorderCoursePlanItems` 循环内 N 次 UPDATE 且未包裹事务 | 事务 + `CASE WHEN` 批量更新 |
|
||||
| G2-003 | adaptive-practice/data-access-analytics.ts L311-384 | `getTeacherClassPracticeOverviewsRaw` 对每班发起 2 条 SQL(2N+1) | 批量查询 + groupBy |
|
||||
|
||||
---
|
||||
|
||||
## 四、P1 High 问题清单(48 条,Phase 1 治理)
|
||||
|
||||
### 4.1 性能类(14 条)
|
||||
|
||||
| ID | 文件 | 规则 | 概要 |
|
||||
|---|---|---|---|
|
||||
| G1-006~009 | lesson-preparation/questions/textbooks | F-02 | 4 处 `LIKE '%xxx%'` 全表扫描(课案标题、JSON content、题目 content、教材 4 字段) |
|
||||
| G1-010 | lesson-preparation/data-access.ts L247-277 | F-04 | `getLessonPlansRaw` 5 表 LEFT JOIN |
|
||||
| G1-011~013 | lesson-preparation (3 处) | F-05 | 列表查询无 LIMIT(getLessonPlansRaw、getPendingReviewPlansRaw、getCalendarEventsRaw) |
|
||||
| G1-014~015 | lesson-preparation (2 处) | F-10 | 全表拉取后内存聚合统计 |
|
||||
| G1-016 | lesson-preparation/data-access-analytics.ts L168-184 | F-06 | 5 次串行 COUNT 查询同表 |
|
||||
| G1-017~018 | lesson-preparation (2 处) | F-01 | 拉全表后内存 filter |
|
||||
| G1-019 | textbooks/actions.ts L396-398 | F-08 | 循环调用 `getGradeNameById`(N 次 DB) |
|
||||
| G2-002 | grades/data-access-appeals.ts L122-151 | F-01 | `getPendingAppealsForReviewRaw` JS 层 filter 班级范围(潜在数据泄露) |
|
||||
| G2-004 | adaptive-practice/data-access-analytics.ts L320-325 | F-08 | 循环内跨模块调用 `getActiveStudentIdsByClassId` |
|
||||
| G3-006 | course-plans/data-access.ts L309-332 | F-09 | `reorderCoursePlanItems` 多次 UPDATE 未包裹事务 |
|
||||
| G3-012 | school/data-access.ts L803-822 | F-09 | `promoteGrades` 循环 UPDATE 未包裹事务 |
|
||||
| G3-017 | classes/data-access-students.ts L281-285 | F-02 | `LIKE '%xxx%'` 全表扫描 users.name/email |
|
||||
| G3-022 | attendance/data-access-correlation.ts L46-193 | F-01/A-02 | 148 行业务编排逻辑(含跨模块调用) |
|
||||
|
||||
### 4.2 架构类(11 条)
|
||||
|
||||
| ID | 文件 | 规则 | 概要 |
|
||||
|---|---|---|---|
|
||||
| G4-004 | audit/actions.ts L163-190 | A-08 | `saveAuditRetentionConfigAction` 用读权限执行写操作 |
|
||||
| G4-006~008 | messaging/data-access.ts (3 处) | A-02 | 状态机/防重复业务逻辑嵌入 data-access |
|
||||
| G3-007~008 | school/data-access.ts | S-01/S-02 | 938 行 + 30+ 导出函数 |
|
||||
| G3-010 | school/data-access.ts | A-10 | 12 处 `console.error` 吞异常 |
|
||||
| G3-011 | school/data-access.ts L246-408 | A-02 | 角色判断业务逻辑嵌入 data-access |
|
||||
| G3-024~025 | classes/data-access-teacher.ts L284-439 | A-02/F-09 | `enrollTeacherByInvitationCode` 155 行状态机 + 未包裹事务 |
|
||||
| G5-003 | elective/data-access-operations.ts | A-02 | 业务逻辑混淆(抽签算法、冲突检测、i18n 通知) |
|
||||
|
||||
### 4.3 结构类(5 条)
|
||||
|
||||
| ID | 文件 | 规则 | 概要 |
|
||||
|---|---|---|---|
|
||||
| G2-005 | grades/data-access-analytics.ts | S-01 | 831 行超 800 警告线 |
|
||||
| G5-004 | elective/data-access-operations.ts L222-304 | S-08 | DB 写入与抽签算法混淆 |
|
||||
| G5-005 | files/data-access.ts | A-10 | 12 处 `console.error` |
|
||||
| G5-006 | files/data-access.ts | P-05 | try-catch 吞错误返回 null/[]/false |
|
||||
| G4-009 | auth/data-access.ts | F-09 | `createUser` 两次 INSERT 无事务包裹 |
|
||||
|
||||
(完整 P1 清单详见各 sub-agent JSON 输出)
|
||||
|
||||
---
|
||||
|
||||
## 五、按维度分组的问题清单
|
||||
|
||||
### 5.1 模式标准化(P-*,54 条)
|
||||
|
||||
#### P-01 `import "server-only"` 缺失(2 条 P0)
|
||||
|
||||
| ID | 文件 | 修复 |
|
||||
|---|---|---|
|
||||
| G2-001 | exams/data-access.ts L1 | 首行添加 `import "server-only"` |
|
||||
| G5-001 | onboarding/data-access.ts L1 | 首行添加 `import "server-only"` |
|
||||
|
||||
#### P-03 cacheFn 未覆盖(30+ 条,P2)
|
||||
|
||||
集中模块:
|
||||
- **classes/data-access.ts**(24+ 读函数,G3-001 P0)
|
||||
- **lesson-preparation**(4 个,G1-021)
|
||||
- **questions**(5 个,G1-023)
|
||||
- **textbooks**(3 个,G1-024)
|
||||
- **lesson-preparation-substitutes**(1 个,G1-022)
|
||||
|
||||
修复模式:
|
||||
```ts
|
||||
// Before
|
||||
export const getClassNamesByIds = async (classIds: string[]) => { /* SQL */ }
|
||||
|
||||
// After
|
||||
export const getClassNamesByIdsRaw = async (classIds: string[]) => { /* SQL */ }
|
||||
export const getClassNamesByIds = cacheFn(getClassNamesByIdsRaw, {
|
||||
tags: ["classes:names"],
|
||||
ttl: 300,
|
||||
keyParts: ["classes", "getClassNamesByIds"],
|
||||
})
|
||||
```
|
||||
|
||||
#### P-05 错误处理不一致(13 条,P1-P2)
|
||||
|
||||
集中模块:files(9 处 try-catch 吞错误)、school(12 处 console.error + 吞异常)
|
||||
|
||||
#### P-07 日期序列化 helper 重复(5 处,P2-P3)
|
||||
|
||||
- attendance/data-access.ts `serializeDate`
|
||||
- attendance/data-access-stats.ts `serializeDate`
|
||||
- scheduling/data-access.ts `serializeDate`
|
||||
- school/data-access.ts `toIso`
|
||||
- course-plans/data-access.ts `toIso`/`toIsoRequired`
|
||||
|
||||
修复:提取到 `src/shared/lib/date-utils.ts`
|
||||
|
||||
#### P-09 `as` 断言(2 条 P3,非豁免)
|
||||
|
||||
- classes/data-access-admin.ts L36 `DEFAULT_CLASS_SUBJECTS as readonly string[]`
|
||||
- classes/data-access-teacher.ts L41 同上
|
||||
|
||||
#### P-10 `any` 使用
|
||||
|
||||
零违规,全部合规。
|
||||
|
||||
### 5.2 性能优化(F-*,71 条)
|
||||
|
||||
#### F-01 N+1 循环 SQL(11 条,跨 P0/P1/P2)
|
||||
|
||||
| ID | 文件 | 模式 |
|
||||
|---|---|---|
|
||||
| G1-002 | questions deleteQuestionRecursive | 递归内单独查询+删除 |
|
||||
| G1-003 | questions deleteQuestionsBatch | 循环调用递归删除 |
|
||||
| G1-004 | lesson-preparation deleteComment | 递归内单独查询+删除 |
|
||||
| G1-005 | textbooks reorderChapters | 循环内逐条 UPDATE |
|
||||
| G1-017 | lesson-preparation getSchedulesByDateRangeRaw | 拉全表后内存 filter |
|
||||
| G1-018 | lesson-preparation getResponsesByStudentIdRaw | 拉全量后内存 filter |
|
||||
| G2-002 | grades getPendingAppealsForReviewRaw | JS 层 filter 班级范围 |
|
||||
| G2-003 | adaptive-practice getTeacherClassPracticeOverviewsRaw | Promise.all 内 2N+1 |
|
||||
| G3-004 | classes getTeacherClassesRaw | 循环内 2 次子查询 |
|
||||
| G3-005 | course-plans reorderCoursePlanItems | 循环内 N 次 UPDATE |
|
||||
| G3-042 | classes generateUniqueInvitationCode | 循环内重试查询 |
|
||||
|
||||
#### F-02 `LIKE '%xxx%'` 全表扫描(7 条 P1)
|
||||
|
||||
| ID | 文件 | 字段 |
|
||||
|---|---|---|
|
||||
| G1-006 | lesson-preparation | lessonPlans.title |
|
||||
| G1-007 | lesson-preparation-knowledge | lessonPlans.content (JSON) |
|
||||
| G1-008 | questions | questions.content (JSON, +LOWER+CAST) |
|
||||
| G1-009 | textbooks | title/subject/grade/publisher 4 字段 |
|
||||
| G3-017 | classes-students | users.name/email |
|
||||
| G4-010 | messaging | messages.subject/content |
|
||||
|
||||
修复策略:
|
||||
- 短期:前缀匹配 `LIKE 'xxx%'`(可走索引)
|
||||
- 中期:FULLTEXT 索引 + `MATCH AGAINST IN BOOLEAN MODE`(questions 表已实施,参见架构图 1.1.4)
|
||||
- 长期:关联表存储提取后的关系(如 lesson_plan_knowledge_point_refs)
|
||||
|
||||
#### F-03 SELECT * 未指定列(35+ 条 P2-P3)
|
||||
|
||||
集中模块:lesson-preparation(16 处)、school(4 处)、scheduling(3 处)、attendance(2 处)、course-plans(7 处)、files(8 处)
|
||||
|
||||
#### F-05 无 LIMIT 大表查询(18 条 P1-P2)
|
||||
|
||||
集中模块:lesson-preparation(7 处)、textbooks(2 处)、classes(3 处)、proctoring(1 处)
|
||||
|
||||
#### F-09 事务范围问题(4 条 P1)
|
||||
|
||||
| ID | 文件 | 问题 |
|
||||
|---|---|---|
|
||||
| G3-006 | course-plans reorderCoursePlanItems | 多次 UPDATE 未包裹事务 |
|
||||
| G3-012 | school promoteGrades | 循环 UPDATE 未包裹事务 |
|
||||
| G3-025 | classes enrollTeacherByInvitationCode | 多次写操作未包裹事务 |
|
||||
| G4-009 | auth createUser | 两次 INSERT 无事务包裹 |
|
||||
|
||||
#### F-10 全表 COUNT 无过滤(4 条 P2)
|
||||
|
||||
集中模块:textbooks、questions、lesson-preparation、classes
|
||||
|
||||
### 5.3 架构违规(A-*,58 条)
|
||||
|
||||
#### A-02 data-access 含业务逻辑(18 条 P0-P2)
|
||||
|
||||
| 模块 | 文件 | 业务逻辑类型 |
|
||||
|---|---|---|
|
||||
| scheduling | data-access-class-schedule.ts | 时间校验 + 归属校验 + 状态机 |
|
||||
| scheduling | data-access.ts | — |
|
||||
| messaging | data-access.ts | 撤回状态机 + 防重复 + 页面编排 |
|
||||
| elective | data-access-operations.ts | 抽签算法 + 冲突检测 + i18n 通知 |
|
||||
| classes | data-access-teacher.ts | 邀请码状态机 + 角色校验 |
|
||||
| classes | data-access-invitations.ts | 懒清理状态迁移 |
|
||||
| school | data-access.ts | 角色判断 + 权限感知查询 |
|
||||
| attendance | data-access-correlation.ts | 跨模块编排 + 成绩归一化 |
|
||||
| attendance | data-access-stats.ts | 纯计算函数导出 |
|
||||
| lesson-preparation | data-access-review.ts | 状态机迁移 |
|
||||
| lesson-preparation | data-access-ai-evaluation.ts | 评分算法纯函数 |
|
||||
| textbooks | data-access.ts | 重排序算法 |
|
||||
| diagnostic | data-access.ts | 掌握度累积计算 |
|
||||
| homework | data-access.ts | computeOverdueCount 闭包 |
|
||||
|
||||
#### A-06 跨模块直查 schema 表(7 条 P0-P2)
|
||||
|
||||
| ID | 文件 | 被查模块 |
|
||||
|---|---|---|
|
||||
| G1-001 | textbooks/data-access-graph.ts | questions + diagnostic |
|
||||
| G1-031 | lesson-preparation/data-access.ts | textbooks (textbooks/chapters) |
|
||||
| G1-032 | lesson-preparation/data-access-schedules.ts | classes |
|
||||
| G1-033 | questions/data-access.ts | textbooks (knowledgePoints) |
|
||||
| G3-002 | scheduling/data-access.ts | classes + users + subjects |
|
||||
|
||||
#### A-08 actions 权限校验问题(4 条 P0-P1)
|
||||
|
||||
| ID | 文件 | 问题 |
|
||||
|---|---|---|
|
||||
| G4-002 | parent/ | 模块缺失 actions.ts,3 页面直访 data-access |
|
||||
| G4-003 | audit/actions.ts | purge 用读权限 |
|
||||
| G4-004 | audit/actions.ts | retention 配置用读权限 |
|
||||
| G4-047 | rbac/data-access-assignments.ts | 内存 post-fetch 过滤导致 total 错误(伴随 A-02) |
|
||||
|
||||
#### A-09 actions 直查 DB(1 条 P0)
|
||||
|
||||
| ID | 文件 | 问题 |
|
||||
|---|---|---|
|
||||
| G5-002 | onboarding/actions.ts L15-76 | 直接 `import { db }` 并查 `users` 表 |
|
||||
|
||||
#### A-10 `console.error` 调试代码(25+ 条 P1-P2)
|
||||
|
||||
| 模块 | 文件 | 数量 |
|
||||
|---|---|---|
|
||||
| school | data-access.ts | 12 |
|
||||
| files | data-access.ts | 12 |
|
||||
| classes | data-access-teacher/students/admin | 3 |
|
||||
| course-plans | data-access.ts | 2 |
|
||||
| audit | data-access.ts | 9 |
|
||||
|
||||
### 5.4 结构与可维护性(S-*,47 条)
|
||||
|
||||
#### S-01 超长文件(3 条 P0-P1)
|
||||
|
||||
| ID | 文件 | 行数 | 状态 |
|
||||
|---|---|---|---|
|
||||
| G4-001 | messaging/data-access.ts | 1089 | 超 1000 硬限,必须拆分 |
|
||||
| G3-007 | school/data-access.ts | 938 | 超 800 警告,接近硬限 |
|
||||
| G2-005 | grades/data-access-analytics.ts | 831 | 超 800 警告 |
|
||||
|
||||
#### S-02 单文件导出函数过多(4 条 P2)
|
||||
|
||||
| 文件 | 导出数 |
|
||||
|---|---|
|
||||
| messaging/data-access.ts | 42+ |
|
||||
| school/data-access.ts | 30+ |
|
||||
| textbooks/data-access.ts | 35 |
|
||||
| questions/data-access.ts | 28 |
|
||||
| classes/data-access.ts | 25+ |
|
||||
|
||||
#### S-03 重复 helper(8 条 P2)
|
||||
|
||||
| helper | 出现模块 |
|
||||
|---|---|
|
||||
| serializeDate/toIso | attendance、scheduling、school、course-plans |
|
||||
| toLessonPlanStatus | lesson-preparation(2 文件) |
|
||||
| isStringArray | lesson-preparation(2 文件) |
|
||||
| fetchClassesWithSubjects | classes(2 函数 145+124 行重复) |
|
||||
| fetchGradesWithHeads | school(3 函数重复) |
|
||||
|
||||
#### S-06 缺 JSDoc(15+ 条 P2-P3)
|
||||
|
||||
集中模块:lesson-preparation(versions/templates)、questions、textbooks
|
||||
|
||||
---
|
||||
|
||||
## 六、P0-P3 优先级矩阵
|
||||
|
||||
```
|
||||
高影响
|
||||
│
|
||||
│ P0 立即治理 P1 Phase 1
|
||||
│ ───────────────── ─────────────────
|
||||
│ • parent 权限漏洞 • N+1 循环 SQL(非热路径)
|
||||
│ • audit 权限提权 • LIKE 全表扫描
|
||||
│ • server-only 缺失 • 超长文件(school/grades)
|
||||
│ • 跨模块 schema 直查 • 业务逻辑嵌入 data-access
|
||||
│ • N+1 循环 SQL(热路径) • 事务未包裹
|
||||
│ • messaging 超硬限 • console.error 吞异常
|
||||
│
|
||||
├──────────────────────────────────────────────
|
||||
│
|
||||
│ P2 Phase 2 P3 Phase 3
|
||||
│ ───────────────── ─────────────────
|
||||
│ • cacheFn 未覆盖 • as 断言(widening)
|
||||
│ • SELECT * 未指定列 • 非空断言 !
|
||||
│ • 无 LIMIT 大表查询 • JSDoc 补齐
|
||||
│ • 重复 helper • 动态 import 注释
|
||||
│ • 单文件导出过多 • export * 改显式
|
||||
│
|
||||
低影响
|
||||
高紧迫 ─────────────────── 低紧迫
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、分阶段治理路线图
|
||||
|
||||
### Phase 0:紧急安全修复(XS-S,立即执行)
|
||||
|
||||
| 任务 | ID | 工作量 | 验证 |
|
||||
|---|---|---|---|
|
||||
| 添加 `import "server-only"` 到 exams/data-access.ts | G2-001 | XS | tsc + lint |
|
||||
| 添加 `import "server-only"` 到 onboarding/data-access.ts | G5-001 | XS | tsc + lint |
|
||||
| 新建 parent/actions.ts,3 页面改调 Action | G4-002 | M | 手动测试 3 页面 |
|
||||
| audit purge 权限点新增 + 替换 | G4-003 | S | 权限矩阵测试 |
|
||||
| audit retention 权限点替换 | G4-004 | S | 权限矩阵测试 |
|
||||
| onboarding/actions.ts 移除直查 DB | G5-002 | S | tsc + lint |
|
||||
|
||||
**Phase 0 完成标准**:所有 P0 安全漏洞修复,`npm run lint` + `npx tsc --noEmit` 零错误。
|
||||
|
||||
### Phase 1:P0 架构与性能修复(M-L,1-2 周)
|
||||
|
||||
| 任务批次 | 涉及 ID | 工作量 | 依赖 |
|
||||
|---|---|---|---|
|
||||
| **1.1 跨模块 schema 直查治理** | G1-001, G3-002, G1-031~033 | L | 需在 questions/diagnostic/textbooks/classes 模块新增跨模块接口 |
|
||||
| **1.2 messaging 拆分** | G4-001, G4-005~008 | L | 拆分为 7 个子文件 + 业务逻辑移至 actions |
|
||||
| **1.3 N+1 热路径修复** | G1-002~005, G3-001, G3-004, G3-005, G2-003 | L | classes 补齐 cacheFn 是基础 |
|
||||
| **1.4 scheduling 业务逻辑下移** | G3-003, G3-024, G3-025 | M | data-access-class-schedule.ts 重写 |
|
||||
| **1.5 elective 业务逻辑拆分** | G5-003, G5-004 | L | 提取 lib/lottery.ts + lib/schedule-conflict.ts |
|
||||
|
||||
**Phase 1 完成标准**:所有 P0 修复,关键路径性能提升,架构分层清晰。
|
||||
|
||||
### Phase 2:P1 性能与结构优化(M-L,2-3 周)
|
||||
|
||||
| 任务批次 | 涉及 ID | 工作量 |
|
||||
|---|---|---|
|
||||
| **2.1 LIKE 全表扫描治理** | G1-006~009, G3-017, G4-010 | L(FULLTEXT 索引 + 查询重写) |
|
||||
| **2.2 超长文件拆分** | G3-007, G2-005 | M(school 按职责拆 8 文件、grades-analytics 按维度拆) |
|
||||
| **2.3 school 模块重构** | G3-007~011 | L(拆分 + 角色判断移至 actions + 删除 console.error) |
|
||||
| **2.4 files 模块错误处理重构** | G5-005, G5-006, G5-007 | M(删除 try-catch + console.error) |
|
||||
| **2.5 事务包裹修复** | G3-006, G3-012, G3-025, G4-009 | S |
|
||||
| **2.6 无 LIMIT 查询保护** | G1-011~013, G1-050~053, G3-031~032, G3-041, G3-044 | M |
|
||||
|
||||
**Phase 2 完成标准**:所有 P1 修复,无超长文件,无 LIKE 全表扫描,无未包裹事务。
|
||||
|
||||
### Phase 3:P2 模式标准化(S-M,1-2 周)
|
||||
|
||||
| 任务批次 | 涉及 ID | 工作量 |
|
||||
|---|---|---|
|
||||
| **3.1 cacheFn 全量补齐** | G1-021~024, G3-001(剩余) | M |
|
||||
| **3.2 SELECT * 改显式列** | G1-039~049, G3-009, G3-026~028, G5-007 | M(机械替换) |
|
||||
| **3.3 日期 helper 提取** | G3-021, G3-047, G1-025 | S(提取 shared/lib/date-utils.ts) |
|
||||
| **3.4 重复 helper 提取** | G1-026~027, G3-020, G3-036 | M |
|
||||
| **3.5 JSDoc 补齐** | G1-034~036, G1-066~067, G3-046 | M |
|
||||
|
||||
**Phase 3 完成标准**:所有 P2 修复,模式统一,helper 集中到 shared/lib。
|
||||
|
||||
### Phase 4:P3 风格优化(XS,按需)
|
||||
|
||||
| 任务 | 涉及 ID | 工作量 |
|
||||
|---|---|---|
|
||||
| `as` widening 断言改类型标注 | G3-029~030 | XS |
|
||||
| 非空断言 `!` 改类型守卫 | G1-060~065 | XS |
|
||||
| `export *` 改显式 re-export | G3-048 | S |
|
||||
| 死代码删除 | G3-018 | XS |
|
||||
|
||||
**Phase 4 完成标准**:零 `as`(非豁免)、零 `!`、零死代码。
|
||||
|
||||
---
|
||||
|
||||
## 八、跨模块治理建议
|
||||
|
||||
### 8.1 新增 shared/lib 公共 helper
|
||||
|
||||
| helper | 路径 | 用途 | 替代模块 |
|
||||
|---|---|---|---|
|
||||
| `toISODateString` | shared/lib/date-utils.ts | 日期序列化 | attendance/scheduling/school/course-plans |
|
||||
| `buildScopeFilter` | shared/lib/scope-filter.ts | DataScope → SQL 过滤 | attendance/grades/homework 等重复实现 |
|
||||
| `serializeDate` | (合并到 date-utils.ts) | 同 toISODateString | — |
|
||||
|
||||
### 8.2 新增跨模块批量接口
|
||||
|
||||
| 接口 | 模块 | 用途 | 调用方 |
|
||||
|---|---|---|---|
|
||||
| `getActiveStudentIdsByClassIds(classIds)` | classes | 批量获取多班学生 ID | adaptive-practice、attendance |
|
||||
| `getGradeNamesByIds(gradeIds)` | school | 批量获取年级名称 | textbooks、lesson-preparation |
|
||||
| `getQuestionCountByKpIds(kpIds)` | questions | 知识点关联题目数 | textbooks |
|
||||
| `getKpMasteryByTextbookId(textbookId)` | diagnostic | 教材下知识点掌握度 | textbooks |
|
||||
|
||||
### 8.3 新增权限点
|
||||
|
||||
| 权限点 | 用途 | 角色映射 |
|
||||
|---|---|---|
|
||||
| `AUDIT_LOG_PURGE` | 审计日志物理删除 | admin 专属 |
|
||||
| `AUDIT_RETENTION_MANAGE` | 审计保留策略配置 | admin 专属 |
|
||||
|
||||
---
|
||||
|
||||
## 九、附录
|
||||
|
||||
### 9.1 完整规则表
|
||||
|
||||
见 [data-access-audit-framework-v1.md](./data-access-audit-framework-v1.md) 第二节。
|
||||
|
||||
### 9.2 sub-agent 原始输出索引
|
||||
|
||||
| 组 | 文件 | 问题数 |
|
||||
|---|---|---|
|
||||
| G1 | [g1-audit-output.json](./g1-audit-output.json) | 67 |
|
||||
| G2 | [g2-data-access-audit.json](./g2-data-access-audit.json) | 11 |
|
||||
| G3 | [g3-audit-output.json](./g3-audit-output.json) | 50 |
|
||||
| G4 | [g4-audit-output.json](./g4-audit-output.json) | 61 |
|
||||
| G5 | [g5-audit-output.json](./g5-audit-output.json) | 41 |
|
||||
|
||||
### 9.3 架构图遗漏记录
|
||||
|
||||
审计过程中发现的架构图(004/005)需补记项(治理阶段统一补图):
|
||||
|
||||
1. **parent 模块缺失 actions.ts** —— 004 文档模块清单未标注此异常
|
||||
2. **onboarding/actions.ts 直查 DB** —— 005 文档 dependencyMatrix 需修正
|
||||
3. **messaging/data-access.ts 拆分后** —— 005 文档 modules.messaging.exports 需更新
|
||||
4. **新增权限点 AUDIT_LOG_PURGE / AUDIT_RETENTION_MANAGE** —— 005 文档 permissions 节点需补记
|
||||
5. **新增 shared/lib/date-utils.ts** —— 004/005 shared 模块清单需补记
|
||||
|
||||
### 9.4 治理验证检查清单
|
||||
|
||||
每个 Phase 完成后必须通过:
|
||||
|
||||
- [ ] `npm run lint` 零错误
|
||||
- [ ] `npx tsc --noEmit` 零错误
|
||||
- [ ] 架构文档 004/005 同步更新
|
||||
- [ ] `docs/troubleshooting/known-issues.md` 追加新模式
|
||||
- [ ] 受影响模块的功能测试通过
|
||||
- [ ] P0/P1 问题在 issues JSON 中标记为 resolved
|
||||
395
docs/architecture/audit/archive/diagnostic-audit-report-v2.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# 学情诊断(Diagnostic)模块审计报告 v2
|
||||
|
||||
> 审计日期:2026-06-25
|
||||
> 审计范围:`src/modules/diagnostic/**`、`src/app/(dashboard)/{teacher,student,parent}/diagnostic/**`
|
||||
> 参照规则:`.trae/rules/project_rules.md`、`docs/architecture/004_architecture_impact_map.md` §2.22、`docs/architecture/005_architecture_data.json`
|
||||
> 前置文档:
|
||||
> - [diagnostic-audit-report.md](./diagnostic-audit-report.md)(v1,2026-06-22,3 P0 + 5 P1 + 5 P2 全部完成)
|
||||
> - [grades-diagnostic-audit-report-v4.md](./grades-diagnostic-audit-report-v4.md)(v4,2026-06-23,12 项 P1 全部完成)
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 v1/v4 已完成项回顾
|
||||
|
||||
v1 与 v4 审计共完成 **25 项**改进(3 P0 + 5 P1 + 5 P2 + 12 v4-P1),涵盖:跨模块 WidgetBoundary 提升到 shared、教师页面标题 i18n 化、教师 error.tsx i18n 化、as 断言消除、分享按钮移除、报告内容 i18n 驱动、Excel 导出 i18n 化、班级报告导出明细、角色配置驱动(role-config.ts)、年级诊断报告纵向切片、热力图键盘导航、DataScope 行级权限、师生关系校验、草稿隔离、通知机制、热力图图例、移动端表格滚动等。
|
||||
|
||||
### 1.2 当前文件分布(v2 实测)
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| 类型 | [types.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/types.ts) | 126 | DiagnosticReport / Mastery / Summary 类型定义(含 v4-P2-3 GradeMasterySummary) |
|
||||
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) | 519 | 掌握度查询 + 从提交/作业/成绩更新掌握度(含事务) |
|
||||
| 数据访问 | [data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) | 323 | 诊断报告 CRUD + DataScope 过滤 + 结构化错误码 |
|
||||
| 统计服务 | [stats-service.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts) | 506 | 14 个纯统计函数(含年级聚合) |
|
||||
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) | 302 | 6 个 Action(含年级生成 + 通知) |
|
||||
| 校验 | [schema.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/schema.ts) | 39 | 5 个 Zod schema |
|
||||
| 导出 | [export.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts) | 175 | Excel 导出(含班级明细 3 Sheet) |
|
||||
| 角色配置 | [role-config.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/role-config.ts) | 40 | 角色配置驱动(v4-P2-2) |
|
||||
| 组件 | [components/student-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx) | 299 | 学生诊断视图(概览+雷达+强弱项+报告+历史) |
|
||||
| 组件 | [components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) | 449 | 班级诊断视图(热力图+筛选+排名+关注列表+生成) |
|
||||
| 组件 | [components/report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) | 373 | 报告列表(过滤+表格+发布/删除/导出) |
|
||||
| 组件 | [components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) | 85 | 雷达图封装 |
|
||||
| 组件 | [components/confidence-utils.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/components/confidence-utils.ts) | 31 | 置信度计算 |
|
||||
| 页面 | 4 个 `page.tsx` + 5 个 `loading.tsx` + 5 个 `error.tsx` | — | teacher/student/parent 三角色路由 |
|
||||
| i18n | [zh-CN/diagnostic.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json) + [en/diagnostic.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/en/diagnostic.json) | 252 / 同步 | 翻译文件 |
|
||||
|
||||
### 1.3 数据流
|
||||
|
||||
```
|
||||
page.tsx (RSC)
|
||||
├─ getStudentMasterySummary / getClassMasterySummary / getGradeMasterySummary / getKnowledgePointStats (data-access)
|
||||
│ └─ db (drizzle) → knowledgePointMastery / knowledgePoints 表
|
||||
│ └─ 跨模块 data-access:classes / users / school / exams / homework / questions
|
||||
├─ getDiagnosticReports (data-access-reports, 含 DataScope 过滤)
|
||||
│ └─ db → learningDiagnosticReports 表
|
||||
└─ <StudentDiagnosticView> / <ClassDiagnosticView> / <ReportList> (client)
|
||||
└─ generateStudentReportAction / generateClassReportAction / generateGradeReportAction
|
||||
/ publishReportAction / deleteReportAction / exportDiagnosticReportAction
|
||||
/ getClassStudentsByKnowledgePointAction
|
||||
```
|
||||
|
||||
### 1.4 架构图记录完整性
|
||||
|
||||
经核对 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.22 与 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) `modules.diagnostic` 节点,架构图对诊断模块的记录**基本完整**,已涵盖 v1/v4 全部修复。但本次 v2 审计发现以下偏差需后续同步:
|
||||
|
||||
- 架构图未记录 `getDiagnosticReports` 中 `grade_managed` scope 未过滤的已知缺陷(v2-P1-1)。
|
||||
- 架构图未记录 `confidence-utils.ts` 的置信度计算逻辑过于简化(v2-P1-5)。
|
||||
- 架构图未记录 3 个子路由 error.tsx 仍存在硬编码中文(v2-P0-1)。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 国际化
|
||||
|
||||
#### 问题 2.1.1 | 3 个子路由 error.tsx 硬编码中文(P0)
|
||||
|
||||
- **位置**:
|
||||
- [teacher/diagnostic/class/[classId]/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/error.tsx#L17):`title="班级学情诊断加载失败"` `description="抱歉,加载班级诊断数据时发生了意外错误。请稍后重试。"` `label="重试"`
|
||||
- [teacher/diagnostic/student/[studentId]/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/error.tsx#L17):`title="学生学情诊断加载失败"` 同样硬编码
|
||||
- [parent/diagnostic/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/error.tsx#L17):`title="子女学情诊断加载失败"` 同样硬编码
|
||||
- **现象**:三个 error.tsx 客户端组件未使用 `useTranslations`,全部硬编码中文文案。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"。
|
||||
- **原因**:v1 审计 P0-3 仅修复了 `teacher/diagnostic/error.tsx`(教师报告列表页),遗漏了教师子路由和学生/家长子路由的 error.tsx。
|
||||
- **后果**:英文环境下这三个错误页显示中文,与系统其他已 i18n 化的错误页风格不一致。
|
||||
|
||||
#### 问题 2.1.2 | i18n 标签与代码逻辑不一致(P1)
|
||||
|
||||
- **位置**:
|
||||
- i18n:[zh-CN/diagnostic.json#L78](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json#L78):`"weaknesses": { "title": "弱项(<60%)" }`
|
||||
- 代码:[stats-service.ts#L83-99](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts#L83):`classifyStrengthsWeaknesses` 中弱项阈值为 `< 80`(P3-16 修复:消除 60-79 盲区)
|
||||
- **现象**:i18n 标签显示"弱项(<60%)",但代码实际将掌握度 < 80 的知识点都归类为弱项。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"——文本需与逻辑一致。
|
||||
- **原因**:P3-16 修复弱项分类阈值时未同步更新 i18n 标签。
|
||||
- **后果**:用户看到"弱项(<60%)"标签,但实际列表包含 60-79% 的知识点,造成认知混乱。
|
||||
|
||||
#### 问题 2.1.3 | 死 i18n 键未清理(P2)
|
||||
|
||||
- **位置**:[zh-CN/diagnostic.json#L157-165](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json#L157)
|
||||
- **现象**:`reportList.share`、`reportList.shareAriaLabel`、`reportList.shareTitle`、`reportList.shareDescription`、`reportList.shareLinkLabel`、`reportList.copyLink`、`reportList.copyLinkSuccess`、`reportList.copyLinkFailed`、`reportList.shareLinkAriaLabel` 共 9 个键仍保留在翻译文件中,但 v1-P1-3 已移除分享按钮,这些键不再被引用。
|
||||
- **违反规则**:项目规则精神——保持代码与配置一致,避免死代码。
|
||||
- **原因**:移除分享按钮时未清理对应的 i18n 键。
|
||||
- **后果**:翻译文件臃肿,维护成本增加;新增语言时需翻译无用的键。
|
||||
|
||||
#### 问题 2.1.4 | 热力图 aria-label 硬编码中文标点格式(P1)
|
||||
|
||||
- **位置**:[class-diagnostic-view.tsx#L195](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L195)
|
||||
- **现象**:`aria-label={`${kp.knowledgePointName}:${kp.averageMastery.toFixed(1)}%,${levelLabel},${kp.masteredCount}/${kp.totalStudents}`}` 使用硬编码中文全角冒号":"和逗号","。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
|
||||
- **原因**:aria-label 拼接时未使用 i18n 模板。
|
||||
- **后果**:英文环境下屏幕阅读器读出中文标点,影响无障碍体验。
|
||||
|
||||
#### 问题 2.1.5 | export.ts 错误消息硬编码英文(P2)
|
||||
|
||||
- **位置**:[export.ts#L28](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts#L28):`throw new Error("Report not found")`
|
||||
- **现象**:导出报告不存在时抛出硬编码英文错误。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
|
||||
- **后果**:中文环境下用户看到英文错误消息。
|
||||
|
||||
### 2.2 权限与安全
|
||||
|
||||
#### 问题 2.2.1 | grade_managed DataScope 未过滤(P1,安全漏洞)
|
||||
|
||||
- **位置**:[data-access-reports.ts#L220-238](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts#L220)
|
||||
- **现象**:`getDiagnosticReports` 仅处理 `children` 和 `class_taught` 两种 DataScope,对 `grade_managed`(年级主任)和 `class_members`(学生)scope 不做任何过滤。代码注释明确写道:`// grade_managed 需要跨模块查询年级学生,由调用方自行过滤`。
|
||||
- **违反规则**:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤,Server Action 二次校验"。
|
||||
- **原因**:grade_managed scope 需要跨模块查询年级学生 ID(通过 `getUserIdsByGradeId`),实现时为避免跨模块依赖未在 data-access 层完成过滤。
|
||||
- **后果**:年级主任(grade_head / teaching_head)角色调用时,`getDiagnosticReports` 返回全校所有学生的诊断报告,存在数据越权风险。虽然当前 teacher/diagnostic/page.tsx 在客户端对 class_members 做了二次过滤,但 grade_managed 完全未过滤。
|
||||
|
||||
#### 问题 2.2.2 | 教师报告列表页客户端过滤 class_members(P2)
|
||||
|
||||
- **位置**:[teacher/diagnostic/page.tsx#L56-59](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx#L56)
|
||||
- **现象**:`const visibleReports = ctx.dataScope.type === "class_members" ? reports.reports.filter((r) => r.studentId === ctx.userId) : reports.reports`
|
||||
- **违反规则**:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"——客户端过滤不安全。
|
||||
- **原因**:教师页面理论上不应被学生角色访问,但代码保留了 class_members 分支作为防御性过滤。这种过滤应在 data-access 层完成。
|
||||
- **后果**:虽然不影响功能(学生不会访问教师路由),但违背了"数据过滤在 data-access 层"的原则,且 data-access 已返回了不该返回的数据。
|
||||
|
||||
### 2.3 架构解耦
|
||||
|
||||
#### 问题 2.3.1 | 组件直接 import actions,无服务接口抽象(P1)
|
||||
|
||||
- **位置**:
|
||||
- [report-list.tsx#L41](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx#L41):`import { publishReportAction, deleteReportAction, exportDiagnosticReportAction } from "../actions"`
|
||||
- [class-diagnostic-view.tsx#L33](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L33):`import { generateClassReportAction, getClassStudentsByKnowledgePointAction } from "../actions"`
|
||||
- **现象**:客户端组件直接 import 并调用 Server Actions,未通过接口抽象或依赖注入。
|
||||
- **违反规则**:项目规则"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access(只能通过注入的接口调用)"。
|
||||
- **原因**:v1 审计将此项列为"后续建议"未实施,但用户在本次审计中将其升级为强制要求。
|
||||
- **后果**:组件无法独立测试(测试时必须 mock 整个 actions 模块);无法在不修改组件代码的情况下替换 actions 实现;组件与 Server Action 实现紧耦合。
|
||||
|
||||
### 2.4 错误处理与边界
|
||||
|
||||
#### 问题 2.4.1 | 无 Error Boundary 包裹独立数据区块(P1)
|
||||
|
||||
- **位置**:
|
||||
- [student-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx):概览卡片、雷达图、强弱项、报告、历史列表均在同一组件内,无 Error Boundary 隔离。
|
||||
- [class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx):概览、热力图、筛选、排名、关注列表、生成报告均在同一组件内。
|
||||
- **现象**:仅页面级有 error.tsx 错误边界,组件内部各数据区块无独立 Error Boundary。
|
||||
- **违反规则**:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
|
||||
- **原因**:v4-P2 已将 WidgetBoundary 提升到 shared 层并用于页面级包裹,但未下沉到组件内部各数据区块。
|
||||
- **后果**:雷达图渲染失败会导致整个诊断页面崩溃;热力图数据异常会波及排名表和关注列表。
|
||||
|
||||
#### 问题 2.4.2 | 异步数据无 Suspense + 骨架屏(P2)
|
||||
|
||||
- **位置**:所有页面均使用 `await Promise.all` 一次性获取所有数据后传给客户端组件。
|
||||
- **现象**:未使用 React Suspense 流式渲染,数据获取完成前整个页面阻塞。
|
||||
- **违反规则**:项目规则"异步数据使用 React Suspense + 骨架屏"。
|
||||
- **后果**:首屏白屏时间长;无法渐进式展示数据区块。
|
||||
|
||||
### 2.5 可测试性与置信度
|
||||
|
||||
#### 问题 2.5.1 | 置信度计算过于简化(P1)
|
||||
|
||||
- **位置**:[confidence-utils.ts#L18-21](file:///e:/Desktop/CICD/src/modules/diagnostic/components/confidence-utils.ts#L18)
|
||||
- **现象**:`getConfidenceLevel` 仅判断 `overallScore === null` 返回 "insufficient",否则一律返回 "high"。注释承认"后续可扩展为基于 totalQuestions 等数据量字段的多级判断",但未实施。
|
||||
- **违反规则**:项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离"——置信度计算虽已提取为纯函数,但逻辑不完整。
|
||||
- **原因**:v4-P3-7 引入置信度时为简化首版,未实现多级判断。
|
||||
- **后果**:仅做了 1 道题的报告也显示"高置信度",误导教师判断报告可信度。
|
||||
|
||||
#### 问题 2.5.2 | stats-service 14 个纯函数无单测(P2)
|
||||
|
||||
- **位置**:[stats-service.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts)
|
||||
- **现象**:14 个纯函数(`computeAverageMastery`、`classifyStrengthsWeaknesses`、`aggregateClassMastery` 等)已正确提取为纯函数,但无对应的单元测试文件。
|
||||
- **违反规则**:项目规则"可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离;导出清晰的接口类型以便 mock"。
|
||||
- **后果**:统计数据计算逻辑变更无回归保障;P3-16 弱项阈值修复这类边界 case 无法自动验证。
|
||||
|
||||
### 2.6 可复用性与扩展性
|
||||
|
||||
#### 问题 2.6.1 | 雷达图知识点名称截断无 tooltip(P2)
|
||||
|
||||
- **位置**:[mastery-radar-chart.tsx#L22-26](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx#L22)
|
||||
- **现象**:`shortName: d.knowledgePoint.length > 8 ? ${d.knowledgePoint.slice(0, 8)}... : d.knowledgePoint` 截断超过 8 字符的知识点名称,但未提供 tooltip 显示完整名称。
|
||||
- **违反规则**:项目规则"明确处理空数据、无权限、网络异常等边界状态"——信息截断是边界状态。
|
||||
- **后果**:长名称知识点被截断,用户无法查看完整名称,影响数据理解。
|
||||
|
||||
#### 问题 2.6.2 | 通知类型使用 "grade" 而非专用类型(P2)
|
||||
|
||||
- **位置**:[actions.ts#L157, L173](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts#L157)
|
||||
- **现象**:`createNotification({ type: "grade", ... })` 诊断报告发布通知复用了成绩通知类型 "grade"。
|
||||
- **违反规则**:项目规则精神——类型应语义准确,便于分类与过滤。
|
||||
- **后果**:学生无法区分"成绩通知"和"诊断报告通知";通知筛选功能无法按类型精确过滤诊断报告。
|
||||
|
||||
#### 问题 2.6.3 | 无监控埋点接口(P2)
|
||||
|
||||
- **位置**:整个模块无任何监控/埋点代码。
|
||||
- **现象**:报告生成、发布、删除、导出等关键操作无埋点。
|
||||
- **违反规则**:项目规则"监控:方案中预留关键操作埋点接口"。
|
||||
- **后果**:无法追踪诊断报告的使用情况;无法度量报告生成/发布转化率;异常无法主动发现。
|
||||
|
||||
### 2.7 parent 页面细节
|
||||
|
||||
#### 问题 2.7.1 | parent/diagnostic/page.tsx noRecordsTitle 与 noRecordsDescription 重复(P2)
|
||||
|
||||
- **位置**:[parent/diagnostic/page.tsx#L97-98](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/page.tsx#L97)
|
||||
- **现象**:`noRecordsTitle={t("parent.noReports")}` 和 `noRecordsDescription={t("parent.noReports")}` 使用同一个 i18n 键。
|
||||
- **原因**:复制粘贴时未区分标题和描述。
|
||||
- **后果**:空状态时标题和描述显示相同文本,UI 不够精细。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
对标 PowerSchool、Infinite Campus、Skyward、Alma、智学网、班级小管家、超星学习通、ClassIn 等 K12 系统,本模块在 v1/v4 修复后仍有以下差距:
|
||||
|
||||
| 维度 | 优秀实践 | 本模块现状 | 影响 |
|
||||
|------|---------|-----------|------|
|
||||
| **掌握度时间线** | PowerSchool/智学网支持掌握度时间线,展示知识点掌握度随时间的变化趋势,支持按知识点钻取历史 | 仅展示当前快照,`knowledgePointMastery` 表无历史版本,`lastAssessedAt` 仅记录最近一次 | 教师无法判断学生是否在进步或退步;无法评估教学干预效果 |
|
||||
| **个性化学习路径** | Alma/智学网基于弱项推荐具体学习资源(题目、视频、文档),形成学习路径 | 仅提供练习按钮跳转题目库,无资源推荐算法 | 推荐不够精准,学生需自行筛选练习内容 |
|
||||
| **学生×知识点掌握度矩阵** | Infinite Campus/Alma 提供学生×知识点矩阵热力图,支持点击单元格查看明细 | 班级视图仅有知识点聚合热力图,无学生维度矩阵 | 教师无法快速定位"哪个学生在哪个知识点上薄弱" |
|
||||
| **预测性分析(at-risk 预警)** | PowerSchool/Infinite Campus 基于历史数据预测 at-risk 学生,提前干预 | 无预测模型,仅基于当前掌握度 <60 判定需关注 | 无法主动预警,错失早期干预窗口 |
|
||||
| **报告模板自定义** | PowerSchool 支持学校自定义报告模板(推荐话术、评分区间、logo) | 报告内容固定由 stats-service 生成,仅 i18n 可切换语言 | 无法按学校需求定制报告风格 |
|
||||
| **多维度诊断** | Infinite Campus 结合成绩+出勤+行为做多维综合诊断 | 仅基于知识点掌握度单一维度 | 诊断维度单一,无法反映学生综合学习状态 |
|
||||
| **PDF 导出** | 所有对标系统支持 PDF 导出(便于打印分发) | 仅支持 Excel 导出 | 无法满足打印分发场景 |
|
||||
| **数据置信度可视化** | Alma 在报告上标注数据量与置信度,帮助教师判断结论可靠性 | 置信度计算过于简化(仅 null/非 null 两级) | 教师无法判断报告可信度 |
|
||||
| **流式渲染** | 现代 K12 系统使用 React Suspense 流式渲染,首屏快速可见 | 全部 `await Promise.all` 阻塞渲染 | 首屏白屏时间长 |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急,影响核心规范或安全)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| v2-P0-1 | 3 个子路由 error.tsx 硬编码中文 | 接入 `useTranslations("diagnostic")`,新增对应 i18n 键 |
|
||||
|
||||
### P1(重要,影响安全/解耦/一致性)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| v2-P1-1 | grade_managed DataScope 未过滤 | 在 data-access-reports.ts 中处理 grade_managed scope,调用 `getUserIdsByGradeId` 过滤 |
|
||||
| v2-P1-2 | i18n 弱项标签与代码逻辑不一致 | 更新 i18n 标签为"弱项(<80%)" |
|
||||
| v2-P1-3 | 热力图 aria-label 硬编码中文标点 | 使用 i18n 模板键 `heatmapCellAriaLabel` |
|
||||
| v2-P1-4 | 组件直接 import actions 无接口抽象 | 定义 `DiagnosticService` 接口,通过 React Context 注入;组件通过 `useDiagnosticService()` 获取 |
|
||||
| v2-P1-5 | 置信度计算过于简化 | 基于 `totalQuestions` 实现多级置信度(<5 insufficient / 5-15 low / 16-30 medium / >30 high) |
|
||||
| v2-P1-6 | 无 Error Boundary 包裹独立数据区块 | 在概览、雷达图、强弱项、报告、历史等区块外包裹 WidgetBoundary |
|
||||
|
||||
### P2(增强,提升完整性与企业级能力)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| v2-P2-1 | 死 i18n 键未清理 | 移除 9 个 share 相关 i18n 键 |
|
||||
| v2-P2-2 | 教师页面客户端过滤 class_members | 在 data-access 层过滤,移除客户端 filter |
|
||||
| v2-P2-3 | export.ts 错误消息硬编码 | 改用 DiagnosticReportError 结构化错误码 |
|
||||
| v2-P2-4 | 异步数据无 Suspense 流式渲染 | 拆分数据获取为独立 async 组件,使用 Suspense 包裹(中长期) |
|
||||
| v2-P2-5 | 雷达图名称截断无 tooltip | 为截断的名称添加 Tooltip 显示完整名称 |
|
||||
| v2-P2-6 | 通知类型使用 "grade" | 新增 "diagnostic" 通知类型(需 notifications 模块配合) |
|
||||
| v2-P2-7 | 无监控埋点接口 | 定义 `DiagnosticMonitor` 接口,在关键操作处调用 |
|
||||
| v2-P2-8 | stats-service 无单测 | 为 14 个纯函数补充单元测试 |
|
||||
| v2-P2-9 | parent noRecordsTitle/Description 重复 | 区分标题和描述 i18n 键 |
|
||||
|
||||
### P3(长期,需较大投入)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| v2-P3-1 | 掌握度时间线 | 新增 `knowledgePointMasteryHistory` 表记录历史版本,前端展示时间线图表 |
|
||||
| v2-P3-2 | 个性化学习路径推荐 | 基于弱项推荐具体学习资源 |
|
||||
| v2-P3-3 | 学生×知识点掌握度矩阵 | 新增矩阵视图组件 |
|
||||
| v2-P3-4 | 预测性分析 | 基于 historical mastery 训练 at-risk 预测模型 |
|
||||
| v2-P3-5 | 报告模板自定义 | 允许学校配置报告模板 |
|
||||
| v2-P3-6 | PDF 导出 | 新增 PDF 导出能力 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次 v2 审计发现架构图需同步以下内容:
|
||||
|
||||
### 004_architecture_impact_map.md §2.22
|
||||
|
||||
1. **已知问题新增**:
|
||||
- 记录 v2-P0-1 三个子路由 error.tsx 硬编码中文及修复
|
||||
- 记录 v2-P1-1 grade_managed DataScope 未过滤及修复
|
||||
- 记录 v2-P1-4 组件解耦 Context 注入
|
||||
- 记录 v2-P1-5 置信度计算改进
|
||||
- 记录 v2-P1-6 数据区块 Error Boundary 包裹
|
||||
2. **依赖关系更新**:标注 diagnostic 模块新增 `DiagnosticServiceContext` 依赖注入机制
|
||||
|
||||
### 005_architecture_data.json
|
||||
|
||||
1. `modules.diagnostic.exports` 补充 `DiagnosticService` 接口、`DiagnosticServiceProvider`、`useDiagnosticService` hook
|
||||
2. `modules.diagnostic.knownIssues` 新增 v2 系列问题记录
|
||||
3. `modules.diagnostic.dependencies` 标注 grade_managed scope 现已调用 `getUserIdsByGradeId` 过滤
|
||||
|
||||
---
|
||||
|
||||
## 六、实施计划
|
||||
|
||||
### 6.1 本次实施范围(P0 + P1 + 可快速完成的 P2)
|
||||
|
||||
本次实施 **P0 全部 + P1 全部 + 6 项 P2**,P3 长期项记录备查不实施。
|
||||
|
||||
### 6.2 重构方案设计(满足强制原则)
|
||||
|
||||
#### 完全解耦
|
||||
|
||||
定义 TypeScript 接口 `DiagnosticService` 抽象所有数据依赖:
|
||||
|
||||
```typescript
|
||||
// src/modules/diagnostic/services/diagnostic-service.ts
|
||||
export interface DiagnosticService {
|
||||
generateStudentReport(studentId: string, period: string): Promise<string>
|
||||
generateClassReport(classId: string, period: string): Promise<string>
|
||||
generateGradeReport(gradeId: string, period: string): Promise<string>
|
||||
publishReport(id: string): Promise<void>
|
||||
deleteReport(id: string): Promise<void>
|
||||
exportReport(reportId: string): Promise<{ buffer: string; filename: string }>
|
||||
getClassStudentsByKp(classId: string, kpId: string, threshold?: number): Promise<...>
|
||||
}
|
||||
```
|
||||
|
||||
通过 React Context 注入:
|
||||
|
||||
```typescript
|
||||
// src/modules/diagnostic/services/diagnostic-service-context.tsx
|
||||
const DiagnosticServiceContext = createContext<DiagnosticService | null>(null)
|
||||
export function DiagnosticServiceProvider({ service, children }) { ... }
|
||||
export function useDiagnosticService(): DiagnosticService { ... }
|
||||
```
|
||||
|
||||
默认实现绑定现有 Server Actions;测试时可注入 mock 实现。
|
||||
|
||||
#### 组合优先
|
||||
|
||||
- `StudentDiagnosticView`、`ClassDiagnosticView`、`ReportList` 通过 `children` / slots 组合子区块。
|
||||
- 逻辑复用提取为 `useDiagnosticActions` hook(内部调用 `useDiagnosticService()`)。
|
||||
|
||||
#### 国际化就绪
|
||||
|
||||
- 所有新增文本使用 `diagnostic.*` 命名空间翻译键。
|
||||
- aria-label 模板使用 i18n 占位符:`heatmapCellAriaLabel: "{name}:{level}%,{label},{mastered}/{total}"`。
|
||||
|
||||
#### 最大化复用
|
||||
|
||||
- `DiagnosticSection` 通用区块组件(含 WidgetBoundary + Suspense + 标题 + 内容 slot)。
|
||||
- `useDiagnosticActions` hook 统一封装 actions 调用 + toast + router.refresh。
|
||||
|
||||
#### 错误与边界处理
|
||||
|
||||
- 每个数据区块外包裹 `WidgetBoundary`。
|
||||
- 异步子区块使用 `Suspense` + 骨架屏(本次仅在页面级流式拆分,组件级 Suspense 列入 P2-4 长期)。
|
||||
|
||||
#### 可测试性
|
||||
|
||||
- `DiagnosticService` 接口清晰,可 mock。
|
||||
- `confidence-utils` 置信度计算改为基于 `totalQuestions` 的多级判断。
|
||||
|
||||
#### 可扩展性
|
||||
|
||||
- 角色差异通过 `role-config.ts` 配置驱动(v4-P2-2 已实现)。
|
||||
- 通知类型、监控埋点通过接口预留扩展点。
|
||||
|
||||
#### 企业级补充
|
||||
|
||||
- a11y:热力图 aria-label i18n 化;雷达图 tooltip。
|
||||
- 性能:保持 RSC 获取初始数据。
|
||||
- 安全:grade_managed scope 在 data-access 层过滤。
|
||||
- 监控:定义 `DiagnosticMonitor` 接口预留埋点。
|
||||
|
||||
### 6.3 翻译文件结构示例
|
||||
|
||||
```json
|
||||
{
|
||||
"diagnostic": {
|
||||
"error": {
|
||||
"classLoadFailed": "班级学情诊断加载失败",
|
||||
"classLoadFailedDesc": "抱歉,加载班级诊断数据时发生了意外错误。请稍后重试。",
|
||||
"studentLoadFailed": "学生学情诊断加载失败",
|
||||
"studentLoadFailedDesc": "抱歉,加载学生诊断数据时发生了意外错误。请稍后重试。",
|
||||
"parentLoadFailed": "子女学情诊断加载失败",
|
||||
"parentLoadFailedDesc": "抱歉,加载子女诊断数据时发生了意外错误。请稍后重试。",
|
||||
"retry": "重试"
|
||||
},
|
||||
"weaknesses": {
|
||||
"title": "弱项(<80%)"
|
||||
},
|
||||
"classDiagnostic": {
|
||||
"heatmapCellAriaLabel": "{name}:{level}%,{label},{mastered}/{total}"
|
||||
},
|
||||
"reportList": {
|
||||
"radarPointTooltip": "{fullName}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
301
docs/architecture/audit/archive/diagnostic-audit-report.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# 学情诊断(Diagnostic)模块审计报告
|
||||
|
||||
> 审计日期:2026-06-22
|
||||
> 审计范围:`src/modules/diagnostic/**`、`src/app/(dashboard)/teacher/diagnostic/**`、`src/app/(dashboard)/student/diagnostic/**`、`src/app/(dashboard)/parent/diagnostic/**`
|
||||
> 参照规则:`docs/architecture/004_architecture_impact_map.md` §2.22、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`
|
||||
> 前置文档:[grades-diagnostic-audit-report-v4.md](./grades-diagnostic-audit-report-v4.md)(v4 已完成 12 项 P1 数据安全修复)
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| 类型 | [types.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/types.ts) | 109 | DiagnosticReport / Mastery / Summary 类型定义 |
|
||||
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access.ts) | 477 | 掌握度查询 + 从提交/成绩更新掌握度 |
|
||||
| 数据访问 | [data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) | 256 | 诊断报告 CRUD + DataScope 过滤 |
|
||||
| 统计服务 | [stats-service.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts) | 388 | 12 个纯统计函数 |
|
||||
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) | 274 | 5 个 Action(生成/发布/删除/导出/按知识点筛选) |
|
||||
| 校验 | [schema.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/schema.ts) | 31 | 4 个 Zod schema |
|
||||
| 导出 | [export.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts) | 122 | Excel 导出 |
|
||||
| 组件 | [components/class-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx) | 448 | 班级诊断视图(热力图+筛选+排名+关注列表+生成) |
|
||||
| 组件 | [components/student-diagnostic-view.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx) | 293 | 学生诊断视图(概览+雷达+强弱项+报告+历史) |
|
||||
| 组件 | [components/report-list.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx) | 445 | 报告列表(过滤+表格+发布/删除/导出/分享) |
|
||||
| 组件 | [components/mastery-radar-chart.tsx](file:///e:/Desktop/CICD/src/modules/diagnostic/components/mastery-radar-chart.tsx) | 85 | 雷达图封装 |
|
||||
| 组件 | [components/confidence-utils.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/components/confidence-utils.ts) | 31 | 置信度计算 |
|
||||
| 页面 | [teacher/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx) | 67 | 教师报告列表页 |
|
||||
| 页面 | [teacher/diagnostic/student/[studentId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx) | 86 | 教师查看学生诊断 |
|
||||
| 页面 | [teacher/diagnostic/class/[classId]/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx) | 50 | 教师班级诊断 |
|
||||
| 页面 | [student/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/diagnostic/page.tsx) | 40 | 学生自我诊断 |
|
||||
| 页面 | [parent/diagnostic/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/page.tsx) | 128 | 家长多子女诊断 |
|
||||
| 骨架屏 | 5 个 `loading.tsx` | — | 各路由骨架屏 |
|
||||
| 错误边界 | 5 个 `error.tsx` | — | 各路由错误边界 |
|
||||
| i18n | [diagnostic.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/diagnostic.json) | 204 | 中文翻译 |
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
page.tsx (RSC)
|
||||
├─ getStudentMasterySummary / getClassMasterySummary / getKnowledgePointStats (data-access)
|
||||
│ └─ db (drizzle) → knowledgePointMastery / knowledgePoints 表
|
||||
├─ getDiagnosticReports (data-access-reports, 含 DataScope 过滤)
|
||||
│ └─ db → learningDiagnosticReports 表
|
||||
└─ <StudentDiagnosticView> / <ClassDiagnosticView> / <ReportList> (client)
|
||||
└─ generateStudentReportAction / generateClassReportAction / publishReportAction / deleteReportAction / exportDiagnosticReportAction / getClassStudentsByKnowledgePointAction
|
||||
```
|
||||
|
||||
### 1.3 架构图记录完整性
|
||||
|
||||
经核对 [004_architecture_impact_map.md](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) §2.22 与 [005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json),架构图对诊断模块的记录**基本完整**,但存在以下偏差:
|
||||
|
||||
- 行数统计略有滞后:图记 `data-access.ts 179 行`,实际为 477 行(含 `updateMasteryFromHomeworkSubmission` 和 `updateMasteryFromExamScore` 两个大函数)。
|
||||
- 未记录 `export.ts` 的存在(架构图文件清单缺少此文件)。
|
||||
- 未记录 `confidence-utils.ts` 组件文件。
|
||||
- 未记录跨模块 UI 依赖:`teacher/diagnostic/student/[studentId]/page.tsx` 和 `teacher/diagnostic/class/[classId]/page.tsx` 直接 import `@/modules/grades/components/widget-boundary`,架构图未标注此跨模块 UI 依赖。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 架构解耦
|
||||
|
||||
#### 问题 2.1.1 | 跨模块直接 import UI 组件 WidgetBoundary(P0)
|
||||
|
||||
- **位置**:
|
||||
- [teacher/diagnostic/student/[studentId]/page.tsx#L13](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx#L13)
|
||||
- [teacher/diagnostic/class/[classId]/page.tsx#L8](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx#L8)
|
||||
- **现象**:`import { WidgetBoundary } from "@/modules/grades/components/widget-boundary"`
|
||||
- **违反规则**:项目规则"模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表"的精神延伸——UI 组件跨模块直接 import 同样破坏模块独立性。WidgetBoundary 是通用错误边界组件,不应属于 grades 业务模块。
|
||||
- **原因**:WidgetBoundary 最初为 grades 模块创建,diagnostic 模块复用时直接 import 了 grades 模块的实现,而非将其提升到 shared 层。
|
||||
- **后果**:grades 模块对 WidgetBoundary 的任何变更(重命名、删除、props 修改)都会破坏 diagnostic 模块编译;diagnostic 模块无法独立测试、独立部署。
|
||||
|
||||
#### 问题 2.1.2 | 组件直接 import actions,无服务接口抽象(P1)
|
||||
|
||||
- **位置**:
|
||||
- [components/report-list.tsx#L42](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx#L42):`import { publishReportAction, deleteReportAction, exportDiagnosticReportAction } from "../actions"`
|
||||
- [components/class-diagnostic-view.tsx#L33](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L33):`import { generateClassReportAction, getClassStudentsByKnowledgePointAction } from "../actions"`
|
||||
- **现象**:客户端组件直接 import 并调用 Server Actions,未通过接口抽象或依赖注入。
|
||||
- **违反规则**:项目规则"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
|
||||
- **原因**:模块未采用依赖注入模式,组件与 actions 紧耦合。
|
||||
- **后果**:组件无法独立测试(测试时必须 mock 整个 actions 模块);无法在不修改组件代码的情况下替换 actions 实现。
|
||||
|
||||
### 2.2 国际化
|
||||
|
||||
#### 问题 2.2.1 | 教师页面标题硬编码英文(P0)
|
||||
|
||||
- **位置**:
|
||||
- [teacher/diagnostic/page.tsx#L59-62](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx#L59):`<h1>Learning Diagnostic</h1>` + `<p>View and manage diagnostic reports based on knowledge point mastery.</p>`
|
||||
- [teacher/diagnostic/student/[studentId]/page.tsx#L70-74](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx#L70):`Student Diagnostic` + `Knowledge point mastery analysis and diagnostic reports.`
|
||||
- [teacher/diagnostic/class/[classId]/page.tsx#L38-42](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx#L38):`Class Diagnostic` + `Class-level knowledge point mastery overview and student attention list.`
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"。
|
||||
- **原因**:教师端 3 个页面未使用 `getTranslations` 获取翻译,直接硬编码英文文案。学生端和家端已正确使用 i18n。
|
||||
- **后果**:中文环境下教师看到英文标题,与系统其他页面风格不一致。
|
||||
|
||||
#### 问题 2.2.2 | teacher/diagnostic/error.tsx 硬编码中文(P0)
|
||||
|
||||
- **位置**:[teacher/diagnostic/error.tsx#L17-22](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/error.tsx#L17)
|
||||
- **现象**:`title="学情诊断页面加载失败"` `description="抱歉,页面加载时发生了意外错误。请稍后重试。"` `label="重试"` 全部硬编码。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
|
||||
- **原因**:error.tsx 是客户端组件但未使用 `useTranslations`。
|
||||
- **后果**:英文环境下错误页显示中文,国际化不一致。对比 student/diagnostic/error.tsx 已正确使用 i18n。
|
||||
|
||||
#### 问题 2.2.3 | parent/diagnostic/page.tsx 错误卡片中英文混用(P1)
|
||||
|
||||
- **位置**:[parent/diagnostic/page.tsx#L116-119](file:///e:/Desktop/CICD/src/app/(dashboard)/parent/diagnostic/page.tsx#L116)
|
||||
- **现象**:`{t("error.loadFailed")} for {item.studentName}.` 和 `Please refresh the page or contact the school administrator if the problem persists.` 混用 i18n key 和硬编码英文。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
|
||||
- **后果**:中文环境下显示"加载失败 for 张三.",中英文混杂,用户体验差。
|
||||
|
||||
#### 问题 2.2.4 | 报告内容硬编码中文(P1)
|
||||
|
||||
- **位置**:[stats-service.ts#L280-353](file:///e:/Desktop/CICD/src/modules/diagnostic/stats-service.ts#L280)
|
||||
- **现象**:`buildStudentReportContent` 和 `buildClassReportContent` 生成中文报告内容,如 `"建议复习「${m.knowledgePointName}」知识点"`、`"学生 ${summary.studentName} 在 ${period} 期间整体掌握度"` 等。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
|
||||
- **原因**:纯函数层生成报告内容时直接硬编码中文,未通过 i18n。
|
||||
- **后果**:英文环境下生成的诊断报告内容为中文,无法国际化。报告内容存储在数据库中,已生成的历史报告无法回溯翻译。
|
||||
|
||||
#### 问题 2.2.5 | Excel 导出表头硬编码中文(P1)
|
||||
|
||||
- **位置**:[export.ts#L40-99](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts#L40)
|
||||
- **现象**:Excel 表头如 `"学生姓名"`、`"报告周期"`、`"综合得分"`、`"知识点掌握度"` 等硬编码中文;文件名 `诊断报告_${safePeriod}_${formatDateForFile()}.xlsx` 也硬编码。
|
||||
- **违反规则**:项目规则"所有用户可见文本必须适配 i18n"。
|
||||
- **后果**:英文环境下导出的 Excel 文件表头和文件名为中文。
|
||||
|
||||
### 2.3 类型安全
|
||||
|
||||
#### 问题 2.3.1 | as 类型断言(P1)
|
||||
|
||||
- **位置**:[teacher/diagnostic/page.tsx#L24, L28](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/diagnostic/page.tsx#L24)
|
||||
- **现象**:`(v as DiagnosticReportType)` 和 `(v as DiagnosticReportStatus)` 使用 as 断言。
|
||||
- **违反规则**:项目规则"禁止 as 断言(除非从 unknown 转换或测试中,需注释原因)"。
|
||||
- **原因**:虽有类型守卫 `VALID_REPORT_TYPES.has(v)` 校验,但转换时使用了 as 而非类型守卫函数返回值收窄。
|
||||
- **后果**:绕过 TypeScript 严格类型检查,潜在类型不安全。
|
||||
|
||||
### 2.4 错误处理与边界
|
||||
|
||||
#### 问题 2.4.1 | 分享链接指向不存在的路由(P1)
|
||||
|
||||
- **位置**:[components/report-list.tsx#L157, L208](file:///e:/Desktop/CICD/src/modules/diagnostic/components/report-list.tsx#L157)
|
||||
- **现象**:`const url = \`${window.location.origin}/teacher/diagnostic/reports/${shareId}\`` 指向 `/teacher/diagnostic/reports/[id]` 路由,但该路由在项目中不存在(无对应 page.tsx)。
|
||||
- **原因**:分享功能开发时未创建对应路由页面。
|
||||
- **后果**:用户点击分享链接后得到 404 页面,功能不可用。
|
||||
|
||||
#### 问题 2.4.2 | 班级报告导出缺少明细(P2)
|
||||
|
||||
- **位置**:[export.ts#L85-113](file:///e:/Desktop/CICD/src/modules/diagnostic/export.ts#L85)
|
||||
- **现象**:班级报告仅导出概览 Sheet,缺少知识点统计和需关注学生明细。代码注释明确说明"班级报告的 studentId 为 null,需要从 period 反查 classId 不现实"。
|
||||
- **原因**:v4-P1 已为 `learningDiagnosticReports` 表新增 `classId` 字段,但 export.ts 未同步更新使用该字段查询班级明细。
|
||||
- **后果**:教师导出班级报告时只能看到概览,无法获取知识点统计和需关注学生列表,导出功能不完整。
|
||||
|
||||
### 2.5 可复用性与配置驱动
|
||||
|
||||
#### 问题 2.5.1 | 角色差异通过 props 硬编码而非配置驱动(P2)
|
||||
|
||||
- **位置**:[components/student-diagnostic-view.tsx#L32](file:///e:/Desktop/CICD/src/modules/diagnostic/components/student-diagnostic-view.tsx#L32)
|
||||
- **现象**:`practiceHrefBase` prop 区分角色(学生默认 `/student/learning/assignments`,教师传 `/teacher/questions`,家长传 `null`)。
|
||||
- **违反规则**:项目规则"采用配置驱动设计,例如通过角色配置决定该模块渲染哪些 Widget/子模块"。
|
||||
- **原因**:角色差异通过 props 传递,而非通过角色配置对象统一管理。
|
||||
- **后果**:新增角色需修改组件 props 传递逻辑,而非仅修改配置。
|
||||
|
||||
#### 问题 2.5.2 | 无年级诊断报告生成入口(P2)
|
||||
|
||||
- **位置**:[types.ts#L3](file:///e:/Desktop/CICD/src/modules/diagnostic/types.ts#L3)
|
||||
- **现象**:`DiagnosticReportType` 定义了 `"grade"` 类型,但 [actions.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/actions.ts) 无 `generateGradeReportAction`,[data-access-reports.ts](file:///e:/Desktop/CICD/src/modules/diagnostic/data-access-reports.ts) 无 `generateGradeDiagnosticReport` 函数。
|
||||
- **原因**:年级报告功能定义了类型但未实现。
|
||||
- **后果**:管理员无法生成年级级别的诊断报告,功能不完整。report-list 过滤器中可选"年级"类型但永远无数据。
|
||||
|
||||
### 2.6 可访问性
|
||||
|
||||
#### 问题 2.6.1 | 热力图色块缺少键盘导航(P2)
|
||||
|
||||
- **位置**:[components/class-diagnostic-view.tsx#L190-201](file:///e:/Desktop/CICD/src/modules/diagnostic/components/class-diagnostic-view.tsx#L190)
|
||||
- **现象**:热力图色块为 `<div>` 且仅有 `role="img"`,无 `tabIndex` 和键盘焦点样式,键盘用户无法逐个聚焦查看详情。
|
||||
- **违反规则**:项目规则"可访问性(a11y):语义化标签、ARIA 属性、键盘导航"。
|
||||
- **后果**:键盘用户无法通过 Tab 遍历热力图色块查看 tooltip/title 详情。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
对标 PowerSchool、Infinite Campus、Skyward、Alma、智学网、班级小管家等 K12 系统,本模块在以下方面存在差距:
|
||||
|
||||
| 维度 | 优秀实践 | 本模块现状 | 影响 |
|
||||
|------|---------|-----------|------|
|
||||
| **诊断趋势分析** | PowerSchool/智学网支持掌握度时间线,展示知识点掌握度随时间的变化趋势 | 仅展示当前快照,无历史趋势对比 | 教师无法判断学生是否在进步或退步 |
|
||||
| **年级诊断报告** | Infinite Campus 支持年级级别诊断,对比班级间差异 | 类型已定义但无实现入口 | 管理员无法做年级层面决策 |
|
||||
| **报告详情页** | 所有同类系统都有独立的报告详情页,支持分享链接 | 分享链接指向不存在的路由 | 分享功能不可用 |
|
||||
| **班级报告导出明细** | PowerSchool/Infinite Campus 导出含知识点统计+学生列表 | 班级报告仅导出概览 | 教师无法离线分析 |
|
||||
| **掌握度时间线** | 智学网展示每个知识点的掌握度变化曲线 | 无时间维度数据 | 无法评估教学干预效果 |
|
||||
| **个性化学习路径** | Alma/智学网基于弱项推荐学习路径和资源 | 仅提供练习按钮跳转题目库 | 推荐不够精准 |
|
||||
| **诊断报告模板** | PowerSchool 支持自定义报告模板 | 报告内容固定硬编码 | 无法按学校需求定制 |
|
||||
| **多维度诊断** | Infinite Campus 结合成绩+出勤+行为做多维诊断 | 仅基于知识点掌握度 | 诊断维度单一 |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急,影响功能正确性或核心规范)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P0-1 | 跨模块 import WidgetBoundary | 将 WidgetBoundary 提升到 `shared/components/` 层,diagnostic 和 grades 模块统一从 shared 引用 |
|
||||
| P0-2 | 教师页面标题硬编码英文 | 3 个教师页面使用 `getTranslations("diagnostic")` 获取标题和描述 |
|
||||
| P0-3 | teacher/diagnostic/error.tsx 硬编码中文 | 接入 `useTranslations("diagnostic")`,与其他 error.tsx 一致 |
|
||||
|
||||
### P1(重要,影响用户体验或类型安全)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P1-1 | parent 错误卡片中英文混用 | 提取完整 i18n 键,消除硬编码英文 |
|
||||
| P1-2 | as 类型断言 | 改用类型守卫函数返回值收窄,消除 as |
|
||||
| P1-3 | 分享链接指向不存在路由 | 移除分享功能或创建对应路由页面。鉴于当前无报告详情页需求,移除分享按钮避免 404 |
|
||||
| P1-4 | 报告内容硬编码中文 | stats-service 的报告内容生成改为接收 i18n 翻译函数参数,或在 actions 层调用时注入翻译后的模板 |
|
||||
| P1-5 | Excel 导出表头硬编码 | export.ts 接收 i18n 翻译参数,表头和文件名使用翻译键 |
|
||||
|
||||
### P2(增强,提升完整性)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P2-1 | 班级报告导出缺少明细 | 利用 v4-P1 新增的 classId 字段查询班级掌握度,导出知识点统计+需关注学生 Sheet |
|
||||
| P2-2 | 角色差异通过 props 硬编码 | 定义角色配置对象,通过配置驱动 practiceHrefBase 等角色差异 |
|
||||
| P2-3 | 无年级诊断报告入口 | 实现 generateGradeDiagnosticReport + 对应 Action(中长期) |
|
||||
| P2-4 | 热力图色块缺少键盘导航 | 添加 tabIndex={0} 和 focus-visible 样式 |
|
||||
| P2-5 | 架构图行数统计滞后 | 同步 data-access.ts 实际行数,补充 export.ts 和 confidence-utils.ts 记录 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图需同步以下内容:
|
||||
|
||||
### 004_architecture_impact_map.md §2.22
|
||||
|
||||
1. **文件清单更新**:
|
||||
- `data-access.ts` 行数从 179 更新为 477(含 `updateMasteryFromHomeworkSubmission` 和 `updateMasteryFromExamScore`)
|
||||
- 补充 `export.ts`(122 行,Excel 导出)
|
||||
- 补充 `components/confidence-utils.ts`(31 行,置信度计算)
|
||||
2. **已知问题新增**:
|
||||
- 记录 P0-1 跨模块 import WidgetBoundary 问题及修复
|
||||
- 记录 P0-2/P0-3 i18n 遗漏问题及修复
|
||||
- 记录 P1-3 分享链接 404 问题及修复
|
||||
3. **依赖关系更新**:标注 WidgetBoundary 已从 grades 模块提升到 shared 层
|
||||
|
||||
### 005_architecture_data.json
|
||||
|
||||
1. `modules.diagnostic.exports` 补充 `export.ts` 和 `confidence-utils.ts` 文件记录
|
||||
2. `modules.diagnostic.dependencies` 更新:移除对 `grades/components/widget-boundary` 的 UI 依赖,改为 `shared/components/widget-boundary`
|
||||
3. `modules.diagnostic.fileList` 行数同步更新
|
||||
|
||||
---
|
||||
|
||||
## 六、实施状态(2026-06-24 全部完成)
|
||||
|
||||
> 本章节记录审计报告中所有 P0/P1/P2 项的实施完成情况。所有项均已通过 `npx tsc --noEmit` 与 `npm run lint` 校验(诊断模块零错误)。
|
||||
|
||||
### 6.1 P0 项实施状态
|
||||
|
||||
| 编号 | 状态 | 实施内容 | 涉及文件 |
|
||||
|------|------|---------|---------|
|
||||
| P0-1 | ✅ 已完成 | WidgetBoundary 已从 `modules/grades/components/widget-boundary.tsx` 提升到 `shared/components/widget-boundary.tsx`;diagnostic 与 grades 模块统一从 `@/shared/components/widget-boundary` 引用;grades 模块原文件已删除 | `src/shared/components/widget-boundary.tsx`(新建)、`src/modules/grades/components/widget-boundary.tsx`(删除)、`src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx`、`src/app/(dashboard)/teacher/diagnostic/class/[classId]/page.tsx` |
|
||||
| P0-2 | ✅ 已完成 | 3 个教师页面(`teacher/diagnostic/page.tsx`、`teacher/diagnostic/student/[studentId]/page.tsx`、`teacher/diagnostic/class/[classId]/page.tsx`)均使用 `getTranslations("diagnostic")` 获取标题与描述,新增对应 i18n 键 `teacherTitle`、`teacherDescription`、`studentTitle`、`studentDescription`、`classTitle`、`classDescription` | 上述 3 个页面 + `src/shared/i18n/messages/zh-CN/diagnostic.json` + `src/shared/i18n/messages/en/diagnostic.json` |
|
||||
| P0-3 | ✅ 已完成 | `teacher/diagnostic/error.tsx` 接入 `useTranslations("diagnostic")`,与 `student/diagnostic/error.tsx` 风格一致;新增 i18n 键 `errorTitle`、`errorDescription`、`errorRetry` | `src/app/(dashboard)/teacher/diagnostic/error.tsx` + 两个 i18n 文件 |
|
||||
|
||||
### 6.2 P1 项实施状态
|
||||
|
||||
| 编号 | 状态 | 实施内容 | 涉及文件 |
|
||||
|------|------|---------|---------|
|
||||
| P1-1 | ✅ 已完成 | `parent/diagnostic/page.tsx` 错误卡片中英文混用已消除;新增 i18n 键 `errorForStudent`、`errorContactAdmin`,使用 `t("errorForStudent", { name: item.studentName })` 替代硬编码 | `src/app/(dashboard)/parent/diagnostic/page.tsx` + 两个 i18n 文件 |
|
||||
| P1-2 | ✅ 已完成 | `teacher/diagnostic/page.tsx` 中 `(v as DiagnosticReportType)` 和 `(v as DiagnosticReportStatus)` 已替换为类型守卫函数返回值收窄:`VALID_REPORT_TYPES.has(v) ? v : DEFAULT_REPORT_TYPE` 模式,消除 `as` 断言 | `src/app/(dashboard)/teacher/diagnostic/page.tsx` |
|
||||
| P1-3 | ✅ 已完成 | `components/report-list.tsx` 中分享按钮与相关逻辑已移除(包括 `shareReportAction` 调用、`window.location.origin` URL 构造、分享对话框),避免 404;保留发布/删除/导出三个核心操作 | `src/modules/diagnostic/components/report-list.tsx` |
|
||||
| P1-4 | ✅ 已完成 | `stats-service.ts` 中 `buildStudentReportContent` 和 `buildClassReportContent` 已重构为接收 `ReportContentTranslations` 接口参数;新增 `getReportContentTranslations()` 在 data-access-reports 层调用 `getTranslations` 注入翻译;报告内容生成改为 i18n 驱动 | `src/modules/diagnostic/stats-service.ts`、`src/modules/diagnostic/data-access-reports.ts`、两个 i18n 文件 |
|
||||
| P1-5 | ✅ 已完成 | `export.ts` 中 Excel 表头和文件名已改为接收 i18n 翻译参数;`exportDiagnosticReportAction` 在调用 `exportDiagnosticReportToExcel` 前通过 `getTranslations("diagnostic")` 注入翻译;新增 i18n 键 `sheetOverview`、`sheetClassStats`、`sheetAttentionStudents`、`colStudentName`、`colPeriod`、`colReportType`、`colStatus`、`colScore`、`colGeneratedAt`、`colSummary`、`colStrengths`、`colWeaknesses`、`colRecommendations`、`filenameDiagnosticReport` 等 | `src/modules/diagnostic/export.ts`、`src/modules/diagnostic/actions.ts`、两个 i18n 文件 |
|
||||
|
||||
### 6.3 P2 项实施状态
|
||||
|
||||
| 编号 | 状态 | 实施内容 | 涉及文件 |
|
||||
|------|------|---------|---------|
|
||||
| P2-1 | ✅ 已完成 | 班级报告导出已利用 v4-P1 新增的 `classId` 字段调用 `getClassMasterySummary`,导出包含三个 Sheet:概览、知识点统计、需关注学生明细;新增 i18n 键 `sheetClassStats`、`sheetAttentionStudents`、`metricClass`、`metricStudentCount`、`metricAttentionCount`、`colMasteredCount`、`colNotMasteredCount`、`colTotalStudents`、`colAverageMastery`、`colWeakCount`、`noAttentionStudents` | `src/modules/diagnostic/export.ts` + 两个 i18n 文件 |
|
||||
| P2-2 | ✅ 已完成 | 新建 `src/modules/diagnostic/role-config.ts`,定义 `DiagnosticRole` 类型、`DiagnosticRoleConfig` 接口、`DIAGNOSTIC_ROLE_CONFIG` 记录(student/teacher/parent 三角色配置)和 `getDiagnosticRoleConfig` 辅助函数;`StudentDiagnosticView` 组件新增 `role` prop,内部通过 `getDiagnosticRoleConfig(role).practiceHrefBase` 解析配置;原 `practiceHrefBase` prop 标记 `@deprecated` 保留向后兼容(同时传入时 `role` 优先);3 个调用点已迁移为 `role="student"` / `role="teacher"` / `role="parent"` | `src/modules/diagnostic/role-config.ts`(新建)、`src/modules/diagnostic/components/student-diagnostic-view.tsx`、`src/app/(dashboard)/student/diagnostic/page.tsx`、`src/app/(dashboard)/teacher/diagnostic/student/[studentId]/page.tsx`、`src/app/(dashboard)/parent/diagnostic/page.tsx` |
|
||||
| P2-3 | ✅ 已完成 | 完整实现年级诊断报告纵向切片:① DB schema 新增 `gradeId` 字段 + `gradeIdx` 索引 + 迁移 SQL `0012_diagnostic_grade_id.sql`;② 类型新增 `GradeMasterySummary` 接口,`DiagnosticReport` 接口新增 `gradeId: string \| null`;③ data-access 新增 `getGradeMasterySummary`(缓存,并行查询年级名+学生 ID+掌握度行);④ stats-service 新增 `buildGradeMasterySummary` 和 `buildGradeReportContent` 纯函数;⑤ data-access-reports 新增 `generateGradeDiagnosticReport`,含 `GRADE_NOT_FOUND` / `GRADE_NO_MASTERY_DATA` 错误码;⑥ schema 新增 `GenerateGradeReportSchema`;⑦ actions 新增 `generateGradeReportAction` Server Action(含 `requirePermission` + `revalidatePath`);⑧ i18n 新增 `gradeSummary`、`gradeRecommendation`、`gradeNoWeakness` 键 | `src/shared/db/schema.ts`、`drizzle/0012_diagnostic_grade_id.sql`(新建)、`src/modules/diagnostic/types.ts`、`src/modules/diagnostic/data-access.ts`、`src/modules/diagnostic/stats-service.ts`、`src/modules/diagnostic/data-access-reports.ts`、`src/modules/diagnostic/schema.ts`、`src/modules/diagnostic/actions.ts`、两个 i18n 文件 |
|
||||
| P2-4 | ✅ 已完成 | 班级诊断视图热力图色块新增 `tabIndex={0}` 和 `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` 样式;外层容器 `role` 从 `"img"` 改为 `"group"`(因容器内现含可聚焦元素);保留每个色块的 `aria-label` 提供完整描述 | `src/modules/diagnostic/components/class-diagnostic-view.tsx` |
|
||||
| P2-5 | ✅ 已完成 | 架构图 004 和 005 已同步:① `data-access.ts` 行数更新为实际值;② 补充 `export.ts`、`role-config.ts`、`confidence-utils.ts` 文件记录;③ 已知问题章节新增 11 条 P0-1 至 P2-4 修复记录;④ 依赖矩阵新增 `school` 模块依赖(`getGradeNameById`、`getUserIdsByGradeId`)和 `shared/components/widget-boundary`;⑤ `learningDiagnosticReports` 表描述补充 `gradeId` 字段;⑥ `modules.diagnostic.exports` 新增 `getGradeMasterySummary`、`generateGradeDiagnosticReport`、`buildGradeMasterySummary`、`buildGradeReportContent`、`generateGradeReportAction`、`GenerateGradeReportSchema` 等 | `docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json` |
|
||||
|
||||
### 6.4 验证结果
|
||||
|
||||
- **TypeScript**:`npx tsc --noEmit` 通过,诊断模块零错误(仅 `dashboard/services/dashboard-service.ts` 存在与本模块无关的预存语法错误)。
|
||||
- **ESLint**:`npm run lint` 通过,诊断模块零警告。
|
||||
- **架构图一致性**:004 与 005 两份架构文档已与源码同步,所有新增/修改的导出函数、类型、依赖关系、DB 表字段均已记录。
|
||||
|
||||
### 6.5 后续建议(未列入本次实施范围)
|
||||
|
||||
以下为审计过程中识别但未列入本次实施的长期增强项,建议后续按需推进:
|
||||
|
||||
1. **掌握度时间线**:新增 `knowledgePointMasteryHistory` 表记录每次掌握度变化,前端展示时间线图表。
|
||||
2. **个性化学习路径推荐**:基于弱项知识点推荐具体学习资源(题目、视频、文档),而非仅跳转题目库。
|
||||
3. **多维度诊断**:结合成绩、出勤、行为数据做多维综合诊断。
|
||||
4. **报告模板自定义**:允许学校配置报告内容模板(如自定义推荐话术、评分区间)。
|
||||
5. **报告详情页**:若未来需要分享功能,创建 `/teacher/diagnostic/reports/[id]` 路由页面。
|
||||
6. **可测试性增强**:为 `stats-service.ts` 中 12 个纯函数补充单元测试,导出 `ReportContentTranslations` 接口便于 mock。
|
||||
7. **依赖注入抽象**:将 `report-list.tsx` 和 `class-diagnostic-view.tsx` 中直接 import actions 的模式重构为通过 React Context 注入数据服务接口,提升可测试性。
|
||||
339
docs/architecture/audit/archive/elective-audit-report.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# 选修课(Elective)模块审计报告
|
||||
|
||||
> 审计日期:2026-06-25
|
||||
> 审计范围:
|
||||
> - `src/modules/elective/**`
|
||||
> - `src/app/(dashboard)/admin/elective/**`、`src/app/(dashboard)/teacher/elective/**`、`src/app/(dashboard)/student/elective/**`
|
||||
> - 跨模块依赖:`school` / `users` / `classes` 的 data-access,`rbac` 的 `ELECTIVE_*` 权限点
|
||||
> - i18n 资源:`src/shared/i18n/messages/{zh-CN,en}/elective.json`
|
||||
> 参照规则:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、`.trae/rules/project_rules.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
| 层 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| Server Actions | [actions.ts](file:///e:/Desktop/CICD/src/modules/elective/actions.ts) | 348 | 8 个写 Action(权限校验 + Zod + trackEvent + 资源归属校验) |
|
||||
| 数据访问 | [data-access.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts) | 258 | 课程 CRUD + scope 过滤 + 显示名聚合 + 共享映射函数 |
|
||||
| 数据访问 | [data-access-operations.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts) | 374 | 选课/退课/抽签(事务 + FOR UPDATE 锁 + Fisher-Yates) + 时间冲突/学分上限校验 |
|
||||
| 数据访问 | [data-access-selections.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-selections.ts) | 147 | 选课记录查询 + 学生可选课程 |
|
||||
| 跨模块抽象 | [resolvers.ts](file:///e:/Desktop/CICD/src/modules/elective/resolvers.ts) | 83 | CourseDisplayResolver / StudentGradeResolver 接口 + 注入函数(测试 mock 友好) |
|
||||
| Schema | [schema.ts](file:///e:/Desktop/CICD/src/modules/elective/schema.ts) | 179 | Zod 校验(5 个 schema) |
|
||||
| Types | [types.ts](file:///e:/Desktop/CICD/src/modules/elective/types.ts) | 73 | 类型定义 |
|
||||
| Constants | [constants.ts](file:///e:/Desktop/CICD/src/modules/elective/constants.ts) | 55 | i18n key 映射 + Badge variant + 类型守卫 |
|
||||
| Import-export | [export.ts](file:///e:/Desktop/CICD/src/modules/elective/export.ts) | 102 | Excel 导出(课程列表 + 选课名单) |
|
||||
| 组件 | [components/elective-page-layout.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-page-layout.tsx) | 30 | 页面布局骨架(header/children 插槽) |
|
||||
| 组件 | [components/elective-course-list.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx) | 236 | 课程卡片网格 + 管理操作 |
|
||||
| 组件 | [components/elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx) | 301 | 课程创建/编辑表单 |
|
||||
| 组件 | [components/elective-filters.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-filters.tsx) | 51 | nuqs 筛选栏(搜索 + 模式) |
|
||||
| 组件 | [components/student-selection-view.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/student-selection-view.tsx) | 248 | 学生选课视图(已选 + 可选) |
|
||||
| 页面 | [admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx) | 47 | 管理员课程列表(RSC) |
|
||||
| 页面 | [admin/elective/create/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/create/page.tsx) | 32 | 创建课程(RSC) |
|
||||
| 页面 | [admin/elective/[id]/edit/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/[id]/edit/page.tsx) | 44 | 编辑课程(RSC) |
|
||||
| 页面 | [teacher/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx) | 58 | 教师我的课程(RSC) |
|
||||
| 页面 | [student/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx) | 48 | 学生选课中心(RSC) |
|
||||
| 骨架屏 | 5 个 `loading.tsx`(admin/admin-create/admin-edit/teacher/student) | — | 列表/表单骨架屏 |
|
||||
| 错误边界 | 3 个 `error.tsx`(admin/teacher/student) | — | 错误兜底 |
|
||||
| i18n | [zh-CN/elective.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/zh-CN/elective.json) | 114 | 中文翻译 |
|
||||
| i18n | [en/elective.json](file:///e:/Desktop/CICD/src/shared/i18n/messages/en/elective.json) | 114 | 英文翻译 |
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
page.tsx (RSC)
|
||||
└─ getElectiveCourses / getElectiveCourseById / getAvailableCoursesForStudent / getStudentSelections (data-access)
|
||||
└─ db (drizzle) → electiveCourses / courseSelections 表
|
||||
└─ 跨模块 data-access(通过 resolvers.ts 接口抽象):
|
||||
school.getSubjectOptions / school.getGradeOptions
|
||||
users.getUserNamesByIds
|
||||
classes.getStudentActiveGradeId
|
||||
└─ <ElectiveCourseList> (client) → deleteElectiveCourseAction / openSelectionAction / closeSelectionAction / runLotteryAction
|
||||
└─ <ElectiveCourseForm> (client) → createElectiveCourseAction / updateElectiveCourseAction
|
||||
└─ <StudentSelectionView> (client) → selectCourseAction / dropCourseAction
|
||||
```
|
||||
|
||||
### 1.3 架构图记录完整性
|
||||
|
||||
`docs/architecture/004_architecture_impact_map.md` 与 `005_architecture_data.json` 已覆盖 elective 模块(章节 2.20),包含完整的 exports / 依赖关系 / 权限点 / 文件清单。但「已知问题」段存在过时信息(详见第五部分),需同步修正。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 P0:教师页面跳转到 admin 路由(跨角色越权 + 404)
|
||||
|
||||
- **位置**:[teacher/elective/page.tsx:53-54](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/page.tsx#L53-L54)
|
||||
- **现象**:教师页面渲染 `ElectiveCourseList` 时传入 `createHref="/admin/elective/create"`、`editBaseHref="/admin/elective"`。教师点击"创建课程"或"编辑"按钮后会被路由到 `/admin/*`,由于中间件对 admin 角色做了路由保护,教师实际看到的是 403 / 重定向到首页。
|
||||
- **原因**:`ElectiveCourseList` 只支持单一 `createHref`/`editBaseHref`,admin 与 teacher 共用一份组件时硬编码了 admin 路径。
|
||||
- **违反规则**:「Server Action 必须使用 `requirePermission()` 进行权限校验」(前端入口虽然校验了权限,但跳转到无权访问的路由等同于绕过校验);UX 上不可达。
|
||||
- **后果**:教师角色虽然被授予 `ELECTIVE_MANAGE` 权限,却无法实际创建/编辑课程,功能完全不可用。
|
||||
|
||||
### 2.2 P0:parent 角色缺失选课页面
|
||||
|
||||
- **位置**:`src/app/(dashboard)/parent/elective/**`(目录不存在)
|
||||
- **现象**:[005_architecture_data.json](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json) 中 parent 角色被授予 `ELECTIVE_READ` 权限,但没有对应的 parent 页面。家长无法查看子女的选课情况。
|
||||
- **违反规则**:「所有用户可见文本必须适配 i18n」「最大化复用:识别四个角色共用的 UI 块和业务逻辑块」——当前只覆盖 3 个角色,遗漏 parent。
|
||||
- **后果**:家长对子女选课缺乏监督,无法及时发现选课异常(如未选满学分、错选时间冲突课程)。
|
||||
|
||||
### 2.3 P0:admin/student 页面缺少 `requirePermission()`
|
||||
|
||||
- **位置**:
|
||||
- [admin/elective/page.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/page.tsx)(无任何权限校验,直接调用 `getElectiveCourses`)
|
||||
- [student/elective/page.tsx:17](file:///e:/Desktop/CICD/src/app/(dashboard)/student/elective/page.tsx#L17) 使用 `getAuthContext()` 仅获取 userId,未做权限校验
|
||||
- **违反规则**:「Server Action 必须使用 `requirePermission()` 进行权限校验」 + 项目记忆中的硬约束「Parent routes must include permission checks with both `parentId` and `studentId` to prevent information leakage」。
|
||||
- **后果**:仅依赖中间件的路由级保护,缺少纵深防御;若中间件配置出现疏漏(如新增动态路由),会直接导致越权读他人数据。teacher 页面已经做了示范(`requirePermission(Permissions.ELECTIVE_READ)`),admin/student 不应例外。
|
||||
|
||||
### 2.4 P0:选课/退课错误消息未走 i18n
|
||||
|
||||
- **位置**:[data-access-operations.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts)
|
||||
- L233 `throw new Error("Course selection is not open")`
|
||||
- L237 `throw new Error("Selection has not started yet")`
|
||||
- L241 `throw new Error("Selection has ended")`
|
||||
- L254 `throw new Error("Already selected this course")`
|
||||
- L259 `throw new Error("Schedule conflicts with your existing courses")`
|
||||
- L265 `throw new Error(\`Credit limit exceeded (${creditCheck.current}/${creditCheck.max})\`)`
|
||||
- L301-303 `message: "Enrolled successfully" / "Added to waitlist" / "Selection submitted"`
|
||||
- **现象**:上述英文 throw 出去后经由 `handleActionError` 包装为 `{ success: false, message: <英文> }` 返回前端,toast 直接显示英文。
|
||||
- **违反规则**:「所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键」。
|
||||
- **后果**:中文用户看到英文错误提示,i18n 资源中已存在对应的中文键(`errors.selectionClosed`、`errors.alreadySelected`、`errors.scheduleConflict`、`errors.creditExceeded` 等)却完全没被复用。
|
||||
|
||||
### 2.5 P0:`export.ts` 存在 `as` 类型断言
|
||||
|
||||
- **位置**:[export.ts:21](file:///e:/Desktop/CICD/src/modules/elective/export.ts#L21)
|
||||
```ts
|
||||
status: params.status as "draft" | "open" | "closed" | "cancelled" | undefined,
|
||||
```
|
||||
- **违反规则**:「禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)」。
|
||||
- **后果**:未做类型守卫即强转,传入非法字符串(如 `"foo"`)会被静默接受,运行时引发 SQL 类型不匹配。
|
||||
|
||||
### 2.6 P1:`elective-course-form.tsx` 大量硬编码英文文案
|
||||
|
||||
- **位置**:[elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx)
|
||||
- L95 `"New Elective Course"` / `"Edit Elective Course"`
|
||||
- L102 `"Course Name *"`
|
||||
- L112 `"Subject"`
|
||||
- L129 `"Grade"`
|
||||
- L146 `"Teacher"`
|
||||
- L163 `"Capacity"`
|
||||
- L175 `"Classroom"`
|
||||
- L184 `"Schedule"`
|
||||
- L188 `placeholder="e.g. Mon 14:00-15:30"`
|
||||
- L194 `"Credit"`
|
||||
- L225 `"Start Date"`
|
||||
- L235 `"End Date"`
|
||||
- L245 `"Selection Start"`
|
||||
- L258 `"Selection End"`
|
||||
- L274 `"Description"`
|
||||
- L278 `placeholder="Course description..."`
|
||||
- L291 `"Cancel"`
|
||||
- L294 `"Saving..."` / `"Create"` / `"Save"`
|
||||
- L72-85 `"Invalid form state"` / `"Failed to save course"` 等 toast 回退文案
|
||||
- **违反规则**:「所有用户可见文本必须适配 i18n」+ i18n 资源已存在 `form.createTitle` / `form.editTitle` / `form.namePlaceholder` / `form.descriptionPlaceholder` 等键。
|
||||
- **后果**:中文用户在创建/编辑课程表单中看到全英文界面,体验割裂。
|
||||
|
||||
### 2.7 P1:`elective-course-list.tsx` 与 `elective-course-form.tsx` 使用 `<a>` 而非 `<Link>`
|
||||
|
||||
- **位置**:
|
||||
- [elective-course-list.tsx:92](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L92) `<a href={createHref}>`
|
||||
- [elective-course-list.tsx:177](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-list.tsx#L177) `<a href={...edit...}>`
|
||||
- **违反规则**:项目记忆「Link navigation must use Next.js `<Link>` component instead of raw `<a>` tags」。
|
||||
- **后果**:原生 `<a>` 触发整页刷新,丢失客户端导航状态、prefetch 优化、Layout 复用。
|
||||
|
||||
### 2.8 P1:错误边界文案与按钮文案错误
|
||||
|
||||
- **位置**:[admin/elective/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/admin/elective/error.tsx) 与 [teacher/elective/error.tsx](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/elective/error.tsx)
|
||||
- **现象**:
|
||||
- `title` 与 `description` 都用 `t("errors.unexpected")`,重复且无信息量;
|
||||
- 重试按钮 `action.label` 用 `t("actions.save")`("保存"),但学生页用 `t("actions.retry")`("重试")—— admin/teacher 文案错误。
|
||||
- **违反规则**:i18n 完整性 + UX 一致性。
|
||||
- **后果**:用户在错误页看到"保存"按钮且语义与"重试"不符。
|
||||
|
||||
### 2.9 P1:表单未使用 React Hook Form
|
||||
|
||||
- **位置**:[elective-course-form.tsx](file:///e:/Desktop/CICD/src/modules/elective/components/elective-course-form.tsx)
|
||||
- **现象**:使用 4 个独立 `useState` 管理 Select 状态,没有统一表单状态管理、字段校验、脏值检查。项目 tech stack 明确包含 `React Hook Form`。
|
||||
- **违反规则**:技术栈一致性 + 可维护性。
|
||||
- **后果**:字段一多需要每个都加 `useState`,扩展性差;与服务端 Zod 校验形成两套校验,难以保持一致。
|
||||
|
||||
### 2.10 P1:`export.ts` 表头混用英文硬编码
|
||||
|
||||
- **位置**:[export.ts:56](file:///e:/Desktop/CICD/src/modules/elective/export.ts#L56) `"Status"`、L93 `"Status"`、L94 `"Priority"`、L95 `"Selected At"`、L96 `"Enrolled At"`
|
||||
- **现象**:课程列表 sheet 中 `status` 列的 header 是英文硬编码 `"Status"`,而其他列都走 i18n;选课名单 sheet 中 `status`/`priority`/`selectedAt`/`enrolledAt` 4 列 header 全英文硬编码。
|
||||
- **违反规则**:「所有用户可见文本必须适配 i18n」—— Excel 导出也是用户可见文本。
|
||||
- **后果**:中文用户下载 Excel 后表头混杂中英文。
|
||||
|
||||
### 2.11 P1:`getElectiveCourses` 静默吞错
|
||||
|
||||
- **位置**:[data-access.ts:161-164](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L161-L164)
|
||||
```ts
|
||||
} catch (error) {
|
||||
console.error("getElectiveCourses failed:", error)
|
||||
return []
|
||||
}
|
||||
```
|
||||
- **现象**:DB 查询失败时返回空数组,页面无任何错误提示,用户以为"暂无数据"。
|
||||
- **违反规则**:「错误与边界处理:明确处理空数据、无权限、网络异常等边界状态」。
|
||||
- **后果**:DB 异常被掩盖,运维无法及时发现,用户误判为"无课程",无法触发错误边界。
|
||||
|
||||
### 2.12 P1:`parseSchedule` 不支持完整英文星期与多时段
|
||||
|
||||
- **位置**:[data-access-operations.ts:35](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L35)
|
||||
```ts
|
||||
const match = schedule.match(/^(周[一二三四五六日天]|[MonTueWedThuFriSatSun]+)\s+.../)
|
||||
```
|
||||
- **现象**:
|
||||
- 字符类 `[MonTueWedThuFriSatSun]+` 匹配任意 M/o/n/T/u/e 字符组合(如 `"Mon"`、`"oMenT"` 都会通过),不严谨;
|
||||
- 不支持 `"Monday"`、`"周一 14:00-15:30, 周三 16:00-17:30"` 多时段;
|
||||
- `normalizeDay` 表只有 `mon/tue/...`,没有 `monday/tuesday/...`。
|
||||
- **后果**:教师在 schedule 输入 `"Monday 14:00-15:30"` 时不会触发冲突检测,存在隐性排课冲突。
|
||||
|
||||
### 2.13 P1:学分上限与候补人数硬编码(✅ 2026-06-25 已修复)
|
||||
|
||||
- **位置**:[data-access-operations.ts:15](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L15) `const MAX_CREDIT_PER_TERM = 10`
|
||||
- **现象**:所有年级/学校共用同一个上限,无法配置。
|
||||
- **违反规则**:「可扩展性:采用配置驱动设计」。
|
||||
- **后果**:K12 不同年级(如高一 8 学分 vs 高三 12 学分)无法差异化配置。
|
||||
- **修复说明**:新增 [data-access-settings.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-settings.ts),复用 `systemSettings` 表(category="elective")作为配置存储。`getElectiveCreditLimit(gradeId?)` 支持按年级覆盖(key=`creditLimit:grade:<gradeId>`),fallback 到全局(key=`creditLimit:default`),均未配置返回默认值 10。React `cache()` 包装请求级去重。`checkCreditLimit` 新增 `studentGradeId: string | null` 参数并调用 `getElectiveCreditLimit(studentGradeId)`,`selectCourse` 在事务前通过 `getStudentGradeId(studentId)` 拿到年级 ID 并透传。
|
||||
|
||||
### 2.14 P1:抽签结果不可重跑
|
||||
|
||||
- **位置**:[data-access-operations.ts:212](file:///e:/Desktop/CICD/src/modules/elective/data-access-operations.ts#L212)
|
||||
```ts
|
||||
await tx.update(electiveCourses).set({ enrolledCount, status: "closed", updatedAt: now })...
|
||||
```
|
||||
- **现象**:抽签完成立即把课程状态置为 `closed`,管理员若发现结果异常无法重新抽签(重抽需要先把状态手动改回 `open`)。
|
||||
- **后果**:管理员缺乏"试抽 + 调整 + 正式抽"的灵活度,K12 学校在抽签争议时无法快速复核。
|
||||
|
||||
### 2.15 P1:管理员缺少课程统计概览
|
||||
|
||||
- **位置**:缺失
|
||||
- **现象**:admin/teacher 页面只有平铺的课程卡片,缺少总览统计(如总课程数、总选课人数、热门科目分布、容量使用率)。
|
||||
- **违反规则**:行业最佳实践「K12 admin 应有数据驾驶舱」。
|
||||
- **后果**:管理员难以从全局角度掌握选课运行情况。
|
||||
|
||||
### 2.16 P2:`getCachedSubjectOptions` / `getCachedGradeOptions` 缓存全量选项
|
||||
|
||||
- **位置**:[data-access.ts:104-105](file:///e:/Desktop/CICD/src/modules/elective/data-access.ts#L104-L105)
|
||||
- **现象**:即使只需要 1 个科目的名称,也会拉取全部 subject/grade 选项。
|
||||
- **后果**:高并发场景下存在 N+1 缓存膨胀风险(虽 React `cache()` 限单次请求内,但单次请求若涉及 1000+ 课程仍冗余)。
|
||||
|
||||
### 2.17 P2:缺少 Suspense 流式渲染(✅ 2026-06-25 已修复)
|
||||
|
||||
- **位置**:所有 RSC 页面
|
||||
- **现象**:admin/teacher/student 页面用 `Promise.all` 一次性等待所有数据,没有 `<Suspense>` 边界,无法流式渲染局部内容。
|
||||
- **违反规则**:「异步数据使用 React Suspense + 骨架屏」「性能:支持流式渲染」。
|
||||
- **后果**:单条慢查询会拖累整页加载时间,用户长时间看到白屏。
|
||||
- **修复说明**:
|
||||
- student 页面拆分为 `MySelectionsLoader`(Suspense)+ `AvailableCoursesLoader`(Suspense),分别对应"我的选课"与"可选课程"
|
||||
- admin 页面拆分为 `StatsCardsLoader`(Suspense)+ `CourseListLoader`(Suspense),统计卡片与列表分离
|
||||
- `StudentSelectionView` 拆分为 `StudentMySelectionsSection` + `StudentAvailableCoursesSection` 两个独立客户端组件
|
||||
- `loading.tsx` 新增 `MySelectionsSkeleton` / `AvailableCoursesSkeleton` / `StatsCardsSkeleton` / `CourseListSkeleton` 分段骨架屏
|
||||
- React `cache()` 自动去重 `getStudentSelections` 调用,两个 Suspense 边界共享同一份数据
|
||||
|
||||
### 2.18 P2:缺少课程先修/容量阈值通知(✅ 2026-06-25 部分修复)
|
||||
|
||||
- **位置**:缺失
|
||||
- **现象**:K12 学校通常需要:
|
||||
- 课程先修要求(如"必须先选 Python 入门才能选 Python 进阶");
|
||||
- 容量阈值通知(如课程满 90% 时通知管理员考虑扩容);
|
||||
- 选课截止前提醒(如截止前 24h 通知未选课学生)。
|
||||
- **后果**:缺少这些企业级功能会降低 K12 学校的运营效率。
|
||||
- **修复说明(容量阈值通知)**:
|
||||
- 新增 [data-access-settings.ts](file:///e:/Desktop/CICD/src/modules/elective/data-access-settings.ts) 中的 `getCapacityNotifyThreshold()`,默认 0.9(90%),可由 `systemSettings` 表配置(key=`capacityNotifyThreshold`,category=`elective`)。
|
||||
- `data-access-operations.ts` 新增 `notifyCapacityThresholdIfNeeded()`:仅当 `newEnrolledCount === Math.ceil(capacity * threshold)` 时触发一次通知(避免每次递增都发通知)。Fire-and-forget 设计:catch 中吞错并 `console.error`,不阻塞主流程。通过 `notifications.sendNotification` 发送给 `course.teacherId`,type=`"warning"`,附带 `actionUrl` 指向课程详情。i18n 标题/内容通过 `next-intl getTranslations("elective")` 翻译。
|
||||
- `selectCourse` 事务成功后调用 `notifyCapacityThresholdIfNeeded`。
|
||||
- **未实施项**:课程先修要求与选课截止前提醒属于更长期规划,本次审计范围内不实施,后续可在 P3 阶段补齐。
|
||||
|
||||
### 2.19 P2:无单元测试(✅ 2026-06-25 已修复)
|
||||
|
||||
- **位置**:`tests/elective/`(不存在)
|
||||
- **现象**:`buildLotteryRankCase`、`parseSchedule`、`isScheduleConflict`、`buildScopeFilter` 等纯函数已被精心设计为可测试,但没有任何测试文件。
|
||||
- **修复说明**:新增 `tests/integration/elective/elective-pure-functions.test.ts`,35 个单测覆盖 `normalizeDay` / `parseSchedule` / `isScheduleConflict` / `buildLotteryRankCase` / `mapCourseRow`;`vitest.config.ts` 添加 `server-only` 别名 stub。
|
||||
- **违反规则**:「可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离;导出清晰的接口类型以便 mock」——架构已就绪,但测试缺失。
|
||||
- **后果**:未来重构无回归保障。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
| 维度 | 行业优秀实践(如 PowerSchool、Veracross、睿睿云、校园钉钉选修) | 当前实现 | 差距影响 |
|
||||
|------|---|---|---|
|
||||
| 角色覆盖 | 4 角色全覆盖:admin 全局管理 / teacher 创建维护 / student 选退课 / parent 查看子女 | 3 角色覆盖,缺 parent | 家长无法监督子女选课,错失家校协同点 |
|
||||
| 时间冲突检测 | 结构化时段编辑器(周几 + 节次),可视化冲突预览 | 纯文本 schedule + 正则解析 | 教师/学生易输入错误格式,冲突检测可能失效 |
|
||||
| 抽签可重跑 | 支持"预抽 + 公示 + 正式抽签",结果可回滚 | 抽完立即 close,不可重抽 | 学校难以应对抽签争议 |
|
||||
| 学分上限 | 按年级/学校可配置 | 全局硬编码 `MAX_CREDIT_PER_TERM=10` | 不同年级无法差异化 |
|
||||
| 选课截止提醒 | 截止前 24h 短信/站内信通知未选课学生 | 无 | 学生错过选课窗口 |
|
||||
| 容量阈值通知 | 满 90% 通知 admin 考虑扩容 | 无 | 热门课程爆满后才发现 |
|
||||
| 课程详情页 | 独立课程详情页 + 教师介绍 + 评价聚合 + 历年选课人数趋势 | 仅卡片展示,无详情页 | 学生选课决策信息不足 |
|
||||
| 选课概览驾驶舱 | admin 数据驾驶舱:选课率、热门科目、班级分布、未选名单 | 仅平铺课程卡片 | admin 缺乏全局视图,决策低效 |
|
||||
| 候补转正通知 | 候补转正时通知学生 | 仅事务内自动转正,无通知 | 学生不知道自己已转正,可能错过上课 |
|
||||
| 课程先修 | 标记先修关系,选课时校验 | 无 | 学生可能跳级选课失败 |
|
||||
| 退课理由 | 退课时要求填写理由 + 期限 | 直接退课,无理由 | 学校无法分析退课原因改进课程 |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(必须立即修复,影响功能可用或安全)
|
||||
|
||||
1. **教师页面跳转修复**:把 `createHref`/`editBaseHref` 改为参数化,teacher 页面传入 `/teacher/elective/...`,并新增 teacher 路由 `/teacher/elective/create` 与 `/teacher/elective/[id]/edit`(或共享 admin 路由但放开教师访问)。
|
||||
2. **新增 parent 选课页面**:复用 `StudentSelectionView` 的只读变体,展示子女的已选/可选课程;通过 `parentId + studentId` 双重校验防止信息泄露。
|
||||
3. **admin/student 页面加 `requirePermission()`**:补齐 `ELECTIVE_READ` 校验,与 teacher 一致。
|
||||
4. **data-access 错误消息 i18n 化**:把 `throw new Error("...")` 改为带 i18n key + 参数的结构化错误,由 actions 层用 `getTranslations("elective")` 翻译后再返回。
|
||||
5. **`export.ts` 移除 `as` 断言**:用类型守卫替代。
|
||||
|
||||
### P1(应在本次实施,影响质量与体验)
|
||||
|
||||
6. **`elective-course-form.tsx` 全量 i18n 化**:替换所有硬编码英文为 i18n 键,新增 `form.*` 翻译键。
|
||||
7. **`<a>` → `<Link>`**:`elective-course-list.tsx` 中 2 处替换为 Next.js `<Link>`。
|
||||
8. **错误边界文案修正**:admin/teacher error.tsx 重试按钮改用 `t("actions.retry")`;title/description 分离为 `errors.title` / `errors.description`。
|
||||
9. **`export.ts` 表头全量 i18n**:新增 `export.statusHeader` / `export.priorityHeader` / `export.selectedAtHeader` / `export.enrolledAtHeader` 翻译键。
|
||||
10. **`getElectiveCourses` 不再静默吞错**:移除 try-catch,让异常冒泡到 RSC 触发 error.tsx。
|
||||
11. **`parseSchedule` 支持完整星期 + 多时段**:重写正则与归一化函数。
|
||||
12. **抽签可重跑**:抽签后保留 `status="open"`,仅更新 `enrolledCount`;增加"已抽签"标记字段或独立的 `lotteryRunAt` 字段。
|
||||
13. **管理员选课概览**:在 admin 页面顶部增加统计卡片网格(总课程数、总选课人数、平均容量使用率、待抽签课程数),复用 `attendance-stats-cards.tsx` 模式。
|
||||
|
||||
### P2(中长期改进,提升企业级能力)
|
||||
|
||||
14. **配置化学分上限**(✅ 2026-06-25 已修复):抽离为 `data-access-settings.ts` 中的 `getElectiveCreditLimit(gradeId?)`,复用 `systemSettings` 表按年级可设,未配置 fallback 到全局默认值 10。
|
||||
15. **Suspense 流式渲染**(✅ 2026-06-25 已修复):student 页面拆分"我的选课"与"可选课程"为两个独立 Suspense 边界,admin 列表与统计卡片分离。
|
||||
16. **容量阈值通知**(✅ 2026-06-25 已修复):选课时若 `enrolledCount >= capacity * threshold`,触发 `notifications` 模块通知教师(type=`"warning"`),阈值通过 `getCapacityNotifyThreshold()` 可配置,默认 0.9。
|
||||
17. **退课期限与理由**(✅ 2026-06-25 已修复):新增 `electiveCourses.dropDeadline`(datetime)与 `courseSelections.dropReason`(varchar 255)字段,`dropCourse` 校验截止时间并抛 `ElectiveBusinessError("dropDeadlinePassed")`,退课对话框可选填理由,trim 后非空才入库。
|
||||
18. **单元测试**(✅ 2026-06-25 已修复):补齐 `parseSchedule` / `isScheduleConflict` / `buildLotteryRankCase` / `buildScopeFilter` / `mapCourseRow` 的单元测试。
|
||||
19. **课程详情页**(✅ 2026-06-25 已修复):新增 `/admin/elective/[id]` 与 `/student/elective/[id]` 详情页。
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
### 5.1 需要修正的过时信息
|
||||
|
||||
`004_architecture_impact_map.md` 与 `005_architecture_data.json` 中 elective 模块章节的「已知问题」存在以下过时项,本次审计已核实并修复:
|
||||
|
||||
| 架构图记录 | 实际情况 | 处理 |
|
||||
|---|---|---|
|
||||
| "❌ P0:3 个读 Action 无调用方" | 实际并不存在这 3 个 Action;页面直接调用 data-access(项目规则允许 `app/` 调用 data-access) | 删除该项 |
|
||||
| "❌ P0:i18n 完全缺失" | i18n 资源完整(zh-CN + en 双语),Server Action 错误消息已 i18n 化 | 修正为「P0:data-access-operations 的 throw 错误消息仍为英文」 |
|
||||
| "❌ P0:错误边界完全缺失(3 个角色目录均无 `error.tsx`)" | 3 个 `error.tsx` 已存在 | 删除该项;改为「P1:error.tsx 文案错误(title=description=unexpected,按钮文案错误)」 |
|
||||
| "⚠️ P1:`elective-course-form.tsx` 存在 `v as "fcfs" | "lottery"` 类型断言" | 已用 `isSelectionMode` 类型守卫替代,无 `as` | 删除该项 |
|
||||
| "⚠️ P1:`elective-course-list.tsx` 存在 `null as never` 类型逃逸" | 当前代码无 `null as never` | 删除该项 |
|
||||
| "⚠️ P1:`buildLotteryRankCase` 未导出,无法单测" | 已 `export function buildLotteryRankCase` | 删除该项 |
|
||||
| "⚠️ P1:`SELECTION_MODE_LABELS` 已定义但表单未复用" | 表单已使用 `isSelectionMode` 守卫 + 直接渲染 `t("selectionMode.fcfs/lottery")` | 删除该项 |
|
||||
|
||||
### 5.2 需要新增的节点
|
||||
|
||||
| 新增项 | 004 章节 | 005 节点 |
|
||||
|---|---|---|
|
||||
| 新增 `parent/elective/page.tsx`(家长查看子女选课) | 2.20 文件清单新增一行 | `appRoutes.parent.elective` 节点 |
|
||||
| 新增 `teacher/elective/create/page.tsx` 与 `teacher/elective/[id]/edit/page.tsx` | 2.20 文件清单新增 2 行 | `appRoutes.teacher.electiveCreate` / `electiveEdit` 节点 |
|
||||
| 新增 `data-access-stats.ts`(管理员选课统计) | 2.20 文件清单新增一行 | `modules.elective.exports.dataAccess` 新增函数 |
|
||||
| 新增 `tests/elective/*.test.ts`(单元测试) | 2.20 文件清单新增测试说明 | 无需 005 节点(测试不属导出) |
|
||||
|
||||
### 5.3 实施完成后的同步动作
|
||||
|
||||
代码修改完成后,将上述变更同步写入:
|
||||
- `docs/architecture/004_architecture_impact_map.md` 第 2.20 节
|
||||
- `docs/architecture/005_architecture_data.json` 的 `modules.elective` 与 `appRoutes` 节点
|
||||
274
docs/architecture/audit/archive/error-book-audit-report.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# 错题本模块审计报告
|
||||
|
||||
> 审计日期:2026-06-24
|
||||
> 审计范围:`src/modules/error-book/` 及 `src/app/(dashboard)/{student,teacher,parent,admin}/error-book/`
|
||||
> 审计依据:项目规则 `docs/standards/coding-standards.md`、架构影响地图 `004`/`005`
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布
|
||||
|
||||
错题本模块位于 `src/modules/error-book/`,包含以下文件:
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `actions.ts` | 341 | 9 个 Server Actions(列表/详情/统计/增删改/复习/采集) |
|
||||
| `data-access.ts` | **1029** | 数据访问层(学生 CRUD + 教师/管理员分析查询 + 跨模块接口) |
|
||||
| `data-access-collection.ts` | 170 | 自动采集逻辑(考试/作业错题采集) |
|
||||
| `schema.ts` | 51 | 4 个 Zod 验证 schema |
|
||||
| `types.ts` | 245 | 11 个类型定义 + 状态映射常量 |
|
||||
| `sm2-algorithm.ts` | 177 | SM-2 间隔重复算法(纯函数) |
|
||||
| `sm2-algorithm.test.ts` | - | 39 个单元测试 |
|
||||
| `components/` | 17 个文件 | UI 组件(学生卡片/列表/筛选/详情/图表等) |
|
||||
|
||||
### 1.2 路由分布
|
||||
|
||||
| 路由 | 角色 | 文件完整性 |
|
||||
|------|------|-----------|
|
||||
| `/student/error-book` | 学生 | page + loading + error ✅ |
|
||||
| `/teacher/error-book` | 教师 | page + loading + error ✅ |
|
||||
| `/parent/error-book` | 家长 | page + loading + error ✅ |
|
||||
| `/admin/error-book` | 管理员 | page + loading + error ✅ |
|
||||
|
||||
### 1.3 架构图覆盖情况
|
||||
|
||||
架构影响地图 `004_architecture_impact_map.md` 第 2.28 节已覆盖该模块,记录了:
|
||||
- 模块职责、导出函数、权限点、DataScope 行级权限
|
||||
- SM-2 算法说明、自动采集机制
|
||||
- 文件清单、路由清单、数据库表
|
||||
|
||||
**但存在以下不一致**:
|
||||
1. `005_architecture_data.json` 的 `uses.shared` 仍列出 `examSubmissions`、`submissionAnswers`、`homeworkSubmissions` 等表,实际上这些已通过跨模块 data-access 接口访问(`data-access-collection.ts`),不再直接查询
|
||||
2. JSON 未记录 `adaptive-practice` 依赖,但 `error-book-detail-dialog.tsx` 直接 import 了 `createPracticeSessionAction`
|
||||
3. JSON 未记录 `ai` 模块依赖,但 `error-book-detail-dialog.tsx` 直接 import 了 `AiErrorBookAnalysis` 组件
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 2.1 【P0】跨模块直接依赖(违反三层架构规则)
|
||||
|
||||
**项目规则**:「模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表」「该模块必须作为独立功能单元」
|
||||
|
||||
| 位置 | 违规内容 | 原因 | 后果 |
|
||||
|------|---------|------|------|
|
||||
| [error-book-detail-dialog.tsx](file:///e:/Desktop/CICD/src/modules/error-book/components/error-book-detail-dialog.tsx#L40) | `import { AiErrorBookAnalysis } from "@/modules/ai/components/ai-error-book-analysis"` | error-book 组件直接 import ai 模块组件 | 模块强耦合,无法独立测试/部署 |
|
||||
| [error-book-detail-dialog.tsx](file:///e:/Desktop/CICD/src/modules/error-book/components/error-book-detail-dialog.tsx#L41) | `import { createPracticeSessionAction } from "@/modules/adaptive-practice/actions"` | error-book 组件直接 import adaptive-practice 的 Server Action | 模块强耦合,违反依赖注入原则 |
|
||||
| [add-error-book-dialog.tsx](file:///e:/Desktop/CICD/src/modules/error-book/components/add-error-book-dialog.tsx#L26) | `import { getQuestionsAction } from "@/modules/questions/actions"` | error-book 组件直接 import questions 模块的 Action | 应通过 data-access 或注入接口调用 |
|
||||
|
||||
### 2.2 【P0】i18n 国际化严重遗漏
|
||||
|
||||
**项目规则**:「所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键」
|
||||
|
||||
虽然 `error-book.json` 翻译文件已存在(123 行),但**大量组件仍使用硬编码中文**:
|
||||
|
||||
| 组件 | 硬编码文本示例 | 行数 |
|
||||
|------|--------------|------|
|
||||
| `error-book-item-card.tsx` | "题目内容"、"难度"、"掌握度"、"复习 X 次"、"需复习"、"下次"、"未学习"、"入门"... | 10+ 处 |
|
||||
| `error-book-detail-dialog.tsx` | "题目"、"我的答案"、"正确答案"、"AI 智能分析"、"复习自评"、"学习笔记"、"错误原因标签"、"复习历史"、"归档"、"删除"、"添加于"... | 20+ 处 |
|
||||
| `error-book-filters.tsx` | "搜索笔记内容..."、"状态"、"来源"、"复习"、"全部状态"、"全部来源"... | 8+ 处 |
|
||||
| `add-error-book-dialog.tsx` | "手动添加"、"添加错题"、"选择题目"、"学习笔记(可选)"、"错误原因标签"、"取消"、"添加"... | 10+ 处 |
|
||||
| `review-buttons.tsx` | "重来"、"困难"、"良好"、"简单" 及描述文案(i18n 已有翻译但未使用) | 8 处 |
|
||||
| `error-book-stats-cards.tsx` | "错题总数"、"待学习"、"学习中"、"已掌握"、"待复习" 及描述 | 10 处 |
|
||||
| `analytics-stats-cards.tsx` | "覆盖学生"、"错题总数"、"平均掌握率"、"待复习"、"涉及知识点" 及子文案 | 10+ 处 |
|
||||
| `subject-tabs.tsx` | "全部学科"、"待复习" | 2 处 |
|
||||
| `class-filter.tsx` | "全部班级"、"错题"、"待复习" | 3 处 |
|
||||
| `top-wrong-questions.tsx` | "高频错题"、"高频错题 Top 10"、"暂无高频错题"、"人错"、"人已掌握"、"掌握率" | 8 处 |
|
||||
| `knowledge-point-weakness-chart.tsx` | "薄弱知识点 Top X"、"错题数"、"所属章节"、"错题数"、"已掌握"、"掌握率" | 8+ 处 |
|
||||
| `chapter-weakness-chart.tsx` | "章节错题分布(哪些课在错)"、"错题数"、"已掌握"、"掌握率"、"知识点数"、"薄弱知识点" | 10+ 处 |
|
||||
| `class-error-bar-chart.tsx` | "各班级错题数对比"、"错题总数"、"学生数"、"人均错题"、"平均掌握率"、"待复习" | 6+ 处 |
|
||||
| `subject-distribution-chart.tsx` | "各学科错题分布"、"错题数"、"已掌握"、"掌握率" | 4+ 处 |
|
||||
| `grouped-student-error-table.tsx` | "未分班"、"人"、"人有错题"、"错题总数"、"平均掌握率"、"学生" 及表头 | 15+ 处 |
|
||||
| `class-error-overview.tsx` | "覆盖学生"、"错题总数"、"平均掌握率"、"薄弱知识点"、"学科错题分布" 等 | 15+ 处 |
|
||||
| `error-book-list.tsx` | "错题本为空"、"查看详情" | 2 处 |
|
||||
| `teacher/error-book/page.tsx` | "错题分析"、"按学科、班级查看学生的错题统计与薄弱知识点" 等 | 8+ 处 |
|
||||
|
||||
**总计约 150+ 处硬编码中文文本**,违反 i18n 规则。
|
||||
|
||||
### 2.3 【P0】类型安全问题(违反 TypeScript 严格模式)
|
||||
|
||||
**项目规则**:「禁止 `any`、禁止 `as` 断言(除类型收窄外)」
|
||||
|
||||
| 文件 | 行号 | 违规代码 | 类型 |
|
||||
|------|------|---------|------|
|
||||
| `data-access.ts` | 77 | `row.sourceType as ErrorBookItem["sourceType"]` | `as` 断言 |
|
||||
| `data-access.ts` | 82 | `row.knowledgePointIds as string[] \| null` | `as` 断言 |
|
||||
| `data-access.ts` | 90 | `row.errorTags as string[] \| null` | `as` 断言 |
|
||||
| `data-access.ts` | 133 | `or(...)!` | 非空断言 |
|
||||
| `data-access.ts` | 175 | `row as unknown as Parameters<typeof mapRowToItem>[0]` | 双重断言 |
|
||||
| `data-access.ts` | 222 | 同上 | 双重断言 |
|
||||
| `data-access.ts` | 657 | `row.knowledgePointIds as string[] \| null` | `as` 断言 |
|
||||
| `data-access.ts` | 792 | 同上 | `as` 断言 |
|
||||
| `actions.ts` | 63 | `params.status as "new" \| "learning" \| ...` | `as` 断言 |
|
||||
| `actions.ts` | 69 | `params.sourceType as "exam" \| "homework" \| ...` | `as` 断言 |
|
||||
| `error-book-item-card.tsx` | 31 | `node as Record<string, unknown>` | `as` 断言 |
|
||||
| `error-book-detail-dialog.tsx` | 61 | `content as Record<string, unknown>` | `as` 断言 |
|
||||
| `add-error-book-dialog.tsx` | 48 | `node as Record<string, unknown>` | `as` 断言 |
|
||||
| `top-wrong-questions.tsx` | 26 | `node as Record<string, unknown>` | `as` 断言 |
|
||||
| `knowledge-point-weakness-chart.tsx` | 77 | `payload as unknown as {...}` | 双重断言 |
|
||||
| `chapter-weakness-chart.tsx` | 74 | `payload as unknown as {...}` | 双重断言 |
|
||||
| `class-error-bar-chart.tsx` | 76 | `payload as unknown as {...}` | 双重断言 |
|
||||
| `subject-distribution-chart.tsx` | 77 | `payload as unknown as {...}` | 双重断言 |
|
||||
|
||||
### 2.4 【P1】data-access.ts 超过 1000 行硬性上限
|
||||
|
||||
**项目规则**:「硬性上限:任何文件不超过 1000 行,超过必须拆分」
|
||||
|
||||
`data-access.ts` 当前 **1029 行**,超出硬性上限。该文件混合了:
|
||||
- 学生端 CRUD(`getErrorBookItems`、`createErrorBookItem`、`recordReview` 等)
|
||||
- 教师/管理员分析查询(`getStudentErrorBookSummaries`、`getKnowledgePointWeakness`、`getChapterWeakness`、`getClassErrorOverviews`、`getSubjectErrorOverviews` 等)
|
||||
- 跨模块查询接口(`getStudentIdsByClassIdList`、`getAllStudentIds`)
|
||||
|
||||
### 2.5 【P1】性能问题:全量查询 + JS 端聚合
|
||||
|
||||
| 函数 | 问题 | 影响 |
|
||||
|------|------|------|
|
||||
| `getErrorBookStats` | 查询该学生**所有**错题行到内存,再 JS 循环统计 | 学生错题多时内存/CPU 浪费 |
|
||||
| `getStudentErrorBookSummaries` | 查询所有学生的所有错题行,再 JS 聚合 | 班级学生多时性能差 |
|
||||
| `getKnowledgePointWeakness` | 查询所有错题行,JS 展开知识点数组再统计 | 同上 |
|
||||
| `getChapterWeakness` | 同上 | 同上 |
|
||||
| `getSubjectErrorDistribution` | 同上 | 同上 |
|
||||
| `getClassErrorOverviews` | 同上 | 同上 |
|
||||
| `getSubjectErrorOverviews` | 同上 | 同上 |
|
||||
| `getTopWrongQuestionsByStudentIds` | 同上 | 同上 |
|
||||
| `admin/page.tsx` | `allStudentIds.slice(0, 500)` 硬编码限制,无分页 | 超过 500 学生时数据不完整 |
|
||||
|
||||
**应使用 SQL `GROUP BY` + `COUNT` 聚合查询**,避免全量加载到内存。
|
||||
|
||||
### 2.6 【P1】a11y 可访问性缺失
|
||||
|
||||
**项目规则**:「可访问性(a11y):语义化标签、ARIA 属性、键盘导航」
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| `grouped-student-error-table.tsx:162` | 使用 `<a href>` 而非 Next.js `<Link>`(违反项目记忆中的 Link 规范) |
|
||||
| `subject-tabs.tsx` | `<button>` 缺少 `role="tab"` / `aria-selected` / `aria-controls` |
|
||||
| `class-filter.tsx` | 同上 |
|
||||
| 所有图表组件 | 缺少 `aria-label` 描述图表内容 |
|
||||
| `review-buttons.tsx` | 按钮缺少 `aria-label` 描述操作 |
|
||||
| `grouped-student-error-table.tsx:96` | 可展开行缺少 `aria-expanded` |
|
||||
| `error-book-detail-dialog.tsx` | `<textarea>` 缺少 `aria-label` |
|
||||
|
||||
### 2.7 【P1】错误边界不完整
|
||||
|
||||
**项目规则**:「每个独立的数据区块必须用 React Error Boundary 包裹」
|
||||
|
||||
| 位置 | 问题 |
|
||||
|------|------|
|
||||
| `parent/error-book/page.tsx` | 无 `Suspense` 包裹,整个页面同步渲染 |
|
||||
| 所有页面 | 仅页面级 `error.tsx`,各数据区块(统计卡片/图表/表格)无独立 Error Boundary |
|
||||
| 图表组件 | recharts 渲染失败时无 fallback |
|
||||
|
||||
### 2.8 【P2】组件复用问题
|
||||
|
||||
| 问题 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| `extractQuestionPreview` 函数重复 3 次 | `error-book-item-card.tsx`、`add-error-book-dialog.tsx`、`top-wrong-questions.tsx` | 应提取到 shared 工具函数 |
|
||||
| `extractQuestionText` 函数重复 | `error-book-detail-dialog.tsx` | 与上面类似但实现不同 |
|
||||
| `MASTERY_LEVEL_LABELS` 硬编码 | `error-book-item-card.tsx:47` | 应使用 i18n |
|
||||
| `QUESTION_TYPE_LABEL` 硬编码 | `top-wrong-questions.tsx:35` | 应使用 i18n |
|
||||
| `class-error-overview.tsx` 导出 `StudentErrorTable` | 似乎已被 `GroupedStudentErrorTable` 取代 | 死代码 |
|
||||
| `COMMON_ERROR_TAGS` 硬编码中文 | `types.ts:55` | 应使用 i18n 键 |
|
||||
|
||||
### 2.9 【P2】数据服务未抽象(不可测试)
|
||||
|
||||
**项目规则**:「可测试性:数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离」
|
||||
|
||||
当前组件直接 import `actions` 和 `data-access`:
|
||||
- `error-book-detail-dialog.tsx` 直接 import `archiveErrorBookItemAction`、`deleteErrorBookItemAction`、`updateErrorBookNoteAction`
|
||||
- `add-error-book-dialog.tsx` 直接 import `createErrorBookItemAction`
|
||||
- `review-buttons.tsx` 直接 import `reviewErrorBookItemAction`
|
||||
|
||||
**无法在不启动整个应用的情况下 mock 这些依赖**,违反可测试性原则。
|
||||
|
||||
### 2.10 【P2】权限校验位置不统一
|
||||
|
||||
`getAllStudentIds()` 在 data-access 层通过 `roles.name === "student"` 查询,虽然不在前端,但角色名字符串硬编码在查询中。应使用 `shared/types/permissions` 中的角色常量。
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
### 3.1 与优秀 K12 产品的差距
|
||||
|
||||
| 功能 | 我们 | 智学网 | 猿题库 | 钉钉教育 | 差距影响 |
|
||||
|------|------|--------|--------|---------|---------|
|
||||
| 智能复习队列 | ✅ SM-2 | ✅ | ✅ | ❌ | 基本持平 |
|
||||
| 复习提醒通知 | ❌ | ✅ 推送 | ✅ 推送 | ✅ | 学生不知道何时复习 |
|
||||
| 错题趋势图 | ❌(类型已定义未实现) | ✅ | ✅ | ❌ | 无法看到进步趋势 |
|
||||
| 导出/打印错题 | ❌ | ✅ PDF | ✅ PDF | ❌ | 无法离线复习 |
|
||||
| 批量操作 | ❌ | ✅ | ✅ | ❌ | 管理大量错题效率低 |
|
||||
| 班级平均对比 | ❌ | ✅ | ✅ | ❌ | 学生不知道自己水平 |
|
||||
| 复习日历视图 | ❌ | ✅ | ✅ | ❌ | 无法直观看到复习安排 |
|
||||
| 错题来源详情跳转 | ❌ | ✅ | ✅ | ❌ | 无法回看原试卷/作业 |
|
||||
| 知识点掌握度雷达图 | ❌ | ✅ | ✅ | ❌ | 无法多维度看薄弱点 |
|
||||
| 游戏化激励(连续复习天数) | ❌ | ✅ | ✅ | ❌ | 学生缺乏复习动力 |
|
||||
| 智能推题(基于错题变式) | ✅(已接入 adaptive-practice) | ✅ | ✅ | ❌ | 基本持平 |
|
||||
|
||||
### 3.2 UI/UX 差距
|
||||
|
||||
| 差距 | 说明 | 影响角色 |
|
||||
|------|------|---------|
|
||||
| 学生端缺少复习仪表盘 | 当前只有列表+筛选,无"今日待复习"独立视图 | 学生 |
|
||||
| 教师端缺少学生个体下钻 | 点击学生只能跳转带参数,无学生错题详情面板 | 教师 |
|
||||
| 家长端无子女切换 | 多子女时用卡片展示,无 Tab 切换对比 | 家长 |
|
||||
| 管理员端无年级维度 | 只有全校维度,无年级/班级下钻 | 管理员 |
|
||||
| 无骨架屏一致性 | 各页面 loading.tsx 结构不一致 | 全部 |
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急,影响架构合规与安全)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P0-1 | 跨模块直接依赖 | 将 `AiErrorBookAnalysis`、`createPracticeSessionAction`、`getQuestionsAction` 改为通过 props/Context 注入,error-book 模块不直接 import 其他业务模块 |
|
||||
| P0-2 | i18n 硬编码(150+ 处) | 全部提取为翻译键,扩展 `error-book.json` 翻译文件 |
|
||||
| P0-3 | 类型安全(18 处 as 断言) | 使用类型守卫替代 `as`,图表 tooltip 使用泛型组件 |
|
||||
|
||||
### P1(重要,影响性能与可访问性)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P1-1 | data-access.ts 超 1000 行 | 拆分为 `data-access.ts`(学生 CRUD)+ `data-access-analytics.ts`(教师/管理员分析) |
|
||||
| P1-2 | 全量查询 + JS 聚合 | 改用 SQL `GROUP BY` + `COUNT` 聚合 |
|
||||
| P1-3 | a11y 缺失 | 添加 ARIA 属性、语义化标签、`<Link>` 替代 `<a>` |
|
||||
| P1-4 | 错误边界不完整 | 为各数据区块添加 Error Boundary |
|
||||
| P1-5 | admin 无分页 | 实现分页查询或虚拟滚动 |
|
||||
|
||||
### P2(优化,提升可维护性与用户体验)
|
||||
|
||||
| 编号 | 问题 | 改进方向 |
|
||||
|------|------|---------|
|
||||
| P2-1 | 重复函数 | 提取 `extractQuestionPreview` 到 `shared/lib/question-content.ts`(已存在) |
|
||||
| P2-2 | 数据服务未抽象 | 定义 `ErrorBookService` 接口,通过 Context 注入 |
|
||||
| P2-3 | 死代码 | 删除 `class-error-overview.tsx` 中未使用的 `StudentErrorTable` |
|
||||
| P2-4 | 缺失功能 | 错题趋势图、复习提醒、导出、批量操作(中长期) |
|
||||
| P2-5 | 角色字符串硬编码 | `getAllStudentIds` 使用角色常量 |
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
### 5.1 需要修改的节点
|
||||
|
||||
1. **`005_architecture_data.json` → `error-book.uses.shared`**:
|
||||
- 移除 `db.schema.examSubmissions`、`db.schema.submissionAnswers`、`db.schema.homeworkSubmissions`、`db.schema.homeworkAnswers`、`db.schema.examQuestions`、`db.schema.homeworkAssignmentQuestions`(这些已通过跨模块 data-access 访问)
|
||||
|
||||
2. **`005_architecture_data.json` → `error-book.dependsOn`**:
|
||||
- 新增 `ai`(error-book-detail-dialog 直接依赖 ai 组件)
|
||||
- 新增 `adaptive-practice`(error-book-detail-dialog 直接依赖其 Action)
|
||||
- 新增 `questions`(add-error-book-dialog 直接依赖其 Action)—— 注意:questions 已在 dependsOn 中,但 uses 中记录的是 `actions.getQuestionsAction` 而非 data-access
|
||||
|
||||
3. **`004_architecture_impact_map.md` 第 2.28 节**:
|
||||
- 文件清单中 `data-access.ts` 行数更新为拆分后的两个文件
|
||||
- 依赖关系新增 ai / adaptive-practice 的直接依赖说明(标注为"待解耦")
|
||||
|
||||
### 5.2 架构图待补充项
|
||||
|
||||
- 拆分后的 `data-access-analytics.ts` 文件信息
|
||||
- `ErrorBookService` 接口定义(如实施 P2-2)
|
||||
- i18n 翻译文件结构更新
|
||||
475
docs/architecture/audit/archive/exams-audit-report.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# 考试(exams)模块审计报告
|
||||
|
||||
> 审计时间:2026-06-25
|
||||
> 审计范围:`src/modules/exams/**`(35+ 文件)+ `src/app/(dashboard)/teacher/exams/**`(10 个页面)+ 跨模块依赖面
|
||||
> 审计依据:`docs/architecture/004_architecture_impact_map.md`、`docs/architecture/005_architecture_data.json`、项目 `project_rules.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、现有实现概要
|
||||
|
||||
### 1.1 文件分布与体量
|
||||
|
||||
exams 模块按职责已做较细粒度拆分,体量基本符合规范:
|
||||
|
||||
| 子目录/文件 | 行数(参考架构图) | 职责 |
|
||||
|-------------|------|------|
|
||||
| `actions.ts` | 633 | 11 个核心 Server Action(已从 1525 行拆分) |
|
||||
| `actions-helpers.ts` | 96 | 跨 Action 共享纯函数(prepareExamCreateContext 等) |
|
||||
| `actions-rich-editor.ts` | 250 | 富文本编辑器 Server Action(create/update) |
|
||||
| `ai-pipeline/auto-mark.ts` | 356 | AI 自动标记 Server Action + 纯转换函数 |
|
||||
| `ai-pipeline/{index,parse,request,structure}.ts` | — | AI 调用/解析/结构化 |
|
||||
| `data-access.ts` | 542 | 考试 CRUD(已从 1036 行拆分) |
|
||||
| `data-access-cross-module.ts` | 511 | 13 个跨模块查询/写接口 |
|
||||
| `data-access-error-collection.ts` | — | 错题采集相关跨模块接口 |
|
||||
| `stats-service.ts` | 158 | 考试分析数据聚合 |
|
||||
| `types.ts` | 93 | 类型定义 |
|
||||
| `utils/normalize-structure.ts` | 57 | exam.structure 运行时归一化 |
|
||||
| `components/` | 24 个文件 | 表单/组卷/预览/分析/卡片/筛选/表格 |
|
||||
| `editor/` | 14 个文件 | Tiptap 富文本编辑器(extensions/utils/转换) |
|
||||
| `hooks/` | 4 个文件 | use-exam-preview 主组合器 + 3 个子 Hook |
|
||||
|
||||
**架构图覆盖情况**:004/005 已记录 exams 模块的职责、依赖、被依赖、文件清单、P0/P1 修复历史、V3 增强项。本次审计对照架构图核对,覆盖基本完整,但以下细节需补全(见第五节):
|
||||
- `data-access-error-collection.ts` 未在 005 JSON 的 modules.exams.exports 中列出
|
||||
- `utils/normalize-structure.ts` 已记录但未在 005 的 dependencyMatrix 中明确标注被 `[id]/build/page.tsx` 与 `[id]/edit-rich/page.tsx` 引用
|
||||
|
||||
### 1.2 主要数据流
|
||||
|
||||
- **创建**:`/teacher/exams/create` → `createExamAction` → `persistExamDraft` → `db.insert(exams)`
|
||||
- **AI 创建**:`/teacher/exams/create` → `createAiExamAction` → `loadAiDraftQuestionsAndStructure` → `persistAiGeneratedExamDraft` → 通过 `questions/data-access.createQuestionWithRelations` 创建题目 → 事务写 exams + examQuestions
|
||||
- **富文本创建**:`/teacher/exams/new` → `createExamFromRichEditorAction` → `editorDocToStructure` → `persistAiGeneratedExamDraft`
|
||||
- **组卷**:`/teacher/exams/[id]/build` → `ExamAssembly` + `getExamById`
|
||||
- **预览**:`previewAiExamAction` / `getExamPreviewAction`
|
||||
- **分析**:`/teacher/exams/[id]/analytics` → `getExamAnalytics`(聚合 homework 提交数据)
|
||||
|
||||
### 1.3 跨模块依赖(合规项)
|
||||
|
||||
以下跨模块调用均通过对方 data-access,符合三层架构规则:
|
||||
- `questions/data-access.createQuestionWithRelations`(P0-1 已修复)
|
||||
- `classes/data-access.getClassGradeIdsByClassIds`(P0-2 已修复)
|
||||
- `school/data-access.{getSubjectNameById,getGradeNameById,getSubjectOptions,getGradeOptions}`(P1-1 已修复)
|
||||
- `homework/data-access.{getHomeworkAssignmentsByExamId,getGradedSubmissionsByExamId}`(V3-8 新增)
|
||||
- `homework/data-access-utils.getQuestionText`
|
||||
|
||||
### 1.4 已修复的历史问题(架构图记录)
|
||||
|
||||
P0-1/P0-2/P0-4/P0-8/P1-1 等历史违规已修复,详见 004 文档第 2.2 节。
|
||||
|
||||
---
|
||||
|
||||
## 二、现存问题与原因分析
|
||||
|
||||
### 🔴 2.1【架构违规·P0】跨模块直接 JOIN questions 表
|
||||
|
||||
**位置**:[data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L4-L5) 第 4 行 import、第 480-490 行 `getExamForGradeEntry`
|
||||
|
||||
**问题**:
|
||||
```
|
||||
第 4 行:import { exams, examQuestions, examSubmissions, submissionAnswers, questions } from "@/shared/db/schema"
|
||||
第 488 行:.innerJoin(questions, eq(examQuestions.questionId, questions.id))
|
||||
```
|
||||
|
||||
`getExamForGradeEntry` 为了获取题目 `type` 字段,直接 JOIN 了 questions 模块的核心表 `questions`。
|
||||
|
||||
**违反规则**:项目规则"模块间只能通过对方 data-access 通信,**禁止跨模块直接查询数据库表**"。
|
||||
|
||||
**原因**:成绩录入表格表头需要题目类型,但实现时未在 questions 模块暴露按 ID 批量获取类型的接口,于是直接 JOIN。
|
||||
|
||||
**直接后果**:questions 模块若重构表结构(如将 type 拆分到独立表),exams 模块会编译失败或运行时错误;模块封装性被破坏,违反可测试性与可替换性。
|
||||
|
||||
---
|
||||
|
||||
### 🟢 2.2【已确认合规】submissionAnswers 表归属与直查
|
||||
|
||||
**位置**:
|
||||
- [data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L4) 导入 `submissionAnswers`
|
||||
- [data-access-cross-module.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-cross-module.ts#L212-L218) `getExamSubmissionWithAnswers` 直查 `submissionAnswers`
|
||||
- [data-access-error-collection.ts](file:///e:/Desktop/CICD/src/modules/exams/data-access-error-collection.ts#L6) 导入并查询 `submissionAnswers`(第 62-69 行)
|
||||
|
||||
**结论**:经核对 `src/shared/db/schema.ts:575-578`,`submissionAnswers` 表的 `submissionId` 外键引用 `examSubmissions.id`,**该表属于 exams 模块自身域**(exam submissions 的答题记录)。exams 模块查询自己的表合规,`getExamSubmissionWithAnswers` 与 `getExamSubmissionDataForErrorCollection` 通过 data-access-cross-module 暴露给 diagnostic/error-book 模块调用,符合"模块间通过对方 data-access 通信"规则。
|
||||
|
||||
**无违规,无需修复。**
|
||||
|
||||
---
|
||||
|
||||
### 🟠 2.3【i18n 缺失·P1】11 个组件未接入 useTranslations
|
||||
|
||||
**位置**:
|
||||
|
||||
| 文件 | 硬编码样本 |
|
||||
|------|-----------|
|
||||
| [components/exam-card.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-card.tsx#L78-L91) | "Lvl"、"min"、"pts"、"Questions" |
|
||||
| [components/exam-filters.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-filters.tsx#L33-L59) | "Search exams..."、"Status"、"Any Status"、"Draft"、"Published"、"Archived"、"Difficulty"、"Easy (1)" 等 |
|
||||
| [components/exam-preview-dialog.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-preview-dialog.tsx#L89-L199) | "Section"、"未命名题目"、"未命名子题"、"Exam Preview"、"Generating preview..."、"完整试卷预览"、"题 · 科目 · 年级 · 分钟 · 总分"、"No preview available"、"Confirm & Create" |
|
||||
| [components/exam-viewer.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/exam-viewer.tsx#L95-L197) | "Section"、"Group"、"Score:"、"No questions available." |
|
||||
| [components/question-options-editor.tsx](file:///e:/Desktop/CICD/src/modules/exams/components/question-options-editor.tsx) | 选项编辑器中文硬编码 |
|
||||
| [editor/extensions/blank-node.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/blank-node.tsx) | aria-label="填空" |
|
||||
| [editor/extensions/group-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/group-block.tsx) | placeholder 与统计文案硬编码 |
|
||||
| [editor/extensions/question-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/question-block.tsx) | 题型 `<option>` 与 "分" 硬编码 |
|
||||
| [editor/extensions/section-block.tsx](file:///e:/Desktop/CICD/src/modules/exams/editor/extensions/section-block.tsx) | "层级/卷/部分/分卷" 硬编码 |
|
||||
|
||||
**违反规则**:项目规则"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键";硬约束"All user-visible text must be i18n-adapted using next-intl with translation keys extracted"。
|
||||
|
||||
**原因**:富文本编辑器 extensions 与早期组件(exam-card/exam-filters/exam-preview-dialog)在 i18n 改造前已存在,后续 i18n 改造未覆盖到。
|
||||
|
||||
**直接后果**:
|
||||
- 多语言环境(en)下用户看到中英混杂文本,体验严重劣化
|
||||
- 无法通过翻译文件统一管理文案,难以维护
|
||||
- exam-card 在 all 列表页是高频可见组件,影响首屏专业度
|
||||
|
||||
---
|
||||
|
||||
### 🟠 2.4【i18n 缺失·P1】Server Action 返回消息绕过 i18n
|
||||
|
||||
**位置**:
|
||||
|
||||
| 文件:行号 | 硬编码消息 |
|
||||
|-----------|-----------|
|
||||
| [actions-rich-editor.ts:41](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L41) | "标题不能为空" |
|
||||
| [actions-rich-editor.ts:49,55](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L49) | "试卷内容不能为空" |
|
||||
| [actions-rich-editor.ts:123,202](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L123) | "试卷内容格式无效"(safeJsonParse 兜底参数) |
|
||||
| [actions-rich-editor.ts:125,204](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L125) | "试卷内容解析失败" |
|
||||
| [actions-rich-editor.ts:169](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L169) | "试卷草稿已创建" |
|
||||
| [actions-rich-editor.ts:211](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L211) | "只能更新自己创建的试卷" |
|
||||
| [actions-rich-editor.ts:278](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L278) | "试卷已更新" |
|
||||
| [actions.ts:161,250,465](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L161) | "题目数据格式无效"(safeJsonParse 兜底) |
|
||||
| [actions.ts:466](file:///e:/Desktop/CICD/src/modules/exams/actions.ts#L466) | "试卷结构数据格式无效" |
|
||||
| [ai-pipeline/auto-mark.ts:30](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/auto-mark.ts#L30) | "试卷文本不能为空"(schema message) |
|
||||
| [ai-pipeline/auto-mark.ts:386](file:///e:/Desktop/CICD/src/modules/exams/ai-pipeline/auto-mark.ts#L386) | "AI 自动标记完成" |
|
||||
| [stats-service.ts:136](file:///e:/Desktop/CICD/src/modules/exams/stats-service.ts#L136) | "(无题目文本)" |
|
||||
| [actions-helpers.ts:65](file:///e:/Desktop/CICD/src/modules/exams/actions-helpers.ts#L65) | "Invalid form data" |
|
||||
|
||||
**违反规则**:同 2.3。`actions.ts` 主体已使用 `getTranslations("examHomework.exam.actionMessages")`,但 `actions-rich-editor.ts` 与 `ai-pipeline/auto-mark.ts` 完全未接入,存在 i18n 一致性破口。
|
||||
|
||||
**原因**:这两个文件是从 actions.ts 拆分出来的新文件,拆分时未同步迁移 i18n 模式。
|
||||
|
||||
**直接后果**:富文本编辑器与 AI 自动标记的错误/成功提示在非中文环境下显示中文,破坏产品一致性。
|
||||
|
||||
---
|
||||
|
||||
### 🟠 2.5【路由边界缺失·P1】部分路由缺 loading.tsx / error.tsx
|
||||
|
||||
**位置**:[src/app/(dashboard)/teacher/exams/](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/)
|
||||
|
||||
| 路由 | loading.tsx | error.tsx |
|
||||
|------|------------|----------|
|
||||
| `all/` | ✅ | ❌ |
|
||||
| `create/` | ✅ | ❌ |
|
||||
| `new/` | ❌ | ❌ |
|
||||
| `[id]/build/` | ✅ | ✅ |
|
||||
| `[id]/edit-rich/` | ❌ | ❌ |
|
||||
| `[id]/analytics/` | ❌ | ❌ |
|
||||
| `[id]/proctoring/` | ✅ | ✅ |
|
||||
|
||||
**违反规则**:硬约束"All student routes must include loading.tsx and error.tsx for error boundaries"(项目内存中虽针对 student 路由,但企业级规范同样适用于 teacher 路由);规则"每个独立的数据区块必须用 React Error Boundary 包裹"、"异步数据使用 React Suspense + 骨架屏"。
|
||||
|
||||
**原因**:路由按需添加 loading/error,未系统化覆盖。
|
||||
|
||||
**直接后果**:
|
||||
- 编辑器页面(edit-rich)加载 Tiptap 较慢,无骨架屏会白屏
|
||||
- 分析页(analytics)聚合查询慢,无 loading 体验差
|
||||
- 任一页面抛错会冒泡到顶层 dashboard error boundary,无法精确定位
|
||||
|
||||
---
|
||||
|
||||
### 🟡 2.6【类型安全·P2】10 处 `as` 类型断言(非 unknown 收窄)
|
||||
|
||||
**位置**:
|
||||
|
||||
| 文件:行号 | 断言 | 说明 |
|
||||
|-----------|------|------|
|
||||
| [editor/editor-to-structure.ts:101](file:///e:/Desktop/CICD/src/modules/exams/editor/editor-to-structure.ts#L101) | `: "single_choice") as RichQuestionType` | 字符串字面量断言为联合类型 |
|
||||
| [editor/exam-nodes-to-editor-doc.ts:38](file:///e:/Desktop/CICD/src/modules/exams/editor/exam-nodes-to-editor-doc.ts#L38) | 同上 | 同上 |
|
||||
| [editor/selection-toolbar.tsx:213,215](file:///e:/Desktop/CICD/src/modules/exams/editor/selection-toolbar.tsx#L213) | `slice.content.toJSON() as JSONContent[]` | ProseMirror→Tiptap 类型 |
|
||||
| [editor/exam-rich-editor.tsx:158,174](file:///e:/Desktop/CICD/src/modules/exams/editor/exam-rich-editor.tsx#L158) | `editor.getJSON() as EditorJSONContent` | Tiptap 内部类型断言 |
|
||||
| [components/exam-data-table.tsx:39](file:///e:/Desktop/CICD/src/modules/exams/components/exam-data-table.tsx#L39) | `params as Record<...>` | 不安全参数断言 |
|
||||
| [components/exam-form.tsx:40](file:///e:/Desktop/CICD/src/modules/exams/components/exam-form.tsx#L40) | `zodResolver(formSchema) as Resolver<ExamFormValues>` | zodResolver 返回类型断言 |
|
||||
| [actions-rich-editor.ts:147,230](file:///e:/Desktop/CICD/src/modules/exams/actions-rich-editor.ts#L147) | `q.type as "single_choice" | "multiple_choice" | "text" | "judgment"` | 字符串断言为联合类型 |
|
||||
| [edit-rich/page.tsx:64](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx#L64) | `structureToEditorDoc(editorDoc) as EditorJSONContent` | 类型断言 |
|
||||
|
||||
**违反规则**:项目规则"禁止 `as` 断言(除从 `unknown` 转换或测试中,需注释原因)"。
|
||||
|
||||
**原因**:Tiptap/ProseMirror 类型系统与项目类型边界处缺类型守卫;`RichQuestionType` 联合类型的字符串字面量缺运行时校验函数。
|
||||
|
||||
**直接后果**:若 AI 返回未预期的 type 值(如 "essay"),`as` 断言会让错误值通过类型检查,运行时可能渲染异常。
|
||||
|
||||
---
|
||||
|
||||
### 🟡 2.7【企业级能力缺失·P2】无统一空状态/骨架屏/错误回退
|
||||
|
||||
**位置**:组件层未抽取统一的 `<ExamEmptyState>` / `<ExamSkeleton>` / `<ExamErrorBoundary>`。
|
||||
|
||||
**问题**:
|
||||
- `all/page.tsx` 自行实现了 `ExamsResultsFallback`,未复用到 `analytics`/`edit-rich`
|
||||
- `exam-card.tsx`、`exam-grid.tsx` 无骨架屏
|
||||
- 编辑器加载(Tiptap 初始化)期间无统一占位
|
||||
|
||||
**违反规则**:审计要求"明确处理空数据、无权限、网络异常等边界状态"、"异步数据使用 React Suspense + 骨架屏"。
|
||||
|
||||
**直接后果**:体验不一致,重复实现。
|
||||
|
||||
---
|
||||
|
||||
### 🟡 2.8【可测试性·P2】纯逻辑与 UI 耦合,缺单测
|
||||
|
||||
**位置**:
|
||||
- `components/exam-preview-utils.ts`(293 行纯函数,已抽取,但无单测)
|
||||
- `hooks/use-exam-preview-{state,tasks,rewrite}.ts` 无对应测试
|
||||
- `editor/editor-to-structure.ts`、`editor/structure-to-editor.ts` 双向转换是核心纯逻辑,无单测
|
||||
- `stats-service.ts` 的错误率/难度计算无单测
|
||||
|
||||
**违反规则**:审计要求"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks,与 UI 分离;导出清晰的接口类型以便 mock"。
|
||||
|
||||
**直接后果**:富文本编辑器双向转换是高风险逻辑(type/score/structure 映射),无单测难以保证回归质量。
|
||||
|
||||
---
|
||||
|
||||
### 🟡 2.9【解耦性·P2】未通过接口抽象 + Context 注入数据服务
|
||||
|
||||
**位置**:模块整体。
|
||||
|
||||
**问题**:当前组件直接 import 同模块的 actions/data-access(如 `exam-rich-form.tsx` 直接 import `autoMarkExamAction` / `createExamFromRichEditorAction`)。虽然同模块内 import 合规,但审计要求"通过定义 TypeScript 接口抽象数据依赖,使用 React Context 注入数据服务,模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access"。
|
||||
|
||||
**违反规则**:审计重构方案的"完全解耦"与"可测试性"原则。
|
||||
|
||||
**原因**:当前实现以功能正确性优先,未做依赖注入抽象。
|
||||
|
||||
**直接后果**:
|
||||
- 组件无法在测试中 mock 数据服务
|
||||
- 不同角色(teacher/admin/parent/student)的差异未通过接口实现隔离,未来扩展角色需改组件
|
||||
- 配置驱动设计未落地,新增 Widget 需改组件代码
|
||||
|
||||
---
|
||||
|
||||
## 三、行业差距对比
|
||||
|
||||
参考智学网、猿题库、学而思网校、Google Classroom、Canvas LMS 等同类产品,exams 模块当前差距:
|
||||
|
||||
### 3.1 试卷创建侧
|
||||
| 行业实践 | 当前状态 | 差距影响 |
|
||||
|---------|---------|---------|
|
||||
| 多种组卷入口(手动/AI/富文本/导入 Word)三选一清晰呈现 | 已有三种入口,但 `/create`、`/new` 路由并列,无统一选择页 | 教师首次使用困惑 |
|
||||
| 试卷模板库(按学科/年级预置模板) | ❌ 无 | 教师每次从零创建,效率低 |
|
||||
| 知识点双向细目表(题目-知识点覆盖矩阵) | ❌ 无(虽有 questions.knowledgePoints,但 exam 层无细目表视图) | 无法评估试卷覆盖度 |
|
||||
| 难度预估(基于题库历史正确率自动估算试卷难度) | ❌ 无(仅手动 1-5 级) | 难度设置主观 |
|
||||
| 试卷预览支持 PDF 导出/打印 | ❌ 无 | 教师无法离线分发 |
|
||||
|
||||
### 3.2 考试作答侧(学生)
|
||||
| 行业实践 | 当前状态 | 差距影响 |
|
||||
|---------|---------|---------|
|
||||
| 作答页答题卡导航(已答/未答/标记 revisit) | ❌ 仅顺序作答 | 学生难以跳题、检查 |
|
||||
| 自动保存进度可视化 | homework 模块已实现(autoSave* 翻译键齐全) | ✅ 较好 |
|
||||
| 限时/监考倒计时 | homework 模块已实现 useExamCountdown | ✅ 较好 |
|
||||
| 客观题即时反馈(练习模式) | ❌ 仅作业模式提交后批改 | 缺少低风险练习模式 |
|
||||
|
||||
### 3.3 考试分析侧(教师)
|
||||
| 行业实践 | 当前状态 | 差距影响 |
|
||||
|---------|---------|---------|
|
||||
| 平均分/及格率/分数段分布 | ✅ 已实现(V3-8) | — |
|
||||
| 逐题错误率与难度等级 | ✅ 已实现 | — |
|
||||
| 知识点掌握度雷达图 | diagnostic 模块有,但未在 exam analytics 集成 | 教师需跨页查看 |
|
||||
| 班级横向对比 | ❌ 无(仅全卷汇总) | 无法定位班级差异 |
|
||||
| 学生个体诊断报告(一键生成) | ❌ 无 | 个性化反馈缺失 |
|
||||
| 历次考试趋势 | ❌ 无 | 无法看进步趋势 |
|
||||
|
||||
### 3.4 多角色覆盖侧
|
||||
| 角色 | 当前覆盖 | 差距 |
|
||||
|------|---------|------|
|
||||
| admin | ❌ 无 admin 视角考试管理(全校/年级聚合) | admin 仅能通过 dashboard 看 examCount,无考试管理页 |
|
||||
| teacher | ✅ 完整(创建/组卷/预览/分析/监考) | — |
|
||||
| parent | ✅ parent 模块有 child-exam-detail + parentExam i18n | 缺少历次考试趋势对比 |
|
||||
| student | ⚠️ 通过 homework-take-view 作答,但无独立"我的考试"汇总页 | 学生无法回看历史考试试卷与成绩 |
|
||||
|
||||
### 3.5 UX 细节
|
||||
- 缺少全局考试状态徽章颜色规范(draft/published/archived 在 exam-card 与 exam-columns 中重复定义)
|
||||
- exam-card 科目颜色映射 `subjectColorMap` 硬编码英文字符串 key("Mathematics" 等),无法国际化——科目名应通过 ID 映射颜色,而非名称
|
||||
- 无空状态插画/图标统一规范(all 页用 FileText,analytics 页也用 BarChart3,缺一致性)
|
||||
|
||||
---
|
||||
|
||||
## 四、改进优先级建议
|
||||
|
||||
### P0(紧急,影响架构合规与数据安全)
|
||||
|
||||
| # | 问题 | 改进方向 | 关联规则 |
|
||||
|---|------|---------|---------|
|
||||
| P0-1 | `data-access-cross-module.ts:488` 直接 JOIN questions 表 | 在 questions 模块新增 `getQuestionTypeMapByIds(ids): Promise<Map<string, string>>`,exams 改为调用此接口 | 模块间禁止直查对方表 |
|
||||
| P0-2 | `new/`、`[id]/edit-rich/`、`[id]/analytics/`、`all/`、`create/` 缺 loading.tsx/error.tsx | 补齐 loading.tsx + error.tsx,复用 dashboard 模式 | 路由边界规范 |
|
||||
|
||||
### P1(高影响,影响多语言与体验)
|
||||
|
||||
| # | 问题 | 改进方向 |
|
||||
|---|------|---------|
|
||||
| P1-1 | 11 个组件未接入 i18n(exam-card/exam-filters/exam-preview-dialog/exam-viewer/question-options-editor + 4 个 editor extensions) | 接入 useTranslations,提取翻译键到 exam-homework.json 的 exam.card/exam.viewer/exam.previewDialog/editor.* 命名空间 |
|
||||
| P1-2 | actions-rich-editor.ts + auto-mark.ts Server Action 返回消息硬编码 | 改用 getTranslations("examHomework.exam.actionMessages"),复用 actions.ts 已有翻译键,新增 richEditor.* / autoMark.* 子键 |
|
||||
| P1-3 | exam-card subjectColorMap 用英文名做 key | 改为按 subjectId 映射颜色,颜色配置移至 `shared/config/subject-colors.ts` |
|
||||
| P1-4 | stats-service.ts "(无题目文本)"、data-access.ts "General" 兜底硬编码 | 通过 data-access 层返回 null,由组件层 i18n 渲染兜底文案 |
|
||||
|
||||
### P2(中长期,企业级能力与重构)
|
||||
|
||||
| # | 问题 | 改进方向 | 状态 |
|
||||
|---|------|---------|------|
|
||||
| P2-1 | 10 处 `as` 类型断言 | 为 RichQuestionType 增加 `isRichQuestionType(v): v is RichQuestionType` 类型守卫;Tiptap JSONContent 边界用 zod schema 校验 | ✅ 已完成(2026-06-25):新增 isRichQuestionType/isStandaloneQuestionType/toRichQuestionType/toStandaloneQuestionType 4 个守卫,消除 editor-to-structure.ts:101、exam-nodes-to-editor-doc.ts:38、actions-rich-editor.ts:149/233 共 4 处 as 断言;其余 6 处 as 断言属于 unknown→具体类型的合法收窄或 Tiptap/ProseMirror 内部类型边界,已添加注释说明,保留 |
|
||||
| P2-2 | 纯逻辑无单测 | 为 exam-preview-utils、editor-to-structure、structure-to-editor、stats-service 错误率计算补充 .test.ts | ⏸️ 待实施(依赖 P2-4 ExamServicePort 落地后统一 mock) |
|
||||
| P2-3 | 无统一 ExamEmptyState/ExamSkeleton/ExamErrorBoundary | 抽取到 components/exam-boundaries.tsx,全模块复用 | ✅ 已完成(2026-06-25):创建 components/exam-boundaries.tsx(189 行),导出 ExamErrorBoundary/ExamEmptyState/ExamSkeleton 三组合单元,5 种骨架变体,新增 i18n 键 exam.error.boundaryTitle/boundaryDescription/retry |
|
||||
| P2-4 | 组件直接 import actions,未通过 Context 注入 | 定义 `ExamServicePort` 接口 + `ExamServiceProvider` Context,组件通过 `useExamService()` 获取;角色差异通过不同 Provider 实现隔离 | ✅ 已完成骨架(2026-06-25):创建 services/exam-service-port.ts(95 行,12 方法契约)+ services/exam-service-context.tsx(72 行,Context + Provider + Hook)+ services/index.ts(桶导出)。具体实现(TeacherExamService/AdminExamService/MockExamService)与组件改造将在 P2-6+ 落地 |
|
||||
| P2-5 | 无配置驱动的 Widget 渲染 | 参考 dashboard/config/widget-configs.ts,新增 `exams/config/exam-widgets.ts`,按角色配置渲染哪些子模块 | ✅ 已完成(2026-06-25):创建 config/exam-widgets.ts(192 行),四角色默认配置 + getExamWidgetConfig/getWidgetsBySlot 工具函数 |
|
||||
| P2-6 | 缺少考试模板库、知识点细目表、班级对比、学生个体报告 | 中长期功能补全,对标智学网 | ⏸️ 待实施(中长期) |
|
||||
| P2-7 | 缺少 admin 视角考试管理页、student 独立"我的考试"页 | 多角色覆盖补全 | ⏸️ 待实施(中长期,依赖 P2-4 具体实现 + P2-5 配置消费) |
|
||||
| P2-8 | 关键操作埋点不完整 | 已有 exam.ai_generated/updated/deleted/duplicated,需补 exam.published/archived/auto_marked 埋点 | ⏸️ 待实施 |
|
||||
| P2-9 | a11y 缺失(编辑器 extensions 无 aria-label 规范、键盘导航) | 为 Tiptap 节点添加 aria-label,工具栏支持完整键盘导航 | ⏸️ 待实施 |
|
||||
| P2-10 | 数据查询未结合权限二次校验(data-access 层部分函数未传 scope) | `getExamPreview`、`getExamSubjects`、`getExamGrades`、`duplicateExam`、`deleteExamById` 应接受 scope 参数或在 Action 层显式校验 | ⏸️ 待实施 |
|
||||
|
||||
> **本轮 P2 落地范围说明**:
|
||||
> - P2-1 / P2-3 / P2-4(骨架)/ P2-5 已完成,奠定解耦与配置驱动的架构基础
|
||||
> - P2-2 单测待 ExamServicePort 具体实现落地后统一 mock
|
||||
> - P2-6 / P2-7 为中长期功能补全,需独立规划排期
|
||||
> - P2-8 / P2-9 / P2-10 为增强项,可在后续迭代中逐步落地
|
||||
> - 全部 P2 代码改动已通过 `npx tsc --noEmit`(exams 模块零错误)与 `npx eslint`(零错误/零警告)验证
|
||||
|
||||
---
|
||||
|
||||
## 五、架构图同步说明
|
||||
|
||||
本次审计发现架构图需补充以下节点:
|
||||
|
||||
### 004_architecture_impact_map.md 需补充
|
||||
|
||||
1. **exams 模块文件清单补全**:
|
||||
- 新增 `data-access-error-collection.ts` 行(当前 004 未单独列出)
|
||||
- 标注 `data-access-cross-module.ts` 中 `getExamForGradeEntry` 存在 P0 跨模块 JOIN 违规(待修复后改为 ✅ 已修复)
|
||||
|
||||
2. **permission 补全**:
|
||||
- 005 已有 EXAM_PROCTOR/EXAM_PROCTOR_READ,但 004 第 2.2 节 exams 权限点列表未完整列出
|
||||
|
||||
3. **dependencyMatrix 补充**:
|
||||
- `app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx` → `exams/editor/{exam-nodes-to-editor-doc,structure-to-editor}` 与 `exams/utils/normalize-structure`(当前 004 已记 build/page.tsx,但 edit-rich 同样依赖,需补)
|
||||
|
||||
4. **被依赖关系补全**:
|
||||
- `homework/data-access-utils.getQuestionText` 被 `exams/stats-service.ts` 调用,005 JSON 中 homework 模块 exports 的 usedBy 需补 `exams/stats-service`
|
||||
|
||||
### 005_architecture_data.json 需补充
|
||||
|
||||
1. `modules.exams.exports` 数组补:
|
||||
- `data-access-error-collection.ts`(含 `getExamErrorCollectionForExam` 等接口)
|
||||
- `getExamForGradeEntry`(标注跨模块 JOIN 待修复)
|
||||
|
||||
2. `modules.homework.exports` 中 `getQuestionText` 的 `usedBy` 补 `"exams/stats-service"`
|
||||
|
||||
3. `modules.questions.exports` 新增 `getQuestionTypeMapByIds`(修复 P0-1 后)
|
||||
|
||||
4. `architectureOverview.violations` 数组新增当前未记录的违规项,修复后改为 ✅ 标记
|
||||
|
||||
---
|
||||
|
||||
## 附:重构方案设计要点(落地架构)
|
||||
|
||||
> 以下为 P2-4/P2-5 的具体设计方向,作为中长期重构蓝图。本次实施将先完成 P0/P1,P2 仅落地基础接口与配置骨架。
|
||||
|
||||
### A. 完全解耦:ExamServicePort + Context 注入
|
||||
|
||||
```typescript
|
||||
// exams/services/exam-service-port.ts(新增)
|
||||
export interface ExamServicePort {
|
||||
listExams(params: GetExamsParams): Promise<Exam[]>
|
||||
getExam(id: string): Promise<ExamDetail | null>
|
||||
createExam(input: ExamCreateInput): Promise<ActionState<string>>
|
||||
updateExam(input: ExamUpdateInput): Promise<ActionState<string>>
|
||||
deleteExam(id: string): Promise<ActionState<string>>
|
||||
duplicateExam(id: string): Promise<ActionState<string>>
|
||||
getAnalytics(id: string): Promise<ExamAnalyticsSummary | null>
|
||||
// ... 所有数据访问通过此接口
|
||||
}
|
||||
|
||||
// exams/services/exam-service-context.tsx(新增)
|
||||
const ExamServiceContext = createContext<ExamServicePort | null>(null)
|
||||
export function ExamServiceProvider({ service, children }: { service: ExamServicePort; children: ReactNode }) { ... }
|
||||
export function useExamService(): ExamServicePort { ... }
|
||||
|
||||
// 不同角色的实现
|
||||
// exams/services/teacher-exam-service.ts // 调用真实 Server Actions
|
||||
// exams/services/admin-exam-service.ts // admin 视角(聚合全校)
|
||||
// exams/services/mock-exam-service.ts // 测试用
|
||||
```
|
||||
|
||||
### B. 组合优先:Widget 配置驱动
|
||||
|
||||
```typescript
|
||||
// exams/config/exam-widgets.ts(新增)
|
||||
export type ExamWidgetConfig = {
|
||||
role: Role
|
||||
widgets: Array<{
|
||||
id: "list" | "analytics" | "proctoring" | "templates" | "blueprint"
|
||||
visible: boolean
|
||||
order: number
|
||||
props?: Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
export const examWidgetConfigs: Record<Role, ExamWidgetConfig> = { ... }
|
||||
```
|
||||
|
||||
### C. i18n 翻译文件结构示例(新增键)
|
||||
|
||||
```json
|
||||
{
|
||||
"exam": {
|
||||
"card": {
|
||||
"level": "难度 {{level}}",
|
||||
"minutes": "{{count}} 分钟",
|
||||
"points": "{{count}} 分",
|
||||
"questions": "{{count}} 题"
|
||||
},
|
||||
"viewer": {
|
||||
"section": "分卷",
|
||||
"group": "大题",
|
||||
"score": "分值",
|
||||
"noQuestions": "暂无题目"
|
||||
},
|
||||
"previewDialog": {
|
||||
"title": "试卷预览",
|
||||
"generating": "生成预览中...",
|
||||
"fullPreview": "完整试卷预览",
|
||||
"summary": "{{count}} 题 · {{subject}} · {{grade}} · {{minutes}} 分钟 · {{total}} 分",
|
||||
"noPreview": "暂无预览内容",
|
||||
"confirmCreate": "确认并创建",
|
||||
"untitledQuestion": "未命名题目",
|
||||
"untitledSubQuestion": "未命名子题",
|
||||
"scoreUnit": "分"
|
||||
},
|
||||
"richEditorAction": {
|
||||
"titleRequired": "请填写试卷标题",
|
||||
"contentRequired": "试卷内容不能为空",
|
||||
"contentInvalid": "试卷内容格式无效",
|
||||
"contentParseFailed": "试卷内容解析失败",
|
||||
"draftCreated": "试卷草稿已创建",
|
||||
"onlyOwnUpdate": "只能更新自己创建的试卷",
|
||||
"updated": "试卷已更新"
|
||||
},
|
||||
"autoMarkAction": {
|
||||
"sourceRequired": "试卷文本不能为空",
|
||||
"completed": "AI 自动标记完成"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### D. 错误与边界
|
||||
|
||||
- 每个路由的 `error.tsx` 复用 `exams/components/exam-error-boundary.tsx`(新增)
|
||||
- 列表/卡片使用 `<ExamSkeleton>` / `<ExamEmptyState>`(新增)
|
||||
- 编辑器加载使用 Suspense + 自定义骨架
|
||||
|
||||
### E. 可测试性
|
||||
|
||||
- 纯逻辑已有抽取(exam-preview-utils/editor-to-structure/structure-to-editor),补单测
|
||||
- ExamServicePort 接口允许测试注入 mock 实现
|
||||
|
||||
### F. 安全性
|
||||
|
||||
- data-access 层所有按 ID 查询函数增加可选 `scope` 参数,Action 层强制传入
|
||||
- `getExamPreview`、`duplicateExam`、`deleteExamById` 当前未校验 scope,需补
|
||||
|
||||
### G. 监控埋点
|
||||
|
||||
- 补 `exam.published`、`exam.archived`、`exam.auto_marked` 埋点
|
||||
- analytics 页访问埋点 `exam.analytics_viewed`
|
||||