@microi.net/cli 5.5.9 → 5.6.1
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/.codebuddy-plugin/marketplace.json +2 -2
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.workbuddy-plugin/marketplace.json +2 -2
- package/.workbuddy-plugin/plugin.json +1 -1
- package/assets/build-meta.json +5 -5
- package/cordis.patch.yml +1 -1
- package/package.json +1 -1
- package/scripts/mcp-server.js +99 -99
- package/scripts/microi-cli.js +39 -36
- package/scripts/microi-skills.meta.json +341 -338
- package/skills/.microi-skills-version.json +2 -2
- package/skills/.progressive-disclosure-manifest.json +3566 -3566
- package/skills/README.md +4 -4
- package/skills/ai-engine/SKILL.md +45 -45
- package/skills/ai-platform-governance/SKILL.md +177 -177
- package/skills/ai-platform-governance/references/progressive-01-/345/212/237/350/203/275/345/274/200/345/205/263.md +190 -190
- package/skills/app-store/SKILL.md +21 -4
- package/skills/business-blueprint/SKILL.md +85 -85
- package/skills/datasource-engine/SKILL.md +91 -91
- package/skills/dos-orm/SKILL.md +15 -0
- package/skills/dos-orm/references/api-reference.md +229 -229
- package/skills/email-engine/SKILL.md +80 -80
- package/skills/email-engine/references/v8-email.md +34 -34
- package/skills/job-engine/SKILL.md +154 -154
- package/skills/message-notification/SKILL.md +27 -1
- package/skills/message-notification/agents/openai.yaml +5 -5
- package/skills/message-notification/references/contracts.md +102 -102
- package/skills/microi-ai-app-auth.js +652 -652
- package/skills/microi-ai-application/SKILL.md +102 -102
- package/skills/microi-ai-application/agents/openai.yaml +4 -4
- package/skills/microi-ai-application/references/frontend-baseline.md +164 -164
- package/skills/microi-client-frontend/SKILL.md +47 -41
- package/skills/microi-client-frontend/references/progressive-01-3-/345/212/250/346/200/201/346/214/211/351/222/256/347/263/273/347/273/237.md +4 -4
- package/skills/microi-client-frontend/references/progressive-02-8-/350/277/220/350/241/214/346/227/266/351/253/230/351/242/221/345/235/221/345/244/215/347/233/230.md +30 -30
- package/skills/microi-client-frontend/references/progressive-03-vue3-/345/211/215/347/253/257/345/276/256/346/234/215/345/212/241/345/256/277/344/270/273/350/247/204/345/210/231.md +58 -58
- package/skills/microi-codex/SKILL.md +2 -0
- package/skills/microi-codex-installer/SKILL.md +216 -216
- package/skills/microi-codex-installer/agents/openai.yaml +7 -7
- package/skills/microi-datasource-mapping/SKILL.md +67 -67
- package/skills/microi-db-schema/SKILL.md +6 -6
- package/skills/microi-db-schema/references/core-tables.md +18 -18
- package/skills/microi-db-schema/references/form-component-options.md +256 -256
- package/skills/microi-db-schema/references/schema-overview.md +11 -11
- package/skills/microi-db-schema/references/schema.md +46 -46
- package/skills/microi-db-schema/references/table-catalog.md +57 -57
- package/skills/microi-deployment/SKILL.md +18 -1
- package/skills/microi-deployment/references/deployment-matrix.md +101 -101
- package/skills/microi-docs-coverage/SKILL.md +10 -0
- package/skills/microi-docs-coverage/references/capability-map.md +3 -2
- package/skills/microi-docs-coverage/scripts/audit-doc-skill-coverage.mjs +894 -894
- package/skills/microi-form-engine/SKILL.md +27 -3
- package/skills/microi-form-engine/references/component-catalog.md +218 -218
- package/skills/microi-form-engine/references/data-source-events.md +124 -124
- package/skills/microi-form-layout/SKILL.md +13 -13
- package/skills/microi-form-layout/references/progressive-01-3-/344/270/211/347/247/215/345/210/206/347/273/204/347/232/204/345/255/230/345/202/250/344/270/216/351/205/215/347/275/256.md +235 -235
- package/skills/microi-frontend-sdk/SKILL.md +4 -4
- package/skills/microi-frontend-sdk/references/progressive-01-token-/345/275/223/345/211/215/347/231/273/345/275/225/347/224/250/346/210/267/344/270/216/345/275/223/345/211/215/347/273/210/347/253/257/347/231/273/345/275/225/345/215/217/350/256/256.md +175 -175
- package/skills/microi-left-right-layout/SKILL.md +141 -141
- package/skills/microi-microservice/SKILL.md +2 -1
- package/skills/microi-microservice/references/runtime-delivery.md +278 -278
- package/skills/microi-mobile-app-quality/references/progressive-01-4-/351/207/215/350/246/201/346/214/211/351/222/256/345/277/205/351/241/273/345/270/246/345/233/276/346/240/207.md +213 -213
- package/skills/microi-solution-quotation/SKILL.md +78 -78
- package/skills/microi-solution-quotation/agents/openai.yaml +4 -4
- package/skills/microi-solution-quotation/scripts/build_solution_quote.py +296 -296
- package/skills/microi-sso/SKILL.md +6 -0
- package/skills/microi-sso/references/acceptance.md +49 -49
- package/skills/microi-sso/references/configuration-and-security.md +53 -53
- package/skills/microi-sso/references/inbound.md +53 -53
- package/skills/microi-sso/references/outbound.md +39 -39
- package/skills/microi-system-delivery/SKILL.md +5 -4
- package/skills/microi-system-delivery/references/progressive-02-/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/345/277/205/351/241/273/350/246/206/347/233/226/347/232/204/345/235/221.md +7 -2
- package/skills/microi-ui/SKILL.md +7 -2
- package/skills/microi-uniapp-frontend/SKILL.md +134 -134
- package/skills/microi-uniapp-frontend/references/progressive-01-/347/247/273/345/212/250/347/253/257/345/210/206/347/261/273-/345/217/214/346/240/217/345/210/227/350/241/250/347/213/254/347/253/213/346/273/232/345/212/250.md +136 -136
- package/skills/microi-uniapp-frontend/references/progressive-02-/345/205/263/351/224/256/344/270/232/345/212/241/350/265/204/344/272/247/344/270/215/345/276/227/351/273/230/350/256/244/351/200/211/344/270/255.md +124 -124
- package/skills/module-engine/SKILL.md +30 -14
- package/skills/ocr-engine/SKILL.md +2 -2
- package/skills/ocr-engine/agents/openai.yaml +1 -1
- package/skills/page-engine/SKILL.md +28 -28
- package/skills/page-engine/references/progressive-01-/346/211/200/346/234/211/347/273/204/344/273/266/347/261/273/345/236/213.md +126 -126
- package/skills/page-engine/references/progressive-02-/347/211/210/346/234/254/345/216/206/345/217/262-/345/271/266/345/217/221/344/277/235/345/255/230/344/270/216/345/233/236/346/273/232.md +42 -42
- package/skills/performance-testing/SKILL.md +120 -120
- package/skills/playwright-e2e/SKILL.md +84 -84
- package/skills/playwright-e2e/references/progressive-01-/345/205/250/350/207/252/345/212/250/347/231/273/345/275/225-/345/205/215/351/252/214/350/257/201/347/240/201-/344/275/206/344/270/215/345/205/215/345/257/206/347/240/201-/345/277/205/350/257/273.md +68 -68
- package/skills/playwright-e2e/references/progressive-02-/346/226/207/345/255/227/345/257/271/346/257/224/345/272/246/344/270/216/345/217/257/350/257/273/346/200/247/350/207/252/345/212/250/345/214/226/346/243/200/346/237/245-/345/277/205/345/201/232.md +64 -64
- package/skills/playwright-e2e/references/progressive-03-microi-helper-/346/250/241/346/235/277.md +20 -20
- package/skills/playwright-e2e/references/progressive-04-ci-/345/273/272/350/256/256.md +69 -69
- package/skills/print-engine/SKILL.md +11 -3
- package/skills/production-readonly-audit/SKILL.md +41 -41
- package/skills/report-engine/SKILL.md +71 -71
- package/skills/scripts/optimize-progressive-disclosure.mjs +204 -204
- package/skills/scripts/refresh-progressive-disclosure.mjs +64 -64
- package/skills/scripts/sync-embedded-skills.mjs +46 -0
- package/skills/scripts/validate-progressive-disclosure.mjs +8 -3
- package/skills/search-engine/SKILL.md +75 -75
- package/skills/spider-engine/SKILL.md +190 -190
- package/skills/system-observability/SKILL.md +169 -157
- package/skills/system-observability/references/memory-incident-triage.md +73 -73
- package/skills/translate-engine/SKILL.md +1 -1
- package/skills/ui-design/SKILL.md +3 -1
- package/skills/uniapp-mall-assets/SKILL.md +170 -170
- package/skills/unity-integration/SKILL.md +155 -155
- package/skills/unity-integration/agents/openai.yaml +4 -4
- package/skills/unity-integration/references/ai-app-delivery.md +103 -103
- package/skills/unity-integration/references/sdk-api.md +82 -82
- package/skills/unity-integration/references/toolbox-migration.md +66 -66
- package/skills/unity-integration/references/webgl-hosting.md +57 -57
- package/skills/v8-api-config/SKILL.md +4 -0
- package/skills/v8-cache-pattern/SKILL.md +306 -306
- package/skills/v8-crud-api/SKILL.md +29 -29
- package/skills/v8-crud-api/references/progressive-01-/346/237/245/350/257/242/345/210/227/350/241/250-/345/210/206/351/241/265.md +56 -56
- package/skills/v8-debugging/SKILL.md +143 -143
- package/skills/v8-explorer-tree/SKILL.md +64 -64
- package/skills/v8-export-import/references/progressive-01-excellayout-/351/253/230/347/272/247/350/207/252/347/224/261/345/270/203/345/261/200.md +201 -201
- package/skills/v8-export-import/references/progressive-02-powerpoint-/345/257/274/345/207/272.md +118 -118
- package/skills/v8-export-import/references/progressive-03-/345/256/211/345/205/250-/346/200/247/350/203/275/346/263/250/346/204/217.md +32 -32
- package/skills/v8-file-upload/SKILL.md +5 -2
- package/skills/v8-file-upload/references/progressive-01-/345/205/254/346/234/211/346/241/266-vs-/347/247/201/346/234/211/346/241/266.md +2 -2
- package/skills/v8-file-upload/references/progressive-02-office-/346/226/207/344/273/266/345/234/250/347/272/277/347/274/226/350/276/221/347/211/210/346/234/254/345/217/267/350/247/204/345/210/231.md +138 -138
- package/skills/v8-formengine-http/SKILL.md +126 -108
- package/skills/v8-frontend-events/SKILL.md +22 -22
- package/skills/v8-frontend-events/references/bluetooth-print-api.md +135 -135
- package/skills/v8-frontend-events/references/bluetooth-print.md +247 -247
- package/skills/v8-frontend-events/references/progressive-01-/345/210/227/350/241/250/344/272/213/344/273/266.md +50 -50
- package/skills/v8-http-integration/SKILL.md +106 -106
- package/skills/v8-http-integration/references/progressive-01-get-/350/257/267/346/261/202.md +31 -31
- package/skills/v8-http-integration/references/progressive-02-/351/224/231/350/257/257/345/244/204/347/220/206/346/250/241/345/274/217.md +5 -5
- package/skills/v8-image-processing/SKILL.md +190 -190
- package/skills/v8-image-processing/agents/openai.yaml +4 -4
- package/skills/v8-image-processing/references/api-reference.md +623 -623
- package/skills/v8-menu-buttons/SKILL.md +103 -103
- package/skills/v8-menu-buttons/references/progressive-01-2-/346/214/211/351/222/256/345/257/271/350/261/241-schema.md +145 -145
- package/skills/v8-menu-buttons/references/progressive-02-8-/346/250/241/345/274/217-f-/345/220/216/345/217/260/344/273/273/345/212/241/346/214/211/351/222/256-/351/225/277/344/273/273/345/212/241.md +228 -228
- package/skills/v8-menu-buttons/references/progressive-03-10-/345/217/215/346/250/241/345/274/217-/351/201/277/345/205/215.md +43 -43
- package/skills/v8-mongodb/SKILL.md +65 -65
- package/skills/v8-mq-mqtt/SKILL.md +175 -175
- package/skills/v8-mq-mqtt/references/mqtt-production.md +342 -342
- package/skills/v8-mq-mqtt/references/progressive-01-v8-mqtt-iot-/347/211/251/350/201/224/347/275/221.md +137 -137
- package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +203 -203
- package/skills/v8-saas-multi-tenant/SKILL.md +223 -223
- package/skills/v8-security/SKILL.md +1 -1
- package/skills/v8-security/references/progressive-01-2-/346/235/203/351/231/220/346/240/241/351/252/214.md +122 -122
- package/skills/v8-sql-query/SKILL.md +7 -5
- package/skills/v8-table-event/SKILL.md +9 -2
- package/skills/v8-table-event/references/progressive-01-informv8-js-/350/241/250/345/215/225/346/211/223/345/274/200/344/272/213/344/273/266.md +1 -1
- package/skills/v8-table-event/references/progressive-02-/345/211/215/347/253/257/344/272/213/344/273/266/345/220/215-v8-eventname-/345/217/257/350/203/275/347/232/204/345/200/274.md +15 -15
- package/skills/v8-tcp-integration/SKILL.md +1 -1
- package/skills/v8-tcp-integration/agents/openai.yaml +4 -4
- package/skills/v8-template-engine/SKILL.md +59 -59
- package/skills/v8-utilities/SKILL.md +92 -92
- package/skills/v8-utilities/references/client-api-index.md +143 -143
- package/skills/v8-utilities/references/platform-http-routes.md +83 -83
- package/skills/v8-utilities/references/server-api-index.md +74 -74
- package/skills/v8-workflow/SKILL.md +13 -4
- package/skills/vision-engine/SKILL.md +19 -19
- package/skills/vision-engine/references/architecture-and-acceptance.md +34 -34
- package/skills/workspace-conventions/SKILL.md +35 -12
- package/skills/workspace-conventions/references/progressive-01-/347/211/210/346/234/254/346/233/264/346/226/260/346/227/245/345/277/227/344/277/235/346/212/244/350/247/204/345/210/231-/345/274/272/345/210/266.md +2 -2
- package/skills/workspace-conventions/references/progressive-02-microi-net-api-/346/234/254/345/234/260/345/220/257/345/212/250/347/272/246/345/256/232.md +179 -179
- package/skills/workspace-conventions/references/progressive-03-cli-/344/270/216-ide-/346/217/222/344/273/266/351/224/231/347/211/210/345/205/261/345/255/230/347/272/246/345/256/232.md +21 -21
|
@@ -3,147 +3,147 @@
|
|
|
3
3
|
> 按需读取;本文件由 SKILL.md 的原章节无损拆分。
|
|
4
4
|
|
|
5
5
|
<!-- microi-progressive:chunk id=v8-file-upload-008 sha256=5e3b59486745d3c54bd8ad25c43cbe2396e6d614394340b93d93794b6df56292 -->
|
|
6
|
-
## Office 文件在线编辑版本号规则
|
|
7
|
-
|
|
8
|
-
当文件上传控件开启【Office 在线预览】、【允许在线编辑】和【开启 Office 文件版本号】时,前后端必须遵循统一版本规则:
|
|
9
|
-
|
|
10
|
-
- 新上传的 Office 文件(`pdf/doc/docx/xls/xlsx/ppt/pptx`)要立即写入初始版本 `v1.0.0`,字段 JSON 的 `Path` 指向该原始文件,`Version` 为 `v1.0.0`,`Versions[0]` 保存同一份原始文件路径。
|
|
11
|
-
- 用户进入 OnlyOffice 在线编辑页后,每次手动点击【保存文件】才生成新版本;第一次保存生成 `v1.0.1`,之后依次生成 `v1.0.2`、`v1.0.3`。
|
|
12
|
-
- 未开启版本号时,保存文件直接覆盖当前 `Path` 对应的 HDFS/OSS 源文件。
|
|
13
|
-
- 开启版本号时,保存文件必须生成带版本号后缀的新文件,例如 `contract_v1.0.1.docx`,字段 JSON 的 `Path` 指向最新版本,`Versions` 保留历史版本路径。
|
|
14
|
-
- 在线 Office 路由和文件上传字段都要能读取 `Versions`,用于右上角切换历史版本预览/编辑。
|
|
15
|
-
|
|
16
|
-
### OnlyOffice 服务端取文件与匿名预览规则
|
|
17
|
-
|
|
18
|
-
- OnlyOffice 文档服务器会在服务端再次下载文件。浏览器可以下载但 OnlyOffice 提示“下载失败”时,优先检查生成地址是否为 `localhost/127.0.0.1/内网域名`。
|
|
19
|
-
- OnlyOffice 可能先对文档地址发起 `HEAD` 探测。响应文件接口除了 `GET 200`,还必须让 `HEAD` 返回相同的 `Content-Type/Content-Length/Content-Disposition`,不能返回 `405`。
|
|
20
|
-
- 私有文件在线预览调用 `/apiengine/platform-private-file-url` 时传 `ForOfficePreview:true` 和权威资源上下文。审计代理应优先使用租户系统配置的公网 `ApiBase`,但仍把真实对象存储签名地址保存在共享 Redis ticket 中,禁止直接返回真实签名地址;旧 Controller 路由只作兼容转发。
|
|
21
|
-
- `/online-office` 可以匿名访问。公有存储模式只允许当前 `OsClient` 目录下的 `filePathName`;接口模式通过 `fileUrl` 接收当前平台正式 `ApiBase`,或由同端口本地后端读取的 loopback `/apiengine/...` 响应文件地址,并要求 URL 显式携带当前 `OsClient`。两种模式都拒绝跨租户、路径穿越和任意第三方 URL;`isPrivate=1` 必须登录。
|
|
22
|
-
- 匿名接口引擎预览不要求 V8 代码先上传 HDFS:接口开启【响应文件】和【允许匿名】后,完整地址 URL 编码传给 `fileUrl`,同时传 `fileName/fileType`。页面通过匿名安全中转让当前后端限域读取源文件,并以确定性路径缓存到当前租户公有对象存储;OnlyOffice 只接收公网 `FileServer` 地址。loopback 仅允许同端口本地后端访问,禁止开放任意 URL 代理。
|
|
23
|
-
- `canEdit` 只是前端请求参数,不是权限。最终编辑条件必须是“有效登录态 && canEdit=true”;匿名始终只读,不能因 URL 参数放开编辑。
|
|
24
|
-
- 匿名预览页隐藏左侧菜单、顶部导航和页签;已登录用户保持原系统布局。
|
|
25
|
-
- 匿名导出响应接口应配置频率限制或保证生成逻辑足够轻量,不能让单个公网 URL 无界消耗 CPU/内存;如业务仍需要落盘缓存,缓存事实必须进入共享 Redis/HDFS,不能用进程内变量。
|
|
26
|
-
|
|
27
|
-
字段 JSON 示例:
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"Name": "contract.docx",
|
|
32
|
-
"Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx",
|
|
33
|
-
"Version": "v1.0.1",
|
|
34
|
-
"Versions": [
|
|
35
|
-
{ "Version": "v1.0.0", "Name": "contract.docx", "Path": "/tenant_demo/file/20260622/contract.docx", "IsLatest": false },
|
|
36
|
-
{ "Version": "v1.0.1", "Name": "contract_v1.0.1.docx", "Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx", "IsLatest": true }
|
|
37
|
-
]
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
6
|
+
## Office 文件在线编辑版本号规则
|
|
7
|
+
|
|
8
|
+
当文件上传控件开启【Office 在线预览】、【允许在线编辑】和【开启 Office 文件版本号】时,前后端必须遵循统一版本规则:
|
|
9
|
+
|
|
10
|
+
- 新上传的 Office 文件(`pdf/doc/docx/xls/xlsx/ppt/pptx`)要立即写入初始版本 `v1.0.0`,字段 JSON 的 `Path` 指向该原始文件,`Version` 为 `v1.0.0`,`Versions[0]` 保存同一份原始文件路径。
|
|
11
|
+
- 用户进入 OnlyOffice 在线编辑页后,每次手动点击【保存文件】才生成新版本;第一次保存生成 `v1.0.1`,之后依次生成 `v1.0.2`、`v1.0.3`。
|
|
12
|
+
- 未开启版本号时,保存文件直接覆盖当前 `Path` 对应的 HDFS/OSS 源文件。
|
|
13
|
+
- 开启版本号时,保存文件必须生成带版本号后缀的新文件,例如 `contract_v1.0.1.docx`,字段 JSON 的 `Path` 指向最新版本,`Versions` 保留历史版本路径。
|
|
14
|
+
- 在线 Office 路由和文件上传字段都要能读取 `Versions`,用于右上角切换历史版本预览/编辑。
|
|
15
|
+
|
|
16
|
+
### OnlyOffice 服务端取文件与匿名预览规则
|
|
17
|
+
|
|
18
|
+
- OnlyOffice 文档服务器会在服务端再次下载文件。浏览器可以下载但 OnlyOffice 提示“下载失败”时,优先检查生成地址是否为 `localhost/127.0.0.1/内网域名`。
|
|
19
|
+
- OnlyOffice 可能先对文档地址发起 `HEAD` 探测。响应文件接口除了 `GET 200`,还必须让 `HEAD` 返回相同的 `Content-Type/Content-Length/Content-Disposition`,不能返回 `405`。
|
|
20
|
+
- 私有文件在线预览调用 `/apiengine/platform-private-file-url` 时传 `ForOfficePreview:true` 和权威资源上下文。审计代理应优先使用租户系统配置的公网 `ApiBase`,但仍把真实对象存储签名地址保存在共享 Redis ticket 中,禁止直接返回真实签名地址;旧 Controller 路由只作兼容转发。
|
|
21
|
+
- `/online-office` 可以匿名访问。公有存储模式只允许当前 `OsClient` 目录下的 `filePathName`;接口模式通过 `fileUrl` 接收当前平台正式 `ApiBase`,或由同端口本地后端读取的 loopback `/apiengine/...` 响应文件地址,并要求 URL 显式携带当前 `OsClient`。两种模式都拒绝跨租户、路径穿越和任意第三方 URL;`isPrivate=1` 必须登录。
|
|
22
|
+
- 匿名接口引擎预览不要求 V8 代码先上传 HDFS:接口开启【响应文件】和【允许匿名】后,完整地址 URL 编码传给 `fileUrl`,同时传 `fileName/fileType`。页面通过匿名安全中转让当前后端限域读取源文件,并以确定性路径缓存到当前租户公有对象存储;OnlyOffice 只接收公网 `FileServer` 地址。loopback 仅允许同端口本地后端访问,禁止开放任意 URL 代理。
|
|
23
|
+
- `canEdit` 只是前端请求参数,不是权限。最终编辑条件必须是“有效登录态 && canEdit=true”;匿名始终只读,不能因 URL 参数放开编辑。
|
|
24
|
+
- 匿名预览页隐藏左侧菜单、顶部导航和页签;已登录用户保持原系统布局。
|
|
25
|
+
- 匿名导出响应接口应配置频率限制或保证生成逻辑足够轻量,不能让单个公网 URL 无界消耗 CPU/内存;如业务仍需要落盘缓存,缓存事实必须进入共享 Redis/HDFS,不能用进程内变量。
|
|
26
|
+
|
|
27
|
+
字段 JSON 示例:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"Name": "contract.docx",
|
|
32
|
+
"Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx",
|
|
33
|
+
"Version": "v1.0.1",
|
|
34
|
+
"Versions": [
|
|
35
|
+
{ "Version": "v1.0.0", "Name": "contract.docx", "Path": "/tenant_demo/file/20260622/contract.docx", "IsLatest": false },
|
|
36
|
+
{ "Version": "v1.0.1", "Name": "contract_v1.0.1.docx", "Path": "/tenant_demo/file/20260622/contract_v1.0.1.docx", "IsLatest": true }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
41
|
<!-- /microi-progressive:chunk -->
|
|
42
42
|
<!-- microi-progressive:chunk id=v8-file-upload-009 sha256=50fba71370f45de3dd677f5731f3956c328a8d3b1ca2125b853128067f9af40a -->
|
|
43
|
-
## ImgUpload / FileUpload 字段值兼容规则
|
|
44
|
-
|
|
45
|
-
`ImgUpload` 不能假设只是一种值结构。PC 表单、移动端、旧数据、单图/多图、公开/私有桶会混合出现以下格式:
|
|
46
|
-
|
|
47
|
-
| 场景 | 可能的值 |
|
|
48
|
-
|------|----------|
|
|
49
|
-
| 空值/占位 | `''`、`null`、`undefined`、`'[]'`、`'null'`、`'正在上传中...'` |
|
|
50
|
-
| 旧单图 | `'/upload/xxx/a.png'`、`'https://cdn/a.png'` |
|
|
51
|
-
| 新单图 | `{ Path, Name, Size, Id, State }` 或 JSON 字符串 `'{"Path":"..."}'` |
|
|
52
|
-
| 多图 | `[{ Path, Name, Id, State }]` 或 JSON 字符串 `'[{"Path":"..."}]'` |
|
|
53
|
-
| 其它兼容字段 | `Path`、`FilePathName`、`FullPath`、`Url`、`url`、`src` |
|
|
54
|
-
|
|
55
|
-
任何端(PC、uni-app、H5、小程序)渲染图片前都必须先做“归一化 -> 取 Path -> 转最终 URL”,不要直接 `JSON.parse` 后只处理数组,也不要直接把字段值拼到 `FileServer`。
|
|
56
|
-
|
|
57
|
-
推荐归一化:
|
|
58
|
-
|
|
59
|
-
```javascript
|
|
60
|
-
function normalizeUploadValue(value) {
|
|
61
|
-
if (value == null || value === '' || value === 'undefined' || value === 'null') return [];
|
|
62
|
-
if (value === '正在上传中...' || value === '[]' || value === '[ ]') return [];
|
|
63
|
-
|
|
64
|
-
var raw = value;
|
|
65
|
-
if (typeof raw === 'string') {
|
|
66
|
-
var s = raw.trim();
|
|
67
|
-
if ((s.indexOf('{') === 0 || s.indexOf('[') === 0)) {
|
|
68
|
-
try { raw = JSON.parse(s); } catch (e) { raw = s; }
|
|
69
|
-
} else {
|
|
70
|
-
raw = s;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (Array.isArray(raw)) {
|
|
75
|
-
return raw.map(normalizeUploadItem).filter(function (it) { return !!it.Path; });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
var one = normalizeUploadItem(raw);
|
|
79
|
-
return one.Path ? [one] : [];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function normalizeUploadItem(item) {
|
|
83
|
-
if (!item) return {};
|
|
84
|
-
if (typeof item === 'string') {
|
|
85
|
-
return { Path: item, Name: item.split('/').pop() || item, State: 1 };
|
|
86
|
-
}
|
|
87
|
-
if (typeof item === 'object') {
|
|
88
|
-
var path = item.Path || item.FilePathName || item.FullPath || item.Url || item.url || item.src || '';
|
|
89
|
-
return {
|
|
90
|
-
Id: item.Id || item.id || '',
|
|
91
|
-
Name: item.Name || item.FileName || item.name || (path ? String(path).split('/').pop() : ''),
|
|
92
|
-
Size: item.Size || item.size || '',
|
|
93
|
-
CreateTime: item.CreateTime || item.createTime || '',
|
|
94
|
-
State: item.State == null ? 1 : item.State,
|
|
95
|
-
Path: path
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return {};
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
公开图片 URL 解析原则与 `Microi.Client/src/utils/diy.common.js` 的 `GetServerPath` 一致:
|
|
103
|
-
|
|
104
|
-
```javascript
|
|
105
|
-
function publicUploadUrl(path) {
|
|
106
|
-
if (!path) return '';
|
|
107
|
-
var s = String(path).trim();
|
|
108
|
-
if (!s || s === '正在上传中...') return '';
|
|
109
|
-
if (s.indexOf('.') === 0) return s; // ./static/img/loading.gif 等本地静态资源
|
|
110
|
-
if (/^(https?:|data:|blob:)/i.test(s)) return s; // 已经是最终 URL
|
|
111
|
-
if (s.indexOf('{') === 0 || s.indexOf('[') === 0) {
|
|
112
|
-
var list = normalizeUploadValue(s);
|
|
113
|
-
s = list.length ? list[0].Path : '';
|
|
114
|
-
}
|
|
115
|
-
if (!s) return '';
|
|
116
|
-
return String(V8.SysConfig.FileServer || '').replace(/\/+$/, '') + '/' + s.replace(/^\/+/, '');
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
私有桶(`Limit === true`)不要拼 `FileServer`,必须把归一化后的 `Path` 传给 `V8.Method.GetPrivateFileUrl({ FilePathName: path })` 或后端签名接口换临时 URL。
|
|
43
|
+
## ImgUpload / FileUpload 字段值兼容规则
|
|
44
|
+
|
|
45
|
+
`ImgUpload` 不能假设只是一种值结构。PC 表单、移动端、旧数据、单图/多图、公开/私有桶会混合出现以下格式:
|
|
46
|
+
|
|
47
|
+
| 场景 | 可能的值 |
|
|
48
|
+
|------|----------|
|
|
49
|
+
| 空值/占位 | `''`、`null`、`undefined`、`'[]'`、`'null'`、`'正在上传中...'` |
|
|
50
|
+
| 旧单图 | `'/upload/xxx/a.png'`、`'https://cdn/a.png'` |
|
|
51
|
+
| 新单图 | `{ Path, Name, Size, Id, State }` 或 JSON 字符串 `'{"Path":"..."}'` |
|
|
52
|
+
| 多图 | `[{ Path, Name, Id, State }]` 或 JSON 字符串 `'[{"Path":"..."}]'` |
|
|
53
|
+
| 其它兼容字段 | `Path`、`FilePathName`、`FullPath`、`Url`、`url`、`src` |
|
|
54
|
+
|
|
55
|
+
任何端(PC、uni-app、H5、小程序)渲染图片前都必须先做“归一化 -> 取 Path -> 转最终 URL”,不要直接 `JSON.parse` 后只处理数组,也不要直接把字段值拼到 `FileServer`。
|
|
56
|
+
|
|
57
|
+
推荐归一化:
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
function normalizeUploadValue(value) {
|
|
61
|
+
if (value == null || value === '' || value === 'undefined' || value === 'null') return [];
|
|
62
|
+
if (value === '正在上传中...' || value === '[]' || value === '[ ]') return [];
|
|
63
|
+
|
|
64
|
+
var raw = value;
|
|
65
|
+
if (typeof raw === 'string') {
|
|
66
|
+
var s = raw.trim();
|
|
67
|
+
if ((s.indexOf('{') === 0 || s.indexOf('[') === 0)) {
|
|
68
|
+
try { raw = JSON.parse(s); } catch (e) { raw = s; }
|
|
69
|
+
} else {
|
|
70
|
+
raw = s;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(raw)) {
|
|
75
|
+
return raw.map(normalizeUploadItem).filter(function (it) { return !!it.Path; });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var one = normalizeUploadItem(raw);
|
|
79
|
+
return one.Path ? [one] : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeUploadItem(item) {
|
|
83
|
+
if (!item) return {};
|
|
84
|
+
if (typeof item === 'string') {
|
|
85
|
+
return { Path: item, Name: item.split('/').pop() || item, State: 1 };
|
|
86
|
+
}
|
|
87
|
+
if (typeof item === 'object') {
|
|
88
|
+
var path = item.Path || item.FilePathName || item.FullPath || item.Url || item.url || item.src || '';
|
|
89
|
+
return {
|
|
90
|
+
Id: item.Id || item.id || '',
|
|
91
|
+
Name: item.Name || item.FileName || item.name || (path ? String(path).split('/').pop() : ''),
|
|
92
|
+
Size: item.Size || item.size || '',
|
|
93
|
+
CreateTime: item.CreateTime || item.createTime || '',
|
|
94
|
+
State: item.State == null ? 1 : item.State,
|
|
95
|
+
Path: path
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
公开图片 URL 解析原则与 `Microi.Client/src/utils/diy.common.js` 的 `GetServerPath` 一致:
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
function publicUploadUrl(path) {
|
|
106
|
+
if (!path) return '';
|
|
107
|
+
var s = String(path).trim();
|
|
108
|
+
if (!s || s === '正在上传中...') return '';
|
|
109
|
+
if (s.indexOf('.') === 0) return s; // ./static/img/loading.gif 等本地静态资源
|
|
110
|
+
if (/^(https?:|data:|blob:)/i.test(s)) return s; // 已经是最终 URL
|
|
111
|
+
if (s.indexOf('{') === 0 || s.indexOf('[') === 0) {
|
|
112
|
+
var list = normalizeUploadValue(s);
|
|
113
|
+
s = list.length ? list[0].Path : '';
|
|
114
|
+
}
|
|
115
|
+
if (!s) return '';
|
|
116
|
+
return String(V8.SysConfig.FileServer || '').replace(/\/+$/, '') + '/' + s.replace(/^\/+/, '');
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
私有桶(`Limit === true`)不要拼 `FileServer`,必须把归一化后的 `Path` 传给 `V8.Method.GetPrivateFileUrl({ FilePathName: path })` 或后端签名接口换临时 URL。
|
|
121
121
|
|
|
122
122
|
<!-- /microi-progressive:chunk -->
|
|
123
123
|
<!-- microi-progressive:chunk id=v8-file-upload-010 sha256=f34d476435c3a8321df017b481771d757f580c8d75f7648fdd54ee1c624d116c -->
|
|
124
|
-
## 安全注意
|
|
125
|
-
|
|
126
|
-
- ❌ 不要让前端任意指定 `Path`(路径穿越风险),只允许后端固定路径
|
|
127
|
-
- ❌ 不要不校验文件类型 / 大小:根据 ContentType + 后缀双重校验
|
|
128
|
-
- ❌ 不要把持有 Token 当成私有文件授权;普通用户必须证明菜单、记录和字段引用关系
|
|
129
|
-
- ❌ 不要向普通角色开放文件列表、移动、重命名、删除、覆盖等管理接口;这些接口仅限 `Level >= 9999`
|
|
130
|
-
- ❌ 不要开启 UEditor `catchimage` 远程抓图;默认关闭,确需采集时另建带域名白名单、DNS/IP 校验、禁止跳转、超时和响应上限的受控接口
|
|
131
|
-
- ❌ 敏感文件(合同、身份证)必须用私有桶 `Limit: true`
|
|
132
|
-
- ✅ 公有桶 URL 可缓存到前端,私有桶临时 URL 每次重新生成
|
|
133
|
-
- ✅ 删除数据时同步清理 HDFS 文件(避免存储泄漏)
|
|
134
|
-
- ✅ Excel/PDF 等导出文件通过【响应文件】配置返回,不要拼接到 JSON 数据里
|
|
135
|
-
|
|
136
|
-
### 安全升级兼容
|
|
137
|
-
|
|
138
|
-
- 旧页面如果只传 `FilePathName` 获取私有地址,升级后普通帐号会失败;必须补齐 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId`,不能改回匿名或放开管理权限。
|
|
139
|
-
- 旧自定义上传若依赖普通用户设置 `Limit:false` 或任意多级 `Path`,应改成私有上传;公开资源改走受控发布流程。
|
|
140
|
-
- 历史公有文件不会自动变成私有文件。迁移时先复制对象、验证私有读取,再停止旧公有访问;数据库继续保存租户内相对路径,不能持久化临时 URL。
|
|
141
|
-
- 上线验收至少覆盖:普通角色跨菜单/跨记录/跨字段读取被拒绝、单文件/单请求/数量限制、帐号与租户配额、Redis 故障失败关闭、多节点并发、公有匿名 `200`、私有匿名 `403`、授权私有访问 `200`。
|
|
142
|
-
|
|
143
|
-
### 复盘:ZIP 发布端与安装端 SHA256 口径不一致
|
|
144
|
-
|
|
145
|
-
- 触发场景:应用包已成功生成 ZIP 和摘要,但安装端下载 ZIP 后调用不存在的哈希辅助函数,或尝试通过当前 V8 环境不可用的 `.NET SHA256.Create()` 校验,导致安装中断。
|
|
146
|
-
- 根因:发布端实际使用 `V8.EncryptHelper.Sha256Hex(FileByteBase64)`,安装端却按原始字节设计了另一套实现,函数名称、输入数据和运行时能力均未对齐。
|
|
147
|
-
- 通用规则:文件摘要必须在包清单中记录算法和输入口径;当前应用 ZIP 统一使用 `SHA256-Base64Text`,发布端与安装端都对同一份 Base64 文本调用 `V8.EncryptHelper.Sha256Hex`。禁止仅凭函数名推测算法,也不要在未验证 V8 互操作能力时直接实例化 `.NET` 加密对象。
|
|
148
|
-
- 自动化检查:生成同时包含源码 ZIP、编译 ZIP 的应用包,再从官方地址下载并安装;分别篡改 Base64 文本和摘要,正常包应安装成功,两个篡改包都必须在解压前被拒绝。
|
|
124
|
+
## 安全注意
|
|
125
|
+
|
|
126
|
+
- ❌ 不要让前端任意指定 `Path`(路径穿越风险),只允许后端固定路径
|
|
127
|
+
- ❌ 不要不校验文件类型 / 大小:根据 ContentType + 后缀双重校验
|
|
128
|
+
- ❌ 不要把持有 Token 当成私有文件授权;普通用户必须证明菜单、记录和字段引用关系
|
|
129
|
+
- ❌ 不要向普通角色开放文件列表、移动、重命名、删除、覆盖等管理接口;这些接口仅限 `Level >= 9999`
|
|
130
|
+
- ❌ 不要开启 UEditor `catchimage` 远程抓图;默认关闭,确需采集时另建带域名白名单、DNS/IP 校验、禁止跳转、超时和响应上限的受控接口
|
|
131
|
+
- ❌ 敏感文件(合同、身份证)必须用私有桶 `Limit: true`
|
|
132
|
+
- ✅ 公有桶 URL 可缓存到前端,私有桶临时 URL 每次重新生成
|
|
133
|
+
- ✅ 删除数据时同步清理 HDFS 文件(避免存储泄漏)
|
|
134
|
+
- ✅ Excel/PDF 等导出文件通过【响应文件】配置返回,不要拼接到 JSON 数据里
|
|
135
|
+
|
|
136
|
+
### 安全升级兼容
|
|
137
|
+
|
|
138
|
+
- 旧页面如果只传 `FilePathName` 获取私有地址,升级后普通帐号会失败;必须补齐 `FormEngineKey`、`FormDataId`、`FieldId`、`SysMenuId`,不能改回匿名或放开管理权限。
|
|
139
|
+
- 旧自定义上传若依赖普通用户设置 `Limit:false` 或任意多级 `Path`,应改成私有上传;公开资源改走受控发布流程。
|
|
140
|
+
- 历史公有文件不会自动变成私有文件。迁移时先复制对象、验证私有读取,再停止旧公有访问;数据库继续保存租户内相对路径,不能持久化临时 URL。
|
|
141
|
+
- 上线验收至少覆盖:普通角色跨菜单/跨记录/跨字段读取被拒绝、单文件/单请求/数量限制、帐号与租户配额、Redis 故障失败关闭、多节点并发、公有匿名 `200`、私有匿名 `403`、授权私有访问 `200`。
|
|
142
|
+
|
|
143
|
+
### 复盘:ZIP 发布端与安装端 SHA256 口径不一致
|
|
144
|
+
|
|
145
|
+
- 触发场景:应用包已成功生成 ZIP 和摘要,但安装端下载 ZIP 后调用不存在的哈希辅助函数,或尝试通过当前 V8 环境不可用的 `.NET SHA256.Create()` 校验,导致安装中断。
|
|
146
|
+
- 根因:发布端实际使用 `V8.EncryptHelper.Sha256Hex(FileByteBase64)`,安装端却按原始字节设计了另一套实现,函数名称、输入数据和运行时能力均未对齐。
|
|
147
|
+
- 通用规则:文件摘要必须在包清单中记录算法和输入口径;当前应用 ZIP 统一使用 `SHA256-Base64Text`,发布端与安装端都对同一份 Base64 文本调用 `V8.EncryptHelper.Sha256Hex`。禁止仅凭函数名推测算法,也不要在未验证 V8 互操作能力时直接实例化 `.NET` 加密对象。
|
|
148
|
+
- 自动化检查:生成同时包含源码 ZIP、编译 ZIP 的应用包,再从官方地址下载并安装;分别篡改 Base64 文本和摘要,正常包应安装成功,两个篡改包都必须在解压前被拒绝。
|
|
149
149
|
<!-- /microi-progressive:chunk -->
|
|
@@ -55,50 +55,68 @@ description: 移动端 / 外部系统通过 HTTP 直接调用 Microi FormEngine
|
|
|
55
55
|
|
|
56
56
|
URL 中的表名小写最稳,平台不区分大小写。匿名版本目前**只在形式一上有**(`GetTableDataAnonymous` 等)。
|
|
57
57
|
|
|
58
|
-
## 请求 Header
|
|
59
|
-
|
|
60
|
-
| Header | 是否必填 | 说明 |
|
|
61
|
-
| --- | --- | --- |
|
|
62
|
-
| `Content-Type` | 必填 | `application/json` 推荐 |
|
|
63
|
-
| `OsClient` | 必填 | 租户标识;亦可放 querystring `?OsClient=xxx` 或 body |
|
|
64
|
-
| `authorization` | 鉴权接口必填 | `Bearer <Token>`;兼容旧客户端的 `Token` Header |
|
|
65
|
-
| `did` | 登录及鉴权接口推荐 | 当前终端稳定设备标识;首次生成后持久化,不能每次请求随机变化 |
|
|
66
|
-
|
|
67
|
-
注意:上表是 FormEngine 路由约定。ApiEngine HTTP 复测和移动端 `callEngine` 使用稳定路径 `/apiengine/{key}`,租户通过唯一的 `osclient` Header 传递,JSON/Form Body 可冗余携带 `OsClient`。普通 POST/PUT/PATCH/DELETE 禁止追加 `--OsClient--...--`;特殊路径只用于无法设置 Header/Form/Query 的 GET/HEAD 或第三方回调。
|
|
68
|
-
|
|
69
|
-
平台可能通过响应 Header `authorization`(兼容 `token`)续签或替换 Token。客户端必须立即保存新 Token,并保证并发旧响应不能覆盖已经写入的新 Token。跨域部署还必须在 CORS 中暴露 `authorization`、`token` 等需要读取的响应 Header。Token 属于凭据,禁止写入 URL、日志、错误上报和页面源码。
|
|
70
|
-
|
|
71
|
-
## FormEngine 数据授权边界
|
|
72
|
-
|
|
73
|
-
“Token 有效”只代表身份有效,不代表可以访问任意表。客户端 FormEngine CRUD 还会经过表、菜单、角色和行级范围授权:
|
|
74
|
-
|
|
75
|
-
1. 受保护的平台敏感表优先拒绝普通用户;匿名接口也不能绕过。
|
|
76
|
-
2. 标准菜单页应携带真实 `_SysMenuId`,或使用 `ModuleEngineKey`。服务器会严格校验该菜单是否绑定目标表、当前角色是否拥有菜单及对应操作权限;列表、计数、导出追加该菜单的数据范围。
|
|
77
|
-
3. 列表、写入、导入、导出显式传错 `_SysMenuId` / `ModuleEngineKey` 会直接拒绝,不会降级为其它菜单或表权限。单行详情只要当前角色拥有至少一个直接绑定同表的菜单(或精确表级 `Read` 权限)即可读取,不应用菜单 `SqlWhere` / `SqlJoin`;旧客户端携带过期菜单 Id 时也按该规则恢复。
|
|
78
|
-
4. 为兼容历史前端 V8/外部客户端,普通 CRUD 未传菜单上下文时,服务器会从当前用户的版本化授权缓存中查找“当前角色已获授权且直接绑定目标表”的菜单。无菜单列表仍安全合并候选菜单的数据范围,不能靠漏参绕过;详情按同表菜单访问,写入按 `Add/Edit/Del` 动作权限。也可以使用角色的精确表级 `Read/Add/Edit/Del` 授权。`JoinTables` 不是独立访问授权。
|
|
79
|
-
5. 导入、导出必须锚定具体菜单,不能依赖无菜单兼容推断。
|
|
80
|
-
6. `TableChild` 子表委托由标准表单运行时生成 `_TableChildAuth`,服务器重新校验父菜单、父记录、字段绑定、子表和外键,并强制父子范围。它是内部不透明上下文,外部客户端不要手工拼装。
|
|
81
|
-
7. `_InvokeType:'Client'` 只决定是否执行客户端语义的表单事件,不是权限开关。可信服务器调用标记也不能通过 HTTP Body 伪造。
|
|
58
|
+
## 请求 Header
|
|
59
|
+
|
|
60
|
+
| Header | 是否必填 | 说明 |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `Content-Type` | 必填 | `application/json` 推荐 |
|
|
63
|
+
| `OsClient` | 必填 | 租户标识;亦可放 querystring `?OsClient=xxx` 或 body |
|
|
64
|
+
| `authorization` | 鉴权接口必填 | `Bearer <Token>`;兼容旧客户端的 `Token` Header |
|
|
65
|
+
| `did` | 登录及鉴权接口推荐 | 当前终端稳定设备标识;首次生成后持久化,不能每次请求随机变化 |
|
|
66
|
+
|
|
67
|
+
注意:上表是 FormEngine 路由约定。ApiEngine HTTP 复测和移动端 `callEngine` 使用稳定路径 `/apiengine/{key}`,租户通过唯一的 `osclient` Header 传递,JSON/Form Body 可冗余携带 `OsClient`。普通 POST/PUT/PATCH/DELETE 禁止追加 `--OsClient--...--`;特殊路径只用于无法设置 Header/Form/Query 的 GET/HEAD 或第三方回调。
|
|
68
|
+
|
|
69
|
+
平台可能通过响应 Header `authorization`(兼容 `token`)续签或替换 Token。客户端必须立即保存新 Token,并保证并发旧响应不能覆盖已经写入的新 Token。跨域部署还必须在 CORS 中暴露 `authorization`、`token` 等需要读取的响应 Header。Token 属于凭据,禁止写入 URL、日志、错误上报和页面源码。
|
|
70
|
+
|
|
71
|
+
## FormEngine 数据授权边界
|
|
72
|
+
|
|
73
|
+
“Token 有效”只代表身份有效,不代表可以访问任意表。客户端 FormEngine CRUD 还会经过表、菜单、角色和行级范围授权:
|
|
74
|
+
|
|
75
|
+
1. 受保护的平台敏感表优先拒绝普通用户;匿名接口也不能绕过。
|
|
76
|
+
2. 标准菜单页应携带真实 `_SysMenuId`,或使用 `ModuleEngineKey`。服务器会严格校验该菜单是否绑定目标表、当前角色是否拥有菜单及对应操作权限;列表、计数、导出追加该菜单的数据范围。
|
|
77
|
+
3. 列表、写入、导入、导出显式传错 `_SysMenuId` / `ModuleEngineKey` 会直接拒绝,不会降级为其它菜单或表权限。单行详情只要当前角色拥有至少一个直接绑定同表的菜单(或精确表级 `Read` 权限)即可读取,不应用菜单 `SqlWhere` / `SqlJoin`;旧客户端携带过期菜单 Id 时也按该规则恢复。
|
|
78
|
+
4. 为兼容历史前端 V8/外部客户端,普通 CRUD 未传菜单上下文时,服务器会从当前用户的版本化授权缓存中查找“当前角色已获授权且直接绑定目标表”的菜单。无菜单列表仍安全合并候选菜单的数据范围,不能靠漏参绕过;详情按同表菜单访问,写入按 `Add/Edit/Del` 动作权限。也可以使用角色的精确表级 `Read/Add/Edit/Del` 授权。`JoinTables` 不是独立访问授权。
|
|
79
|
+
5. 导入、导出必须锚定具体菜单,不能依赖无菜单兼容推断。
|
|
80
|
+
6. `TableChild` 子表委托由标准表单运行时生成 `_TableChildAuth`,服务器重新校验父菜单、父记录、字段绑定、子表和外键,并强制父子范围。它是内部不透明上下文,外部客户端不要手工拼装。
|
|
81
|
+
7. `_InvokeType:'Client'` 只决定是否执行客户端语义的表单事件,不是权限开关。可信服务器调用标记也不能通过 HTTP Body 伪造。
|
|
82
|
+
|
|
83
|
+
因此:
|
|
84
|
+
|
|
85
|
+
- 标准模块、详情页和其字段元数据请求优先传真实 `_SysMenuId`,使服务器确定菜单绑定与操作权限;其中只有列表、计数、导出应用菜单查询范围。
|
|
86
|
+
- 历史无 `_SysMenuId` 请求可继续工作,但前提是当前用户确实拥有目标表对应的菜单或精确表权限;不能把兼容推断理解为“有 Token 即可查表”。
|
|
87
|
+
- 历史单表字段请求可在菜单缺失/过期时回退到当前角色另一个引用同表的已授权菜单;`GetDiyFieldByDiyTables` 批量字段请求按“第一张主表必须授权、后续表逐张授权并过滤”兼容。后续未授权/保护表不会导致主表失败,也不会返回其字段配置;这些规则不授予数据行权限。
|
|
88
|
+
- 表单设计器 `/api/DiyField/UptDiyFieldList` 属于 `Level >= 9999` 控制面:外层一次授权、字段归属校验、同事务批量更新、批次末一次缓存/版本刷新。禁止在 100+ 字段循环中逐条调用完整 `UptFormDataAsync("diy_field", ...)`,否则会重复执行授权、V8、日志及 SaaS/Redis 缓存工作。
|
|
89
|
+
- 接口引擎内的 `V8.FormEngine.*` 属于服务器端调用,不要求客户端 `_SysMenuId`。但接口引擎本身必须正确限制谁能调用,并在服务端校验业务对象范围,不能把表名和条件原样交给不可信客户端。
|
|
90
|
+
- 客户端新增、修改、删除分别校验真实菜单的 `Add`、`Edit`、`Del` 权限。`SqlWhere` / `SqlJoin` 是模块查询过滤,不是行级写权限:不得把它们追加到写入 SQL,也不得因查询包含 Join 拒绝已获授权的主表写入。
|
|
91
|
+
- 进入 `SubmitBeforeServerV8` / `SubmitAfterServerV8` 后,事件内 FormEngine/数据库调用与接口引擎一样属于可信服务器执行,可实现当前租户内的跨表事务。需要“只能修改本人数据”等业务约束以及归属字段写入时,应在这里或专用接口引擎中完成。
|
|
92
|
+
|
|
93
|
+
## 原生详情关联数据
|
|
82
94
|
|
|
83
|
-
|
|
95
|
+
- `POST /api/FormEngine/GetFormRelatedData` 接收父表 `ParentFormEngineKey`、父记录
|
|
96
|
+
`ParentTableRowId`、真实 `_SysMenuId`,以及固定 `RelatedType`:
|
|
97
|
+
`Counts/DataLog/DataComment/DataVersion`。身份、租户来自 DiyToken,不能透传请求的可信标记。
|
|
98
|
+
- `AuthorizeClientTableOperationAsync(..., 'Read')` 仅完成表/菜单授权,不证明父行可读。
|
|
99
|
+
必须再执行 Client `GetFormDataAsync`,让父记录不存在或 `ServerDataV8` 拒绝阻断关联查询。
|
|
100
|
+
- 固定辅助表查询使用服务端新构造的 CLR 参数与可信来源;`JObject` 中写
|
|
101
|
+
`_InvokeType:'Server'` 仍是非可信参数。不得通过给普通角色开放辅助表权限来修复此类拒绝。
|
|
102
|
+
- 日志和版本返回固定元信息,`DataAppend.HistoryContentMode='MetadataOnly'`;
|
|
103
|
+
当前行脱敏不授予历史 `Content/Data` 原文。客户端不能据元信息开放预览、对比、加载。
|
|
104
|
+
- 评论必须同时按真实 `TableId + TableRowId` 过滤。缺少 `TableId` 时,Counts 返回
|
|
105
|
+
`DataComment=null`,以 `DataAppend.DataCommentUnavailableReason/Message` 显式说明;
|
|
106
|
+
评论详情失败。不能按单键兼容、猜旧数据归属、假报零条或承诺不存在的可用升级包。
|
|
107
|
+
- 回归须区分实际 HTTP 与替身组件测试:普通角色父行可读、同菜单另一归属被 DataFilter
|
|
108
|
+
拒绝、父 Id 不存在、同 Id 不同表、伪造租户/类型/可信标记、历史敏感原文不返回都需验收。
|
|
109
|
+
安全评论绑定写入及旧数据处理须独立交付后才能宣称评论完整可用。
|
|
84
110
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```jsonc
|
|
96
|
-
{
|
|
97
|
-
"OsClient": "demo",
|
|
98
|
-
"FormEngineKey": "mall_product", // 形式一必填;形式二可选(已在 URL 中)
|
|
99
|
-
"_SysMenuId": "目标菜单Id", // 标准模块推荐;必须是真实绑定目标表且当前角色有权访问的菜单
|
|
100
|
-
"_Where": [["Status","=","OnSale"], ["AND","Stock",">",0]],
|
|
101
|
-
"_SelectFields": ["Id","Title","CurrentPrice","MainImg"],
|
|
111
|
+
## Body 结构(POST JSON)
|
|
112
|
+
|
|
113
|
+
```jsonc
|
|
114
|
+
{
|
|
115
|
+
"OsClient": "demo",
|
|
116
|
+
"FormEngineKey": "mall_product", // 形式一必填;形式二可选(已在 URL 中)
|
|
117
|
+
"_SysMenuId": "目标菜单Id", // 标准模块推荐;必须是真实绑定目标表且当前角色有权访问的菜单
|
|
118
|
+
"_Where": [["Status","=","OnSale"], ["AND","Stock",">",0]],
|
|
119
|
+
"_SelectFields": ["Id","Title","CurrentPrice","MainImg"],
|
|
102
120
|
"_OrderBy": "SoldCount",
|
|
103
121
|
"_OrderByType": "DESC",
|
|
104
122
|
"_PageIndex": 1,
|
|
@@ -108,7 +126,7 @@ URL 中的表名小写最稳,平台不区分大小写。匿名版本目前**
|
|
|
108
126
|
|
|
109
127
|
写操作(Add/Upt)将业务字段平铺到 body:
|
|
110
128
|
```jsonc
|
|
111
|
-
{ "OsClient":"demo", "FormEngineKey":"biz_order",
|
|
129
|
+
{ "OsClient":"demo", "FormEngineKey":"biz_order",
|
|
112
130
|
"Id":"01ABC...", "Quantity": 2, "Selected": 1 }
|
|
113
131
|
```
|
|
114
132
|
|
|
@@ -127,63 +145,63 @@ URL 中的表名小写最稳,平台不区分大小写。匿名版本目前**
|
|
|
127
145
|
## 客户端封装样板(uni-app)
|
|
128
146
|
|
|
129
147
|
```javascript
|
|
130
|
-
const BASE = 'https://api.itdos.com';
|
|
131
|
-
const OS_CLIENT = runtimeConfig.osClient;
|
|
132
|
-
const TOKEN_KEY = 'mall_token';
|
|
133
|
-
const DID_KEY = 'mall_did';
|
|
134
|
-
|
|
135
|
-
function normalizeToken(value) {
|
|
136
|
-
return String(value || '').replace(/^Bearer\s+/i, '').trim();
|
|
137
|
-
}
|
|
138
|
-
function getToken() {
|
|
139
|
-
return normalizeToken(uni.getStorageSync(TOKEN_KEY));
|
|
140
|
-
}
|
|
141
|
-
function getDid() {
|
|
142
|
-
let did = uni.getStorageSync(DID_KEY);
|
|
143
|
-
if (!did) {
|
|
144
|
-
did = `uni-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
145
|
-
uni.setStorageSync(DID_KEY, did);
|
|
146
|
-
}
|
|
147
|
-
return did;
|
|
148
|
-
}
|
|
149
|
-
function readHeader(headers, name) {
|
|
150
|
-
const key = Object.keys(headers || {}).find(k => k.toLowerCase() === name);
|
|
151
|
-
return key ? headers[key] : '';
|
|
152
|
-
}
|
|
153
|
-
function applyResponseToken(headers, requestToken) {
|
|
154
|
-
const responseToken = normalizeToken(
|
|
155
|
-
readHeader(headers, 'authorization') || readHeader(headers, 'token')
|
|
156
|
-
);
|
|
157
|
-
if (!responseToken) return;
|
|
158
|
-
|
|
159
|
-
const currentToken = getToken();
|
|
160
|
-
// 旧请求若只回显旧 Token,不得覆盖其它请求/标签页已保存的新 Token。
|
|
161
|
-
if (currentToken && requestToken && currentToken !== requestToken && responseToken === requestToken) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
uni.setStorageSync(TOKEN_KEY, responseToken);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function formEngineRequest(action, table, body = {}) {
|
|
168
|
-
return new Promise((resolve, reject) => {
|
|
169
|
-
const requestToken = getToken();
|
|
170
|
-
uni.request({
|
|
171
|
-
url: `${BASE}/api/formengine/${action}-${table}`,
|
|
172
|
-
method: 'POST',
|
|
148
|
+
const BASE = 'https://api.itdos.com';
|
|
149
|
+
const OS_CLIENT = runtimeConfig.osClient;
|
|
150
|
+
const TOKEN_KEY = 'mall_token';
|
|
151
|
+
const DID_KEY = 'mall_did';
|
|
152
|
+
|
|
153
|
+
function normalizeToken(value) {
|
|
154
|
+
return String(value || '').replace(/^Bearer\s+/i, '').trim();
|
|
155
|
+
}
|
|
156
|
+
function getToken() {
|
|
157
|
+
return normalizeToken(uni.getStorageSync(TOKEN_KEY));
|
|
158
|
+
}
|
|
159
|
+
function getDid() {
|
|
160
|
+
let did = uni.getStorageSync(DID_KEY);
|
|
161
|
+
if (!did) {
|
|
162
|
+
did = `uni-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
163
|
+
uni.setStorageSync(DID_KEY, did);
|
|
164
|
+
}
|
|
165
|
+
return did;
|
|
166
|
+
}
|
|
167
|
+
function readHeader(headers, name) {
|
|
168
|
+
const key = Object.keys(headers || {}).find(k => k.toLowerCase() === name);
|
|
169
|
+
return key ? headers[key] : '';
|
|
170
|
+
}
|
|
171
|
+
function applyResponseToken(headers, requestToken) {
|
|
172
|
+
const responseToken = normalizeToken(
|
|
173
|
+
readHeader(headers, 'authorization') || readHeader(headers, 'token')
|
|
174
|
+
);
|
|
175
|
+
if (!responseToken) return;
|
|
176
|
+
|
|
177
|
+
const currentToken = getToken();
|
|
178
|
+
// 旧请求若只回显旧 Token,不得覆盖其它请求/标签页已保存的新 Token。
|
|
179
|
+
if (currentToken && requestToken && currentToken !== requestToken && responseToken === requestToken) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
uni.setStorageSync(TOKEN_KEY, responseToken);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formEngineRequest(action, table, body = {}) {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const requestToken = getToken();
|
|
188
|
+
uni.request({
|
|
189
|
+
url: `${BASE}/api/formengine/${action}-${table}`,
|
|
190
|
+
method: 'POST',
|
|
173
191
|
header: {
|
|
174
|
-
'Content-Type': 'application/json',
|
|
175
|
-
'OsClient': OS_CLIENT,
|
|
176
|
-
'authorization': requestToken ? `Bearer ${requestToken}` : '',
|
|
177
|
-
'did': getDid()
|
|
178
|
-
},
|
|
179
|
-
data: { OsClient: OS_CLIENT, FormEngineKey: table, ...body },
|
|
180
|
-
success: (res) => {
|
|
181
|
-
applyResponseToken(res.header || res.headers, requestToken);
|
|
182
|
-
resolve(res.data || {});
|
|
183
|
-
},
|
|
184
|
-
fail: reject
|
|
185
|
-
});
|
|
186
|
-
});
|
|
192
|
+
'Content-Type': 'application/json',
|
|
193
|
+
'OsClient': OS_CLIENT,
|
|
194
|
+
'authorization': requestToken ? `Bearer ${requestToken}` : '',
|
|
195
|
+
'did': getDid()
|
|
196
|
+
},
|
|
197
|
+
data: { OsClient: OS_CLIENT, FormEngineKey: table, ...body },
|
|
198
|
+
success: (res) => {
|
|
199
|
+
applyResponseToken(res.header || res.headers, requestToken);
|
|
200
|
+
resolve(res.data || {});
|
|
201
|
+
},
|
|
202
|
+
fail: reject
|
|
203
|
+
});
|
|
204
|
+
});
|
|
187
205
|
}
|
|
188
206
|
export const formEngineGet = (t, w) => formEngineRequest('gettabledata', t, w);
|
|
189
207
|
export const formEngineGetOne = (t, w) => formEngineRequest('getformdata', t, w);
|
|
@@ -198,13 +216,13 @@ export const formEngineDel = (t, d) => formEngineRequest('delformdata', t, d
|
|
|
198
216
|
| --- | --- |
|
|
199
217
|
| 404 Not Found | URL 缺 `/api/` 前缀,或表名/动作之间用 `/` 而不是 `-` |
|
|
200
218
|
| 405 Method Not Allowed | 用了 GET(FormEngine 全部为 POST) |
|
|
201
|
-
| 1001 登录身份已过期 | 没传 Token / Token 过期 / Redis 重启 |
|
|
202
|
-
| 1002 身份验证失败 | OsClient 不匹配 |
|
|
203
|
-
| `NoAuth` / `您没有权限做此操作` | Token 有效但菜单、表操作权限、表绑定角色、行级范围或敏感表策略不允许 |
|
|
204
|
-
| 显式传 `_SysMenuId` 后无权限 | 列表/写入使用错误菜单会严格拒绝;唯一详情在当前角色仍拥有另一个同表菜单时可恢复,不应用菜单查询范围 |
|
|
205
|
-
| 无 `_SysMenuId` 仍无权限 | 当前角色没有直接绑定该表的菜单授权,也没有精确表级授权 |
|
|
206
|
-
| 并发后提示 TokenReplaced / MissingToken | 客户端没有接收响应新 Token,或旧请求/其它标签页覆盖或清除了共享新 Token |
|
|
207
|
-
| Code:0 表不存在 | `FormEngineKey` 在 `diy_table` 不存在 |
|
|
219
|
+
| 1001 登录身份已过期 | 没传 Token / Token 过期 / Redis 重启 |
|
|
220
|
+
| 1002 身份验证失败 | OsClient 不匹配 |
|
|
221
|
+
| `NoAuth` / `您没有权限做此操作` | Token 有效但菜单、表操作权限、表绑定角色、行级范围或敏感表策略不允许 |
|
|
222
|
+
| 显式传 `_SysMenuId` 后无权限 | 列表/写入使用错误菜单会严格拒绝;唯一详情在当前角色仍拥有另一个同表菜单时可恢复,不应用菜单查询范围 |
|
|
223
|
+
| 无 `_SysMenuId` 仍无权限 | 当前角色没有直接绑定该表的菜单授权,也没有精确表级授权 |
|
|
224
|
+
| 并发后提示 TokenReplaced / MissingToken | 客户端没有接收响应新 Token,或旧请求/其它标签页覆盖或清除了共享新 Token |
|
|
225
|
+
| Code:0 表不存在 | `FormEngineKey` 在 `diy_table` 不存在 |
|
|
208
226
|
| Code:0 字段不存在 | `_Where` / `_SelectFields` 写了表上没有的字段 |
|
|
209
227
|
| 返回 `null` 而不是 DosResult | Controller 抛了异常被吞,到后端日志看 `Microi.Core` 报错 |
|
|
210
228
|
|
|
@@ -216,5 +234,5 @@ export const formEngineDel = (t, d) => formEngineRequest('delformdata', t, d
|
|
|
216
234
|
| POST `/api/formengine/getformdata-mall_member` | `V8.FormEngine.GetFormData('mall_member', {...})` |
|
|
217
235
|
| POST `/api/formengine/uptformdata-mall_shopping_cart` | `V8.FormEngine.UptFormData('mall_shopping_cart', {...})` |
|
|
218
236
|
|
|
219
|
-
> 客户端 HTTP 调用**会**触发 `SubmitBeforeServerV8`、`SubmitAfterServerV8`、`DataFilterV8` 等服务端事件;
|
|
220
|
-
> 而 V8 引擎内调用 `V8.FormEngine.*` 默认**不**触发,除非显式传 `_InvokeType:'Client'`。`_InvokeType` 只影响事件语义,不授予任何客户端表权限。
|
|
237
|
+
> 客户端 HTTP 调用**会**触发 `SubmitBeforeServerV8`、`SubmitAfterServerV8`、`DataFilterV8` 等服务端事件;
|
|
238
|
+
> 而 V8 引擎内调用 `V8.FormEngine.*` 默认**不**触发,除非显式传 `_InvokeType:'Client'`。`_InvokeType` 只影响事件语义,不授予任何客户端表权限。
|