---
title: "存储与事务"
description: "使用按应用绑定的文件存储、共享数据库事务和公共钱包接口。"
canonical_url: "https://docs.mcent.cn/qicore/storage-and-transactions"
---
# 存储与事务

> 使用按应用绑定的文件存储、共享数据库事务和公共钱包接口。

应用借用宿主数据库和存储。文件、资金及业务记录都保留完整的 `tenantId/applicationKey/userId`，并按各自的版本、生命周期和恢复规则处理。

## 应用存储接口

宿主向 `createRuntime({ db, storage })` 注入的 `storage` 已绑定应用 key。业务调用的 `scope` 至少包含可信的 `tenantId/userId`；若传入 `applicationKey`，必须与绑定值一致。

存储范围校验不能代替身份与业务授权。处理器先核验有效会话、应用可用性和资源权益，再传入来自可信上下文的范围。

<table>
<thead>
  <tr>
    <th>
      方法
    </th>
    
    <th>
      结果与约束
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        publicPolicy(scope?)
      </code>
    </td>
    
    <td>
      读取当前目标的 <code>
        provider/revisionId/external/maxFileMb
      </code>
      
       等公开策略
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        uploadTarget(scope, externalConsent)
      </code>
    </td>
    
    <td>
      选择当前站点的有效存储修订
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        put(revision, scope, bytes, extension, mime, expiresAt, options?)
      </code>
    </td>
    
    <td>
      写入文件并返回不透明存储引用
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        get(ref, scope)
      </code>
    </td>
    
    <td>
      在相同范围内读取未删除、未过期文件
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        remove(ref, scope)
      </code>
    </td>
    
    <td>
      明确请求删除，失败保留持久重试状态
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        outputTarget(ref, scope)
      </code>
    </td>
    
    <td>
      取得该文件原始存储修订
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        extendRetention(ref, scope, expiresAt)
      </code>
    </td>
    
    <td>
      延长可延长的应用管理对象访问期限，返回布尔结果
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        claimLegacy(ref, scope, retention)
      </code>
    </td>
    
    <td>
      仅在严格归属条件下认领既有未归属对象
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        sweep()
      </code>
    </td>
    
    <td>
      处理本应用已明确交给到期清理的文件
    </td>
  </tr>
</tbody>
</table>

站点业务使用 `publicPolicy({ tenantId })` 读取本站策略；省略范围时读取平台默认目标，不能据此覆盖站点独立存储。`uploadTarget()` 返回服务端修订对象，业务接口只向客户端公开策略字段，不返回完整修订或凭据。

### 云服务商同意值

`externalConsent` 是用户明确同意的当前存储修订 `revisionId`，不是布尔值。云配置改变后旧同意值失效，`uploadTarget()` 返回 `EXTERNAL_STORAGE_CONSENT_REQUIRED`。

客户端可先读取公开策略，向用户展示目标，再把同意的修订提交给业务服务。服务端不能静默替用户填入当前修订来绕过同意流程。站点选择独立存储但尚未启用时，相关上传会明确失败。

### 写入条件

当前 `put()` 接受：

- `Buffer` 类型的字节，扩展名为 `png` 或 `jpg`。
- 大小不超过所选修订配置的 `maxFileMb`。
- 有效且晚于当前时间的 `Date` 类型 `expiresAt`。
- `retention` 为 `application` 或 `expiry`，默认 `application`。

业务层先完成图片解码、实际内容和业务限制验证，再调用存储。不要把客户端提供的扩展名或 MIME 当作已验证的图片。

```js
// storage 来自宿主；session 已经通过身份和业务授权。
const scope = { tenantId: session.tenantId, userId: session.userId }
const revision = await storage.uploadTarget(scope, consentRevisionId)
const expiresAt = new Date(Date.now() + 60 * 60 * 1000)

const ref = await storage.put(
  revision, scope, validatedPng, 'png', 'image/png', expiresAt,
  { retention: 'expiry' }
)
const bytes = await storage.get(ref, scope)
```

示例将临时图片保留一小时；正式期限由业务规则决定。`ref` 是存储引用，不是磁盘路径，应用按原样持久保存，不自行拼接对象键或换用另一存储修订。

## 留存、到期与删除

<table>
<thead>
  <tr>
    <th>
      留存类型
    </th>
    
    <th>
      实体文件处理
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        expiry
      </code>
    </td>
    
    <td>
      应用明确交给框架按到期时间清理
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        application
      </code>
    </td>
    
    <td>
      由应用规则决定何时显式删除，框架不按到期自动清除实体
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        legacy
      </code>
    </td>
    
    <td>
      旧记录缺少明确归属与策略，保留待核实；新写入不能使用此值
    </td>
  </tr>
</tbody>
</table>

`retention` 决定实体清理责任，`expiresAt` 决定访问期限。应用管理文件即使仍保留实体，到期后 `get()` 也拒绝读取。

`extendRetention()` 只处理 `application` 对象；已删除、已请求删除或进入删除重试的对象不能通过延长操作恢复。新期限须为有效未来 `Date`，早于既有期限不会缩短文件的有效期。

`remove()` 表示已经批准删除；它将对象转为到期清理并尝试删除，失败时保留重试时间。业务操作先检查引用和恢复义务，不把停用、卸载或更换应用等同于批准删除数据。

旧对象认领应来自可验证的业务引用。不能按目录名、文件编号或默认应用猜测归属，也不能把 `legacy` 对象批量改为自动清理。

## 数据库与共享事务

宿主提供的 `db` 同时承载框架模型及经过验证的应用模型。模型和物理表使用应用自己的命名空间，不能覆盖框架或其他应用的名称。

`TenantApplications.transaction(fn)` 以 Serializable 隔离级别运行 `fn(tx)`。当前实现最多尝试 4 次，仅重试明确识别的事务冲突；业务回调可能被重新执行。

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

const applications = new TenantApplications(db)
await applications.transaction(async tx => {
  await applications.admin(
    adminSession, tenantId, 'notes-app', 'notes.write', tx, true
  )
  // 在同一 tx 中校验版本并修改应用自有记录、关联状态及审计。
  // 调用需要共享事务的公共适配器时继续传 tx。
})
```

事务回调内不发送外部支付、短信或存储服务请求；这些外部调用无法随数据库回滚。持久化稳定任务和输入快照后，在事务外执行，按回执及状态恢复。

有幂等需求的请求键在事务回调外确定，重试不重新生成。对同一资源并发修改，继续使用版本条件和数据库唯一约束。

## 公共钱包与单位

`Wallet` 维护 `points` 和 `balance`。积分单位为 `POINT`；余额单位为 `CNY_CENT`，`100` 表示 1 元。金额及积分数量使用安全整数；不使用浮点金额计算后截断。

<table>
<thead>
  <tr>
    <th>
      方法
    </th>
    
    <th>
      用途
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        read(session)
      </code>
    </td>
    
    <td>
      校验消费者状态并返回账户摘要
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        authorize(tx, session, lock?)
      </code>
    </td>
    
    <td>
      核验内部消费者会话、应用与账号状态
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        credit(tx, scope, asset, input, { version? }?)
      </code>
    </td>
    
    <td>
      在调用方事务中增加资产及来源批次
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        debit(tx, scope, asset, input, options?)
      </code>
    </td>
    
    <td>
      在调用方事务中按来源批次扣减
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        restore(tx, scope, asset, input, originalId)
      </code>
    </td>
    
    <td>
      按原扣减流水恢复资产
    </td>
  </tr>
</tbody>
</table>

`credit/debit/restore` 是受信服务端写入接口，不能代替业务授权、订单校验或事务管理。`debit` 的选项包括 `sourceOrder/untouched/version`；版本来自真实账户摘要，不由客户端任意指定。

`input` 包含 `requestKey/kind/reference/reason/actor/amount`。`amount` 为正安全整数，`requestKey` 是稳定业务键；来源、操作者和金额由服务端业务规则确定。

同一账户、同一 `requestKey` 的相同完整输入返回原记录，输入或相关选项不同返回 `WALLET_REQUEST_CONFLICT`。重复请求保留原版本及原参数，不通过换键制造第二笔资金事实。

`restore()` 关联原负向流水；输入要求 `kind: 'return'`、`reference: originalId`。累计恢复不能超过原扣减，来源批次也必须一致。实际资金退款、应用权益回收和钱包恢复是不同步骤，不能互相冒充。

## 并发、审计与恢复

共享写入遵守同一锁定顺序：租户、应用、订单或支付记录、钱包账户。涉及原交易的写操作先锁并检查对应业务记录，再处理账户；不要倒置顺序制造死锁。

金额、状态、来源及关键审计在同一事务内提交。网络超时只表示结果未知，先按原请求键或订单编号查询，不换编号盲目重发。支付成功须由服务端可信通知或查询确认，客户端成功提示不触发直接发放权益。

文件失败清理、支付补偿和应用任务都需要持久状态。已有迁移、历史交易、文件来源和审计保留；异常恢复不能靠清空数据库或改写旧迁移完成。

继续阅读[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.
