Skip to content

VIO 对外 API 参考(External API) ​

本文档说明如何通过 API Key 调用 VIO 对外接口;文中路径、请求与响应字段名与正式环境一致。

目录 ​


1. 概述(Overview) ​

什么是 VIO External API? ​

VIO External API 供租户以编程方式将自有系统与 VIO 平台对接。可通过本 API:

  • 管理礼券(创建、更新、删除、核销)
  • 管理用户及其数据
  • 创建并管理礼券活动
  • 处理代币、余额与交易
  • 获取分析与报表数据
  • 管理并校验店员核销 PIN

基础 URL(Base URL) ​

https://{your-domain}/api/external/v1

所有接口路径均相对于上述基础 URL。

API 版本(Versioning) ​

当前 API 发布版本为 1.5.0,URL 路径版本保持为 v1。当出现破坏性变更时将发布新的 URL 路径版本,并尽可能保持旧版本可用。

架构示意(Architecture) ​

┌─────────────────────────────────────────────────────────────────┐
│                       你的应用                                   │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                                                           │  │
│  │   1. 携带 X-API-Key 请求头                                 │  │
│  │   2. 向 /api/external/v1/* 发起 HTTPS 请求                 │  │
│  │   3. 解析 JSON 响应                                        │  │
│  │                                                           │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────────┐
                    │    VIO API 服务     │
                    │  /api/external/v1   │
                    │                     │
                    │  • 鉴权             │
                    │  • 限流             │
                    │  • Scope 校验       │
                    │  • 请求处理         │
                    └─────────────────────┘
                              │
                              ▼
                    ┌─────────────────────┐
                    │      数据库         │
                    │   (租户数据)      │
                    └─────────────────────┘

2. 鉴权(Authentication) ​

/api/external/v1 接口使用 API Key。不在 /api/external/v1 下的会员接口——包括使用 zhichong 礼券和刷新履约状态——使用账号密码登录拿到的会员 JWT。

API Key ​

/api/external/v1 请求须使用 API Key 鉴权。API Key 绑定租户,可配置不同权限范围(scope)。

如何获取 API Key ​

  1. 登录 VIO 管理后台(Admin Portal)
  2. 进入 设置 > API Keys
  3. 点击 创建 API Key
  4. 为该 Key 选择 scope(权限)
  5. 可选:配置 IP 白名单
  6. 复制并安全保存生成的 Key

重要:API Key 仅在创建时完整展示一次,请务必妥善保管。

在请求中使用 API Key ​

每个请求在请求头中携带 X-API-Key:

bash
curl -X GET "https://your-domain.com/api/external/v1/vouchers" \
  -H "X-API-Key: vio_live_your_api_key_here"

API Key 格式 ​

  • 生产:vio_live_xxxxxxxxxxxxxxxx
  • 测试:vio_test_xxxxxxxxxxxxxxxx

Scope(权限范围) ​

每个 API Key 可配置一个或多个 scope,决定可访问的接口:

Scope说明路径前缀
vouchers礼券与领取记录/vouchers/*
users用户/users/*
campaigns活动/campaigns/*
tokens代币与余额/tokens/*
analytics分析数据/analytics/*
staff_pins店员 PIN 管理与校验/staff-pins/*

IP 白名单 ​

为加强安全,可将 API Key 限制在指定 IP:

  1. 打开 管理后台 > 设置 > API Keys
  2. 编辑对应 API Key
  3. 添加允许的 IP 或 CIDR
  4. 保存

非白名单 IP 请求将返回 403 Forbidden。

会员 JWT(账号密码登录) ​

使用 POST /users 或 POST /users/find-or-create 创建的会员账号登录。登录接口不属于 External API。调用 POST /api/auth/login。

租户标识 只传一种:slug 或 tenant ID 都可以,不要两个都传。

二选一怎么传
Tenant slug请求头 X-Tenant-Slug
Tenant ID请求头 X-Tenant-ID,或写在 body 的 tenantId

请求体:

字段类型必填说明
identifierstring是创建用户时使用的邮箱或手机号
identifierTypestring是email 或 phone
passwordstring是该用户的密码
tenantIdstring否只用 tenant ID 识别租户、且没有租户请求头时必填。已经传了 X-Tenant-Slug 或 X-Tenant-ID 就不要再传

用 slug 的示例:

bash
curl -X POST "https://your-domain.com/api/auth/login" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Slug: {tenant_slug}" \
  -d '{
    "identifier": "user@example.com",
    "identifierType": "email",
    "password": "securepassword123"
  }'

用 tenant ID 的示例:

bash
curl -X POST "https://your-domain.com/api/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "user@example.com",
    "identifierType": "email",
    "password": "securepassword123",
    "tenantId": "507f1f77bcf86cd799439011"
  }'

成功响应示例(关键字段):

json
{
  "success": true,
  "data": {
    "user": {
      "id": "507f1f77bcf86cd799439016",
      "email": "user@example.com",
      "role": "member"
    },
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": "15m"
  },
  "message": "Login successful"
}

将 accessToken 作为 {member_jwt} 使用:

Authorization: Bearer {accessToken}
Token默认有效期用途
accessToken15 分钟调用会员接口。以响应里的 expiresIn 为准
refreshToken7 天换取新的一对 token。不要把它放进 Authorization

accessToken 过期后,会员接口返回 401 且 code 为 TOKEN_EXPIRED。此时调用 POST /api/auth/refresh。不要重新登录,也不要重新提交 /redeem。

bash
curl -X POST "https://your-domain.com/api/auth/refresh" \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "{refreshToken}"
  }'
json
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiresIn": "15m"
  }
}

刷新会同时轮换两种 token。必须保存新的 refreshToken,旧的会被撤销。

若刷新返回 401(Invalid refresh token、Refresh token expired 或 Refresh token not found or revoked),refresh token 已失效,需要再次调用 POST /api/auth/login。若直充订单仍在处理中,拿到新的 access token 后继续调用 POST /api/vouchers/me/{userVoucherId}/fulfillment/refresh。


3. 限流(Rate Limiting) ​

默认限额 ​

为保证公平与稳定,接口实行限流:

限额类型默认值
每分钟60 次请求
每天10,000 次

限流响应头 ​

响应中会包含限流相关信息:

Header说明
X-RateLimit-Limit当前时间窗口内上限
X-RateLimit-Remaining当前窗口剩余次数
X-RateLimit-Reset限额重置的 Unix 时间戳

响应头示例 ​

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1699574400

超出限流 ​

超限将返回 429 Too Many Requests:

json
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please retry after 60 seconds."
  }
}

响应通常包含 Retry-After,提示多久后可重试。

最佳实践 ​

  • 收到 429 时使用指数退避重试
  • 在合适场景下缓存响应
  • 尽量批量操作
  • 根据响应头监控用量

4. 请求与响应格式(Request & Response) ​

请求格式 ​

  • 请求体须为 JSON
  • POST/PATCH 须带 Content-Type: application/json
  • 过滤与分页使用查询参数

成功响应 ​

成功时结构如下:

json
{
  "success": true,
  "data": { ... },
  "message": "Optional success message"
}

分页列表 ​

列表类接口返回分页结构:

json
{
  "success": true,
  "data": [ ... ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 150,
    "totalPages": 8,
    "hasNextPage": true,
    "hasPrevPage": false
  }
}

分页参数 ​

参数类型默认值说明
pageinteger1页码(从 1 开始)
limitinteger20每页条数(最大 100)

错误响应 ​

错误时结构如下:

json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error description"
  }
}

5. 错误处理(Error Handling) ​

HTTP 状态码 ​

Status Code含义
200 OK成功
201 Created创建成功
400 Bad Request参数或 body 无效
401 Unauthorized缺少或无效的 API Key,或会员 JWT 缺失/无效/过期
403 ForbiddenScope 不足或未通过 IP 白名单
404 Not Found资源不存在
429 Too Many Requests超出限流
500 Internal Server Error服务器错误

业务错误码(Error Codes) ​

Error Code说明
VALIDATION_ERROR校验失败
UNAUTHORIZEDAPI Key 或会员 JWT 缺失或无效
INVALID_TOKEN会员 JWT 格式错误
TOKEN_EXPIRED会员 JWT access token 已过期
FORBIDDEN无所需 scope
NOT_FOUND资源不存在
RATE_LIMIT_EXCEEDED请求过于频繁
ALREADY_EXISTS资源已存在(如重复用户)
INSUFFICIENT_BALANCE代币余额不足
ALREADY_REDEEMED礼券已核销
EXPIRED礼券实例或钱包代币已过期
INTERNAL_ERROR未预期的服务器错误

错误响应示例 ​

json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request parameters",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      }
    ]
  }
}

6. API 参考:礼券(Vouchers) ​

说明: 本章中 HTTP 路径、方法、JSON 字段名及 cURL/代码块与线上实现一致;说明性文字为中文。

所需 Scope: vouchers

兑换与使用流程(重要) ​

术语:两个不同的动作 ​

产品界面区分两个动作,而 API 路径与字段名沿用历史命名、并不总是与之一致,请以下表为准:

产品术语含义对应 API结果状态
购买/领取进钱包(接口历史名称为 Redeem/Claim)礼券进入会员钱包,通常会在此时扣除代币POST /vouchers/:voucherId/redeem-with-tokens、POST /api/vouchers/:voucherId/claim、POST /vouchers/:voucherId/send创建 UserVoucher,状态为 active
实际使用礼券(Use)礼券被真正使用;对于 zhichong,此时才提交直充订单POST /vouchers/redeem-by-code、POST /vouchers/redeem/:code/pin、POST /api/vouchers/me/:userVoucherId/redeem使用成功后 UserVoucher.status 变为 redeemed(界面显示为已使用)

为保持向后兼容,API 保留了历史命名。redeem-by-code、redeem/:code/pin、/api/vouchers/me/:userVoucherId/redeem、redemptionCode 以及状态 redeemed 表示的是实际使用礼券,不是购买/领取进钱包。

以下为两类不同概念,请勿混淆:

  • 礼券模板(Voucher):租户在平台中创建或与外部同步的礼券定义。
  • 用户礼券实例(UserVoucher):礼券经「兑换」或「发放(send)」进入某个用户钱包后得到的单用户实例。

按场景选择流程 ​

若仅通过 API 对接,请按下表选用路径:

场景第 1 步 — 兑换(进入钱包)第 2 步 — 使用(核销)典型用途
会员从活动兑换POST /api/vouchers/:voucherId/claim 且带 campaignIdPOST /api/vouchers/me/:userVoucherId/redeem会员端应用(JWT)
租户/后端直接向用户发券POST /vouchers/:voucherId/send 且带 userIdPOST /vouchers/redeem-by-code 或 POST /vouchers/redeem/:code/pin服务端 API Key 对接
后端代会员兑换(扣 token)POST /vouchers/:voucherId/redeem-with-tokens 且带 userId + campaignIdPOST /vouchers/redeem-by-code 或 POST /vouchers/redeem/:code/pinAPI Key — 原子 token 兑券

租户自管礼券与外部供货礼券的阶段顺序相同:均为先兑换(或 send)→ 再使用(核销)。差异在核销阶段:

  • 租户自管礼券:核销将该礼券实例标记为已使用,不调用外部供货系统。若 consumptionType 为 qr_code 或 coupon_code 且配置了 CSV 码池(管理端上传),核销时会从池中原子分配下一条码至 externalVoucherCode,再标记已使用;端侧应在此时再展示供应商码 / 由该码生成的 QR 或条码。
  • 外部供货礼券:核销会触发供货方履约(如下单)。核销后须根据响应中的 externalFulfillmentStatus 确认是否成功。

POST /vouchers/:voucherId/send 在 External API 中等价于「为指定用户完成兑换」:会创建 UserVoucher 并返回 redemptionCode(店员扫码核销时使用的码)。

类型判断规则 ​

依据礼券模板字段区分来源与用法:

  • externalProvider 与 externalId 均存在:为外部供货礼券。
  • externalProvider 与 externalId 均缺失:为租户自管礼券。
  • consumptionType 表示核销交互方式(vio_code、coupon_code、url、qr_code、manual、zhichong),不能单独用作区分「外部 / 租户自管」的依据。租户自管的 qr_code / coupon_code 可与管理端 CSV 码池 配合:每次会员 / 店员 / API 核销成功时从池中取一条唯一码写入 externalVoucherCode;模板 兑换上限 不得超过池中码总数(见 Admin 用户指南)。
  • voucherType 为业务分类字段,不能单独用作区分「外部 / 租户自管」的依据。

应在哪些接口读取判断信息 ​

需要判断的内容使用的接口关键字段
外部供货还是租户自管?GET /vouchers 或 GET /vouchers/:voucherIdexternalProvider、externalId
核销时需要哪种输入方式?GET /vouchers 或 GET /vouchers/:voucherIdconsumptionType
直充商品是否需要额外供货参数?GET /vouchers 或 GET /vouchers/:voucherIdexternalRequiresDirectOrderParams
应对哪一条礼券实例核销?兑换/send 的响应或用户礼券列表userVoucherId(_id)、redemptionCode、status
外部履约结果?核销响应externalVoucherCode、externalRedemptionUrl、externalOrderId、externalFulfillmentStatus

GET /vouchers/redeem/:code/info 仅用于展示,不要作为判断礼券类型的数据源。

兑换 / 使用示例 ​

A) 使用 External API(API Key)向指定用户发放(send) ​

适用于由贵方后端直接向用户派券的场景。

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/{voucherId}/send" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "507f1f77bcf86cd799439015"
  }'

成功响应示例(关键字段):

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "voucherId": "507f1f77bcf86cd799439011",
    "userId": "507f1f77bcf86cd799439015",
    "status": "active",
    "redemptionCode": "VCH-M1ABC2-XY3Z"
  },
  "message": "Voucher sent to user"
}

B) 使用 External API(API Key)按码实际使用/核销 ​

适用于非 zhichong 的服务端对接或门店收银等。

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/redeem-by-code" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "redemptionCode": "VCH-M1ABC2-XY3Z",
    "location": "Store #42",
    "notes": "POS order #12345"
  }'

成功响应示例(关键字段):

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "status": "redeemed",
    "redemptionCode": "VCH-M1ABC2-XY3Z",
    "redeemedAt": "2024-02-01T10:15:00.000Z",
    "externalVoucherCode": "ABC-123-XYZ",
    "externalRedemptionUrl": null,
    "externalOrderId": "69f75fe31134f32e472b2f98",
    "externalFulfillmentStatus": "fulfilled"
  },
  "message": "Voucher redeemed"
}

C) 实际使用 zhichong 礼券并触发直充(会员 API,JWT) ​

当 consumptionType 为 zhichong 时,须调用会员侧核销接口,并在 body 中传入充值账号。路径参数必须使用用户已领取礼券实例的 UserVoucher._id,不能使用礼券模板 ID。

{member_jwt} 是 POST /api/auth/login 返回的 accessToken。见 会员 JWT(账号密码登录)。履约过程中 access token 过期时先刷新再继续,不要重新提交 /redeem。

bash
curl -X POST "https://your-domain.com/api/vouchers/me/{userVoucherId}/redeem" \
  -H "Authorization: Bearer {member_jwt}" \
  -H "Content-Type: application/json" \
  -d '{
    "providerParams": {
      "account": "12312332123"
    }
  }'

zhichong 必须传 providerParams.account。

礼券过期规则应读取 validityType、validityDays 和 endDate:

  • validityType: "relative":领取后 validityDays 天内有效,endDate 为 null。
  • validityType: "fixed_date":到 endDate 过期,validityDays 为 null。

礼券进入会员钱包后,该张用户礼券的准确过期时间以 UserVoucher.expiresAt 为准。

成功响应示例(关键字段):

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "status": "redeemed",
    "externalFulfillmentStatus": "fulfilled",
    "externalIsDirectRecharge": true,
    "externalRechargeAccount": "12312332123",
    "externalOrderId": "69f75fe31134f32e472b2f98"
  },
  "message": "Voucher redeemed"
}

/redeem 三种结果都是同一套响应:success: true + data。不要看 HTTP 状态码判断成败,看下面两个字段:

字段含义取值
data.status用户礼券生命周期active:还没核销完成;redeemed:已使用
data.externalFulfillmentStatus供应商直充履约processing:仍在处理中;fulfilled:直充成功;failed:直充失败

仍在处理中(不是 fail)。用户礼券仍是 active,履约是 processing。不要再次提交 /redeem,改为调用下面的刷新接口:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "status": "active",
    "externalFulfillmentStatus": "processing",
    "externalIsDirectRecharge": true,
    "externalRechargeAccount": "12312332123",
    "externalOrderId": "69f75fe31134f32e472b2f98",
    "externalFulfillmentError": "VouChain order 69f75fe31134f32e472b2f98 is still pending fulfillment"
  },
  "message": "Supplier fulfillment is still processing"
}

直充失败。用户礼券仍是 active,履约是 failed。展示 externalFulfillmentError;不要自动重新提交 /redeem。仅当 externalFulfillmentRetryAllowed 为 true 时,才允许人工再点一次:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "status": "active",
    "externalFulfillmentStatus": "failed",
    "externalIsDirectRecharge": true,
    "externalRechargeAccount": "12312332123",
    "externalOrderId": "69f75fe31134f32e472b2f98",
    "externalFulfillmentError": "The supplier could not complete this top-up.",
    "externalFulfillmentRetryAllowed": false
  },
  "message": "Supplier fulfillment failed"
}

直充异步完成 ​

直充履约可能异步完成。POST /api/vouchers/me/:userVoucherId/redeem 只提交一次。若请求超时或提示原供货订单仍在处理中,不要再次提交 /redeem。

使用同一个会员 JWT 刷新原订单:

bash
curl -X POST "https://your-domain.com/api/vouchers/me/{userVoucherId}/fulfillment/refresh" \
  -H "Authorization: Bearer {member_jwt}"

响应示例(关键字段):

已完成:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "status": "redeemed",
    "externalFulfillmentStatus": "fulfilled",
    "externalIsDirectRecharge": true,
    "externalRechargeAccount": "12312332123",
    "externalOrderId": "69f75fe31134f32e472b2f98"
  },
  "message": "Supplier fulfillment status refreshed"
}

仍在处理中:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "status": "active",
    "externalFulfillmentStatus": "processing",
    "externalIsDirectRecharge": true,
    "externalRechargeAccount": "12312332123",
    "externalOrderId": "69f75fe31134f32e472b2f98",
    "externalFulfillmentError": "The original supplier order is still processing. No new order has been submitted."
  },
  "message": "Supplier fulfillment status refreshed"
}

失败:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "status": "active",
    "externalFulfillmentStatus": "failed",
    "externalIsDirectRecharge": true,
    "externalRechargeAccount": "12312332123",
    "externalOrderId": "69f75fe31134f32e472b2f98",
    "externalFulfillmentError": "The supplier could not complete this top-up.",
    "externalFulfillmentRetryAllowed": false
  },
  "message": "Supplier fulfillment status refreshed"
}

根据返回的礼券实例处理:

  • externalFulfillmentStatus: "fulfilled" 且 status: "redeemed":充值完成。
  • externalFulfillmentStatus: "processing":等待后再次调用刷新接口;系统不会创建新的供货订单。
  • externalFulfillmentStatus: "failed":展示 externalFulfillmentError,不要自动重新提交 /redeem。

礼券来源与类型对照 ​

来源externalProviderexternalId常见 consumptionType建议领取/发放建议核销
租户自管无无vio_code、coupon_code、url、qr_code、manualPOST /vouchers/:voucherId/send(API Key)或会员领取流程POST /vouchers/redeem-by-code、POST /vouchers/redeem/:code/pin 或会员侧核销
外部供货(码/链接类)有有多为 coupon_code 或 url须先领取/send领取/send 后再核销
外部供货(zhichong)有有zhichong须先领取/sendPOST /api/vouchers/me/:userVoucherId/redeem 且带 providerParams.account

POST /api/external/v1/vouchers/redeem-by-code 不接受 providerParams。
POST /api/external/v1/vouchers/redeem/:code/pin 不能用于 zhichong 类型。

错误处理与履约状态 ​

常见发放(send/claim)错误 ​

HTTP 状态错误码原因
404NOT_FOUNDvoucherId 或 userId 不存在
400VALIDATION_ERROR礼券未启用或已过期
400VALIDATION_ERROR已抢完(claimedQuantity >= totalQuantity)
400VALIDATION_ERROR用户已达 maxClaimsPerUser 上限

常见核销错误 ​

HTTP 状态错误码原因
404NOT_FOUNDredemptionCode 无法匹配有效领取记录
400VALIDATION_ERROR领取记录已核销(ALREADY_REDEEMED)
400VALIDATION_ERROR领取记录已过期(EXPIRED)
400VALIDATION_ERRORzhichong 礼券不能使用 PIN 核销

外部礼券履约状态 ​

核销外部供货礼券后,请检查响应中的 externalFulfillmentStatus:

externalFulfillmentStatus含义建议处理
fulfilled供货成功;非直充场景下通常可得到 externalVoucherCode 或 externalRedemptionUrl将码或链接交付用户
failed供货失败展示 externalFulfillmentError;仅当 externalFulfillmentRetryAllowed 明确为 true 时才允许人工重试,禁止自动重试
processing原异步供货订单仍在处理中会员 JWT 流程调用 POST /api/vouchers/me/:userVoucherId/fulfillment/refresh;不要重新提交 /redeem

若供货方购买失败,核销请求会返回错误,并记录 externalFulfillmentStatus: "failed" 与 externalFulfillmentError。失败记录不一定允许创建新的供货订单。

外部履约失败后的领取记录状态示例:

json
{
  "_id": "507f1f77bcf86cd799439050",
  "status": "active",
  "externalFulfillmentStatus": "failed",
  "externalFulfillmentError": "Provider returned: insufficient inventory",
  "externalOrderId": null
}

当 externalFulfillmentStatus 为 failed 时,应检查 externalFulfillmentRetryAllowed。供货失败订单不得自动重新提交。


列举礼券 ​

分页获取当前租户下可见的礼券模板列表。可选地排除当前 tenant 自管创建的礼券。

GET /vouchers

查询参数:

参数类型必填约束说明
pageinteger否最小 1,默认 1页码(从 1 起)
limitinteger否1–100,默认 20每页条数
visibilitystring否枚举:private、public、shared按可见性筛选
isActiveboolean否字符串 "true" / "false",解析为布尔值是否仅返回启用中的模板
categorystring否任意字符串按分类标签筛选
searchstring否任意字符串按礼券名称模糊搜索
subCompanyIdstring否MongoDB ObjectId(24 位十六进制)按子公司范围筛选
excludeTenantManualCreatedboolean否字符串 "true" / "false",默认 false为 true 时排除当前 tenant 自管创建的礼券,保留 vouchain 等外部供货礼券

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/vouchers?page=1&limit=10&isActive=true&excludeTenantManualCreated=true" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439011",
      "name": "20% Off Discount",
      "description": "Get 20% off your next purchase",
      "value": 20,
      "valueType": "percentage",
      "terms": "Valid on orders over $50",
      "images": ["https://example.com/image.jpg"],
      "isActive": true,
      "totalQuantity": 100,
      "claimedQuantity": 45,
      "maxClaimsPerUser": 1,
      "voucherType": "discount",
      "consumptionType": "coupon_code",
      "externalProvider": "vouchain",
      "externalId": "VCH-EXT-001",
      "externalRequiresDirectOrderParams": false,
      "category": "Lifestyle & Services",
      "categories": ["food", "lifestyle"],
      "settlementAmount": 80,
      "settlementCurrency": "THB",
      "startDate": "2024-01-01T00:00:00.000Z",
      "endDate": "2024-12-31T23:59:59.000Z",
      "visibility": "public",
      "createdAt": "2024-01-01T00:00:00.000Z",
      "updatedAt": "2024-01-15T10:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 45,
    "totalPages": 5,
    "hasNextPage": true,
    "hasPrevPage": false
  }
}

响应字段(data 数组每一项):

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId(24 位十六进制)礼券模板唯一 ID
namestring否至少 1 字符,已 trim展示名称
descriptionstring否可为空串 ""礼券说明
valuenumber否>= 0,默认 0面额/折扣数值,具体含义见 valueType
valueTypestring否枚举:fixed、percentagefixed 为固定金额,percentage 为折扣比例
valueCurrencystring否ISO 4217(如 THB、HKD、USD),默认 THB当 valueType 为 fixed 时 value 的币种
minSpendnumber否>= 0,默认 0最低消费门槛;0 表示无门槛
maxDiscountnumber是> 0;未设置可为 null百分比折扣的封顶金额
termsstring否可为空串 ""使用条款
imagesstring[]否合法 URL 数组,可为 []配图 URL,首张为主图
isActiveboolean否true / false是否启用、可被领取
isTransferableboolean否true / false,默认 false用户领取后是否允许转让
totalQuantityinteger否-1 表示不限量;否则 >= 0可领取总量上限;当 claimedQuantity >= totalQuantity(且非 -1)时不可再领取
claimedQuantityinteger否>= 0已被领取的次数
maxClaimsPerUserinteger否0 表示每用户不限次;否则 >= 1单用户最多可领取次数
voucherTypestring否枚举:cash、discount、product、cash_discount,默认 discount业务分类;勿单独用于判断外部/租户自管
consumptionTypestring否枚举:vio_code、coupon_code、url、qr_code、manual、zhichong核销交互形态,配合 externalProvider / externalId 决定对接方式
externalProviderstring是供货方代码或 null与 externalId 同时存在时表示外部供货礼券
externalIdstring是供货侧模板 ID 或 null与 externalProvider 同时存在时表示外部供货礼券
externalRequiresDirectOrderParamsboolean是true / false / null直充商品是否需要额外的供货方参数;zhichong 仍必须传 providerParams.account
externalDataobject是对象或 null外部供货方原始/扩展数据;结构可能因供货方而异
categorystring是枚举:Wellness、Health、Food & Beverage、Leisure & Entertainment、Travel & Hospitality、Lifestyle & Services、Others;可为 null礼券主分类,可用于分类筛选与展示
categoriesstring[]否trim 后的字符串数组,可为 []旧版自定义分类标签(已弃用,建议使用 category),为兼容仍会返回
applicableBrandsobject[]否每项包含 _id、name、slug适用品牌/子公司(旧字段,建议优先使用 applicableBrandTags)
applicableBrandTagsobject[]否每项包含 _id、name、logo适用品牌标签
settlementAmountnumber否>= 0,默认 0该礼券的结算单价:当此礼券在跨租户场景下被用户核销时,VIO 与发券租户之间记账的结算金额(即 VIO 给客户的结算价)。可用于客户对账
settlementCurrencystring否ISO 4217 代码,大写,默认 THBsettlementAmount 的币种
crossTenantReceivableTimingstring否枚举:redemption、consumption,默认 redemption跨租户应收入账时点
startDatestring是ISO 8601;未设置可为 null生效起始时间
endDatestring是ISO 8601;null 表示不设结束失效时间;relative(相对有效期)礼券没有绝对结束时间,该字段恒为 null
validityTypestring否枚举:fixed_date、relative,默认 fixed_date每条领取记录的到期方式:fixed_date 到 endDate 过期;relative 领取后 validityDays 天过期
validityDaysinteger是整数 1–3650;非 relative 时为 null领取后可用天数,从领取日开始计算;仅当 validityType 为 relative 时有值
visibilitystring否枚举:private、public、shared可见范围;亦影响用户间转让规则
tenantIdobject否包含 _id、name、slug礼券所属租户
subCompanyIdobject是包含 _id、name、slug;未归属子公司时可为 null礼券所属子公司
isOwnboolean否true / false是否属于当前请求上下文的组织
ownerOrgobject是{ "name": string, "type": "tenant/subCompany" };自有礼券通常不返回非自有礼券的拥有方信息
targetTiersstring[]否字符串数组,可为 []限定可见/可领取的会员等级
targetUserGroupsstring[]否MongoDB ObjectId 数组,可为 []限定可见/可领取的用户组
applicableScopestring否枚举:all_outlets、partial_outlets、single_store,默认 all_outlets适用门店范围
applicableCountrystring是ISO 国家/地区代码;可为 null单一适用国家/地区(旧字段)
applicableCountriesstring[]否ISO 国家/地区代码数组,可为 []适用国家/地区列表
storeIdstring是MongoDB ObjectId;可为 null单门店适用范围对应的门店 ID
storeLocationobject是包含 name、address、latitude、longitude;可为 null内嵌门店位置
bookingEnabledboolean否true / false,默认 false是否启用预约
bookingDaysInAdvanceinteger否>= 0,默认 0可提前预约天数
consumptionMessagestring否可为空串 ""manual 核销方式的提示文本
consumptionUrlstring否可为空串 ""url 核销方式的链接
consumptionQrCodestring否可为空串 ""qr_code 核销方式的二维码图片路径
contractAddressstring是区块链合约地址;可为 null礼券 NFT 合约地址
createdBystring是MongoDB ObjectId;可为 null创建/部署该礼券的管理员用户 ID
metadataobject否对象,默认 {}扩展元数据
createdAtstring否ISO 8601创建时间
updatedAtstring否ISO 8601最近更新时间

创建礼券 ​

为当前租户新建一条礼券模板。

POST /vouchers

请求体:

字段类型必填约束说明
namestring是至少 1 字符,已 trim会员端展示的礼券名称
descriptionstring否无最大长度,默认 ""礼券说明,纯文本
voucherTypestring否枚举:cash、discount、product、cash_discount,默认 discount礼券业务类型
visibilitystring否枚举:private、public、shared,默认 private可见范围:private 本租户;public 全平台;shared 仅 sharedWithTenants 中租户
sharedWithTenantsstring[]否每项为 MongoDB ObjectId(24 位十六进制)共享目标租户 ID 列表;仅当 visibility 为 shared 时有效
sharedWithUsersstring[]否每项为 MongoDB ObjectId限定可见的用户 ID,用于定向投放
termsstring否无最大长度,默认 ""兑换或使用前向用户展示的条款
valuenumber否最小 0,默认 0面额或折扣数值:valueType 为 fixed 时为扣减金额;为 percentage 时为百分比(如 20 表示 20% off)
valueTypestring否枚举:fixed、percentage,默认 fixedvalue 的语义:固定金额或比例
valueCurrencystring否ISO 4217(如 THB、HKD、USD),默认 THB当 valueType 为 fixed 时 value 的币种
minSpendnumber否最小 0,默认 0最低消费门槛;0 表示无门槛
maxDiscountnumber否须 > 0(若填写)折扣上限,常见于百分比券(如八折但最多减 100)
totalQuantityinteger否整数,-1 表示不限量,默认 -1可领取总次数上限;抢完后不可再领取
startDatestring否ISO 8601,默认当前时间开始可领取时间;省略表示立即生效
endDatestring否ISO 8601,须晚于 startDate;validityType 为 relative 时忽略过期时间;省略表示不设过期;过期后不可兑换/核销
validityTypestring否枚举:fixed_date、relative,默认 fixed_date有效期模式:fixed_date 所有领取记录统一在 endDate 过期;relative 每条领取记录在领取后 validityDays 天过期
validityDaysinteger条件整数 1–3650;validityType 为 relative 时必填领取后可用天数,从领取日开始计算(如 30 表示领取后 30 天内有效)
maxClaimsPerUserinteger否整数,最小 0,0 表示每用户不限次,默认 1单用户最多可领取次数
settlementAmountnumber否最小 0,默认 0跨租户核销时的法币结算金额
settlementCurrencystring否ISO 4217,存盘为大写,默认 THBsettlementAmount 币种;默认取租户账单设置主币种;结算记录均以此币种记账
categorystring否枚举:Wellness、Health、Food & Beverage、Leisure & Entertainment、Travel & Hospitality、Lifestyle & Services、Others礼券主分类,建议优先使用;例如 "Others"
categoriesstring[]否字符串数组,元素已 trim分类标签,如 ["food", "lifestyle"]
imagesstring[]否每项为合法 URL配图,首图为主图
targetTiersstring[]否字符串数组,元素已 trim会员等级名称;仅这些等级可见/可领
targetUserGroupsstring[]否每项为 MongoDB ObjectId用户组 ID;仅组内用户可见/可领
subCompanyIdstring否MongoDB ObjectId将礼券归属到指定子公司

对外 POST /vouchers 与 PATCH /vouchers/:voucherId 不接受 consumptionType。新建礼券默认为 vio_code。其他核销方式在管理端配置,读取接口会原样返回。

礼券有效期:fixed_date 与 relative

validityType 决定每一条领取记录如何计算到期时间:

validityType每条领取记录的到期时间依赖字段
fixed_date(默认)统一在礼券的绝对结束时间 endDate 过期endDate
relative领取后 validityDays 天过期validityDays
  • validityType 为 relative 时,validityDays(整数 1–3650)必填;缺失会返回 VALIDATION_ERROR。
  • relative 礼券不保存 endDate:请求中携带的 endDate 会被丢弃;把已有礼券改为 relative 也会清空该字段。此类礼券会一直可领取,直到你停用(isActive: false)或库存售罄。
  • 相对有效期从「领取时间与 startDate 中较晚者」开始计算,因此在 startDate 之前领取仍可获得完整可用天数。
  • 改回 fixed_date 会清空 validityDays(置为 null)。
  • 两种模式下,最终到期时间都通过领取记录的 expiresAt 返回,只读取 expiresAt 的集成无需改动。

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Sale 20% Off",
    "description": "Valid for summer collection",
    "value": 20,
    "valueType": "percentage",
    "category": "Others",
    "totalQuantity": 100,
    "maxClaimsPerUser": 1,
    "startDate": "2024-06-01T00:00:00.000Z",
    "endDate": "2024-08-31T23:59:59.000Z",
    "visibility": "public",
    "terms": "Cannot be combined with other offers"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439012",
    "name": "Summer Sale 20% Off",
    "description": "Valid for summer collection",
    "value": 20,
    "valueType": "percentage",
    "totalQuantity": 100,
    "claimedQuantity": 0,
    "maxClaimsPerUser": 1,
    "startDate": "2024-06-01T00:00:00.000Z",
    "endDate": "2024-08-31T23:59:59.000Z",
    "visibility": "public",
    "isActive": true,
    "consumptionType": "vio_code",
    "createdAt": "2024-05-15T10:00:00.000Z"
  },
  "message": "Voucher created"
}

请求示例 — 相对有效期(领取后 30 天内有效):

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome Voucher",
    "description": "领取后 30 天内有效",
    "value": 100,
    "valueType": "fixed",
    "valueCurrency": "THB",
    "category": "Others",
    "totalQuantity": 500,
    "maxClaimsPerUser": 1,
    "startDate": "2024-06-01T00:00:00.000Z",
    "validityType": "relative",
    "validityDays": 30,
    "visibility": "public"
  }'

创建成功后返回 "validityType": "relative"、"validityDays": 30、"endDate": null。会员在 2024-07-10 领取,则该条领取记录的 expiresAt 为 2024-08-09。

响应字段:

返回新建后的完整礼券对象,包含 consumptionType(默认 vio_code)。大多数字段与「列举礼券」响应字段一致,但 isOwn、ownerOrg 等列表接口计算字段不会由创建接口额外添加。


获取礼券详情 ​

获取指定礼券模板的完整信息。

GET /vouchers/:voucherId

路径参数:

参数类型必填说明
voucherIdstring是礼券模板 ID

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439011",
    "name": "20% Off Discount",
    "description": "Get 20% off your next purchase",
    "value": 20,
    "valueType": "percentage",
    "terms": "Valid on orders over $50",
    "images": ["https://example.com/image.jpg"],
    "isActive": true,
    "totalQuantity": 100,
    "claimedQuantity": 45,
    "maxClaimsPerUser": 1,
    "voucherType": "discount",
    "consumptionType": "coupon_code",
    "externalProvider": "vouchain",
    "externalId": "VCH-EXT-001",
    "externalRequiresDirectOrderParams": false,
    "startDate": "2024-01-01T00:00:00.000Z",
    "endDate": "2024-12-31T23:59:59.000Z",
    "visibility": "public",
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-01-15T10:30:00.000Z"
  }
}

响应字段:

返回完整礼券对象,包含 consumptionType。大多数字段与「列举礼券」响应字段一致,但 isOwn、ownerOrg 等列表接口计算字段不会由详情接口额外添加。若礼券为 shared,sharedWithTenants、sharedWithSubCompanies、sharedWithUsers 可能返回已填充的摘要对象。


更新礼券 ​

更新已有礼券模板(部分字段)。

PATCH /vouchers/:voucherId

路径参数:

参数类型必填说明
voucherIdstring是礼券模板 ID

请求体:

所有字段均为可选,仅传需要修改的字段。

字段类型约束说明
namestring至少 1 字符展示名称
descriptionstring无最大长度说明
voucherTypestring枚举:cash、discount、product、cash_discount礼券业务类型
visibilitystring枚举:private、public、shared可见范围
sharedWithTenantsstring[]每项为 MongoDB ObjectId共享租户 ID(仅 visibility 为 shared 时)
sharedWithUsersstring[]每项为 MongoDB ObjectId定向投放的用户 ID
termsstring无最大长度条款
valuenumber最小 0面额/折扣数值
valueTypestring枚举:fixed、percentagevalue 语义
valueCurrencystringISO 4217fixed 时的币种
minSpendnumber最小 0最低消费,0 表示无门槛
maxDiscountnumber须 > 0(若填写)折扣封顶(常见于百分比券)
totalQuantityinteger整数,-1 表示不限量可领取总量
startDatestringISO 8601生效时间
endDatestringISO 8601失效时间;validityType 改为 relative 时自动清空
validityTypestring枚举:fixed_date、relative有效期模式;改为 relative 会清空 endDate,改回 fixed_date 会清空 validityDays
validityDaysinteger整数 1–3650;validityType 为 relative 时必填领取后可用天数,从领取日开始计算
maxClaimsPerUserinteger最小 0,0 表示不限单用户领取上限
settlementAmountnumber最小 0跨租户结算金额
settlementCurrencystringISO 4217,存盘大写结算币种;默认租户账单主币种
categorystring枚举:Wellness、Health、Food & Beverage、Leisure & Entertainment、Travel & Hospitality、Lifestyle & Services、Others礼券主分类,例如 "Others"
isActivebooleantrue / false是否启用
categoriesstring[]每项已 trim分类标签
imagesstring[]每项为合法 URL配图
targetTiersstring[]每项已 trim目标会员等级
targetUserGroupsstring[]每项为 MongoDB ObjectId目标用户组

请求示例:

bash
curl -X PATCH "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Voucher Name",
    "isActive": false
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439011",
    "name": "Updated Voucher Name",
    "isActive": false,
    "updatedAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "Voucher updated"
}

响应字段:

返回更新后的完整礼券对象,包含 consumptionType。大多数字段与「列举礼券」响应字段一致。上方示例为节选。


删除礼券 ​

软删除礼券模板(标记删除,非物理清除)。

DELETE /vouchers/:voucherId

路径参数:

参数类型必填说明
voucherIdstring是礼券模板 ID

请求示例:

bash
curl -X DELETE "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439011",
    "isDeleted": true,
    "isActive": false,
    "deletedAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "Voucher deleted"
}

响应字段:

返回软删除后的礼券对象。关键字段为 _id、isDeleted: true、isActive: false、deletedAt;响应中也可能包含其他礼券字段。


复制礼券 ​

基于现有模板复制一条新礼券。

POST /vouchers/:voucherId/duplicate

路径参数:

参数类型必填说明
voucherIdstring是待复制的礼券模板 ID

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/duplicate" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439013",
    "name": "20% Off Discount (Copy)",
    "description": "Get 20% off your next purchase",
    "value": 20,
    "valueType": "percentage",
    "claimedQuantity": 0,
    "createdAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "Voucher duplicated"
}

响应字段:

返回复制后的完整礼券对象。副本会复制原模板字段,将 name 加上 (Copy) 后缀,将 claimedQuantity 重置为 0,并部署新的 contractAddress。


向用户发放礼券(send) ​

将礼券直接发放给指定用户:系统会铸造对应 NFT(如链上流程尚未完成则可能延后),并创建一条状态为 active 的用户礼券记录;不经过活动领取流程。

POST /vouchers/:voucherId/send

/send 与 /send-campaign 如何选择

当你需要直接发放(不占用活动配额)并且希望响应中获得完整的 UserVoucher 文档(包含 redemptionCode、expiresAt、nftTokenId 等字段)时,使用本接口。

若需要平台自动挑选一个仍有配额的活动来发放礼券(小程序场景常见,奖品配置中通常只保存 voucher schema id),请使用 通过活动发放礼券(send-campaign)。两个接口返回结构不同,详见对应章节。

路径参数:

参数类型必填说明
voucherIdstring是礼券模板 ID

请求体:

字段类型必填约束说明
userIdstring是至少 1 字符,MongoDB ObjectId(24 位十六进制)接收用户 ID

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/send" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "507f1f77bcf86cd799439015"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439050",
    "userId": "507f1f77bcf86cd799439015",
    "voucherId": "507f1f77bcf86cd799439011",
    "tenantId": "507f1f77bcf86cd799439001",
    "nftTokenId": "42",
    "status": "active",
    "redemptionCode": "VCH-M1ABC2-XY3Z",
    "expiresAt": "2024-08-31T23:59:59.000Z",
    "settlementAmount": 0,
    "settlementCurrency": "HKD",
    "claimedAt": "2024-02-01T12:00:00.000Z",
    "createdAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "Voucher sent to user"
}

响应字段:

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId用户礼券记录(UserVoucher)ID
userIdstring否MongoDB ObjectId接收用户
voucherIdstring否MongoDB ObjectId礼券模板 ID
tenantIdstring否MongoDB ObjectId所属租户
nftTokenIdstring是链上 token ID 的数字字符串;铸造中可为 nullNFT token ID
statusstring否枚举:active、claimed、redeemed、expired;新发放恒为 active领取记录状态
redemptionCodestring否格式 VCH-{base36 时间戳}-{4 位随机}用户出示用于核销的码
expiresAtstring是ISO 8601;null 表示不设过期该条领取的过期时间:fixed_date 取模板 endDate,relative 取「领取日 + validityDays」
settlementAmountnumber否>= 0,默认 0发放时自模板复制的跨租户结算金额
settlementCurrencystring否ISO 4217 大写settlementAmount 币种
claimedAtstring否ISO 8601发放(到账)时间
createdAtstring否ISO 8601记录创建时间

错误响应:

HTTP 状态错误码说明
400VALIDATION_ERROR礼券未启用、已过期、已抢完,或用户已达领取上限
404NOT_FOUND礼券或用户不存在

通过活动发放礼券(send-campaign) ​

将礼券通过仍有配额的有效活动发放给指定用户:平台会自动从包含该 voucher schema 的活动中挑选一个可用的活动来下发。打卡、签到、抽奖、收据领奖、集章、推荐、摇一摇等小程序通常只保存 voucher schema id,因此默认使用本接口由 VIO 选择活动。

POST /vouchers/:voucherId/send-campaign

所需 Scope: vouchers

路径参数:

参数类型必填说明
voucherIdstring是礼券模板(schema)ID(Voucher._id)

请求体:

字段类型必填约束说明
userIdstring是至少 1 字符,MongoDB ObjectId(24 位十六进制)接收用户 ID
campaignIdstring否MongoDB ObjectId(24 位十六进制)指定活动 ID。若不传,VIO 会自动选取一个包含该 voucher schema 且仍可发放的有效活动

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/send-campaign" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "507f1f77bcf86cd799439015"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "voucherNftId": "507f1f77bcf86cd799439050",
    "campaignId": "507f1f77bcf86cd799439020",
    "campaignVoucherSchemaId": "507f1f77bcf86cd799439030",
    "voucherSchemaId": "507f1f77bcf86cd799439011"
  },
  "message": "Campaign voucher sent to user"
}

响应字段:

字段类型可空说明
voucherNftIdstring否新建的用户礼券(UserVoucher)_id,即用户领取记录的 ID。请见下方提示。
campaignIdstring否实际选中的活动 ID
campaignVoucherSchemaIdstring否该活动下对应的 campaign-voucher 条目 ID
voucherSchemaIdstring否礼券模板(schema)ID,与路径参数一致,方便客户端使用

与 /send 的响应结构不同

/send-campaign 不返回完整的 UserVoucher 文档;用户礼券 ID 字段名是 voucherNftId,而不是 _id。

如果你之前调用 POST /vouchers/:voucherId/send 并读取了 data._id,仅把 URL 改为 /send-campaign 会静默失效:

  • /send 返回 { success, data: { _id, voucherId, userId, redemptionCode, expiresAt, ... } }。
  • /send-campaign 返回 { success, data: { voucherNftId, campaignId, campaignVoucherSchemaId, voucherSchemaId } }。

解析时应读取 data.voucherNftId 拿到 UserVoucher _id。下面的兼容写法可同时支持两种返回结构:

js
function extractUserVoucherId(body) {
  const data = body?.data;
  if (!data || typeof data !== 'object') return null;
  return data.voucherNftId || data.userVoucherId || data._id || data.id || null;
}

/send-campaign 也不会返回 redemptionCode、expiresAt、nftTokenId 等字段。如果需要这些信息,请在发券后调用 GET /users/:userId/vouchers(按 voucherNftId 过滤),或改用 /send。

跳转到会员端的礼券详情页

小程序发券成功后,使用宿主提供的 redirectUrl 跳转到会员端的礼券详情页:

js
// voucherNftId 即 /send-campaign 返回的 UserVoucher _id
window.top.location.href = `${redirectUrl}/voucher/${voucherNftId}`;

请使用 window.top.location.href(而不是 window.location.href),以便跳出小程序 iframe。

错误响应:

HTTP 状态错误码说明
400VALIDATION_ERROR礼券未启用 / 未到生效时间 / 已过期 / 已抢完,找不到可发放的有效活动,或用户已达每人领取上限
404NOT_FOUND礼券模板或用户不存在

关于 maxClaimsPerUser:若礼券模板配置了 maxClaimsPerUser 且用户已达上限,响应为 HTTP 400,error.message 中包含 maximum claim limit for this voucher。允许在该错误时重抽的小程序(如抽奖)应识别该错误并改抽其他奖品,而非直接把错误抛给用户。


列举礼券兑换记录 ​

分页查询当前租户下所有用户礼券(领取)记录。

GET /vouchers/claims/list

查询参数:

参数类型必填约束说明
pagestring否解析为整数,最小 1,默认 1页码
limitstring否解析为整数,最小 1,默认 20每页条数
voucherIdstring否MongoDB ObjectId按礼券模板 ID 筛选
statusstring否枚举:active、claimed、redeemed、expired按领取记录状态筛选
fromDatestring否ISO 8601起始时间(含)
toDatestring否ISO 8601结束时间(含)
searchstring否任意字符串按用户姓名或邮箱模糊搜索

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/vouchers/claims/list?status=active&limit=10" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439014",
      "voucherId": {
        "_id": "507f1f77bcf86cd799439011",
        "name": "20% Off Discount",
        "images": ["https://example.com/image.jpg"],
        "value": 20,
        "valueType": "percentage"
      },
      "userId": {
        "_id": "507f1f77bcf86cd799439015",
        "displayName": "John Doe",
        "email": "john@example.com",
        "avatar": "https://example.com/avatar.jpg"
      },
      "tenantId": "507f1f77bcf86cd799439001",
      "status": "active",
      "redemptionCode": "VCH-M1ABC2-XY3Z",
      "claimedAt": "2024-01-15T14:30:00.000Z",
      "expiresAt": "2024-12-31T23:59:59.000Z",
      "settlementAmount": 80,
      "settlementCurrency": "THB"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 45,
    "totalPages": 5,
    "hasNextPage": true,
    "hasPrevPage": false
  }
}

响应字段(data 数组每一项):

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId领取记录 ID
voucherIdobject否已填充的礼券摘要对象关联礼券模板
voucherId._idstring否MongoDB ObjectId礼券模板 ID
voucherId.namestring否已 trim礼券名称
voucherId.imagesstring[]否URL 数组,可为 []礼券图片
voucherId.valuenumber否>= 0礼券数值
voucherId.valueTypestring否枚举:fixed、percentage数值语义
userIdobject否已填充的用户摘要对象接收/领取该礼券的用户
userId._idstring否MongoDB ObjectId用户 ID
userId.displayNamestring是可能为 null用户展示名
userId.emailstring是邮箱用户邮箱
userId.avatarstring是URL 或 null用户头像
tenantIdstring否MongoDB ObjectId领取记录所属租户
voucherOwnerTenantIdstring是MongoDB ObjectId礼券模板所属租户
nftTokenIdstring是链上 token ID,可为 null该领取记录对应的 NFT token
statusstring否枚举:active、claimed、redeemed、expired当前状态
redemptionCodestring否格式 VCH-{base36 时间戳}-{4 位随机}核销码
claimedAtstring否ISO 8601领取/发放时间
redeemedAtstring是ISO 8601,可为 null核销时间
expiresAtstring是ISO 8601,null 表示不设过期该条记录过期时间
campaignIdstring是MongoDB ObjectId,可为 null关联活动 ID
settlementAmountnumber否>= 0,默认 0领取时记录的结算金额
settlementCurrencystring否ISO 4217结算币种
tokenAmountPaidstring否数字字符串,默认 "0"领取时支付的代币数量
redemptionDetailsobject是对象或 null核销后的元数据
externalVoucherCodestring是字符串或 null外部供货返回的券码
externalRedemptionUrlstring是URL 或 null外部供货返回的核销链接
externalOrderIdstring是供货方订单 ID 或 null外部供货订单 ID
externalBuyStatusstring是枚举:pending、recorded、failed、null外部购买记录状态
externalFulfillmentStatusstring是枚举:pending、processing、fulfilled、failed、null外部履约状态
externalFulfillmentErrorstring是字符串或 null外部履约错误详情
externalIsDirectRechargeboolean否true / false,默认 false是否为直充履约
externalRechargeAccountstring是字符串或 null直充账号
metadataobject否对象,默认 {}扩展元数据
createdAtstring否ISO 8601记录创建时间
updatedAtstring否ISO 8601最近更新时间

此处 voucherId 仅为摘要(_id、name、images、value、valueType),不包含 consumptionType。需要核销方式时请调用 GET /vouchers/:voucherId 或 GET /users/:userId/vouchers。


按核销码核销 ​

根据用户出示的核销码完成核销;适用于门店收银、自助结算等场景(API Key 鉴权)。

POST /vouchers/redeem-by-code

请求体:

字段类型必填约束说明
redemptionCodestring是至少 1 字符,区分大小写领取记录上的核销码(如 VCH-M1ABC2-XY3Z)
locationstring否无最大长度核销地点(如门店名、分店)
notesstring否无最大长度备注(如订单号)

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/redeem-by-code" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "redemptionCode": "ABC123XYZ",
    "location": "Store #42",
    "notes": "Customer purchased item XYZ"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439014",
    "voucherId": "507f1f77bcf86cd799439011",
    "userId": "507f1f77bcf86cd799439015",
    "status": "redeemed",
    "redemptionCode": "VCH-M1ABC2-XY3Z",
    "claimedAt": "2024-01-15T14:30:00.000Z",
    "redeemedAt": "2024-02-01T10:15:00.000Z",
    "redemptionDetails": {
      "location": "Store #42",
      "notes": "Customer purchased item XYZ",
      "method": "api_key",
      "redeemedBy": "api:507f1f77bcf86cd799439099",
      "redeemedByType": "api_key",
      "redeemedByApiKey": "507f1f77bcf86cd799439099"
    },
    "externalVoucherCode": "ABC-123-XYZ",
    "externalRedemptionUrl": null,
    "externalOrderId": "69f75fe31134f32e472b2f98",
    "externalFulfillmentStatus": "fulfilled"
  },
  "message": "Voucher redeemed"
}

响应字段:

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId领取记录 ID
voucherIdstring 或 object否MongoDB ObjectId 或已填充的礼券对象礼券模板
userIdstring否MongoDB ObjectId礼券持有人原用户 ID
statusstring否成功后为 redeemed状态
redemptionCodestring否格式 VCH-{base36 时间戳}-{4 位随机}已核销的码
claimedAtstring否ISO 8601原领取时间
redeemedAtstring否ISO 8601核销时间
redemptionDetails.locationstring是请求未传则可能不存在核销地点
redemptionDetails.notesstring是请求未传则可能不存在核销备注
redemptionDetails.methodstring否api_key核销方式
redemptionDetails.redeemedBystring否api:{apiKeyId}记录为核销主体的 API Key
redemptionDetails.redeemedByTypestring否api_key核销主体类型
redemptionDetails.redeemedByApiKeystring是MongoDB ObjectIdAPI Key ID
externalVoucherCodestring是字符串或 null外部供货返回的券码
externalRedemptionUrlstring是URL 或 null外部供货返回的核销链接
externalOrderIdstring是供货方订单 ID 或 null外部供货订单 ID
externalFulfillmentStatusstring是枚举:fulfilled、failed、processing、pending、null外部履约状态

按核销码查询信息(核销前) ​

在正式核销前根据核销码拉取礼券与领取信息,用于收银台展示与校验。

GET /vouchers/redeem/:code/info

路径参数:

参数类型必填说明
codestring是核销码

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/vouchers/redeem/ABC123XYZ/info" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "redemptionCode": "VCH-M1ABC2-XY3Z",
    "status": "active",
    "voucher": {
      "name": "20% Off Discount",
      "description": "Get 20% off your next purchase",
      "value": 20,
      "valueType": "percentage",
      "valueCurrency": "THB",
      "image": "https://example.com/image.jpg",
      "terms": "Valid on orders over $50"
    },
    "tenant": {
      "name": "Demo Tenant",
      "logo": "https://example.com/logo.png",
      "slug": "demo-tenant"
    },
    "pinPrefix": "VI",
    "expiresAt": "2024-12-31T23:59:59.000Z",
    "canRedeem": true
  }
}

响应字段:

字段类型可空约束 / 格式说明
redemptionCodestring否核销码本次查询的核销码
statusstring否枚举:active、claimed、redeemed、expired展示状态;已过期的 active 记录返回 expired
voucherobject是对象或 null礼券展示摘要
voucher.namestring否已 trim礼券名称
voucher.descriptionstring否可为 ""说明
voucher.valuenumber否>= 0面额/折扣数值
voucher.valueTypestring否枚举:fixed、percentage数值语义
voucher.valueCurrencystring否ISO 4217fixed 类时的币种
voucher.imagestring是URL 或 null第一张礼券图片
voucher.termsstring否可为 ""条款
tenantobject是对象或 null礼券创建方租户摘要
tenant.namestring否文本租户名称
tenant.logostring是URL 或 null租户 logo
tenant.slugstring是slug 或 null租户 slug
pinPrefixstring否文本,如 VIPIN 核销时应输入的前缀
expiresAtstring是ISO 8601,null 表示不设过期该条领取记录的过期时间
canRedeemboolean否true / false当前状态是否允许核销

此处 voucher 为展示摘要,不包含 consumptionType。判断核销方式请使用 GET /vouchers 或 GET /vouchers/:voucherId。


店员 PIN 核销(consume) ​

使用店员个人 PIN 在核销前完成身份校验,再将礼券标记为已消费。适用于门店需落实到具体店员责任的场景。

重要:PIN 校验归属「创建礼券」的组织。 PIN 与创建该礼券模板的租户(或子公司)绑定,而非分发活动的组织。跨租户场景下(例如 A 公司创建礼券、B 公司通过活动分发),会员须在 A 公司门店 由 A 公司店员输入 PIN。若礼券由子公司创建,仅该公司子公司的 PIN 有效。

PIN 权限: 店员 PIN 须具备 voucher_redemption 权限方可核销礼券;仅有 token_claim 的 PIN 不能用于礼券核销。PIN 权限配置见管理后台文档。创建、列表、更新或单独校验 PIN(不核销礼券)请使用 /staff-pins(staff_pins scope)。

按码核销(redeem-by-code)与本接口(PIN)的区别

  • 按码核销(POST /vouchers/redeem-by-code):凭 API Key 鉴权,核销主体记为 API Key 对应方;适合服务端对接。
  • PIN 核销(POST /vouchers/redeem/:code/pin):凭店员 PIN 鉴权,核销主体记为具体店员;适合门店落实到人。
POST /vouchers/redeem/:code/pin

路径参数:

参数类型必填说明
codestring是领取记录上的核销码

请求体:

字段类型必填约束说明
pinstring是5–20 字符;常见为 2 位字母前缀 + 4 位数字(如 HA1234);须具备 voucher_redemption 权限店员 PIN

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/redeem/ABC123XYZ/pin" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "pin": "HA1234"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "success": true,
    "voucher": {
      "name": "20% Off Discount",
      "value": 20,
      "valueType": "percentage",
      "valueCurrency": "THB"
    },
    "redeemedAt": "2024-02-01T10:15:00.000Z",
    "redeemedBy": "John Staff"
  },
  "message": "Voucher consumed"
}

响应字段:

字段类型可空约束 / 格式说明
successboolean否成功时为 true业务成功标记
voucher.namestring否已 trim已核销礼券名称
voucher.valuenumber否>= 0面额/折扣数值
voucher.valueTypestring否枚举:fixed、percentage数值语义
voucher.valueCurrencystring否ISO 4217fixed 类时的币种
redeemedAtstring否ISO 8601核销时间
redeemedBystring否文本校验 PIN 的店员展示名

错误响应:

HTTP 状态错误码说明
400VALIDATION_ERRORPIN 格式无效(须 5–20 字符)
400VALIDATION_ERRORPIN 无效(前缀或号码与任一店员不匹配)
400VALIDATION_ERRORPIN 缺少 voucher_redemption 权限
400VALIDATION_ERROR礼券已核销或已过期
404NOT_FOUND无此核销码对应的礼券记录

Token 兑换礼券(原子操作) ​

在一次 API 调用中,原子性地从用户余额扣减 token 并发放礼券。该接口执行的操作与会员端自助从活动领券完全相同——包括 token 扣减、公司钱包入账、NFT 铸造、库存预留及结算记录——但由服务端通过 API Key 触发。

所需 Scope: vouchers 且 tokens(API Key 须同时拥有两个 scope)

POST /vouchers/:voucherId/redeem-with-tokens

路径参数:

参数类型必填说明
voucherIdstring是要为用户领取的礼券 ID

请求体:

字段类型必填约束说明
userIdstring是至少 1 字符,MongoDB ObjectId将收到礼券并支付 token 的用户
campaignIdstring是至少 1 字符,MongoDB ObjectId决定 token 价格和配额的活动
idempotencyKeystring否最长 128 字符防止重复扣款。若重复提交相同 key,将返回首次调用的结果而非再次扣减

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/vouchers/507f1f77bcf86cd799439011/redeem-with-tokens" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "507f1f77bcf86cd799439015",
    "campaignId": "507f1f77bcf86cd799439020",
    "idempotencyKey": "order-12345-voucher-abc"
  }'

响应示例(首次调用):

json
{
  "success": true,
  "data": {
    "userVoucher": {
      "_id": "507f1f77bcf86cd799439050",
      "userId": "507f1f77bcf86cd799439015",
      "voucherId": "507f1f77bcf86cd799439011",
      "tenantId": "507f1f77bcf86cd799439001",
      "campaignId": "507f1f77bcf86cd799439020",
      "nftTokenId": "42",
      "status": "active",
      "redemptionCode": "VCH-M1ABC2-XY3Z",
      "expiresAt": "2026-12-31T23:59:59.000Z",
      "tokenAmountPaid": "100",
      "settlementAmount": 0,
      "settlementCurrency": "THB",
      "claimedAt": "2026-06-18T06:00:00.000Z"
    },
    "voucher": {
      "_id": "507f1f77bcf86cd799439011",
      "name": "20% Off Discount"
    },
    "nftTokenId": "42",
    "growthPointsEarned": 10
  },
  "message": "Voucher claimed with tokens"
}

响应示例(幂等重放):

json
{
  "success": true,
  "data": {
    "userVoucher": { "..." },
    "idempotent": true
  },
  "message": "Voucher already claimed (idempotent)"
}

响应字段:

字段类型可为空说明
userVoucherobject否创建的 UserVoucher 记录(与 /send 响应结构一致)
userVoucher.tokenAmountPaidstring否实际扣减的 token 数量(已应用会员折扣,若有)
userVoucher.campaignIdstring否此领取对应的活动 ID
voucherobject否礼券模板摘要
nftTokenIdstring是为此领取铸造的链上 NFT token ID
growthPointsEarnednumber是因此次付费兑换获得的成长值(仅 token 价格 > 0 时有值)
idempotentboolean是若为 true,表示此响应是之前成功调用的幂等重放

原子性保证:

本接口提供以下保证:

  1. Token 扣减原子化:使用 MongoDB 条件更新(balance >= cost),在并发请求下防止双重扣款。
  2. 补偿式回滚:如果 token 扣减之后的任何步骤失败(NFT 铸造、记录创建等),所有变更均会回滚——用户 token 恢复、库存计数器释放、部分记录清理。
  3. 幂等性:提供 idempotencyKey 后,重复请求返回首次结果,不会再次扣款。

内部执行流程:

  1. 校验活动、礼券、用户资格及会员等级限制
  2. 检查并预留礼券库存 + 活动配额 + 用户领取上限
  3. 从用户余额扣减 token(FIFO 批次消费)
  4. 将 token 入账至公司管理员钱包
  5. 创建 token 交易记录
  6. 为用户铸造 NFT 至区块链钱包
  7. 创建 UserVoucher 记录
  8. 记录结算交易(processedByType: api_key)
  9. 发放成长值(若适用)

错误响应:

HTTP 状态错误码说明
400VALIDATION_ERROR活动未开始、已结束或未激活
400VALIDATION_ERROR礼券不在此活动中
400VALIDATION_ERRORToken 余额不足
400VALIDATION_ERROR礼券已售罄或活动配额已用完
400VALIDATION_ERROR用户已达领取上限
400VALIDATION_ERROR用户会员等级不符合此活动要求
403FORBIDDENAPI Key 缺少所需的 tokens scope
404NOT_FOUND礼券、用户或活动不存在

7. API 参考:用户(Users) ​

所需 Scope: users

列举用户 ​

分页获取当前租户下的用户列表。

GET /users

查询参数:

参数类型必填说明
pageinteger否页码(默认 1)
limitinteger否每页条数(默认 20)
rolestring否按角色筛选:super_admin、tenant_admin、sub_company_admin、member
searchstring否按邮箱、手机号或姓名搜索
isActiveboolean否是否仅返回启用用户
subCompanyIdstring否按子公司 ID 筛选

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/users?role=member&isActive=true&limit=10" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439015",
      "email": "john@example.com",
      "phone": "+66812345678",
      "displayName": "John Doe",
      "role": "member",
      "isActive": true,
      "walletAddress": "0x1234567890abcdef...",
      "registrationSource": "direct",
      "storeId": null,
      "campaignId": null,
      "createdAt": "2024-01-01T00:00:00.000Z",
      "updatedAt": "2024-01-15T10:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 150,
    "totalPages": 15,
    "hasNextPage": true,
    "hasPrevPage": false
  }
}

创建用户 ​

在当前租户下创建新用户。查重范围限定在当前门户(租户 + 子公司)内 — 相同邮箱/手机可存在于不同门户。创建时会自动生成 GlobalIdentity 以支持跨租户 SSO。

POST /users

请求体:

字段类型必填说明
emailstring否*邮箱地址
phonestring否*手机号
passwordstring是密码(至少 6 位)
displayNamestring否展示名
rolestring否member 或 sub_company_admin(默认 member)
subCompanyIdstring否指派到的子公司 ID
metadataobject否自定义键值元数据

*email 与 phone 至少填一项。

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/users" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newuser@example.com",
    "phone": "+66812345678",
    "password": "securepassword123",
    "displayName": "New User",
    "role": "member",
    "metadata": {
      "referralSource": "website",
      "tier": "gold"
    }
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439016",
    "email": "newuser@example.com",
    "phone": "+66812345678",
    "displayName": "New User",
    "role": "member",
    "isActive": true,
    "walletAddress": "0xabcdef1234567890...",
    "metadata": {
      "referralSource": "website",
      "tier": "gold"
    },
    "createdAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "User created"
}

查找或创建用户 ​

幂等端点:如果用户已存在于当前门户(tenant + subCompany)则直接返回;如果用户的邮箱/手机号已在其他租户注册过,则自动创建关联 profile(共享显示名称、头像和钱包地址);否则创建全新用户。

POST /users/find-or-create

请求参数:

字段类型必填描述
emailstring否*用户邮箱
phonestring否*用户手机号
passwordstring是密码(至少 6 位)
displayNamestring否显示名称(仅用于新用户)
subCompanyIdstring否要分配到的子公司 ID
metadataobject否自定义元数据键值对

*email 和 phone 至少需要填写一个。

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/users/find-or-create" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "password": "securepassword123",
    "displayName": "Alice"
  }'

响应场景:

响应中包含两个布尔标志:

标志描述
createdtrue 表示创建了新用户,false 表示返回了已有用户
linkedtrue 表示用户被关联到已有的跨租户身份

场景 1 — 用户已存在于当前门户(HTTP 200):

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439016",
    "email": "alice@example.com",
    "displayName": "Alice",
    "isActive": true,
    "created": false,
    "linked": false
  },
  "message": "User already exists in this portal"
}

场景 2 — 用户存在于其他租户,创建关联 profile(HTTP 201):

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439017",
    "email": "alice@example.com",
    "displayName": "Alice",
    "globalIdentityId": "609f1f77bcf86cd799439099",
    "walletAddress": "0xabcdef...",
    "isActive": true,
    "created": true,
    "linked": true
  },
  "message": "User profile created and linked to existing identity"
}

场景 3 — 全新用户(HTTP 201):

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439018",
    "email": "alice@example.com",
    "displayName": "Alice",
    "globalIdentityId": "609f1f77bcf86cd799439100",
    "walletAddress": "0x123456...",
    "isActive": true,
    "created": true,
    "linked": false
  },
  "message": "User created"
}

何时使用 find-or-create vs. create

当从外部系统同步用户时,推荐使用 POST /users/find-or-create — 可以安全地重复调用,并自动处理跨租户关联。当需要严格控制,希望用户已存在时返回错误,则使用 POST /users。


获取用户详情 ​

按用户 ID 获取详细信息。

GET /users/:userId

路径参数:

参数类型必填说明
userIdstring是用户 ID

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439015",
    "email": "john@example.com",
    "phone": "+66812345678",
    "displayName": "John Doe",
    "role": "member",
    "isActive": true,
    "walletAddress": "0x1234567890abcdef...",
    "avatar": "https://example.com/avatar.jpg",
    "metadata": {
      "tier": "gold"
    },
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-01-15T10:30:00.000Z"
  }
}

更新用户 ​

更新已有用户信息。

PATCH /users/:userId

路径参数:

参数类型必填说明
userIdstring是用户 ID

请求体:

字段类型说明
displayNamestring展示名
avatarstring头像 URL
isActiveboolean是否启用
subCompanyIdstring子公司 ID
metadataobject自定义元数据

请求示例:

bash
curl -X PATCH "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "John Smith",
    "metadata": {
      "tier": "platinum"
    }
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439015",
    "displayName": "John Smith",
    "metadata": {
      "tier": "platinum"
    },
    "updatedAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "User updated"
}

停用用户 ​

软删除用户(将 isActive 置为 false)。

DELETE /users/:userId

路径参数:

参数类型必填说明
userIdstring是用户 ID

请求示例:

bash
curl -X DELETE "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": null,
  "message": "User deactivated"
}

获取用户代币余额 ​

查询指定用户在各代币下的可用余额(含锁定余额)。

GET /users/:userId/balances

路径参数:

参数类型必填说明
userIdstring是用户 ID

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015/balances" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "tokenId": "507f1f77bcf86cd799439020",
      "tokenName": "Loyalty Points",
      "symbol": "LP",
      "balance": "1500",
      "lockedBalance": "0"
    },
    {
      "tokenId": "507f1f77bcf86cd799439021",
      "tokenName": "Reward Coins",
      "symbol": "RC",
      "balance": "250",
      "lockedBalance": "50"
    }
  ]
}

获取用户已领取礼券 ​

分页返回该用户已领取的礼券记录。

GET /users/:userId/vouchers

路径参数:

参数类型必填说明
userIdstring是用户 ID

查询参数:

参数类型必填说明
pageinteger否页码(默认 1)
limitinteger否每页条数(默认 20)
statusstring否按状态筛选:active、redeemed、expired

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/users/507f1f77bcf86cd799439015/vouchers?status=active" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439014",
      "voucherId": {
        "_id": "507f1f77bcf86cd799439011",
        "name": "20% Off Discount",
        "value": 20,
        "valueType": "percentage",
        "consumptionType": "coupon_code",
        "externalProvider": "vouchain",
        "externalId": "VCH-EXT-001"
      },
      "status": "active",
      "redemptionCode": "ABC123XYZ",
      "claimedAt": "2024-01-15T14:30:00.000Z",
      "expiresAt": "2024-12-31T23:59:59.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 5,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

响应字段(data 数组每一项):

voucherId 是已填充的完整礼券对象(字段与「列举礼券」一致,不含 externalAccountOptions)。没有名为 voucher 的别名字段。

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId领取记录 ID
voucherIdobject否已填充的礼券对象礼券模板
voucherId.consumptionTypestring否枚举:vio_code、coupon_code、url、qr_code、manual、zhichong核销交互方式
statusstring否枚举:active、redeemed、expired该条记录的展示状态
redemptionCodestring是唯一核销码用于核销的码
claimedAtstring否ISO 8601领取时间
expiresAtstring是ISO 8601;null 表示不设过期该条记录过期时间

按标识符搜索用户 ​

通过邮箱、手机号或钱包地址定位用户。

GET /users/search/by-identifier

查询参数:

参数类型必填说明
emailstring否*邮箱
phonestring否*手机号
walletAddressstring否*钱包地址

*至少提供上述之一。

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/users/search/by-identifier?email=john@example.com" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439015",
    "email": "john@example.com",
    "phone": "+66812345678",
    "displayName": "John Doe",
    "role": "member",
    "isActive": true,
    "walletAddress": "0x1234567890abcdef..."
  }
}

8. API 参考:活动(Campaigns) ​

所需 Scope: campaigns

列举活动 ​

分页获取当前租户下的礼券活动列表。

GET /campaigns

查询参数:

参数类型必填约束说明
pagestring否解析为整数,最小 1,默认 1页码(从 1 起)
limitstring否解析为整数,最小 1,默认 20每页条数
searchstring否任意字符串按活动名称模糊搜索
isActivestring否字符串 "true" / "false",解析为布尔值按是否启用筛选

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/campaigns?isActive=true&limit=10" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439030",
      "name": "Summer Promotion",
      "description": "Summer 2024 voucher campaign",
      "slug": "summer-promotion",
      "tokenId": "507f1f77bcf86cd799439020",
      "isActive": true,
      "isPublic": true,
      "startDate": "2024-06-01T00:00:00.000Z",
      "endDate": "2024-08-31T23:59:59.000Z",
      "images": ["https://example.com/campaign.jpg"],
      "createdAt": "2024-05-15T10:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 5,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

响应字段(data 数组每一项):

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId(24 位十六进制)活动唯一 ID
namestring否至少 1 字符,已 trim活动展示名称
descriptionstring否可为空串 ""活动说明
slugstring否小写、URL 安全字符;租户+子公司内唯一活动落地页友好路径(如 summer-promotion)
tokenIdstring否MongoDB ObjectId(24 位十六进制)用户领取礼券时需消耗的代币 ID
isActiveboolean否true / false活动是否启用
isPublicboolean否true / false是否公开(免登录可访问)
startDatestring是ISO 8601;未传时默认为创建时间活动开始时间
endDatestring是ISO 8601;null 表示不设结束时间活动结束时间
imagesstring[]否合法 URL 数组,可为 []横幅/封面图
createdAtstring否ISO 8601创建时间

创建活动 ​

新建一条礼券活动。

POST /campaigns

请求体:

字段类型必填约束说明
namestring是至少 1 字符,已 trim活动名称;未传 slug 时据此自动生成
descriptionstring否无最大长度,默认 ""活动说明
slugstring否小写、URL 安全;租户+子公司内唯一;省略时对 name 做 slugify活动页路径标识(如 summer-promotion-2024)
tokenIdstring是MongoDB ObjectId,须为已存在的 Token用户领取消耗用的代币
startDatestring否ISO 8601,默认当前时间开始接受领取的时间
endDatestring否ISO 8601,宜晚于 startDate;省略表示不设结束活动结束时间
isActiveboolean否默认 true停用后对用户隐藏
isPublicboolean否默认 true是否在公开页展示(无需登录)
imagesstring[]否每项为合法 URL,可为 [];首张为主图横幅/配图 URL

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/campaigns" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer Promotion 2024",
    "description": "Exclusive summer deals for loyal customers",
    "tokenId": "507f1f77bcf86cd799439020",
    "startDate": "2024-06-01T00:00:00.000Z",
    "endDate": "2024-08-31T23:59:59.000Z",
    "isPublic": true
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439031",
    "name": "Summer Promotion 2024",
    "description": "Exclusive summer deals for loyal customers",
    "slug": "summer-promotion-2024",
    "tokenId": "507f1f77bcf86cd799439020",
    "isActive": true,
    "isPublic": true,
    "startDate": "2024-06-01T00:00:00.000Z",
    "endDate": "2024-08-31T23:59:59.000Z",
    "createdAt": "2024-05-15T10:00:00.000Z"
  },
  "message": "Campaign created"
}

响应字段:

返回新建的活动对象;各字段含义参见 列举活动 中的响应字段。


获取活动详情 ​

按活动 ID 获取单个活动的完整信息。

GET /campaigns/:campaignId

路径参数:

参数类型必填说明
campaignIdstring是活动 ID

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439030",
    "name": "Summer Promotion",
    "description": "Summer 2024 voucher campaign",
    "slug": "summer-promotion",
    "tokenId": "507f1f77bcf86cd799439020",
    "isActive": true,
    "isPublic": true,
    "startDate": "2024-06-01T00:00:00.000Z",
    "endDate": "2024-08-31T23:59:59.000Z",
    "images": ["https://example.com/campaign.jpg"],
    "createdAt": "2024-05-15T10:00:00.000Z"
  }
}

响应字段:

返回完整活动对象;各字段含义参见 列举活动 中的响应字段。


更新活动 ​

更新已有活动信息。

PATCH /campaigns/:campaignId

路径参数:

参数类型必填说明
campaignIdstring是活动 ID

请求体:

字段类型约束说明
namestring至少 1 字符,已 trim活动展示名称
descriptionstring无最大长度活动说明
tokenIdstringMongoDB ObjectId,须引用已存在代币活动消耗的代币
startDatestringISO 8601开始时间
endDatestringISO 8601结束时间
isActivebooleantrue / false是否启用
isPublicbooleantrue / false是否公开页可见
imagesstring[]每项为合法 URL配图 URL

请求示例:

bash
curl -X PATCH "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Extended Summer Promotion",
    "endDate": "2024-09-30T23:59:59.000Z"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439030",
    "name": "Extended Summer Promotion",
    "endDate": "2024-09-30T23:59:59.000Z",
    "updatedAt": "2024-08-15T10:00:00.000Z"
  },
  "message": "Campaign updated"
}

删除活动 ​

删除指定活动。

DELETE /campaigns/:campaignId

路径参数:

参数类型必填说明
campaignIdstring是活动 ID

请求示例:

bash
curl -X DELETE "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439030"
  },
  "message": "Campaign deleted"
}

响应字段:

字段类型约束 / 格式说明
_idstringMongoDB ObjectId(24 位十六进制)已删活动 ID

获取活动关联礼券 ​

分页返回已加入该活动的礼券模板及活动内配置。

GET /campaigns/:campaignId/vouchers

路径参数:

参数类型必填说明
campaignIdstring是活动 ID

查询参数:

参数类型必填说明
pageinteger否页码(默认 1)
limitinteger否每页条数(默认 20)

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439040",
      "campaignId": "507f1f77bcf86cd799439030",
      "voucherId": {
        "_id": "507f1f77bcf86cd799439011",
        "name": "20% Off Discount",
        "value": 20,
        "valueType": "percentage",
        "consumptionType": "coupon_code",
        "externalProvider": "vouchain",
        "externalId": "VCH-EXT-001",
        "isActive": true
      },
      "tokenAmount": "100",
      "tokenPrice": 0,
      "sortOrder": 1,
      "isMaxQuantity": false,
      "isActive": true,
      "campaignQuantity": 100,
      "campaignClaimedQuantity": 12,
      "campaignRemainingQuantity": 88
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 3,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

响应字段(data 数组每一项):

每一项是活动-礼券关联行,不是压平后的礼券。voucherId 是已填充的完整礼券对象(字段与「列举礼券」一致),包含 consumptionType。

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId活动-礼券行 ID
campaignIdstring否MongoDB ObjectId活动 ID
voucherIdobject否已填充的礼券对象礼券模板
voucherId.consumptionTypestring否枚举:vio_code、coupon_code、url、qr_code、manual、zhichong核销交互方式
tokenAmountstring否数字字符串;"0" 表示活动级不额外限制库存活动内该礼券的库存上限
tokenPricenumber否>= 0,默认 0本活动领取该礼券的代币价格
sortOrderinteger否默认 0,越小越靠前在活动内的展示顺序
isMaxQuantityboolean否true / false,默认 false是否沿用礼券自身库存上限
isActiveboolean否true / false该礼券在本活动中是否可领
campaignQuantityinteger是整数或 null活动内有效库存上限
campaignClaimedQuantityinteger否>= 0本活动行已领取次数
campaignRemainingQuantityinteger否>= -1;-1 表示不限量本活动剩余可领数量

向活动添加礼券 ​

将礼券模板加入活动,并设置活动维度代币定价/库存上限。

POST /campaigns/:campaignId/vouchers

路径参数:

参数类型必填说明
campaignIdstring是活动 ID

请求体:

字段类型必填约束说明
voucherIdsstring[]是至少 1 个 ObjectId;同一活动内重复提交会被拒绝要加入活动的礼券模板 ID 列表
tokenAmountstring是非空字符串,高精度存储;"0" 表示不在活动层做额外库存上限这些礼券在活动内的代币/库存上限配置

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "voucherIds": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"],
    "tokenAmount": "100"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "added": 2
  },
  "message": "Vouchers added to campaign"
}

响应字段:

字段类型约束 / 格式说明
addedinteger>= 0成功加入活动的礼券条数

更新活动内礼券配置 ​

调整某礼券在该活动内的排序、库存上限及是否可领取等。

PATCH /campaigns/:campaignId/vouchers/:voucherId

路径参数:

参数类型必填说明
campaignIdstring是活动 ID
voucherIdstring是礼券 ID

请求体:

字段类型约束说明
tokenAmountstring数字字符串;"0" 表示活动层不额外限制本礼券在该活动内的库存/代币上限
sortOrderinteger可为负数,默认 0,越小越靠前活动内展示顺序
isActivebooleantrue / false是否允许在本活动中领取(可临时关闭)

请求示例:

bash
curl -X PATCH "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers/507f1f77bcf86cd799439011" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "tokenAmount": "150",
    "sortOrder": 1
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "campaignId": "507f1f77bcf86cd799439030",
    "voucherId": "507f1f77bcf86cd799439011",
    "tokenAmount": "150",
    "sortOrder": 1
  },
  "message": "Campaign voucher updated"
}

响应字段:

字段类型约束 / 格式说明
campaignIdstringMongoDB ObjectId活动 ID
voucherIdstringMongoDB ObjectId礼券 ID
tokenAmountstring数字字符串更新后的活动库存上限
sortOrderinteger越小越靠前更新后的排序

从活动移除礼券 ​

将礼券从活动中移除(不删除礼券模板本身)。

DELETE /campaigns/:campaignId/vouchers/:voucherId

路径参数:

参数类型必填说明
campaignIdstring是活动 ID
voucherIdstring是礼券 ID

请求示例:

bash
curl -X DELETE "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/vouchers/507f1f77bcf86cd799439011" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": null,
  "message": "Voucher removed from campaign"
}

可取加入活动的礼券列表 ​

返回尚未加入该活动、可被添加的礼券模板(分页)。

GET /campaigns/:campaignId/available-vouchers

路径参数:

参数类型必填说明
campaignIdstring是活动 ID

查询参数:

参数类型必填约束说明
pagestring否解析为整数,最小 1,默认 1页码
limitstring否解析为整数,最小 1,默认 20每页条数
searchstring否任意字符串按礼券名称模糊搜索

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/campaigns/507f1f77bcf86cd799439030/available-vouchers?search=discount" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439013",
      "name": "10% New User Discount",
      "value": 10,
      "valueType": "percentage",
      "isActive": true
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 10,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

响应字段(data 数组每一项):

字段类型可空约束 / 格式说明
_idstring否MongoDB ObjectId礼券模板 ID
namestring否已 trim名称
valuenumber否>= 0面额/折扣
valueTypestring否fixed / percentage数值语义
isActiveboolean否true / false模板是否启用

9. API 参考:代币(Tokens) ​

所需 Scope: tokens

列举代币 ​

分页列出当前租户下的代币定义。

GET /tokens

查询参数:

参数类型必填说明
pageinteger否页码(默认 1)
limitinteger否每页条数(默认 20)
isActiveboolean否按是否启用筛选
subCompanyIdstring否按子公司筛选

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/tokens?isActive=true" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439020",
      "name": "Loyalty Points",
      "symbol": "LP",
      "description": "Earn points on every purchase",
      "totalSupply": "1000000",
      "isActive": true,
      "createdAt": "2024-01-01T00:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 2,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

获取代币详情 ​

按代币 ID 获取代币元数据。

GET /tokens/:tokenId

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439020",
    "name": "Loyalty Points",
    "symbol": "LP",
    "description": "Earn points on every purchase",
    "totalSupply": "1000000",
    "isActive": true,
    "decimals": 0,
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-01-15T10:30:00.000Z"
  }
}

获取代币统计 ​

返回某代币的供应量、持有人数等汇总指标。

GET /tokens/:tokenId/stats

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/stats" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "totalSupply": "1000000",
    "circulatingSupply": "750000",
    "holdersCount": 1250,
    "totalTransactions": 15000,
    "averageBalance": "600"
  }
}

获取代币持有者 ​

分页列出持有该代币的用户及其余额。

GET /tokens/:tokenId/holders

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

查询参数:

参数类型必填说明
pageinteger否页码(默认 1)
limitinteger否每页条数(默认 20)
sortstring否排序:balance、name、recent(默认 balance)
searchstring否按用户姓名或邮箱搜索

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/holders?sort=balance&limit=10" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "user": {
        "_id": "507f1f77bcf86cd799439015",
        "displayName": "John Doe",
        "email": "john@example.com"
      },
      "balance": "5000"
    },
    {
      "user": {
        "_id": "507f1f77bcf86cd799439016",
        "displayName": "Jane Smith",
        "email": "jane@example.com"
      },
      "balance": "3500"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 1250,
    "totalPages": 125,
    "hasNextPage": true,
    "hasPrevPage": false
  },
  "token": {
    "_id": "507f1f77bcf86cd799439020",
    "name": "Loyalty Points",
    "symbol": "LP"
  }
}

增发代币(mint) ​

向指定用户账户增发代币并记入流水。

POST /tokens/:tokenId/mint

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求体:

字段类型必填说明
toUserIdstring是接收用户 ID
amountstring是增发数量
memostring否备注

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/mint" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "toUserId": "507f1f77bcf86cd799439015",
    "amount": "1000",
    "memo": "Welcome bonus"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439040",
    "tokenId": "507f1f77bcf86cd799439020",
    "type": "mint",
    "amount": "1000",
    "toUserId": "507f1f77bcf86cd799439015",
    "memo": "Welcome bonus",
    "createdAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "Tokens minted successfully"
}

增发代币到企业(mint-to-company) ​

直接向另一家企业的管理员账户增发代币(通过邮箱或企业 URL 标识)。仅代币创建者(与你的 API Key 绑定的租户/子公司)可以调用此接口。

  • PUBLIC(公开) 代币:可增发给任意企业。
  • SHARED(共享) 代币:目标企业必须在该代币的共享列表中。
  • PRIVATE(私有) 代币:不能增发给其他企业。
POST /tokens/:tokenId/mint-to-company

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求体:

字段类型必填说明
identifierstring是目标企业管理员邮箱或企业 URL/slug
amountstring是增发数量
memostring否备注
expiryTypestring否permanent(默认)、fixed_date 或 duration_days
expiresAtstring否ISO 日期字符串。当 expiryType 为 fixed_date 时必填
expiryDaysnumber否正整数。当 expiryType 为 duration_days 时必填

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/mint-to-company" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "admin@partner-company.com",
    "amount": "10000",
    "memo": "Partner allocation"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439043",
    "tokenId": "507f1f77bcf86cd799439020",
    "type": "mint",
    "amount": "10000",
    "recipientAdmin": {
      "id": "507f1f77bcf86cd799439099",
      "email": "admin@partner-company.com",
      "displayName": "Partner Admin"
    },
    "recipientOrg": {
      "tenant": { "id": "507f1f77bcf86cd799439088", "name": "Partner Company", "slug": "partner-company" },
      "subCompany": null
    }
  },
  "message": "Tokens minted to company"
}

销毁代币(burn) ​

从指定用户余额中销毁代币。

POST /tokens/:tokenId/burn

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求体:

字段类型必填说明
fromUserIdstring是销毁来源用户 ID
amountstring是销毁数量
memostring否备注

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/burn" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "fromUserId": "507f1f77bcf86cd799439015",
    "amount": "500",
    "memo": "Redemption for reward"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439041",
    "tokenId": "507f1f77bcf86cd799439020",
    "type": "burn",
    "amount": "500",
    "fromUserId": "507f1f77bcf86cd799439015",
    "memo": "Redemption for reward",
    "createdAt": "2024-02-01T12:00:00.000Z"
  },
  "message": "Tokens burned successfully"
}

调整用户余额 ​

在公司管理池与指定用户之间划拨或收回代币。

POST /tokens/:tokenId/adjust

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求体:

字段类型必填说明
userIdstring是用户 ID
amountstring是调整数量
operationstring是send(拨给用户)或 recall(收回)
reasonstring是调整原因(审计用)

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/adjust" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "507f1f77bcf86cd799439015",
    "amount": "100",
    "operation": "send",
    "reason": "Manual adjustment for customer service"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "userBalance": "1100",
    "adminBalance": "8900",
    "transaction": {
      "_id": "507f1f77bcf86cd799439042",
      "tokenId": "507f1f77bcf86cd799439020",
      "type": "transfer",
      "amount": "100"
    }
  },
  "message": "Balance sent"
}

到期行为: 发放代币时,接收方分组的到期日继承来源分组在增发(mint)时定义的到期规则;发放与收回流程中不可单独覆盖到期日。从用户收回时,回到公司池的分组继承用户侧分组到期信息,以保持账本与可发放余额一致。

向用户发放代币 ​

从公司代币池向用户发放;等价于 adjust 且 operation 固定为 send 的简化接口。

POST /tokens/:tokenId/send

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求体:

字段类型必填说明
userIdstring是目标用户 ID
amountstring是发放数量
reasonstring否发放原因

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/send" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "507f1f77bcf86cd799439015",
    "amount": "1000",
    "reason": "Loyalty reward"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "userBalance": "2000",
    "adminBalance": "8000",
    "transaction": {
      "_id": "507f1f77bcf86cd799439043",
      "tokenId": "507f1f77bcf86cd799439020",
      "type": "transfer",
      "amount": "1000"
    }
  },
  "message": "Tokens sent to user"
}

错误响应:

HTTP 状态错误码说明
400VALIDATION_ERROR公司池余额不足,无法完成发放
404NOT_FOUND代币或用户不存在

从用户收回代币 ​

将用户余额中的代币收回至公司池;等价于 adjust 且 operation 固定为 recall。

POST /tokens/:tokenId/recall

路径参数:

参数类型必填说明
tokenIdstring是代币 ID

请求体:

字段类型必填说明
userIdstring是收回来源用户 ID
amountstring是收回数量
reasonstring否收回原因

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/tokens/507f1f77bcf86cd799439020/recall" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "507f1f77bcf86cd799439015",
    "amount": "500",
    "reason": "Balance correction"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "userBalance": "1500",
    "adminBalance": "8500",
    "transaction": {
      "_id": "507f1f77bcf86cd799439044",
      "tokenId": "507f1f77bcf86cd799439020",
      "type": "transfer",
      "amount": "500"
    }
  },
  "message": "Tokens recalled from user"
}

错误响应:

HTTP 状态错误码说明
400VALIDATION_ERROR用户余额不足,无法收回
404NOT_FOUND代币或用户不存在

列举代币流水 ​

分页查询代币转账、增发、销毁等流水记录。

GET /tokens/transactions/list

查询参数:

参数类型必填说明
pageinteger否页码(默认 1)
limitinteger否每页条数(默认 20)
tokenIdstring否按代币 ID 筛选
typestring否按类型:mint、transfer、burn、reward、redeem
fromDatedatetime否起始时间
toDatedatetime否结束时间
searchstring否按用户姓名或流水备注搜索

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/tokens/transactions/list?type=mint&limit=10" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "_id": "507f1f77bcf86cd799439040",
      "tokenId": "507f1f77bcf86cd799439020",
      "type": "mint",
      "amount": "1000",
      "toUserId": "507f1f77bcf86cd799439015",
      "memo": "Welcome bonus",
      "createdAt": "2024-02-01T12:00:00.000Z",
      "token": {
        "name": "Loyalty Points",
        "symbol": "LP"
      },
      "toUser": {
        "displayName": "John Doe",
        "email": "john@example.com"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 500,
    "totalPages": 50,
    "hasNextPage": true,
    "hasPrevPage": false
  }
}

用户代币余额(/tokens/balance/user/:userId) ​

与用户章节中 /users/:userId/balances 数据一致,仅为不同入口。

GET /tokens/balance/user/:userId

路径参数:

参数类型必填说明
userIdstring是用户 ID

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/tokens/balance/user/507f1f77bcf86cd799439015" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": [
    {
      "tokenId": "507f1f77bcf86cd799439020",
      "tokenName": "Loyalty Points",
      "symbol": "LP",
      "balance": "1500",
      "lockedBalance": "0"
    },
    {
      "tokenId": "507f1f77bcf86cd799439021",
      "tokenName": "Reward Coins",
      "symbol": "RC",
      "balance": "250",
      "lockedBalance": "50"
    }
  ]
}

10. API 参考:分析(Analytics) ​

所需 Scope: analytics

分析总览 ​

返回租户用户、礼券、交易等汇总指标。

GET /analytics/overview

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/analytics/overview" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "users": {
      "total": 5000,
      "active": 4500,
      "newThisMonth": 250
    },
    "vouchers": {
      "totalClaims": 12000,
      "redeemed": 8500,
      "active": 3500,
      "redemptionRate": "70.83%"
    },
    "transactions": {
      "total": 25000,
      "thisMonth": 3500
    },
    "generatedAt": "2024-02-01T12:00:00.000Z"
  }
}

礼券分析 ​

按时间范围返回礼券兑换/核销趋势及 Top 礼券等。

GET /analytics/vouchers

查询参数:

参数类型必填说明
fromDatedatetime否统计开始时间
toDatedatetime否统计结束时间

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/analytics/vouchers?fromDate=2024-01-01T00:00:00.000Z&toDate=2024-01-31T23:59:59.000Z" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "totalVouchers": 50,
    "totalClaims": 1200,
    "totalRedemptions": 850,
    "redemptionRate": "70.83%",
    "claimsByDay": [
      { "date": "2024-01-01", "count": 45 },
      { "date": "2024-01-02", "count": 52 }
    ],
    "redemptionsByDay": [
      { "date": "2024-01-01", "count": 30 },
      { "date": "2024-01-02", "count": 38 }
    ],
    "topVouchers": [
      {
        "voucherId": "507f1f77bcf86cd799439011",
        "name": "20% Off Discount",
        "claims": 250,
        "redemptions": 180
      }
    ]
  }
}

用户分析 ​

按时间范围返回新增用户、活跃用户数及按角色分布等。

GET /analytics/users

查询参数:

参数类型必填说明
fromDatedatetime否统计开始时间
toDatedatetime否统计结束时间

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/analytics/users?fromDate=2024-01-01T00:00:00.000Z&toDate=2024-01-31T23:59:59.000Z" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "summary": {
      "totalUsers": 5000,
      "newUsersInPeriod": 500,
      "activeUsers": 4500
    },
    "byRole": {
      "member": 4800,
      "sub_company_admin": 150,
      "tenant_admin": 50
    },
    "dailySignups": [
      { "date": "2024-01-01", "count": 15 },
      { "date": "2024-01-02", "count": 22 }
    ]
  }
}

代币分析 ​

按时间范围返回代币流水汇总、按类型分布等。

GET /analytics/tokens

查询参数:

参数类型必填说明
fromDatedatetime否统计开始时间
toDatedatetime否统计结束时间

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/analytics/tokens?fromDate=2024-01-01T00:00:00.000Z&toDate=2024-01-31T23:59:59.000Z" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "summary": {
      "totalTransactions": 3500,
      "totalMinted": "500000",
      "totalBurned": "150000",
      "netChange": "350000"
    },
    "dailyTransactions": [
      { "date": "2024-01-01", "count": 120, "volume": "15000" },
      { "date": "2024-01-02", "count": 145, "volume": "18500" }
    ],
    "byType": {
      "mint": 800,
      "transfer": 2000,
      "burn": 400,
      "reward": 200,
      "redeem": 100
    }
  }
}

11. API 参考:店员 PIN(Staff PINs) ​

所需 Scope: staff_pins

店员 PIN 用于在核销礼券或领取代币时识别店员。4 位数字与租户 PIN 前缀组合(例如 HA + 1234 → HA1234)。创建、列表、更新、删除与单独校验走本资源。用 PIN 核销礼券仍是 POST /vouchers/redeem/:code/pin,且仍需 vouchers scope。

已有 API Key: staff_pins 为新增 scope。本版本之前创建的 Key 不含该权限。请在 管理后台 > 设置 > API Keys 中编辑并勾选 staff_pins。新建 Key 若保留全部 scope,会自动包含此项。

子公司范围: 子公司 API Key 只能查看和管理该地点的 PIN。租户级 Key 默认列出租户级 PIN;传入 subCompanyId 可针对某地点。租户级 Key 做校验时,除非传入 subCompanyId,否则接受该租户下任意地点的 PIN。

列出店员 PIN ​

GET /staff-pins

查询参数:

参数类型必填说明
pageinteger否页码(默认:1)
limitinteger否每页条数(默认:20)
statusstring否按状态筛选:active 或 inactive
permissionstring否按权限筛选:voucher_redemption 或 token_claim
subCompanyIdstring否仅租户级 Key。列出该子公司的 PIN

请求示例:

bash
curl -X GET "https://your-domain.com/api/external/v1/staff-pins?status=active" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": {
    "pins": [
      {
        "_id": "507f1f77bcf86cd799439011",
        "label": "Front Desk",
        "pin": "1234",
        "permissions": ["voucher_redemption"],
        "isActive": true,
        "subCompanyId": null,
        "createdAt": "2026-08-01T10:00:00.000Z",
        "updatedAt": "2026-08-01T10:00:00.000Z"
      }
    ],
    "prefix": "HA"
  },
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

响应字段:

字段类型可空约束 / 格式说明
pinsarray否店员 PIN 对象列表当前页 PIN
pins[]._idstring否MongoDB ObjectId(24 位十六进制)PIN ID
pins[].labelstring否1–30 字符展示名称(店员或柜台)
pins[].pinstring否恰好 4 位数字PIN 数字。与 prefix 拼接为完整 PIN
pins[].permissionsstring[]否voucher_redemption、token_claim,至少一项该 PIN 可授权的操作
pins[].isActiveboolean否默认 true停用后不可校验或核销
pins[].subCompanyIdstring是MongoDB ObjectId;null 表示租户级PIN 所属地点
prefixstring否通常为 2 个字母,如 HA租户前缀。完整 PIN 为 prefix + pin

创建店员 PIN ​

POST /staff-pins

请求体:

字段类型必填约束说明
labelstring是1–30 字符展示名称
pinstring是恰好 4 位数字密钥数字,不要带租户前缀
permissionsstring[]否voucher_redemption、token_claim,至少 1 项。默认 ["voucher_redemption"]允许的操作
subCompanyIdstring否MongoDB ObjectId。子公司 API Key 会忽略此字段在该地点下创建 PIN

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/staff-pins" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Front Desk",
    "pin": "1234",
    "permissions": ["voucher_redemption"]
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "_id": "507f1f77bcf86cd799439011",
    "label": "Front Desk",
    "pin": "1234",
    "permissions": ["voucher_redemption"],
    "isActive": true,
    "subCompanyId": null,
    "createdAt": "2026-08-13T09:00:00.000Z",
    "updatedAt": "2026-08-13T09:00:00.000Z",
    "prefix": "HA"
  },
  "message": "Staff PIN created"
}

校验店员 PIN ​

校验完整店员 PIN(前缀 + 4 位数字),不核销礼券。用于在其他操作前识别店员。核销礼券仍使用 POST /vouchers/redeem/:code/pin。

POST /staff-pins/verify

请求体:

字段类型必填约束说明
pinstring是5–20 字符。典型格式:前缀 + 4 位数字(如 HA1234)完整店员 PIN
subCompanyIdstring否MongoDB ObjectId。子公司 API Key 会忽略此字段将校验限制在该地点

请求示例:

bash
curl -X POST "https://your-domain.com/api/external/v1/staff-pins/verify" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "pin": "HA1234"
  }'

响应示例:

json
{
  "success": true,
  "data": {
    "pinId": "507f1f77bcf86cd799439011",
    "label": "Front Desk",
    "permissions": ["voucher_redemption"],
    "subCompanyId": null,
    "isActive": true,
    "prefix": "HA"
  },
  "message": "PIN verified successfully"
}

错误响应:

状态代码说明
400VALIDATION_ERRORPIN 格式、前缀或数字无效
403FORBIDDENAPI Key 缺少 staff_pins scope

更新店员 PIN ​

PATCH /staff-pins/:id

路径参数:

参数类型必填说明
idstring是店员 PIN ID

请求体:

字段类型必填约束说明
labelstring否1–30 字符展示名称
pinstring否恰好 4 位数字更换密钥数字
isActiveboolean否启用或停用 PIN
permissionsstring[]否voucher_redemption、token_claim,至少 1 项替换允许的操作

请求示例:

bash
curl -X PATCH "https://your-domain.com/api/external/v1/staff-pins/507f1f77bcf86cd799439011" \
  -H "X-API-Key: vio_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Cashier",
    "isActive": true,
    "permissions": ["voucher_redemption", "token_claim"]
  }'

删除店员 PIN ​

DELETE /staff-pins/:id

请求示例:

bash
curl -X DELETE "https://your-domain.com/api/external/v1/staff-pins/507f1f77bcf86cd799439011" \
  -H "X-API-Key: vio_live_your_api_key_here"

响应示例:

json
{
  "success": true,
  "data": null,
  "message": "Staff PIN deleted"
}

12. 数据模型(Data Models) ​

以下模型与 API JSON 字段名一致;约束/格式列保留与校验规则相关的原文术语(如 ISO 8601、ObjectId),说明列为中文。

Voucher(礼券模板) ​

字段类型约束 / 格式说明
_idstringMongoDB ObjectId(24 位十六进制)唯一 ID
namestring至少 1 字符,已 trim展示名称
descriptionstring可为空串 "",无最大长度说明(纯文本)
valuenumber>= 0,默认 0面额或折扣数值,含义由 valueType 决定
valueTypestring枚举:fixed、percentage,默认 fixedfixed 为固定金额,percentage 为折扣比例
valueCurrencystringISO 4217(如 THB、HKD),默认 THB固定金额类礼券的币种
voucherTypestring枚举:cash、discount、product、cash_discount礼券业务类型
consumptionTypestring枚举:vio_code、coupon_code、url、qr_code、manual、zhichong礼券核销交互方式
termsstring可为空串条款
imagesstring[]每项为合法 URL,可为 []配图,首张为主图
isActiveboolean默认 true是否启用、可领取
isTransferableboolean默认 false领取后是否允许转让
totalQuantityinteger-1 表示不限量;否则 >= 0,默认 -1可领取总量
claimedQuantityinteger>= 0,默认 0已领取次数
maxClaimsPerUserinteger0 表示每用户不限;否则 >= 1,默认 1单用户领取上限
startDatestringISO 8601,默认创建时间生效时间
endDatestringISO 8601;null 表示不设到期失效时间(仅 fixed_date 有效期使用)
validityTypestring枚举:fixed_date、relative,默认 fixed_date每条领取记录的到期计算方式
validityDaysinteger整数 1–3650;fixed_date 时为 null领取后可用天数
visibilitystring枚举:private、public、shared,默认 private可见范围,亦影响用户间转让规则
categorystring枚举:Wellness、Health、Food & Beverage、Leisure & Entertainment、Travel & Hospitality、Lifestyle & Services、Others;可空礼券主分类
categoriesstring[]字符串数组,每项已 trim分类标签
minSpendnumber>= 0,默认 0最低消费门槛;0 表示无门槛
maxDiscountnumber若设置须 > 0;可空折扣封顶(比例类礼券常用)
settlementAmountnumber>= 0,默认 0跨租户结算金额
settlementCurrencystringISO 4217,默认 THB结算币种
externalProviderstring供货方代码;可空外部供货方,如 vouchain
externalIdstring供货方模板 ID;可空外部供货模板 ID
externalRequiresDirectOrderParamsboolean默认 false直充是否需要额外供货方参数;zhichong 仍必须传 providerParams.account
applicableScopestring枚举:all_outlets、partial_outlets、single_store适用门店范围
bookingEnabledboolean默认 false是否启用预约
bookingDaysInAdvanceinteger>= 0,默认 0可提前预约天数
createdAtstringISO 8601,自动生成创建时间
updatedAtstringISO 8601,自动更新最近更新时间

VoucherClaim(UserVoucher,用户礼券实例) ​

字段类型约束 / 格式说明
_idstringMongoDB ObjectId唯一 ID
voucherIdstringMongoDB ObjectId关联的礼券模板 ID
userIdstringMongoDB ObjectId所属用户 ID
statusstring枚举:active、claimed、redeemed、expired,默认 active领取记录状态
redemptionCodestring唯一;格式如 VCH-{base36时间戳}-{4位随机}核销码
claimedAtstringISO 8601,默认当前时间领取时间
redeemedAtstringISO 8601;未核销前为 null核销时间
expiresAtstringISO 8601;null 为不设到期;取模板 endDate,relative 礼券取「领取日 + validityDays」本条记录到期时间
redemptionDetails.locationstring可选核销地点
redemptionDetails.notesstring可选核销备注
redemptionDetails.methodstring可选核销方式,如 api_key 或 pin
externalVoucherCodestring可空外部供货返回的券码
externalRedemptionUrlstring可空外部供货返回的核销链接
externalOrderIdstring可空外部供货订单 ID
externalFulfillmentStatusstring枚举:pending、processing、fulfilled、failed、null外部履约状态

User(用户) ​

字段类型约束 / 格式说明
_idstringMongoDB ObjectId唯一 ID
emailstring邮箱格式;未设置为 null;email 与 phone 至少其一有值邮箱
phonestring手机号格式;未设置为 null手机
displayNamestring未设置为 null,已 trim展示名
avatarstring合法 URL;未设置为 null头像
rolestring枚举:member、sub_company_admin、tenant_admin、super_admin角色
isActiveboolean默认 true是否启用
walletAddressstring以太坊地址 0x…;创建时自动分配托管钱包地址
registrationSourcestring枚举:created、direct、store、campaign注册来源
storeIdobject可能被 populate;含 _id、name 等;否则 null门店注册场景
campaignIdobject可能被 populate;含 _id、name、slug 等;否则 null活动注册场景
metadataobject任意键值自定义元数据
createdAtstringISO 8601注册时间
updatedAtstringISO 8601最近更新时间

Campaign(活动) ​

字段类型约束 / 格式说明
_idstringMongoDB ObjectId唯一 ID
namestring至少 1 字符活动名称
descriptionstring可为空串活动说明
slugstring小写 URL 安全;租户+子公司内唯一;可由 name 生成活动页路径标识
tokenIdstringMongoDB ObjectId,必填用户领取礼券时消耗的代币
isActiveboolean默认 true是否启用;停用则对用户隐藏
isPublicboolean默认 true是否公开(免登录可访问)
startDatestringISO 8601,默认创建时间开始时间
endDatestringISO 8601;null 为不设结束结束时间
imagesstring[]每项为合法 URL,可为 []横幅/配图
createdAtstringISO 8601创建时间

Token(代币) ​

字段类型约束 / 格式说明
_idstringMongoDB ObjectId唯一 ID
namestring已 trim名称
symbolstring通常 2–5 位大写符号(如 LP)
descriptionstring可为空串说明
totalSupplystring数字字符串,精度由业务定义总供应量
decimalsinteger>= 0,默认 0小数位
isActiveboolean默认 true是否启用
createdAtstringISO 8601创建时间
updatedAtstringISO 8601更新时间

TokenBalance(代币余额) ​

字段类型约束 / 格式说明
tokenIdstringMongoDB ObjectId代币 ID
tokenNamestring已 trim代币名称
symbolstring通常 2–5 位大写符号
balancestring数字字符串,>= "0"可用余额
lockedBalancestring数字字符串,>= "0"锁定余额

Transaction(交易流水) ​

字段类型约束 / 格式说明
_idstringMongoDB ObjectId流水 ID
tokenIdstringMongoDB ObjectId代币 ID
typestring枚举:mint、transfer、burn、reward、redeem、expire类型
amountstring数字字符串,> "0"金额
fromUserIdstringMongoDB ObjectId;增发场景为 null转出用户
toUserIdstringMongoDB ObjectId;销毁场景为 null转入用户
memostring未提供为 null备注/原因
createdAtstringISO 8601发生时间

StaffPin(店员 PIN) ​

字段类型约束 / 格式说明
_idstringMongoDB ObjectId唯一 ID
labelstring1–30 字符,已 trim展示名称(店员或柜台)
pinstring恰好 4 位数字PIN 数字。完整 PIN 为租户 prefix + pin
permissionsstring[]枚举:voucher_redemption、token_claim,至少一项该 PIN 可授权的操作
isActiveboolean默认 true停用后不可校验或核销
subCompanyIdstringMongoDB ObjectId;null 表示租户级PIN 所属地点
prefixstring通常为 2 个字母列表/创建/校验时返回的租户前缀
createdAtstringISO 8601创建时间
updatedAtstringISO 8601最近更新时间

Pagination(分页元数据) ​

字段类型约束 / 格式说明
pageinteger>= 1当前页(从 1 起)
limitinteger1–100每页条数
totalinteger>= 0符合条件的总数
totalPagesinteger>= 0总页数
hasNextPagebooleantrue/false是否有下一页
hasPrevPagebooleantrue/false是否有上一页

13. 代码示例(Code Examples) ​

以下为可运行示例:路径与 JSON 字段名与线上一致,便于直接复制;示例代码内注释仍为英文不影响运行。

JavaScript / Node.js 示例 ​

javascript
// VIO API Client Example
const VIO_API_BASE = "https://your-domain.com/api/external/v1";
const API_KEY = "vio_live_your_api_key_here";

// Helper function for API calls
async function vioApi(method, endpoint, body = null) {
  const options = {
    method,
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json",
    },
  };

  if (body) {
    options.body = JSON.stringify(body);
  }

  const response = await fetch(`${VIO_API_BASE}${endpoint}`, options);
  const data = await response.json();

  if (!data.success) {
    throw new Error(data.error?.message || "API request failed");
  }

  return data;
}

// Example: Create a voucher
async function createVoucher() {
  const result = await vioApi("POST", "/vouchers", {
    name: "Welcome Discount",
    description: "10% off your first purchase",
    value: 10,
    valueType: "percentage",
    totalQuantity: 1000,
    maxClaimsPerUser: 1,
    visibility: "public",
    endDate: "2024-12-31T23:59:59.000Z",
  });

  console.log("Created voucher:", result.data._id);
  return result.data;
}

// Example: Mint tokens to a user
async function mintTokensToUser(tokenId, userId, amount) {
  const result = await vioApi("POST", `/tokens/${tokenId}/mint`, {
    toUserId: userId,
    amount: amount.toString(),
    memo: "Reward for purchase",
  });

  console.log("Minted tokens:", result.data);
  return result.data;
}

// Example: Redeem a voucher by code
async function redeemVoucher(redemptionCode, location) {
  const result = await vioApi("POST", "/vouchers/redeem-by-code", {
    redemptionCode,
    location,
    notes: "Redeemed at checkout",
  });

  console.log("Voucher redeemed:", result.data);
  return result.data;
}

// Example: Consume a voucher with staff PIN
async function consumeVoucherByPin(redemptionCode, staffPin) {
  const result = await vioApi(
    "POST",
    `/vouchers/redeem/${redemptionCode}/pin`,
    {
      pin: staffPin, // e.g., 'HA1234'
    },
  );

  console.log("Voucher consumed by:", result.data.redeemedBy);
  return result.data;
}

// Example: List staff PINs
async function listStaffPins() {
  const result = await vioApi("GET", "/staff-pins?status=active");
  return result.data;
}

// Example: Verify a staff PIN (does not consume a voucher)
async function verifyStaffPin(fullPin) {
  const result = await vioApi("POST", "/staff-pins/verify", {
    pin: fullPin, // e.g., 'HA1234'
  });
  return result.data;
}

// Example: Get user with their balances
async function getUserWithBalances(userId) {
  const [userResult, balancesResult] = await Promise.all([
    vioApi("GET", `/users/${userId}`),
    vioApi("GET", `/users/${userId}/balances`),
  ]);

  return {
    user: userResult.data,
    balances: balancesResult.data,
  };
}

// Example: Search for a user by email
async function findUserByEmail(email) {
  const result = await vioApi(
    "GET",
    `/users/search/by-identifier?email=${encodeURIComponent(email)}`,
  );
  return result.data;
}

// Example: Find or create a user (idempotent, handles cross-tenant linking)
async function findOrCreateUser(email, password, displayName) {
  const result = await vioApi("POST", "/users/find-or-create", {
    email,
    password,
    displayName,
  });

  if (result.data.created && result.data.linked) {
    console.log("Linked to existing cross-tenant identity");
  } else if (result.data.created) {
    console.log("Brand new user created");
  } else {
    console.log("User already exists in this portal");
  }

  return result.data;
}

Python 示例 ​

python
import requests
from typing import Optional, Dict, Any

VIO_API_BASE = 'https://your-domain.com/api/external/v1'
API_KEY = 'vio_live_your_api_key_here'

def vio_api(method: str, endpoint: str, body: Optional[Dict] = None) -> Dict[str, Any]:
    """Make a request to the VIO API."""
    headers = {
        'X-API-Key': API_KEY,
        'Content-Type': 'application/json',
    }

    url = f'{VIO_API_BASE}{endpoint}'

    if method == 'GET':
        response = requests.get(url, headers=headers)
    elif method == 'POST':
        response = requests.post(url, headers=headers, json=body)
    elif method == 'PATCH':
        response = requests.patch(url, headers=headers, json=body)
    elif method == 'DELETE':
        response = requests.delete(url, headers=headers)
    else:
        raise ValueError(f'Unsupported method: {method}')

    data = response.json()

    if not data.get('success'):
        error = data.get('error', {})
        raise Exception(error.get('message', 'API request failed'))

    return data

# Example: List active vouchers
def list_vouchers(page: int = 1, limit: int = 20):
    result = vio_api('GET', f'/vouchers?page={page}&limit={limit}&isActive=true')
    return result['data'], result['pagination']

# Example: Create a user
def create_user(email: str, password: str, display_name: str):
    result = vio_api('POST', '/users', {
        'email': email,
        'password': password,
        'displayName': display_name,
        'role': 'member',
    })
    return result['data']

# Example: Find or create a user (idempotent, handles cross-tenant linking)
def find_or_create_user(email: str, password: str, display_name: str):
    result = vio_api('POST', '/users/find-or-create', {
        'email': email,
        'password': password,
        'displayName': display_name,
    })
    user = result['data']
    if user['created'] and user['linked']:
        print('Linked to existing cross-tenant identity')
    elif user['created']:
        print('Brand new user created')
    else:
        print('User already exists in this portal')
    return user

# Example: Get analytics overview
def get_analytics_overview():
    result = vio_api('GET', '/analytics/overview')
    return result['data']

# Example: List staff PINs
def list_staff_pins(page: int = 1, limit: int = 20):
    result = vio_api('GET', f'/staff-pins?page={page}&limit={limit}&status=active')
    return result['data']

# Example: Verify a staff PIN (does not consume a voucher)
def verify_staff_pin(full_pin: str):
    result = vio_api('POST', '/staff-pins/verify', {'pin': full_pin})
    return result['data']

# Usage
if __name__ == '__main__':
    # List vouchers
    vouchers, pagination = list_vouchers()
    print(f'Found {pagination["total"]} vouchers')

    # Get analytics
    analytics = get_analytics_overview()
    print(f'Total users: {analytics["users"]["total"]}')

完整流程:创建礼券并跟踪兑换 ​

bash
#!/bin/bash
# 使用 cURL 的完整流程示例

API_BASE="https://your-domain.com/api/external/v1"
API_KEY="vio_live_your_api_key_here"

# 1. 新建礼券
echo "Creating voucher..."
VOUCHER_RESPONSE=$(curl -s -X POST "$API_BASE/vouchers" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Flash Sale 25% Off",
    "description": "Limited time offer - 25% discount",
    "value": 25,
    "valueType": "percentage",
    "totalQuantity": 100,
    "maxClaimsPerUser": 1,
    "visibility": "public",
    "startDate": "2024-02-01T00:00:00.000Z",
    "endDate": "2024-02-28T23:59:59.000Z"
  }')

VOUCHER_ID=$(echo $VOUCHER_RESPONSE | jq -r '.data._id')
echo "Created voucher: $VOUCHER_ID"

# 2. 查询礼券详情
echo "Fetching voucher details..."
curl -s -X GET "$API_BASE/vouchers/$VOUCHER_ID" \
  -H "X-API-Key: $API_KEY" | jq

# 3. 列出该礼券的领取记录
echo "Listing claims..."
curl -s -X GET "$API_BASE/vouchers/claims/list?voucherId=$VOUCHER_ID" \
  -H "X-API-Key: $API_KEY" | jq

# 4. 顾客出示核销码时先查询
REDEMPTION_CODE="ABC123XYZ"
echo "Looking up redemption code..."
curl -s -X GET "$API_BASE/vouchers/redeem/$REDEMPTION_CODE/info" \
  -H "X-API-Key: $API_KEY" | jq

# 5. 核销礼券
echo "Redeeming voucher..."
curl -s -X POST "$API_BASE/vouchers/redeem-by-code" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"redemptionCode\": \"$REDEMPTION_CODE\",
    \"location\": \"Main Store\",
    \"notes\": \"Customer order #12345\"
  }" | jq

# 6. 礼券维度分析数据
echo "Fetching voucher analytics..."
curl -s -X GET "$API_BASE/analytics/vouchers" \
  -H "X-API-Key: $API_KEY" | jq

14. 更新日志(Changelog) ​

版本 1.6.0(2026 年 8 月) ​

相对有效期礼券(Relative Validity)

  • POST /vouchers、PATCH /vouchers/:voucherId 及所有礼券响应新增字段 validityType(fixed_date | relative,默认 fixed_date)
  • 新增字段 validityDays(整数 1–3650):validityType 为 relative 时必填,表示领取后多少天过期
  • relative 礼券不保存绝对 endDate:请求中的 endDate 会被丢弃,把已有礼券改为 relative 也会清空该字段;改回 fixed_date 则清空 validityDays
  • 相对有效期从「领取时间与 startDate 中较晚者」开始计算,因此活动开始前领取仍可获得完整天数
  • 领取记录的 expiresAt 字段结构不变,两种模式都会写入该字段,现有集成无需改动

版本 1.5.0(2026 年 8 月) ​

店员 PIN 管理

  • 新增资源 /staff-pins:列出、创建、更新、删除并校验店员核销 PIN
  • 新增 API Key scope staff_pins。已有 Key 需编辑后勾选;新建 Key 若保留全部 scope 会自动包含
  • POST /staff-pins/verify 校验完整 PIN(如 HA1234),不核销礼券。核销仍使用 POST /vouchers/redeem/:code/pin
  • 校验范围为当前租户(与公开接口 POST /api/public/verify-pin 不同)

版本 1.4.0(2026 年 6 月) ​

跨租户身份 & 查找或创建

  • 新增端点:POST /users/find-or-create — 幂等的用户查找/创建,通过 GlobalIdentity 自动处理跨租户关联
  • POST /users 查重范围从全局调整为当前门户(租户 + 子公司) — 相同邮箱/手机号可存在于不同门户
  • POST /users 现在自动为新用户创建 GlobalIdentity,支持跨租户 SSO
  • 响应中包含 created 和 linked 布尔标志,指示用户是否为新建以及是否关联到已有的跨租户身份

版本 1.3.0(2026 年 6 月) ​

Token 兑换礼券原子操作

  • 新增接口:POST /vouchers/:voucherId/redeem-with-tokens — 在一次服务端调用中原子性地扣减 token 并向用户发放礼券
  • API Key 须同时拥有 vouchers 和 tokens 两个 scope
  • 支持通过可选的 idempotencyKey 参数实现幂等性,防止重复扣款
  • 结算交易以 processedByType: api_key 记录,与会员自行领取清晰区分

版本 1.2.0(2026 年 5 月) ​

通过活动发放礼券(文档补充)

  • 新增对 POST /vouchers/:voucherId/send-campaign 接口的文档说明。该接口此前未在 External API 文档中公开,但已被多数小程序用于「按 voucher schema 自动选择有配额的活动并发放」的场景。
  • 明确该接口与 POST /vouchers/:voucherId/send 的响应字段差异:/send-campaign 把新建的 UserVoucher._id 暴露为 voucherNftId(而不是 _id),且不返回 redemptionCode / expiresAt / nftTokenId。
  • 提供了同时兼容两种响应结构的解析示例,便于在 /send 与 /send-campaign 之间切换的对接方使用。

版本 1.1.1(2026 年 5 月) ​

活动优惠券 Max Qty 与库存指示器

  • 修复:Max Qty 开关首次启用时,数量现在自动填充优惠券的当前剩余库存(总量减去已领取量),而非初始总库存
  • 保存后活动数量固定,不会随用户领取而自动减少
  • 新增 Out of Stock(红色)和 Low Stock(黄色,剩余 ≤ 10)标签,显示在活动优惠券管理弹窗及优惠券页面(网格和列表视图)

版本 1.1.0(2026 年 4 月) ​

注册来源、活动分析与 PIN 权限

  • User 模型增加 registrationSource(created、direct、store、campaign)
  • User 响应中 storeId、campaignId 可能为 populate 后的对象引用
  • 增加活动访问与报名等分析统计
  • 新增公开接口 POST /api/campaigns/:id/track-visit 用于活动页访问统计
  • 活动列表项包含 analytics 对象,含 visits、registrations 等计数
  • 核销 PIN 增加 permissions 字段,可取 voucher_redemption、token_claim 等
  • 每个 PIN 可配置一项或多项权限,决定可授权的操作类型
  • POST /api/public/verify-pin 响应中增加 permissions 数组
  • 店员 PIN 核销礼券(POST /vouchers/redeem/:code/pin)要求 PIN 具备 voucher_redemption 权限
  • 历史上未显式配置权限的 PIN 默认视为拥有 voucher_redemption,以保持兼容

版本 1.0.0(2024 年 2 月) ​

首次发布

  • 礼券完整 CRUD
  • 用户完整 CRUD
  • 活动完整 CRUD
  • 代币管理(列表、增发、销毁、调整)
  • 礼券、用户、代币分析接口
  • API Key 鉴权与 scope
  • 限流(每分钟 60 次、每日 10,000 次)
  • 支持 API Key IP 白名单

需要帮助? ​

  • 管理后台:在 设置 > API Keys 创建与管理 API Key
  • Swagger UI:交互式文档位于 /api-docs
  • 支持:请联系 VIO 技术支持

本文档对应 VIO External API v1。最后更新:2026 年 6 月。

VIO v4 平台文档