@microi.net/cli 4.6.2
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.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/mcp-codex-stdio-adapter.js +189 -0
- package/dist/mcp-server.js +972 -0
- package/dist/mcp-trae-windows-launcher.cmd +21 -0
- package/dist/microi-cli-mcp.js +7 -0
- package/dist/microi-cli.js +1645 -0
- package/dist/microi-skills.meta.json +335 -0
- package/dist/microi.skills/.microi-skills-version.json +6 -0
- package/dist/microi.skills/README.md +276 -0
- package/dist/microi.skills/ai-engine/SKILL.md +140 -0
- package/dist/microi.skills/ai-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/app-store/SKILL.md +105 -0
- package/dist/microi.skills/app-store/agents/openai.yaml +4 -0
- package/dist/microi.skills/business-blueprint/SKILL.md +184 -0
- package/dist/microi.skills/datasource-engine/SKILL.md +89 -0
- package/dist/microi.skills/datasource-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/dos-orm/SKILL.md +76 -0
- package/dist/microi.skills/dos-orm/references/api-reference.md +229 -0
- package/dist/microi.skills/job-engine/SKILL.md +141 -0
- package/dist/microi.skills/job-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/message-notification/SKILL.md +113 -0
- package/dist/microi.skills/message-notification/agents/openai.yaml +6 -0
- package/dist/microi.skills/message-notification/references/contracts.md +99 -0
- package/dist/microi.skills/microi-ai-app-auth.js +651 -0
- package/dist/microi.skills/microi-ai-application/SKILL.md +80 -0
- package/dist/microi.skills/microi-ai-application/agents/openai.yaml +4 -0
- package/dist/microi.skills/microi-ai-application/references/frontend-baseline.md +164 -0
- package/dist/microi.skills/microi-client-frontend/SKILL.md +562 -0
- package/dist/microi.skills/microi-datasource-mapping/SKILL.md +108 -0
- package/dist/microi.skills/microi-db-schema/SKILL.md +170 -0
- package/dist/microi.skills/microi-db-schema/agents/openai.yaml +4 -0
- package/dist/microi.skills/microi-db-schema/references/core-tables.md +695 -0
- package/dist/microi.skills/microi-db-schema/references/form-component-options.md +256 -0
- package/dist/microi.skills/microi-db-schema/references/schema-overview.md +203 -0
- package/dist/microi.skills/microi-db-schema/references/schema.md +647 -0
- package/dist/microi.skills/microi-db-schema/references/table-catalog.md +1607 -0
- package/dist/microi.skills/microi-deployment/SKILL.md +117 -0
- package/dist/microi.skills/microi-deployment/references/deployment-matrix.md +94 -0
- package/dist/microi.skills/microi-docs-coverage/SKILL.md +91 -0
- package/dist/microi.skills/microi-docs-coverage/references/capability-map.md +65 -0
- package/dist/microi.skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +887 -0
- package/dist/microi.skills/microi-form-engine/SKILL.md +159 -0
- package/dist/microi.skills/microi-form-engine/references/component-catalog.md +116 -0
- package/dist/microi.skills/microi-form-engine/references/data-source-events.md +117 -0
- package/dist/microi.skills/microi-form-layout/SKILL.md +373 -0
- package/dist/microi.skills/microi-frontend-sdk/SKILL.md +304 -0
- package/dist/microi.skills/microi-left-right-layout/SKILL.md +132 -0
- package/dist/microi.skills/microi-microservice/SKILL.md +115 -0
- package/dist/microi.skills/microi-microservice/references/runtime-delivery.md +145 -0
- package/dist/microi.skills/microi-mobile-app-quality/SKILL.md +436 -0
- package/dist/microi.skills/microi-solution-quotation/SKILL.md +76 -0
- package/dist/microi.skills/microi-solution-quotation/agents/openai.yaml +4 -0
- package/dist/microi.skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -0
- package/dist/microi.skills/microi-system-delivery/SKILL.md +446 -0
- package/dist/microi.skills/microi-ui/SKILL.md +321 -0
- package/dist/microi.skills/microi-uniapp-frontend/SKILL.md +483 -0
- package/dist/microi.skills/microi.v8.js +1758 -0
- package/dist/microi.skills/module-engine/SKILL.md +131 -0
- package/dist/microi.skills/module-engine/references/module-config.md +174 -0
- package/dist/microi.skills/page-engine/SKILL.md +397 -0
- package/dist/microi.skills/performance-testing/SKILL.md +207 -0
- package/dist/microi.skills/playwright-e2e/SKILL.md +769 -0
- package/dist/microi.skills/print-engine/SKILL.md +237 -0
- package/dist/microi.skills/production-readonly-audit/SKILL.md +39 -0
- package/dist/microi.skills/report-engine/SKILL.md +69 -0
- package/dist/microi.skills/report-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/search-engine/SKILL.md +73 -0
- package/dist/microi.skills/search-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/spider-engine/SKILL.md +188 -0
- package/dist/microi.skills/translate-engine/SKILL.md +91 -0
- package/dist/microi.skills/translate-engine/agents/openai.yaml +4 -0
- package/dist/microi.skills/ui-design/SKILL.md +1575 -0
- package/dist/microi.skills/ui-design/assets/pattern-showcase/app.js +54 -0
- package/dist/microi.skills/ui-design/assets/pattern-showcase/index.html +163 -0
- package/dist/microi.skills/ui-design/assets/pattern-showcase/styles.css +311 -0
- package/dist/microi.skills/ui-design/assets/templates/MCI-DESIGN.md +98 -0
- package/dist/microi.skills/ui-design/references/design-pattern-library.md +171 -0
- package/dist/microi.skills/ui-design/references/mci-design-contract.md +84 -0
- package/dist/microi.skills/ui-design/references/motion-and-media.md +71 -0
- package/dist/microi.skills/ui-design/references/product-flow-recipes.md +94 -0
- package/dist/microi.skills/uniapp-mall-assets/SKILL.md +105 -0
- package/dist/microi.skills/v8-api-config/SKILL.md +272 -0
- package/dist/microi.skills/v8-cache-pattern/SKILL.md +286 -0
- package/dist/microi.skills/v8-crud-api/SKILL.md +398 -0
- package/dist/microi.skills/v8-debugging/SKILL.md +279 -0
- package/dist/microi.skills/v8-explorer-tree/SKILL.md +224 -0
- package/dist/microi.skills/v8-export-import/SKILL.md +590 -0
- package/dist/microi.skills/v8-file-upload/SKILL.md +497 -0
- package/dist/microi.skills/v8-formengine-http/SKILL.md +218 -0
- package/dist/microi.skills/v8-frontend-events/SKILL.md +349 -0
- package/dist/microi.skills/v8-frontend-events/references/bluetooth-print-api.md +107 -0
- package/dist/microi.skills/v8-frontend-events/references/bluetooth-print.md +185 -0
- package/dist/microi.skills/v8-http-integration/SKILL.md +379 -0
- package/dist/microi.skills/v8-image-processing/SKILL.md +187 -0
- package/dist/microi.skills/v8-image-processing/agents/openai.yaml +4 -0
- package/dist/microi.skills/v8-image-processing/references/api-reference.md +620 -0
- package/dist/microi.skills/v8-menu-buttons/SKILL.md +661 -0
- package/dist/microi.skills/v8-mongodb/SKILL.md +149 -0
- package/dist/microi.skills/v8-mq-mqtt/SKILL.md +227 -0
- package/dist/microi.skills/v8-saas-multi-tenant/SKILL.md +193 -0
- package/dist/microi.skills/v8-security/SKILL.md +417 -0
- package/dist/microi.skills/v8-sql-query/SKILL.md +290 -0
- package/dist/microi.skills/v8-table-event/SKILL.md +385 -0
- package/dist/microi.skills/v8-template-engine/SKILL.md +165 -0
- package/dist/microi.skills/v8-utilities/SKILL.md +79 -0
- package/dist/microi.skills/v8-utilities/references/client-api-index.md +136 -0
- package/dist/microi.skills/v8-utilities/references/platform-http-routes.md +80 -0
- package/dist/microi.skills/v8-utilities/references/server-api-index.md +129 -0
- package/dist/microi.skills/v8-workflow/SKILL.md +322 -0
- package/dist/microi.skills/workspace-conventions/SKILL.md +479 -0
- package/package.json +40 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: v8-sql-query
|
|
3
|
+
description: Microi V8 安全 SQL 查询指南。用于选择 V8.FormEngine _Where 或 V8.Db.FromSql,处理参数化查询、联表、聚合并避免 SQL 注入。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Microi V8 安全 SQL 查询
|
|
7
|
+
|
|
8
|
+
你正在开发 Microi 吾码平台的 V8 引擎代码。数据库查询有两种方式:`V8.FormEngine`(推荐)和 `V8.Db`(原始 SQL)。必须遵守安全规范。
|
|
9
|
+
|
|
10
|
+
## 性能门禁(必须执行)
|
|
11
|
+
|
|
12
|
+
写接口引擎前必须先做数据访问计划,避免“循环套循环查数据库”:
|
|
13
|
+
|
|
14
|
+
- 禁止在 `for` / `while` / `forEach` / `map` 循环内调用 `V8.FormEngine.GetFormData`、`GetTableData`、`V8.Db.FromSql`、`V8.ApiEngine.Run` 或 `V8.Http.*`。确实无法避免时,必须先说明原因,并加分页、缓存或限流。
|
|
15
|
+
- 多条 Id、编码、外键查询必须一次性用 `_Where: [['Id','In', ids]]`、SQL `IN`、JOIN 或聚合查询取回,再用内存字典映射。
|
|
16
|
+
- 需要父子、主从、用户/部门/角色名称映射时,先批量取关联表,只保留 `_SelectFields` 必要列,不要逐行查名称。
|
|
17
|
+
- 统计、计数、汇总优先让数据库一次 `GROUP BY` / `COUNT` / `SUM` 完成,不要把大表全部拉到 V8 里循环统计。
|
|
18
|
+
- 列表接口必须限制 `_PageSize`,管理端默认不要超过 100,导出或批处理必须显式分批。
|
|
19
|
+
- 每次查询都要写 `_SelectFields` 或明确 SQL 字段列表,禁止 `SELECT *` 用在大表、接口列表、循环前置查询中。
|
|
20
|
+
- 外部 HTTP、翻译、短信、AI 等慢调用不能放在数据库事务和大循环中;要么异步队列,要么批量预处理并设置超时。
|
|
21
|
+
- 返回前自检一次:数据库访问次数应与数据量无关或近似常数级,不能随着行数线性增长为 N 次查询。
|
|
22
|
+
|
|
23
|
+
## 首选:V8.FormEngine + _Where(自动防注入)
|
|
24
|
+
|
|
25
|
+
`_Where` 是参数化查询语法,自动防 SQL 注入,**永远优先使用**。
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// ✅ 安全:_Where 自动参数化
|
|
29
|
+
var result = V8.FormEngine.GetTableData('SysUser', {
|
|
30
|
+
_Where: [
|
|
31
|
+
['Account', '=', V8.Param.account],
|
|
32
|
+
['AND', 'Status', '=', 1]
|
|
33
|
+
],
|
|
34
|
+
_PageIndex: 1,
|
|
35
|
+
_PageSize: 20
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### _Where 完整语法
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// 基本条件
|
|
43
|
+
[['Field', '操作符', value]]
|
|
44
|
+
|
|
45
|
+
// 操作符:=, ==, <>, !=, >, >=, <, <=, Like, NotLike, StartLike, EndLike, In, NotIn
|
|
46
|
+
|
|
47
|
+
// 多条件 AND
|
|
48
|
+
[['A', '=', 1], ['AND', 'B', '>', 10]]
|
|
49
|
+
|
|
50
|
+
// 多条件 OR
|
|
51
|
+
[['A', '=', 1], ['OR', 'B', '=', 2]]
|
|
52
|
+
|
|
53
|
+
// IN 查询
|
|
54
|
+
[['Id', 'In', ['id1', 'id2', 'id3']]]
|
|
55
|
+
|
|
56
|
+
// NULL 判断
|
|
57
|
+
[['Field', '=', null]] // IS NULL
|
|
58
|
+
[['Field', '<>', null]] // IS NOT NULL
|
|
59
|
+
|
|
60
|
+
// 分组(括号):(Age > 18 OR Status = 1)
|
|
61
|
+
[['Name', 'Like', '张'], ['AND', '(', 'Age', '>', 18], ['OR', 'Status', '=', 1, ')']]
|
|
62
|
+
|
|
63
|
+
// 日期范围
|
|
64
|
+
[['CreateTime', '>=', '2024-01-01'], ['AND', 'CreateTime', '<', '2024-02-01']]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 旧版 _Where 兼容(V8.Method.ParseWhere)
|
|
68
|
+
|
|
69
|
+
老版本前端可能传旧格式(`{Name, Value, Type, AndOr, Group}`),转换成新格式:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
var newWhere = V8.Method.ParseWhere(V8.Param._Where);
|
|
73
|
+
V8.FormEngine.GetTableData('Table', { _Where: newWhere });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 次选:V8.Db.FromSql(仅 SQL 字符串 + AddInParameter)
|
|
77
|
+
|
|
78
|
+
> **⚠️ FormEngine 优先原则:** 增删改操作(INSERT / UPDATE / DELETE)必须**优先**使用 `V8.FormEngine.AddFormData` / `UptFormData` / `UptFormDataByWhere` / `DelFormData` 等方法。**只有**多表 JOIN、复杂子查询、GROUP BY 聚合等 FormEngine 无法表达的场景才使用 `V8.Db.FromSql`。
|
|
79
|
+
|
|
80
|
+
> **⚠️ FromSql 调用规则:** `V8.Db.FromSql` 在 V8 中只传 SQL 字符串,不要把动态值作为第二个或后续参数传给 `FromSql`。动态值必须用链式 `.AddInParameter('@p0', value)` 绑定;否则会生成平台不支持的调用签名。
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
// ❌ 错误形态:不要把动态值作为 FromSql 的第二个参数传入
|
|
84
|
+
|
|
85
|
+
// ✅ 优先:改用 FormEngine(单表增删改查都优先这样写)
|
|
86
|
+
V8.FormEngine.UptFormData('t', { Id: id, A: val1, B: val2 });
|
|
87
|
+
|
|
88
|
+
// ✅ 必须用原生 SQL 时:FromSql 只传 SQL,参数用 AddInParameter
|
|
89
|
+
V8.Db.FromSql("UPDATE t SET A=@p0, B=@p1 WHERE Id=@p2")
|
|
90
|
+
.AddInParameter("@p0", val1)
|
|
91
|
+
.AddInParameter("@p1", val2)
|
|
92
|
+
.AddInParameter("@p2", id)
|
|
93
|
+
.ExecuteNonQuery();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
当 `_Where` 无法满足复杂查询(多表 JOIN、子查询、聚合统计)时使用 `V8.Db`:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
// ✅ 安全:使用 @p0, @p1 占位符,并用 AddInParameter 绑定
|
|
100
|
+
var list = V8.Db.FromSql(
|
|
101
|
+
'SELECT a.Id, a.Name, b.OrderCount FROM Customer a LEFT JOIN (SELECT CustomerId, COUNT(*) OrderCount FROM OrderHeader GROUP BY CustomerId) b ON a.Id = b.CustomerId WHERE a.Status = @p0'
|
|
102
|
+
).AddInParameter("@p0", 1).ToArray();
|
|
103
|
+
|
|
104
|
+
// ✅ 安全:多个参数
|
|
105
|
+
var row = V8.Db.FromSql(
|
|
106
|
+
'SELECT * FROM SysUser WHERE Account = @p0 AND DeptId = @p1'
|
|
107
|
+
).AddInParameter("@p0", V8.Param.account)
|
|
108
|
+
.AddInParameter("@p1", V8.Param.deptId)
|
|
109
|
+
.First();
|
|
110
|
+
|
|
111
|
+
// 统计
|
|
112
|
+
var count = V8.Db.FromSql(
|
|
113
|
+
'SELECT COUNT(*) FROM OrderHeader WHERE Status = @p0 AND CreateTime >= @p1'
|
|
114
|
+
).AddInParameter("@p0", 1)
|
|
115
|
+
.AddInParameter("@p1", V8.Param.startDate)
|
|
116
|
+
.ToScalar();
|
|
117
|
+
|
|
118
|
+
// 非查询(UPDATE / INSERT / DELETE)
|
|
119
|
+
V8.Db.FromSql(
|
|
120
|
+
'UPDATE SysUser SET LastLoginTime = @p0 WHERE Id = @p1'
|
|
121
|
+
).AddInParameter("@p0", DateNow('yyyy-MM-dd HH:mm:ss'))
|
|
122
|
+
.AddInParameter("@p1", V8.CurrentUser.Id)
|
|
123
|
+
.ExecuteNonQuery();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> 老版本兼容:要把维护接口复制到旧部署时,不要假定 `DateNow` 或 `System.DateTime.Now.ToString(...)` 一定可用。若数据库类型固定,可直接使用该数据库的当前时间表达式(例如 MySQL 的 `NOW()`);跨数据库代码则应按数据库类型选择表达式或传入平台已确认支持的时间值。
|
|
127
|
+
|
|
128
|
+
### V8.Db 方法速查
|
|
129
|
+
|
|
130
|
+
| 方法 | 返回 | 用途 |
|
|
131
|
+
|------|------|------|
|
|
132
|
+
| `.ToArray()` | 数组 | 查询多条 |
|
|
133
|
+
| `.First()` | 对象 \| null | 查询单条 |
|
|
134
|
+
| `.ToScalar()` | 单值 | COUNT / MAX / SUM 等 |
|
|
135
|
+
| `.ExecuteNonQuery()` | 影响行数 | UPDATE / DELETE / INSERT |
|
|
136
|
+
|
|
137
|
+
> 别名:`.ToList()` = `.ToArray()`,`.ToModel()` = `.First()`,`.ExecuteScalar()` = `.ToScalar()`
|
|
138
|
+
|
|
139
|
+
### 读写分离
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
V8.Db.FromSql(...) // 主库(读写)
|
|
143
|
+
V8.DbRead.FromSql(...) // 从库(只读,适合报表和大量查询)
|
|
144
|
+
// 未部署读写分离时 V8.DbRead 与 V8.Db 一致
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 跨应用查询(扩展数据库)
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
var list = V8.Dbs.OracleDB1.FromSql('SELECT * FROM Table WHERE Id = @p0')
|
|
151
|
+
.AddInParameter("@p0", id)
|
|
152
|
+
.ToArray();
|
|
153
|
+
|
|
154
|
+
// 不写入 microi_database:创建仅当前请求使用的临时会话
|
|
155
|
+
var tempDb = V8.Dbs.Open(
|
|
156
|
+
'SqlServer',
|
|
157
|
+
'Server=127.0.0.1,1433;Database=app;User Id=user;Password=***;TrustServerCertificate=True;'
|
|
158
|
+
);
|
|
159
|
+
var tempList = tempDb.FromSql('SELECT Id, Name FROM Customer WHERE Status = @p0')
|
|
160
|
+
.AddInParameter('@p0', 1)
|
|
161
|
+
.ToArray();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`V8.Dbs.Open` 与保存连接均只支持 Dos.ORM 已认证的 `MySql`、`SqlServer`、`Oracle`、`PostgreSql`、`DaMeng`、`KingBase`。动态连接串只能来自可信服务端密钥或管理员代码,禁止使用 `V8.Param.ConnectionString`,禁止记录或返回。外部 SQL 的表名、列名、排序字段必须来自已校验元数据白名单;动态值继续使用 `AddInParameter`。
|
|
165
|
+
|
|
166
|
+
MCP 结构发现使用 `microi_inspect_external_database`,安全抽样默认使用只读的 `microi_query_external_database`。当用户明确要求数据库管理级能力时,使用独立的 `microi_execute_external_database`:它只允许后端确认的 `Level >= 9999` 当前用户调用,显式确认后可执行目标数据库账号有权执行的任意 DML、DDL、存储过程、数据库原生命令或多语句。输出行数限制只保护 MCP 传输,不限制数据库副作用;审计只记录 SQL 哈希、长度、模式和结果,不得记录 SQL 正文、连接串或密码。
|
|
167
|
+
|
|
168
|
+
不要用 `microi_get_db_schema` 读取第三方库,也不要在对话中搬运全库数据;持续同步应创建参数化、分页、幂等的服务端任务。最高权限入口不等于跨租户,也不能超越目标数据库账号自身权限。
|
|
169
|
+
|
|
170
|
+
## 数据库事务
|
|
171
|
+
|
|
172
|
+
### 接口引擎事务(自动管理)
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
// 接口引擎中 V8.Db 自动开启事务:
|
|
176
|
+
// 返回 Code=1 → 自动提交事务
|
|
177
|
+
// 返回 Code≠1 → 自动回滚事务
|
|
178
|
+
// 手动调用 V8.DbTrans.Commit() 或 V8.DbTrans.Rollback() 均无效
|
|
179
|
+
V8.Db.FromSql('UPDATE Account SET Balance = Balance - @p0 WHERE Id = @p1')
|
|
180
|
+
.AddInParameter("@p0", 100)
|
|
181
|
+
.AddInParameter("@p1", fromId)
|
|
182
|
+
.ExecuteNonQuery();
|
|
183
|
+
V8.Db.FromSql('UPDATE Account SET Balance = Balance + @p0 WHERE Id = @p1')
|
|
184
|
+
.AddInParameter("@p0", 100)
|
|
185
|
+
.AddInParameter("@p1", toId)
|
|
186
|
+
.ExecuteNonQuery();
|
|
187
|
+
|
|
188
|
+
// V8.DbTrans 可传给 FormEngine 和 ApiEngine.Run 共享事务
|
|
189
|
+
V8.FormEngine.UptFormData('Table1', { Id: 'x', Status: 1 }, V8.DbTrans);
|
|
190
|
+
V8.ApiEngine.Run('other-engine', { Id: 'x' }, V8.DbTrans);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 扩展数据库事务(手动管理)
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
// 扩展数据库需要手动管理事务
|
|
197
|
+
var exTrans = V8.Dbs.OracleDB1.BeginTransaction();
|
|
198
|
+
try {
|
|
199
|
+
exTrans.FromSql('UPDATE t1 SET a = @p0 WHERE Id = @p1')
|
|
200
|
+
.AddInParameter("@p0", 1)
|
|
201
|
+
.AddInParameter("@p1", id1)
|
|
202
|
+
.ExecuteNonQuery();
|
|
203
|
+
exTrans.FromSql('UPDATE t2 SET b = @p0 WHERE Id = @p1')
|
|
204
|
+
.AddInParameter("@p0", 2)
|
|
205
|
+
.AddInParameter("@p1", id2)
|
|
206
|
+
.ExecuteNonQuery();
|
|
207
|
+
exTrans.Commit();
|
|
208
|
+
} catch (ex) {
|
|
209
|
+
exTrans.Rollback();
|
|
210
|
+
} finally {
|
|
211
|
+
exTrans.Close(); // 必须释放事务对象
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## 绝对禁止
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// ❌ 绝对禁止:拼接 SQL 字符串
|
|
219
|
+
var sql = "SELECT * FROM SysUser WHERE Account = '" + V8.Param.account + "'";
|
|
220
|
+
V8.Db.FromSql(sql).ToArray(); // SQL 注入漏洞!
|
|
221
|
+
|
|
222
|
+
// ❌ 禁止:动态拼接表名
|
|
223
|
+
var sql = "SELECT * FROM " + V8.Param.tableName + " WHERE Id = @p0";
|
|
224
|
+
|
|
225
|
+
// ✅ 正确做法:单表查询优先使用 FormEngine + _Where
|
|
226
|
+
var result = V8.FormEngine.GetTableData('SysUser', {
|
|
227
|
+
_Where: [['Account', '=', V8.Param.account]],
|
|
228
|
+
_PageSize: 20
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 常见查询模式
|
|
233
|
+
|
|
234
|
+
### 分页查询
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
var pageIndex = parseInt(V8.Param.pageIndex) || 1;
|
|
238
|
+
var pageSize = Math.min(parseInt(V8.Param.pageSize) || 20, 100); // 限制最大100
|
|
239
|
+
|
|
240
|
+
var result = V8.FormEngine.GetTableData('TableName', {
|
|
241
|
+
_Where: [['Status', '=', 1]],
|
|
242
|
+
_OrderBy: 'CreateTime',
|
|
243
|
+
_OrderByType: 'DESC',
|
|
244
|
+
_PageIndex: pageIndex,
|
|
245
|
+
_PageSize: pageSize
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return { Code: 1, Data: result.Data, Total: result.DataCount };
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 模糊搜索(多字段)
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
var keyword = V8.Param.keyword;
|
|
255
|
+
var where = [['Status', '=', 1]];
|
|
256
|
+
if (keyword) {
|
|
257
|
+
where.push(['AND', '(', 'Name', 'Like', keyword]);
|
|
258
|
+
where.push(['OR', 'Code', 'Like', keyword]);
|
|
259
|
+
where.push(['OR', 'Phone', 'Like', keyword, ')']);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var result = V8.FormEngine.GetTableData('Customer', {
|
|
263
|
+
_Where: where,
|
|
264
|
+
_PageIndex: 1,
|
|
265
|
+
_PageSize: 20
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 关联查询(SQL JOIN)
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
var list = V8.Db.FromSql(`
|
|
273
|
+
SELECT o.Id, o.OrderNo, o.TotalAmount, c.Name AS CustomerName
|
|
274
|
+
FROM OrderHeader o
|
|
275
|
+
INNER JOIN Customer c ON o.CustomerId = c.Id
|
|
276
|
+
WHERE o.Status = @p0 AND o.CreateTime >= @p1
|
|
277
|
+
ORDER BY o.CreateTime DESC
|
|
278
|
+
`).AddInParameter("@p0", 1)
|
|
279
|
+
.AddInParameter("@p1", V8.Param.startDate)
|
|
280
|
+
.ToArray();
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## 注意事项
|
|
284
|
+
|
|
285
|
+
- `V8.Db.FromSql` 只传 SQL 字符串,参数占位符从 `@p0` 开始递增,动态值用 `.AddInParameter("@p0", value)` 绑定
|
|
286
|
+
- 服务端 `V8.FormEngine` 操作默认不触发表单 V8 事件;确需触发时在参数中加 `_InvokeType: 'Client'`
|
|
287
|
+
- 查询结果数量较大时务必分页,`_PageSize` 默认最大 1000
|
|
288
|
+
- `V8.DbRead` 适用于不需要实时性的报表查询
|
|
289
|
+
- 接口引擎的事务由平台自动管理,**不要手动调用** `V8.DbTrans.Commit/Rollback`
|
|
290
|
+
- 扩展数据库事务必须手动调用 `BeginTransaction/Commit/Rollback/Close`
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: v8-table-event
|
|
3
|
+
description: Microi V8 表单事件开发。用于编写 InFormV8、SubmitFormV8、SubmitBeforeServerV8、SubmitAfterServerV8、OutFormV8、DataFilterV8 和事务感知事件。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Microi V8 表单事件开发
|
|
7
|
+
|
|
8
|
+
你正在开发 Microi 吾码平台的 V8 表单事件。事件绑定在表单引擎的表上,在数据操作的不同阶段自动触发。
|
|
9
|
+
|
|
10
|
+
## 本地优先与版本头(必做)
|
|
11
|
+
|
|
12
|
+
AI 本地开发表单 V8 事件时,优先修改 `microi-v8-engine/<租户>/<项目>/表单引擎/.../<事件Label>(<EventType>).js` 本地文件,再通过 MCP 或 VS Code 插件同步到数据库。文件中文名必须取 `diy_table` 对应 `diy_field.Label`,其中 `SubmitFormV8` 为 `前端表单提交前V8事件(SubmitFormV8).js`,`OutFormV8` 为 `前端表单提交后V8事件(OutFormV8).js`。若插件显示“本地和远端不一致”,先读取本地与远端并合并有效差异,不能直接覆盖。
|
|
13
|
+
|
|
14
|
+
每一次修改、上传、推送 `InFormV8.js`、`SubmitFormV8.js`、`SubmitBeforeServerV8.js`、`SubmitAfterServerV8.js`、`OutFormV8.js`、`DataFilterV8.js` 等事件文件,都必须维护顶部版本区域。版本号从 `v1.0.0` 开始;每次上传/推送/修改递增 1;补丁位和次版本位最大为 9 并向前进位(`v1.0.9 -> v1.1.0`、`v1.9.9 -> v2.0.0`、`v9.9.9 -> v10.0.0`)。代码头只写完整功能说明,不写修改历史、时间戳或 ChangeLog。
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
/*
|
|
18
|
+
* V8 Event
|
|
19
|
+
* TableKey: 示例表Key
|
|
20
|
+
* EventType: SubmitBeforeServerV8
|
|
21
|
+
* Version: v1.0.0
|
|
22
|
+
* 功能说明:
|
|
23
|
+
* - 完整说明该事件的触发时机、校验/加工逻辑、读写字段和阻止提交条件。
|
|
24
|
+
*/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
同步流程:`读取远端 -> 修改本地并递增语义版本头 -> JS 语法检查 -> 保存远端 -> 回读远端确认版本头一致 -> 查询 mic_data_version 确认形成新版本 -> 执行触发该事件的最小表单操作验证`。表单设计器保存和 MCP 保存共用服务端代码版本链路;只有代码真实变化才新增版本,普通布局/字段属性保存不得产生代码版本。修改记录不得写进事件代码头。
|
|
28
|
+
|
|
29
|
+
使用 MCP 时,先调用 `microi_list_events` 发现事件,再用
|
|
30
|
+
`microi_get_event_code` 读取目标源码;修改后通过 `microi_save_event_code`
|
|
31
|
+
保存并回读。不要把接口引擎的保存工具用于表单事件,也不要在未读取远端
|
|
32
|
+
版本时直接覆盖。
|
|
33
|
+
|
|
34
|
+
生成 V8 事件代码时,代码内容本身(文件头、普通注释、`console.log`、返回 `Msg` 等)不要包含 `Microi`、`吾码` 等平台品牌文字,除非业务数据或字段值本身必须如此。生成代码要有可维护注释:每个 `function` 前写清用途、关键参数和返回值;提交前校验、提交后联动、字段显隐、数据脱敏、跨表写入、复杂条件判断等代码段前写短注释说明业务原因;避免“给变量赋值”这类无信息量注释。
|
|
35
|
+
|
|
36
|
+
## 事件类型
|
|
37
|
+
|
|
38
|
+
| 事件 | 运行端 | V8.EventName | 触发时机 | 用途 |
|
|
39
|
+
|------|--------|-------------|---------|------|
|
|
40
|
+
| `InFormV8.js` | **前端** | `FormIn` | 表单打开时 | 初始化字段显隐、默认值 |
|
|
41
|
+
| `SubmitFormV8.js` | **前端** | `FormSubmitBefore` | 表单提交时 | 前端校验、数据预处理 |
|
|
42
|
+
| `OutFormV8.js` | **前端** | `FormOut` | 表单提交后离开时 | 刷新列表、跳转 |
|
|
43
|
+
| `SubmitBeforeServerV8.js` | **后端** | `FormSubmitBefore` | 数据写入 DB 之前 | 服务端校验、数据加工 |
|
|
44
|
+
| `SubmitAfterServerV8.js` | **后端** | `FormSubmitAfter` | 数据写入 DB 之后 | 触发通知、同步其它表、日志 |
|
|
45
|
+
| `DataFilterV8.js` | **后端** | `DataFilter` | 获取列表/表单数据后 | 每行数据加工、脱敏、补充字段 |
|
|
46
|
+
|
|
47
|
+
## 事件触发规则
|
|
48
|
+
|
|
49
|
+
- 后端 V8 事件 / 接口引擎中调用 `V8.FormEngine` 增删改 → **不触发**表单 V8 事件
|
|
50
|
+
- 传入 `_InvokeType: 'Client'` → **触发**表单 V8 事件
|
|
51
|
+
- Postman 等直接调用接口 → 前端事件**不执行**,后端事件**仍执行**
|
|
52
|
+
- 服务器端提交前/后 V8 事件在**同一事务**中执行
|
|
53
|
+
- `diy_table.V8Unlimited` 只控制该表的后端提交前、提交后和数据处理 V8;仅当事件链必须保持一个事务且无法安全分片时开启。它解除当前 Jint Engine 的超时、语句、函数递归和累计分配限制,但不解除进程常驻内存、取消、并发、接口嵌套深度、权限和数据库保护。
|
|
54
|
+
|
|
55
|
+
## ⚠️ 关键陷阱(必读)
|
|
56
|
+
|
|
57
|
+
### 1. 设计模式保护(前端事件必加)
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
// 防止【表单设计器】中编辑字段时误触发事件
|
|
61
|
+
if (V8.LoadMode === 'Design') return;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. 死循环禁忌
|
|
65
|
+
|
|
66
|
+
- ❌ **禁止** 在 `SubmitFormV8.js` 中调 `V8.FormSubmit()` —— 无限递归
|
|
67
|
+
- ❌ **禁止** 在 `FieldValueChange` 中 `V8.FormSet(同字段, ...)` —— 循环触发
|
|
68
|
+
- ❌ **禁止** 在后端提交前/后事件中再次写当前表;即使默认 Server 调用不递归,也可能覆盖本次增量数据、引发死锁或重复副作用。确需跨表联动时使用同一 `V8.DbTrans`,且不要传 `_InvokeType:'Client'`
|
|
69
|
+
|
|
70
|
+
### 3. 阻止提交(后端)
|
|
71
|
+
|
|
72
|
+
后端事件返回 `{ Code: 0, Msg: '错误' }` 平台自动回滚事务并阻止提交:
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
if (V8.Form.Money > 100000 && V8.CurrentUser.RoleName.indexOf('总经理') === -1) {
|
|
76
|
+
return { Code: 0, Msg: '金额超过 10 万必须总经理提交' };
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 4. 共享事务操作其它表
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
// 在 SubmitBeforeServerV8 / SubmitAfterServerV8 中
|
|
84
|
+
V8.FormEngine.UptFormData('OtherTable', { Id: 'x', Field: 'v' }, V8.DbTrans);
|
|
85
|
+
V8.ApiEngine.Run('other-engine', { Form: V8.Form }, V8.DbTrans);
|
|
86
|
+
// 不传 V8.DbTrans 会导致并行事务、可能死锁或脏读
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 5. 模板引擎与 DataFilterV8 的区别
|
|
90
|
+
|
|
91
|
+
- 数据**加工**(计算字段、脱敏、查关联表名)→ 用 `DataFilterV8`(后端,每行执行,可用 `V8.CacheData` 防 N+1)
|
|
92
|
+
- 数据**渲染**(颜色徽章、HTML、图片)→ 用【表格 V8 模板引擎】,详见 `v8-template-engine/SKILL.md`
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 前端事件特有 API
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
// 普通 diy-form 中设置字段值(触发目标字段值变更事件)
|
|
100
|
+
// diy-table 列表上下文只更新当前行和模板,不递归触发目标字段 V8
|
|
101
|
+
V8.FormSet('FieldName', 'value');
|
|
102
|
+
V8.FormSet('DropdownField', { Id: 1, Name: '选项' }); // 下拉框
|
|
103
|
+
|
|
104
|
+
// 设置字段属性
|
|
105
|
+
V8.FieldSet('FieldName', 'Visible', false); // 隐藏字段
|
|
106
|
+
V8.FieldSet('FieldName', 'Readonly', true); // 只读字段
|
|
107
|
+
V8.FieldSet('FieldName', 'Required', true); // 必填
|
|
108
|
+
V8.FieldSet('FieldName', 'Data', [{Id:1, Name:'选项'}]); // 动态设置数据源
|
|
109
|
+
|
|
110
|
+
// 访问字段属性
|
|
111
|
+
var isReadonly = V8.Field.UserName.Readonly;
|
|
112
|
+
// 属性:Name, Label, Config, Data, Readonly, Visible, Placeholder 等
|
|
113
|
+
|
|
114
|
+
// 前端提示
|
|
115
|
+
V8.Tips('操作成功', true); // 成功提示(1秒消失)
|
|
116
|
+
V8.Tips('操作失败', false); // 错误提示(5秒消失)
|
|
117
|
+
|
|
118
|
+
// 确认框
|
|
119
|
+
V8.ConfirmTips('确定删除?', function() { /* 确定 */ }, function() { /* 取消 */ });
|
|
120
|
+
// content 使用 HTML 模式渲染;只能传固定文案或已转义内容,复杂交互用 OpenAppDialog
|
|
121
|
+
|
|
122
|
+
// 前端 HTTP 请求
|
|
123
|
+
V8.Post('/api/xxx', { key: 'value' }, function(res) { });
|
|
124
|
+
V8.Get('/api/xxx', {}, function(res) { });
|
|
125
|
+
|
|
126
|
+
// 前端调用接口引擎
|
|
127
|
+
var result = await V8.ApiEngine.Run('engineKey', { param1: 'value' });
|
|
128
|
+
|
|
129
|
+
// 当前表单模式
|
|
130
|
+
V8.FormMode // 'Add' / 'Edit' / 'View'
|
|
131
|
+
V8.FormOutAction // 'Insert' / 'Update' / 'Close' / 'Delete'
|
|
132
|
+
|
|
133
|
+
// 刷新表格
|
|
134
|
+
V8.RefreshTable({ _PageIndex: 1 }); // -1 = 最后一页
|
|
135
|
+
|
|
136
|
+
// 表单操作
|
|
137
|
+
V8.FormSubmit({ CloseForm: true }); // 提交表单(不能在提交前事件中调用)
|
|
138
|
+
V8.FormClose(); // 关闭表单
|
|
139
|
+
V8.ReloadForm({ Id: 'xxx' }, 'Edit'); // 重新加载
|
|
140
|
+
|
|
141
|
+
// 按钮和Tab控制
|
|
142
|
+
V8.HideFormBtn('Update'); // 隐藏按钮:'Delete' / 'Save' / 'Update'
|
|
143
|
+
V8.HideFormTab('tabName'); // 隐藏Tab
|
|
144
|
+
V8.ShowFormTab('tabName'); // 显示Tab
|
|
145
|
+
V8.ClickFormTab('tabName'); // 选中Tab
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## InFormV8.js — 表单打开事件
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// 新增时设置默认值
|
|
152
|
+
if (V8.FormMode === 'Add') {
|
|
153
|
+
V8.FormSet('Status', 1);
|
|
154
|
+
V8.FormSet('CreateTime', DateNow('yyyy-MM-dd HH:mm:ss'));
|
|
155
|
+
V8.FormSet('CreatorId', V8.CurrentUser.Id);
|
|
156
|
+
V8.FormSet('CreatorName', V8.CurrentUser.Name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 编辑时禁用某些字段
|
|
160
|
+
if (V8.FormMode === 'Edit') {
|
|
161
|
+
V8.FieldSet('Account', 'Readonly', true); // 账号不可修改
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 查看模式隐藏操作按钮
|
|
165
|
+
if (V8.FormMode === 'View') {
|
|
166
|
+
V8.HideFormBtn('Save');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 根据角色控制字段可见性
|
|
170
|
+
if (V8.CurrentUser.RoleName.indexOf('管理员') === -1) {
|
|
171
|
+
V8.FieldSet('AuditField', 'Visible', false);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## SubmitFormV8.js — 前端提交校验
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// 自定义校验
|
|
179
|
+
var phone = V8.Form.Phone;
|
|
180
|
+
if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
|
|
181
|
+
V8.Tips('手机号格式不正确', false);
|
|
182
|
+
return false; // 返回 false 阻止提交
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 业务逻辑校验
|
|
186
|
+
if (V8.Form.StartDate > V8.Form.EndDate) {
|
|
187
|
+
V8.Tips('开始日期不能大于结束日期', false);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## SubmitBeforeServerV8.js — 服务端提交前
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// V8.Form 是即将写入数据库的数据
|
|
196
|
+
// V8.OldForm 是修改前的旧数据(仅更新时有值)
|
|
197
|
+
// V8.FormSubmitAction:'Insert' / 'Update' / 'Delete'
|
|
198
|
+
|
|
199
|
+
// 新增时:自动填充审计字段
|
|
200
|
+
if (V8.FormSubmitAction === 'Insert') {
|
|
201
|
+
V8.Form.CreateTime = DateNow('yyyy-MM-dd HH:mm:ss');
|
|
202
|
+
V8.Form.CreateUserId = V8.CurrentUser.Id;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 更新时:记录修改人
|
|
206
|
+
if (V8.FormSubmitAction === 'Update') {
|
|
207
|
+
V8.Form.UpdateTime = DateNow('yyyy-MM-dd HH:mm:ss');
|
|
208
|
+
V8.Form.UpdateUserId = V8.CurrentUser.Id;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 删除时:校验是否允许删除
|
|
212
|
+
if (V8.FormSubmitAction === 'Delete') {
|
|
213
|
+
var related = V8.FormEngine.GetTableDataCount('OrderDetail', {
|
|
214
|
+
_Where: [['OrderId', '=', V8.Form.Id]]
|
|
215
|
+
});
|
|
216
|
+
if (related.DataCount > 0) {
|
|
217
|
+
return { Code: 0, Msg: '该订单下有明细数据,不允许删除' };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 唯一性校验
|
|
222
|
+
if (V8.FormSubmitAction === 'Insert' || V8.FormSubmitAction === 'Update') {
|
|
223
|
+
var where = [['Code', '=', V8.Form.Code]];
|
|
224
|
+
if (V8.FormSubmitAction === 'Update') {
|
|
225
|
+
where.push(['AND', 'Id', '<>', V8.Form.Id]);
|
|
226
|
+
}
|
|
227
|
+
var exist = V8.FormEngine.GetFormData('Product', { _Where: where });
|
|
228
|
+
if (exist.Code === 1 && exist.Data) {
|
|
229
|
+
return { Code: 0, Msg: '编码已存在' };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 返回 { Code: 0, Msg: '...' } 阻止提交并自动回滚事务
|
|
234
|
+
// 无需手动调用 V8.DbTrans.Rollback()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## SubmitAfterServerV8.js — 服务端提交后
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
// 此时数据已成功写入数据库(仍在事务中)
|
|
241
|
+
// 返回 { Code: 0 } 仍可回滚事务
|
|
242
|
+
|
|
243
|
+
// 新增后:自动创建关联数据。保持默认 Server 调用,不触发目标表事件
|
|
244
|
+
if (V8.FormSubmitAction === 'Insert') {
|
|
245
|
+
V8.FormEngine.AddFormData('UserProfile', {
|
|
246
|
+
UserId: V8.Form.Id,
|
|
247
|
+
NickName: V8.Form.Name
|
|
248
|
+
}, V8.DbTrans);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 更新后:同步更新其它表的冗余字段
|
|
252
|
+
if (V8.FormSubmitAction === 'Update') {
|
|
253
|
+
if (V8.OldForm.Name !== V8.Form.Name) {
|
|
254
|
+
V8.FormEngine.UptFormDataByWhere('OrderHeader', {
|
|
255
|
+
_Where: [['CustomerId', '=', V8.Form.Id]],
|
|
256
|
+
CustomerName: V8.Form.Name
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 通知(调用其他接口引擎,可共享事务)
|
|
262
|
+
V8.ApiEngine.Run('send-notification', {
|
|
263
|
+
userId: V8.Form.Id,
|
|
264
|
+
type: V8.FormSubmitAction
|
|
265
|
+
}, V8.DbTrans);
|
|
266
|
+
|
|
267
|
+
// 记录操作日志
|
|
268
|
+
V8.Method.AddSysLog({
|
|
269
|
+
Title: V8.FormSubmitAction + ' ' + V8.TableModel.Name,
|
|
270
|
+
Content: JSON.stringify({ Id: V8.Form.Id }),
|
|
271
|
+
Type: '业务日志'
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`SubmitAfterServerV8` 的“After”仍是“写入后、提交前”。需要在事务真正提交后才发布的缓存版本、跨节点通知等副作用,不能直接在事件中执行。平台为 `microi_database` 提供专用 `V8.Method.RefreshExtensionDatabases()`:事件调用时只登记提交后回调,提交成功才递增当前租户共享 Redis 版本,回滚时自动丢弃。普通业务的外部消息仍优先使用同事务 outbox,不能把任意不可撤销副作用都塞进内存回调。
|
|
276
|
+
|
|
277
|
+
## DataFilterV8.js — 服务端数据处理事件
|
|
278
|
+
|
|
279
|
+
获取列表/表单数据后,每行数据都会执行一次此事件。
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
// V8.RowIndex — 当前行索引(从 0 开始)
|
|
283
|
+
// V8.Form — 当前行数据
|
|
284
|
+
// V8.NotSaveField — 指定哪些字段编辑时不保存(数组)
|
|
285
|
+
// V8.CacheData — 用于缓存数据,避免每行重复查询
|
|
286
|
+
|
|
287
|
+
// 补充计算字段
|
|
288
|
+
V8.Form.TotalPrice = V8.Form.Price * V8.Form.Quantity;
|
|
289
|
+
|
|
290
|
+
// 指定某些字段不保存(仅在编辑表单时有效)
|
|
291
|
+
V8.NotSaveField = ['TotalPrice', 'CompanyName'];
|
|
292
|
+
|
|
293
|
+
// 使用 CacheData 避免 N+1 查询
|
|
294
|
+
if (!V8.CacheData.deptMap) {
|
|
295
|
+
var depts = V8.FormEngine.GetTableData('Department', {});
|
|
296
|
+
var map = {};
|
|
297
|
+
for (var i = 0; i < depts.Data.length; i++) {
|
|
298
|
+
map[depts.Data[i].Id] = depts.Data[i].Name;
|
|
299
|
+
}
|
|
300
|
+
V8.CacheData.deptMap = map;
|
|
301
|
+
}
|
|
302
|
+
V8.Form.DeptName = V8.CacheData.deptMap[V8.Form.DeptId] || '';
|
|
303
|
+
|
|
304
|
+
// 数据脱敏
|
|
305
|
+
if (V8.Form.Phone) {
|
|
306
|
+
V8.Form.Phone = V8.Form.Phone.substring(0, 3) + '****' + V8.Form.Phone.substring(7);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## 事件上下文变量
|
|
311
|
+
|
|
312
|
+
### 前端事件
|
|
313
|
+
|
|
314
|
+
| 变量 | 说明 | 可用事件 |
|
|
315
|
+
|------|------|---------|
|
|
316
|
+
| `V8.Form` | 当前表单数据(新增时也有 Id) | 全部 |
|
|
317
|
+
| `V8.OldForm` | 已加载的修改前旧数据 | 普通表单;提交前/后 |
|
|
318
|
+
| `V8.FormMode` | `'Add'` / `'Edit'` / `'View'` | 全部 |
|
|
319
|
+
| `V8.FormOutAction` | `'Insert'`/`'Update'`/`'Close'`/`'Delete'` | FormOut |
|
|
320
|
+
| `V8.FormSubmitAction` | `'Insert'` / `'Update'` / `'Delete'` | SubmitBefore |
|
|
321
|
+
| `V8.EventName` | 当前事件名 | 全部 |
|
|
322
|
+
| `V8.CurrentUser` | 当前用户 | 全部 |
|
|
323
|
+
| `V8.TableId` / `V8.TableName` | 当前表 Id / Name | 全部 |
|
|
324
|
+
| `V8.SelectedData` / `V8.TableRowSelected` | 选中的行数组 | 列表/批量按钮 |
|
|
325
|
+
| `V8.CurrentTableData` | 当前表当页数据 | 全部 |
|
|
326
|
+
| `V8.ClientType` | `'PC'`/`'IOS'`/`'Android'`/`'H5'`/`'WeChat'` | 全部 |
|
|
327
|
+
| `V8.ThisValue` | 当前字段新值:可能是对象、原始值或行内 `{New,Old}` | FieldValueChange |
|
|
328
|
+
| `V8.OldValue` | 当前字段旧值,仅行内编辑可靠 | 表格行内 FieldValueChange |
|
|
329
|
+
| `V8.KeyCode` | 键盘事件的键码 | FieldOnKeyup |
|
|
330
|
+
| `V8.Event` | 原生事件;键盘事件目前不提供 | FieldSlotButtonClick 等显式事件 |
|
|
331
|
+
| `V8.ParentV8` | 子表中访问父表 V8 对象 | 子表事件 |
|
|
332
|
+
|
|
333
|
+
### 后端事件
|
|
334
|
+
|
|
335
|
+
| 变量 | 说明 | 可用事件 |
|
|
336
|
+
|------|------|---------|
|
|
337
|
+
| `V8.Form` | 当前表单/行数据 | 全部 |
|
|
338
|
+
| `V8.OldForm` | 提交前旧数据 | SubmitBefore/After |
|
|
339
|
+
| `V8.FormSubmitAction` | `'Insert'`/`'Update'`/`'Delete'` | SubmitBefore/After |
|
|
340
|
+
| `V8.TableModel` | 表模型(Id, Name 等) | SubmitBefore/After |
|
|
341
|
+
| `V8.EventName` | 当前事件名 | 全部 |
|
|
342
|
+
| `V8.InvokeType` | `'Server'`/`'Client'` | 全部 |
|
|
343
|
+
| `V8.CurrentUser` | 当前用户 | 全部 |
|
|
344
|
+
| `V8.RowIndex` | 行索引(从 0) | DataFilter |
|
|
345
|
+
| `V8.NotSaveField` | 不保存的字段(数组,可写) | DataFilter |
|
|
346
|
+
| `V8.CacheData` | 缓存数据(避免 N+1,可写) | DataFilter |
|
|
347
|
+
|
|
348
|
+
## 前端事件名(V8.EventName 可能的值)
|
|
349
|
+
|
|
350
|
+
| 值 | 说明 |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `FormIn` | 进入表单事件 |
|
|
353
|
+
| `FormSubmitBefore` | 提交前事件 |
|
|
354
|
+
| `FormOut` | 离开表单事件 |
|
|
355
|
+
| `FieldValueChange` | 字段值变更事件 |
|
|
356
|
+
| `FieldOnKeyup` | 文本框键盘事件 |
|
|
357
|
+
| `TableFieldOnKeyup` | 表格行内文本框键盘事件 |
|
|
358
|
+
| `FieldSlotButtonClick` | 单行文本插槽按钮点击事件 |
|
|
359
|
+
| `V8BtnRun` | V8 按钮执行事件 |
|
|
360
|
+
| `V8BtnLimit` | V8 按钮是否显示事件 |
|
|
361
|
+
| `BtnFormDetailRun` | 详情按钮 V8 按钮 |
|
|
362
|
+
| `TableRowClick` | 表格行点击 V8 事件 |
|
|
363
|
+
| `OpenTableBefore` | 弹出表格前事件 |
|
|
364
|
+
| `OpenTableSubmit` | 弹出表格提交事件 |
|
|
365
|
+
| `PageTab` | 多 Tab 页签 V8 事件 |
|
|
366
|
+
| `WFNodeStart` | 流程节点开始 V8 事件 |
|
|
367
|
+
| `WFNodeEnd` | 流程节点结束 V8 事件 |
|
|
368
|
+
|
|
369
|
+
## 注意事项
|
|
370
|
+
|
|
371
|
+
- 前端事件可使用 `window` 对象和 `async/await`,后端事件不可以
|
|
372
|
+
- 后端提交前/后事件返回 `{ Code: 0, Msg: '...' }` 可阻止数据写入并回滚事务
|
|
373
|
+
- 直接修改 `V8.Form` 的字段值即可改变最终写入的数据
|
|
374
|
+
- 后端事件中使用 `V8.FormEngine` 默认是 Server 调用,不触发目标表事件;`_InvokeType:'Client'` 恰好会触发目标表事件,不能用于“避免递归”
|
|
375
|
+
- `_InvokeType:'Server'` 只表达事件调用语义,不是客户端授权开关;浏览器伪造它不会获得受信任权限
|
|
376
|
+
- `V8.FormSubmitAction` 的值是 `'Insert'`/`'Update'`/`'Delete'`(非 Add/Upt/Del)
|
|
377
|
+
- 在 DataFilterV8 中使用 `V8.CacheData` 缓存查询结果,避免每行执行 N+1 查询
|
|
378
|
+
- 表事件调用下游接口时,`diy_table.V8Unlimited` 不会自动放开下游接口;下游 `sys_apiengine.V8Unlimited` 必须独立配置,避免一次开关无边界扩散到整条调用链
|
|
379
|
+
|
|
380
|
+
### 复盘:提交后事件误把增量表单当作完整记录
|
|
381
|
+
|
|
382
|
+
- 触发场景:插件、MCP 或后端只更新代码等少数字段时,`SubmitAfterServerV8` 直接对 `V8.Form.Id`、业务 Key 调用 `toLowerCase()`,保存动作因字段未出现在增量参数中而异常。
|
|
383
|
+
- 根因:事件假设 `V8.Form` 始终包含整行数据,没有兼容稀疏更新(sparse patch)。
|
|
384
|
+
- 通用规则:提交后事件使用非本次必传字段前,必须从 `V8.OldForm` 合并兜底,或凭已有 `Id` 回查当前记录;调用字符串方法前先显式 `String(...)` 并校验空值。写缓存时应缓存合并后的完整模型,不能用稀疏对象覆盖整行缓存。
|
|
385
|
+
- 自动化检查:分别只更新一个普通字段、只更新大文本代码字段并执行一次完整表单保存;事件均不得报错,按 Id、唯一 Key 和可选地址回读缓存都应得到完整记录。
|