Files
form-center-service/sql/h2_userbase_app_and_modules.sql
T
chenxing bf39c220f2
build / build-push (push) Successful in 1m31s
test(perf)+sql(h2): T9 压测探针与 UserBase 登记交接脚本
Perf200kProbe(20 万答卷/78 万索引行,独立库 bsch_formcenter_perf):
- V10 量化:列表首屏 p95=22ms、三条件筛选 p95=313ms、深分页 keyset p95=31ms、
  统计 p95=1~334ms、导出 2 万级 4.1~8.1s(冷缓冲池首跑 22.1s 已如实记档)
- 铁律取证:org.hibernate.SQL 日志抓真实语句——第一段只 select id、不含 values_json;
  翻页谓词是 answer.id<游标;EXPLAIN 显示首屏/计数走 idx_answer_form_time 且 using_index=true
- P26:8 线程×250 并发实测 answer_no 重号率=1392/2000(69.6%,"不限次+报名号"组合)
- P37:新答卷后 cached=true、升版后 cached=false(铁律④语义与实现一致)
- P35/P31:information_schema 复核索引基数与 ROW_FORMAT=Dynamic(本机 8.0.45)
- 类名不带 Test/IT → 不进 mvn test 门禁(190 用例仍是快门禁),手点 -Dtest 才跑

PerfTreeCallsProbe(控制器形状,复用同一份数据):
- P44 请求级取树次数:列表/详情/导出=2、统计=3、筛选候选=1(P44 的"1~2 次"上界在统计路径实测为 3)
- V10 请求级延迟:筛选 p95=163ms、导出 2297ms/1.15MB
- P36 升版补建实测:19,600 行表单的发布=373,272ms 且本次补出 0 行——
  P36 记档的"2 万级秒级"不成立,N+1 逐行探测是主因(数字交验收轮裁决是否触发 publish 处改造)

sql/h2_userbase_app_and_modules.sql(H2 + 第一个快应用登记交接件):
中心应用六条模块行(手册 §2.2,含 path 空 fc:scope:all 锚点)+ 快应用五条模块行
(方案 §6.2 checklist,导入/导出为 path 空锚点)+ 组/角色授权与开通 + 验收查询 + 回滚段。
AI 不代跑(沿 flow-center 先例;H2 既定路径是超管在管理端录入,脚本为批量/复核形态)。
步骤 3.5 显式记录"快应用数据视图的后端守卫仍扫中心树"这一过渡口径(T9 报告 P78 附注)。

docs/T6-report.md 已按 P69 从本仓 docs/ 挪至 form-center/docs/(本提交不含该文件)。
2026-10-15 12:57:32 +08:00

203 lines
15 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================================
-- 表单中心 H2 + 第一个快应用:UserBase 侧应用行/模块行/授权登记脚本
-- 编制:Qoder(T9 联调轮)|日期:2026-10-15
-- 依据:《表单中心执行手册》§2.2(中心应用六条模块行表 + 验证口径)、
-- 《表单中心建设方案 v1.4》§6.2(快应用登记 checklist R8)、§6.4(customId=fc:scope:all)、
-- §6.5(中心=BUSINESS 应用,模块行与快应用模块行两棵树不混)
-- 先例:D:\SoftProject\yunhai\UserBase\flow-center接入UserBase菜单.sql(同一套形态与幂等写法)
--
-- ⚠ 纪律:本脚本**不由 AI 执行**(沿 flow-center 先例"AI 不代跑,交由你在 dev / prod 各自执行")。
-- H2 的既定路径是 UserBase 管理端(超管)逐项录入;本脚本是同一份数据的批量/复核形态,
-- 两者取其一即可,重复执行安全(全部按唯一键做"不存在才插",不覆盖人工改过的值)。
-- 目标库=**UserBase 的库**(本地 bsch_userbase / 160 容器网 userbase 库),不是 bsch_formcenter。
-- MySQL 5.7 兼容(无 CTE / 无窗口函数)。
-- ============================================================================
-- ----------------------------------------------------------------------------
-- 步骤 0:定位 form-center 的应用主键(关键,排第一大坑)
-- 开放接口按「调用方 app_key → application_info 主键 id」反查,而 app_module.app_id 存的是【主键 id】,
-- 不是 app_key(100xxx)。中心后端签名用的 app_key 由 projman 注入(P7:jar 内占位 100021 上线前替换)。
-- 期望下面这条命中一行;命中 0 行=应用行还没建(走 0b 建,或去管理端建)。
-- ----------------------------------------------------------------------------
SELECT id AS app_id_pk, app_key, name, app_code, app_nature, pc_url
FROM application_info
WHERE app_code = 'form-center' OR app_key = 100021;
SET @center_app_id = 0; -- ← 按上一步结果填:form-center 在 application_info 的主键 id
SET @center_app_key = 100021; -- ← 按 UserBase 注册所得 app_key 填(占位值仅本地)
SET @operator = 0; -- create_user 占位(0=系统)
-- 0b(可选):应用行缺失时才需要。app_secret 请改成 UserBase 注册实际下发的值,
-- 不要把这里的占位串带到任何环境(D7:一期无写 oapi,应用行由超管建=本行就是那个"手工建")。
-- INSERT INTO application_info (app_key, app_secret, name, app_code, app_nature, pc_url, pc_icon,
-- clients, create_time, create_user)
-- SELECT @center_app_key, 'REPLACE-WITH-REAL-SECRET', '表单中心', 'form-center', 'BUSINESS',
-- 'http://base.yunhaixingtong.com/form-center/front/', 'category', JSON_ARRAY('pc'),
-- NOW(), @operator
-- WHERE @center_app_id = 0;
-- ----------------------------------------------------------------------------
-- 步骤 1:中心应用六条模块行(执行手册 §2.2 表;path 必须逐字等于 front/src/router/routes.ts)
-- ①表单设计 /admin/forms ②数据管理 /admin/data ③统计 /admin/stats
-- ④字典管理 /admin/dicts ⑤我的填写 /my(全员) ⑥数据全量【path 留空 + customId=fc:scope:all】
-- 第⑥行=纯权限锚点:导航不渲染、后端 DataScopeService 只扫 path 空节点的 customId(P47 收紧后口径),
-- 授权给哪个组,该组的数据范围即升级为"全部"(方案 §6.4)。
-- 子页面路由(/admin/forms/designer、/admin/data/detail、/app/:appCode/data/detail)
-- **不建模块行**(手册 §2.2 注记 + P63/P77):不占 modId、不进导航,前端按 meta.parentPath 归位。
-- custom_id 用 200~205 段(避开 flow-center 的 100~105;后端 resolveModId 只认数字 customId)
-- ----------------------------------------------------------------------------
INSERT INTO app_module
(app_id, parent_id, name, custom_id, path, icon, level, sort_no, create_user, create_time)
SELECT * FROM (
SELECT @center_app_id AS app_id, NULL AS parent_id, '表单设计' AS name, '200' AS custom_id,
'/admin/forms' AS path, 'word' AS icon, 1 AS level, 200 AS sort_no, @operator AS create_user, NOW() AS create_time
UNION ALL SELECT @center_app_id, NULL, '数据管理', '201', '/admin/data', 'category', 1, 201, @operator, NOW()
UNION ALL SELECT @center_app_id, NULL, '统计', '202', '/admin/stats','tongji', 1, 202, @operator, NOW()
UNION ALL SELECT @center_app_id, NULL, '字典管理', '203', '/admin/dicts','zidingyi', 1, 203, @operator, NOW()
UNION ALL SELECT @center_app_id, NULL, '我的填写', '204', '/my', '', 1, 204, @operator, NOW()
-- 锚点行:path 空 + customId 协议值(非数字 customId 是刻意的,前端 modId 会回退主键 id,不影响消费)
UNION ALL SELECT @center_app_id, NULL, '数据全量', 'fc:scope:all', '', '', 1, 205, @operator, NOW()
) t
WHERE @center_app_id > 0
AND NOT EXISTS (SELECT 1 FROM app_module m WHERE m.app_id = t.app_id AND m.custom_id = t.custom_id);
-- ----------------------------------------------------------------------------
-- 步骤 2:第一个快应用(演练对象,方案 §6.2 checklist R8 四步的前两步)
-- 快应用=真实应用行 + 无自有后端 + pc_url 指中心分发路由。app_code 一经确定不可改
-- (表单归属列 app_code 与模块树查询都按它走,P25/D8)。
-- ----------------------------------------------------------------------------
SET @quick_code = 'fc-drill'; -- ← 演练用快应用 app_code(前端 mock 桩也叫这个名字)
SET @quick_app_key = 0; -- ← UserBase 注册所得 app_key(无后端应用也要有身份行)
SELECT id AS quick_app_id_pk, app_key, name, app_code, app_nature
FROM application_info WHERE app_code = @quick_code;
SET @quick_app_id = 0; -- ← 按上一步结果填;0=还没建,走 2b
-- 2b(可选):快应用应用行(BUSINESS 之外按平台既有 nature 填;无自有后端=不建 projman 项目)
-- INSERT INTO application_info (app_key, app_secret, name, app_code, app_nature, pc_url, pc_icon,
-- clients, create_time, create_user)
-- SELECT @quick_app_key, 'REPLACE-WITH-REAL-SECRET', '表单演练应用', @quick_code, 'BUSINESS',
-- CONCAT('http://base.yunhaixingtong.com/form-center/app/', @quick_code, '/'), 'edit',
-- JSON_ARRAY('pc'), NOW(), @operator
-- WHERE @quick_app_id = 0;
-- SET @quick_app_id = LAST_INSERT_ID(); -- ← 紧接着上面那条 INSERT 生效;否则手工填
-- 模块行五条(方案 §6.2 表的典型配法)。path 形态:
-- 填报 = /f/{code}(该快应用名下的表单 code,经 resolve 换 uuid——快应用不授权定义管理行,§3.4)
-- 数据查看/统计 = /app/{app_code}/data|stats(AppContext.vue 的分派段)
-- 导入/导出 = path 留空的按钮级锚点(导出按钮显隐按它判,P76 修的就是"判哪棵树")
INSERT INTO app_module
(app_id, parent_id, name, custom_id, path, icon, level, sort_no, create_user, create_time)
SELECT * FROM (
SELECT @quick_app_id AS app_id, NULL AS parent_id, '填报' AS name, '300' AS custom_id,
CONCAT('/f/', @quick_code, '_form') AS path, 'edit' AS icon, 1 AS level, 300 AS sort_no,
@operator AS create_user, NOW() AS create_time
UNION ALL SELECT @quick_app_id, NULL, '数据查看', '301', CONCAT('/app/', @quick_code, '/data'), 'category', 1, 301, @operator, NOW()
UNION ALL SELECT @quick_app_id, NULL, '统计', '302', CONCAT('/app/', @quick_code, '/stats'), 'tongji', 1, 302, @operator, NOW()
UNION ALL SELECT @quick_app_id, NULL, '导入', '303', '', '', 1, 303, @operator, NOW()
UNION ALL SELECT @quick_app_id, NULL, '导出', '304', '', '', 1, 304, @operator, NOW()
) t
WHERE @quick_app_id > 0
AND NOT EXISTS (SELECT 1 FROM app_module m WHERE m.app_id = t.app_id AND m.custom_id = t.custom_id);
-- ----------------------------------------------------------------------------
-- 步骤 3:授权 + 开通(方案 §6.1 三层:①应用开通(校级)②模块授权(组/角色)③数据范围)
-- 判定口径(UserBase 侧现成实现):最终模块权限 =(组授权模块 ∩ 已开通应用)∪(启用角色授权模块)。
-- 组授权路径**必须先开通应用**(开通不继承,子组要自己开通或挂在已开通的祖先链上)。
-- ----------------------------------------------------------------------------
-- 3.1 中心管理员组(示例:把 @grp_admin 换成目标学校的管理员组 id;sys_group 里查)
SET @grp_admin = 0; -- ← 中心管理员组
SET @grp_all = 0; -- ← 全员组(学校根组/基础组,"我的填写"给它)
SET @grp_teacher = 0; -- ← 快应用教师组(数据查看 + 导出锚点)
SET @grp_student = 0; -- ← 快应用学生组(只给填报行)
-- 3.2 应用开通(中心 + 快应用各一条;@school_id 是要开通的学校 id)
SET @school_id = 0; -- ← 目标学校 id
INSERT INTO sys_group_app_rel (group_id, app_id, create_user, create_time)
SELECT g.group_id, a.app_id, @operator, NOW()
FROM (SELECT @grp_admin AS group_id, @center_app_id AS app_id UNION ALL
SELECT @grp_all, @center_app_id UNION ALL
SELECT @grp_teacher, @quick_app_id UNION ALL
SELECT @grp_student, @quick_app_id) a
JOIN (SELECT 1 AS dummy) g ON a.group_id > 0
WHERE NOT EXISTS (SELECT 1 FROM sys_group_app_rel r
WHERE r.group_id = a.group_id AND r.app_id = a.app_id);
-- 3.3 中心模块授权:管理员组=前五行(除锚点),全员组=只"我的填写",
-- 要看全校数据的组=额外勾"数据全量"锚点行(customId=fc:scope:all)
INSERT INTO sys_group_module_perm (group_id, module_id, inherit_flag, create_user, create_time)
SELECT @grp_admin, m.id, 0, @operator, NOW()
FROM app_module m
WHERE m.app_id = @center_app_id AND @grp_admin > 0 AND COALESCE(m.custom_id,'') <> 'fc:scope:all'
AND NOT EXISTS (SELECT 1 FROM sys_group_module_perm p WHERE p.group_id = @grp_admin AND p.module_id = m.id);
INSERT INTO sys_group_module_perm (group_id, module_id, inherit_flag, create_user, create_time)
SELECT @grp_all, m.id, 0, @operator, NOW()
FROM app_module m
WHERE m.app_id = @center_app_id AND @grp_all > 0 AND m.path = '/my'
AND NOT EXISTS (SELECT 1 FROM sys_group_module_perm p WHERE p.group_id = @grp_all AND p.module_id = m.id);
-- 数据全量锚点(V42 的授权动作;只给确实要看全校数据的组)
INSERT INTO sys_group_module_perm (group_id, module_id, inherit_flag, create_user, create_time)
SELECT @grp_admin, m.id, 0, @operator, NOW()
FROM app_module m
WHERE m.app_id = @center_app_id AND @grp_admin > 0 AND m.custom_id = 'fc:scope:all'
AND NOT EXISTS (SELECT 1 FROM sys_group_module_perm p WHERE p.group_id = @grp_admin AND p.module_id = m.id);
-- 3.4 快应用模块授权:教师组=数据查看+统计+导出锚点(不给填报也算得出数据);学生组=只给填报行
INSERT INTO sys_group_module_perm (group_id, module_id, inherit_flag, create_user, create_time)
SELECT @grp_teacher, m.id, 0, @operator, NOW()
FROM app_module m
WHERE m.app_id = @quick_app_id AND @grp_teacher > 0 AND COALESCE(m.custom_id,'') <> '303' -- 303=导入(1.5 预留位不授)
AND NOT EXISTS (SELECT 1 FROM sys_group_module_perm p WHERE p.group_id = @grp_teacher AND p.module_id = m.id);
INSERT INTO sys_group_module_perm (group_id, module_id, inherit_flag, create_user, create_time)
SELECT @grp_student, m.id, 0, @operator, NOW()
FROM app_module m
WHERE m.app_id = @quick_app_id AND @grp_student > 0 AND m.level = 1 AND m.path LIKE '/f/%'
AND NOT EXISTS (SELECT 1 FROM sys_group_module_perm p WHERE p.group_id = @grp_student AND p.module_id = m.id);
-- 3.5 ⚠ 过渡口径(T9 报告 §5.4 末段 / 问题单 P78 附注,需裁决后才可能取消这一步):
-- 快应用的数据视图复用 /api/form/admin/data/**,而后端守卫扫的是【中心会话树】(签名调用方=表单中心自己),
-- 不消费快应用自己的"数据查看"行。只勾快应用行的教师组进应用数据视图会被后端拒。
-- 演练要端到端跑通,就必须给同一批组**再勾中心的 /admin/data 行**(下面这条就是干这个的)。
-- 这不是"权限放宽"的偷懒——它把两棵树的关系显式写在登记脚本里,避免演练时把它当 bug 报。
INSERT INTO sys_group_module_perm (group_id, module_id, inherit_flag, create_user, create_time)
SELECT @grp_teacher, m.id, 0, @operator, NOW()
FROM app_module m
WHERE m.app_id = @center_app_id AND @grp_teacher > 0 AND m.path = '/admin/data'
AND NOT EXISTS (SELECT 1 FROM sys_group_module_perm p WHERE p.group_id = @grp_teacher AND p.module_id = m.id);
-- ----------------------------------------------------------------------------
-- 步骤 4:验收(对应任务书 T9 演练清单与手册 §2.2"验证"行)
-- ①超管登录中心:/form-center/api/base/GetUserMod 返回步骤 1 的树(前端导航恰 5 项,锚点行不出现)
-- ②无授权用户:只见"我的填写"
-- ③快应用上下文:/form-center/api/base/GetUserMod?appCode=fc-drill 返回步骤 2 那棵树
-- ⚠ 这条要 **H4 上线**(UserBase /oapi/user/modules 的可选 appCode 参数)+ 中心消费侧接线才通;
-- 中心侧当前按 P3 返回空树+warn,见 T9 报告问题单 P78。
-- ④教师组账号进 /form-center/app/fc-drill/data:导出按钮出现(锚点判定生效,P76)
-- ⑤学生组账号:V16 门户可见性由开通闸门决定;进应用只见填报
-- ----------------------------------------------------------------------------
SELECT '中心模块行数(期望 6)' AS chk, COUNT(*) AS n FROM app_module WHERE app_id = @center_app_id
UNION ALL
SELECT '快应用模块行数(期望 5)', COUNT(*) FROM app_module WHERE app_id = @quick_app_id
UNION ALL
SELECT '中心 path 空锚点数(期望 1)', COUNT(*) FROM app_module
WHERE app_id = @center_app_id AND (path IS NULL OR path = '')
UNION ALL
SELECT '已开通中心应用的组数', COUNT(*) FROM sys_group_app_rel WHERE app_id = @center_app_id
UNION ALL
SELECT '已授权中心模块的组-行对数', COUNT(*) FROM sys_group_module_perm p
JOIN app_module m ON m.id = p.module_id WHERE m.app_id = @center_app_id;
-- ----------------------------------------------------------------------------
-- 回滚(演练完要撤就跑这段;顺序与建立相反)
-- DELETE p FROM sys_group_module_perm p JOIN app_module m ON m.id=p.module_id
-- WHERE m.app_id IN (@center_app_id, @quick_app_id);
-- DELETE FROM sys_group_app_rel WHERE app_id IN (@center_app_id, @quick_app_id);
-- DELETE FROM app_module WHERE app_id IN (@center_app_id, @quick_app_id);
-- 应用行不在此删(D7:应用身份由超管在管理端维护,脚本不碰主数据删除)
-- ----------------------------------------------------------------------------