卡券
受众:开发者 / 运维 摘要:Voucher 模块提供中性的卡券模板、发放、锁定和核销底座,供 Mall、Meeting、Marketing 等业务模块复用。
spring-open-core-voucher 是通用卡券底座,不绑定商城优惠券、会议兑换券或营销活动单一语义。业务模块通过 consumerCode、ownerType、ownerId、businessType、businessId 和幂等键隔离自己的使用场景。
模块边界
| 项 | 内容 |
|---|---|
| Maven 模块 | spring-open-core/spring-open-core-voucher |
| Java 包 | com.springopen.core.voucher |
| 配置前缀 | spring.open.voucher |
| 默认开关 | spring.open.voucher.enabled=true |
| 表前缀 | voucher_* |
卡券底座只保存卡券事实和审计事实,不保存商城订单状态、会议套餐状态、营销活动规则、支付余额或资金流水。业务模块需要发券、锁券或核销时,应调用卡券底座 contract,并在自己的模块里保存业务订单、活动或权益关系。
当前范围
当前版本已落地模块骨架、迁移、模板目录、发放领取、可用券查询、锁定、核销、冲正、释放、过期 contract、Admin 运维入口、当前用户领取 / 我的卡券 API、App / PC 用户券包页面、Open Platform 开放 API、Marketing 抽奖卡券投放接入、Mall 订单结算样板和 Meeting 商业套餐样板:
| 能力 | 说明 |
|---|---|
| 卡券模板 | voucher_template 保存消费方、模板编码、名称、面值、折扣、门槛、有效期和状态 |
| 适用范围 | voucher_scope 保存模板适用对象,后续用于商品、栏目、会议套餐等场景 |
| 发放批次 | voucher_batch 保存批量发放计划和执行快照 |
| 卡券实例 | voucher_instance 保存用户或业务对象持有的具体卡券 |
| 发放记录 | voucher_grant_record 保存发放尝试、来源和幂等键 |
| 锁定记录 | voucher_lock 保存下单前的卡券预占事实 |
| 核销记录 | voucher_usage 保存最终核销、冲正和优惠金额 |
| 操作日志 | voucher_operation_log 保存模板、实例、锁定和核销的操作审计 |
Marketing 抽奖奖项类型 coupon 已接入 VoucherGrantService。运营在抽奖奖项 payload 中填写 voucherTemplateId 后,用户中奖会由 Marketing 用 marketing-lottery:{idempotencyKey}:voucher 作为幂等键发放 Voucher,并把抽奖记录派奖状态更新为 granted。
Mall 普通订单已作为首个业务消费样板接入 Voucher。结算预览和下单可传 voucherId,Mall 使用 consumerCode=mall、ownerType=user、ownerId=当前用户 校验卡券归属和模板,按 Mall 自己的金额规则计算优惠;下单锁定 Voucher,支付成功确认核销,取消订单释放锁定。Mall 订单只保存 voucherId、voucherLockId、voucherUsageId 快照,不复制 Voucher 模板、状态或核销事实。
Meeting 商业套餐购买单已作为第二个业务消费样板接入 Voucher。创建套餐购买单可传 voucherId,Meeting 使用 consumerCode=meeting、ownerType=user、ownerId=购买人 校验卡券归属和模板,锁定后按优惠后的应付金额创建 Cashier 订单;支付成功或人工结算后确认核销。Meeting 购买单只保存 voucherId、voucherLockId、voucherUsageId、原价和优惠金额快照,不复制 Voucher 模板、状态或核销事实。
Admin 管理
后台入口位于 Admin 端 /#/voucher,菜单 seed 写入 0000_00_00_034100_core_voucher_seed_data.xml,默认挂在根菜单“卡券中心”。后台 API 挂载在 /api/v1/admin/voucher/**:
| API | 说明 | 权限 |
|---|---|---|
GET /api/v1/admin/voucher/templates | 分页查询卡券模板 | voucher:template:view |
POST /api/v1/admin/voucher/templates | 新增卡券模板 | voucher:template:update |
PUT /api/v1/admin/voucher/templates/{id} | 编辑卡券模板 | voucher:template:update |
POST /api/v1/admin/voucher/templates/{id}/status | 切换模板状态 | voucher:template:update |
GET /api/v1/admin/voucher/instances | 查询卡券实例 | voucher:instance:view |
GET /api/v1/admin/voucher/grants | 查询发放记录 | voucher:instance:view |
GET /api/v1/admin/voucher/locks | 查询锁定记录 | voucher:redemption:view |
GET /api/v1/admin/voucher/usages | 查询核销记录 | voucher:redemption:view |
Admin 页面只做模板配置和运维查询,不承载业务优惠试算、营销活动策略或订单状态。业务模块接入时仍应在自己的结算流程里调用 Voucher contract。
用户侧 API
用户侧 API 挂载在 /api/v1/vouchers/**,用于 App / PC 或业务前台展示可领取模板、主动领取卡券和查看当前登录用户持有的卡券。领取和“我的卡券”查询使用当前登录用户作为 ownerType=user、ownerId=当前用户,前端不需要也不应该传入持有人 ID。
| API | 说明 |
|---|---|
GET /api/v1/vouchers/templates/claimable?consumerCode=mall | 查询某个消费方当前启用的可领取模板 |
POST /api/v1/vouchers/templates/{templateId}/claim | 当前登录用户领取一张模板卡券 |
GET /api/v1/vouchers/mine | 分页查询当前登录用户持有的卡券,可按消费方、模板和状态过滤 |
领取接口请求体可选传入 idempotencyKey 和 remark。未传 idempotencyKey 时,Voucher 会按当前用户和模板生成稳定幂等键,避免同一用户重复点击领取按钮造成重复发券。业务端如果有自己的领取流水号,也可以传入稳定 idempotencyKey。
App 端会员券包和 PC 端用户中心优惠券页已直接接入通用 Voucher API。页面按 consumerCode 切换商城券和会议券,领取时调用模板领取接口,“我的券包”按当前登录用户查询,不再依赖 Mall 专用优惠券接口。
Open Platform API
开放 API 前缀为 /api/v1/open/vouchers,复用 Open Platform 的 AppKey + AppSecret + HMAC 签名、防重放、scope、限流和调用日志。第三方应用应在自己的服务端完成签名,不要把 AppSecret 放进浏览器、小程序或移动端源码。
| API | 说明 | Scope |
|---|---|---|
POST /api/v1/open/vouchers/available | 查询某个消费方、持有人当前可用的卡券 | open:voucher:available |
POST /api/v1/open/vouchers/locks | 在业务结算前锁定一张可用卡券,返回卡券和锁定记录 | open:voucher:lock |
POST /api/v1/open/vouchers/locks/{lockId}/confirm | 业务成功后确认核销已锁定卡券 | open:voucher:redeem |
POST /api/v1/open/vouchers/usages/{usageId}/reverse | 业务退款、撤销或履约失败后冲正已核销卡券 | open:voucher:redeem |
POST /api/v1/open/vouchers/locks/{lockId}/release | 业务取消、失败或超时时释放卡券锁定 | open:voucher:lock |
available 请求体包含 consumerCode、ownerType、ownerId 和可选 at。locks 请求体包含 voucherId、consumerCode、ownerType、ownerId、businessType、businessId、originalAmount、discountAmount、expiresAt、idempotencyKey 和 remark。确认核销请求体包含 consumerCode、idempotencyKey 和 remark;冲正请求体包含 consumerCode 和 reason;释放锁定请求体包含 consumerCode 和 reason。
Open API 使用锁定响应里的 lock.id 作为后续确认核销和释放锁定的路径参数,使用确认核销响应里的 usage.id 作为后续冲正路径参数。写操作建议传入业务侧稳定 idempotencyKey,例如外部订单号、支付单号或履约流水号。冲正按 usage.id 幂等,重复冲正同一核销记录会返回已冲正结果,不会重复回退模板使用量。Voucher 模块只保存卡券和锁定 / 核销 / 冲正事实;应用、密钥、scope 授权、配额、计量和调用日志仍由 Open Platform 统一处理。
Java Contract
VoucherTemplateService 可用于创建模板、读取模板、按消费方查询启用模板和切换模板状态。
VoucherGrantService 可用于从启用模板发放或领取一张卡券实例,并执行可用券过期扫描:
| 方法 | 说明 |
|---|---|
grantVoucher(GrantVoucherCommand) | 校验模板启用状态、有效期、库存和持有人限额;创建 available 实例、发放记录和操作日志 |
expireAvailableVouchers(int limit) | 扫描已到期的 available 实例并标记为 expired,单次最多处理 500 条 |
发放命令建议传入业务侧稳定 idempotencyKey,例如订单号、活动发放记录号或用户领取流水号。相同 consumerCode + idempotencyKey 已存在成功发放记录时,服务会返回原卡券实例,不重复扣减模板库存。
VoucherRedemptionService 提供业务结算期需要的原子能力:
| 方法 | 说明 |
|---|---|
listAvailableVouchers(VoucherAvailabilityQuery) | 查询某个消费方、持有人在指定时间可用的卡券实例 |
lockVoucher(LockVoucherCommand) | 把一张 available 卡券锁定到业务单,校验归属、有效期、金额和幂等键 |
confirmUsage(ConfirmVoucherUsageCommand) | 把已锁定卡券确认为 used,生成核销记录并累计模板使用量 |
reverseUsage(ReverseVoucherUsageCommand) | 把已成功核销记录冲正为 reversed,恢复卡券可用状态并回退模板已核销数量 |
releaseLock(ReleaseVoucherLockCommand) | 业务单取消时释放锁定,把卡券恢复为 available |
expireLocks(int limit) | 扫描超时锁定并释放对应卡券,单次最多处理 500 条 |
Voucher 不负责决定“哪张券最优惠”。Mall、Meeting 或其他业务模块应在自己的结算流程中查询可用券、选择券,再调用锁定与核销 contract。这样可以保持卡券底座简单,并允许不同业务域使用自己的优惠试算规则。
使用建议
业务模块接入时保持三个原则:
- 只把通用卡券事实写入 Voucher:模板、实例、锁定、核销和操作日志。
- 业务状态留在业务模块:订单、会议套餐、营销活动、支付结果和资金结算都不复制进 Voucher。
- 关键写操作带幂等键:发放、锁定和核销应使用业务侧稳定幂等键;冲正应保存并复用核销响应中的
usage.id,避免重复优惠回退。
更多交易型业务模块可以参考 Mall 样板接入;订阅 / 套餐型业务模块可以参考 Meeting 商业套餐样板接入。新增业务场景时优先复用 consumerCode 扩展模板和用户侧筛选,不要为每个业务域再复制一套优惠券状态机。