h3factory 0.0.0-stage → 0.2.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.
Files changed (37) hide show
  1. package/README.md +241 -2
  2. package/bin/h3factory.js +87 -0
  3. package/cli.py +951 -0
  4. package/config.example.json +8 -0
  5. package/docs/platform_gotchas.md +118 -0
  6. package/docs/requirement_checklist.md +54 -0
  7. package/docs/schema_doc.md +216 -0
  8. package/docs/workflow.md +146 -0
  9. package/h3design/__init__.py +0 -0
  10. package/h3design/controls.py +414 -0
  11. package/h3design/dsl.py +553 -0
  12. package/h3design/newbuilder.py +852 -0
  13. package/h3platform/__init__.py +0 -0
  14. package/h3platform/api.py +214 -0
  15. package/h3platform/h3console.py +99 -0
  16. package/h3platform/settings.py +74 -0
  17. package/h3verify/__init__.py +0 -0
  18. package/h3verify/verify.py +217 -0
  19. package/package.json +51 -4
  20. package/projects/demo/dicts.json +10 -0
  21. package/projects/demo/generated/product.cs +82 -0
  22. package/projects/demo/groups.json +3 -0
  23. package/projects/demo/sheets/product.json +24 -0
  24. package/scripts/postinstall.js +109 -0
  25. package/skills/h3factory/SKILL.md +137 -0
  26. package/skills/h3yun-dev-assistant/BACKEND_GUIDELINES.md +120 -0
  27. package/skills/h3yun-dev-assistant/DEV_DOC_ROUTING.md +39 -0
  28. package/skills/h3yun-dev-assistant/FORM_ACTIONS_GUIDELINES.md +116 -0
  29. package/skills/h3yun-dev-assistant/FRONTEND_GUIDELINES.md +47 -0
  30. package/skills/h3yun-dev-assistant/OPENAPI_GUIDELINES.md +8 -0
  31. package/skills/h3yun-dev-assistant/PLATFORM_CONSTRAINTS.md +160 -0
  32. package/skills/h3yun-dev-assistant/POST_GUIDELINES.md +47 -0
  33. package/skills/h3yun-dev-assistant/PRACTICE_NOTES.md +626 -0
  34. package/skills/h3yun-dev-assistant/SKILL.md +70 -0
  35. package/skills/h3yun-dev-assistant/SQL_GUIDELINES.md +9 -0
  36. package/skills/h3yun-dev-assistant/TOOL_FUNCTIONS.md +96 -0
  37. package/templates/controller.cs.tpl +78 -0
@@ -0,0 +1,626 @@
1
+ # 实测速查与实践经验(PRACTICE_NOTES)
2
+
3
+ > 本文件整理自历史项目实测,是 `*_GUIDELINES.md` 规则文件之外的经验补充(控件取值类型、完整工具代码、系统表、流程/消息代码骨架、OpenAPI 附件实测、已知坑等)。
4
+ > 与规则文件冲突时以规则文件为准;标注“实测/经验”的数值(批量上限、定时器预算等)是经验起点,不是平台硬限制,应以压测与当前租户实测为准。
5
+ > 官方文档索引:https://h3yunpro.github.io/llms.txt ;OpenAPI:https://h3yun-pro-doc-openapi.apifox.cn/llms.txt
6
+
7
+ ## 一、实测环境与调用约定
8
+
9
+ - 引擎 EngineCode=`lhwof65gjkn00u12uekc8lfp6`(示例,客户各项目不同);OpenAPI 统一入口 `https://www.h3yun.com/OpenApi/Invoke`。
10
+ - 请求头 `EngineCode`、`EngineSecret`:**企业引擎**的 Code/Secret(头像→系统管理→系统集成,仅系统管理员可见;不是应用的)。
11
+ - 返回统一 `{"Successful":true,"ErrorMessage":null,"ReturnData":{...}}`;失败 `Successful:false` + `ErrorMessage`。
12
+ - 调用频率 1-2 次/秒、单次请求 ≤60s(官方限制,无返回限流消息;大批量建议 200ms+ 间隔 + 重试一次)。
13
+ - 响应 UTF-8:Windows 控制台 GBK 下用 `PYTHONIOENCODING=utf-8` 或写文件防乱码。
14
+ - 技术栈:后端 C#(.NET Framework 4.6.2,**不能引第三方库、禁 IO**);前端 ES5;数据库 MySQL 8(业务表可直接 SQL 查询)。
15
+ - 开发方式:在线编辑,4 个代码编辑器 = 表单设计-前端/后端 + 列表设计-前端/后端,保存即生效;建议先在测试应用上验证。
16
+
17
+ Python 调用模板(参考 `D:\AI\天财商龙\probe_h3.py`):
18
+
19
+ ```python
20
+ import json, urllib.request
21
+ URL = "https://www.h3yun.com/OpenApi/Invoke"
22
+ EC, ES = "引擎Code", "引擎Secret"
23
+
24
+ def invoke(body: dict) -> dict:
25
+ req = urllib.request.Request(URL,
26
+ json.dumps(body, ensure_ascii=False).encode("utf-8"),
27
+ {"Content-Type": "application/json", "EngineCode": EC, "EngineSecret": ES})
28
+ raw = urllib.request.urlopen(req, timeout=60).read()
29
+ for enc in ("utf-8", "gbk"): # 返回UTF-8, Windows控制台GBK可能乱码
30
+ try: return json.loads(raw.decode(enc))
31
+ except (UnicodeDecodeError, json.JSONDecodeError): continue
32
+ return {"raw": raw[:200]}
33
+ ```
34
+
35
+ ## 二、编码查看
36
+
37
+ - **应用编码**:应用重命名弹框里;或设计表单 URL 的 `appcode` 参数。
38
+ - **表单编码**:设计表单 URL 的 `id` 参数(**最准**;专业版表单带 D00xxx 前缀,表单属性里灰框前的 D00xxx 属于编码一部分)。
39
+ - **控件编码**:控件属性栏;子表内控件完整格式 `子表编码.控件编码`,前端取实例用完整格式,后端/OpenAPI 只取 `.` 后半部分。
40
+ - **EngineCode/Secret**:系统管理→系统集成(仅管理员)。
41
+
42
+ ## 三、控件取值/赋值速查
43
+
44
+ ### 3.1 后端 `bo["编码"]` 类型(主表与子表一致)
45
+
46
+ | 控件 | 取值 | 赋值 |
47
+ |---|---|---|
48
+ | 文本/单选/下拉/流水号 | `bo["code"] + string.Empty` | `bo["code"] = "xxx";` 清空 `= null` |
49
+ | 人员/部门单选、关联表单 | String=Id(Id 查 `H_User`/`H_Organizationunit`) | 同上 |
50
+ | 人员/部门多选、关联多选 | `(string[])bo["code"]` | `bo["code"] = new string[]{...}` |
51
+ | 日期 | `+ string.Empty` 后 `DateTime.Parse` | `bo["code"] = DateTime.Now` |
52
+ | 数字 | `decimal.Parse(bo["code"] + "")`(**decimal,16 位小数精度**) | `bo["code"] = 100.22m` |
53
+ | 复选框 | `str.Split(';')`,选中判断 `str.Contains("选项")` | `bo["code"] = "选项1;选项2"` |
54
+ | 是/否 | string → `bool.Parse`(先判空) | `bo["code"] = true`,清空 `= false` |
55
+ | 地址/位置 | JSON 字符串(高德 GCJ-02) | JSON 字符串 |
56
+ | 附件/图片 | **取不到值**,查 `H_BizObjectFile` 表 | 只能 `CopyFiles` 复制;删 `RemoveFile` |
57
+ | 子表 | `(H3.DataModel.BizObject[])bo["子表code"]`(**禁止转 List**,平台返回数组) | `bo["子表code"] = list.ToArray()` |
58
+
59
+ - 子表读取前先判 `null` / 长度 0,再 `foreach` 遍历。
60
+ - 新增子表行:`GetChildSchema("子表code")` → new BizObject 填值 → 绑回主表数组 → 主表 Create/Update 一起存;删除行 = 重组成新数组再赋回;清空 = `new H3.DataModel.BizObject[]{}`。
61
+ - 系统属性用 `bo.属性名`(`ObjectId` `Name` `OwnerId` `OwnerDeptId` `Status` `WorkflowInstanceId` `CreatedBy` `CreatedTime` `ModifiedBy` `ModifiedTime`),不用索引器;**这些字段表里一定存在**,即使表单没拖对应控件。
62
+ - 状态枚举:`Draft=0` `Effective=1` `Running=2` `Canceled=3`。
63
+
64
+ ### 3.2 OpenAPI 传值格式(Filter/BizObject 是 **JSON 字符串**,不是对象)
65
+
66
+ | 控件 | 格式 | 示例 |
67
+ |---|---|---|
68
+ | 单行/多行/单选框/下拉框 | String | `{"F0000001":"文本值"}` |
69
+ | 人员单选/部门单选/关联表单 | String=Id | `{"F0000001":"<用户Id/部门Id/数据Id>"}` |
70
+ | 人员多选/部门多选/关联多选 | **String 数组** | `{"F0000001":["id1","id2"]}` |
71
+ | 日期 | String `2024-01-01` 或 `2024-01-01 13:59:01` | `{"F0000001":"2024-01-01"}` |
72
+ | 数字 | Number | `{"F0000001":12.34}` |
73
+ | 复选框 | String `选项1;选项2` | `{"F0000001":"选项1;选项2"}` |
74
+ | 是/否 | Boolean | `{"F0000001":true}` |
75
+ | 地址/位置 | JSON 字符串 | `{"F0000001":"{\"adcode\":\"440305\",...}"}` |
76
+ | 附件/图片/签名 | **不能随数据传**,创建后 `UploadAttachment` 上传;查询返回文件 Id 数组 | — |
77
+ | 子表 | Object 数组 | `{"子表code":[{"F0000002":"文本一"},...]}` |
78
+
79
+ **查询返回补充字段**:人员/部门类控件多返回 `${控件编码}Object`(如 `CreatedByObject`、`OwnerIdObject`、`SingleUserObject`、`SingleDepartmentObject`),内容 `{ObjectId, Name}`——只在查询类接口返回,**创建/更新时千万别传**。
80
+ **ObjectId/SeqNo** 是系统生成字段不能写入(写入前剔除);Association 字段写目标记录 **ObjectId**;子表行含 `ParentObjectId`,子表字段 code 只取 `.` 后半部分。
81
+
82
+ ### 3.3 前端取值赋值
83
+
84
+ - `that.控件编码.GetValue()/SetValue(v)`:人单选取值返回数组取 `[0]`,赋值传单个 Id;日期 SetValue 只收 `yyyy-MM-dd` / `yyyy-MM-dd HH:mm:ss`;复选框 `"a;b"`。
85
+ - 前端取子表内控件实例用完整格式 `子表编码.控件编码`。
86
+
87
+ ## 四、Filter 与查询
88
+
89
+ ### 4.1 获取 BizObject 实例 4 种方式
90
+
91
+ ```csharp
92
+ H3.IEngine engine = this.Engine; // 定时器里用参数 engine
93
+ H3.DataModel.BizObject bo = this.Request.BizObject; // 当前表单数据
94
+ H3.DataModel.BizObject bo = new H3.DataModel.BizObject(engine, schema, H3.Organization.User.SystemUserId); // 新建(schema 用 engine.BizObjectManager.GetPublishedSchema("表单编码"))
95
+ H3.DataModel.BizObject bo = H3.DataModel.BizObject.Load(SystemUserId, engine, schemaCode, objectId, false); // 按Id查
96
+ H3.DataModel.BizObject[] boArr = H3.DataModel.BizObject.GetList(engine, SystemUserId, schema,
97
+ H3.DataModel.GetListScopeType.GlobalAll, filter); // 批量查
98
+ ```
99
+
100
+ ### 4.2 C# 侧 Filter 构造(与 OpenAPI Filter JSON 同构)
101
+
102
+ ```csharp
103
+ H3.Data.Filter.Filter filter = new H3.Data.Filter.Filter();
104
+ H3.Data.Filter.And and = new H3.Data.Filter.And(); // Or 同理
105
+ and.Add(new H3.Data.Filter.ItemMatcher("Status", H3.Data.ComparisonOperatorType.Equal,
106
+ H3.DataModel.BizObjectStatus.Effective));
107
+ filter.Matcher = and;
108
+ filter.FromRowNum = 0; filter.ToRowNum = 1000; // 分页:每页最多 1000 条(底层硬限)
109
+ filter.AddSortBy("CreatedTime", H3.Data.Filter.SortDirection.Ascending); // 分页必须排序防重复
110
+ ```
111
+
112
+ ### 4.3 运算符映射(C# 枚举 ↔ OpenAPI Operator ↔ SQL)
113
+
114
+ `Above 0 >`;`NotBelow 1 >=`;`Equal 2 =`;`NotAbove 3 <=`;`Below 4 <`;`NotEqual 5 !=`;`In 6 in`;`NotIn 7 not in`;`Contains 8 like '%?%'`;`StartWith 13 like '?%'`;`EndWith 14`;`IsNull 18`;`NotNull 19`;`IsNone 20 (is null or ='')`;`NotNone 21`;`NotStartWith 22`;`NotEndWith 23`;`NotContains 24`。
115
+
116
+ ### 4.4 OpenAPI Filter JSON 结构(字符串)
117
+
118
+ ```json
119
+ {"FromRowNum":0,"ToRowNum":500,"Matcher":{"Type":"And","Matchers":[{"Type":"And","Matchers":[{"Type":"Item","Name":"F0000001","Operator":2,"Value":"值"}]}]},"SortByCollection":[],"RequireCount":false}
120
+ ```
121
+
122
+ ## 五、增删改、批量与附件
123
+
124
+ ### 5.1 创建/更新/删除
125
+
126
+ ```csharp
127
+ // 创建:ObjectId/OwnerDeptId/CreatedTime/Name/SeqNo 自动生成,无需设置;OwnerId 必须设
128
+ bo.CreatedBy = userId; bo.OwnerId = userId;
129
+ bo.Status = H3.DataModel.BizObjectStatus.Draft; // Effective 自动触发生效业务规则;Running 不会自动建流程!
130
+ bo["F0000001"] = "xxx";
131
+ H3.ErrorCode err = bo.Create(); // Update() / Remove() 同理
132
+ if (err != H3.ErrorCode.Success) throw new Exception("操作失败:" + err);
133
+ ```
134
+
135
+ - `base.OnSubmit` **上方**改 `this.Request.BizObject` 值自动保存;**下方**改需先 `bo.Load()` 重载(系统字段可能已变)再改再 `bo.Update()`。
136
+ - Post 请求中更新/删除当前业务对象:必须先按 `ObjectId` `BizObject.Load` 完整加载再改(不能直接操作 `this.Request.BizObject`)。
137
+ - 删除时 `Request.BizObject` 只有系统字段,要先 `Load()`。
138
+ - 常见 ErrorCode:`ValidateNoRepeatRuleFailed`=不允许重复录入;`ValidateSubmitRuleFailed`=提交校验。
139
+ - 触发公式:`CalcPropertyComputationRule(bo, "控件code")`——主表传主表 bo+主表 code,子表传子表 bo+子表内 code(**不传 "子表.子表内" 完整格式**),结果直接写回 bo;跨主子表引用的公式不支持,需自行实现。
140
+
141
+ ### 5.2 批量(BulkCommit)
142
+
143
+ ```csharp
144
+ H3.DataModel.BulkCommit commit = new H3.DataModel.BulkCommit();
145
+ foreach (var b in list) b.Create(commit); // Create/Update/Remove 传入 commit 只收集不执行
146
+ string errorMsg = null; commit.Commit(engine.BizObjectManager, out errorMsg);
147
+ if (!string.IsNullOrEmpty(errorMsg)) throw new Exception("批量操作失败:" + errorMsg);
148
+ ```
149
+
150
+ - 一次 Commit 经验值 ≤300 条(官方工具示例每 300 条一提交),500 内通常可行,具体按压测;`list` 内只能同一种模式。
151
+ - 批量增删改会触发自动化/业务规则,把额外耗时算进 60s 预算;重试前考虑幂等。
152
+
153
+ ### 5.3 附件(后端)
154
+
155
+ - `engine.BizObjectManager.CopyFiles(源主表, 源子表, 源控件, 源Id, 目标主表, 目标子表, 目标控件, 目标Id, 是否覆盖, 是否物理删除)`
156
+ - `RemoveFile(附件Id, true)` 删除。
157
+ - 查附件:`SELECT objectid FROM H_BizObjectFile WHERE fileflag=0 AND schemacode=... AND propertyname=... AND bizobjectid=...`
158
+ - **表单未保存前(base.OnSubmit 之前)不能取/复制当前表单附件**。
159
+
160
+ ### 5.4 SQL 速查
161
+
162
+ ```csharp
163
+ System.Data.DataTable dt = engine.Query.QueryTable("SELECT ObjectId FROM i_表单编码 WHERE Status=1", null);
164
+ // 参数化(防注入,必备);DbType 必须与值类型匹配
165
+ List<H3.Data.Database.Parameter> ps = new List<H3.Data.Database.Parameter>();
166
+ ps.Add(new H3.Data.Database.Parameter("@name", System.Data.DbType.String, "张三"));
167
+ engine.Query.QueryTable("SELECT ObjectId FROM H_User WHERE Name=@name", ps.ToArray());
168
+ ```
169
+
170
+ - 只查不改表结构(**无 DDL**);增删改尽量走 BizObject;SQL **30 秒超时**(报 `Timeout in IO operation` / `Connection must be valid and open to rollback transaction` 时拆分批次)。
171
+ - SQL 语句压缩为**单行**字符串,避免换行导致平台编译器报错。
172
+ - 空值存 NULL,查询用 `IS NULL`/`IS NOT NULL`,不用 `= NULL`。
173
+ - 业务主表 `i_表单编码`,子表 `i_子表控件编码`,关联多选中间表 `i_表单编码_关联表单多选控件编码`(子表内则为 `i_子表控件编码_关联表单多选控件编码`)。
174
+ - 不确定表单/控件编码时,先通过氚云 SQL 排查工具读取 `cmax.json` 与 `fields.md`。
175
+
176
+ 常用 SQL 片段:
177
+
178
+ ```sql
179
+ -- 查表单结构(含子表、显示名)
180
+ SELECT SchemaCode AS `主表编码`, ChildSchemas AS `子表编码`,
181
+ extractvalue(Content,'/BizObjectSchema/DisplayName') AS `主表名称`
182
+ FROM H_PublishedBizObjectSchema
183
+ WHERE SchemaCode = '表单编码' OR ChildSchemas LIKE '%表单编码%';
184
+
185
+ -- 查表单设计代码(表单/列表设计)
186
+ SELECT Javascript AS `旧版前端代码`, NewJsCode AS `新版前端代码`, BehindCode AS `后端代码`
187
+ FROM H_PublishedFormSetting WHERE SchemaCode = '表单编码';
188
+ SELECT Javascript AS `前端代码`, BehindCode AS `后端代码`
189
+ FROM H_PublishedListViewSetting WHERE SchemaCode = '表单编码';
190
+
191
+ -- 查审批通过时间
192
+ SELECT date_format(b.FinishTime, '%Y-%m-%d %H:%i:%s') AS `审批通过时间`, Approval AS `是否最终审批通过`
193
+ FROM i_表单编码 a LEFT JOIN H_WorkflowInstance b ON a.WorkflowInstanceId = b.ObjectId
194
+ WHERE b.Approval = 1;
195
+ ```
196
+
197
+ ## 六、官方实践工具方法(完整代码,复制进代码块即可用)
198
+
199
+ **重要原则**:开发时优先把以下工具方法加进代码块,业务代码里直接调用,实现精简和复用。声明变量不赋默认值会导致调试模式报错。
200
+
201
+ ### 6.1 后端类型转换
202
+
203
+ ```csharp
204
+ // 业务对象字段值转 string
205
+ public static string gStr(object val) { return val + string.Empty; }
206
+
207
+ // 业务对象字段值转 bool
208
+ public static bool gBool(object val) {
209
+ string valStr = (val + string.Empty).ToLower();
210
+ return (valStr == "true" || valStr == "1");
211
+ }
212
+
213
+ // 通用数值转换(支持默认值)
214
+ public static decimal gNum(object val, decimal defVal) {
215
+ string valStr = val + string.Empty;
216
+ valStr = string.IsNullOrWhiteSpace(valStr) ? "0" : valStr;
217
+ decimal n = defVal;
218
+ return decimal.TryParse(valStr, out n) ? n : defVal;
219
+ }
220
+ public static double gNum(object val, double defVal) {
221
+ string valStr = val + string.Empty;
222
+ valStr = string.IsNullOrWhiteSpace(valStr) ? "0" : valStr;
223
+ double n = defVal;
224
+ return double.TryParse(valStr, out n) ? n : defVal;
225
+ }
226
+ public static int gNum(object val, int defVal) {
227
+ string valStr = val + string.Empty;
228
+ valStr = string.IsNullOrWhiteSpace(valStr) ? "0" : valStr;
229
+ int n = defVal;
230
+ return int.TryParse(valStr, out n) ? n : defVal;
231
+ }
232
+ public static decimal gDecimalNum(object val) { return gNum(val, 0m); }
233
+ public static double gDoubleNum(object val) { return gNum(val, 0.0); }
234
+ public static int gIntNum(object val) { return gNum(val, 0); }
235
+ ```
236
+
237
+ ### 6.2 子表汇总
238
+
239
+ ```csharp
240
+ // 通用子表汇总(支持默认值)
241
+ public static decimal gSum(object objArray, string field, decimal defVal) {
242
+ if(objArray == null || string.IsNullOrWhiteSpace(field)) { return defVal; }
243
+ H3.DataModel.BizObject[] bObjArray = (H3.DataModel.BizObject[])objArray;
244
+ if(bObjArray == null || bObjArray.Length == 0) { return defVal; }
245
+ decimal sVal = defVal;
246
+ foreach(H3.DataModel.BizObject bObj in bObjArray) {
247
+ sVal += gNum(bObj[field], defVal);
248
+ }
249
+ return sVal;
250
+ }
251
+ public static double gSum(object objArray, string field, double defVal) { return Convert.ToDouble(gSum(objArray, field, 0m)); }
252
+ public static int gSum(object objArray, string field, int defVal) { return Convert.ToInt32(gSum(objArray, field, 0m)); }
253
+ public static decimal gDecimalSum(object objArray, string field) { return gSum(objArray, field, 0m); }
254
+ public static double gDoubleSum(object objArray, string field) { return gSum(objArray, field, 0.0); }
255
+ public static int gIntSum(object objArray, string field) { return gSum(objArray, field, 0); }
256
+ ```
257
+
258
+ ### 6.3 时间与字符串
259
+
260
+ ```csharp
261
+ // 尝试转换时间(out 参数方式)
262
+ public static bool tTime(object val, out DateTime time) { return DateTime.TryParse(val + string.Empty, out time); }
263
+
264
+ // 转换时间(带默认值)
265
+ public static DateTime gTime(object val, DateTime defaultTime) {
266
+ string valStr = val + string.Empty;
267
+ if(string.IsNullOrWhiteSpace(valStr)) { return defaultTime; }
268
+ DateTime outTime = defaultTime;
269
+ return DateTime.TryParse(valStr, out outTime) ? outTime : defaultTime;
270
+ }
271
+
272
+ // 截断(单行文本限 200 字符、多行限 2000,赋值前必过)
273
+ public static string g200Str(string str) { return (!string.IsNullOrWhiteSpace(str) && str.Length > 200 ? str.Substring(0, 200) : str); }
274
+ public static string g2000Str(string str) { return (!string.IsNullOrWhiteSpace(str) && str.Length > 2000 ? str.Substring(0, 2000) : str); }
275
+
276
+ // 中国式四舍五入(AwayFromZero,非银行家舍入)
277
+ public static decimal gRound(decimal num, int digits) { return System.Math.Round(num, digits, System.MidpointRounding.AwayFromZero); }
278
+ ```
279
+
280
+ ### 6.4 批量提交(每 300 条自动 Commit)
281
+
282
+ ```csharp
283
+ // mode 取值:Create/Update/Remove;waitCommitBoList 里只能加同一种模式的待提交业务对象
284
+ public static void BatchCommitData(H3.IEngine engine, List<H3.DataModel.BizObject> waitCommitBoList, string mode)
285
+ {
286
+ if(waitCommitBoList == null || waitCommitBoList.Count == 0) { return; }
287
+ H3.DataModel.BulkCommit commit = new H3.DataModel.BulkCommit();
288
+ string errorMsg = null;
289
+ int count = 0;
290
+ for(int i = 0; i < waitCommitBoList.Count; i++)
291
+ {
292
+ if(mode == "Create") { waitCommitBoList[i].Create(commit); }
293
+ else if(mode == "Update") { waitCommitBoList[i].Update(commit); }
294
+ else if(mode == "Remove") { waitCommitBoList[i].Remove(commit); }
295
+ else { throw new Exception("mode值有误!"); }
296
+ count += 1;
297
+ if(count >= 300)
298
+ {
299
+ commit.Commit(engine.BizObjectManager, out errorMsg);
300
+ if(!string.IsNullOrEmpty(errorMsg)) { throw new Exception("批量" + mode + "数据失败:" + errorMsg); }
301
+ commit = new H3.DataModel.BulkCommit();
302
+ count = 0;
303
+ }
304
+ }
305
+ if(count > 0)
306
+ {
307
+ commit.Commit(engine.BizObjectManager, out errorMsg);
308
+ if(!string.IsNullOrEmpty(errorMsg)) { throw new Exception("批量" + mode + "数据失败:" + errorMsg); }
309
+ }
310
+ }
311
+ ```
312
+
313
+ ### 6.5 分页查全部
314
+
315
+ ```csharp
316
+ // 分页轮询查询某表单全部生效数据(每页 1000 + CreatedTime 排序 + 数量不足一页即停)
317
+ public static List<H3.DataModel.BizObject> GetAllBizObject(H3.IEngine engine, H3.DataModel.BizObjectSchema schema)
318
+ {
319
+ List<H3.DataModel.BizObject> boList = new List<H3.DataModel.BizObject>();
320
+ int pageIndex = 0;
321
+ int pageSize = 1000;
322
+ while(true)
323
+ {
324
+ H3.Data.Filter.Filter filter = new H3.Data.Filter.Filter();
325
+ H3.Data.Filter.And andMatcher = new H3.Data.Filter.And();
326
+ andMatcher.Add(new H3.Data.Filter.ItemMatcher("Status", H3.Data.ComparisonOperatorType.Equal,
327
+ H3.DataModel.BizObjectStatus.Effective));
328
+ filter.FromRowNum = pageIndex * pageSize;
329
+ filter.ToRowNum = (pageIndex + 1) * pageSize;
330
+ filter.AddSortBy("CreatedTime", H3.Data.Filter.SortDirection.Ascending);
331
+ filter.Matcher = andMatcher;
332
+ H3.DataModel.BizObject[] boArray = H3.DataModel.BizObject.GetList(engine,
333
+ H3.Organization.User.SystemUserId, schema, H3.DataModel.GetListScopeType.GlobalAll, filter);
334
+ if(boArray == null || boArray.Length == 0) { break; }
335
+ foreach(H3.DataModel.BizObject bo in boArray) { if(bo != null) { boList.Add(bo); } }
336
+ if(boArray.Length < pageSize) { break; }
337
+ pageIndex++;
338
+ }
339
+ return boList;
340
+ }
341
+ ```
342
+
343
+ ### 6.6 JSON 转换
344
+
345
+ ```csharp
346
+ // 将对象数组 JSON 转成 List<Dictionary<string,object>>;格式必须是 [{"k1":"v1"},...]
347
+ // this.Deserialize<T> 是非静态方法;自定义类可继承 H3.SmartForm.Timer 借用其非静态方法
348
+ public List<Dictionary<string, object>> JsonToObjectList(string jsonStr)
349
+ {
350
+ if(string.IsNullOrWhiteSpace(jsonStr)) { return null; }
351
+ object[] objArray = this.Deserialize<object[]>(jsonStr);
352
+ if(objArray == null || objArray.Length == 0) { return null; }
353
+ List<Dictionary<string, object>> dataList = new List<Dictionary<string, object>>();
354
+ foreach(object obj in objArray)
355
+ {
356
+ string dataStr = obj + string.Empty;
357
+ if(string.IsNullOrWhiteSpace(dataStr)) { continue; }
358
+ Dictionary<string, object> data = this.Deserialize<Dictionary<string, object>>(dataStr);
359
+ if(data == null || data.Count == 0) { continue; }
360
+ dataList.Add(data);
361
+ }
362
+ return dataList;
363
+ }
364
+ ```
365
+
366
+ ### 6.7 前端小工具
367
+
368
+ ```javascript
369
+ // 转数值(GetValue 返回值 → number),失败返回 0
370
+ function convertToNumber(v) {
371
+ if(typeof v === "string") {
372
+ if(v && v.length) { try { return parseFloat(v); } catch(e) { return 0; } }
373
+ } else if(typeof v === "number") { return v; }
374
+ return 0;
375
+ }
376
+
377
+ // 转字符串(GetValue 返回值 → string),统一 0/""/undefined/null 等特殊值
378
+ function convertToString(v) {
379
+ if(typeof v === "string") { return v; }
380
+ else if(typeof v === "number") { return v + ""; }
381
+ return "";
382
+ }
383
+ ```
384
+
385
+ ## 七、系统表速查
386
+
387
+ ### 7.1 业务主表 / 子表公共列
388
+
389
+ - 主表(`i_表单编码`):ObjectId、Name、CreatedBy、OwnerId、OwnerDeptId、CreatedTime、ModifiedBy、ModifiedTime、WorkflowInstanceId、Status(0草稿/1生效/2进行中/3作废)+ 自定义控件编码列。
390
+ - 子表(`i_子表控件编码`):ObjectId、Name、ParentObjectId(主表 ObjectId)、ParentPropertyName(子表编码)、ParentIndex(行索引,从 0 起)+ 自定义列。
391
+
392
+ ### 7.2 系统表
393
+
394
+ | 表 | 要点 |
395
+ |---|---|
396
+ | `H_User` | ObjectId, Name, Mobile, ParentId(主部门), State(0在职1离职), EmployeeNumber, IdNumber, Email, DingTalkAccount(钉钉用户Id), WechatUserId(企微) |
397
+ | `H_Organizationunit` | ObjectId, Name, ParentId, ManagerId, State(0有效1无效) |
398
+ | `H_User_Viceparentids` | 用户兼职部门:ObjectId=用户Id, PropertyValue=部门Id |
399
+ | `H_BizObjectFile` | 附件:ObjectId, SchemaCode, ChildSchemaCode, PropertyName, BizObjectId, ContentLength, FileName, SortKey(第几个), FileFlag, SourceBizObjectId, SourcePropertyName |
400
+ | `H_WorkflowInstance` | 流程实例:ObjectId, WorkflowDisplayName, InstanceName, State 0初始化1启动2运行3结束中4完成5取消; Approval 1同意0不同意-1未处理; FinishTime |
401
+ | `H_WorkItem` | 流程任务/日志:TokenId 最大=当前节点, ActivityCode, State 0等待1进行2完成3取消6转交, ItemType 0发起2审批3抄送, Approval, ItemComment(意见,废弃) |
402
+ | `H_Token` | 流程步骤:Activity, Approval, Participants |
403
+ | `H_Comment` | 审批意见:Text, Approval, CreatedTime |
404
+ | `H_PublishedFormSetting` | 表单配置:BehindCode=后端代码, NewJsCode=新版前端, Javascript=旧版前端 |
405
+ | `H_PublishedListViewSetting` | 列表配置:BehindCode, Javascript |
406
+ | `H_FunctionAcl` | 角色-表单关系(可见按钮编码) |
407
+ | `H_OrgRole` / `H_Orgpost` / `H_OrgRoleGroup` | 角色 / 角色-用户 / 角色分组 |
408
+ | `i_表单编码_关联表单多选控件编码` | 关联多选中间表:ObjectId, ValueIndex, PropertyValue |
409
+
410
+ ## 八、流程与消息通知代码骨架
411
+
412
+ ### 8.1 流程操作
413
+
414
+ ```csharp
415
+ // 流程Id 三来源:this.Request.InstanceId / bo.WorkflowInstanceId / SELECT ObjectId FROM H_WorkflowInstance WHERE SchemaCode=... AND ObjectId=...
416
+ // 创建数据并发起流程(关键步骤):
417
+ bo.WorkflowInstanceId = System.Guid.NewGuid().ToString(); // 必须先给流程Id
418
+ bo.Create();
419
+ H3.Workflow.Template.WorkflowTemplate wfTemp = engine.WorkflowTemplateManager.GetDefaultWorkflow(schemaCode);
420
+ string workItemId = "", errorMsg = "";
421
+ engine.WorkflowInstanceManager.OriginateInstance(
422
+ bo.OwnerId, // 发起人必须传真实用户Id(不能 SystemUserId,否则随机选人!)
423
+ schemaCode, wfTemp.WorkflowVersion, bo.ObjectId, bo.WorkflowInstanceId,
424
+ H3.Workflow.WorkItem.AccessMethod.Web, true, "", true, out workItemId, out errorMsg);
425
+ if(!string.IsNullOrEmpty(errorMsg)) throw new Exception("流程创建失败:" + errorMsg);
426
+ // 草稿数据发起流程:设 draftBo.WorkflowInstanceId=新Guid → Update() → 再 OriginateInstance(流程进行中/生效/作废的数据需先 Update 为草稿状态才能重新发起)
427
+ // 结束流程:engine.WorkflowInstanceManager.GetWorkflowInstance(insId).FinalTokenId → FinishInstanceMessage(insId, (int)finalTokenId)
428
+ // 取消流程:CancelInstanceMessage(insId, false)(仅进行中);重新激活:ActivateInstanceMessage(insId)(配合激活节点 ActivateActivityMessage)
429
+ // 当前运行节点(避免 ActivityCode 错乱):this.Request.WorkflowInstance.RunningActivties(数组)
430
+ // 流程结束/重新激活事件:OnWorkflowInstanceStateChanged —— 没有 base 自动保存,改完必须自行 Update()
431
+ // 提醒:OnWorkflowInstanceStateChanged 会被导入触发且不稳定,别用它做导入后处理(推荐列表按钮+执行状态字段方案)
432
+ ```
433
+
434
+ ### 8.2 状态判断(表单后端)
435
+
436
+ ```csharp
437
+ bool isCreate = this.Request.IsCreateMode; // 新增模式
438
+ H3.SmartForm.SmartFormMode mode = this.Request.FormMode; // Create/Edit/View
439
+ bool isWorkflow = this.Request.FormDataType == H3.SmartForm.SmartFormDataType.Workflow; // 开了流程
440
+ string activityCode = this.Request.ActivityCode; // 当前节点编码(注意错乱问题,多节点用 Request.WorkflowInstance.RunningActivties)
441
+ bool isMobile = this.Request.IsMobile;
442
+ bool isAdmin = this.Request.UserContext.IsAdministrator;
443
+ H3.DataModel.BizObjectStatus st = this.Request.BizObject.Status; // Draft=0 Effective=1 Running=2 Canceled=3
444
+ ```
445
+
446
+ **提交 vs 同意**:按钮编码都是 `Submit`。判断"发起提交"需组合:
447
+ `(actionName=="Submit" && IsCreateMode) || (Submit && Status==Draft) || (Submit && ActivityCode=="Activity2")`
448
+ 其余:`Save`=暂存,`Reject`=不同意,`RetrieveInstance`=撤回,`Remove`=删除(删除时 `Request.BizObject` 只有系统字段,要先 `Load()`),`CancelInstance`=作废。
449
+
450
+ ### 8.3 消息通知 / 提醒
451
+
452
+ ```csharp
453
+ // 站内/钉钉文本:engine.Notifier.Send(new UserMessage[]{ new UserMessage(
454
+ // H3.Notification.UserMessageType.DingTalkPlainText, SystemUserId, 接收人Id, "", "标题", "内容", "") });
455
+ // 邮件:UserMessageType.Email(收件人传邮箱,非氚云用户也行)
456
+ // 提醒任务:new H3.Task.UserTask(){ ObjectId=Guid, Sender, UserId, AppCode, TaskType=Reminder, SchemaCode,
457
+ // TargetType=LinkTargetType.BizObject, TargetId=数据Id, TargetName, Name, Summary, AlertTime, StartTime, EndTime }
458
+ // → engine.TaskManager.AddTask(task)
459
+ // 催办:engine.WorkItemManager.Urge(userId, instanceId, content)
460
+ ```
461
+
462
+ ## 九、前端 API 速查(FRONTEND_GUIDELINES.md 之外)
463
+
464
+ ```javascript
465
+ $.extend($.JForm, {
466
+ OnLoad: function(){ var that = this; // 回调里 this 会错,统一存 that
467
+ if ($.SmartForm.ResponseContext.IsCreateMode) { } }, // FormMode: 0审批/1完结/2创建/4查阅
468
+ OnLoadActions: function(actions){},
469
+ OnValidate: function(actionControl){ if (actionControl.Action == "Submit") {} return true; },
470
+ BeforeSubmit: function(action, postValue){},
471
+ AfterSubmit: function(action, responseValue){}
472
+ });
473
+ ```
474
+
475
+ - 控件方法:`SetVisible` `SetReadonly` `ClearItems` `AddItem` `BindChange($.IGuid(), fn)`(失焦才触发)`UnbindChange(key)` `OnKeyDown(e => e.srcElement.value)` `SetFocus` `SetColor/BgColor/FontSize/FontWeight/Line`
476
+ - 子表:`AddRow($.IGuid(), {"子表code.控件code": 值})` `ClearRows` `UpdateRow(行Id, {...})` `GetRowsCount` `GetCellManager(行Id, "子表code.控件code")`;行序号 = 子表 GetValue 数组下标 + 1
477
+ - 弹窗:`$.IShowSuccess/Warn/Error(标题, 内容)`;`$.IConfirm` 回调式不阻塞(要阻塞用原生 `confirm`);`$.IShowForm(schemaCode, objectId, params, checkIsChange, showlist, {showInModal:true, title, height, width, onHiddenCallback(data){data.Data["控件code"]}})`;`$.IGetParams("param1")` 取弹窗传入参数;关闭 `this.ClosePage()`(新版)
478
+ - 交互:`$.SmartForm.PostForm`(表单)/ `$.ListView.Post`(列表)/ `$.ListView.RefreshView()` / `$.ListView.GetSelected()`;`AfterSubmit` 里 `responseValue.ClosePage=false` 不关页、`responseValue.Refresh=true` 刷新
479
+ - 定位:`$.ILocation(true, fn)` 仅钉钉移动端(返回 lat/lng/address)
480
+ - 列表前端唯一事件:`$.ListView.ActionPreDo = function(actionCode){ $.ListView.GetSelected(); return true; }`
481
+
482
+ ## 十、定时器经验
483
+
484
+ ```csharp
485
+ public class MyTest_Timer : H3.SmartForm.Timer // 类名 自定义名_Timer,定义在表单后端代码里即可
486
+ {
487
+ public MyTest_Timer() { }
488
+ protected override void OnWork(H3.IEngine engine) { }
489
+ }
490
+ ```
491
+
492
+ - 引擎约**每 4 小时**扫描触发一次;每个应用每次约 **20 分钟**执行预算,超时直接中止(经验值,非硬上限)。
493
+ - 无 `this.Request`:业务对象自行查询,`this.Request.Engine` 换成参数 `engine`,用户用 `H3.Organization.User.SystemUserId`;用时间范围(≥4h 窗口)判断执行时机。
494
+ - 处理大量数据按可恢复的游标/状态分段,避免每次从头全扫。
495
+ - 调试:把业务代码封装成 `public static` 方法,临时在表单 `OnLoad` 里 `IsCreateMode` 时调用;建议建"日志表"记录执行结果。
496
+
497
+ ## 十一、OpenAPI 实测补充(见 OPENAPI_GUIDELINES.md 之后)
498
+
499
+ ### 11.1 可用动作
500
+
501
+ | ActionName | 用途 |
502
+ |---|---|
503
+ | `LoadBizObject` | 按 ObjectId 查单条 |
504
+ | `LoadBizObjects` | 按 Filter 批量查 |
505
+ | `CreateBizObject` | 新增单条 |
506
+ | `CreateBizObjects` | 批量新增 |
507
+ | `UpdateBizObject` | 更新 |
508
+ | `RemoveBizObject` | 删除(官方文档已提供) |
509
+ | `UploadAttachment` / `DownloadBizObjectFile` | 附件上传/下载 |
510
+ | 自定义接口名 | 平台内开发者自建接口 |
511
+
512
+ > ⚠️ 实测坑:部分引擎只有 `LoadBizObjects` / `CreateBizObject` / `UpdateBizObject` 三个动作可用,传其他 ActionName(如 DeleteBizObject/GetAppList)报 `未指定类名称`——遇到先按官方动作名试,报错再退回三动作方案。
513
+
514
+ ### 11.2 标准 CRUD 请求体
515
+
516
+ ```json
517
+ {"ActionName":"LoadBizObjects","SchemaCode":"表单code",
518
+ "Filter":"<JSON字符串>","IsSubmit":true}
519
+ {"ActionName":"CreateBizObject","SchemaCode":"表单code",
520
+ "BizObject":"<JSON字符串>","IsSubmit":true}
521
+ {"ActionName":"UpdateBizObject","SchemaCode":"表单code",
522
+ "BizObjectId":"目标ObjectId","BizObject":"<JSON字符串>"}
523
+ ```
524
+
525
+ - `Filter`/`BizObject` 必须是 **JSON 字符串**,否则报"过滤器解析失败" KeyNotFoundException。
526
+ - 新增返回 `ReturnData.BizObjectId`;查询返回 `ReturnData.BizObjectArray`。
527
+ - 是否触发自动化/业务规则取决于 `IsSubmit` 与数据状态;批量长耗时由调用方分批(见 PLATFORM_CONSTRAINTS.md)。
528
+
529
+ ### 11.3 表单元数据(自建自定义接口 FieldMetaController)
530
+
531
+ > ⚠️ 键名是 **`Controller`**,写成 `ControllerName` 报"未指定类名称"
532
+
533
+ ```json
534
+ {"ActionName":"GetFormList","AppCode":"应用AppCode",
535
+ "Controller":"FieldMetaController","IsSubmit":true}
536
+ {"ActionName":"GetFieldMeta","AppCode":"应用AppCode","SchemaCode":"表单code",
537
+ "Controller":"FieldMetaController","IsSubmit":true}
538
+ ```
539
+ → 返回 `ReturnData.forms: [{code, displayName, appCode}]` 和 `ReturnData.fields: [{code, displayName, dataType, associationSchemaCode}]`。
540
+
541
+ ### 11.4 上传/下载附件(实测可用)
542
+
543
+ - **上传 URL 与其它接口不同**:`POST https://www.h3yun.com/OpenApi/UploadAttachment?SchemaCode={表单编码}&FilePropertyName={附件控件编码}&BizObjectId={数据ID}`
544
+ - 参数全部走 **URL 查询串**;Body 为 `multipart/form-data`,**只有一个字段 `name="media"`**;必须显式设 `ContentLength`(缓冲关闭);Header 同带 EngineCode/EngineSecret。
545
+ - **下载**:`POST https://www.h3yun.com/Api/DownloadBizObjectFile`,body 为 form-urlencoded `attachmentId={附件ID}`,响应即文件字节流,文件名在 Content-Disposition 头。
546
+
547
+ .NET Framework 4.6.2 上传实现(同步版,HttpWebRequest):
548
+
549
+ ```csharp
550
+ using System.Text;
551
+ using System.IO;
552
+ using System.Net;
553
+
554
+ /// <summary>上传附件到氚云指定记录的附件控件,返回是否成功</summary>
555
+ public static bool H3UploadAttachment(string engineCode, string engineSecret, string schemaCode, string bizObjectId, string controlCode, string fileName, string contentType, byte[] fileBytes)
556
+ {
557
+ string url = "https://www.h3yun.com/OpenApi/UploadAttachment?SchemaCode=" + schemaCode + "&FilePropertyName=" + controlCode + "&BizObjectId=" + bizObjectId;
558
+ string boundary = "----------" + DateTime.Now.Ticks.ToString("x");
559
+
560
+ // 手工拼 multipart:Content-Disposition 必须是第一个头,字段名为 media
561
+ StringBuilder sb = new StringBuilder();
562
+ sb.Append("--").Append(boundary).Append("\r\n");
563
+ sb.Append("Content-Disposition: form-data; name=\"media\";");
564
+ sb.Append(" filename=\"").Append(fileName).Append("\"");
565
+ sb.Append("\r\n");
566
+ sb.Append("Content-Type: ").Append(contentType).Append("\r\n\r\n");
567
+ byte[] headerBytes = Encoding.UTF8.GetBytes(sb.ToString());
568
+ byte[] boundaryBytes = Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");
569
+
570
+ HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
571
+ req.Method = "POST";
572
+ req.ContentType = "multipart/form-data; boundary=" + boundary;
573
+ req.Headers.Add("EngineCode", engineCode);
574
+ req.Headers.Add("EngineSecret", engineSecret);
575
+ req.AllowWriteStreamBuffering = false;
576
+ req.ContentLength = headerBytes.Length + fileBytes.Length + boundaryBytes.Length;
577
+
578
+ using(Stream stream = req.GetRequestStream())
579
+ {
580
+ stream.Write(headerBytes, 0, headerBytes.Length);
581
+ stream.Write(fileBytes, 0, fileBytes.Length);
582
+ stream.Write(boundaryBytes, 0, boundaryBytes.Length);
583
+ }
584
+
585
+ using(HttpWebResponse resp = (HttpWebResponse)req.GetResponse())
586
+ {
587
+ using(StreamReader sr = new StreamReader(resp.GetResponseStream()))
588
+ {
589
+ string respJson = sr.ReadToEnd();
590
+ // 解析 respJson 中 Successful 字段判断成功与否
591
+ return respJson.IndexOf("\"Successful\":true") >= 0;
592
+ }
593
+ }
594
+ }
595
+ ```
596
+
597
+ 调用示例:`bool ok = H3UploadAttachment(engineCode, engineSecret, "D149201stxnahazawukhqsutjdw4", "数据ObjectId", "F0000016", "转入转出证明_订单号.pdf", "application/pdf", pdfBytes);`
598
+
599
+ ## 十二、已知坑汇总与调试
600
+
601
+ 1. OpenAPI:Filter/BizObject 是 JSON 字符串;`Controller` 不是 `ControllerName`;查询返回的 `xxObject` 补充字段创建/更新时别传;限流 1-2 次/秒、超时 60s。
602
+ 2. OpenAPI 只有官方文档那 9 个动作,实测部分引擎仅 3 个 CRUD 动作,无删除 → 脏数据后台手删(官方已提供 RemoveBizObject,按引擎版本试)。
603
+ 3. 返回 UTF-8,Windows 控制台用 `PYTHONIOENCODING=utf-8` 或写文件。
604
+ 4. 二开代码禁止引第三方库/IO;Json 用 `this.Serialize` / `this.Deserialize<T>`(非静态方法里用,自定义类可继承 `H3.SmartForm.Timer` 借用)。
605
+ 5. `base.OnLoad/OnSubmit` 不要删除、不要放 if 里;`base.OnSubmit` 上方改值自动保存,下方改值必须 `bo.Update()`;OnSubmit 无论成功/校验失败/提前返回都必须调 `base.OnSubmit`。
606
+ 6. `GetList` 每页最多 1000 条,分页必须 `AddSortBy`;`BulkCommit` 一次经验 ≤300~500 条。
607
+ 7. 定时器约 4h 触发、约 20 分钟预算、无 this.Request;SQL 30s 超时。
608
+ 8. 删除时 `Request.BizObject` 只有系统字段,要 `Load()`;附件在 base.OnSubmit 之前取不到。
609
+ 9. 代码创建生效数据会触发业务规则但不触发表单代码;自动化创建/更新数据不会触发代码;`OnWorkflowInstanceStateChanged` 会被导入触发且不稳定,别用它做导入后处理(推荐列表按钮+执行状态字段方案)。
610
+ 10. 调试:F12 打开后端调试器(F10/F11/F9 断点);前端加 `debugger` 标记;声明变量不赋默认值会导致调试模式报错 `无法将类型为...ResponseContext...转换为...LoadListViewResponse`。
611
+ 11. 数字控件空值为 null,运算结果恒 null → 公式用 `IF(字段,字段,0)` 兼容。
612
+ 12. 氚云对接钉钉待办上限 200 个流程表单;发布到钉钉工作台图标上限 50。
613
+ 13. 回调里 `this` 会错位,统一先存 `var that = this`;人员单选取值返回数组取 `[0]`。
614
+ 14. **`BizObject.GetList` 只能查主表,不能查子表 schema**(实测:自定义接口里对子表 schema 调 `GetList` + `GetListScopeType.GlobalAll`)。后果很隐蔽:引擎在**事务提交阶段**回滚,而回滚本身又抛 `ObjectDisposedException: 无法访问已释放的对象。对象名:"MySqlTransaction"`(堆栈 `MySqlTExecutor.Rollback` → `H3.Engine.RollbackSession` → `EngineContainer.InvokeMethod` → `TransactionManagerClient.CommitTransaction`),**把真正的异常完全盖住**,返回 `Successful:false` + `OpenApiError.Code=100500`,看不到根因。
615
+ 正确做法:子表数据从父记录的子表数组取——`H3.DataModel.BizObject[] rows = parentBo["子表编码"] as H3.DataModel.BizObject[];`(父记录用主表 `GetList`/`Load` 查出来时实测会带上子表数据)。用 `as` 转换,父记录没带出子表时返回 null 而不是抛异常。
616
+ 15. **关联表单控件(含 schema 标了 `isMultiple:true` 的)要赋单值 ObjectId 字符串,不是 `string[]`**。赋数组会被存成字面量 `"System.String[]"`,关联丢失。判断依据:读一条人工建的老数据,看该字段 OpenAPI 返回的是字符串还是数组。踩坑样本:`bo["F0000001"] = new string[]{id}` ❌ → `bo["F0000001"] = id` ✅。
617
+ 16. **后端 `bo.Create()` 不执行表单的联动(DataLink)和公式(computationRule)**,人工建单两者都会跑——所以接口/代码建出来的单字段是残缺的。要补全就得读表单 schema 里查询控件的 `mappingControls`(`{"本表字段":"源表字段"}`)和 `extra.DataLinkCondition`/`DataLinkResult`/`DataLinkSchemaCode`,手工把快照字段一个个带出来。另注:后端建单 `Name`(数据标题)也不会按 `nameSchema` 生成,会留空导致列表页不显示,需手工赋 `bo.Name`。
618
+ 17. **不要 `catch` 后吞掉引擎层异常**(`GetList`/`Load`/`Create`/`QueryTable`)。吞掉后事务状态不可控,最终仍会在提交阶段炸,且报错是回滚失败而非原始错误。业务校验异常可以抛给外层统一 catch,引擎异常一律往外抛。
619
+ 18. **排查接口报错的高效手法**:在接口里加临时参数(如 `__Stop`),走到指定步骤就 `throw new Exception("【联调中止】...")`——由本类 catch 捕获后原样返回 `result:error`+message。保存一次后连打几次不同 `__Stop` 值即可二分定位到具体哪一行炸,比反复改代码重部署快得多。定位完记得删掉再对上游开放(否则带该参数的请求会"不建单但返回 success")。
620
+
621
+ ## 十三、项目实例参考
622
+
623
+ - 天财商龙→氚云同步:`D:\AI\天财商龙\TcslSync\TcslSync\TcslSync.H3Yun\H3YunClient.cs`
624
+ - InvokeAsync(EngineCode/Secret 头)→ BuildFilterString(Filter JSON 字符串)→ QueryAllAsync(ToRowNum=500 分页循环)→ CreateDataAsync(剔除 ObjectId 后写 BizObject,读 BizObjectId)→ FindExistingIdAsync(Filter 等值查 ObjectId 去重)
625
+ - 天财单据按天滚动拉取防漏(接口日期是单日过滤)
626
+ - OpenAPI 外部对接实测(云星辰 KingdeeCloudStar 项目 `Services/H3CloudClient.cs`):认证/动作用法见本文件第十一节。