@gadmin2n/schematics 0.0.150 → 0.0.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +125 -36
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +42 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +2 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +0 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +2 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-dev-env.js +218 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +78 -12
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +37 -67
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -17
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +46 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +60 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +3 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +9 -1
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +55 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +491 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.dto.ts +213 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.module.ts +28 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/resource-transfer.service.ts +137 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/role-transfer.service.ts +248 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-token.service.ts +65 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/user-transfer.service.ts +146 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +55 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +2 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +55 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +2 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.spec.ts +46 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.ts +3 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +55 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +2 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +1 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +96 -14
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +41 -41
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.spec.ts +36 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.ts +90 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +4 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +1 -0
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +115 -63
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.spec.ts +166 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.ts +217 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +33 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +57 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +196 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.ts +125 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.controller.ts +9 -3
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +148 -4
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +12 -1
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +14 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +75 -45
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +25 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferExportModal.tsx +115 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferImportModal.tsx +257 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +40 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +15 -13
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +40 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +15 -13
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -1
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +89 -65
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +51 -127
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +53 -109
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +45 -41
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +51 -72
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +49 -40
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +16 -5
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.test.tsx +28 -10
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +60 -1
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +94 -12
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +13 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +3 -3
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +8 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +167 -94
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +95 -95
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.test.tsx +81 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.tsx +126 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +48 -52
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.ts +2 -2
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -3
  79. package/package.json +1 -1
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +0 -8
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/ac.config.ts +0 -24
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +0 -495
@@ -1,41 +1,43 @@
1
1
  ## 项目简介
2
+
2
3
  - 本项目由 lowcode 生成工具 Gadmin2 生成:Admin page(CRUD) + Dashboard,包括前端和后端。
3
4
  - 前端的 Admin page(CRUD) 代码由配置(`config/ui/{ModelName}.ts`)驱动生成,后端的数据模型和 CRUD 代码由 prisma schema(`config/prisma/{module}.prisma`)驱动生成。
5
+ - 项目使用的 ai coding context(`.agent/*`、`.codebuddy/*`)相关文件需要使用 `gadmin2 update --coding-context` 来安装和更新。
4
6
 
5
7
  ## 常用命令
6
8
 
7
9
  ```bash
8
- # 改了 config/prisma/*.prisma —— 迁移数据库 + 生成前后端代码
9
- gadmin2 g prisma:dev
10
-
11
- # 只改了 config/ui/*.ts —— 只生成代码,不碰数据库
12
- gadmin2 g prisma
13
-
14
10
  cd server && yarn start:dev # 后端,端口 8000(优先读 server/.env.local)
15
- cd web && yarn dev # 前端,端口 3000(注意web/ 用 yarn,不用 npm)
16
- cd server && yarn seed # 执行 seed(页面/资源/权限声明)
11
+ cd web && yarn dev # 前端,端口 3000(优先读 web/.env.local)
12
+ cd server && yarn seed # 执行 seed(页面/资源/权限声明)
17
13
  ```
18
14
 
19
- > gadmin2 命令需要在项目根目录执行
15
+ ## 架构概览
20
16
 
21
- ## Architecture Overview
17
+ ### 前端技术栈
22
18
 
23
- ### Frontend Tech Stack
24
19
  React 18, TypeScript, Vite, Ant Design v5, Refinedev, ECharts, i18next
25
20
 
26
21
  ```
27
22
  web/src/
28
- ├── routes/ #页面模块:user/、role/ 等
29
- ├── components/ # 共享 UI 组件(PageHeader、RowActions、SearchBar 等)
30
- ├── hooks/ # 自定义 hooks(useECharts、useDynamicResources、useListPageState 等)
31
- ├── providers/ # auth / data / access control provider
32
- ├── config/ # routeRegistry.tsx(路由注册)、http.ts
33
- ├── constants/# layout.ts(页面布局共享常量)
34
- ├── enums/ # 共享枚举
35
- └── helpers/ # 工具函数(http.ts的 customRequest 等)
23
+ ├── routes/ # 页面模块:user/、role/ 等
24
+ ├── locales/ # i18n 文案:en/、zh_CN/
25
+ ├── components/ # 共享 UI 组件(PageHeader、RowActions、SearchBar 等)
26
+ ├── hooks/ # 自定义 hooks(useECharts、useDynamicResources、useListPageState 等)
27
+ ├── config/ # routeRegistry.tsx(路由注册)、http.ts
28
+ ├── constants/ # layout.ts(页面布局共享常量)
29
+ ├── enums/ # 共享枚举
30
+ ├── helpers/ # 工具函数(http.ts 的 customRequest 等)
31
+ ├── utilities/ # 通用工具(lazy-with-reload.ts)
32
+ ├── generated/ # 生成产物(禁止直接修改)
33
+ ├── dev-shell/ # 本地开发悬浮面板(仅 dev)
34
+ ├── plugins/ # Vite 插件(devShellPlugin.ts)
35
+ ├── authProvider.ts # auth provider(扁平文件,非目录)
36
+ └── auditLogProvider.ts # audit log provider
36
37
  ```
37
38
 
38
- ### Backend Tech Stack
39
+ ### 后端技术栈
40
+
39
41
  NestJS v10, Prisma v6, TypeScript, nest-access-control, Winston logger
40
42
 
41
43
  ```
@@ -43,28 +45,115 @@ server/src/
43
45
  ├── modules/ # 业务模块,每个含 Module → Controller → Service
44
46
  ├── generated/ # Prisma 自动生成(禁止直接修改)
45
47
  ├── lib/ # 核心:auth.guard.ts、roles.guard.ts、taihu.ts、woaAuth.ts、logger.ts
46
- ├── enums/ # 共享枚举
47
- └── utils/ # 工具函数
48
+ ├── config/ # index.ts(端口/日志/prisma/swagger 等运行时配置)
49
+ └── app.module.ts # 根模块(跨切面,改动需确认)
48
50
  ```
49
51
 
50
- ### Permission Model (RBAC)
51
- 参考 `.agent/references/permission-model.md`
52
+ `server/seed/` 存放页面/资源/权限声明的 seed 脚本。
52
53
 
53
- ### 数据库迁移、dev/prod 模式差异、认证绕过等本地开发
54
- 细节见 `.agent/references/local-dev.md`。
54
+ > **RBAC 授权数据在数据库**,由 `RoleService.getBuilder()` 构建成 RolesBuilder 注入 `AccessControlModule`,`RolesRefresherService` 定时刷新。**不要在代码里硬编码角色授权表。**
55
55
 
56
- ### playwright 无头浏览器访问项目页面
57
- 用 `server/scripts/gen-jwt.ts` 生成临时 token。
56
+ ## 参考文档
57
+
58
+ - 权限模型(RBAC):`.agent/references/permission-model.md`
59
+ - 本地开发、dev/prod 模式差异、认证绕过等本地开发细节:`.agent/references/local-dev.md`
60
+ - playwright 无头浏览器访问页面:用 `server/scripts/gen-jwt.ts` 生成临时 token
58
61
 
59
62
  ## 开发原则(必须遵守)
63
+
60
64
  - 使用 TypeScript 严格模式
61
65
  - 严格遵循前、后端既有技术栈,如无必要不要引入新库
62
66
  - 不确定时询问,不要猜;存在歧义时不要默默选择;有更简单的方案要说出来
63
- - 修改 config/ui/ 或 config/prisma/ 配置后,必须在**项目根目录**执行生成命令 `gadmin2 g prisma` 或者 `gadmin2 g prisma:dev`。
64
- - 修改 config/prisma/ 后必须同步检查 server/seed/:raw SQL 引用的列/表、seed 数据的必填字段与字段名要与新 schema 一致,改完需要提醒开发者 yarn seed 验证。
65
- - **优先使用 skill 解决问题** skill 的决策逻辑(配置优先、DB 先于页面等)见 `.agent/references/decision-map.md`
66
- - **默认业务代码范围:** 前端修改限定在 `web/src/routes/`,后端修改限定在 `server/src/modules/`、`server/seed/`。
67
- - **配置驱动源码:** `config/prisma/`、`config/ui/` 可按生成流程修改;标准 CRUD 优先改配置,不要绕过配置直接改生成物。
68
- - **跨切面窄白名单:** `web/src/config/routeRegistry.tsx` 仅可用于注册自定义路由。
69
- - skill 中列出的目标文件不会自动扩大修改权限。除上述范围外,修改 `web/src/components/`、`web/src/hooks/`、`web/src/constants/`、`web/src/providers/`、全局认证文件,以及 `server/src/lib/`、`server/src/app.module.ts`、`server/src/main.ts` 等跨切面文件,必须由用户明确要求或确认具体实施计划,并限定为最小必要改动。
70
- - **生成目录禁止直接修改:** `web/src/generated/`、`server/src/generated/` 只能由生成命令更新。
67
+ - 修改 `config/ui/` 或 `config/prisma/` 后,必须在**项目根目录**执行生成命令:
68
+ - 改 prisma 文件 → 执行 `gadmin2 g prisma:dev`
69
+ - 只改 ui 配置文件 → 执行 `gadmin2 g prisma`
70
+ - 修改 `config/prisma/` 后必须同步检查 `server/seed/`:
71
+ - raw SQL 引用的列/表、seed 数据的必填字段与字段名要与新 schema 一致
72
+ - 改完需要提醒开发者 `yarn seed` 验证
73
+ - **优先使用 skill 解决问题**,具体走哪条路径见下方「项目改动路径决策」
74
+
75
+ ### 代码修改范围
76
+
77
+ | 类别 | 路径 |
78
+ |------|------|
79
+ | 默认可改 | `web/src/routes/`、`web/src/locales/`、`server/src/modules/`、`server/seed/` |
80
+ | 配置驱动源码(按生成流程改) | `config/prisma/`、`config/ui/` |
81
+ | 窄白名单 | `web/src/config/routeRegistry.tsx`(仅用于注册自定义路由) |
82
+ | 禁止直接修改 | `web/src/generated/`、`server/src/generated/`(只能由生成命令更新) |
83
+
84
+ > ⚠️ **以上范围之外的任何文件**,**必须由用户明确要求或确认具体实施计划**,并限定为最小必要改动。包括但不限于:
85
+ >
86
+ > - 前端:`web/src/components/`、`hooks/`、`constants/`、`utilities/`、`authProvider.ts`、`App.tsx`、`i18n.ts`
87
+ > - 后端:`server/src/lib/`、`server/src/config/`、`app.module.ts`、`main.ts` 等跨切面文件
88
+ >
89
+ > skill 与参考文档中列出的目标文件**不会自动扩大修改权限**;若 skill 需要改动上表之外的文件,先向用户确认。
90
+
91
+ ## 项目改动路径决策
92
+
93
+ 本节只回答一个问题:**这次改动该走哪条路径**(配置驱动 / 手写代码 / 数据库先行)。
94
+
95
+ 具体操作由 skill 承载——skill 靠自身 `description` 匹配任务,这里只做分流、不复制 skill 的操作步骤。
96
+
97
+ ### 一、三条核心原则
98
+
99
+ 1. **配置优先**
100
+ - 配置能表达的前端改动一律走 `ui-config-update` skill(无需再问用户),不要手写页面代码、不要绕过配置直接改生成物。
101
+ - 配置无法满足才手写,手写页面统一走 `page-management` skill。
102
+ - 涉及数据库表和字段的变更走 `prisma-schema-management` skill。
103
+ - 后端任何代码改动走 `backend-module` skill。
104
+
105
+ 2. **DB 先于页面**
106
+ - 页面要用的字段在数据库里不存在时,先改 schema + 迁移,再改 UI 配置。
107
+ - 顺序颠倒会生成引用不存在字段的代码。
108
+
109
+ 3. **不凭记忆自己写**
110
+ - 局部定制类需求(列渲染、搜索过滤、批量操作、表单联动、导入导出、i18n、菜单、权限…)都有对应 skill,按需求描述直接触发。
111
+
112
+ ### 二、先定页面类型,再定能否用配置表达
113
+
114
+ **「gadmin2 标准的数据管理页面」的判定标准:** 页面 URL 以 `/admin/data-mngt/` 开头。
115
+
116
+ - 该前缀下的页面全部由生成器产出(`routeRegistry.tsx` 的 `buildDataMngtChildren` 扫描 `generated/resources.tsx` 自动注册)。
117
+ - 与 `config/ui/{ModelName}.ts` 一一对应。
118
+
119
+ > ⚠️ 注意:**CRUD 外形 ≠ gadmin2 标准的数据管理页面。**
120
+ >
121
+ > - `/admin/permission/user`、`/admin/audit-log-list`、`/admin/workflow/node-instances` 等是手写 CRUD,改动走 `page-management`,不要试图改 config/ui。
122
+ > - 新建页面没有 URL 可判时,看需求是否为「业务模型的标准 CRUD」——是则走 prisma 生成路径(生成后自动落在 `/admin/data-mngt/` 下)。
123
+
124
+ ```
125
+ 这个页面是什么类型?
126
+ │
127
+ ├── gadmin2 标准的数据管理页面(CRUD:列表 / 创建 / 编辑 / 详情)
128
+ │ └── 需求能否用 config/ui 表达?
129
+ │ ├── 能 → [ui-config-update] → 根目录 gadmin2 g prisma
130
+ │ └── 不能 → [page-management](手动修改已经生成的 CRUD 页面,样例见其 examples/)
131
+ │
132
+ └── 自定义页面(统计、设置、非 CRUD 交互)→ [page-management]
133
+ ```
134
+
135
+ **判断「能否用 config/ui 表达」的分界线:**
136
+
137
+ - ✅ 配置能力范围内:字段增删、字段顺序、排序、精确匹配搜索、批量操作开关、工具栏按钮增删、表单默认值与联动、详情页字段
138
+ - ✍️ 才需要手写:自定义 Hook、非标准布局、复杂交互、Tree 选择器等
139
+
140
+ ### 三、新建 / 删除页面的联动清单
141
+
142
+ 新建页面和删除页面都会牵涉多处,容易漏。四个环节各有归属 skill,**按环节分流、不要凭记忆自己拼**:
143
+
144
+ | 环节 | 去哪个 skill |
145
+ |------|-------------|
146
+ | 页面代码 | 配置能表达 → `ui-config-update`;需手写 → `page-management` |
147
+ | 路由注册 | `route-registration` |
148
+ | 菜单 | `menu-management` |
149
+ | 角色权限 | `permission-management` |
150
+
151
+ - **新建**:按上表**自上而下**推进 —— 页面代码 → 路由注册 → 菜单 → 授权。
152
+ - **删除**:按**自下而上**回收 —— 回收授权 → 删 `t_page` 行 → 删路由注册 → 删页面代码。
153
+
154
+ > ⚠️ 顺序反了会卡住——`revoke` 要求页面仍存在。只删页面代码会留下菜单残留和权限脏数据,完整的按序清理清单见 `menu-management`。
155
+
156
+ **两个最常踩的联动漏项:**
157
+
158
+ 1. 只建了 `t_page` 没授权 → 菜单**完全不出现**(`t_role_pages` 无行)
159
+ 2. 只有 `t_role_pages` 没有 `t_role_resource` → 菜单能看见但**所有 API 403**;且 `t_role_resource` 不会被定时任务自动补出来,详见 `permission-management`
@@ -40,6 +40,17 @@ enum WorkflowEventDeliveryStatus {
40
40
  FAILED
41
41
  }
42
42
 
43
+ enum WorkflowLifecycleEventType {
44
+ FAILED
45
+ }
46
+
47
+ enum WorkflowLifecycleOutboxStatus {
48
+ PENDING
49
+ PROCESSING
50
+ COMPLETED
51
+ FAILED
52
+ }
53
+
43
54
  enum WorkflowNodeCategory {
44
55
  TRIGGER
45
56
  ACTION
@@ -164,6 +175,7 @@ model WorkflowInstance {
164
175
  nodeExecutions WorkflowNodeExecution[]
165
176
  externalInteractions ExternalInteraction[]
166
177
  eventDelivery WorkflowEventDelivery?
178
+ lifecycleOutboxes WorkflowLifecycleOutbox[]
167
179
 
168
180
  @@unique([workflowId, idempotencyKey], map: "uk_wi_workflow_idempotency") // NULL 不参与唯一性,未提供幂等键的内部执行不受影响
169
181
  @@unique([workflowId, sourceOutboxId], map: "uk_wi_workflow_outbox") // 一个 outbox 事件对一个 workflow 至多一条 instance(NULL 不参与唯一性,正常 execute / cron / webhook 不受影响)
@@ -452,6 +464,36 @@ model WorkflowEventDelivery {
452
464
  @@map("t_workflow_event_delivery")
453
465
  }
454
466
 
467
+ /// Workflow 终态向业务系统的可靠通知。当前只投递事件触发实例的 FAILED 事件。
468
+ /// @IgnoreNestCode
469
+ /// @IgnoreReactCode
470
+ /// @IgnoreAutoField
471
+ model WorkflowLifecycleOutbox {
472
+ id BigInt @id @default(autoincrement())
473
+ eventKey String @unique @map("event_key") @db.VarChar(128)
474
+ workflowInstanceId BigInt @map("workflow_instance_id")
475
+ eventType WorkflowLifecycleEventType @map("event_type")
476
+ workflowCode String @map("workflow_code") @db.VarChar(21)
477
+ businessReference Json @map("business_reference")
478
+ connectionRef String @map("connection_ref") @db.VarChar(64)
479
+ callbackPath String @map("callback_path") @db.VarChar(1024)
480
+ failedAt DateTime @map("failed_at") @db.Timestamptz(3)
481
+ status WorkflowLifecycleOutboxStatus @default(PENDING)
482
+ retryCount Int @default(0) @map("retry_count")
483
+ nextAttemptAt DateTime @default(now()) @map("next_attempt_at")
484
+ lockedAt DateTime? @map("locked_at") @db.Timestamptz(3)
485
+ lockedBy String? @map("locked_by") @db.VarChar(128)
486
+ processedAt DateTime? @map("processed_at") @db.Timestamptz(3)
487
+ lastError String? @map("last_error") @db.Text
488
+ createdAt DateTime @default(now()) @map("created_at")
489
+ updatedAt DateTime @updatedAt @default(now()) @map("updated_at")
490
+
491
+ workflowInstance WorkflowInstance @relation(fields: [workflowInstanceId], references: [id], onDelete: Cascade)
492
+
493
+ @@index([status, nextAttemptAt], map: "idx_wlo_status_next_attempt_at")
494
+ @@map("t_workflow_lifecycle_outbox")
495
+ }
496
+
455
497
  /// @IgnoreReactCode
456
498
  /// @IgnoreAutoField
457
499
  model TemporalTenantConfig {
@@ -6,6 +6,7 @@ generated/
6
6
  build/
7
7
  server/prisma/dbml
8
8
  server/prisma/schema.prisma
9
+ server/.seed-timestamp
9
10
  upload/
10
11
  # Logs
11
12
  logs/
@@ -60,3 +61,4 @@ agent-eval/runs/
60
61
  agent-eval/baseline/
61
62
  tmp/
62
63
  CODEBUDDY.local.md
64
+ .scratch/
@@ -12,6 +12,7 @@
12
12
  "prepare": "cd .. && husky install || true",
13
13
  "start": "dotenv -e .env.local -e .env -- nest start -c gadmin-cli.json",
14
14
  "start:dev": "dotenv -e .env.local -e .env -- nest start -c gadmin-cli.json --watch",
15
+ "prestart:dev": "node scripts/check-dev-env.js prestart",
15
16
  "start:dev:lite": "dotenv -e .env.local -e .env -- sh -c 'nest build -cgadmin-cli.json && concurrently -k -n build,run \"nodemon --config nodemon.lite.json\" \"node --enable-source-maps --watch dist/src/main\"'",
16
17
  "start:debug": "dotenv -e .env.local -e .env -- nest start -c gadmin-cli.json --debug --watch",
17
18
  "start:prod": "/bin/sh start-prod.sh",
@@ -32,6 +33,7 @@
32
33
  "postgadminGenerate": "node scripts/notify-dev-servers.js",
33
34
  "push:db": "dotenv -e .env.local -e .env -- prisma db push --skip-generate",
34
35
  "seed": "dotenv -e .env.local -e .env -- ts-node seed/index.ts",
36
+ "postseed": "node scripts/check-dev-env.js mark-seed",
35
37
  "prisma:studio": "dotenv -e .env.local -e .env -- prisma studio",
36
38
  "gen:jwt": "dotenv -e .env.local -e .env -- ts-node scripts/gen-jwt.ts"
37
39
  },
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 开发环境同步检测:schema / UI 配置 / 迁移 / seed 落后时给出提示(或自动应用迁移)。
4
+ *
5
+ * 两种触发入口共用本脚本,保证提示文案与判断逻辑只有一份:
6
+ *
7
+ * node check-dev-env.js hook <prevSha>
8
+ * git hook 模式(.husky/post-merge、post-rewrite 调用):
9
+ * 按 git diff 的文件变更检测,拉取后立即提示。只提示不执行。
10
+ *
11
+ * node check-dev-env.js prestart
12
+ * server prestart:dev 模式(yarn start:dev 前自动运行):
13
+ * 按本地状态检测(mtime 对比 + prisma migrate status),
14
+ * 兜底覆盖 VS Code 等 GUI 拉代码看不到 hook 输出的场景。
15
+ * 未应用的迁移会自动执行 yarn migrate:deploy(非交互、幂等),其余只提示。
16
+ *
17
+ * node check-dev-env.js mark-seed
18
+ * yarn seed 的 postseed 钩子调用,记录 seed 时间戳供 prestart 对比。
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { execSync, spawnSync } = require('child_process');
24
+
25
+ const SERVER_DIR = path.join(__dirname, '..');
26
+ const ROOT_DIR = path.join(SERVER_DIR, '..');
27
+ const SEED_TS_FILE = path.join(SERVER_DIR, '.seed-timestamp');
28
+
29
+ // ---------- 提示文案(两种模式共用) ----------
30
+
31
+ function warnPrisma(files) {
32
+ console.log('');
33
+ console.log('⚠ 检测到 Prisma schema 变更未同步到本地:');
34
+ (files || []).forEach((f) => console.log(` ${f}`));
35
+ console.log('');
36
+ console.log(' 请在项目根目录运行以下命令,同步本地代码与数据库:');
37
+ console.log(' → gadmin2 g prisma:dev');
38
+ console.log(' (应用新迁移到本地库 + 重新生成后端 CRUD 代码)');
39
+ console.log('');
40
+ }
41
+
42
+ function warnUi(files) {
43
+ console.log('');
44
+ console.log('⚠ 检测到 UI 配置变更未同步到本地:');
45
+ (files || []).forEach((f) => console.log(` ${f}`));
46
+ console.log('');
47
+ console.log(' 请在项目根目录运行以下命令,重新生成前端页面代码:');
48
+ console.log(' → gadmin2 g prisma');
49
+ console.log('');
50
+ }
51
+
52
+ function warnSeed(files) {
53
+ console.log('');
54
+ console.log('⚠ 检测到 seed 文件变更未同步到本地:');
55
+ (files || []).forEach((f) => console.log(` ${f}`));
56
+ console.log('');
57
+ console.log(' 请在 server 目录运行以下命令,同步本地 seed 数据(页面/资源/权限声明):');
58
+ console.log(' → cd server && yarn seed');
59
+ console.log('');
60
+ }
61
+
62
+ // ---------- 工具 ----------
63
+
64
+ /** 目录/ glob 内文件的最新 mtime(毫秒),不存在返回 0 */
65
+ function newestMtime(target) {
66
+ let stat;
67
+ try {
68
+ stat = fs.statSync(target);
69
+ } catch {
70
+ return 0;
71
+ }
72
+ if (stat.isFile()) return stat.mtimeMs;
73
+ let newest = 0;
74
+ for (const entry of fs.readdirSync(target)) {
75
+ newest = Math.max(newest, newestMtime(path.join(target, entry)));
76
+ }
77
+ return newest;
78
+ }
79
+
80
+ function globMtime(dir, ext) {
81
+ let newest = 0;
82
+ let files = [];
83
+ try {
84
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(ext));
85
+ } catch {
86
+ return 0;
87
+ }
88
+ for (const f of files) {
89
+ newest = Math.max(newest, fs.statSync(path.join(dir, f)).mtimeMs);
90
+ }
91
+ return newest;
92
+ }
93
+
94
+ // ---------- hook 模式:按 git diff 文件变更检测 ----------
95
+
96
+ function runHook(prevSha) {
97
+ if (!prevSha) return;
98
+ let changed;
99
+ try {
100
+ changed = execSync(`git diff --name-only ${prevSha} HEAD`, {
101
+ cwd: ROOT_DIR,
102
+ encoding: 'utf8',
103
+ stdio: ['pipe', 'pipe', 'pipe'],
104
+ }).trim();
105
+ } catch {
106
+ return;
107
+ }
108
+ if (!changed) return;
109
+ const files = changed.split('\n');
110
+
111
+ const prismaChanged = files.filter((f) => /^config\/prisma\/.*\.prisma$/.test(f));
112
+ const uiChanged = files.filter((f) => /^config\/ui\/.*\.ts$/.test(f));
113
+ const seedChanged = files.filter((f) => f.startsWith('server/seed/'));
114
+ const migrationsChanged = files.filter((f) => f.startsWith('server/prisma/migrations/'));
115
+
116
+ if (prismaChanged.length) warnPrisma(prismaChanged);
117
+ else if (uiChanged.length) warnUi(uiChanged);
118
+ if (seedChanged.length) warnSeed(seedChanged);
119
+ // gadmin2 g prisma:dev 已包含应用迁移,schema 变更时不重复提示。
120
+ if (migrationsChanged.length && !prismaChanged.length) {
121
+ console.log('');
122
+ console.log('⚠ 检测到本次拉取包含新的数据库迁移:');
123
+ migrationsChanged.forEach((f) => console.log(` ${f}`));
124
+ console.log('');
125
+ console.log(' 请在 server 目录运行以下命令,将新迁移应用到本地数据库:');
126
+ console.log(' → cd server && yarn migrate:deploy');
127
+ console.log('');
128
+ }
129
+ }
130
+
131
+ // ---------- prestart 模式:按本地状态检测 ----------
132
+
133
+ function isPrismaStale() {
134
+ const configMtime = globMtime(path.join(ROOT_DIR, 'config/prisma'), '.prisma');
135
+ if (!configMtime) return false;
136
+ const schemaMtime = newestMtime(path.join(SERVER_DIR, 'prisma/schema.prisma'));
137
+ return configMtime > schemaMtime;
138
+ }
139
+
140
+ function isUiStale() {
141
+ const configMtime = globMtime(path.join(ROOT_DIR, 'config/ui'), '.ts');
142
+ if (!configMtime) return false;
143
+ const generatedMtime = newestMtime(path.join(ROOT_DIR, 'web/src/generated'));
144
+ return !generatedMtime || configMtime > generatedMtime;
145
+ }
146
+
147
+ function isSeedStale() {
148
+ const seedMtime = newestMtime(path.join(SERVER_DIR, 'seed'));
149
+ if (!seedMtime) return false;
150
+ const tsMtime = newestMtime(SEED_TS_FILE);
151
+ return !tsMtime || seedMtime > tsMtime;
152
+ }
153
+
154
+ /** 有未应用的迁移返回 true;DB 不可达等异常返回 null(跳过检测) */
155
+ function hasPendingMigrations() {
156
+ const res = spawnSync('yarn', ['--silent', 'migrate:status'], {
157
+ cwd: SERVER_DIR,
158
+ encoding: 'utf8',
159
+ timeout: 60000,
160
+ });
161
+ if (res.error || res.status === null) return null;
162
+ const out = (res.stdout || '') + (res.stderr || '');
163
+ if (/not yet been applied/i.test(out)) return true;
164
+ if (/Have not been applied|following migration/i.test(out)) return true;
165
+ // 无法连库等场景静默跳过
166
+ if (/Can't reach database|ECONNREFUSED|P1001/i.test(out)) return null;
167
+ return false;
168
+ }
169
+
170
+ function applyMigrations() {
171
+ console.log('');
172
+ console.log('⚠ 检测到有数据库迁移尚未应用到本地库,自动执行 yarn migrate:deploy ...');
173
+ const res = spawnSync('yarn', ['--silent', 'migrate:deploy'], {
174
+ cwd: SERVER_DIR,
175
+ stdio: 'inherit',
176
+ });
177
+ if (res.status !== 0) {
178
+ console.log('');
179
+ console.log('✗ migrate:deploy 执行失败(本地库可能有 drift)。请手动到 server 目录排查:');
180
+ console.log(' → cd server && yarn migrate:status');
181
+ console.log('');
182
+ } else {
183
+ console.log('✓ 迁移已应用到本地库。');
184
+ console.log('');
185
+ }
186
+ }
187
+
188
+ function runPrestart() {
189
+ // schema 过期时 gadmin2 g prisma:dev 会一并应用迁移,跳过 migrate 检测避免重复。
190
+ if (isPrismaStale()) {
191
+ warnPrisma();
192
+ } else if (hasPendingMigrations()) {
193
+ applyMigrations();
194
+ }
195
+ if (isUiStale()) warnUi();
196
+ if (isSeedStale()) warnSeed();
197
+ }
198
+
199
+ // ---------- mark-seed ----------
200
+
201
+ function markSeed() {
202
+ fs.writeFileSync(SEED_TS_FILE, new Date().toISOString() + '\n');
203
+ }
204
+
205
+ // ---------- 入口 ----------
206
+
207
+ const [mode, arg] = process.argv.slice(2);
208
+ try {
209
+ if (mode === 'hook') runHook(arg);
210
+ else if (mode === 'prestart') runPrestart();
211
+ else if (mode === 'mark-seed') markSeed();
212
+ else {
213
+ console.error('usage: check-dev-env.js <hook <prevSha> | prestart | mark-seed>');
214
+ process.exit(1);
215
+ }
216
+ } catch {
217
+ // 检测脚本永不阻断主流程
218
+ }
@@ -20,7 +20,8 @@
20
20
  * --zhName "新名称" (optional)
21
21
  * --enName "New Name" (optional)
22
22
  * --sortOrder 1090 (optional)
23
- * --parentPageCode game (optional)
23
+ * --parentPageCode game (optional — 会校验不形成环)
24
+ * --isVirtual (optional — 从侧边栏隐藏;--isVirtual false 取消隐藏)
24
25
  *
25
26
  * ── 删除页面 ──────────────────────────────────────────────────────────────────
26
27
  * ts-node scripts/page-manage.ts delete \
@@ -85,6 +86,50 @@ async function requirePage(pageCode: string) {
85
86
  return page;
86
87
  }
87
88
 
89
+ /**
90
+ * 校验把 pageId 挂到 newParentId 下不会形成环。
91
+ *
92
+ * 环不会报错,但会静默破坏菜单:useDynamicResources 从 root 做深度优先遍历
93
+ * (addPageWithChildren),环里的页面遍历不到,最终落进 orphan 兜底被 append
94
+ * 到菜单末尾 —— 症状是「菜单项莫名跑到最底部」,极难排查。
95
+ */
96
+ async function assertNoCycle(
97
+ pageId: number,
98
+ pageCode: string,
99
+ newParentId: number,
100
+ ): Promise<void> {
101
+ if (pageId === newParentId) {
102
+ console.error(`[page-manage] "${pageCode}" 不能把自己设为父节点。`);
103
+ process.exit(1);
104
+ }
105
+
106
+ const all = await prisma.page.findMany({
107
+ select: { id: true, code: true, parentId: true },
108
+ });
109
+ const byId = new Map(all.map((p) => [p.id, p]));
110
+
111
+ // seen 兼容库里已经存在的环,避免死循环
112
+ const seen = new Set<number>();
113
+ const trail: string[] = [];
114
+ let cursor: number | null = newParentId;
115
+
116
+ while (cursor != null && !seen.has(cursor)) {
117
+ const node = byId.get(cursor);
118
+ if (!node) break;
119
+ trail.push(node.code);
120
+ if (cursor === pageId) {
121
+ // trail 是从新父节点往 root 走的,反过来才是自上而下的环路径
122
+ const cyclePath = [...trail].reverse().join(' → ');
123
+ console.error(
124
+ `[page-manage] 该父节点是 "${pageCode}" 的后代,会形成环:${cyclePath} → ${pageCode}`,
125
+ );
126
+ process.exit(1);
127
+ }
128
+ seen.add(cursor);
129
+ cursor = node.parentId;
130
+ }
131
+ }
132
+
88
133
  // ─── Actions ──────────────────────────────────────────────────────────────────
89
134
 
90
135
  async function actionCreate(flags: Record<string, string>) {
@@ -101,11 +146,19 @@ async function actionCreate(flags: Record<string, string>) {
101
146
  console.error('[page-manage] --pageCode is required.');
102
147
  process.exit(1);
103
148
  }
149
+ if (parentPageCode && parentPageCode === pageCode) {
150
+ console.error(`[page-manage] "${pageCode}" 不能把自己设为父节点。`);
151
+ process.exit(1);
152
+ }
104
153
 
105
154
  const parentId = parentPageCode
106
155
  ? await resolveParent(parentPageCode)
107
156
  : undefined;
108
157
 
158
+ // create 走 upsertPage,其 update 为 {} —— 页面已存在时不会改任何字段。
159
+ // 先查一次拿到准确的存在性(不要用 createdAt/updatedAt 的时间差去猜)。
160
+ const existing = await prisma.page.findUnique({ where: { code: pageCode } });
161
+
109
162
  const page = await upsertPage(prisma, {
110
163
  code: pageCode,
111
164
  name: pageName || pageCode,
@@ -115,14 +168,15 @@ async function actionCreate(flags: Record<string, string>) {
115
168
  parentId,
116
169
  });
117
170
 
118
- const wasCreated =
119
- Math.abs(page.createdAt.getTime() - page.updatedAt.getTime()) < 1000;
120
- if (wasCreated) {
121
- console.log(`[page-manage] ✚ Created page: ${pageCode} (id=${page.id})`);
122
- } else {
171
+ if (existing) {
172
+ console.log(
173
+ `[page-manage] · Page already exists: ${pageCode} (id=${page.id}) — 未改动任何字段。`,
174
+ );
123
175
  console.log(
124
- `[page-manage] · Page already exists: ${pageCode} (id=${page.id}) — skipped`,
176
+ `[page-manage] 要修改请用: page-manage.ts update --pageCode ${pageCode} ...`,
125
177
  );
178
+ } else {
179
+ console.log(`[page-manage] ✚ Created page: ${pageCode} (id=${page.id})`);
126
180
  }
127
181
 
128
182
  if (resources) {
@@ -145,8 +199,15 @@ async function actionCreate(flags: Record<string, string>) {
145
199
  }
146
200
 
147
201
  async function actionUpdate(flags: Record<string, string>) {
148
- const { pageCode, pageName, zhName, enName, parentPageCode, sortOrder } =
149
- flags;
202
+ const {
203
+ pageCode,
204
+ pageName,
205
+ zhName,
206
+ enName,
207
+ parentPageCode,
208
+ sortOrder,
209
+ isVirtual,
210
+ } = flags;
150
211
  if (!pageCode) {
151
212
  console.error('[page-manage] --pageCode is required.');
152
213
  process.exit(1);
@@ -159,12 +220,17 @@ async function actionUpdate(flags: Record<string, string>) {
159
220
  if (zhName !== undefined) data.zhName = zhName;
160
221
  if (enName !== undefined) data.enName = enName;
161
222
  if (sortOrder !== undefined) data.sortOrder = parseInt(sortOrder, 10);
162
- if (parentPageCode !== undefined)
163
- data.parentId = await resolveParent(parentPageCode);
223
+ // 裸 --isVirtual 被 parseArgs 解析成 'true';仅 --isVirtual false 取消隐藏
224
+ if (isVirtual !== undefined) data.isVirtual = isVirtual !== 'false';
225
+ if (parentPageCode !== undefined) {
226
+ const newParentId = await resolveParent(parentPageCode);
227
+ await assertNoCycle(page.id, page.code, newParentId);
228
+ data.parentId = newParentId;
229
+ }
164
230
 
165
231
  if (Object.keys(data).length === 0) {
166
232
  console.log(
167
- '[page-manage] No fields to update. Pass at least one of: --pageName --zhName --enName --sortOrder --parentPageCode',
233
+ '[page-manage] No fields to update. Pass at least one of: --pageName --zhName --enName --sortOrder --parentPageCode --isVirtual',
168
234
  );
169
235
  process.exit(0);
170
236
  }