---
title: "服务端 SDK"
description: "使用公开服务端接口借用宿主资源、校验作用域并处理业务错误。"
canonical_url: "https://docs.mcent.cn/qicore/server-sdk"
---
# 服务端 SDK

> 使用公开服务端接口借用宿主资源、校验作用域并处理业务错误。

服务端 SDK 面向受信应用和插件。它提供宿主公共能力的接口与类型声明，实际服务由宿主装配时注入。使用与目标宿主匹配的 SDK kit；包版本、装配协议和业务应用版本分别管理。

## 入口与版本

<table>
<thead>
  <tr>
    <th>
      标识
    </th>
    
    <th>
      当前含义
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        @qicore/application-sdk/server
      </code>
    </td>
    
    <td>
      应用构建时引用的公共服务端 facade
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        serverSdk.version
      </code>
    </td>
    
    <td>
      装配协议版本，当前为 <code>
        1
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        serverSdk.apiVersion
      </code>
    </td>
    
    <td>
      公共服务 API 版本，当前为 <code>
        1.0
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        serverSdk.api
      </code>
    </td>
    
    <td>
      宿主实际提供的公共类、函数和常量集合
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        @qicore/application-sdk/contracts
      </code>
    </td>
    
    <td>
      描述、发行清单及包布局校验
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        @qicore/application-sdk/package-tools
      </code>
    </td>
    
    <td>
      应用入口包装工具
    </td>
  </tr>
</tbody>
</table>

SDK 的 npm 包版本不等于 `serverSdk.version`。应用自己的 `version` 也不跟随框架版本自动变化。

## 构建时外置，运行时注入

业务源码可以这样引用公共能力：

```js
import { Fault, TenantApplications } from '@qicore/application-sdk/server'

export async function readSummary(db, session, tenantId) {
  const applications = new TenantApplications(db)
  await applications.admin(session, tenantId, 'notes-app', 'notes.read')
  return { tenantId, applicationKey: 'notes-app', ready: true }
}
```

将这个 specifier 外置，不能把 facade 或框架实现复制进业务 bundle。下面演示应用自己的 CommonJS bundle 如何包装成宿主可装配入口：

```js
import { build } from 'esbuild'
import { writeFile } from 'node:fs/promises'
import { applicationEntry } from '@qicore/application-sdk/package-tools'

const result = await build({
  entryPoints: ['apps/api/src/application.mjs'],
  bundle: true,
  write: false,
  platform: 'node',
  target: 'node24',
  format: 'cjs',
  external: ['@qicore/application-sdk/server']
})
await writeFile('entry.mjs', applicationEntry(result.outputFiles[0].text))
```

此例的 `apps/api/src/application.mjs` 导出应用的 `format`、`version`、`definition` 和 `createRuntime`；`applicationEntry()` 负责生成外层 `createApplication(serverSdk)`。路径与[推荐开发结构](https://docs.mcent.cn/qicore/application-development#%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E5%B7%A5%E7%A8%8B%E6%8E%A8%E8%8D%90%E7%BB%93%E6%9E%84)一致，其他布局须同步调整构建入口。已有工厂模板的应用可按[最小服务端开发闭环](https://docs.mcent.cn/qicore/development-workspace#%E6%9C%80%E5%B0%8F%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%BC%80%E5%8F%91%E9%97%AD%E7%8E%AF)直接复制装配，不重复包装。

直接用普通 Node 进程导入 SDK 的 `server` 入口会抛出 `QICORE_SERVER_HOST_REQUIRED`。这是运行方式检查；类型声明可用于开发，真实公共服务必须来自宿主。

## 共享资源与依赖

`createApplication(serverSdk)` 和 `createRuntime(resources)` 承担不同职责：

<table>
<thead>
  <tr>
    <th>
      位置
    </th>
    
    <th>
      使用方式
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        createApplication(serverSdk)
      </code>
    </td>
    
    <td>
      检查协议，取得共享 API，返回应用定义与运行时工厂
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        createRuntime({ db, storage })
      </code>
    </td>
    
    <td>
      使用宿主数据库和已绑定应用身份的存储，创建应用自己的服务
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        connect(services)
      </code>
    </td>
    
    <td>
      接收宿主连接的支付、订阅等公共服务
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        close()
      </code>
    </td>
    
    <td>
      释放应用自有资源，保留宿主共享数据库的生命周期
    </td>
  </tr>
</tbody>
</table>

不要自行创建第二套宿主身份服务、授权状态或钱包实现。应用自有生产依赖放进自己的发行包；不能依赖开发目录中偶然存在的相邻依赖。

`serverSdk.modules` 保留共享模块兼容映射。新业务源码使用公开 `server` 入口，不新增对框架内部模块路径的依赖，也不重新绑定宿主授权策略。

## 应用与权限接口

先从可信会话取得目标范围，再调用公共服务。路径中的租户编号只是请求目标。

<table>
<thead>
  <tr>
    <th>
      方法
    </th>
    
    <th>
      参数与结果
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        new TenantApplications(db)
      </code>
    </td>
    
    <td>
      借用宿主数据库客户端
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        available(tenantId, key, db?, lock?, session?)
      </code>
    </td>
    
    <td>
      检查租户状态、应用当前绑定及权益，返回可用绑定
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        admin(session, tenantId, key, permission, db?, lock?)
      </code>
    </td>
    
    <td>
      检查租户后台身份、站点权限和应用权限，返回绑定
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        featureContext(db, session, tenantId, key, feature, permission)
      </code>
    </td>
    
    <td>
      为公共能力选择当前可用且有权访问的应用来源
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        transaction(fn)
      </code>
    </td>
    
    <td>
      以宿主事务调用 <code>
        fn(tx)
      </code>
      
      ，只重试被识别的事务冲突
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        config(session, tenantId, key)
      </code>
    </td>
    
    <td>
      返回 <code>
        id/applicationKey/config/version
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        updateConfig(session, tenantId, key, input)
      </code>
    </td>
    
    <td>
      接受 <code>
        { config, version }
      </code>
      
      ，返回递增后的配置版本
    </td>
  </tr>
</tbody>
</table>

`permission` 传应用内名称，例如 `notes.read`；宿主核验带应用前缀的授权。不要把菜单可见、应用已加载或目录中存在该应用当作授权成功。

`available()` 检查应用可用性，不能代替后台账号权限检查。消费者会话则由宿主 HTTP 入口核验；读取用户数据时仍须保留完整的 `tenantId/applicationKey/userId`。

## 资料与资产读取

<table>
<thead>
  <tr>
    <th>
      接口
    </th>
    
    <th>
      作用
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        new ConsumerProfiles(db, storage?)
      </code>
    </td>
    
    <td>
      宿主公共资料服务，头像存储参数须满足下述契约
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ConsumerProfiles.view(session)
      </code>
    </td>
    
    <td>
      读取当前消费者资料与默认头像
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ConsumerProfiles.save(session, input)
      </code>
    </td>
    
    <td>
      由公共资料服务按版本更新字段并规范化头像
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        consumerIdentities(db, tenantId, userIds, applicationKey)
      </code>
    </td>
    
    <td>
      按明确应用批量查询展示身份，返回 Map
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        new Wallet(db)
      </code>
    </td>
    
    <td>
      公共积分、余额及来源流水服务
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Wallet.read(session)
      </code>
    </td>
    
    <td>
      读取 <code>
        points
      </code>
      
      、<code>
        balance
      </code>
      
       的金额、版本和单位
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Wallet.consumerRecords(session, asset, query)
      </code>
    </td>
    
    <td>
      读取当前消费者某类资产的分页流水
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Wallet.consumerRecord(session, asset, id)
      </code>
    </td>
    
    <td>
      在当前账户范围内读取单条流水
    </td>
  </tr>
</tbody>
</table>

`ConsumerProfiles` 的头像存储依赖需要提供 `imageReference(value, siteId)` 和 `saveImage(bytes, mime, { siteId })`。`createRuntime()` 获得的应用绑定存储提供 `uploadTarget/put/get/remove` 等应用文件接口，不包含这两个头像方法，不能直接作为构造参数传给资料服务更新头像。

应用更新公共资料和头像应使用 `GET/PUT /api/v1/apps/:applicationKey/profile`，或通过客户端 SDK 的请求封装调用该接口，由宿主统一处理头像。不要用应用绑定存储自行组装公共资料更新服务。

其他服务端调用传入宿主提供的内部会话对象，不从请求体组装 `session`。消费者 HTTP 登录返回的原始 token 由客户端发送；内部会话如何保存由宿主管理，应用不自行转换或伪造。

钱包变更、存储期限及事务顺序见[存储与事务](https://docs.mcent.cn/qicore/storage-and-transactions)。

## 可选公共能力适配器

能力对象放在 `definition` 中；业务支付回调放在运行时的 `paymentAdapter` 中。它们不是 HTTP 请求体或 `application.json` 顶层字段。以下 `db` 为调用方数据库或事务客户端，`s` 为可信会话，`q` 为已解析查询。

应用在闭包中固定自己的业务 key；`q.applicationKey` 只是可选目标。查询、计数、关联和回执都限定实际租户、应用与用户，不能仅靠客户端传入的编号隔离数据。

### 用户目录与应用资产

<table>
<thead>
  <tr>
    <th>
      定义项与方法
    </th>
    
    <th>
      当前参数与返回约定
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        consumers.list(db, tenantId, q, permissions)
      </code>
    </td>
    
    <td>
      返回 <code>
        { items, total, page, pageSize }
      </code>
      
      ；每项含 <code>
        userId
      </code>
      
       和展示身份 <code>
        user: { id, nickname, avatar }
      </code>
      
      ，按公共界面补充资料与业务统计
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        consumers.detail(db, tenantId, userId, permissions)
      </code>
    </td>
    
    <td>
      返回同一范围内的用户详情及展示身份，不存在时返回 <code>
        null
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        consumerAssets.summaries(db, tenantId, userIds)
      </code>
    </td>
    
    <td>
      返回以 userId 为键的 Map，值为相应用户的应用资产及会员摘要
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        consumerAssets.read(db, tenantId, userId)
      </code>
    </td>
    
    <td>
      返回 <code>
        { points, memberPoints, membership, version }
      </code>
      
      ；membership 含 <code>
        active/expiresAt
      </code>
      
      ，version 为 64 个十六进制字符组成的资产摘要
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        consumerAssets.apply(db, tenantId, userId, operationId, input)
      </code>
    </td>
    
    <td>
      在传入事务中返回 <code>
        { before, after, grantId, snapshot }
      </code>
      
      ；before/after 为资产 DTO，snapshot 记录可审计的业务来源分配
    </td>
  </tr>
</tbody>
</table>

用户目录查询支持关键字、等级、标签及分页，`permissions` 控制业务字段展示；宿主某些存在性检查会传空数组。结果不夹带密码散列、密钥或其他范围的用户。

`consumerAssets` 同时依赖用户目录供宿主核对归属。`apply()` 仍由应用比较资产摘要版本、执行权益规则；operationId 是公共层生成的操作编号，不替换原请求编号。公共层负责请求摘要去重、操作记录和审计，适配器使用传入 db 原子变更，不另开事务。

### 套餐配置

<table>
<thead>
  <tr>
    <th>
      <code>
        packages
      </code>
      
       项
    </th>
    
    <th>
      当前调用与责任
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        catalog
      </code>
    </td>
    
    <td>
      <code>
        { currency, maxPriceCents, maxPackages, types }
      </code>
      
      ；类型含 <code>
        key/label/fields
      </code>
      
      ，字段含 <code>
        key/label/unit/min/max
      </code>
      
       和可选 optional
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        items(config)
      </code>
    </td>
    
    <td>
      同步返回完整套餐数组，公共层再筛选和分页
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        normalize(input)
      </code>
    </td>
    
    <td>
      同步返回校验后的套餐输入
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        save(config, id, input)
      </code>
    </td>
    
    <td>
      修改传入配置副本，公共层会等待此调用
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        setEnabled(config, id, enabled)
      </code>
    </td>
    
    <td>
      同步修改配置副本中的启用状态
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        remove(config, ids)
      </code>
    </td>
    
    <td>
      同步从配置副本移除指定套餐
    </td>
  </tr>
</tbody>
</table>

公共套餐 DTO 使用 `id/name/type/priceCents/currency/enabled/benefits`，并提供界面使用的 `image/badge/originalPriceCents`。benefits 按 catalog 字段保存整数或允许的 null；售价单位为分。

回调只修改应用配置副本，不自行落库整份租户配置。公共层执行 configVersion 条件更新与审计；这些回调不在事务内执行不可回滚的外部请求。

### 权益与流水读取

`entitlements`、`entitlementLedger`、`pointsLedger`、`transactions` 共用以下只读协议：

<table>
<thead>
  <tr>
    <th>
      项与方法
    </th>
    
    <th>
      返回约定
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        catalog
      </code>
    </td>
    
    <td>
      types/statuses 为 <code>
        { key, label }[]
      </code>
      
      ；交易可提供同结构 providers
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list(db, tenantId, q, permissions)
      </code>
    </td>
    
    <td>
      返回 <code>
        { items, total, page, pageSize }
      </code>
      
      ，筛选和计数使用同一范围
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        detail(db, tenantId, id, q, permissions)
      </code>
    </td>
    
    <td>
      返回该范围的详情或 null；q 带详情分页参数
    </td>
  </tr>
</tbody>
</table>

q 带 `q/type/status/provider/page/pageSize` 及日期范围；宿主提供解析后的 start/end，结束边界为次日开始。记录提供 `id/userId/user/name/type/typeLabel/status/unit`，并按记录类别提供数量、金额变动、发生时间、来源和详情分页，字段对齐匹配 SDK 的公共记录组件。读取回调不扣款、删除流水或重算权益。

### 订阅消息

<table>
<thead>
  <tr>
    <th>
      <code>
        subscriptions
      </code>
      
       项与方法
    </th>
    
    <th>
      当前调用与返回
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        events
      </code>
    </td>
    
    <td>
      场景数组，项含 key/label；配置按场景 key 匹配
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fields
      </code>
    </td>
    
    <td>
      业务字段数组，项含 value/label/type；type 与模板字段种类匹配
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        target(db, s, targetId)
      </code>
    </td>
    
    <td>
      核验消费者和业务目标，返回 <code>
        { job, attempt, terminal }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        event(job)
      </code>
    </td>
    
    <td>
      返回 <code>
        tenantId/applicationKey/userId/targetId/attempt/event/values/page
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        titleScore(eventKey, title)
      </code>
    </td>
    
    <td>
      同步返回模板匹配分数；大于 0 表示候选，越高越优先
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sourceFor(eventKey, label, fieldType)
      </code>
    </td>
    
    <td>
      同步返回匹配的 fields.value，无法确定时返回空值
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        statusValue(eventKey)
      </code>
    </td>
    
    <td>
      同步返回场景状态文本，供带枚举限制的模板核对
    </td>
  </tr>
</tbody>
</table>

自动模板匹配启用时提供后三个方法，未知字段不猜测。values 按 fields.value 提供合法字符串。业务完成时可在共享事务中将 `event(job)` 交给已连接的 `subscriptions.enqueue(db, event)`；公共层处理同意记录、模板快照、投递和重试，应用不伪造微信发送成功回执。

### 支付订单回调

`definition.payments: true` 只是能力声明。宿主将运行时 `paymentAdapter` 与公共充值适配器组合，实际调用业务回调如下：

<table>
<thead>
  <tr>
    <th>
      <code>
        paymentAdapter
      </code>
      
       方法
    </th>
    
    <th>
      当前调用与责任
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        lock(db, tenantId, orderId)
      </code>
    </td>
    
    <td>
      锁定本应用、该租户的真实订单，返回 <code>
        id/userId/amount/currency/status
      </code>
      
      ；支付描述使用 description，消费者隐藏状态使用 consumerDeletedAt
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        payable(db, order)
      </code>
    </td>
    
    <td>
      校验当前可支付条件，不满足时抛业务错误
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        settle(db, order, payment)
      </code>
    </td>
    
    <td>
      同事务履约；true 表示需要补偿，false 表示正常履约
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cancel(db, order)
      </code>
    </td>
    
    <td>
      公共执行器确认可关单后更新业务状态
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reserveRefund(db, order)
      </code>
    </td>
    
    <td>
      核验退款资格、冻结或回收对应权益，不自行调用渠道退款
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        refunded(db, order, full)
      </code>
    </td>
    
    <td>
      公共层确认适用退款成功后更新业务状态，full 表示累计是否全退
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        orders(db, s, q)
      </code>
    </td>
    
    <td>
      返回本人范围的 <code>
        { items, total, page, pageSize }
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ordersWithRecharge(db, s, q, recharge)
      </code>
    </td>
    
    <td>
      可选；提供时取代 orders，应用显式合并公共充值与业务订单后统一排序、计数和分页
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        balancePayable(db, order)
      </code>
    </td>
    
    <td>
      可选；余额准备及支付方式查询时追加业务限制
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        afterSettlement(db, order, payment)
      </code>
    </td>
    
    <td>
      可选；仅在正常履约后执行同事务的附加处理
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        refundPreview(db, payment)
      </code>
    </td>
    
    <td>
      可选；返回 orderName/orderKind/refundEffect/refundUnavailableReason 供退款界面展示
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        removeFromConsumer(db, order)
      </code>
    </td>
    
    <td>
      可选；公共层允许消费者删除时隐藏其订单展示，保留原交易及资金证据
    </td>
  </tr>
</tbody>
</table>

组合层提供 `authorize(db, s)`，执行公共消费者会话、应用及钱包范围检查，并覆盖应用适配器的同名字段。不能依赖自定义 `paymentAdapter.authorize` 追加业务授权；归属和业务限制落实在实际调用的 lock/payable/settle 等回调中。

公共层核对订单用户、金额和币种。写回调参加传入事务，不另开事务或调用网关；客户端成功提示不能替代到账事实。

`settle()` 返回 true 不是履约成功：外部支付进入公共补偿流程，余额支付以业务不可履约错误撤销该次事务。公共充值订单由宿主自己的回调处理；未提供 ordersWithRecharge 时，宿主不会把充值订单自动插入应用 orders 的分页结果。

### 其他当前能力

<table>
<thead>
  <tr>
    <th>
      定义项
    </th>
    
    <th>
      当前参数、返回与范围
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        overview.read(db, tenantId, range)
      </code>
    </td>
    
    <td>
      返回含 counts 的安全业务展示 DTO；range 为 <code>
        { now, today, periodStart }
      </code>
      
      ，值为 Date
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        overview.orderRows(tenantId)
      </code>
    </td>
    
    <td>
      同步返回参数化订单查询片段，列含 id/userId/status/createdAt；固定本应用且限定 tenantId，公共层据此合并资金统计
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        memberMetrics(db, tenantId, userId)
      </code>
    </td>
    
    <td>
      返回 <code>
        { paidCents, paidOrders }
      </code>
      
      ；实付分为十进制字符串，订单数量为整数，按完整消费者范围计算
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        decoration.defaults()
      </code>
      
       与 targets/blocks
    </td>
    
    <td>
      返回匹配 SDK 装修类型的默认方案、目标和组件声明
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        decoration.publicContent(db, { tenantId, applicationKey })
      </code>
    </td>
    
    <td>
      可选；返回公开 assets/components/targets，可附公开 applicationData，不返回会员、钱包或私密配置
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        virtualGoods(config)
      </code>
    </td>
    
    <td>
      同步返回商品来源，含 localId/name/price/image/remark；不代表任意业务订单已支持虚拟支付
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        virtualRecharge
      </code>
    </td>
    
    <td>
      公共充值的微信虚拟商品来源开关；仍受当前支持的订单类型、商品配置及渠道校验约束
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        helpCenter/sms/h5/wechatMini/wechatOfficial
      </code>
    </td>
    
    <td>
      按接入能力声明的开关，仍需配置、权限、渠道及真实产物，声明本身不授予权限
    </td>
  </tr>
</tbody>
</table>

参数化查询片段不能拼接客户端 SQL。装修与商品来源使用匹配 SDK 的公开 DTO；超出既有渲染契约的界面放在应用自己的页面，不让客户端导入任意服务端代码。

## 业务错误

```js
import { fail } from '@qicore/application-sdk/server'

if (!record) fail('NOTE_NOT_FOUND', '记录不存在', 404)
if (record.version !== input.version) {
  fail('NOTE_CONFLICT', '记录已更新，请重新读取', 409)
}
```

`Fault(code, message, status = 400)` 与 `fail(code, message, status)` 由宿主提供同一个实现。错误码保持稳定；消息面向用户，不包含 SQL、服务器路径、密钥或堆栈。

必需能力缺失时阻止装配；可选能力使用前检测是否存在，并给出明确降级行为。兼容范围来自实际验收，不能仅凭版本号字符串推断。

继续阅读[HTTP 接口](https://docs.mcent.cn/qicore/http-api)和[应用生命周期与打包](https://docs.mcent.cn/qicore/lifecycle-and-packaging)。


## Sitemap

See the full [sitemap](https://docs.mcent.cn/sitemap.md) for all pages.
