🖥️ V8 函数列表 - 后端
服务器端 V8 引擎支持 ES6 语法,集成后端对象和方法
📌 介绍
- 服务器端 V8 引擎代码与前端 V8 的编程语言均为 JavaScript 语法
- 服务器端 V8 引擎支持 ES6 语法
- 集成了后端对象、方法,可使用 JS 调用后端方法(非 HTTP)
- 服务器端 V8 代码在服务器端执行
- 主要用于表单属性的服务器端 V8 事件、接口引擎、数据源引擎等
接口引擎 V8.ApiEngine
服务器端 V8 事件可以直接调用接口引擎(非 HTTP),接口引擎也可以调用其它接口引擎。传入 V8.DbTrans 时,共享外层事务;不传时由被调用接口引擎管理自己的事务。
StopHttp、允许匿名调用和接口角色限制约束的是外部 HTTP 入口。V8.ApiEngine.Run 属于可信服务端调用,不经过 HTTP 门禁。因此,被其它接口引擎复用的敏感业务仍必须在被调用引擎内部校验当前用户、业务状态和数据范围;能够编辑接口引擎、数据源、Job 或后端事件的账号属于“服务端代码执行”信任边界,只应授予高权限管理员。
外部客户端推荐直接向动态路由发送 JSON:
POST /apiengine/{ApiEngineKey}?OsClient={OsClient}
Content-Type: application/json
{"Action":"Bootstrap","Keyword":"客户"}只有不能立即升级的旧客户端才使用兼容入口,并把接口 Key 放在 JSON Body 中:
POST /api/ApiEngine/Run
Content-Type: application/json
{"ApiEngineKey":"your_key","Action":"Bootstrap"}两种入口都会把 JSON Body 恢复到 V8.Param;同名 Query/Form 参数保持既有优先级。接口层只负责 HTTP 路由、参数绑定和可信上下文恢复,不承载 AI、模型路由等业务逻辑。客户端提交的 _CurrentUser、_InvokeType:'Server' 或 _TrustedServerInvocation 不能建立服务端信任,身份和调用类型始终由认证中间件及接口层决定。
新增或可修改的前端、微服务、UniApp、MCP 与外部集成必须使用动态路径或引擎配置的唯一 ApiAddress,不得新增 /api/ApiEngine/Run 依赖。这样系统日志/监控、网关限流、访问审计和流量排行才能直接显示真实接口引擎;旧地址只保留在显式 RunLegacy 兼容方法中。
一个接口配置多个兼容路由
sys_apiengine.ApiAddress 是唯一主路由;“多路由”字段 ApiRoutes 可让同一接口继续接收多个历史 Controller/移动端地址,多个路径用英文分号分隔:
ApiAddress: /apiengine/platform-sys-menu
ApiRoutes: /api/SysMenu/GetSysMenuModel;/api/SysMenu/GetSysMenuStep接口缓存会同时按记录 Id、ApiEngineKey、主路由和全部多路由命中同一份代码。路由按完整路径、不区分大小写精确匹配;Query 不属于路由。主路由与多路由不得重复或与其它启用接口冲突,保存和启动缓存遇到冲突会失败关闭。新代码仍应使用稳定 Key 地址,多路由只用于旧客户端和已登记第三方回调兼容。
若旧租户仅更新后端程序,应用自动升级尚未完成,后端保留 LegacyMobileCompatibilityController 作为临时启动兼容入口。/api/SysUser/Login、续签、Token 登录、退出、公开系统设置、语言包、登录壁纸、域名租户解析、当前用户及菜单读取,在主库确认对应地址与固定接口 Key 均缺失时复用现有可信后端能力,仍执行密码、验证码、DiyToken 和菜单权限校验。管理员登录后可进入应用商城安装或更新平台应用;资源齐备后立即优先使用接口引擎。已禁用、禁止 HTTP、权限拒绝、数据库异常及接口执行失败不会触发兜底。
历史 /api/SysUser/* 地址固定绑定对应会话动作,不能用请求体 Action 改成其它操作,并支持 --OsClient--{OsClient}-- 租户后缀。路径与 Query 同时指定租户时必须一致。兼容 Controller 随后端程序交付,商城 SaaS 应用交付接口源码与多路由;两者需要分别更新。此 Controller 仅用于兼容,未来可能整体删除,新业务继续使用接口引擎。
旧地址 /api/Os/GetDateTimeNow 和 /api/SysLog/AddSysLog 也纳入同一兼容入口,并分别由 SaaS 引擎应用中的 platform-os-legacy-compatibility、platform-client-log 唯一交付。路径及 OS 动作大小写不敏感,支持 getDateTimeNow 和租户路径后缀。时间接口允许匿名 GET/POST,保持 { Code:1, Data:'yyyy/MM/dd HH:mm:ss' };日志接口支持已登录用户 GET/POST,租户和用户由 DiyToken 确定,不能通过 UserId/UserName/Category/Action 伪造平台审计。日志返回成功表示进入现有异步持久化流程,查到日志记录后才代表持久化验收通过。
旧部门树地址 /api/SysDept/GetSysDeptStep 由 SaaS 引擎应用的 platform-sys-dept 通过 ApiRoutes 交付,支持已登录用户 GET/POST、大小写和租户路径后缀。旧 JSON 请求 { "FormEngineKey":"Sys_Dept" } 不必补 Action,返回原有 { Code, Data } 及递归 _Child 结构,排序和组织范围仍使用既有部门逻辑。该历史地址固定为读树,即使传入 Action:"DelSysDept" 也不会删除;用户与租户来自 DiyToken,不信任请求的 _CurrentUser。新客户端使用 /apiengine/platform-sys-dept 并传 { "Action":"GetSysDeptStep" }。
若部门引擎尚未安装,更新后的兼容 Controller 仅为此历史读树地址复用既有 Core 原子;不会为新增、修改、删除部门提供缺引擎兜底。引擎已配置但停用、禁止 HTTP、拒绝权限或执行失败时照常失败。要同时覆盖正常引擎与未完成应用升级的租户,需分别更新后端镜像和“SaaS引擎”应用,不能只保存一份 V8 源码就宣称 Controller 已更新。
// 同步调用
var result = V8.ApiEngine.Run('ApiEngineKey', {
Param1: '1'
});
// 共享当前事务
var result2 = V8.ApiEngine.Run('ApiEngineKey', {
Param2: '1'
}, V8.DbTrans);接口引擎返回值与事务语义:
新代码推荐 return { Code:1, Data:... }。历史 V8.Result = { Code:1, Data:... } 继续兼容,包括脚本末尾或裸 return;;两种写法用于同步和异步引擎都保留结果。 若两种写法同时使用,以明确返回的非 undefined 值优先;失败的 Code=0 不能被 静默改成成功。重定向等响应配置与结果写法独立,迁移不需要把所有旧脚本批量重写。
- 返回
DosResult或带Code的对象:Code === 1提交,其它值回滚。 - 返回对象但没有
Code:回滚,避免“忘记返回状态”时误提交。 - 返回字符串、数字、数组、布尔值或
null,且脚本未抛异常:默认提交。 - 嵌套调用传入外层事务时,最终提交或回滚由外层调用者决定。
V8.DbTrans.Commit()、Rollback()、Close()会被安全代理忽略,不要在脚本中手动管理平台事务。
接口引擎流式响应(SSE / NDJSON)
将接口引擎“响应类型”设为 Stream 后,脚本可通过 V8.Stream 逐段输出。浏览器默认收到 text/event-stream;请求头 Accept: application/x-ndjson(或 Query streamFormat=ndjson)时返回 NDJSON。身份、匿名开关、角色、压力保护、Jint 预算、分布式锁和事务规则与普通接口完全一致。
for (var i = 0; i < 5; i++) {
var write = await V8.Stream.WriteAsync(
{ Index: i, Text: '第 ' + (i + 1) + ' 段' },
'chunk',
'row-' + i
);
if (write.Code !== 1) return write; // 客户端断开或超过配额时停止业务
}
return { Code: 1, Data: { Count: 5 } };V8.Stream.Write(data, eventName?, id?) 是同步写法,WriteAsync 会等待网络背压,更适合循环输出。 事件名只能使用字母开头的 1–64 位字母、数字、点、下划线或连字符;open/done/error/heartbeat 由宿主保留。所有脚本分片都带 Provisional:true,只是暂态进度,不代表数据库已经提交;只有脚本 返回且事务真正提交后,宿主才发送 done 与 Committed:true。失败或回滚发送 error,客户端必须 丢弃依赖未提交事务的暂态结果。
连接断开会触发请求取消;脚本应检查每次写入结果并尽快退出。默认单分片 256 KB、单请求累计 16 MB、心跳 15 秒,可在当前租户 sys_osclients 的 ApiEngineStreamMaxChunkKB / ApiEngineStreamMaxTotalMB / ApiEngineStreamHeartbeatSeconds 调整;宿主仍分别 限制为 4–1024 KB、1–256 MB、5–60 秒。流式接口不能用来绕过文件响应、HDFS、大文件上传、后台任务 或 MQ;需要可靠断点续跑的长任务仍使用后台任务并持久化 Checkpoint。
接口引擎受控 HTTP 响应
将“响应类型”设为 HTTP 后,接口引擎可返回标准协议需要的状态码、Content-Type、正文和安全响应头,不必创建 Controller:
通过 MCP 创建或保存时,分别在 microi_create_engine、microi_save_engine_code 中传 responseType: "HTTP"。工具枚举和目标后端必须同时支持该模式;保存后以真实 HTTP 请求检查状态码、响应头和正文,内部运行成功不能替代协议验收。
return {
Code: 1,
DataAppend: { HttpResponse: {
StatusCode: 302,
ContentType: 'text/plain; charset=utf-8',
Body: '',
Headers: {
Location: 'https://identity.example.com/login',
'Cache-Control': 'no-store'
}
} }
};普通接口引擎只能设置经过白名单和换行检查的响应头;Location 只允许站内地址、HTTPS 或本机开发地址。Host、Content-Length、Transfer-Encoding 等宿主/逐跳头始终禁止,Set-Cookie 只接受平台可信原子签名结果。状态码范围为 100–599,204/304 不得带正文。
ApiAddress 与 ApiRoutes 都支持完整路径段模板,例如 /sso/{OsClient}/.well-known/openid-configuration 和 /saml/{OsClient}/sp/{ConnectionKey}/metadata。模板值写入 V8.Param._RouteValues,并覆盖同名外部参数;包含 {OsClient} 时租户由路径解析,歧义匹配失败关闭。
普通第三方 HTTP 回调即使包含验签、AES 或服务端隐藏密钥,也应把公开地址放在 Managed 接口引擎的 ApiAddress / ApiRoutes,由接口引擎返回 DataAppend.HttpResponse 并调度 CreateIfMissing 租户 Hook。C# 只扩展接口引擎缺失的最小安全原子,例如畅捷通 V2:
var decoded = V8.Method.DecodeChanjetCallbackV2({
EncryptedMessage: V8.Param.encryptMsg
});
if (!decoded || Number(decoded.Code) !== 1) {
return { Code: 0, DataAppend: { HttpResponse: {
StatusCode: 400,
ContentType: 'application/json; charset=utf-8',
Body: '{"result":"fail"}',
Headers: { 'Cache-Control': 'no-store' }
} } };
}该原子只允许 platform-chanjet-callback-v2 调用,租户从当前 V8 执行上下文取得,AES Key 与 AppKey 白名单不会进入脚本;接口引擎不能传入或覆盖租户、密钥。其它回调应沿用同一模式,不要把公开路由和业务流程重新写入 Controller。
接口引擎通用实时事件(SignalR)
接口引擎负责业务命令、权限、事务和权威状态,SignalR 负责把事务成功后的服务端事件低延迟推送给已授权订阅者。该能力不是游戏专用:订单进度、协同编辑、设备状态、审批提醒和多人房间都使用同一个通用 Hub。共享数据库、Redis 或业务状态机仍是事实源,不能把业务完成与否只保存在 Hub、进程内字典或 SignalR 消息中。
写接口成功时返回固定大小写的 DataAppend.RealtimeEvent:
return {
Code: 1,
Data: snapshot,
DataAppend: {
RealtimeEvent: {
EventId: requestId, // 全局稳定;同一次重试保持不变
ChannelKey: 'order_updates', // 业务频道,小写字母/数字/下划线
SubjectId: order.Id, // 频道内资源 Id
Version: order.VersionNo, // 非负、单调递增
EventType: 'StatusChanged',
Data: { Status: order.Status } // 可选;只放该群组可见的安全投影,最大 32KB
}
}
};平台只在外部接口引擎请求执行完成且 Code === 1 后读取该对象;失败或回滚结果不会广播。宿主只读取固定大小写的 DataAppend.RealtimeEvent,重新生成 OccurredAt,并把事件收敛为 EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt。业务返回的其它 DataAppend、Data、私有手牌、用户信息和额外字段都不会进入通用 SignalR 事件。
跨节点发布使用共享 Redis:同一个 OsClient + EventId 先取得短时 Claim,再按 ChannelKey + SubjectId 原子维护单调 latest。低于当前 Version 的事件作为过期事件拒绝广播;同版本但事件指纹不同会判为版本冲突并拒绝;完全相同的事件重放不会推进 latest。只有 SignalR 真实广播成功后,平台才写入 24 小时 EventId 完成标记。若节点在广播前退出,Claim 到期后可重试,不会形成“已经去重但从未广播”的永久窗口;故障恢复可能产生重复通知,所以客户端仍必须去重。Redis 或 SignalR 故障不能反写已经提交的业务结果,客户端通过 HTTP Snapshot 收敛。
通用 Hub 固定契约:
宿主将事件和订阅结果中的 Latest.Data 转为仅含 JSON 标量、字典和数组的独立传输副本,保留上述字段及大小写,不把 JToken/JObject 直接交给 SignalR。Redis backplane 会预序列化已注册的 JSON/MessagePack 协议;即使浏览器只用 JSON,MessagePack 不支持 JToken 也会使整次广播失败。遇到“已连接但无推送”时,必须同时检查订阅授权、真实 RealtimeEvent 帧及 ApiEngineRealtime 日志,不能只凭握手成功判断实时功能通过。此传输修复须随兼容后端部署,保存业务 V8 不能代替后端升级。
| 项目 | 值 |
|---|---|
| 协议版本 | 2 |
| URL | /api-engine-realtime |
| 订阅方法 | SubscribeChannel |
| 取消订阅 | UnsubscribeChannel |
| 客户端事件 | RealtimeEvent |
| 订阅参数 | { ChannelKey, SubjectId } |
| 订阅结果 | ProtocolVersion/ChannelKey/SubjectId/Version/Latest/RenewAfterMilliseconds/LeaseExpiresAt |
| 租约 | 30 秒时隙;客户端按返回值续租 |
| 事件字段 | EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt |
客户端不能指定任意接口 Key、用户或租户。连接必须使用当前有效的普通登录 Token;Hub 会检查 JWT 有效期、平台活跃 Token 缓存和租户配置的 Token 生命周期。现有 AccessKey 权限模型没有 realtime:subscribe scope,因此平台会直接拒绝 AccessKey 实时连接;在平台正式增加并校验该 scope 前,不得通过放宽 Hub 校验绕过此边界。
Hub 从登录 Token 恢复 OsClient 与 CurrentUser,再按约定调用 realtime_{channel_key}_authorize 接口引擎。例如 order_updates 对应 realtime_order_updates_authorize:
// ApiEngineKey: realtime_order_updates_authorize
var order = V8.FormEngine.GetFormData('biz_order', {
Id: V8.Param.SubjectId,
_SelectFields: ['Id', 'OwnerUserId', 'VersionNo']
});
if (!order || order.Code !== 1 || order.Data.OwnerUserId !== V8.CurrentUser.Id) {
return { Code: 0, Msg: '您无权订阅该订单' };
}
return {
Code: 1,
Data: {
Authorized: true,
ChannelKey: V8.Param.ChannelKey,
SubjectId: order.Data.Id,
Version: order.Data.VersionNo
}
};SubscribeChannel 不是连接全生命周期的一次性授权,而是 30 秒时隙租约。每次调用都会重新校验 Token,通过共享 Redis 的 OsClient + UserId 限流,并重新执行授权接口引擎;当前限额是所有标签页和 API 节点合计 10 秒最多 96 次。服务端把通过授权的连接加入当前和下一时隙,只向当前时隙广播,并返回建议续租时间。客户端必须按本次响应的 RenewAfterMilliseconds 串行再次调用 SubscribeChannel,不要写死间隔;停止续租后,连接最迟在后续时隙自然停止收到事件。授权失败会移除该频道租约,Token 失效会清理全部租约并断开连接。Redis 限流不可用时订阅失败关闭,业务仍可使用 HTTP Snapshot。
客户端按 EventId 去重、按 Version 忽略旧事件并检测缺口;发现版本跳跃、重连、续租失败或服务降级时,立即调用业务接口获取按当前用户裁剪的 Snapshot,并保留有界 HTTP 轮询兜底。一个频道群组中的所有订阅者都会收到同一份 Data,因此用户私有手牌、Token、密钥和按用户不同的字段不得放入群组事件。
浏览器使用项目本地打包的 @microsoft/signalr,下面是续租与 Snapshot 降级的最小骨架:
const subscription = { ChannelKey: 'order_updates', SubjectId: orderId };
const connection = new signalR.HubConnectionBuilder()
.withUrl(apiBase + '/api-engine-realtime', {
accessTokenFactory: () => loginToken // 普通登录 Token,不是 AccessKey
})
.withAutomaticReconnect()
.build();
const seenEventIds = new Set();
let snapshotVersion = 0;
let renewTimer;
connection.on('RealtimeEvent', async event => {
if (seenEventIds.has(event.EventId) || event.Version <= snapshotVersion) return;
seenEventIds.add(event.EventId);
// 即使 Data 有公共增量,也要在缺口、重连和关键状态变化时回读权威 Snapshot。
const snapshot = await getOrderSnapshot(orderId);
snapshotVersion = snapshot.Version;
render(snapshot);
});
async function renewLease() {
clearTimeout(renewTimer);
try {
const lease = await connection.invoke('SubscribeChannel', subscription);
renewTimer = setTimeout(
() => void renewLease(),
Math.max(1000, lease.RenewAfterMilliseconds)
);
} catch (error) {
await refreshByHttpSnapshot();
renewTimer = setTimeout(() => void renewLease(), 3000);
}
}
await connection.start();
await renewLease();下面的 /game-realtime 是已有五款游戏的向后兼容协议;新业务和完成迁移后的游戏使用上面的通用协议。
多人游戏实时失效通知(兼容协议)
发牌、出牌、碰杠胡、结算、捕鱼命中等规则必须在接口引擎中执行,并用数据库事务、RequestId、ExpectedVersion、唯一索引和行锁维护权威状态。SignalR 不承载这些业务命令,也不发送手牌;它只在接口引擎成功提交后通知同房玩家“房间版本已变化”,客户端随后重新调用 gateway 的 Snapshot。
写操作成功时,gateway 返回固定大小写的 DataAppend.RealtimeInvalidation。对象只能包含以下六个公开字段:
return {
Code: 1,
Data: snapshot,
DataAppend: {
RealtimeInvalidation: {
EventId: requestId, // 全局稳定,重试时保持不变
AppKey: 'landlord-arena',
RoomId: room.Id,
Version: room.VersionNo,
Command: 'Play',
OccurredAt: DateNow('yyyy-MM-dd HH:mm:ss')
}
}
};平台只在 ApiEngine.RunAsync 返回、接口引擎事务已经提交后读取该对象;Code !== 1 或回滚结果不会广播。宿主会重新生成 OccurredAt,并丢弃对象中任何额外字段,因此误放入 PrivateHand/UserId/StateJson 也不会进入 SignalR。共享 Redis 按 OsClient + EventId 保存 24 小时去重,同一个 EventId 若对应不同 AppKey/RoomId/Version/Command 会被判为冲突并拒绝广播。多 API 节点通过现有 SignalR Redis backplane 发送;Redis 或 SignalR 短暂故障不能反写已经提交的业务结果。
Hub 固定契约:
| 项目 | 值 |
|---|---|
| URL | /game-realtime |
| 订阅方法 | SubscribeGameRoom |
| 取消订阅 | UnsubscribeGameRoom |
| 客户端事件 | GameRoomChanged |
| 事件字段 | EventId/AppKey/RoomId/Version/Command/OccurredAt |
订阅参数为 { AppKey, GatewayKey, RoomId },其中 GatewayKey 必须符合 app_*_gateway。Hub 不接受客户端传入的 UserId/_CurrentUser/OsClient:它从当前有效登录 Token 恢复用户和租户,再以服务端可信身份调用对应 gateway 的 Command='AuthorizeRealtime'。gateway 必须复用 Snapshot 相同的房间成员校验,并精确回显房间:
if (V8.Param.Command === 'AuthorizeRealtime') {
// 先按 V8.CurrentUser.Id 查询房间成员;不要信任 V8.Param.UserId
var member = getCurrentRoomMember(V8.CurrentUser.Id, V8.Param.RoomId);
if (!member) return { Code: 0, Msg: '您不是该房间成员' };
return {
Code: 1,
Data: {
Authorized: true,
AppKey: V8.Param.AppKey,
RoomId: V8.Param.RoomId,
Version: member.VersionNo
}
};
}浏览器使用项目本地打包的 @microsoft/signalr,不要依赖运行时 CDN:
const connection = new signalR.HubConnectionBuilder()
.withUrl(apiBase + '/game-realtime', {
accessTokenFactory: () => loginToken
})
.withAutomaticReconnect()
.build();
const seenEventIds = new Set();
let snapshotVersion = 0;
connection.on('GameRoomChanged', async event => {
if (seenEventIds.has(event.EventId) || event.Version <= snapshotVersion) return;
seenEventIds.add(event.EventId);
const snapshot = await runGateway({ Command: 'Snapshot', RoomId: event.RoomId });
snapshotVersion = snapshot.Data.Version;
render(snapshot.Data);
});
await connection.start();
await connection.invoke('SubscribeGameRoom', {
AppKey: 'landlord-arena',
GatewayKey: 'app_ddz_gateway',
RoomId: roomId
});SignalR 是低延迟提示,不是事实源:通知可能丢失、重复或乱序,客户端必须按 EventId 去重、按 Version 忽略旧通知,并保留约 1.2 秒的 Snapshot 轮询兜底。滚动升级期间旧节点可能暂时没有 Hub,客户端应继续轮询并自动重连,不能因此允许本地发牌或本地判定胜负。
接口嵌套与 Jint 资源预算
V8.ApiEngine.Run 嵌套调用是平台支持的正常编排方式。新版默认允许 32 层、节点硬上限默认 64 层;实际业务可以有 5、10 甚至更多层,但仍应避免循环调用,并让每层保持单一职责。这里的“接口嵌套深度”与 JavaScript 函数递归深度不是同一个概念。
Jint 的 LimitMemory 统计当前执行线程自约束重置后的累计托管分配字节数,不是当前仍存活的对象、进程工作集,也不会预留 2GB 物理内存。因此,一个接口触发“2GB累计分配上限”不等于服务器当时真实占用了 2GB;100 个并发接口也不能据此直接推算为 200GB 实时内存。大量临时对象、重复 JSON 序列化/反序列化、整表加载和数组复制都会快速累加,即使对象随后已被 GC 回收。
旧版中,父引擎执行子接口时,子接口初始化、查询、JSON 和业务对象分配还会被每一层父引擎重复计入,四层编排可能远早于预期触发父层 2GB。新版默认启用嵌套隔离:
- 每个接口引擎拥有自己的单层累计分配预算,默认 2048MB、节点硬上限默认 8192MB;
- 子接口分配不再重复计入每个父接口的单层预算;
- 根调用树仍有独立累计分配总预算,默认 8192MB、节点硬上限默认 32768MB,防止通过无限嵌套绕过整体保护;
- 嵌套调用不会重复占用全局和租户并发名额;同一调用树重入同一个 Key 也不会再次抢占自己的 Key 名额,循环调用最终由嵌套深度上限终止。
当前片段的有效预算可从 V8.Limits 查看:
console.log(JSON.stringify(V8.Limits));
// TimeoutSeconds, MaxStatements, LimitMemoryMB,
// CallTreeLimitMemoryMB, LimitRecursion, NestedApiDepthLimit,
// CurrentDepth, IsBackgroundTask, IsolateNestedApiMemory,
// ResidentMemoryGuardOnly, UnlimitedRuntime, MemoryAccounting资源异常会在 DataAppend.V8Limit 返回结构化分类,例如 V8_MEMORY_LIMIT、V8_CALL_TREE_MEMORY_LIMIT、V8_STATEMENTS_LIMIT、V8_RECURSION_LIMIT、V8_TIMEOUT、V8_NESTED_DEPTH_LIMIT 或 V8_EXECUTION_QUEUE_TIMEOUT,同时包含限制值、调用深度和调用路径。排查时应按分类处理,不要把所有异常都归为“服务器内存不足”。
后台任务仍通过接口引擎执行,所以一个未分片的 30 分钟脚本仍会受同一套单片超时、语句和累计分配预算约束。后台任务的总时长可以是数小时,但每片应控制在默认 600 秒以内,在提交本片事务后返回 HasMore + Checkpoint,由 Worker 创建新的执行片段继续;新片会获得新的超时、语句和累计分配预算。
开启分布式锁的可信后台任务使用最长 60 秒的 Redis 租约,并在执行期间按持有者令牌持续续租。接口的长执行预算与锁的单次存活时间分别计算;节点退出后,未续租的锁会在该短租约到期后释放,检查点无需等待原来一小时的接口预算。正常运行的长任务仍保留最多 12 小时(显式更长的执行预算优先)的续租边界。普通 HTTP 调用继续使用原有固定租约;前端伪造后台任务参数不能开启可信续租。
接口引擎的“V8运行限制”
接口引擎使用正向开关 sys_apiengine.V8Limit,表单显示名称为 V8运行限制:
- 默认关闭(
0):不为该接口设置 Jint 单次执行超时、最大语句数、JavaScript 函数递归和累计分配预算,同时取消 Promise 的固定等待时限; - 打开(
1):按Timeout / MaxStatements / LimitMemory / LimitRecursion配置执行限制,并在表单中显示这些配置项; - 老字段
sys_apiengine.V8Unlimited仅作滚动升级兼容,新的表单、MCP 和 Manifest 均使用V8Limit/v8Limit。
无论开关状态如何,进程/容器常驻内存保护、HTTP 断开与后台任务取消、节点停机取消、执行并发、接口嵌套深度、CLR 类型沙箱、租户权限、SQL/ORM/HTTP/文件限制都始终生效。接口调用的下游接口分别读取自己的 V8Limit,不会继承上游设置。
表后端事件同样使用正向开关 diy_table.V8Limit:字段缺失、null、0/false 都不设置 Jint 单次预算,只有 1/true 才启用限制。旧 diy_table.V8Unlimited 仅在新字段不存在时按反向语义兼容,新版表单、MCP、Manifest 与应用资源统一写 V8Limit/v8Limit。复杂逻辑位于 SubmitBeforeServerV8、SubmitAfterServerV8 或 ServerDataV8 时,仍建议满足以下条件:
- 由后台任务承载,避免依赖长时间浏览器连接;
- 已评估数据库长事务的锁等待、事务日志/Undo、回滚耗时和连接超时;
- 任务具备稳定幂等键,节点故障导致数据库自动回滚后可安全重试;
- 控制并发和查询字段,不把“48GB 内存”理解为单任务可以无界分配;常驻内存保护是全进程边界,接近阈值仍会拒绝新执行或有界停机。
V8.Notification
V8.Notification.Send 向当前 OsClient 的指定用户发送“平台内部”SignalR 提示。它是低延迟提示原语,不负责创建权威日志;业务通知优先调用 msg_event,由接口引擎先在 mic_msg_event_log 原子 claim,再调用本方法。
var result = V8.Notification.Send({
NotificationId: 'event-123-user-1',
EventId: 'event-123',
ReceiverUserIds: ['user-1'],
Title: '待办提醒',
Content: '您有一条新的待办',
LinkUrl: '/#/todo/123',
Payload: { TodoId: '123' }
});ReceiverUserId或ReceiverUserIds必传其一,去重后最多 200 个。Title最长 200 字符;Content、序列化后的Payload各最多 32 KiB。LinkUrl最长 500 字符,只允许站内路径、锚点或 HTTP/HTTPS。- 客户端事件固定为
ReceivePlatformNotification。 - 存在当前事务时,推送只在事务提交后进行最多 1.8 秒的有界等待;回滚不推送。
- 实时链路不可用时返回可降级结果,客户端应调用
msg_internal_list回读持久通知。
完整的策略表、公众号/服务号与小程序区别、幂等和多节点验收见消息通知。
表单引擎 V8.FormEngine
见平台文档:FormEngine 用法。
后端接口引擎和后端表单 V8 事件在活跃 V8 上下文中调用 FormEngine 时,由服务端写入不可被外部 JSON 构造的可信标记,因此不要求 _SysMenuId。租户边界、平台保护表和脚本自身的业务校验仍然生效。浏览器或其它外部 HTTP 请求不能通过伪造 _InvokeType: 'Server' 获得该信任;_InvokeType 只控制是否触发表单事件,不是身份或授权标记。
这里要区分“进入事件前”和“事件内部”:浏览器调用 AddFormData 仍要先通过目标菜单的 Add 权限,菜单 SqlWhere / SqlJoin 只约束已有记录的查询、修改和删除,不用于拒绝一条尚不存在的新增记录;进入 SubmitBeforeServerV8 / SubmitAfterServerV8 后,事件代码与接口引擎具有相同的服务器 FormEngine/数据库执行能力,可在当前租户内完成跨表事务、复杂 SQL 及归属字段写入。
原生新增管线独立确定实际主键,SubmitBeforeServerV8 的 V8.Form.Id 可能为空;在事件里自行补 GUID 不等于改变实际插入主键。条码、明细或审计等需要主表 Id 的写入,应在 SubmitAfterServerV8 核验 V8.Form.Id 并通过同一 V8.DbTrans 回读主记录后执行。After 仍在事务提交前,附属记录失败应返回 Code=0,主表与附属记录一起回滚。验收同时比较新增响应 Id、主表 Id、附属外键和审计 RowId,避免成功响应掩盖孤立引用。
前端/外部 HTTP 的菜单授权、历史无 _SysMenuId 推断、TableChild 委托和行级权限规则详见 FormEngine 安全授权。
平台内部的多层封装也必须保留来源:如果一个已校验管理员的设计器或升级任务在内部再次调用 FormEngine,应传递原管理员上下文,或由服务器构造带 _TrustedServerInvocation 的强类型参数;不要把数据转成裸 JObject 后依赖类型推断。可信标记是服务端实现细节,V8 代码和 HTTP 客户端都不需要、也不能自行设置。
这一规则也适用于 AddDiyField/AddField:它会先读取表定义,再在事务中调用通用 FormEngine 写入 diy_field,最后创建物理列。内部 diy_field 写入必须继承外层已验证的管理员或可信升级上下文;普通客户端不能借动态建字段入口绕过保护表授权。
缓存操作 V8.Cache
V8.Cache 是当前租户命名空间内的 Redis 能力。传逻辑 Key 时服务端自动生成 Microi:${V8.OsClient}:{逻辑Key};传完整的当前租户 Key 继续兼容,传入其它租户的 Microi: 前缀会被拒绝。它不暴露 Redis IDatabase、连接管理、服务器扫描或任意连接能力。
过期时间可传秒数,也可传 d.HH:mm:ss 字符串,例如 59 或 0.00:00:59;省略时为永久。常用方法包括 Set/Get/Delete/Del/Remove、KeyExist/Exists、SetIfNotExists、Expire,以及 HashSet/HashGet/HashGetAll/HashGetAllKeys/HashGetAllValues/HashDelete/HashRemove/HashExists/HashLength/HashIncrement。
// 推荐只传逻辑 Key,租户前缀由服务端添加
var cacheKey = 'FormData:baoming';
var cacheValue = JSON.stringify(formData);
var result1 = V8.Cache.Set(cacheKey, cacheValue, 59);
var result2 = V8.Cache.Get(cacheKey);
var result3 = V8.Cache.Remove(cacheKey);
V8.Cache.HashSet('Customer:Stats', 'Count', '1');
var count = V8.Cache.HashGet('Customer:Stats', 'Count');
V8.Cache.Expire('Customer:Stats', 3600); // 为整个 Hash Key 设置 TTL
// 只在 Key 不存在时写入,并强制使用正数秒 TTL。
var first = V8.Cache.SetIfNotExists('Idempotency:Order:123', 'processing', 60);SetIfNotExists 适合短期去重窗口,但仍没有唯一持有者令牌、续租和仅持有者释放语义。不要用它或“先 KeyExist、再 Set、最后 Remove”实现分布式锁。接口引擎应使用平台的分布式锁配置,Job/Worker 使用带租约和持有者令牌的锁;锁之外还必须使用稳定幂等键、唯一约束或状态机保证副作用只执行一次。
Expire 直接调整 Redis TTL,不会缩短当前源码中已经存在的 String L1 副本。可能进入 L1 的 String/对象应优先用带 TTL 的 Set 重写,或先删除再写入;Hash 不进入 L1,可以直接为整个 Hash Key 设置 TTL。L1/L2 数据流、Pub/Sub 失效、管理接口和源码配置详见分布式缓存(L1/L2)。
菜单、角色和表权限保存会递增 Redis 授权版本并使各节点的短期快照失效。不要把“重启容器”或“清空整个 Redis”当作权限刷新方案。
.NET 互操作与异步边界
后端 V8 对部分 .NET 类型开放互操作,但平台能力应优先使用 V8.*:例如用 V8.Method.NewUlid() 生成标识、用 V8.Base64 编解码、用 DateNow() 处理时间。不要依赖全局 System 名称访问任意 CLR 类型;平台还提供了 V8.System 主机监控扩展,两者可能发生名称冲突,且部分危险 CLR 类型会被禁用。
setTimeout 和 System.Threading.Tasks.Task.Run 不能作为“请求返回后可靠执行”的方案。V8Engine.Run 返回后会释放当前 Jint Engine、租户上下文、事务和并发租约,延迟回调可能面对已失效的上下文。请求内异步 API 使用 await;需要脱离请求执行时,使用接口引擎后台任务、Job、MQ 或 outbox,并设计幂等、重试和多节点故障恢复。
后台接口引擎通过 V8.Method.UpdateBackgroundTask 上报真实单位进度:
var taskId = V8.Param._BackgroundTaskId;
V8.Method.UpdateBackgroundTask({
_BackgroundTaskId: taskId,
Current: committedCount,
Total: totalCount,
Msg: '已提交第 ' + committedCount + ' 条',
Log: '批次 ' + batchNo + ' 已提交'
});Current 必须表示已经提交、重试不会重复的工作量;有自然单位时不要同时自行计算 Progress。Log/AppendLog 会追加到任务详情,不得写入密码、Token 或密钥。总量未知时省略 Total,平台显示不定进度;ETA 由服务端根据真实吞吐采样计算。失败和取消保留最后进度,只有最终 Code=1 才显示 100%。预计超过 10 分钟的接口应分页处理,并通过 Data.BackgroundTask={HasMore:true,Checkpoint,Current,Total,NextDelaySeconds} 让平台持久化检查点后重新入队。
var now = DateNow('yyyy-MM-dd HH:mm:ss');
var id = V8.Method.NewUlid();
// 请求内异步方法(仅在方法本身提供 Async 版本时)
var result = await V8.ApiEngine.RunAsync('ApiEngineKey', { Id: id });常用函数 V8.Method
全局日期与函数库
DateNow(format)、DateFormat(date, format)、DateAdd(date, unit, amount, format) 由后端引擎初始化提供,不依赖系统设置表是否已安装。DateAdd 支持 s/m/h/d/w/M/q/y;大写 M 是月,小写 m 是分钟;月末或闰年加减会截到目标月的有效日期。
“系统设置”应用的 mci_global_function 子表可按前端/后端维护具名函数。函数库与原有全局脚本合并后按租户缓存,不会把合并结果写回覆盖用户脚本;保存记录后在真实提交时失效。原有同名自定义函数优先,详见系统全局函数。V8.Method.ValidateGlobalFunction(name, code) 只做语法与单函数声明校验,返回标准 DosResult,不执行代码。
V8.Method 同时包含业务工具、管理员运维能力和平台内部能力。普通业务脚本优先使用下列稳定接口;数据库备份、清空数据库、认证缓存维护等管理方法不能作为普通业务 API 暴露。
展开查看 JavaScript 代码
// 当前 Token 与身份。不要把返回对象直接透传给前端。
var currentTokenObj = V8.Method.GetCurrentToken(token, osClient)
// { OsClient:'', CurrentUser:{}, Token:'不包含 Bearer ' } 或 null
// 当前用户资料或权限已提交后刷新登录投影;osClient 只是当前租户一致性断言
var refreshLogin = V8.Method.RefreshLoginUser(V8.CurrentUser.Id, V8.OsClient);
// 仅兼容历史 microi-init 的 body Token:必须传原始 Token,宿主会重新权威验证
var legacyRefresh = V8.Method.RefreshLoginUser(
tokenResult.CurrentUser.Id,
V8.OsClient,
V8.Param.Token
);
var id = V8.Method.NewUlid();
var timestamp = V8.Method.GetTimestamp();
// 当前运行中的 Microi.Core 文件版本,格式为 vX.Y.Z
var backendVersion = V8.Method.GetBackendVersion();
// 后端可信 V8 按租户内对象路径签发短期代理地址
var result = V8.Method.GetPrivateFileUrl({
FilePathName: '/microi/file/2023-08-06/xxx.doc'
});
// 结构化系统日志;不要记录密码、Token、密钥或完整请求体
V8.Method.AddSysLog({
Type: '接口日志',
Title: '同步完成',
Content: '记录数:20',
Level: 1
});
// microi_database 后端提交后事件专用:事务提交后刷新全部节点的 V8.Dbs
var refreshResult = V8.Method.RefreshExtensionDatabases();
// sys_role 后端提交前事件专用:读取服务端唯一的表直连授权策略
var directTablePolicies = V8.Method.GetDirectTableGrantPolicies();
// 接口引擎保存需要再次读取的密码或 Token:密钥永不进入 V8,
// 密文只允许同一 OsClient、同一 ApiEngineKey 解密。
var cipher = V8.Method.ProtectApiEngineSecret(secretText);
var plainText = V8.Method.UnprotectApiEngineSecret(cipher);
// 仅官方消息通知应用的 Managed 接口 wechat_send_tpl_msg 可调用。
// AppId/AppSecret 固定从当前租户 wx_mp 读取,不进入 V8 参数或返回值。
var sendResult = V8.Method.SendWeChatTemplateMessage({
WxMpId: V8.Param.WxMpId,
OpenId: V8.Param.OpenId,
TemplateId: V8.Param.TemplateId,
TemplateData: V8.Param.TemplateData,
Url: V8.Param.Url,
MiniProgramAppId: V8.Param.MiniProgramAppId,
PagePath: V8.Param.PagePath
});GetBackendVersion() 读取当前后端运行程序集的 FileVersion,只返回规范化的 vX.Y.Z,不暴露程序路径、主机名或其它环境信息。官方匿名 Managed 接口 GET /apiengine/platform-service-health?OsClient={OsClient} 使用它返回 Data.Status=Healthy 与 Data.BackendVersion;客户端应以该固定接口判断整个 API 服务是否可用,不能把任一菜单、表单或其它业务接口的失败直接升级为全局离线。 应用包先于后端二进制滚动升级时,接口仍返回 Healthy,但 BackendVersion 可暂时为空; 新二进制上线后会自动补齐真实版本。旧节点的 /api/Diagnostics/health 仅用于客户端 滚动升级兼容,不是新版客户端的主要健康契约。
RefreshExtensionDatabases(osClient?) 绑定当前 V8 租户。存在 V8.DbTrans 时只注册提交后回调:真实事务提交成功才递增共享 Redis 版本,回滚不刷新;没有事务时立即刷新。它适合“数据库扩展”应用的 microi_database.SubmitAfterServerV8,不应暴露成匿名或普通业务接口。
GetDirectTableGrantPolicies() 返回平台统一维护的表直连授权模式和允许操作。它只供角色管理等可信后端表单事件校验,不能替代当前用户、菜单、表和行级权限判断,也不能直接作为匿名业务接口返回。
RefreshLoginUser(userId, osClient?, token?) 是登录身份投影刷新原子,不是按调用方参数任意重载租户缓存的工具。有效租户只来自当前 V8 上下文、已认证 DiyToken,或宿主为可信表事件/升级任务显式建立的“用户 + 租户”作用域;osClient 仅作兼容一致性断言,不一致时失败关闭。
普通用户只能刷新本人,同租户平台超级管理员经主库复核后才可刷新其他用户;访问密钥、匿名/空身份和跨租户调用均拒绝。宿主查询 sys_user、角色权限与读写登录缓存时始终使用同一个规范化租户,避免把一个租户的用户投影写入另一个租户。主写入已经提交时,刷新失败应作为 Warning 返回,不能把已完成业务伪装成失败并诱导客户端重试。
第三参 token 只用于兼容历史 microi-init 把 Token 放在请求体、未发送 Authorization Header 的客户端。它必须是原始 Bearer 凭据;宿主会在 RefreshLoginUser 内重新调用 DiyToken 权威验证,并要求 Token 恢复出的租户和用户与当前 V8 租户、osClient 一致性断言及 userId 完全一致。伪造、失效、跨租户或“管理员 Token 刷新别人”均失败,且不会回退当前 ambient 身份。V8.Method.GetCurrentToken(...) 返回的对象不能作为第三参或认证证明。
ProtectApiEngineSecret(plainText) / UnprotectApiEngineSecret(cipherText) 只允许在后端接口引擎上下文调用。宿主把密文同时绑定当前租户与当前 ApiEngineKey,调用方不能传入 OsClient、密钥或 Purpose,也不会获得派生密钥。适用于远程连接密码、短期刷新 Token 等“业务明确需要再次读取”的接口私有凭据;列表必须继续脱敏,读取动作仍要执行当前用户、行归属和权限校验,禁止把解密结果写日志、审计或返回无权前端。接口引擎改 Key 后旧密文不可解,因此升级已有 Managed 引擎时应保持 Key 稳定。
SendWeChatTemplateMessage(options) 不是普通业务脚本可复用的微信 SDK。宿主只允许消息通知官方应用的 Managed 接口 wechat_send_tpl_msg 调用,并把公众号配置固定为当前租户 wx_mp 记录;参数只能选择公众号、接收人、模板、受限模板字段、HTTP(S) 跳转地址和受限小程序路径,不能传入或读取 AppSecret。租户的模板选择、接收人计算、通知记录与个性化逻辑继续由 V8 接口引擎及其 CreateIfMissing Hook 编排。
平台启动与私有文件可信原子
官网 PC、UniApp 与微服务的运行时启动能力已经迁入“SaaS引擎”官方应用。新客户端使用以下稳定接口引擎地址;历史 Controller 路由只作为旧客户端兼容入口,不应再被新代码引用:
| 接口引擎 | 鉴权 | 返回边界 |
|---|---|---|
platform-os-client-by-domain | 匿名 | 只返回匹配的 OsClient |
platform-sys-config | 匿名 | 只返回浏览器安全系统设置投影,绝不返回 ServerPrivateSettings 或密钥 |
platform-lang-bundle | 匿名 | 返回当前租户、指定语言和前缀的词条 |
platform-login-wallpapers | 匿名 | 最多返回 200 条启用壁纸的 Id/Name/Category/ImgUrl 投影 |
microi-init | 匿名启动;用户/菜单需请求体原始 DiyToken 重验 | 兼容旧 UniApp 的 OsClient/SysConfig/DateTimeNow/CurrentUser/Token/ModuleList 聚合;禁止匿名跨租户读配置,菜单按角色过滤 |
platform-current-user | DiyToken;访问密钥只允许自省 | 只返回宿主恢复的 V8.CurrentUser |
platform-private-file-url | DiyToken 或具备 file:read 的访问密钥 | 重算租户、菜单、表、行、字段和对象引用后签发短效代理地址 |
platform-sys-user-public-info | DiyToken | 分页返回 Id/Name/Avatar 最小公共投影 |
platform-private-file-url 不接受“已登录即可签任意路径”。FormField 必须提交菜单、表、记录和字段四元组并命中字段原值;FormFieldDerivedPreview 还必须提交字段保存的原文件,宿主只重算 DWG→_preview.dxf、STEP/STP→_preview.stl 的唯一同目录派生对象;FileManagerObject 要求单个大小写精确对象 Key、能力探针返回的当前租户文件柜菜单、对象实际存在,并且只接受平台超级管理员 DiyToken,访问密钥即使带 file:read 也不能打开文件柜对象。
其中涉及宿主秘密、不可伪造状态或受限匿名投影的能力分别使用 V8.Method.ResolveOsClientByDomain、GetPublicSysConfig、GetLangBundle、GetLoginWallpapers、GetLegacyInitMenuTree 和 GetAuthorizedPrivateFileUrl。这些不是通用 V8 API:宿主会同时校验当前 OsClient 与固定 ApiEngineKey,其它接口引擎、表单事件或直接调用一律失败关闭。GetLegacyInitMenuTree(rawToken, osClient?) 只允许 microi-init 调用,内部再次验证原始 DiyToken、拒绝访问密钥与跨租户请求,并用 SysMenuLogic.GetSysMenuStep 按权威角色权限构树;它不是通用菜单查询器。GetLoginWallpapers 在 Core 内固定查询启用且未删除的壁纸,只投影 Id/Name/Category/ImgUrl 并限制 200 条;它不会、也不要求把 diy_wallpaper.IsAnonymousRead 打开。公共用户目录继续由已鉴权的 Managed 接口使用 FormEngine 做固定投影与有界分页。
需要鉴权且允许个性化扩展的 Managed 接口会调用 platform-runtime-custom-hook。匿名启动接口(包括登录壁纸和 microi-init)不调用租户 Hook,避免匿名请求触发写表、通知或外呼副作用;microi-init 即使携带有效 Token,也只执行固定兼容聚合,不把 Token、用户投影或菜单交给租户 Hook。该 Hook 以 CreateIfMissing 首次创建,默认只有 return { Code : 1 };,之后归租户维护且官方升级不会覆盖。官方 Managed 接口顶部会明确提示其所属应用;从可信官方源安装、更新或重新安装应用会恢复官方代码,因此个性化逻辑必须写入 Hook,不能直接修改 Managed 接口。Hook 只接收阶段、来源 Key、当前用户 Id 或结果数量等脱敏元数据,不得传入 DiyToken、密码、Secret、原始 SSO 断言或私有文件短链。
系统账号与租户设置可信原子
用户资料、用户偏好、租户开通和非 Secret 系统设置同样采用“Managed 编排 + 最小可信原子 + CreateIfMissing Hook”:
| Managed 接口 | 官方应用 / Hook | 可信原子边界 |
|---|---|---|
platform-create-tenant | SaaS引擎 / platform-runtime-custom-hook | AuthorizeCurrentUserTenantProvisioning、ProvisionCurrentUserTenant 从可信当前用户派生所有者与密码材料 |
platform-user-update-preferences | 系统账号 / platform-user-custom-hook | 固定当前 V8.CurrentUser.Id 和界面偏好白名单 |
platform-user-update-profile | 系统账号 / platform-user-custom-hook | PrepareCurrentUserProfileUpdate 固定当前用户并校验租户头像目录 |
platform-sys-user-admin | 系统账号 / platform-user-custom-hook | ManageSysUserAdmin 固定租户与身份,复核表权限、角色层级、改密 step-up、内容安全和会话吊销;授权预检返回规范化 ChangesPassword,V8 不自行解析密码字段 |
platform-tenant-system-settings | 系统设置 / platform-system-settings-custom-hook | ValidateTenantSystemSettingsOperation 与 GetTenantSystemSettingsSecurityProjection 只允许超级管理员管理非 Secret 值 |
这些可信方法只允许表中指定的固定 ApiEngineKey 调用,并重复校验当前租户、DiyToken 用户和访问密钥会话,不能作为普通 V8 方法复用。租户 Hook 只收到安全最小投影;Before Hook 可以阻断,主写入完成后的 After Hook、登录投影刷新或审计失败只能返回 Warning,不能把已完成结果改成失败并诱导重试。
密码哈希/重置、DiyToken 签发、管理员查看历史密码、Secret/Sensitive Key 加密保存以及 GetRevealChallenge + Reveal 的一次性步进验证继续留在可信 C#。V8 不获得密码材料、SecretCipher、通用解密器或 Reveal 原子。
官方升级资源发布可信授权
get-microi-upgrade-resource 的固定白名单读取保持匿名兼容;只有 Publish/PublishBatch 写入分支调用 V8.Method.AuthorizeOfficialResourcePublish()。该方法不是通用管理员判断 API,只允许同名官方 Managed 接口调用,并同时固定 iTdos 官方租户、拒绝访问密钥会话、验证 DiyToken 身份,再从租户主库复核用户、状态和平台管理员角色仍有效。资源 JSON 校验、SHA256 乐观锁、固定顺序事务行锁、写入、精确选择元数据同步及发布后哈希回读继续由接口引擎编排。
不得用 V8.CurrentUser.Level >= 9999 代替该控制面授权:Level 是登录投影,可能在角色降级后短暂陈旧;也不得把 AuthorizeOfficialResourcePublish 暴露给其它接口引擎、表单事件或租户 Hook。
系统日志/监控可信原子
系统日志、请求归因和进程/主机观测继续由接口引擎编排;C# 只提供接口引擎无法安全完成的当前节点采样、日志读取和安全封禁原子。官方应用使用 mci-system-observability-query 与 mci-system-observability-action,两者都必须保持登录校验,不能开放匿名调用。
// 当前节点请求、接口、来源 IP、进程、主机、日志队列和安全访问快照
var snapshot = V8.Method.GetSystemObservability({
Action: 'Snapshot',
WindowMinutes: 5, // 1~15 分钟
Top: 20, // 5~50 项
IncludeHost: true,
IncludeDocker: false
});
// 只允许平台超级管理员执行;会写安全审计
var blocked = V8.Method.ManageSystemObservability({
Action: 'BlockIp',
Ip: '203.0.113.10',
BlockMinutes: 30,
Reason: '高频异常请求'
});
var unblocked = V8.Method.ManageSystemObservability({
Action: 'UnblockIp',
Ip: '203.0.113.10'
});GetSystemObservability 支持 Snapshot、Logs、LogTypes、LogStats、Signal、Trace、ApiRank、AppLogs,以及 Memory、MemoryIncidents、MemoryIncident。事故详情须传 32 位十六进制 IncidentId;历史最多返回 50 条摘要,按可信租户隔离。服务端会重新验证当前租户平台管理员身份,限制分页、时间窗口、排行与日志长度,并对应用日志中的密码、Token、Cookie、Secret、ApiKey 等内容脱敏。观测中间件不采集请求体、查询字符串、Authorization 或 Cookie。
内存诊断独立于 V8Limit,默认采集尚未完成的执行身份、父子链、代码哈希及有界分配样本,不对复杂脚本增加执行限制。累计分配和 CLR 分配栈不等于存活堆/RSS,也不保证 JavaScript 行号。必须部署含独立采集器的 API 并挂载持久 logs 卷;Mongo 不可用时先本机留证,恢复后幂等补传。完整数据边界、采集健康、重启恢复和使用步骤见系统日志/监控:内存事故定位。
Snapshot.Scope.CurrentNodeOnly=true 表示数据只属于当前 API 节点;多实例部署必须逐节点或在外部指标系统聚合。页面展示的请求耗时、吞吐、并发和错误率用于定位 CPU 相关热点,不等同于逐请求 CPU 采样,不能据此宣称某个请求独占了精确 CPU 百分比。ManageSystemObservability 仅接受 BlockIp/UnblockIp,拒绝本机、未指定和组播地址,封禁最长 7 天并写入用户行为审计。
身份应用可信原子
SSO 与平台短信登录遵循“接口引擎编排,C# 只补不可伪造的底层原子”。下列方法不是普通业务 API,只允许对应官方 Managed 接口引擎在精确 OsClient + ApiEngineKey 上调用;其它接口引擎、表单事件、匿名请求或直接 HTTP 调用都会失败关闭:
| 方法 | 唯一用途 | 允许的接口引擎 |
|---|---|---|
CreateFederatedUser | 校验非管理员角色并以专用带盐哈希创建 SSO JIT 用户 | sso_resolve_federated_identity |
CreateSsoLoginTicket | 为已解析用户创建短时一次性 SSO 登录票据 | sso_legacy_token_login |
CompleteSsoLogin | 原子消费 SSO 票据、重读启用用户并签发 DiyToken | sso_complete_login |
RotateSsoClientSecret | 生成只显示一次的客户端 Secret 并持久化验证哈希 | sso_rotate_client_secret |
RunSsoProtocol | 执行固定 OIDC/SAML/CAS 协议原子并返回进程内签名 HTTP 契约 | 对应的 24 个 sso_http_* 精确 Key |
CreatePlatformSmsProof | 限频并原子消费短信验证码,签发短时证明 | platform_auth_sms_login |
CreatePlatformSmsUser | 在短信证明约束下创建带盐哈希用户 | platform_auth_sms_login |
CompletePlatformSmsLogin | 原子消费短信证明并签发 DiyToken | platform_auth_sms_login |
接口引擎不得把这些方法包装成“通用用户创建/Token 生成”接口,也不得让客户端传入目标 OsClient、任意用户对象、Role Level、Secret 保存位置或回调地址。官方应用分别通过 app.microi.sso 与自动安装的 app.microi.saas-engine 交付编排代码和 ResourcePolicies;应用尚未安装时,普通账号密码登录仍由最小启动内核保证可用。
SSO 的完整应用合同、协议端点和 35 个接口引擎见 SSO 身份联邦。
V8.Method.Upload
V8.Method.Upload 使用当前租户文件配置,并执行平台上传上限与租户动态配额。默认上限为:单文件 500 MB、单请求 500 MB、最多 10 个文件、每用户每天 2 GB、每租户每天 20 GB;sys_osclients.DisableFileUpload 默认关闭(即允许上传),只有显式开启才禁止当前租户上传。FileUploadMaxFileMB、FileUploadMaxRequestMB、FileUploadMaxCount、FileUploadDailyUserQuotaMB、FileUploadDailyTenantQuotaMB 可按租户调整;旧 FileUploadEnabled 仅供未升级节点兼容。HTTP 上传会在 Base64 解码前预检体积,日配额用 Redis Lua 原子计数;配额服务不可用时失败关闭。
var uploadResult = V8.Method.Upload({
FilesByteBase64: V8.FilesByteBase64,
Limit: true,
Preview: false,
Path: '/file',
OsClient: V8.OsClient
});
// 接口引擎生成 JSON、源码或模板时直接上传 UTF-8 文本,避免 Base64 4/3 膨胀。
// 仍受当前租户 HDFS、公有/私有桶、扩展名、配额和 256 MB 文本上限约束。
var textUploadResult = V8.Method.UploadText({
Content: JSON.stringify(packageModel),
FileName: 'application-v1_2_3.json',
Path: '/microi-store/packages/application/202608',
Limit: false,
Preview: false,
OsClient: V8.OsClient
});UploadText 只接受 Content 和一个安全的 FileName,禁止同时传 FilesByteBase64/FilesByte/Files。它避免字符串先转 Base64 再还原字节产生的额外内存和错误文本编码,但不会替调用方完成内容哈希:商城等可信发布流程仍须对上传结果回读,并核对 UTF-8 字节数与 SHA-256 后才能提交数据库指针。
上传 HTTP 请求的接口引擎扩展
V8.Method.UploadCurrentRequestAsync() 执行当前上传 HTTP 请求的可信文件流操作; V8.Method.IsLegacyUploadCompatibilityEnabled() 读取当前租户“兼容平台旧版本”开关。 两者不接受身份、租户、桶或文件载荷参数,只能在宿主选定的上传接口引擎作用域中使用。 文件字节不进入 Jint,同一请求重复调用复用同一个上传任务,失败也不自动重试。
platform-hdfs-upload 负责返回编排,platform-hdfs-upload-hook 可扩展成功结果。 普通后端脚本上传继续使用 V8.Method.Upload;离开上传请求的脚本不能调用上述请求原子。 接口字段、开关和升级步骤见 旧移动端上传返回兼容。
私有文件访问与审计
普通 HTTP 上传只允许平台规定的目录并默认按私有文件处理;可信后端 V8 可进行租户内受控文件操作,但不能把 GetPrivateFileByte、对象列举或删除等管理能力直接暴露给普通用户。浏览器访问私有文件时还必须证明菜单、记录、字段与附件绑定关系,详见 文件上传与私有文件。
GetPrivateFileUrl 返回的是后端短期票据代理地址,而不是可泄露的对象存储真实签名地址。后端会分别记录链接签发和实际 GET/HEAD 打开/下载行为;登录用户记录为 Name(Account),转发链接被无身份访问时记录为匿名访问。代理支持 Range 流式响应并对分片请求短时去重,失败时不会退回未经审计的真实签名地址。Limit:false 的公有文件仍可直接走 CDN/公有桶,不记录此类行为日志。
系统日志队列与持久化
系统日志调用会先进入后端真正有界的内存队列,由单一后台消费者按批次写入 MongoDB;请求线程通常不等待 MongoDB。平台固定使用主队列 4096 条、内存重试区 512 条、每批 250 条,安装者无需维护队列容量环境变量。两级内存都满时会同步写持久化 spool 形成回压,禁止用无界内存队列继续堆积;健康信息会公开 Capacity、OverflowPending、EmergencySpooled 和 Dropped,其中 Dropped 必须保持为 0。
每批日志在写 MongoDB 前先写入固定目录 logs/syslog-spool,MongoDB 暂时不可用或服务正常重启时会自动幂等重放。容器部署时应直接把该目录挂载到持久卷,节点标识由平台自动生成,不需要额外环境变量。所有节点共享 MongoDB/Redis,日志按全局 EventId 幂等 upsert,详情停留状态和私有附件票据可跨节点继续读取。
用户行为日志
平台内置用户行为日志还会记录 Category、Action、Source、TargetType、TargetId、SessionId、DurationSeconds、Success、OccurredAt 等结构化字段。用户显示统一采用 Name(Account);密码、Token、Authorization、Secret、ApiKey、连接字符串等敏感内容会在进入队列时脱敏和限长。
API 进程内存保护
API 默认启用进程级内存保护。达到软阈值后节点会停止接收普通请求并返回 HTTP 503;连续达到硬阈值后先请求宿主有界停机,宽限期结束仍未释放时以退出码 137 强制结束,由 Docker/Kubernetes/服务管理器重启,避免单个节点耗尽整台宿主机内存。GET /api/Diagnostics/health 同时承担 readiness:内存保护期间返回 503;GET /api/Diagnostics/liveness 只表示进程仍存活。
阈值与度量口径
保护阈值统一依据进程实际驻留内存(Windows Working Set / Linux RSS),不能依据 Linux 下的 PrivateMemorySize64。后者可能包含 .NET GC 预留但尚未占用物理内存的巨大虚拟地址空间,数值甚至会超过宿主机物理内存数倍,只能作为诊断值。健康接口会同时返回 PressureMetric=ResidentSet、WorkingSetMB、PrivateAddressSpaceMB 与 ManagedHeapMB,其中只有驻留内存参与熔断判断。
默认先识别 Linux cgroup v2/v1 容器内存上限;容器未限额时使用宿主机物理内存,其他平台回退到 .NET GC 可用内存。软阈值固定为该有效内存额度的 95%,硬阈值固定为 98%,不再固定封顶为 4096 MB。例如 48 GiB 单节点默认约为 Soft=46694 MB、Hard=48168 MB,RSS 3.94 GB 不会触发保护。平台仍采用安全轮询、连续样本和有界退出策略。
内存保护不增加任何专用环境变量,也不要求在 appsettings.json 中维护一组节点参数。95%/98% 属于平台自动安全边界;后续确需面向用户开放调整时,应进入 SaaS 引擎或系统设置统一管理。
多节点部署与缓存预热
阈值按单个 API 节点的有效内存额度计算。单节点独占宿主机时直接使用默认 95%/98%;多个 API/Worker 或数据库共用同一宿主机时,必须由容器编排层给每个容器设置独立 memory limit,避免所有节点都按整机额度计算造成超卖。生产环境必须配置自动重启和 readiness 摘除;多节点滚动发布时,一个节点进入内存保护不能影响其它节点继续服务。
启动缓存和批量预热也必须自身有界。例如多语言运行时缓存先做有效行数预算检查,只读取租户实际启用的语言列,再按 Id 游标分页;默认每页 500、最多扫描 10000 行、最多保留 5000000 字符、单条 SQL 最长 30 秒。超过行数预算时不会先物化预算上限内的巨大对象图,而是立即拒绝本次重载并保留旧缓存。分页数、最大行数、最大字符数和 SQL 超时统一在主租户 SaaS 引擎的“平台运行配置”中维护,不增加环境变量;提高上限前必须测量单节点峰值内存。禁止使用 SELECT * 后一次性 ToList,再复制为第二份字典;数据库异常时保留旧缓存并失败关闭。
V8.Base64
- Base64转换,与System.Convert.ToBase64String(bytes)不同的是V8.Base64若遇异常会直接返回源字符串
var result = V8.Base64.StringToBase64('123456');
var result = V8.Base64.Base64ToString('MTIzNDU2');图像处理 V8.Image
V8.Image 提供跨平台的服务端图片生成、合并和编辑能力。所有方法都以对象形式传参,只处理内存中的 Base64、Data URI 或字节数组,不直接读取本地路径,也不会主动访问 URL。
图片来源与返回值
图片来源支持以下形式:
// 顶层 Base64
{ FileByteBase64: '<base64>' }
// 等价字段
{ Base64: '<base64>' }
{ DataUrl: 'data:image/png;base64,...' }
{ Bytes: response.RawBytes }
// 单图方法也支持 Image / Source 嵌套,值可以是对象或字符串
{ Image: { FileByteBase64: '<base64>' } }
{ Source: '<base64>' }处理成功时,除 GetInfo 外均返回标准 DosResult:
{
Code: 1,
Data: {
FileName: 'image.png',
ContentType: 'image/png',
FileByteBase64: '<base64>',
Width: 800,
Height: 600,
Size: 12345,
Format: 'png'
},
Msg: ''
}每次调用后必须先判断 Code。接口引擎开启“响应文件”后,可以直接返回这个结果,在浏览器中预览或下载图片。
公共输出参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
OutputFormat / Format | png | 支持 png、jpeg / jpg、webp、bmp;OutputFormat 优先 |
Quality | 90 | 编码质量,运行时限制到 1 至 100 |
BackgroundColor | 透明;JPEG 为白色 | 画布背景色 |
FileName | image.<扩展名> | 输出文件名,扩展名会按真实格式修正 |
兼容公共别名:ImageFormat / OutputType → OutputFormat,Background / BgColor → BackgroundColor,单图方法的 ImageBase64 → FileByteBase64。
方法列表
| 方法 | 说明 |
|---|---|
V8.Image.Create(param) | 生成纯色、渐变、文字或基础图形图片 |
V8.Image.Merge(param) | 横向、纵向、网格或覆盖合并图片 |
V8.Image.Overlay(param) | 覆盖合并快捷方法,未设置模式时自动使用 overlay |
V8.Image.Resize(param) | 调整宽高 |
V8.Image.Crop(param) | 裁剪矩形区域 |
V8.Image.Rotate(param) | 旋转图片 |
V8.Image.Flip(param) | 水平或垂直翻转 |
V8.Image.Convert(param) | 转换图片编码格式 |
V8.Image.RemoveSolidBackground(param) | 将纯色背景透明化,输出 PNG |
V8.Image.Draw(param) | 在已有图片上绘制文字和图形 |
V8.Image.Watermark(param) | 添加图片水印 |
V8.Image.CreateQRCode(param) | 生成二维码 |
V8.Image.GetInfo(param) | 读取宽高、格式、帧数等信息 |
RemoveSolidBackground 接收内存图片及 Tolerance、Feather,输入最多 20,000,000 像素。可用 ChromaKeyColor: '#00ff00' 指定抠像色,并以 EdgeConnectedOnly: true 只处理连接画布边缘的背景,保留主体内部同色区域。专门生成的纯绿幕素材还可开启 SuppressGreenSpill: true,清除手臂间封闭绿幕并压低边缘溢绿;主体本身有绿色时不应开启。省略这些参数保持四角估色与全图匹配的兼容行为。该方法是确定性颜色处理,不能替代语义分割。
Create 的专用参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
Width / Height | 800 / 600 | 新画布宽高 |
CanvasWidth / CanvasHeight | 未设置 | 设置后分别覆盖 Width / Height |
BackgroundColorEnd | 未设置 | 设置后与 BackgroundColor 形成线性渐变 |
GradientDirection | left-to-right | 支持横向、top-to-bottom / vertical、diagonal |
Text / TextColor / FontSize / FontFamily | 未设置 / #111827 / 32 / 默认字体 | 在画布中心追加快捷文字 |
Elements | 未设置 | 文字、矩形、椭圆、圆形和线段列表 |
生成图片并覆盖合并
下面示例先生成大图和小图,再把小图覆盖到大图的指定坐标。覆盖模式按 ZIndex 从小到大绘制,数值更大的图层位于上方;相同 ZIndex 时数组中靠后的图层位于上方。
var baseResult = V8.Image.Create({
Width: 1200,
Height: 700,
BackgroundColor: '#2563eb',
BackgroundColorEnd: '#0f172a',
GradientDirection: 'left-to-right',
Text: 'Microi',
TextColor: '#ffffff',
FontSize: 72,
FileName: 'poster.png'
});
if (baseResult.Code !== 1) return baseResult;
var badgeResult = V8.Image.Create({
Width: 240,
Height: 120,
BackgroundColor: '#f97316',
Text: 'NEW',
TextColor: '#ffffff',
FontSize: 42
});
if (badgeResult.Code !== 1) return badgeResult;
var result = V8.Image.Overlay({
CanvasWidth: 1200,
CanvasHeight: 700,
Images: [
{
FileByteBase64: baseResult.Data.FileByteBase64,
Width: 1200,
Height: 700,
Fit: 'fill',
ZIndex: 0
},
{
FileByteBase64: badgeResult.Data.FileByteBase64,
X: 900,
Y: 80,
Scale: 0.75,
Opacity: 0.95,
CornerRadius: 16,
ZIndex: 10
}
],
OutputFormat: 'png',
FileName: 'poster-with-badge.png'
});
return result;也可以使用双图简写:
return V8.Image.Overlay({
BaseImage: baseResult.Data.FileByteBase64,
OverlayImage: badgeResult.Data.FileByteBase64,
X: 900,
Y: 80,
OverlayWidth: 180,
OverlayHeight: 90,
Opacity: 0.9
});主图兼容 BaseImage、BackgroundImage、FirstImage、Base;覆盖图兼容 OverlayImage、ForegroundImage、SecondImage、Overlay。简写结构中的顶层 X、Y、Position、Opacity、OverlayWidth、OverlayHeight、Scale 会应用到覆盖图。
合并模式
// 左右拼接
var horizontal = V8.Image.Merge({
Mode: 'horizontal',
Direction: 'ltr',
Gap: 20,
Padding: 20,
Alignment: 'center',
Images: [
{ FileByteBase64: firstBase64, Height: 320 },
{ FileByteBase64: secondBase64, Height: 320 }
]
});
// 上下拼接
var vertical = V8.Image.Merge({
Mode: 'vertical',
Direction: 'ttb',
Gap: 16,
Alignment: 'left',
Images: [firstBase64, secondBase64]
});
// 网格拼接
var grid = V8.Image.Merge({
Mode: 'grid',
Columns: 3,
Gap: 12,
Padding: 12,
Images: imageBase64List
});| 参数 | 说明 |
|---|---|
Mode | horizontal、vertical、grid、overlay |
Layout | 优先于 Mode;支持 row、column、canvas、cover,以及 left/right/top/bottom/up/down 方向快捷值 |
Direction | ltr、rtl、ttb、btt,也支持 left-to-right 等完整写法 |
Images / Layers | 图片或图层数组;数组项可以直接是 Base64 / Data URI 字符串 |
CanvasWidth / CanvasHeight | 固定画布尺寸;未设置时按布局自动计算 |
Padding / Gap | 内边距 / 图片间距,负数按 0 处理 |
Alignment | 横向时控制上下对齐,纵向时控制左右对齐,网格时控制单元格内对齐 |
Columns | 网格列数 |
合并兼容别名:MergeType / Type → Mode,Items → Images。
图层参数
| 参数 | 默认值 | 说明 |
|---|---|---|
Width / Height | 原尺寸 | 只设置一个时按比例计算另一个 |
Scale | 1 | 在宽高计算后再次按比例缩放,范围大于 0 且不超过 100 |
Fit | contain | 同时设置宽高时支持 contain、cover、fill / stretch、none |
X / Y | 未设置 | 覆盖模式绝对坐标;设置其中一个后,另一个默认使用 Padding |
Position / Anchor | top-left | 未设置坐标时的锚点;Position 优先 |
OffsetX / OffsetY | 0 | 坐标或锚点定位后的偏移 |
Opacity | 1 | 透明度,限制到 0 至 1 |
Rotation | 0 | 顺时针旋转角度 |
ZIndex | 0 | 覆盖顺序,数值越大越靠上 |
FlipHorizontal / FlipVertical | false | 翻转当前图层 |
CropX / CropY / CropWidth / CropHeight | 原图范围 | 缩放前裁剪源图 |
CornerRadius | 0 | 圆角半径 |
BorderColor / BorderWidth | 未设置 / 0 | 图层边框 |
BlendMode | src-over | 混合模式 |
contain 保持完整内容并等比缩放;cover 居中裁剪并填满目标宽高;fill / stretch 强制拉伸;none 使用原尺寸。Scale 在上述计算后继续生效。
常用锚点:top-left、top、top-right、left、center、right、bottom-left、bottom、bottom-right。混合模式支持 src-over、multiply、screen、overlay、darken、lighten、plus / add、src、dst-over。
图层兼容别名:Order → ZIndex、Alpha → Opacity、Rotate → Rotation、Left / Top → X / Y。
其它图片操作
// 缩放:Width、Height 至少设置一个;Pad=true 时保留完整目标画布
var resized = V8.Image.Resize({
Image: sourceBase64,
Width: 800,
Height: 600,
Fit: 'cover',
Pad: false,
AllowUpscale: true,
Alignment: 'center'
});
// 裁剪;Clamp=true 时把部分越界区域收缩到图片范围
var cropped = V8.Image.Crop({
Image: sourceBase64,
X: 100,
Y: 80,
Width: 640,
Height: 360,
Clamp: false
});
// 旋转;Expand=false 时保持原画布,边缘可能被裁掉
var rotated = V8.Image.Rotate({
Image: sourceBase64,
Degrees: 30,
Expand: true
});
// 水平、垂直翻转;Horizontal 默认 true,Vertical 默认 false
var flipped = V8.Image.Flip({
Image: sourceBase64,
Horizontal: true,
Vertical: false
});
// 格式转换
var converted = V8.Image.Convert({
Image: sourceBase64,
OutputFormat: 'webp',
Quality: 85,
FileName: 'converted.webp'
});
// 图片水印
var watermarked = V8.Image.Watermark({
BaseImage: sourceBase64,
Watermark: logoBase64,
Width: 180,
Height: 90,
Scale: 1,
Position: 'bottom-right',
Margin: 24,
OffsetX: 0,
OffsetY: 0,
Opacity: 0.7,
Rotation: 0
});
// 二维码;Content 优先于 Text,Size 默认 300
var qr = V8.Image.CreateQRCode({
Content: 'https://microi.net/',
Size: 420,
FileName: 'qrcode.png'
});
// 读取原始图片信息
var info = V8.Image.GetInfo({ Image: sourceBase64 });
// Data: Width、Height、Format、ContentType、Size、FrameCount、
// RepetitionCount、Origin、HasAlphaWatermark 的 BaseImage 也可写为 Image,兼容 Base → BaseImage、Overlay → Watermark。
绘制文字和图形
Create 和 Draw 使用相同的 Elements。Create 在新画布上绘制;Draw 在输入图片上绘制,输出宽高与原图相同。
var result = V8.Image.Draw({
Image: sourceBase64,
Elements: [
{
Type: 'text',
X: 40,
Y: 40,
Text: 'CONFIDENTIAL',
Color: 'rgba(239,68,68,0.75)',
FontSize: 36,
FontFamily: 'Arial',
FontStyle: 'bold-italic',
Align: 'left',
VerticalAlign: 'top',
Rotation: -8
},
{
Type: 'round-rect',
X: 40,
Y: 90,
Width: 320,
Height: 100,
FillColor: '#ffffff88',
StrokeColor: '#ef4444',
StrokeWidth: 3,
CornerRadius: 16,
Opacity: 0.9
},
{
Type: 'line',
X: 40,
Y: 220,
X2: 360,
Y2: 220,
StrokeColor: '#ef4444',
StrokeWidth: 3
}
]
});| 元素类型 | 参数 |
|---|---|
text | Text、Color、FontSize、FontFamily、FontStyle、Align、VerticalAlign |
rectangle / rect / round-rect | X、Y、Width、Height、填充、描边、圆角 |
ellipse / circle | X、Y、Width、Height、填充、描边 |
line | X、Y、X2、Y2 或 Width、Height、描边 |
所有元素还支持 Opacity 和 Rotation。单次最多绘制 500 个元素。
颜色、安全与资源限制
颜色支持常用英文颜色名、transparent、#RGB、#RGBA、#RRGGBB、#RRGGBBAA、rgb(...)、rgba(...)。颜色自身的 Alpha 会与 Opacity 相乘。
运行时内置限制:单次最多合并 50 张图;单边不超过 16,384 像素;单张输入或输出画布不超过 25,000,000 像素;单次解码和单次缩放后图层分别不超过 50,000,000 像素;单张输入不超过 25 MB;单次输入总量不超过 100 MB;输出不超过 50 MB。
这些限制是保护上限,不是业务推荐值。匿名接口应增加更严格的数量、尺寸、并发和权限限制。远程图片必须先通过 V8.Http 下载,并对用户可控 URL 做协议、域名和目标地址白名单校验,不能把 URL 或服务器路径直接传给 V8.Image。
FontFamily 是首选字体。运行时会逐个 Unicode 字符验证字形:未传字体、指定字体不存在或某个字体缺少部分字符时,先回退到服务器已安装且包含该字形的字体,再回退到随 Dos.Common 程序集发布的 Noto Sans CJK SC;同一段中英文混排文字可使用多个字体段。因此没有安装任何系统字体的 Linux / 群晖 / 精简容器也能绘制基础拉丁字符、数字和简体中文。如果系统字体与内置字体都不包含某字符,接口会返回带字符及 U+XXXX 码位的明确错误,绝不会生成“口口”缺字方框。内置字体解决可用性,不替代品牌字体、繁体异体字、特殊符号或 Emoji 字体;要求固定字形时仍应在服务器安装业务字体并显式传 FontFamily。
当前用户 V8.CurrentUser
- 当前登陆用户信息,包含用户所属角色、组织机构等,包含使用表单引擎对sys_user表新增字段的信息。
- 未登录时访问到的值为{}
var userName = V8.CurrentUser.Name;数据库对象 V8.Db
- 数据库访问对象,支持Dos.ORM、SqlSugar切换
FromSql只传 SQL 字符串;动态值请使用.AddInParameter("@p0", value)链式绑定,不要写FromSql(sql, value)。
// 查询多条
var list = V8.Db.FromSql("select Id, Account, Name from sys_user where Status = @p0")
.AddInParameter("@p0", 1)
.ToArray();
// 执行 insert/update/delete,返回受影响行数
var affected = V8.Db.FromSql("update sys_user set Status = @p0 where Id = @p1")
.AddInParameter("@p0", 0)
.AddInParameter("@p1", userId)
.ExecuteNonQuery();
// 查询单条
var user = V8.Db.FromSql("select Id, Account, Name from sys_user where Id = @p0")
.AddInParameter("@p0", userId)
.First();
// 查询单个标量
var count = V8.Db.FromSql("select count(1) from sys_user where Status = @p0")
.AddInParameter("@p0", 1)
.ToScalar();数据库只读对象 V8.DbRead
- 数据库只读对象,用法和V8.Db一样,当数据库未部署读写分离时,此对象与V8.Db对象值一致。
扩展数据库对象 V8.Dbs
“数据库管理”中启用的连接按 DbKey 暴露为 V8.Dbs.<DbKey>。当前认证类型为 MySql、SqlServer、Oracle、PostgreSql、DaMeng、KingBase;完整配置、MCP 结构读取和附件迁移说明见扩展数据库与外部数据迁移。
安装或更新应用商城中的“数据库扩展”后,microi_database 的后端提交后事件会调用 V8.Method.RefreshExtensionDatabases()。事件在事务提交后递增按 OsClient 隔离的 Redis 版本,因此新增、修改、停用或删除连接后,各 API 节点下一次访问 V8.Dbs 即可看到新配置,无需重启;短 TTL 仅作为旧版本节点的兼容兜底。
Oracle 原生日期格式化使用 TO_CHAR(日期值, 'YYYY-MM-DD'),不要交换值与格式的顺序。 ToArray() / ToList() 才会执行并读取查询,因此错误堆栈停在该方法不等于集合转换失败。 若出现 ORA-01722,应核对最终 SQL、参数类型及账号筛选条件,分别验证计数和列表查询, 不要用重复执行掩盖确定性的转换错误。旧版 Oracle Provider 曾错误交换 TO_CHAR 参数, 应升级包含原生 SQL 保留修复的后端;无需修改数据库列或将业务查询改为无条件查询。
var dataList = V8.Dbs.OracleDB1
.FromSql('SELECT ID, NAME FROM CUSTOMER WHERE STATUS = @p0')
.AddInParameter('@p0', 1)
.ToArray();
// 不保存到数据库管理:创建仅当前请求使用的临时会话
// 连接串只能来自可信服务端配置,禁止直接传 V8.Param.ConnectionString
var tempDb = V8.Dbs.Open(
'SqlServer',
'Server=127.0.0.1,1433;Database=app;User Id=user;Password=***;TrustServerCertificate=True;'
);
var tempRows = tempDb.FromSql('SELECT Id, Name FROM Customer WHERE Status = @p0')
.AddInParameter('@p0', 1)
.ToArray();
// 扩展数据库事务与 V8.DbTrans 完全独立,需要手动管理生命周期
var recordId = V8.Param.Id;
var emptyExTrans = V8.Dbs.EmptyEx.BeginTransaction();
try {
var count = emptyExTrans
.FromSql("delete from diy_extend_test where Id = @p0")
.AddInParameter("@p0", recordId)
.ExecuteNonQuery();
emptyExTrans.Commit();
return { Code : 1, Data : count };
} catch (error) {
emptyExTrans.Rollback();
throw error;
} finally {
emptyExTrans.Close();
}新增或修改保存连接后会递增共享 Redis 版本,各节点在下一次访问时立即回源,不需要重启 API。默认兜底 TTL 为 60 秒,需要调整时修改 SaaS 引擎主租户的 ExtensionDatabaseCacheSeconds。连接串、密码和鉴权参数不得出现在日志、前端代码或接口返回中。
数据库事务 V8.DbTrans
V8.Db 是主库会话,V8.Db.FromSql 不会自动加入当前接口引擎事务。需要共同提交或回滚的 SQL 必须使用 V8.DbTrans.FromSql;依赖本事务尚未提交的数据时,查询也使用该入口。FormEngine 和嵌套 ApiEngine.Run 显式传入 V8.DbTrans 共享事务。不要读取安全代理的内部事务来手动提交。
- 数据库事务对象,可以像V8.Db一样使用,如:
var array = V8.DbTrans.FromSql('...').ToArray();无需在接口引擎中手动调用 V8.DbTrans.Commit() 或 Rollback()。事务生命周期由平台安全代理统一管理:
- 返回
DosResult或带Code的对象时,只有Code === 1提交。 - 返回对象但没有
Code时回滚。 - 返回字符串、数字、数组、布尔值或
null,且脚本未异常时提交。 - 复用外层传入的事务时,最终结果由外层调用决定。
- 脚本显式调用平台事务的
Commit/ Rollback/Close会被忽略。
- 接口引擎示例
//操作第一张表,带事务
var result1 = V8.FormEngine.UptFormData('表名或表Id,不区分大小写', {
Id : '',//必传
Age : 20, //要修改的字段,注意字段值不能是{}或[],需要序列化
Sex : '女'
}, V8.DbTrans);
//操作第二张表,带事务
var result2 = V8.FormEngine.UptFormData('表名或表Id,不区分大小写', {
Id : '',//必传
Age : 20, //要修改的字段,注意字段值不能是{}或[],需要序列化
Sex : '女'
}, V8.DbTrans);
//如果第二张表操作成功
if(result2.Code == 1){
return { Code : 1 };//平台会自动提交事务,因为返回的Code=1
}else{//如果第二张表操作失败
return { Code : 0, Msg : result2.Msg };//平台会自动回滚事务,因为返回的Code=0
}V8.MQTT
V8.MQTT 是 MQTT 事件接口引擎的只读上下文,配合 V8.EventName 使用。它不是普通接口引擎中的通用发布函数;服务端设备下行使用带租户上下文的 IMicroiMQTT.PublishAsync(osClient, ...)。
V8.EventName | 说明 |
|---|---|
StartServer / StopServer | 当前 Broker 启动或正常停止 |
Connected / Disconnected | 当前租户设备连接或断开 |
Subscribing | 订阅通过租户 Topic ACL 后 |
MessageReceived | 收到发布消息;返回 Code != 1 可阻止向订阅者广播 |
MessageChanged | Retained Message 发生变化 |
V8.MQTT 字段 | 说明 |
|---|---|
ClientId、OsClient、Topic | 已由 Broker 校验的设备、租户与规范化 Topic |
Payload、PayloadRaw | JSON 自动解析结果与原始 UTF-8 文本 |
UserName、UserProperties | 连接用户名与 MQTT v5 User Properties |
Qos、Retain | QoS 级别与保留消息标记 |
if (V8.EventName === 'MessageReceived') {
console.log(V8.MQTT.ClientId + ' -> ' + V8.MQTT.Topic);
return { Code: 1, Data: V8.MQTT.Payload };
}完整的 Broker 配置、SaaS 凭据、Topic 隔离、设备级接口引擎、TLS、数据落库、服务端下行与集群边界见 MQTT 引擎(IoT 物联网)。
V8.MongoDb
平台兼容镜像基于驱动 3.11.2 的安全修复维护 MongoDB 3.6 及当前服务端的 CRUD、日志路径,保留 BSON、认证和逐项特性版本检查。遇到 wire version 6 ... requires at least 9 时,需要更新包含兼容构建的后端镜像;该错误不表示日志为空或 Token 失效。驱动兼容不会自动更新数据库,也不修复旧服务端自身的缺陷。
介绍
- 本篇介绍如何在接口引擎、后端V8事件中对MongoDB进行相关操作
- 对MongoDB的新增操作会自动生成对应数据库名和表名,因此可自定义分库、分表规则
新增数据 AddFormData
*自定义数据库名、表名,不存在时会自动创建
//可以指定固定的Id值
var newId = V8.MongoDb.NewId();
V8.MongoDb.AddFormData({
DbName : '', //数据库名称,如:sys_log_2024
TableName: '', //表名名称,如:log_2024_12
Id : newId, //也可以不指定,会自动生成
_FormData : {
Name : '张三',
Sex : '男',
Age : 18
}
});修改数据 UptFormData
V8.MongoDb.UptFormData({
DbName : '', //数据库名称,如:sys_log_2024
TableName: '', //表名名称,如:log_2024_12
Id : '', //数据Id
_FormData : {
Name : '张三',
Sex : '男',
Age : 18
}
});按条件批量修改 UptFormDataByWhere
var result = V8.MongoDb.UptFormDataByWhere({
DbName: 'diy_chat_' + V8.OsClient.toLowerCase(),
TableName: 'chat_' + DateNow('yyyy'),
_Where: [
['FromUserId', '=', V8.Param.PeerUserId],
['ToUserId', '=', V8.CurrentUser.Id],
['IsRead', '=', false]
],
_FormData: { IsRead: true }
});_Where 必须是至少包含一个有效条件的参数化数组;空条件、无效字段或操作符会失败关闭,不会退化为全集合修改。_FormData 不得修改 _id 及其子路径。成功返回 MatchedCount 和 ModifiedCount。
删除数据 DelFormData
V8.MongoDb.DelFormData({
DbName : '', //数据库名称,如:sys_log_2024
TableName: '', //表名名称,如:log_2024_12
Id : '', //数据Id
});按条件批量删除 DelFormDataByWhere
var result = V8.MongoDb.DelFormDataByWhere({
DbName: 'diy_chat_' + V8.OsClient.toLowerCase(),
TableName: 'chat_last_contact',
_Where: [
['UserId', '=', V8.CurrentUser.Id],
['ContactUserId', '=', V8.Param.PeerUserId]
]
});此方法同样强制非空 _Where,成功返回 DeletedCount。删除业务必须把当前用户、权威资源 Id 等边界写入条件,不得仅依赖前端传入的行 Id。
查询数据列表 GetTableData
V8.MongoDb.GetTableData({
DbName : '', //数据库名称,如:sys_log_itdos
TableName: '', //表名名称,如:log_202412
_Where : [
['Type', '=', '访问菜单'],
['OR', 'Type', '=', '点击V8按钮']
],
_OrderBy: 'CreateTime',
_OrderByType: 'DESC',
_PageIndex: 1,
_PageSize: 20
});查询单条数据 GetFormData
V8.MongoDb.GetFormData({
DbName : '', //数据库名称,如:sys_log_2024
TableName: '', //表名名称,如:log_2024_12
Id : '', //数据Id
});GetFormData 和 GetTableData 属于 dynamic 无模式读取。服务端会在 MongoDB 投影阶段过滤 BSON 内部 _t CLR 类型判别字段,避免存量强类型 C# 写入留下的旧类型名称触发反序列化失败。_t 不是 V8 业务契约;业务需要类型标记时应使用自有字段(例如 DocumentType),不得读取、筛选或依赖 _t。强类型 C# MongoDB 模型仍按自身的多态映射处理。
MongoDB 调用使用当前 V8 租户上下文;非主库租户脚本传入其它 OsClient 不能跨租户访问。MongoDB 不参与 V8.DbTrans:批量写方法返回 Code=1 时应视为已提交事实,后续关系库 Hook、SignalR 或外部投递失败不得伪装成“整体未发生”并盲目重试;需要重试时使用稳定业务 Id 实现幂等。
V8.Http
- 对 RestSharp 的受控封装,支持 GET、POST、PATCH。前后端 V8 使用相同的 PascalCase 对象参数;后端同步方法直接返回,显式
*Async方法在 Jint 中使用await,前端浏览器端也需使用await。
| 方法 | 主要参数 | 返回值 |
|---|---|---|
V8.Http.Get | GetParam | 响应字符串 |
V8.Http.Post | PostParam / PostParamString | 响应字符串 |
V8.Http.Patch | PatchParam / PatchParamString | 响应字符串 |
GetResponse/PostResponse/PatchResponse | 同上 | 完整响应对象 |
GetAsync/PostAsync/PatchAsync | 同步版对应参数 | await 后得到响应字符串 |
GetResponseAsync/PostResponseAsync/PatchResponseAsync | 同步版对应参数 | await 后得到完整响应对象 |
完整响应对象包含 Content、Headers、RawBytes、StatusCode、ErrorMessage。Timeout 与兼容参数名 TimeOut 的单位均为秒,默认 600 秒(10 分钟);Headers 与 Header 等效;ParamType 支持 form(默认)、json、xml、binary。GetParam 是 URL 查询参数,GET、POST、PATCH 均可使用。字符串版遇到部分网络错误时可能直接返回错误文本,因此支付、同步、回调等关键集成应使用 *Response 并检查 StatusCode 与 ErrorMessage,不要假定字符串一定是 JSON。
// POST JSON。嵌套对象使用 PostParamString,避免对象转换丢失层级。
var loginText = V8.Http.Post({
Url: 'https://api.example.com/login',
PostParamString: JSON.stringify({
User: { Account: 'admin', Pwd: '******' },
OsClient: 'demo'
}),
ParamType: 'json',
Timeout: 10,
Headers: { 'X-Trace-Id': V8.Method.NewGuid() }
});
var loginResult = JSON.parse(loginText);
// GET 查询参数
var listText = V8.Http.Get({
Url: 'https://api.example.com/users',
GetParam: { page: 1, size: 20 },
Headers: { Authorization: 'Bearer ' + loginResult.token }
});
// PATCH JSON。嵌套对象使用 PatchParamString。
var patchText = V8.Http.Patch({
Url: 'https://api.example.com/users/123',
PatchParamString: JSON.stringify({ profile: { name: '新名字' } }),
ParamType: 'json',
Headers: { Authorization: 'Bearer ' + loginResult.token }
});
// PATCH 完整响应
var patchResp = V8.Http.PatchResponse({
Url: 'https://api.example.com/users/123',
PatchParam: { Status: 1 },
ParamType: 'json'
});
if (patchResp.StatusCode < 200 || patchResp.StatusCode >= 300) {
return { Code: 0, Msg: patchResp.ErrorMessage || patchResp.Content };
}
// 请求内异步。不要用 setTimeout/Task.Run 把它变成请求外后台任务。
var asyncResp = await V8.Http.GetResponseAsync({
Url: 'https://api.example.com/health',
Timeout: 5
});
// XML 请求
var xmlText = V8.Http.Post({
Url: 'https://api.example.com/xml',
ParamType: 'xml',
PostParamString: '<xml><text>1</text></xml>'
});
// 上传文件:键同时作为表单字段名和文件名
var uploadText = V8.Http.Post({
Url: 'https://api.example.com/upload',
PostParam: { title: '附件' },
FilesByteBase64: { 'report.pdf': pdfBase64 }
});接口引擎中必须使用对象参数格式,例如 V8.Http.Get({ Url: url })。不要使用 V8.Http.Get(url);旧的 .NET 同名异步重载可能被 Jint 解析为 Promise。
后端 V8.Http 的严格 SSRF 防护默认关闭,未配置时完全保留历史行为:不限制协议、URL 内嵌凭据、localhost、私网、链路本地或云元数据地址,并继续自动处理重定向。只有在 SaaS 引擎主租户启用 SsrfProtectionEnabled 后,才只允许 HTTP(S),拒绝 URL 内嵌凭据、回环、私网、链路本地、云元数据和其它特殊地址,同时禁止自动跟随 3xx;受控目标可通过 SsrfAllowedHosts 精确放行。白名单匹配主机,不匹配 URL 子串;严格模式下需要跳转时,先用 GetResponse/PostResponse/PatchResponse 检查状态码和 Location,再显式发起下一次请求。租户配置方法见 平台安全总览。
从后端 V8 调用 Microi.AI
后端接口引擎和表单后端事件已注入第一等 V8.AI,无需再通过 HTTP 自请求当前服务器。对象创建时固定绑定当前 V8 执行的 OsClient 与 V8.CurrentUser;脚本传入的用户、租户、Endpoint、ApiKey、NL2SQL 表白名单和服务端授权标记都会被清除:
var result = await V8.AI.Chat({
UserChatMsg: String(V8.Param.Question || ''),
AiModel: String(V8.Param.AiModel || ''),
AiModelId: String(V8.Param.AiModelId || '')
});
return result;可用方法包括 GetLicenseState、UpdateConversationTitle、Chat、ChatStream、RecognizeIntent、NL2SQL、NL2SQLStream、NL2V8 和 NL2V8Stream。UpdateConversationTitle(conversationId, title, source) 只修改当前 V8 租户、当前登录用户自己的对话。匿名接口没有可信用户上下文时返回 Code=1001;NL2SQL 始终重新按当前用户授权,NL2V8/NL2V8Stream 仅平台管理员可调用。
官方 app.microi.ai-engine v6.3.6 使用 Managed platform-ai-runtime 编排标题修改、意图识别、非流式对话、NL2SQL 与非流式 NL2V8。租户个性化逻辑写在 CreateIfMissing platform-ai-custom-hook,该 Hook 只接收来源、阶段和动作,不接收问题、回答、标题、SQL、模型、附件或凭据。浏览器流式请求继续走原生 SSE 路由。
后端流式回调会占用本次 Jint 请求,只适合确实需要由同一脚本消费增量的场景;面向页面的打字机效果仍优先让浏览器使用前端 V8.AI.ChatStream。MCP 直接使用专用 microi_chat 获取最终结果,不再绕到 microi_run_engine 包装接口。不要把 Token、完整问题或回答写入日志。完整代码与授权矩阵见 AI 引擎与 Microi.AI 中转站。
V8.Tcp
后端接口引擎和后端表单事件可通过
V8.Tcp连接 TCP 服务端并发送原始字节,适用于 ESC/POS 网络小票机、串口服务器、PLC 等设备。它是一次性客户端,不提供 TCP 监听、持久 Socket 或 TLS 隧道。
| 方法 | 说明 | 返回值 |
|---|---|---|
V8.Tcp.Send | 连接、发送、关闭 | DosResult |
await V8.Tcp.SendAsync | 当前请求内异步连接、发送、关闭 | Promise<DosResult> |
V8.Tcp.SendAndReceive | 发送后读取有界响应,再关闭 | DosResult |
await V8.Tcp.SendAndReceiveAsync | 当前请求内异步发送并读取有界响应 | Promise<DosResult> |
所有方法使用同一个对象参数:
| 参数 | 必填 | 说明 |
|---|---|---|
Host | 是 | 主机名或 IP,不能传 URL |
Port | 是 | 1-65535;RAW/JetDirect 打印常用 9100 |
Bytes / RawBytes / ByteArray | 四选一 | 0-255 的整数数组 |
ByteBase64 / BytesBase64 / Base64 | 四选一 | Base64 编码的原始字节 |
Hex | 四选一 | 十六进制;可包含空格、0x、连字符、冒号或逗号 |
Text | 四选一 | 按 Encoding 编码后发送 |
Encoding | 否 | 默认 utf-8;支持 ascii、gb18030、gbk/gb2312、UTF-16/UTF-32 大小端 |
ConnectTimeout | 否 | 连接超时秒数,默认 10,范围 1-120 |
SendTimeout | 否 | 发送超时秒数,默认 10,范围 1-120 |
ReceiveTimeout | 否 | 收发方法的总接收超时秒数,默认 3,范围 1-120 |
MaxReceiveBytes | 否 | 最大响应字节数,默认 65536,最大 1048576 |
NoDelay | 否 | 是否禁用 Nagle,默认 true |
// ESC/POS 网络小票机。实际地址必须从后端可信配置或受权限保护的设备表读取。
var printResult = V8.Tcp.Send({
Host: '192.168.1.88',
Port: 9100,
// 初始化、ASCII 小票正文、换行、切纸
Bytes: [
27, 64,
77, 105, 99, 114, 111, 105, 10,
84, 111, 116, 97, 108, 58, 32, 49, 50, 46, 48, 48, 10,
10, 10,
29, 86, 0
],
ConnectTimeout: 5,
SendTimeout: 5
});
if (printResult.Code !== 1) return printResult;
return {
Code: 1,
Data: {
BytesSent: printResult.Data.BytesSent,
RemoteEndPoint: printResult.Data.RemoteEndPoint
}
};如果打印机需要 GBK/GB18030 文本,可直接发送文本;若同一帧还包含 ESC/POS 控制命令,应在调用前把完整帧编码为一个 Bytes、ByteBase64 或 Hex 载荷:
var textResult = await V8.Tcp.SendAsync({
Host: '192.168.1.88',
Port: 9100,
Text: '吾码小票\n合计:12.00\n\n',
Encoding: 'gb18030'
});
if (textResult.Code !== 1) return textResult;需要读取设备响应时:
var deviceResult = V8.Tcp.SendAndReceive({
Host: '192.168.1.20',
Port: 4001,
Hex: '01 03 00 00 00 02 C4 0B',
ReceiveTimeout: 3,
MaxReceiveBytes: 4096
});
if (deviceResult.Code !== 1) return deviceResult;
// Data.RawBytes、Data.ByteBase64、Data.Hex
// ReceiveEndReason: RemoteClosed / Timeout / MaxReceiveBytes
return deviceResult;Send 成功时 Data 包含 BytesSent 和 RemoteEndPoint。收发方法还返回 BytesReceived、RawBytes、ByteBase64、Hex、ReceiveEndReason 和 Truncated。发送载荷最大 4 MiB。收到部分数据后达到接收超时,会以 Code=1 返回已收到的数据并标记 ReceiveEndReason='Timeout';一个字节也未收到则返回失败。
安全与验收边界:
Host、Port必须来自 SaaS 可信配置或受权限保护的设备表,并在业务代码中做精确白名单校验;禁止直接透传V8.Param。匿名接口尤其不能开放任意 TCP 目标。- TCP 不经过
V8.Http的 SSRF 防护。生产网络应再用容器网络策略或防火墙只放行打印机/设备网段与端口,且不要记录原始票据、设备口令或响应秘密。 - 每次调用都会新建并关闭连接。
Code=1只证明字节已写入 TCP 连接,不证明设备已执行、更不证明小票已经出纸;正式验收仍需设备状态或实物小票证据。 - 写入结果不确定时不要盲目自动重试:打印不是天然幂等,重试可能产生重复小票。需要可靠打印时使用业务幂等号、设备回执以及 Job/MQ/outbox 编排。
- 在 Docker 中,
Host是从后端容器视角解析;localhost指容器自身。部署前必须验证容器到设备 IP/端口的实际路由。
V8.Header、V8.Param
- 目前两者均只支持在接口引擎中使用,用于获取客户端http post请求接口引擎地址发送的报文和Request Payload参数。
微信支付最小原子 V8.WeChat
V8.WeChat 是供后端接口引擎调用的微信支付协议原子,不是订单、退款或通知业务 SDK。商户号、订单状态、金额、幂等、事务、日志和通知仍应在接口引擎中编排;只有 HTTP 路由、可信验签、租户恢复、密钥隔离等协议边界才放在最小 C# 网关中。
| 函数 | 说明 |
|---|---|
AesGcmDecrypt(associatedData, nonce, ciphertext, apiV3Key) | 使用 AES-256-GCM 解密微信支付 API v3 回调或平台证书资源,并验证密文尾部认证标签 |
GetWeChatSign(privateKeyPem, paramList) | 按参数顺序以换行符拼接待签名串,使用商户 RSA 私钥生成 SHA256-RSA2048 Base64 签名 |
GetWeChatAuthorization(mchid, serialNo, privateKeyPem, wxApiAddress, body) | 为 POST 请求生成 WECHATPAY2-SHA256-RSA2048 Authorization 值;wxApiAddress 传参与签名一致的请求路径和 Query |
AesGcmDecrypt 参数编码
微信支付回调中的四个参数不是全部 Base64。必须保持下列编码边界:
| 参数 | 正确处理 |
|---|---|
apiV3Key | 商户 APIv3 密钥原文,按 UTF-8 转为 32 字节;不要先做 Base64 解码 |
nonce | resource.nonce 原文,按 UTF-8 转为 12 字节随机 IV;不是 Base64 |
associatedData | 原样使用 resource.associated_data 并按 UTF-8 编码;字段为空时按空字节串处理 |
ciphertext | 仅此参数执行 Base64 解码;解码后的完整字节串末尾包含 16 字节 GCM 认证标签,标签必须和密文一起交给解密器 |
以下是接口引擎中的核心调用片段。WeChatPay.ApiV3Key 是示例系统设置 Key,应与当前租户实际配置保持一致:
var resource = V8.Param && V8.Param.resource;
if (!resource
|| resource.algorithm != 'AEAD_AES_256_GCM'
|| !resource.nonce
|| !resource.ciphertext) {
return { Code: 0, Msg: '微信支付回调资源格式错误。' };
}
var privateSettings = (V8.SysConfig && V8.SysConfig.ServerPrivateSettings) || {};
var apiV3Key = privateSettings['WeChatPay.ApiV3Key'];
if (!apiV3Key) {
return { Code: 0, Msg: '当前租户未配置微信支付 APIv3 密钥。' };
}
var decryptedText;
try {
decryptedText = V8.WeChat.AesGcmDecrypt(
resource.associated_data || '',
resource.nonce,
resource.ciphertext,
apiV3Key
);
} catch (ex) {
// 不回传异常、密钥、密文或解密内容;只记录不含秘密的追踪标识。
var traceId = V8.Method.NewUlid();
console.error('微信支付回调资源解密失败,TraceId=' + traceId);
return { Code: 0, Msg: '微信支付回调资源解密失败。', DataAppend: { TraceId: traceId } };
}
var paymentNotice = JSON.parse(decryptedText);
// 从这里开始仍属于接口引擎业务:从数据库重读订单,校验 appid/mchid/out_trade_no/金额,
// 以 transaction_id 或通知 Id 做唯一幂等,按状态机更新,并通过 outbox 编排后续副作用。AesGcmDecrypt 会校验 APIv3 密钥和 nonce 字节长度,并认证密文尾部标签;密文或标签被篡改时会抛出异常。不要把 nonce 改成 Convert.FromBase64String(nonce),也不要为了兼容错误调用而同时接受两种 nonce 编码。该函数只完成资源解密与 GCM 完整性认证,不等于微信支付 HTTP 回调签名验证;正式回调必须先按 Wechatpay-Timestamp、Wechatpay-Nonce、请求原文和 Wechatpay-Signature 完成平台签名验证,再进入业务事务。参见微信支付官方“如何解密回调报文和平台证书”。
APIv3 密钥、商户私钥不得写入 V8 源码、接口参数、日志或响应。后端接口引擎从当前租户 V8.SysConfig.ServerPrivateSettings 读取私密设置;浏览器和匿名公开配置不会获得该节点。
加密类 V8.EncryptHelper
- Dos.Common加密帮助类
var pwd = V8.EncryptHelper.DESEncode('123456');//DES加密
var pwd = V8.EncryptHelper.DESDecode('JdZe5gWKjZo=');//DES解密
var pwd = V8.EncryptHelper.SHA1('123456');
var pwd = V8.EncryptHelper.SHA256('123456');
var pwd = V8.EncryptHelper.SHA512('123456');
var digest = V8.EncryptHelper.MD5Encrypt('123456');//兼容用不可逆摘要,不是加密
var pwd = V8.EncryptHelper.Sha256Hex('123456');MD5、SHA1、SHA256 等摘要不能用于新密码存储;登录密码必须使用平台认证流程和带盐的专用密码哈希。
DESEncode/DESDecode 可用于业务明确要求“加密保存且授权后显示明文”的兼容字段,但只能在后端接口引擎/表单事件中处理:列表默认掩码,显示明文使用独立受权动作并审计,响应禁止缓存,不向匿名、访问密钥会话或普通 FormEngine 暴露批量解密。DES 是现有兼容格式;新高价值秘密优先使用带版本的现代认证加密和集中密钥管理。完整分级见平台安全与兼容基线。
接口引擎自身需要持久保存可逆凭据时,不要从已脱敏的 V8.OsClientModel 读取 AuthSecret、DbConn,也不要自行拼接 AES 密钥;使用上文的 V8.Method.ProtectApiEngineSecret/UnprotectApiEngineSecret,由可信宿主完成租户与接口引擎作用域绑定。
强身份验证票据 V8.Method.ConsumeIdentityVerificationTicket
登录后的前端 V8 可通过 V8.Identity.Verify 完成 Passkey、Authenticator TOTP 或严格人脸验证,取得两分钟有效的一次性票据。后端接口引擎必须从数据库重读业务事实、重新计算 ActionHash,再原子消费:
var actionHash = V8.EncryptHelper.Sha256Hex(canonicalBusinessCommand);
var verified = V8.Method.ConsumeIdentityVerificationTicket({
Ticket: V8.Param.IdentityVerificationTicket,
Purpose: 'ApproveSensitiveOperation',
ActionHash: actionHash
});
if (verified.Code !== 1) return verified;票据绑定当前 DiyToken 用户、OsClient、用途和操作摘要,只能成功消费一次;访问密钥会话不能使用。它不代替菜单/表权限、业务状态机、事务和幂等。完整配置与前端示例见 Passkey、Authenticator、设备生物识别与严格人脸验证。
V8.TranslateEngine
V8.TranslateEngine 绑定当前 V8 执行租户的 SaaS 翻译配置。兼容入口 Translate(text, to, from?) 的 Data 是单个译文字符串;新代码需要批量、HTML、自动检测、候选译文或完整返回信息时使用 TranslateText:
底层实现来自开源 NuGet/类库 Microi.Translate;平台插件只负责把当前租户上下文安全注入 V8,不在闭源 Microi.net 内复制翻译业务逻辑。
var result = V8.TranslateEngine.TranslateText({
SourceTexts: ['你好', '世界'], // 与 SourceText 二选一
FromLang: 'auto',
Lang: 'en',
Format: 'text', // text | html
Alternatives: 2
});
if (result.Code !== 1) return result;
var detected = V8.TranslateEngine.Detect({ SourceText: 'Bonjour' });
var languages = V8.TranslateEngine.GetLanguages();
var health = V8.TranslateEngine.Health();完整业务方法包括 Translate/TranslateText、Detect、GetLanguages、TranslateFile、Suggest、Health,以及不调用供应商的 GetLang/GetLangData/GetLangCode 词条方法。文件翻译支持 TXT、HTML、ODT、ODP、DOCX、PPTX、XLSX、EPUB、PDF,返回文件名、ContentType、长度和 Base64;接口引擎返回文件时需开启“响应文件”。
脚本传入的 OsClient 会被当前 V8TenantContext 覆盖,且参数中不存在 Endpoint、API Key、Authorization 或 Header。普通租户不会隐式借用主租户翻译凭据。LibreTranslate 的前端设置、API Key 管理、指标和 Web UI 属于运维面,不开放给 V8。完整返回结构、MCP 工具和部署方式见翻译引擎。
V8.OCR
V8.OCR 是图片/PDF 文字识别的统一租户网关。Microi API 负责鉴权、租户隔离、文件校验、大小/超时/页数限制和统一结果;OCR 模型由独立的 PaddleX 服务承载,避免在每个 .NET API 节点重复加载模型。
本节保留后端 V8 方法签名和返回协议;方案选型、部署、SaaS 配置、HTTP/MCP 调用、生产模式与故障排查见 OCR 识别引擎。
var result = await V8.OCR.Recognize({
FileByteBase64: V8.FilesByteBase64.invoice,
FileName: 'invoice.png',
UseDocOrientationClassify: true,
UseDocUnwarping: true,
UseTextlineOrientation: true,
TextRecScoreThresh: 0.5,
ReturnWordBox: false
});
if (result.Code !== 1) {
return result;
}
return {
Code: 1,
Data: {
Text: result.Data.Text,
AverageConfidence: result.Data.AverageConfidence,
Pages: result.Data.Pages,
TraceId: result.Data.TraceId
}
};支持 PDF、PNG、JPEG、BMP、GIF、TIFF、WebP。平台会同时校验扩展名、Base64 和文件魔数;成功结果统一包含 Provider、TraceId、FileType、Text、AverageConfidence、PageCount、ElapsedMilliseconds 和 Pages。每页包含文本区域、置信度和多边形坐标。
也可由已登录客户端调用官方 Managed 接口 POST /apiengine/platform-ocr-recognize,请求体与上例参数相同。请求体中的 OsClient 会被忽略,以验证后的 Token 租户为准。接口不接受 endpoint、API key、任意 Header、代理地址或服务端文件路径;个性化逻辑写入 platform-runtime-custom-hook,文件和识别原文不会传给 Hook。
SaaS 引擎配置
管理员在 SaaS引擎 → OCR识别 独立 Tab 配置:
| 字段 | 说明 | 默认值 |
|---|---|---|
OcrEnabled | 当前租户总开关;未开启时失败关闭 | 0 |
OcrProvider | PaddleX(基础服务)或 PaddleXHighStability(KServe) | PaddleX |
OcrEndpoint | 服务完整接口地址 | 空 |
OcrApiKey | 可选 Bearer 密钥 | 空 |
OcrHeadersJson | 可选服务端固定 Header JSON | 空 |
OcrTimeoutSeconds | 单次超时,平台硬上限 300 秒 | 60 |
OcrMaxFileMB | 单文件上限,平台硬上限 100 MB | 20 |
OcrMaxPages | PDF 页数上限,平台硬上限 100 页 | 10 |
OcrMinConfidence | 统一结果最低置信度,范围 0-1 | 0 |
OcrEndpoint、OcrApiKey、OcrHeadersJson 等 OCR 配置不会进入 V8.OsClientModel,业务脚本只能使用绑定当前租户的 V8.OCR。密码控件用于减少界面误显,但不等同于数据库静态加密;生产环境仍应限制 SaaS 配置表权限、数据库备份权限和审计日志内容。
部署边界
PaddleX 基础服务协议为 POST /ocr,高稳定服务协议为 POST /v2/models/ocr/infer。生产环境应固定 PaddleX、PaddlePaddle 和模型版本,并为 OCR 服务配置访问控制、readiness、请求体/并发限制与 CPU/GPU 资源上限。Microi API 节点保持无状态,可以共同调用同一服务池。
仓库提供固定 PaddleX 3.6.1 + PaddlePaddle 3.2.2 的 linux/amd64 CPU 基线。PaddlePaddle 3.3.0 当前存在 CPU oneDNN PIR 推理兼容问题,因此不要自行替换为 3.3.0。推荐直接拉取吾码杭州公开镜像;镜像发布阶段已经预置默认 OCR 产线模型:
cd Microi.Server/Microi.OCR/deploy/paddlex
docker compose -f compose.cpu.yml pull
docker compose -f compose.cpu.yml up -d --no-build
docker compose -f compose.cpu.yml ps默认只映射 127.0.0.1:18080,宿主机运行的 API 将 OcrEndpoint 配成 http://127.0.0.1:18080/ocr。若 API 也在 Docker 中,可将其加入名为 microi-ocr 的外部网络,并配置 http://microi-ocr:8080/ocr。全新的 named volume 会从镜像复制预置模型并在容器重启后复用;healthcheck 的 10 分钟启动宽限用于低配机器首次复制和加载模型,不代表单次识别可运行 10 分钟。本地修改 Dockerfile 后可显式执行 docker compose -f compose.cpu.yml build。
Compose 的 CPU/内存限制是安全基线,应按真实图片尺寸、页数和并发压测后调整。GPU 环境必须按显卡驱动选择官方 CUDA 11.8/12.6 镜像与对应 PaddlePaddle wheel,不应直接复用 CPU Dockerfile 或使用浮动 latest。
当前入口是请求内同步识别。批量、超大文件或长耗时 OCR 必须建立共享数据库/MQ/outbox 任务,以全局任务 Id、唯一约束和状态机保证幂等恢复;进程内队列、static 字典或本地文件不能作为任务完成事实源。
V8.Vision
V8.Vision 是绑定当前租户的通用视觉原子能力。它负责有界图片解码、宿主已安装 ONNX 流水线的检测/分割、人脸对齐、对象级特征、HNSW 近邻检索、IoU 跟踪和连续帧投票;对象/商品/人员表、匹配状态、AI 回退和业务 Hook 均由接口引擎编排。完整架构、后台资源、模型与人脸治理见视觉引擎。
var analyzed = await V8.Vision.Analyze({
FileByteBase64: V8.FilesByteBase64['frame'],
FileName: 'camera-frame.jpg',
PipelineKey: 'general-yolox-dinov2-v1',
Mode: 'General',
FrameId: 'scale-01-000123',
StreamSessionId: 'scale-01-session-42',
FrameSequence: 123
});
if (!analyzed || analyzed.Code !== 1) return analyzed;
var ranked = await V8.Vision.Search({
QueryEmbeddingBase64: analyzed.Data.Detections[0].EmbeddingBase64,
Candidates: [
{ Key: 'sample-id', Name: '鲤鱼', EmbeddingBase64: '...' }
],
Threshold: 0.82,
TopK: 5,
EfSearch: 80,
IndexKey: 'ready-general-samples'
});
var stable = await V8.Vision.Stabilize({
StreamSessionId: 'scale-01-session-42',
FrameId: 'scale-01-000123',
Key: ranked.Data.Matches.length ? ranked.Data.Matches[0].Key : '__unmatched__',
Confidence: ranked.Data.Matches.length ? ranked.Data.Matches[0].Similarity : 0,
WindowSize: 5,
MinimumVotes: 3
});| 方法 | 返回 | 说明 |
|---|---|---|
Analyze(param) | Promise<DosResult<MicroiVisionAnalyzeResult>> | 检测/分割、逐目标嵌入、Face 五点对齐与 TrackId;返回对象框而不暴露模型路径 |
Search(param) | Promise<DosResult<MicroiVisionSearchResult>> | HNSW Top-K 检索,索引按租户和候选指纹缓存,小集合精确回退 |
Stabilize(param) | Promise<DosResult<MicroiVisionStabilizeResult>> | 按租户、StreamSessionId 与 TrackId 执行有界连续帧投票 |
Extract(param) | Promise<DosResult<MicroiVisionExtractResult>> | 单张 JPEG/PNG/WebP 特征;包含模型、尺寸、质量、耗时、向量和警告 |
ExtractBatch(param) | Promise<DosResult<MicroiVisionFrameBatchResult>> | 有界帧批次;宿主默认最多 12 帧 |
Compare(param) | Promise<DosResult<MicroiVisionCompareResult>> | 两个同维向量的余弦相似度与是否达阈值 |
CompareBatch(param) | Promise<DosResult<MicroiVisionCompareBatchResult>> | 候选集排序、TopK 与命中标记 |
GetCapabilities() | DosResult<MicroiVisionCapabilitiesResult> | 模型 Key、版本、任务类型、许可证、就绪状态与硬上限 |
调用方不能传模型文件路径、模型目录、执行提供程序、网络地址或 OsClient。ModelKey/PipelineKey 只能解析到可信宿主模型目录中的单层安全名称,并校验 model.json 与每个 ONNX 文件 SHA-256。执行提供程序由宿主已安装运行时与受控描述符决定,支持 CPU、CUDA、TensorRT、OpenVINO、DirectML 的安全回退;调用方不能覆盖。Face 模式必须使用 FaceDetection + FaceEmbedding 流水线;内置指纹仅保留联调能力,不能用于生产身份判断。
一般业务不要直接把公开向量发到浏览器,而应调用官方 platform-vision-runtime:它从当前租户样本库读取候选,在数据库未命中时先返回 AiPending,再通过持久后台任务调用 Microi.AI。特征向量使用接口引擎绑定的保护能力保存,公开响应和租户 Hook 不包含向量、图片或 AI 提示词。
运行时动作包括 Recognize/Enroll/Result/Recent/Correct。Recent/Result 只可返回 HasImage 和由 V8.Method.GetPrivateFileUrl 生成的短期授权图片地址,禁止返回 InputFilePath 或图片 Base64。Correct 重新校验当前租户的对象与模式,保存原识别结果、修正人和原因;SaveAsSample=true 时按来源请求幂等录入。对于水产、生鲜和蔬菜,返回的冰冻状态、新鲜度与预估价格必须分别携带 Database/AI/Manual 来源,AI 估价不能直接作为门店正式售价或订单结算依据。
V8.Office
V8.Office 可在接口引擎中生成 Excel、Word、PowerPoint 文件。导出方法返回 DosResult<byte[]>,接口引擎需要开启【响应文件】,并把 Data 转成 Base64 返回。
标准表格导出的下载扩展名为 .xlsx。以前下载为 .xls、实际内容却为 XLSX 的平台文件,Excel 导入入口会在验证真实工作簿结构后兼容读取;真正的 .xls 也继续支持。任意 ZIP、HTML 和损坏文件仍会被拒绝,OnlyOffice 文件回源继续严格校验扩展名与格式。
| 方法 | 说明 |
|---|---|
ExportExcel({...}) | 导出 .xlsx,支持单/多 Sheet、标准表格、高级自由布局、图片、公式、合并、边框、打印和行分组 |
ExcelToList({...}) | 解析 Excel 或 CSV;CSV 自动识别 UTF-8/GBK 编码与常见分隔符,SheetIndex 从 0 开始 |
ExportWordText({...}) | 旧版纯文本 Word 导出,继续兼容 |
ExportWord({...}) | 导出 .docx,支持段落、章节、表格、图片、页眉页脚、页码 |
ExportPowerPoint({...}) | 导出 .pptx,支持多页、文本、项目符号、表格、图片、主题、页码 |
SendEmail({...}) | 发送 HTML 邮件 |
导出 Excel
ExportExcel 提供两种可在同一工作簿混用的模式:
- 标准表格模式:继续使用
ExcelData + ExcelHeader,适合数据列表、图片列和常规报表,完全兼容旧代码。 - 高级布局模式:使用
ExcelLayout按 A1 区域写入任意单元格、公式、样式和合并区域,适合审批单、套打表、主子表、多级表头、统计卡片及截图同款复杂版式。
需要控制工作表名称、列宽、行高、冻结窗格、筛选、打印和公共样式时传 ExcelOptions;标准表格中某一列的宽度、隐藏、数字格式或样式仍在 ExcelHeader 对应项中配置。
完整单 Sheet 示例:
var excelResult = V8.Office.ExportExcel({
OsClient: V8.OsClient,
ExcelData: dataList,
ExcelHeader: [
{
Name: 'Name', Label: '姓名', Component: 'Text',
Width: 18, MinWidth: 12, MaxWidth: 30,
HeaderStyle: { BackgroundColor: '17365D', FontColor: 'FFFFFF' },
Style: { WrapText: true, VerticalAlignment: 'Center' }
},
{
Name: 'Amount', Label: '金额', Component: 'NumberText', Type: 'decimal',
Width: 16,
NumberFormat: '#,##0.00',
Style: { HorizontalAlignment: 'Right' }
},
{
Name: 'Remark', Label: '备注', Component: 'Textarea',
AutoSize: true, MinWidth: 20, MaxWidth: 50,
Style: { WrapText: true }
}
],
ExcelOptions: {
SheetName: '销售明细',
DefaultColumnWidth: 14, // 未设置 Width 的列,单位为 Excel 字符宽度
HeaderRowHeight: 30, // 单位:磅(pt)
DataRowHeight: 24, // 单位:磅(pt)
FreezeHeader: true,
FreezeColumns: 1,
AutoFilter: true,
ShowGridLines: false,
HeaderStyle: {
FontName: 'Microsoft YaHei', FontSize: 11, Bold: true,
HorizontalAlignment: 'Center', VerticalAlignment: 'Center',
BorderStyle: 'Thin', BorderColor: 'B7C9D6'
},
CellStyle: {
FontName: 'Microsoft YaHei', FontSize: 10,
VerticalAlignment: 'Center'
}
}
});
if (excelResult.Code !== 1) return excelResult;
return {
Code: 1,
Data: {
FileName: '业务数据.xlsx',
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
FileByteBase64: System.Convert.ToBase64String(excelResult.Data)
}
};ExcelOptions 工作表配置
| 参数 | 类型 | 说明 |
|---|---|---|
SheetName | string | 单 Sheet 名称;多 Sheet 通常直接使用每项的 SheetName |
DefaultColumnWidth | number | 默认列宽,单位为 Excel 字符宽度;有效范围 0.1~255 |
DefaultRowHeight | number | 默认行高,单位为磅(pt);最大 409.5 |
HeaderRowHeight | number | 表头行高,单位为磅(pt);最大 409.5 |
DataRowHeight | number | 数据行高,单位为磅(pt);最大 409.5 |
FreezeHeader | boolean | 是否冻结首行 |
FreezeRows | number | 冻结顶部行数;传入后优先于 FreezeHeader |
FreezeColumns | number | 冻结左侧列数,例如 1 表示冻结首列 |
AutoFilter | boolean | 是否为表头及数据区域启用自动筛选 |
AutoFilterRange | string | 自定义筛选区域,如 A3:K20;传入后自动启用筛选 |
AutoSizeColumns | boolean | 是否自动计算所有列宽;大数据量导出建议使用固定 Width,性能更稳定 |
ShowGridLines | boolean | 是否显示工作表网格线 |
Zoom | number | 工作表缩放比例,范围 10~400 |
PrintOrientation | string | 打印方向:Portrait / Landscape |
PaperSize | string | 纸张:A3 / A4 / A5 / Letter / Legal,也可传 NPOI 数字代码 |
FitToWidth / FitToHeight | number | 打印时缩放为指定页宽/页高;常用 1 / 0 |
PrintArea | string | 打印区域,如 A1:K15 |
MarginTop/Right/Bottom/Left | number | 打印页边距,单位为英寸 |
CenterHorizontally/Vertically | boolean | 打印时水平/垂直居中 |
HeaderText / FooterText | string | 页眉/页脚文字 |
ShowPageNumber | boolean | 在页脚右侧显示“第 X 页 / 共 Y 页” |
HeaderStyle | object | 全局表头样式,列级 HeaderStyle 可覆盖 |
CellStyle | object | 全局数据单元格样式,列级 Style 可覆盖 |
ExcelHeader 列配置
| 参数 | 类型 | 说明 |
|---|---|---|
Name | string | 数据字段名,对应 ExcelData 每项的属性 |
Label | string | Excel 表头文字 |
Component / Config | string / object | 吾码字段组件与配置;图片、下拉、多选等按原组件规则导出 |
Type | string | int / decimal 等数值类型会生成 Excel 数值单元格 |
Width / ColumnWidth | number | 固定列宽,单位为 Excel 字符宽度;Width 为推荐写法 |
AutoSize | boolean | 单列自动宽度;开启后可配合 MinWidth / MaxWidth 限制范围 |
MinWidth / MaxWidth | number | 自动或固定列宽的下限/上限 |
Hidden | boolean | 是否隐藏该列,数据仍保留在文件中 |
HeaderHeight | number | 表头行高候选值;同一 Sheet 取所有列与 HeaderRowHeight 的最大值 |
RowHeight | number | 数据行高候选值;同一 Sheet 取所有列与 DataRowHeight 的最大值 |
NumberFormat | string | 数字/日期显示格式简写,如 #,##0.00、0.00%、yyyy-mm-dd |
HeaderStyle | object | 当前列表头样式 |
Style | object | 当前列数据单元格样式 |
HeaderStyle / Style 支持以下属性:
| 参数 | 说明 |
|---|---|
FontName、FontSize、FontColor | 字体、字号、字体颜色 |
Bold、Italic、Underline | 加粗、斜体、下划线 |
BackgroundColor | 背景色 |
HorizontalAlignment | Left / Center / Right / Justify 等 |
VerticalAlignment | Top / Center / Bottom 等 |
WrapText、ShrinkToFit、Rotation | 自动换行、缩小字体填充、文字旋转角度(-90~90) |
NumberFormat | 数字/日期格式;也可直接写在 ExcelHeader 项上 |
BorderStyle、BorderColor | 统一设置四边边框样式与颜色,例如 Thin、B7C9D6 |
BorderTop/Right/Bottom/LeftStyle | 分别设置上/右/下/左边框类型,可用 Thin、Medium、Dashed、Dotted、Double、DashDot 等 |
BorderTop/Right/Bottom/LeftColor | 分别设置四条边框颜色;未传时继承 BorderColor |
颜色支持 RRGGBB 或 #RRGGBB。列级样式会继承全局样式并覆盖同名属性。若同时启用 AutoSize 与固定 Width,以自动计算结果为准,再由 MinWidth / MaxWidth 限制。要让 NumberFormat 按数值或日期参与 Excel 计算,源数据应为数值/日期,并给数值列传 Type:'int' 或 Type:'decimal';显示格式不会把普通文本强制转换为数值。没有传任何新配置时,旧导出宽度、行高和样式行为保持不变。
ExcelLayout 高级自由布局
高级布局不要求每行具有相同字段。每个 Cells 项通过 Range 指定 A1 单元格或区域;样式作用于整个区域,Value / Formula 写入区域左上角,Merge:true 同时合并该区域。多个样式区域可以重叠,后写入的非空样式属性覆盖先前同名属性,因此可先给 A3:K15 统一画细网格,再给首尾行和左右列覆盖中粗外框。
var cells = [
// 整张申请表的细网格
{ Range: 'A1:H10', Style: {
FontName: 'Microsoft YaHei', FontSize: 10,
BorderStyle: 'Thin', BorderColor: '7F8C9A',
VerticalAlignment: 'Center', WrapText: true
}},
// 合并标题
{ Range: 'A1:H1', Value: '盘盈亏及报废申请表', Merge: true, Style: {
FontSize: 18, Bold: true, BackgroundColor: 'EEF2F7',
HorizontalAlignment: 'Center',
BorderTopStyle: 'Medium', BorderTopColor: '34495E'
}},
// 表头与一行数据
{ Range: 'A2', Value: '序号', Style: { Bold: true, HorizontalAlignment: 'Center' }},
{ Range: 'B2', Value: '物料编码', Style: { Bold: true, HorizontalAlignment: 'Center' }},
{ Range: 'G2', Value: '数量', Style: { Bold: true, HorizontalAlignment: 'Center' }},
{ Range: 'H2', Value: '金额', Style: { Bold: true, HorizontalAlignment: 'Center' }},
{ Range: 'A3', Value: 1 },
{ Range: 'B3', Value: 'V3-MAT-1200' },
{ Range: 'G3', Value: 2, DataType: 'Number' },
{ Range: 'H3', Formula: 'G3*1200', Style: { NumberFormat: '#,##0.00' }},
// 合计与审批意见
{ Range: 'A8:G8', Value: '合计', Merge: true, Style: { Bold: true, HorizontalAlignment: 'Right' }},
{ Range: 'H8', Formula: 'SUM(H3:H7)', Style: { Bold: true, NumberFormat: '#,##0.00' }},
{ Range: 'A9:D9', Value: '(1) 申请人:张三(已电子签)', Merge: true },
{ Range: 'E9:H9', Value: '(2) 主管意见:同意(已电子签)', Merge: true },
{ Range: 'A10:H10', Value: '备注:审批完成后原件交财务归档。', Merge: true }
];
var excelResult = V8.Office.ExportExcel({
OsClient: V8.OsClient,
ExcelSheets: [{
SheetName: '审批单',
ExcelLayout: {
Cells: cells,
// 也可集中传 MergedRanges: ['A1:H1', 'A8:G8']
Columns: [
{ Column: 'A', Width: 9 },
{ Column: 'B', Width: 22 },
{ Column: 'H', Width: 16 }
],
Rows: [{ Row: 1, Height: 42 }, { Row: 2, Height: 32 }],
// Excel 原生分组,适合主表/子表明细展开折叠
RowGroups: [{ StartRow: 3, EndRow: 7, Collapsed: false }]
},
ExcelOptions: {
ShowGridLines: false,
FreezeRows: 2,
FreezeColumns: 1,
AutoFilterRange: 'A2:H8',
PrintOrientation: 'Landscape',
PaperSize: 'A4',
FitToWidth: 1,
PrintArea: 'A1:H10',
ShowPageNumber: true
}
}]
});ExcelLayout 参数:
| 参数 | 说明 |
|---|---|
Cells | 单元格/区域列表;每项支持 Range、Value、Formula、DataType、Merge、Style |
MergedRanges | 额外合并区域列表,如 ['A1:K1','A10:F10'];重叠且不完全相同的合并区域会明确报错 |
Columns | 列配置;用 Column:'A' 或从 1 开始的 Index 定位,支持 Width/Hidden/AutoSize/MinWidth/MaxWidth |
Rows | 行配置;Row 从 1 开始,支持 Height/Hidden |
RowGroups | Excel 原生行分组;StartRow/EndRow 从 1 开始,Collapsed 控制初始折叠状态 |
DataType 可用 String、Number、Boolean、DateTime、Blank;不传时按值自动识别。公式可带或不带开头的 =,生成后会要求 Excel 重新计算。xlsx 限制为最大 1,048,576 行、16,384 列;布局区域越大,创建的单元格和样式越多,应只覆盖实际使用范围。
官方完整示例接口引擎为 export-excel-advanced-demo,一次返回 5 张 Sheet:截图同款盘盈亏报废申请、可折叠主子表、复杂多级合并表头、标准数据表、边框与卡片样式库。接口必须开启【响应文件】;示例地址:/apiengine/export-excel-advanced-demo--OsClient--iTdos--。
如需把接口地址直接发送给客户在线查看,使用配套匿名接口 export-excel-advanced-demo-preview。该接口也必须开启【响应文件】,直接访问会下载 .xlsx;/online-office 通过 fileUrl 接收它的完整地址,平台后端限域读取文件后透明缓存到当前租户公有 HDFS,再把可回源静态地址交给 OnlyOffice。接口引擎仍只响应文件,不需要返回 FileUrl/OnlineOfficePath JSON;响应文件动态路由同时支持 GET/HEAD。
/apiengine/export-excel-advanced-demo-preview--OsClient--iTdos--本地预览链接示例(fileUrl 已 URL 编码):
http://localhost:1988/?OsClient=iTdos#/online-office?fileUrl=https%3A%2F%2Flocalhost%3A7266%2Fapiengine%2Fexport-excel-advanced-demo-preview--OsClient--iTdos--&fileName=%E5%90%BE%E7%A0%81V8%E9%AB%98%E7%BA%A7Excel%E5%A4%9ASheet%E7%A4%BA%E4%BE%8B.xlsx&fileType=xlsx&canEdit=0匿名 fileUrl 只允许当前平台 ApiBase 下的 /apiengine/...,且必须显式包含当前 OsClient,禁止把任意外部 URL 交给 OnlyOffice。开发环境中的 localhost/127.0.0.1 仅允许由同端口本地后端读取;文件通过 SHA-256 确定性路径写入当前租户 office-preview 公有目录并共享缓存 10 分钟,限制 50MB、不跟随重定向、校验扩展名和文件头。未登录时即使传 canEdit=1 也强制只读,并隐藏系统菜单、顶部导航和页签;敏感数据不得开放匿名接口。
多 Sheet
外层 ExcelOptions 是全部 Sheet 的默认配置;每个 Sheet 可传自己的 ExcelOptions 覆盖局部属性。这样可以统一字体和默认宽度,同时单独控制每个页签的冻结、筛选和行高。
var excelResult = V8.Office.ExportExcel({
OsClient: V8.OsClient,
ExcelOptions: {
DefaultColumnWidth: 14,
HeaderRowHeight: 28,
HeaderStyle: { Bold: true, BackgroundColor: 'D9EAF7' }
},
ExcelSheets: [
{
SheetName: '订单',
ExcelData: orderList,
ExcelHeader: [
{ Name: 'OrderNo', Label: '订单号', Component: 'Text', Width: 22 },
{ Name: 'Amount', Label: '金额', Component: 'NumberText', Type: 'decimal', Width: 16, NumberFormat: '#,##0.00' }
],
ExcelOptions: { FreezeHeader: true, AutoFilter: true }
},
{
SheetName: '客户',
ExcelData: customerList,
ExcelHeader: [
{ Name: 'Name', Label: '客户名称', Component: 'Text', Width: 24 },
{ Name: 'Phone', Label: '联系电话', Component: 'Text', Width: 18 }
],
ExcelOptions: { DataRowHeight: 22 }
}
]
});
if (excelResult.Code !== 1) return excelResult;
return {
Code: 1,
Data: {
FileName: '订单与客户.xlsx',
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
FileByteBase64: System.Convert.ToBase64String(excelResult.Data)
}
};ExcelSheets 每项可使用 ExcelLayout 高级布局,也可使用 ExcelData + ExcelHeader 标准表格;标准表格还可传 FormEngineKey/TableId/_Where/_OrderBy/_PageSize 等查询参数。同一个工作簿可以混用两种模式。Sheets 是兼容别名,新代码使用 ExcelSheets。Sheet 名称中的非法字符、31 字符上限和重名会自动处理。
图片列说明:ImgUpload.Multiple=1 仍会按最大图片数展开为多列并合并表头;Width 会应用到展开后的每一列,DataRowHeight / RowHeight 控制图片所在行高度。私有文件仍按原规则输出受限值,不会绕过文件权限。自动列宽需要遍历单元格,大批量导出优先显式设置 Width,避免不必要的内存和 CPU 开销。
导出 Word
新代码使用对象参数的 ExportWord;ExportWordText 仅作为旧版纯文本接口继续保留。页面边距、图片宽高单位为厘米,字体大小单位为磅。
var wordResult = V8.Office.ExportWord({
Title: '月度经营报告',
Subtitle: DateNow('yyyy年MM月'),
Author: V8.CurrentUser.Name,
Subject: '经营分析',
Keywords: '经营,月报',
Description: '月度经营分析报告',
PageSize: 'A4', // A4 | Letter
Orientation: 'Portrait', // Portrait | Landscape
MarginTop: 2.2,
MarginRight: 2.0,
MarginBottom: 2.2,
MarginLeft: 2.0,
FontFamily: 'Microsoft YaHei',
FontSize: 10.5,
TitleFontSize: 20,
SubtitleFontSize: 12,
TitleAlignment: 'Center',
LineSpacing: 1.25,
ParagraphSpacingAfter: 6,
HeaderText: '吾码经营中心',
FooterText: '内部资料',
ShowPageNumber: true,
Paragraphs: [
{ Text: '本月经营情况总体稳定。', FirstLineIndent: 0.74 },
{ Text: '以下数据未经授权不得外传。', Bold: true, FontColor: 'C00000' }
],
Sections: [{
Heading: '一、核心指标',
HeadingLevel: 1,
Content: '本节展示主要经营指标。',
Tables: [{
Title: '指标明细',
Headers: ['指标', '本月', '同比'],
Rows: [['销售额', 1280000, '12.5%'], ['订单数', 860, '8.1%']],
ColumnWidths: [4, 4, 4],
HeaderBackgroundColor: 'D9EAF7',
BorderColor: 'B7C9D6'
}]
}],
Images: [{
FileByteBase64: chartBase64, // 纯 Base64 或 data URI
FileName: 'chart.png',
ContentType: 'image/png',
Width: 15,
Height: 8,
Alignment: 'Center',
Caption: '图 1:趋势分析'
}]
});
if (wordResult.Code !== 1) return wordResult;
return {
Code: 1,
Data: {
FileName: '月度经营报告.docx',
ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
FileByteBase64: System.Convert.ToBase64String(wordResult.Data)
}
};常用子参数:
| 对象 | 支持参数 |
|---|---|
Paragraphs[] | Text/Alignment/Bold/Italic/Underline/FontFamily/FontSize/FontColor/SpacingBefore/SpacingAfter/LineSpacing/FirstLineIndent/PageBreakBefore |
Sections[] | Heading/HeadingLevel/Content/Paragraphs/Tables/Images/PageBreakBefore |
Tables[] | Title/Headers/Rows/ColumnWidths/Alignment/HeaderBold/HeaderBackgroundColor/HeaderFontColor/BorderColor/FontSize |
Images[] | FileByteBase64/FileName/ContentType/Width/Height/Alignment/Caption |
导出 PowerPoint
幻灯片尺寸、图片/表格位置和宽高单位均为英寸;默认画布为 16:9(13.333 × 7.5)。
var pptResult = V8.Office.ExportPowerPoint({
Title: '季度经营汇报',
Author: V8.CurrentUser.Name,
Subject: '季度复盘',
Keywords: '经营,季度',
SlideWidth: 13.333,
SlideHeight: 7.5,
FontFamily: 'Microsoft YaHei',
BackgroundColor: 'FFFFFF',
TitleColor: '17365D',
TextColor: '222222',
TitleFontSize: 28,
BodyFontSize: 18,
ShowSlideNumber: true,
Slides: [
{
Layout: 'TitleSlide',
Title: '季度经营汇报',
Subtitle: DateNow('yyyy-MM-dd')
},
{
Layout: 'TitleAndContent',
Title: '核心结论',
Bullets: ['收入保持增长', '重点客户续约稳定'],
TextItems: [
{ Text: '风险:回款周期延长', Bullet: true, Level: 0, Bold: true, FontColor: 'C00000' }
],
Tables: [{
Headers: ['指标', '本期', '目标'],
Rows: [['销售额', '128万', '120万']],
X: 0.7, Y: 4.0, Width: 11.9, Height: 2.2,
HeaderBackgroundColor: '17365D'
}]
},
{
Title: '趋势图',
Images: [{
FileByteBase64: chartBase64,
FileName: 'trend.png',
ContentType: 'image/png',
X: 1.2, Y: 1.5, Width: 10.9, Height: 5.2
}]
}
]
});
if (pptResult.Code !== 1) return pptResult;
return {
Code: 1,
Data: {
FileName: '季度经营汇报.pptx',
ContentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
FileByteBase64: System.Convert.ToBase64String(pptResult.Data)
}
};| 对象 | 支持参数 |
|---|---|
| 顶层 | Title/Author/Subject/Keywords/Company/SlideWidth/SlideHeight/FontFamily/BackgroundColor/TitleColor/TextColor/TitleFontSize/BodyFontSize/ShowSlideNumber/Slides |
Slides[] | Layout/Title/Subtitle/Content/Bullets/TextItems/Images/Tables/BackgroundColor/TitleColor/TextColor/TitleFontSize/BodyFontSize |
TextItems[] | Text/Level/Bullet/Bold/Italic/FontSize/FontColor/Alignment |
Images[] | FileByteBase64/FileName/ContentType/X/Y/Width/Height |
Tables[] | Headers/Rows/ColumnWidths/X/Y/Width/Height/HeaderBackgroundColor/HeaderFontColor/CellBackgroundColor/CellFontColor/FontSize |
解析 Excel / CSV
var rows = V8.Office.ExcelToList({
FileByteBase64: excelBase64,
FileType: 'excel', // CSV 传 csv,或通过 FileName: 'data.csv' 自动判断
SheetIndex: 0, // 从 0 开始
HeaderStartRow: 5, // 其余行号均从 1 开始
HeaderEndRow: 6,
DataStartRow: 7,
DataEndRow: 1000,
Columns: [
{ ColumnIndex: 0, Name: 'CustomerName', Label: '客户名称' },
{ Column: 'C', Name: 'Phone', Label: '手机号' }
]
});
if (rows.Code !== 1) return rows;| 参数 | 说明 |
|---|---|
FileType/FileName | CSV 传 FileType:'csv' 或带 .csv 后缀的 FileName;Excel 省略即可。 |
Encoding/Delimiter | CSV 可选固定编码/分隔符;通常应省略,由服务端自动识别 UTF-8/GBK 与逗号、制表符、分号、竖线。 |
SheetIndex | 工作表索引,从 0 开始;省略时读取第一张。 |
HeaderStartRow/HeaderEndRow | 表头一基起止行,支持合并单元格和多级表头;省略时保持旧行为:首行为表头。 |
DataStartRow/DataEndRow | 数据一基起止行;省略时从表头下一行读到已用区域末尾。 |
Columns | 确认后的列映射。ColumnIndex 从 0 开始,也可传 Excel 列字母 Column;Name 为返回对象字段名。 |
MaxDataRows/MaxColumns | 解析上限;服务端仍强制不超过平台 50000 行、256 个有效映射列。 |
增强参数全部可省略,旧的首行表头模板继续兼容。传入增强范围后,每行额外返回 _ExcelRow 便于精确报错;数据行是否有效按所有已映射列判断,不再因 A 列为空而丢弃;公式单元格读取计算结果。CSV 解析成功时,DataAppend.FileType/Encoding/Delimiter 返回服务端实际识别结果,便于接口引擎与浏览器元数据交叉复核。
菜单配置【导入接口替换】时,统一弹层会在用户确认后上传原始文件,并同时传 V8.Param._ImportMetaJson、_ImportErrorPolicy 和 _ImportUniqueRulesJson。_ImportMetaJson v2.2 内也包含 ErrorPolicy/UpsertMode/UniqueRules。自定义接口应解析元数据,把相同范围传给 V8.Office.ExcelToList,从服务端原文件重新取数;不能直接信任浏览器预览,也不能退回固定首行表头:
var meta = JSON.parse(V8.Param._ImportMetaJson || '{}');
var errorPolicy = V8.Param._ImportErrorPolicy || meta.ErrorPolicy || 'RollbackAll';
if (errorPolicy !== 'RollbackAll' && errorPolicy !== 'ContinueOnError') {
return { Code: 0, Msg: '不支持的导入错误处理策略' };
}
var fileMap = V8.FilesByteBase64 || {};
var fileBase64 = Object.values(fileMap)[0];
if (!fileBase64) return { Code: 0, Msg: '请上传 Excel 文件' };
var parsed = V8.Office.ExcelToList({
FileByteBase64: fileBase64,
FileType: meta.FileType || V8.Param._ImportFileType,
FileName: V8.Param._ImportFileName,
SheetIndex: meta.SheetIndex == null ? 0 : meta.SheetIndex,
HeaderStartRow: meta.HeaderStartRow,
HeaderEndRow: meta.HeaderEndRow,
DataStartRow: meta.DataStartRow,
DataEndRow: meta.DataEndRow,
Columns: meta.Columns || []
});
if (parsed.Code !== 1) return parsed;
if (meta.FileType === 'csv' && parsed.DataAppend) {
if (meta.Encoding && meta.Encoding !== parsed.DataAppend.Encoding) {
return { Code: 0, Msg: 'CSV 编码复核不一致,请重新上传后确认预览。' };
}
if (meta.Delimiter && meta.Delimiter.replace('\t', '\\t') !== parsed.DataAppend.Delimiter) {
return { Code: 0, Msg: 'CSV 分隔符复核不一致,请重新上传后确认预览。' };
}
}
// 接下来仍须重做权限、字段、唯一性和状态校验。
// meta.UniqueRules / _ImportUniqueRulesJson 仅用于向用户说明,不能代替服务端权威字段配置。自定义接口的 Upsert 规则必须与通用导入一致:每个“单字段唯一”各自判断,全部“组合唯一”字段共同判断;任一完整规则命中同一 Id 则按 Id 修改,全部未命中则新增;不同规则命中不同 Id,或一条规则命中多条数据,必须把该行记为冲突。禁止把多个独立规则错误拼成同一个 AND 条件,也禁止按宽泛唯一条件一次更新多行。
RollbackAll:首个行错误返回Code:0,由接口引擎事务整体回滚,禁止手动Commit/Rollback。ContinueOnError:捕获行错误并继续处理下一行,最终返回Code:1,同时返回Added/Updated/Failed/Errors。若数据库异常已使当前事务不可继续,应先做整批预校验或改用平台允许的独立幂等行操作,不能声称已继续。
菜单【导入接口替换】与 V8.OpenImportDialog 后台接口引擎都必须遵守以上策略;区别只是前者使用 V8.FilesByteBase64 在服务端重读原文件,后者读取 _ImportRowsJson 的确认后映射行。
保留标准导入、仅增加事务内整批校验
业务只需要增加跨行汇总、额度或并发校验时,不必替换整套导入接口。将模块 ImportV8 配置为 ApiEngine:<ApiEngineKey>,标准 /api/FormEngine/ImportDiyTableRow 会继续负责权限、原文件复核、字段映射、唯一规则、进度和写入,并在 RollbackAll 的同一数据库事务内、实际写入前调用指定接口引擎。
校验接口通常只返回 { Code: 1 }。支持安全回填的平台会在 V8.Param.Capabilities.PreflightFixedValuesV1 传入 true;接口确认该能力后,如果能依据当前用户有权访问的权威数据,唯一推导出整批共同缺失的业务字段,可以返回 { Code: 1, Data: { FixedValues: { ProjectId: "..." } } }。平台会再次校验:字段必须是当前表的可导入业务字段,值必须是非空标量,且不得设置 Id、审计、租户等平台字段,也不得覆盖 TableChild 已固定的父记录上下文;通过后该值才会用于本批全部写入。无法唯一推导时应返回 Code != 1,并在 Msg 中说明缺失信息和可操作的解决方法。
接口引擎从 V8.Param.Rows 读取服务端规范化后的整批行,从 V8.Param.FixedValues 读取父表固定值;另有 TableId/TableName/SysMenuId/ImportErrorPolicy/ImportFileName/ImportIdempotencyKey/UniqueRuleCount/Capabilities。此处的 V8.DbTrans 与后续标准写入共用事务,适合先按稳定顺序锁定额度行,再重新汇总并校验。接口以 Code:1 放行(可选返回上述 Data.FixedValues)或以 Code!=1 拒绝,不应自行写入、Commit 或 Rollback。
启用该钩子后只允许 RollbackAll。TableChild 内导入时,平台固定父记录外键覆盖 Excel 同名列,避免把数据写到其它父记录。客户端每次重新选择文件生成新的 _ImportIdempotencyKey,同一 HTTP 重试沿用该键;服务端按租户、表、菜单和用户隔离请求状态,避免重复创建写入任务。它不负责判断“相同文件以一个全新请求再次上传”是否属于业务重复。
发送邮件 SendEmail
return V8.Office.SendEmail({
SmtpServer : 'smtp.qq.com',
SmtpPort : 587,
EnableSSL : true,
SystemEmail : 'admin@itdos.com',
SystemEmailPwd : 'uuzrnazvv*******',
EmailSubject : '测试接口引擎发邮件标题',
EmailBody : '<b>测试接口引擎发邮件内容,<span style="color:red;">支持html</span></b>',
Receivers : ['123446172@qq.com', '973702@qq.com']
});邮箱协议 V8.Email
邮箱业务通过接口引擎 mci-email 编排,详见 邮箱系统。 V8.Email 只提供租户凭据加密、IMAP、SMTP 和 MIME 协议原子,不创建第二套 HTTP 接口。
| 方法 | 用途 |
|---|---|
ProtectCredential(value) | 将授权码加密为当前租户密文;没有对 V8 开放的解密方法 |
TestConnection(p) | 检测 IMAP / SMTP 的 TLS 连接与账号认证 |
ListFolders(p) | 读取目录路径、类型和服务端计数 |
Fetch(p) | 分页读取信封,支持 UID 游标、UIDVALIDITY 重置与最新邮件优先 |
Inspect(p) | 最多核对 100 个 UID 的存在、已读、星标和删除状态 |
GetMessage(p) / GetAttachment(p) | 按需读取正文或一个附件,不加载远程图片 |
SetFlags(p) / Move(p) | 修改单封邮件标记或移动到目标目录 |
Send(p) | 发送 MIME 邮件,返回 Accepted / Rejected / Unknown 投递状态 |
StoreSent(p) | 按 Message-Id 查重并保存远端已发送副本,独立于 SMTP 投递 |
连接参数为 Host, Port, Security, UserName, Credential, Timeout。 Credential 必须为当前租户密文,Security 仅支持 SslOnConnect 或 StartTls。 邮件参数包括 To, Cc, Bcc, Subject, TextBody, HtmlBody, InReplyTo, MessageId, Attachments。 附件为 { FileName, ContentType, FileByteBase64 },最多 10 个,总大小不超过 10MB。
读取时传 Folder, UidValidity, Uid;分页传 AfterUid, Limit(最高 100)。 Recent=true 先读取最新一批而保留历史游标;GetAttachment 另传从 0 开始的 AttachmentIndex。 目录路径不可自行改写,UID 必须与 UIDVALIDITY 一起持久化。
发信前由接口引擎保存稳定草稿 Id、Message-Id 与共享去重状态。 Accepted 代表 SMTP 接受,不代表收件人已经阅读;Unknown 必须先核对远端目录,禁止自动重发。 普通调用优先使用邮箱业务接口,避免遗漏权限、状态机和发送幂等。
系统设置 V8.SysConfig
- 后端接口引擎与后端 V8 事件访问当前租户完整系统配置;返回值是独立副本,脚本修改不会写回缓存或数据库。
sys_config的全部字段位于根对象;mci_system_setting的全部启用设置位于独立的ServerPrivateSettings节点,Secret 在可信后端按当前租户解密。不存在PublicSettings属性。V8.FormEngine.GetSysConfig(...)强制绑定当前 V8 租户,显式传其它OsClient也不能跨租户读取。
var sysTitle = V8.SysConfig.SysTitle;
var privateSettings = V8.SysConfig.ServerPrivateSettings || {};
var loginName = privateSettings['Login.Gitee.Name'];
var clientSecret = privateSettings['Login.Gitee.ClientSecret'];
// clientSecret 只能用于当前后端逻辑,禁止 return 或写日志。匿名 FormEngine/GetSysConfig 与前端 V8 使用另一条公开投影:只返回 sys_config 的浏览器安全字段,整个 ServerPrivateSettings 节点永远不进入浏览器。后端 V8 不需要查询 mci_system_setting.SecretCipher 或自行解密,直接按 Key 读取 V8.SysConfig.ServerPrivateSettings;任何 Secret 都不得返回客户端、写日志、写审计或保存到前端可读字段。
SaaS引擎信息 V8.OsClientModel / V8.ClientModel
- 两者是当前租户 SaaS 配置的独立脱敏副本,脚本修改不会写回服务端运行配置。
- 数据库连接、鉴权密钥以及共享 Redis、对象存储、RabbitMQ、MQTT、Search 的地址、账号和密码不会注入 V8;即使接口错误地
return V8.ClientModel,也不会泄露主库基础设施凭据。sys_osclients自定义业务字段只作为存量兼容;新增公开配置使用当前租户库的sys_config实体字段,新增私密业务设置使用mci_system_setting。后端通过V8.SysConfig.ServerPrivateSettings使用私密值与 Secret;这些设置不通过V8.OsClientModel暴露。- 存储类型
HDFS与公开文件域名可以读取;访问缓存、文件、MQ、MQTT 和 Search 必须使用对应的V8.*受控能力,服务端自动添加当前租户命名空间。
var title = V8.OsClientModel.SysTitle;
var storageType = V8.ClientModel.HDFS; // ClientModel 是兼容别名
// 以下字段为 undefined,不再暴露:
// V8.ClientModel.DbConn / RedisPwd / MinIOSecretKey / MQPassword / MqttPwd / SearchEngineApiKey共享基础设施可以复用同一 Redis、对象存储、RabbitMQ Broker、MQTT Broker 和搜索集群,但隔离边界由服务端强制执行:缓存 Key、对象路径、队列、Topic、索引分别绑定当前 OsClient。RabbitMQ、MQTT 和 Search 还必须为子租户配置独立凭据;缺少独立凭据时对应能力失败关闭,不会回退使用主租户账号。
表单数据 V8.Form
- 表单提交事件中可访问表单数据,接口引擎中此对象为空。
V8.OldForm
- 在修改数据时,后端V8事件可访问到V8.OldForm修改前的数据值
原生删除事件中,平台将删除前的数据库记录放入 V8.Form,V8.OldForm 可能为空。删除审计或关联清理应使用 V8.Form;修改事件继续使用 V8.OldForm。删除上下文中的版本字段是数据库当前快照,不能据此声称已校验客户端期望版本。业务要求防止过期删除时,应由受控接口接收期望版本、锁定并校验主库记录,然后在同一事务删除。
V8.FormSubmitAction
- 表单提交类型:可能的值:
InsertDeleteUpdate(string类型)- 注意服务器端V8事件里面没有
FormOutAction、FormOutAfterAction,只有FormSubmitAction
V8.EventName
- 后端V8事件名称,在全局V8引擎代码中比较好用,可能的值:
FormSubmitBefore:表单提交前V8事件
FormSubmitAfter:表单提交后V8事件
DataFilter:数据处理V8事件
WFNodeLine:流程节点条件判断V8事件
WFNodeEnd:流程节点结束V8事件
WFNodeStart:流程节点开始V8事件V8.Param
- 用于访问前端传入的参数,能访问到url参数、form-data参数、payload-json参数
V8.Action
- 用于访问在全局服务器V8代码处自定义的方法
V8.InvokeType
- 访问当前调用类型,可能的值:
Server、Client,当访问到的V8.InvokeType为空时,则默认ServerServer:服务器端调用,如在接口引擎中调用接口引擎,在后端V8事件中调用接口引擎Client:前端调用,如在前端V8事件中调用接口引擎,在前端提交表单
V8.TableModel
- 在后端V8事件中,可访问到操作的当前
diy_table表的信息
V8.OsClient
- 访问当前的OsClient值
其它后端能力与支持边界
后端会把多个扩展同时注册为 V8.* 和兼容全局对象。新代码统一使用 V8.*,不要依赖全局别名,避免与 JavaScript、CLR 或其它扩展同名。
| 能力 | 推荐入口 | 适用范围与安全边界 |
|---|---|---|
| 数据源引擎 | V8.DataSourceEngine.Run/RunAsync | 只运行当前租户数据源;动态 SQL、远程连接和返回字段仍需按业务授权 |
| 模块引擎 | V8.ModuleEngine | 读取当前用户可见模块模型;不能代替 FormEngine 的数据权限 |
| 工作流引擎 | V8.WFEngine、事件中的 V8.WF | 启动、发送、撤回、取消等动作必须校验当前用户、当前租户、流程状态与表单范围 |
| 翻译引擎 | V8.TranslateEngine.Translate/TranslateText/Detect/GetLanguages/TranslateFile/Suggest/Health/GetLang... | 使用当前租户的语言与供应商配置,不能传其它租户、endpoint 或密钥;批量/文件受硬上限保护 |
| OCR | V8.OCR.Recognize | 只使用当前租户服务端配置;调用方不能覆盖 endpoint、密钥、Header 或 OsClient;批量任务需要共享持久状态与幂等 |
| 文件能力 | V8.HDFS、V8.Method.Upload/GetPrivateFileUrl | 普通业务优先使用受控上传和短期代理;列举、删除、读取私有字节属于可信管理能力 |
| 消息能力 | V8.MQ.SendMsg | 队列名绑定当前租户;消费、关闭通道等属于 Worker 内部能力,消息必须有全局 EventId 和幂等消费 |
| 短信 | V8.Sms.Send | 供应商配置必须脱敏,发送接口要有频率、金额/条数、模板和收件人限制 |
| 爬虫 | V8.Spider | 属于高风险 Worker 能力;必须使用租户目标地址策略、租户/用户会话隔离、并发和运行时限,禁止脚本指定浏览器可执行文件 |
| 主机监控 | V8.System | CPU、内存、磁盘、网络等运维数据仅供管理员/运维,不应从普通或匿名接口返回 |
| 任务调度 | V8.Method.SaveScheduleJob、V8.Method.ManageScheduleJob | 仅当前租户超级管理员;保存只允许接口引擎任务。表单先检查 ManageScheduleJob({Action:'Capabilities'}) 的 RuntimeOnly 能力,再直接调用 SaveScheduleJob 并传 RuntimeOnly:true:仅同步 Quartz 并回读状态,由原表单事务写元数据。直接调用可保留业务 ApiEngineKey,避免嵌套 V8.ApiEngine.Run 将同名路由参数改成管理接口 Key;默认保存仍写完整任务。不能用 HTTP 回调本平台或在提交事件中再次写当前表 |
| 支付、微信、DNS | 对应 V8.* 扩展 | 单独校验签名、幂等键、回调重放、金额与租户凭据,不要返回密钥 |
动态建表、动态字段、数据库备份/清空、缓存连接管理、接口引擎代码写入等属于控制面能力。即使某个低层方法在 V8 对象上可见,也不等于普通业务脚本可以安全暴露;控制面 HTTP API 还会独立执行 Level >= 9999 管理员门禁。
执行上下文与资源限制
常见上下文包括 V8.Param、V8.Header、V8.CurrentUser、V8.OsClient、V8.Form、V8.OldForm、V8.TableModel、V8.TableData、V8.FormSubmitAction、V8.EventName、V8.InvokeType、V8.RowIndex、V8.CacheData、V8.NotSaveField、V8.LineValue、V8.NextNodeId、V8.FilesByteBase64 和 V8.WF。Engine、HttpContext、执行租约等宿主对象属于内部实现,不要保存到静态变量、缓存或延迟回调。
平台的 SecurityGuard、PressureGuard、V8Limits、OrmLimits、StartupLimits 以及 V8 并发门共同保护单次脚本和单节点资源。V8 的默认/最大超时、语句数、单层累计分配、调用树累计分配、JavaScript 递归、接口嵌套深度和并发等待可在 sys_config 的开发配置中查看;接口引擎与表后端事件分别默认 sys_apiengine.V8Limit=0、diy_table.V8Limit=0,不设置 Jint 单次预算,只有打开 V8运行限制 后才应用 Timeout/MaxStatements/LimitMemory/LimitRecursion。进程内并发门不是集群级配额或分布式锁;多节点副作用仍必须依赖 Redis/数据库租约、幂等键、唯一约束、状态机或 outbox/inbox。
脚本应主动控制:
- SQL 页大小、字段数、循环次数和返回体大小;
- HTTP 超时、响应大小与目标地址;
- 图片、Office、ZIP 的文件数、解压体积和托管内存;
- MQ/短信/支付等外部副作用的幂等与重试;
- 日志内容的脱敏、限长与关联 ID。
完整的部署与安全基线见 平台安全总览。
console
- Microi.net.dll从v3.5.1开始支持console往服务器端输出日志
console.log('日志输出');
console.error('日志输出');
console.warn('日志输出');
console.info('日志输出');
//服务端查看日志
docker logs microi-api