@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,229 @@
|
|
|
1
|
+
# Dos.ORM API 参考
|
|
2
|
+
|
|
3
|
+
## 数据库 Provider
|
|
4
|
+
|
|
5
|
+
| DatabaseType | Provider |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `SqlServer` | SQL Server 2000 |
|
|
8
|
+
| `SqlServer9` | SQL Server 2005+ |
|
|
9
|
+
| `MySql` | MySQL |
|
|
10
|
+
| `Oracle` | Oracle |
|
|
11
|
+
| `PostgreSql` | PostgreSQL |
|
|
12
|
+
| `DaMeng` | 达梦 |
|
|
13
|
+
| `KingBase` | 人大金仓 |
|
|
14
|
+
| `Sqlite3` | SQLite |
|
|
15
|
+
| `MsAccess` | Access |
|
|
16
|
+
|
|
17
|
+
兼容协议数据库仍需目标库实测,不能只凭协议名称判定所有 DDL、分页和 BulkCopy 一致。
|
|
18
|
+
|
|
19
|
+
默认会话入口是 `DbSession.Default`;多数据库、读写分离和分片场景使用明确的
|
|
20
|
+
`DbSession` 实例,避免把默认连接误用于其它租户或数据库。
|
|
21
|
+
|
|
22
|
+
## 实体
|
|
23
|
+
|
|
24
|
+
```csharp
|
|
25
|
+
[TableName("sys_user")]
|
|
26
|
+
public class SysUser : Entity
|
|
27
|
+
{
|
|
28
|
+
public string Id { get; set; }
|
|
29
|
+
public string Account { get; set; }
|
|
30
|
+
public DateTime CreateTime { get; set; }
|
|
31
|
+
|
|
32
|
+
public override Field[] GetFields() => new[] { _.Id, _.Account, _.CreateTime };
|
|
33
|
+
public override object[] GetValues() => new object[] { Id, Account, CreateTime };
|
|
34
|
+
public override Field GetIdentityField() => _.Id;
|
|
35
|
+
|
|
36
|
+
public sealed class _
|
|
37
|
+
{
|
|
38
|
+
public static readonly Field Id = new Field("Id", "sys_user");
|
|
39
|
+
public static readonly Field Account = new Field("Account", "sys_user");
|
|
40
|
+
public static readonly Field CreateTime = new Field("CreateTime", "sys_user");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`Entity.GetFields()` 的字段顺序也是批量写入和 CodeFirst 映射依据;实体声明、
|
|
46
|
+
字段数组与数据库列变更必须同步。
|
|
47
|
+
|
|
48
|
+
## 查询链
|
|
49
|
+
|
|
50
|
+
| API | 作用 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `From<T>()` / `From("table")` | 查询入口 |
|
|
53
|
+
| `Where(predicate)` / `Where(WhereClip)` | 条件 |
|
|
54
|
+
| `OrderBy` / `OrderByDescending` | 排序 |
|
|
55
|
+
| `GroupBy` / `Having` | 分组 |
|
|
56
|
+
| `Select` / `AddSelect` | 选择字段 |
|
|
57
|
+
| `Distinct` / `Top` / `Page` | 去重、Top、分页 |
|
|
58
|
+
| `InnerJoin/LeftJoin/RightJoin/CrossJoin/FullJoin` | Join |
|
|
59
|
+
| `Union/UnionAll` | 集合联合 |
|
|
60
|
+
| `SetCacheTimeOut` / `Refresh` | 查询缓存/绕过缓存 |
|
|
61
|
+
| `ToList/ToListAsync` | 列表 |
|
|
62
|
+
| `ToFirst/ToFirstAsync` | 第一条或 null |
|
|
63
|
+
| `ToFirstDefault` | 第一条或 new 实体 |
|
|
64
|
+
| `ToScalar/ToScalarAsync` | 标量 |
|
|
65
|
+
| `ToDataReader` / `ToDataTable/ToDataTableAsync` | Reader/DataTable |
|
|
66
|
+
| `ToEnumerable` | 流式枚举 |
|
|
67
|
+
| `Count/CountAsync` | 计数 |
|
|
68
|
+
| `ExecuteNonQuery/ExecuteNonQueryAsync` | 非查询 |
|
|
69
|
+
|
|
70
|
+
查询缓存入口的完整成员名是 `FromSection.SetCacheTimeOut`。缓存 Key 包含 SQL
|
|
71
|
+
与参数值,但写后失效、租户隔离和多节点一致性仍由业务负责。
|
|
72
|
+
|
|
73
|
+
```csharp
|
|
74
|
+
var users = await dbSession.From<SysUser>()
|
|
75
|
+
.Where(u => u.Account == account)
|
|
76
|
+
.OrderByDescending(u => u.CreateTime)
|
|
77
|
+
.Page(20, 1)
|
|
78
|
+
.ToListAsync();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## WhereClip
|
|
82
|
+
|
|
83
|
+
```csharp
|
|
84
|
+
var where = new Where<SysUser>();
|
|
85
|
+
where.And(u => u.Status == 1);
|
|
86
|
+
if (!string.IsNullOrWhiteSpace(keyword))
|
|
87
|
+
where.And(u => u.Name.Like(keyword));
|
|
88
|
+
if (deptIds?.Any() == true)
|
|
89
|
+
where.And(u => u.DeptId.In(deptIds));
|
|
90
|
+
|
|
91
|
+
var list = dbSession.From<SysUser>()
|
|
92
|
+
.Where(where.ToWhereClip())
|
|
93
|
+
.ToList();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
支持比较、`Like/NotLike/StartsWith/EndsWith`、`In/NotIn` 和 NULL。
|
|
97
|
+
|
|
98
|
+
## 原生 SQL
|
|
99
|
+
|
|
100
|
+
```csharp
|
|
101
|
+
var users = dbSession
|
|
102
|
+
.FromSql("SELECT Id,Account FROM sys_user WHERE Status=@p0")
|
|
103
|
+
.AddInParameter("@p0", 1)
|
|
104
|
+
.ToList<SysUser>();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
动态值只走参数;动态标识符先从固定白名单映射。
|
|
108
|
+
Provider 的标识符替换最终由 `DataUtils.FormatSQL` 完成;不要绕过该步骤手工
|
|
109
|
+
拼接用户输入的表名或字段名。
|
|
110
|
+
|
|
111
|
+
## 写入
|
|
112
|
+
|
|
113
|
+
```csharp
|
|
114
|
+
dbSession.Insert(entity);
|
|
115
|
+
dbSession.Insert(list);
|
|
116
|
+
dbSession.Update<SysUser>(u => u.Status, 0, u => u.Id == id);
|
|
117
|
+
dbSession.Delete<SysUser>(u => u.Id == id);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
批量:
|
|
121
|
+
|
|
122
|
+
```csharp
|
|
123
|
+
var affected = dbSession.BulkInsert(list, batchSize: 5000, bulkCopyTimeoutSeconds: 600);
|
|
124
|
+
var affectedAsync = await dbSession.BulkInsertAsync(list, batchSize: 5000, ct: token);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
原生优选:
|
|
128
|
+
|
|
129
|
+
- SQL Server:SqlBulkCopy
|
|
130
|
+
- MySQL:MySqlBulkCopy
|
|
131
|
+
- PostgreSQL/KingBase:Binary COPY
|
|
132
|
+
- Oracle/达梦/SQLite/Access:多行参数化 INSERT 回退
|
|
133
|
+
|
|
134
|
+
批大小 5000 只是起点;按行宽、索引、日志、网络和数据库负载测量。
|
|
135
|
+
|
|
136
|
+
## Upsert
|
|
137
|
+
|
|
138
|
+
```csharp
|
|
139
|
+
dbSession.Upsert(user, SysUser._.Account);
|
|
140
|
+
await dbSession.UpsertAsync(
|
|
141
|
+
user,
|
|
142
|
+
ct: token,
|
|
143
|
+
conflictFields: new[] { SysUser._.Account });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
各 Provider 使用 `ON DUPLICATE KEY`、`ON CONFLICT`、`MERGE` 或更新后插入回退。
|
|
147
|
+
冲突字段必须有数据库唯一约束。
|
|
148
|
+
|
|
149
|
+
## SqlFunc 与子查询
|
|
150
|
+
|
|
151
|
+
`SqlFunc` 提供 `SqlFunc.IfNull`、`SqlFunc.IIF`、`SqlFunc.Length`、
|
|
152
|
+
`SqlFunc.Substring`、`SqlFunc.Now`、`SqlFunc.DateDiff`、
|
|
153
|
+
`SqlFunc.JsonValue`、`SqlFunc.Concat`、`SqlFunc.Upper`、`SqlFunc.Lower`、
|
|
154
|
+
`SqlFunc.Trim`、`SqlFunc.Abs`、`SqlFunc.Round`、`SqlFunc.Count`、
|
|
155
|
+
`SqlFunc.Sum`、`SqlFunc.Avg`、`SqlFunc.Min`、`SqlFunc.Max` 的 Provider 方言。
|
|
156
|
+
|
|
157
|
+
`SqlSubQuery` 提供 `SqlSubQuery.Exists`、`SqlSubQuery.NotExists`、
|
|
158
|
+
`SqlSubQuery.In`、`SqlSubQuery.NotIn`、`SqlSubQuery.Scalar` 和
|
|
159
|
+
`SqlSubQuery.Count`。
|
|
160
|
+
返回的是 SQL 片段,数据值仍要参数化。
|
|
161
|
+
|
|
162
|
+
## 导航属性
|
|
163
|
+
|
|
164
|
+
`[Navigate]` 支持 `NavigateType.OneToOne`、`NavigateType.OneToMany`、
|
|
165
|
+
`NavigateType.ManyToMany`;加载入口:
|
|
166
|
+
|
|
167
|
+
- `IncludeOne`
|
|
168
|
+
- `IncludeMany`
|
|
169
|
+
- `IncludeManyToMany`
|
|
170
|
+
|
|
171
|
+
实现使用批量 IN,避免逐行 N+1。仍要限制主结果规模。
|
|
172
|
+
|
|
173
|
+
## CodeFirst
|
|
174
|
+
|
|
175
|
+
```csharp
|
|
176
|
+
dbSession.CreateTable<SysUser>();
|
|
177
|
+
dbSession.SyncSchema(typeof(SysUser), typeof(Order));
|
|
178
|
+
var exists = dbSession.TableExists("sys_user");
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`CreateTable<T>(dropIfExists: true)` 是破坏性操作,只可在明确授权的隔离环境使用。
|
|
182
|
+
实体索引可用 `[Index]` 声明;Microi 低代码业务表的运行时索引仍必须走 Manifest/MCP。
|
|
183
|
+
|
|
184
|
+
## 读写分离
|
|
185
|
+
|
|
186
|
+
```csharp
|
|
187
|
+
var router = new ReadWriteRouter(master);
|
|
188
|
+
router.AddSlave(slave1, weight: 1);
|
|
189
|
+
router.AddSlave(slave2, weight: 2);
|
|
190
|
+
|
|
191
|
+
var read = router.GetReadSession();
|
|
192
|
+
var write = router.GetWriteSession();
|
|
193
|
+
|
|
194
|
+
using (router.ForceMaster())
|
|
195
|
+
{
|
|
196
|
+
var latest = router.GetReadSession().From<SysUser>().ToList();
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
支持加权轮询、摘除和回退。事务与读自己刚写的数据必须读主。
|
|
201
|
+
|
|
202
|
+
## 分库分表
|
|
203
|
+
|
|
204
|
+
表名路由:
|
|
205
|
+
|
|
206
|
+
- `ShardingRouter.MonthlyTable`
|
|
207
|
+
- `ShardingRouter.HashModTable`
|
|
208
|
+
- `ShardingRouter.ModTable`
|
|
209
|
+
|
|
210
|
+
数据库路由:
|
|
211
|
+
|
|
212
|
+
- `DbShardingRouter.AddNode`
|
|
213
|
+
- `RouteByHash`
|
|
214
|
+
- `AllNodes`
|
|
215
|
+
|
|
216
|
+
稳定 Hash 使用 FNV-1a。跨分片事务、全局唯一键、分页排序和聚合需要业务层明确设计。
|
|
217
|
+
|
|
218
|
+
## 监控与缓存
|
|
219
|
+
|
|
220
|
+
```csharp
|
|
221
|
+
Dos.ORM.Section.SlowSqlThresholdMs = 1000;
|
|
222
|
+
Dos.ORM.Section.OnSlowSql = (cmd, elapsed, operation) =>
|
|
223
|
+
{
|
|
224
|
+
// 记录脱敏 SQL 模板、耗时和关联 Id;不要记录密钥参数
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
反序列化器、实体元数据和字段名缓存是进程内优化,允许节点重启后重建;
|
|
229
|
+
不能作为跨节点业务事实。查询缓存使用滑动过期,写后按业务失效。
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: job-engine
|
|
3
|
+
description: Microi 定时任务与可靠后台任务规范。用于配置 Microi.Job、Quartz 和接口引擎任务,设计多节点租约、幂等、重试、停机排空、恢复、进度与验收。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Microi Job 定时与后台任务
|
|
7
|
+
|
|
8
|
+
## 何时使用
|
|
9
|
+
|
|
10
|
+
- 周期扫描、补偿、归档:`Microi.Job`
|
|
11
|
+
- 用户触发的长耗时安装/导入/同步:菜单后台任务
|
|
12
|
+
- 可靠跨服务异步:MQ + outbox/inbox
|
|
13
|
+
- 请求内等待外部结果:`await`,不要创建后台线程
|
|
14
|
+
|
|
15
|
+
禁止用后端 `setTimeout`、`Task.Run`、`static bool` 或本机定时器承载可靠业务。
|
|
16
|
+
|
|
17
|
+
## 平台对象
|
|
18
|
+
|
|
19
|
+
Quartz 管理能力包含查询、添加、更新、暂停、恢复、删除任务。任务配置、接口引擎代码和执行日志属于控制面,只允许 `Level >= 9999` 维护;业务用户只能触发明确授权的后台动作。
|
|
20
|
+
|
|
21
|
+
任务通常调用稳定的 `ApiEngineKey`。接口引擎返回 `Code=1` 只表示本次执行成功,不代表调度系统可忽略重试与幂等。
|
|
22
|
+
|
|
23
|
+
## 多节点设计
|
|
24
|
+
|
|
25
|
+
每个任务必须同时具备:
|
|
26
|
+
|
|
27
|
+
1. 分布式租约:Key 至少含 `OsClient + JobKey + 计划时间/业务分片`,有唯一持有者、TTL、续租和仅持有者释放。
|
|
28
|
+
2. 业务幂等:稳定 `IdempotencyKey/EventId`、数据库唯一约束或条件状态迁移。
|
|
29
|
+
3. 可恢复状态:待处理、处理中、成功、失败、下次重试时间写共享数据库/Redis/MQ。
|
|
30
|
+
4. fencing:锁可能过期的资金、库存等任务使用版本号/条件更新拒绝旧持有者写入。
|
|
31
|
+
|
|
32
|
+
锁只能减少并发,不能替代幂等。
|
|
33
|
+
|
|
34
|
+
## 任务骨架
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// 接口引擎由 Job 调用;JobRunId/FireTime 由调度层传入
|
|
38
|
+
var idempotencyKey = String(V8.Param.JobRunId || '');
|
|
39
|
+
if (!idempotencyKey) return { Code: 0, Msg: '缺少 JobRunId' };
|
|
40
|
+
|
|
41
|
+
// 推荐调用专用后端能力,以唯一约束抢占执行记录
|
|
42
|
+
var claim = V8.FormEngine.AddFormData('job_execution', {
|
|
43
|
+
JobKey: 'daily_order_summary',
|
|
44
|
+
IdempotencyKey: idempotencyKey,
|
|
45
|
+
Status: 'Running'
|
|
46
|
+
});
|
|
47
|
+
if (claim.Code !== 1) return { Code: 1, Msg: '已执行或正在执行' };
|
|
48
|
+
|
|
49
|
+
// 分页处理;每个业务副作用仍需自己的幂等键
|
|
50
|
+
return { Code: 1, Data: { IdempotencyKey: idempotencyKey } };
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
实际项目优先由数据库唯一索引和接口引擎事务完成抢占,不能仅用“先查再新增”。该唯一索引必须声明在 Manifest `tables[].indexes`,并用 `microi_create_table_index` 创建、`microi_get_table_indexes` 回读;禁止在 Job/V8 内手写 `CREATE INDEX`。任务扫描还应按实际 SQL 建立 `(OsClient, Status, NextRetryTime)` 或 `(OsClient, JobKey, ScheduleTime)` 等组合索引。
|
|
54
|
+
|
|
55
|
+
## 失败、重试与停机
|
|
56
|
+
|
|
57
|
+
- 失败记录错误分类、重试次数和 `NextRetryTime`,采用有上限退避;永久错误进入人工处理。
|
|
58
|
+
- 外部调用设置超时;无法确认对方是否成功时用业务幂等号查询,不盲目重发。
|
|
59
|
+
- 服务停机先停止接单,再在有限宽限期排空或持久化;重启扫描未完成任务。
|
|
60
|
+
- 若要求 `kill -9` 前也零丢失,业务成功响应前必须获得共享 outbox/MQ/WAL 持久化确认。
|
|
61
|
+
|
|
62
|
+
## 后台按钮
|
|
63
|
+
|
|
64
|
+
满足任一条件即按后台任务设计:预计超过 2 分钟、500 条以上、1000 个以上扇出子操作、100 次以上外部调用、总量未知且可能持续运行,或安装/初始化/批量导入/批量生成/全量同步/迁移/备份。预计超过 10 分钟时,仅设置 `RunBackground=true` 仍不够,必须按 checkpoint 分片,每片独立事务。
|
|
65
|
+
|
|
66
|
+
菜单按钮设置 `RunBackground/BackgroundTask/IsBackgroundTask=true` 和 `ApiEngineKey`,并配置 `BackgroundTaskOptions`:
|
|
67
|
+
|
|
68
|
+
- `IdempotencyKey` 或 `IdempotencyKeyFields`:跨节点、重试和重复点击保持稳定。
|
|
69
|
+
- `ConcurrencyKey`:DDL、安装等不能并行的工作使用同一租约组。
|
|
70
|
+
- `BusinessTable + BusinessId`:关联业务记录。
|
|
71
|
+
- `BusinessStatusField + BusinessTaskIdField`:业务记录至少标记“后台处理中”和任务 Id;推荐再配置 `BusinessProgressField + BusinessEtaField`。
|
|
72
|
+
|
|
73
|
+
按钮提交成功后,平台前端会通过当前用户的 `V8.FormEngine` 权限把业务记录标记为“后台处理中”并写入任务 Id;后台服务不能直接相信客户端字段名而绕过表单权限。接口引擎仍必须在最后一片或异常补偿中把该业务记录改成“已完成 / 失败 / 已取消”,并保留任务 Id 供详情追溯:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
var task = V8.Param._BackgroundTask || {};
|
|
77
|
+
if (task.BusinessTable && task.BusinessId) {
|
|
78
|
+
var patch = { Id: task.BusinessId };
|
|
79
|
+
patch[task.BusinessStatusField] = '后台处理中';
|
|
80
|
+
patch[task.BusinessTaskIdField] = task.Id;
|
|
81
|
+
V8.FormEngine.UptFormData(task.BusinessTable, patch);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
不得让通用后台服务按客户端传入的任意表名/字段名直接写库;需要脱离前端自动标记的专用任务,应在受控接口引擎中使用固定表名和固定字段名。
|
|
86
|
+
|
|
87
|
+
接口通过 `V8.Method.UpdateBackgroundTask({Current,Total,Msg,Log})` 上报已提交的真实工作量,`Log`/`AppendLog` 用于追加任务详情(不得包含密码、Token 或密钥)。平台按实际吞吐计算 `EstimatedEndTime`;总量未知时不传 `Total`,通知中心显示“不定进度/估算中”,禁止用固定 10%、阶段占位或计时器伪造进度。失败和取消停在最后真实进度,不得显示 100%。
|
|
88
|
+
|
|
89
|
+
分片接口在仍有后续工作时返回:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
return {
|
|
93
|
+
Code: 1,
|
|
94
|
+
Data: {
|
|
95
|
+
BackgroundTask: {
|
|
96
|
+
HasMore: true,
|
|
97
|
+
Checkpoint: { LastId: lastId },
|
|
98
|
+
Current: committedCount,
|
|
99
|
+
Total: totalCount,
|
|
100
|
+
NextDelaySeconds: 1,
|
|
101
|
+
Msg: '本批已提交,等待下一批'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
最后一片返回普通 `Code:1`。每个业务副作用还要用 `_BackgroundTaskIdempotencyKey + 业务行Id` 建唯一约束;`_BackgroundTaskFencingToken` 用于拒绝租约过期旧执行者的写入。
|
|
108
|
+
|
|
109
|
+
## 长任务的 Jint 预算
|
|
110
|
+
|
|
111
|
+
后台 Worker 最终仍调用接口引擎,因此每个执行片段都受 `Timeout`、`MaxStatements`、单层累计分配预算、根调用树累计分配预算、JavaScript 递归和接口嵌套深度限制。`LimitMemory=2048` 表示当前片段累计分配了多少托管字节,不表示实时占用或预留 2GB 物理内存。
|
|
112
|
+
|
|
113
|
+
- 总任务运行 10 分钟、30 分钟或数小时是允许的;单个连续 Jint 调用不应承担全部时长。
|
|
114
|
+
- 每片在预算内提交事务并返回 `HasMore + Checkpoint`;Worker 重新入队后会创建新的 Jint Engine,新片重新获得超时、语句和累计分配预算。
|
|
115
|
+
- `V8.ApiEngine.Run` 的多层编排可以保留。新版父子单层分配隔离后,子接口不会被所有祖先重复计费,但根调用树仍有整体预算;循环调用由独立的嵌套深度上限终止。
|
|
116
|
+
- 捕获失败时读取 `DataAppend.V8Limit.Code`。内存/调用树/语句/超时分别缩小批次,递归错误修复函数递归,嵌套深度错误检查循环编排;不要统一归因于服务器资源不足。
|
|
117
|
+
- 可记录 `V8.Limits` 到脱敏诊断日志,但不要在每条业务数据上重复输出。
|
|
118
|
+
- 若分片提交会破坏“全部成功或全部回滚”的业务原子性,可为受控接口开启 `sys_apiengine.V8Unlimited`,并继续使用后台任务承载进度。该模式仍受进程常驻内存、取消、并发、接口嵌套深度和数据库限制;必须评估长事务锁/日志/回滚,保留幂等重试,并为每个下游接口和表后端事件分别配置,不能由根接口自动继承。
|
|
119
|
+
|
|
120
|
+
## MCP 工作流
|
|
121
|
+
|
|
122
|
+
1. 读取表、接口引擎和现有任务。
|
|
123
|
+
2. 先设计幂等键、状态机、租约和补偿。
|
|
124
|
+
3. `microi_save_job` 保存任务,写入需明确确认。
|
|
125
|
+
4. 回读任务 cron、启用状态、Key、接口引擎。
|
|
126
|
+
5. 两节点同时触发、重复投递、持有者中止、Redis 故障和滚动升级验收。
|
|
127
|
+
|
|
128
|
+
## 验收清单
|
|
129
|
+
|
|
130
|
+
- [ ] 任务配置仅管理员可改
|
|
131
|
+
- [ ] 两节点同一时刻触发,业务副作用仅一次
|
|
132
|
+
- [ ] 重复消息/请求不会重复扣减或生成流水
|
|
133
|
+
- [ ] 锁持有者退出后可恢复,无永久死锁
|
|
134
|
+
- [ ] 失败可重试、可追踪、可人工补偿
|
|
135
|
+
- [ ] 新旧版本滚动共存,状态和消息合约兼容
|
|
136
|
+
- [ ] 未知总量不显示假百分比;已知总量由 Current/Total 唯一推导
|
|
137
|
+
- [ ] ETA 来自真实吞吐,样本不足时明确显示“估算中”
|
|
138
|
+
- [ ] 业务记录可通过 BackgroundTaskId 跳转通知中心排查
|
|
139
|
+
- [ ] 超过 10 分钟的任务有 checkpoint,重启后从最后已提交批次恢复
|
|
140
|
+
- [ ] 单片低于超时/语句/累计分配预算,任务总时长不依赖放大单次接口上限
|
|
141
|
+
- [ ] 嵌套接口没有循环调用,且错误日志包含结构化 `V8Limit` 分类和调用路径
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: message-notification
|
|
3
|
+
description: 设计、实现、迁移和验收 Microi 多通道消息通知。用于涉及 wx_tpl_msg、mic_msgset、mic_msg_event_log、微信公众号或服务号模板消息、小程序跳转、短信、邮件、平台内部通知、V8.Notification、通知中心 SignalR、msg_event、消息幂等或通知应用商城交付的任务。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Microi 消息通知
|
|
7
|
+
|
|
8
|
+
## 目标
|
|
9
|
+
|
|
10
|
+
交付“配置可维护、事件先持久化、实时可降级、多节点不重复、租户不串线”的通知能力。支持微信公众号/服务号模板消息、短信、邮件和平台内部通知;小程序是公众号模板消息的跳转目标,不是独立的公众号发送主体。
|
|
11
|
+
|
|
12
|
+
## 开始前
|
|
13
|
+
|
|
14
|
+
1. 读取工作区 `AGENTS.md`,并按任务同时读取 `microi-db-schema`、`v8-api-config`、`v8-frontend-events`、`microi-client-frontend`;涉及商城时再读 `app-store`,涉及浏览器时再读 `playwright-e2e`。
|
|
15
|
+
2. 用用户点名的 MCP 连接读取实时结构,不以本地字典替代远端事实。至少读取 `wx_tpl_msg`、`mic_msgset`、`mic_msg_event_log`,按需读取 `wx_mp`、`wx_mini_program`、`sys_menu` 和接口引擎。
|
|
16
|
+
3. 多租户比较按字段语义合并:保留双方新增字段、控件、说明和数据源,再把并集同步到双方。每次写入后重新读取字段、物理索引和接口源码。
|
|
17
|
+
4. 只有用户明确要求时才复制渠道配置。复制微信公众号/小程序密钥时不在输出中打印秘密;模板、发送主体和小程序跳转引用必须一起回读验证。
|
|
18
|
+
|
|
19
|
+
## 核心模型
|
|
20
|
+
|
|
21
|
+
- `mic_msgset`:通知策略。`Key` 是稳定业务键,`Type` 是多选渠道,`ChannelApiEngineMap` 配置短信、邮件或自定义渠道适配器。
|
|
22
|
+
- `wx_tpl_msg`:微信公众号/服务号模板。`WxMpId` 决定发送主体;`MiniProgramId`、`MiniProgramAppId`、`MiniProgramPagePath` 仅表示点击模板消息后跳入的小程序。
|
|
23
|
+
- `mic_msg_event_log`:每位接收人、每个渠道的权威事件记录。至少包含稳定 `EventId`、`ChannelType`、`ReceiverUserId`、标题、内容、链接、Payload、已读状态和结果。
|
|
24
|
+
- 唯一约束:`EventId + ChannelType + ReceiverUserId`。租户使用独立业务库时表内无需虚构 `OsClient` 字段;共享库模型则必须把租户键加入唯一约束。
|
|
25
|
+
|
|
26
|
+
完整字段、接口和可靠性契约见 [references/contracts.md](references/contracts.md)。
|
|
27
|
+
|
|
28
|
+
## 实现流程
|
|
29
|
+
|
|
30
|
+
### 1. 合并结构
|
|
31
|
+
|
|
32
|
+
对两个租户分别读取字段列表,按 `Name` 生成差异表。新增缺失字段后刷新缓存,并回读:
|
|
33
|
+
|
|
34
|
+
- `mic_msgset.Type` 包含 `微信公众号模板消息`、`短信`、`邮件`、`平台内部`;
|
|
35
|
+
- `mic_msgset.ChannelApiEngineMap` 为 JSON 对象;
|
|
36
|
+
- `wx_tpl_msg` 同时有 `WxMpId/WxMpName` 和小程序跳转字段;
|
|
37
|
+
- `mic_msg_event_log` 有完整的事件、接收人、渠道、内容和已读字段;
|
|
38
|
+
- 业务唯一索引与常用未读查询索引存在。
|
|
39
|
+
|
|
40
|
+
不要用一次性 SQL 修某个租户而跳过通用表单/资源升级路径。应用包与平台升级资源必须携带同一结构。
|
|
41
|
+
|
|
42
|
+
### 2. 配置发送策略
|
|
43
|
+
|
|
44
|
+
`mic_msgset.Key` 对业务长期稳定。接收人可以来自固定用户、角色和调用参数,必须去重并限制扇出。渠道适配器统一接收:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
{
|
|
48
|
+
EventId: '业务稳定幂等键',
|
|
49
|
+
ChannelType: '短信',
|
|
50
|
+
User: { Id: '...', Phone: '...', Email: '...', WxOpenId: '...' },
|
|
51
|
+
Title: '审批提醒',
|
|
52
|
+
Content: '您有一条待审批记录',
|
|
53
|
+
LinkUrl: '/#/approval/123',
|
|
54
|
+
Payload: { BusinessId: '123' }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
适配器必须按 `EventId` 幂等。不要把密钥放进 `ChannelApiEngineMap` 或 Payload;密钥保存在对应渠道配置表或租户安全配置中。
|
|
59
|
+
|
|
60
|
+
### 3. 后端发送
|
|
61
|
+
|
|
62
|
+
业务代码优先调用 `msg_event`,由它读取策略、解析接收人、原子登记日志后分发。调用方在重试时保持同一个 `EventId`:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
return V8.ApiEngine.Run('msg_event', {
|
|
66
|
+
MsgKey: 'order_wait_approve',
|
|
67
|
+
EventId: 'order-wait-approve-' + V8.Param.OrderId,
|
|
68
|
+
ReceiverUserIds: [V8.Param.ApproverId],
|
|
69
|
+
Content: '订单 ' + V8.Param.OrderNo + ' 等待审批',
|
|
70
|
+
LinkUrl: '/#/orders/detail?id=' + V8.Param.OrderId,
|
|
71
|
+
Payload: { OrderId: V8.Param.OrderId }
|
|
72
|
+
}, V8.DbTrans);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`V8.Notification.Send` 是宿主的“平台内部实时提示”原语。它不替代日志 claim,通常只由 `msg_event` 在日志成功登记后调用。事务存在时,推送在提交后进行有界等待;回滚不得推送。
|
|
76
|
+
|
|
77
|
+
### 4. 前端通知中心
|
|
78
|
+
|
|
79
|
+
前端 V8 使用 `V8.Notification.List` 获取当前登录用户的权威快照,使用 `MarkRead` 标记本人通知。SignalR 固定事件 `ReceivePlatformNotification` 只用于低延迟刷新:客户端按 `Id/EventId` 去重,收到后仍以列表接口回读为准。
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
await V8.Notification.Send('order_wait_approve', {
|
|
83
|
+
EventId: 'order-wait-approve-' + V8.Form.Id,
|
|
84
|
+
ReceiverUserIds: [V8.Form.ApproverId],
|
|
85
|
+
Content: '订单等待审批'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
var result = await V8.Notification.List({ PageIndex: 1, PageSize: 20 });
|
|
89
|
+
await V8.Notification.MarkRead(result.Data[0].Id);
|
|
90
|
+
await V8.Notification.MarkRead({ All: true });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
列表和已读接口必须以 `V8.CurrentUser.Id` 作为服务端过滤条件,不能信任客户端传入的用户 Id。外链只允许站内路径、锚点或 `http/https`。
|
|
94
|
+
|
|
95
|
+
### 5. 多节点可靠性
|
|
96
|
+
|
|
97
|
+
- 数据库日志是事实源,SignalR 是可丢失提示;Redis backplane 使任一节点能通知连接在其它节点的用户。
|
|
98
|
+
- “先查询再新增”不能防并发;依赖唯一索引抢占。同一事件重复投递只能产生一份 `EventId + 渠道 + 接收人` 记录。
|
|
99
|
+
- 外部供应商在“已发送但响应丢失”时无法凭本地状态保证恰好一次。适配器必须把 `EventId` 传给支持幂等的供应商;不支持时进入可审计的人工确认/重试状态。
|
|
100
|
+
- 发布中新旧版本短暂共存,先扩展字段与接口,再发布读写代码,最后才收缩旧字段。
|
|
101
|
+
|
|
102
|
+
## 应用商城交付
|
|
103
|
+
|
|
104
|
+
“消息通知”应用包至少包含三张业务表、相关菜单、`msg_event`、`msg_internal_list`、`msg_internal_mark_read` 和必要索引。包内不得包含真实公众号 Token/AppSecret、用户接收人、OpenId、历史发送记录或租户专属 URL。先 `ValidateOnly`,再在获准的目标租户安装并回读;结构校验不能替代真实安装验收。
|
|
105
|
+
|
|
106
|
+
## 最低验收
|
|
107
|
+
|
|
108
|
+
1. 两个 MCP 租户的字段、数据源、物理索引和三段接口代码回读一致。
|
|
109
|
+
2. 重复 `EventId`、重复接收人和两个 API 节点并发发送,持久副作用仅一次。
|
|
110
|
+
3. 事务回滚不推送;提交后在线用户即时收到,离线/SignalR/Redis 故障后登录仍能回读。
|
|
111
|
+
4. 用户只能查询和标记自己的通知;危险链接、超长正文、跨租户接收人和匿名调用被拒绝。
|
|
112
|
+
5. 公众号/服务号发送主体与小程序跳转目标分别验证,不把 `MiniProgramAppId` 当作模板发送主体。
|
|
113
|
+
6. 源码定向测试、后端编译、远端 MCP 回读、真实浏览器点击和商城安装/校验分别报告;未执行的生产发布不得写成已上线。
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# 消息通知契约
|
|
2
|
+
|
|
3
|
+
## 表结构并集
|
|
4
|
+
|
|
5
|
+
### `mic_msgset`
|
|
6
|
+
|
|
7
|
+
| 字段 | 用途 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `Key` | 稳定业务键,租户内唯一 |
|
|
10
|
+
| `Title` | 配置名称/默认标题 |
|
|
11
|
+
| `IsEnable` | 总开关 |
|
|
12
|
+
| `Type` | 多选渠道:微信公众号模板消息、短信、邮件、平台内部 |
|
|
13
|
+
| `Receivers` | 固定接收用户 JSON |
|
|
14
|
+
| `ReceiversRoles` | 固定接收角色 JSON |
|
|
15
|
+
| `WxTplMsgId` | 公众号模板配置 Id |
|
|
16
|
+
| `ChannelApiEngineMap` | 渠道到适配器接口引擎 Key 的 JSON 映射 |
|
|
17
|
+
| `TenantId/TenantName` | 业务层租户信息;不能替代 MCP 的 OsClient 边界 |
|
|
18
|
+
| `TableChild99` | 兼容已有子表配置 |
|
|
19
|
+
|
|
20
|
+
### `wx_tpl_msg`
|
|
21
|
+
|
|
22
|
+
| 字段 | 用途 |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `Key/Title/TemplateId/Content/Remark/LinkUrl` | 模板业务键、标题、微信模板 Id、内容与普通跳转 |
|
|
25
|
+
| `WxMpId/WxMpName` | 发送模板消息的公众号或服务号;关联 `wx_mp` |
|
|
26
|
+
| `MiniProgramId/MiniProgramName` | 可选小程序配置;关联 `wx_mini_program` |
|
|
27
|
+
| `MiniProgramAppId/MiniProgramPagePath` | 模板消息的可选小程序跳转目标 |
|
|
28
|
+
|
|
29
|
+
公众号和服务号都属于微信公众帐号,由 `wx_mp` 保存发送凭据。小程序由 `wx_mini_program` 保存,不能使用小程序 AppId 调用公众号模板消息发送接口。
|
|
30
|
+
|
|
31
|
+
### `mic_msg_event_log`
|
|
32
|
+
|
|
33
|
+
| 字段 | 用途 |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `EventId` | 调用方稳定幂等键 |
|
|
36
|
+
| `MsgEventId` | 关联的消息设置 Id |
|
|
37
|
+
| `ChannelType` | 本条日志对应的渠道 |
|
|
38
|
+
| `ReceiverUserId` | 单一接收用户 Id |
|
|
39
|
+
| `Receivers` | 接收人安全快照 JSON |
|
|
40
|
+
| `Title/MsgContent/LinkUrl/Payload` | 通知展示快照 |
|
|
41
|
+
| `IsRead/ReadTime` | 平台内部通知已读状态 |
|
|
42
|
+
| `IsSuccess/MsgResult` | 分发状态和经过脱敏的结果 |
|
|
43
|
+
|
|
44
|
+
索引基线:
|
|
45
|
+
|
|
46
|
+
- 唯一:`EventId, ChannelType, ReceiverUserId`;共享表模型再前置 `OsClient`。
|
|
47
|
+
- 普通:`ReceiverUserId, ChannelType, IsRead, CreateTime`。
|
|
48
|
+
- `mic_msgset.Key`、`wx_tpl_msg.Key` 在租户业务库内唯一。
|
|
49
|
+
|
|
50
|
+
## V8 接口
|
|
51
|
+
|
|
52
|
+
### `msg_event`
|
|
53
|
+
|
|
54
|
+
输入:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
{
|
|
58
|
+
MsgKey: '策略 Key',
|
|
59
|
+
EventId: '稳定幂等键',
|
|
60
|
+
ReceiverUserId: '可选单用户',
|
|
61
|
+
ReceiverUserIds: ['可选用户数组'],
|
|
62
|
+
Title: '可覆盖配置标题',
|
|
63
|
+
Content: '正文',
|
|
64
|
+
LinkUrl: '/#/route',
|
|
65
|
+
Payload: { BusinessId: '...' }
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
处理顺序:校验当前租户与登录上下文 → 读取启用策略 → 合并固定用户、角色用户和参数用户 → 去重/限流 → 按接收人和渠道插入日志 claim → 仅对 claim 成功项分发 → 汇总成功、失败、重复。出现部分失败时仍要提交已经产生的日志与外部副作用,并把失败逐项返回,不能为了漂亮的 `Code` 回滚成功事实。
|
|
70
|
+
|
|
71
|
+
### `msg_internal_list`
|
|
72
|
+
|
|
73
|
+
- 仅返回 `V8.CurrentUser.Id` 的 `ChannelType=平台内部` 记录。
|
|
74
|
+
- 支持 `PageIndex/PageSize`,`PageSize` 最大 100。
|
|
75
|
+
- `DataAppend.UnreadCount` 返回同一用户权威未读数。
|
|
76
|
+
|
|
77
|
+
### `msg_internal_mark_read`
|
|
78
|
+
|
|
79
|
+
- `{ Id }` 只允许更新当前用户拥有的单条平台内部通知。
|
|
80
|
+
- `{ All: true }` 更新当前用户全部未读平台内部通知。
|
|
81
|
+
- 不接受客户端指定 `ReceiverUserId` 越权操作。
|
|
82
|
+
|
|
83
|
+
### `V8.Notification.Send`
|
|
84
|
+
|
|
85
|
+
`ReceiverUserId/ReceiverUserIds` 必填其一,最多 200 个;`Title` 最长 200 字符;`Content` 与序列化后的 `Payload` 各最多 32 KiB;`LinkUrl` 最长 500 字符且只允许站内路径、锚点、HTTP/HTTPS。事件名固定为 `ReceivePlatformNotification`。
|
|
86
|
+
|
|
87
|
+
## 验收矩阵
|
|
88
|
+
|
|
89
|
+
| 场景 | 断言 |
|
|
90
|
+
|---|---|
|
|
91
|
+
| 重复请求 | 同一接收人/渠道只存在一条日志,适配器不重复产生业务副作用 |
|
|
92
|
+
| 两节点同时发送 | 唯一索引只有一个 claim 成功,两节点都不崩溃 |
|
|
93
|
+
| 写入后节点退出 | 日志仍可查询;发送状态可审计、可补偿 |
|
|
94
|
+
| SignalR/Redis 短故障 | 业务写入不回滚,客户端列表回读恢复 |
|
|
95
|
+
| 事务回滚 | 不产生实时通知,业务日志随事务回滚 |
|
|
96
|
+
| 离线用户 | 下次打开通知中心能看到并标记已读 |
|
|
97
|
+
| 越权读取/已读 | 其它用户 Id 无效,记录不改变 |
|
|
98
|
+
| 微信配置 | `WxMpId` 决定发送主体,小程序字段只决定跳转 |
|
|
99
|
+
| 应用安装 | 无密钥、OpenId、历史日志或固定用户;安装后字段/索引/接口回读一致 |
|