feat(pc,platform): 验收走查修复(平台按学校代管 + 菜单/403/日志范围/分页)
客户在验收里点出来的问题,一次改完。两段改动:
一、平台运营与租户范围(后端为主)
- 新增请求头 X-Tenant-Id:平台运营是唯一跨租户身份,租户拦截器对它整体关掉了过滤,
于是「组织架构 / 账号管理 / 工作台」拿到的是所有租户混在一起的数据 —— 而只有一所
学校有数据时,它和学校管理员看到的完全一样,看不出错(这是最危险的一点)。
现在这些页面必须先选定学校,服务端再把本次请求的租户上下文换成目标租户:
只认平台身份(其它身份带头一律忽略)、目标租户必须存在且启用。
- OrgService.tree / UserService.list 补显式 tenant_id 条件(原来只靠拦截器,而拦截器对
平台不生效);TaskService.seesWholeTenant 改用新的 LoginUser.isPlatformStaff() ——
isPlatformAdmin 回答"当前租户是不是平台租户",代管时它为 false,会让平台退化成普通
用户(任务列表掉进"班主任只看自己带的班"分支,得到空列表);PLATFORM_ADMIN 补
TASK_VIEW(PRD §3.2「查看填报进度看板:平台运营 ✅」)。
- 操作日志对平台永远是空的:SQL 写死 tenant_id = 当前租户,而平台的 tenantId 是 0。
按契约 §13.4 补 tenantId 参数与 log:viewAll:平台默认看全量、可指定某所学校;
学校管理员恒为本租户,传别家返回 40301(不静默忽略,否则会让人以为"筛过了")。
- 开放接口"推送重试"从未生效(F-112):定时扫描缺 @InterceptorIgnore,异常被 log.warn
吞掉;同一方法里"接入方已停用"那条 update 也没有租户上下文,会每 10 秒重试到永远。
E2E 补了"真的重试了"的断言(原来只断言"安排了下次重试")。
二、PC 端走查体验
- 去掉 3 条无用菜单(名单管理 / 表单设计器 / 材料规则):功能都在「收集任务」行内入口,
菜单里那一条点进去只有指路;后端菜单键同步删除,entryAt 那套机制一并删掉。
- 班主任点工作台必弹 403:工作台无条件调了要 org:view / progress:viewAll 的两个接口。
403 走 HTTP 状态码,try/catch 拦不住它的弹窗 —— 改成按权限决定发不发请求。
- 操作日志:分页器被裁在可视区外(新增 .g-list-card 让表体自己滚、表头钉住、分页器留在
卡片底部)+ 去掉页面上那段免责声明;收集任务的状态切换改成单独一行 tab;去掉顶栏假数据
铃铛;配色按客户要求还原。
顺带修两处"检查器自己不严":check-wiring 新增第 ⑦ 项(PC 端引用的权限码必须存在于
PermissionDef —— 本轮我自己把 progress:viewAll 写成 progress:view:all,判断恒为 false,
而编译 / typecheck / lint / E2E 全都不红);verify-docs 的表格列数检查不再把 GFM 转义
竖线当成列分隔符。两个检查器都拿"故意写错的样例"试过,确认真的会红。
验证:typecheck / mvn package / check-wiring(未接线声明 0 条)/ verify-docs(22 项)全过;
e2e-m0 36、e2e-m1-orgs 48、e2e-m0-user 55、e2e-m5-logs 44、e2e-m5-notify-wiring 43、
e2e-m1-tenant-domain 6、e2e-m8-open 61 全绿;浏览器按角色逐个真点复验。
详见 docs/交接摘要.md §一·三十三 与 §一·三十四。
This commit is contained in:
+102
-1
@@ -21,7 +21,7 @@
|
||||
逐条由人判断是"忘记接线"还是"有意留的"(有意留的要写进下面的已知例外表,
|
||||
而不是靠下次再想一遍)。
|
||||
|
||||
## 查五件事
|
||||
## 查七件事
|
||||
|
||||
| # | 查什么 | 没接上的后果 |
|
||||
|:--:|---|---|
|
||||
@@ -31,6 +31,17 @@
|
||||
| 4 | `NotifyTemplates` 的站内信模板零发送点 | 模板页能改,但那条通知根本不发 / 不走模板 |
|
||||
| 5 | `CollectMessage.BIZ_*` 零使用 | 消息中心那个分类永远是空的 |
|
||||
| 6 | 内置角色声明的菜单键 ↔ PC 端 `XtMenu` 的 key | 菜单永远不显示(键拼错),或前端有菜单谁都看不到 |
|
||||
| 7 | **PC 端引用的权限码 ↔ `PermissionDef`** | 判断恒为 false/true:整块界面静默消失(见下) |
|
||||
|
||||
### 第 7 项是 2026-10-09 补的(自己踩了一次)
|
||||
|
||||
在工作台按权限决定"发不发请求"时,我把 `progress:viewAll` 写成了 `progress:view:all`
|
||||
(多一个冒号)。后果是那个判断**恒为 false**:「审核情况」整块对**所有人**都不显示 ——
|
||||
而编译、typecheck、lint、后端日志、甚至 E2E **全都不红**。
|
||||
|
||||
这一族错的特征与上面六项完全一样:**声明对不上,谁都不报错**;
|
||||
而"权限不足就少显示一块"看起来又很像设计(不是崩溃,是安静地少一块)。
|
||||
|
||||
|
||||
## 它**不查**什么(免得被当成"全绿就没事")
|
||||
|
||||
@@ -79,6 +90,9 @@ PERM_NO_CHECK = {
|
||||
}
|
||||
# 权限码:被校验了,但没有被任何内置角色分配(= 谁都调不了)
|
||||
PERM_NO_ROLE = {}
|
||||
# PC 端引用的权限码:PermissionDef 里没有的(**应当保持为空** ——
|
||||
# 写错一个码的后果是"那个判断恒为 false",整块界面静默消失,见文件头第 7 项)
|
||||
FE_PERM_EXCEPTION = {}
|
||||
ERROR_CODE_UNUSED = {
|
||||
'ID_CARD_CHECKSUM_FAILED': 'M4 识别与校验(等 OCR 选型,见 docs/01 D1)',
|
||||
'ID_CARD_MISMATCH_ROSTER': '同上(M4)',
|
||||
@@ -227,6 +241,53 @@ def strip_line_comments(block):
|
||||
return '\n'.join(re.sub(r'//.*$', '', ln) for ln in block.split('\n'))
|
||||
|
||||
|
||||
def strip_js_comments(src):
|
||||
"""去掉 JS/TS 的注释,**但要认引号**(不能见到 `//` 就砍)。
|
||||
|
||||
⚠️ 为什么需要它:第 ⑦ 项第一版把 `directives/hasPerm.ts` 里的
|
||||
**注释与示例字符串**(`<button v-hasPerm="['home:btn:add']">`、报错文案里的示例)
|
||||
当成了真实用法,一口气报了 5 条假阳性 —— 模板作者的示例码当然不在 PermissionDef 里。
|
||||
|
||||
这与本脚本 2026-10-08 修过的第 ③ 条是同一个教训(当时是 Java 注释里的提及
|
||||
被当成接线):**检查器必须能区分"代码"与"解释代码的文字"**,
|
||||
否则它要么天天误报、要么被人加一堆例外表绕过去。
|
||||
"""
|
||||
out = []
|
||||
i, n = 0, len(src)
|
||||
quote = None # 当前是否在字符串里(' 或 ")
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if quote:
|
||||
out.append(c)
|
||||
if c == '\\' and i + 1 < n: # 转义:连同下一个字符一起吞掉
|
||||
out.append(src[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if c == quote:
|
||||
quote = None
|
||||
i += 1
|
||||
continue
|
||||
if c in '\'"`':
|
||||
quote = c
|
||||
out.append(c)
|
||||
i += 1
|
||||
continue
|
||||
if c == '/' and i + 1 < n and src[i + 1] == '/':
|
||||
while i < n and src[i] != '\n':
|
||||
i += 1
|
||||
continue
|
||||
if c == '/' and i + 1 < n and src[i + 1] == '*':
|
||||
i += 2
|
||||
while i + 1 < n and not (src[i] == '*' and src[i + 1] == '/'):
|
||||
i += 1
|
||||
i += 2
|
||||
out.append(' ')
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def template_send_points(sources, code, tpl_file):
|
||||
"""站内信模板的"发送点" = 代码里有人把这个码当成模板码用。
|
||||
|
||||
@@ -425,6 +486,46 @@ def main():
|
||||
for k in sorted(menu_keys & pc_keys):
|
||||
say(' ✓ %s' % k)
|
||||
|
||||
# ── ⑦ PC 端引用的权限码 ↔ PermissionDef ─────────────────────
|
||||
#
|
||||
# 两个写法都认:`userStore.permissions.includes('org:view')` 与
|
||||
# `v-hasPerm="['audit:final']"`(后者是模板里写的指令参数)。
|
||||
#
|
||||
# ⚠️ 这个检查器自己也要能被验:**故意写错一个码,它必须红**。
|
||||
# 2026-10-09 加它的时候正是拿一个写错的码试过的(那时是真的写错了)。
|
||||
perm_codes = set(re.findall(r'\(\s*"([^"]+)"\s*,\s*"', strip_java_comments(read(PERM_DEF))))
|
||||
fe_codes = {} # code -> 出现位置(文件:行)
|
||||
for p in sorted(glob.glob('frontend-pc/src/**/*.vue', recursive=True)
|
||||
+ glob.glob('frontend-pc/src/**/*.ts', recursive=True)):
|
||||
try:
|
||||
raw = read(p)
|
||||
except Exception:
|
||||
continue
|
||||
# 先去掉注释,再找用法 —— 否则注释/示例文案里的码会被当成真用法(假阳性)
|
||||
src = strip_js_comments(raw)
|
||||
is_vue = p.endswith('.vue')
|
||||
for i, line in enumerate(src.split('\n'), 1):
|
||||
for m in re.finditer(r"permissions\.includes\(\s*'([^']+)'", line):
|
||||
fe_codes.setdefault(m.group(1), []).append('%s:%d' % (p, i))
|
||||
# `v-hasPerm` 是**模板**里的写法:只在 .vue 里找,别去 .ts 的示例/报错文案里找
|
||||
if is_vue:
|
||||
for m in re.finditer(r"v-hasPerm\s*=\s*\"\[([^\]]*)\]\"", line):
|
||||
for c in re.findall(r"'([^']+)'", m.group(1)):
|
||||
fe_codes.setdefault(c, []).append('%s:%d' % (p, i))
|
||||
say('\n【⑦】PC 端引用的权限码 %d 个 / PermissionDef 声明 %d 个'
|
||||
% (len(fe_codes), len(perm_codes)))
|
||||
for code in sorted(fe_codes):
|
||||
where = fe_codes[code][0]
|
||||
if code in perm_codes:
|
||||
say(' ✓ %-22s %s' % (code, where))
|
||||
elif code in FE_PERM_EXCEPTION:
|
||||
notes.append('PC 端权限码 %s —— 已知例外:%s' % (code, FE_PERM_EXCEPTION[code]))
|
||||
say(' — %-22s %s' % (code, FE_PERM_EXCEPTION[code]))
|
||||
else:
|
||||
problems.append(('前端权限码', code,
|
||||
'PermissionDef 里没有这个码(判断会恒为 false:%s)' % where))
|
||||
say(' ✗ %-22s PermissionDef 里没有它 → 这个判断永远是 false %s' % (code, where))
|
||||
|
||||
# ── 汇总 ────────────────────────────────────────────────
|
||||
print('\n' + '=' * 88)
|
||||
if notes:
|
||||
|
||||
@@ -472,6 +472,26 @@ def main():
|
||||
rp2 = open_call("POST", "/api/v1/open/v1/callback/push", secret=secret, body={})
|
||||
check("人工重推不传 submissionId → 明确报错", (rp2[1] or {}).get("code") != 0,
|
||||
json.dumps(rp2[1], ensure_ascii=False)[:200])
|
||||
|
||||
# ⭐ 定时扫描**真的会重试**(F-112,2026-10-09 修)
|
||||
#
|
||||
# 这一条与上面那条"安排了下次重试"是**两件事**:上面断言的是 next_retry_at 被写上,
|
||||
# 而这里断言的是到点之后**真的有人来重试**。
|
||||
#
|
||||
# 修之前:`OpenPushLogMapper.dueRetries()` 少了 @InterceptorIgnore,定时任务没有租户上下文
|
||||
# → 每次扫描抛 40101 → 被 retryDue 的 catch 吞成一行 warn。于是"安排了"永远不等于"发生了",
|
||||
# 而 E2E 只断言了前者,所以 60/60 全绿也照样漏掉了这个 bug。
|
||||
#
|
||||
# 等待时间 = 定时任务间隔(fixedDelay 10s)+ 首次退避(1s)+ 余量
|
||||
attempts_before = int(sql("select attempts from collect_open_push_log "
|
||||
"where submission_id=%d order by id desc limit 1;" % sid) or 0)
|
||||
time.sleep(16)
|
||||
attempts_after = int(sql("select attempts from collect_open_push_log "
|
||||
"where submission_id=%d order by id desc limit 1;" % sid) or 0)
|
||||
check("⭐ 定时扫描真的重试了:attempts 增加(不是只安排了下次重试)",
|
||||
attempts_after > attempts_before,
|
||||
"attempts %d → %d(16 秒内至少应被扫描到一次)" % (attempts_before, attempts_after))
|
||||
|
||||
check("⭐ 推送报文里没有明文证件号(与 §14.2 同口径)",
|
||||
"身份证" not in (sql("select coalesce(payload_json,'') from collect_open_push_log "
|
||||
"where submission_id=%d limit 1;" % sid) or "")
|
||||
|
||||
+12
-2
@@ -50,6 +50,16 @@ def read(*parts: str) -> str:
|
||||
|
||||
|
||||
# ── 1. 表格列数 ────────────────────────────────────────
|
||||
def table_pipes(line: str) -> int:
|
||||
"""数表格竖线,**跳过转义过的 `\\|`**。
|
||||
|
||||
GFM 里 `\\|` 表示"内容里的竖线",不是列分隔符(本仓库用手写表格,
|
||||
正文里写 `a \\|\\| b` 是很自然的事)。第一版直接 `line.count("|")`,
|
||||
于是把这种写法报成"列数不一致" —— **检查器自己要先认得它检查的语法**。
|
||||
"""
|
||||
return len(re.findall(r"(?<!\\)\|", line))
|
||||
|
||||
|
||||
def check_tables(name: str, text: str) -> None:
|
||||
lines = text.split("\n")
|
||||
bad_rows = 0
|
||||
@@ -57,10 +67,10 @@ def check_tables(name: str, text: str) -> None:
|
||||
while i < len(lines):
|
||||
if lines[i].strip().startswith("|") and i + 1 < len(lines) \
|
||||
and re.match(r"^\|[\s:|-]+\|$", lines[i + 1].strip()):
|
||||
n = lines[i].count("|")
|
||||
n = table_pipes(lines[i])
|
||||
j = i + 2
|
||||
while j < len(lines) and lines[j].strip().startswith("|"):
|
||||
if lines[j].count("|") != n:
|
||||
if table_pipes(lines[j]) != n:
|
||||
bad_rows += 1
|
||||
j += 1
|
||||
i = j
|
||||
|
||||
Reference in New Issue
Block a user