API 文档
接口总览
所有接口以 /api/v1/ 为前缀,返回 JSON 格式。
| 接口 | 方法 | 说明 | 限流(租户级) | 认证 |
|---|---|---|---|---|
| /api/v1/health/ | GET | 健康检查 | 无 | 无需 |
| /api/v1/promotions/ | GET | 促销规则列表 | 1000/min | 需要 |
| /api/v1/promotions/{code}/metadata/ | GET | 促销元信息 | 1000/min | 需要 |
| /api/v1/promotions/{code}/detail/ | GET | 促销规则详情 | 1000/min | 需要 |
| /api/v1/promotions/calculate/ | POST | 标准促销计算 | 150/s | 需要 |
| /api/v1/promotions/tags/{code}/calculate/ | POST | 标签促销计算 | 50/s | 需要 |
| /api/v1/promotions/calculate/smart/ | POST | 智能促销计算 | 30/s optimal: 1/s | 需要 |
| /api/v1/promotions/tags/ | GET | 标签列表 | 1000/min | 需要 |
| /api/v1/promotions/tags/{code}/ | GET | 标签详情 | 1000/min | 需要 |
| /api/v1/promotions/usage/ | POST | 使用留痕 | 500/s | 需要 |
| /api/v1/refund/calculate/ | POST | 退款计算 | 100/s | 需要 |
| /api/v1/refund/execute/ | POST | 退款执行 | 50/s | 需要 |
| /api/v1/traces/{trace_id}/ | GET | 计算凭证查询 | 1000/min | 需要 |
| /api/v1/coupons/templates/ | GET | 消费券模板列表 | 1000/min | 需要 |
| /api/v1/coupons/templates/{outer_id}/ | GET | 消费券模板详情 | 1000/min | 需要 |
💡 限流策略说明
文档所示为租户级限流。当请求频率超过表中阈值时,您将收到 429 状态码,建议稍后重试。平台在租户级限流之上设有全局保护机制,系统整体负载较高时也可能触发 429,此类情况通常会在数秒内恢复。
容量规划:当前支持日均 1-10 万单。正在推进自动弹性扩容与配额自助调整,大促前可分钟级提升容量,无需业务改造。若日单量持续增长超出 SaaS 共享集群承载能力,或对数据安全/合规有更高要求,建议私有化部署以获得独立资源保障。
认证方式
平台采用 AccessKey 认证。接入申请审批通过后,平台将为您分配一组 AccessKey 与 AccessSecret,调用接口时在请求头中携带即可。
🔑 AccessKey 认证
在每个请求 Header 中携带 X-Access-Key 与 X-Access-Secret:
curl -X POST https://api.example.com/api/v1/promotions/calculate/ \
-H "Content-Type: application/json" \
-H "X-Access-Key: your_access_key" \
-H "X-Access-Secret: your_access_secret" \
-d '{"user_id":"USER001","cart_items":[]}'
各语言认证实现参考下方 API 多语言 SDK 章节。
系统接口
检查服务是否正常运行。
{
"status": "ok",
"service": "promotion-platform",
"version": "1.0.0"
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | OK | 服务正常运行 |
促销规则查询
查询促销规则基本信息,支持分页、筛选、排序。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
status | string | 否 | 状态筛选:active / pending / expired |
strategy_type | string | 否 | 策略类型筛选 |
valid_now | boolean | 否 | 只返回当前有效的规则(true/false) |
ordering | string | 否 | 排序字段,如 -priority,-created_at |
page | int | 否 | 页码,默认 1 |
page_size | int | 否 | 每页数量,默认 20,最大 100 |
{
"count": 10,
"next": "http://api.example.com/api/v1/promotions/?page=2",
"previous": null,
"results": [
{
"promotion_code": "PROMO001",
"name": "新年满减活动",
"description": "满100元减10元",
"status": "active",
"status_display": "生效中",
"strategy_type": "full_reduction",
"strategy_type_display": "满减",
"priority": 10,
"valid_from": "2026-02-01T00:00:00",
"valid_to": "2026-03-03T23:59:59"
}
]
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | OK | 成功返回分页列表 |
| 401 | UNAUTHORIZED | 缺少或无效的认证信息 |
| 429 | TOO_MANY_REQUESTS | 请求过于频繁 |
获取单个促销规则的元信息(不含敏感配置)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
promotion_code | string | 是 | 促销编码,如 PROMO001 |
{
"promotion_code": "PROMO001",
"name": "新年满减活动",
"description": "满100元减10元",
"status": "active",
"status_display": "生效中",
"strategy_type": "full_reduction",
"strategy_type_display": "满减",
"priority": 10,
"valid_from": "2026-02-01T00:00:00",
"valid_to": "2026-03-03T23:59:59"
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | OK | 成功返回促销元信息 |
| 401 | UNAUTHORIZED | 缺少或无效的认证信息 |
| 404 | NOT_FOUND | 促销规则不存在 |
| 429 | TOO_MANY_REQUESTS | 请求过于频繁 |
获取促销规则完整信息(含条件、动作、范围配置),仅限内部使用。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
promotion_code | string | 是 | 促销编码 |
{
"promotion_code": "PROMO001",
"name": "新年满减活动",
"description": "满100元减10元",
"strategy_type": "full_reduction",
"status": "active",
"priority": 10,
"valid_from": "2026-02-01T00:00:00",
"valid_to": "2026-03-03T23:59:59",
"conditions": [
{
"condition_type": "amount_threshold",
"condition_type_display": "金额门槛",
"config": {"threshold": 100},
"sort_order": 0
}
],
"actions": [
{
"action_type": "fixed_amount",
"action_type_display": "固定金额减免",
"config": {"discount": 10},
"max_discount": null,
"sort_order": 0
}
],
"scopes": [
{
"scope_type": "all",
"scope_type_display": "全场通用",
"config": {}
}
],
"created_at": "2026-02-01T10:00:00",
"updated_at": "2026-02-01T10:00:00"
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | OK | 成功返回规则详情 |
| 401 | UNAUTHORIZED | 缺少或无效的认证信息 |
| 404 | NOT_FOUND | 促销规则不存在 |
| 429 | TOO_MANY_REQUESTS | 请求过于频繁 |
促销计算
根据购物车商品和用户信息,计算适用的促销优惠。支持促销与券组合计算、价格保护、自动凑单提示。
标准促销计算,指定促销编码进行计算。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id | int / string | 否 | 用户ID(支持内部数字ID或外部系统字符串 outer_id),不传默认 0 |
order_id | string | 条件必填 | 订单号。preview=false 时必须传入,将计算凭证与订单绑定,供后续退款追溯。preview=true(默认)时无需传入 |
cart_items | array | 是 | 购物车商品列表 |
cart_items[].sku | string | 是 | 商品SKU |
cart_items[].quantity | int | 是 | 商品数量,≥1 |
cart_items[].price | decimal | 是 | 商品单价,≥0.01 |
cart_items[].category_id | string | 否 | 品类ID(未传时自动从商品库补全) |
cart_items[].brand_id | string | 否 | 品牌ID(未传时自动从商品库补全) |
cart_items[].tags | array | 否 | 商品标签列表(未传时自动从商品库补全) |
cart_items[].cost_price | string | 否 | 成本价(用于价格保护 POC 测试,未传时自动从商品库补全) |
cart_items[].member_price | string | 否 | 会员价(用于价格保护 POC 测试,未传时自动从商品库补全) |
promotion_codes | array | 否 | 指定促销编码列表,如 ["PROMO001", "PROMO002"] |
context | object | 否 | 上下文信息(开放结构) |
context.channel | string | 否 | 渠道:app / pc / miniapp / h5 |
context.user_group | string | 否 | 用户群体:vip / member |
context.shipping_method | string | 否 | 配送方式 |
context.payment_method | string | 否 | 支付方式 |
user_info | object | 否 | 用户信息(用于生日等画像条件判断) |
user_info.birthday | string | 否 | 用户生日,格式 YYYY-MM-DD |
coupon_codes | array | 否 | 券实例编码列表,系统按 outer_id 精确查询 |
coupon_template_ids | array | 否 | 券模板编码列表,系统自动匹配可用实例 |
used_coupons | array | 否 | 券详细信息列表(内部完整模式,直接参与抵扣) |
calculation_order | array / string | 否 | 计算顺序。默认 ["promotions","coupons"],支持 "optimal"(并行取最优) |
preview | boolean | 否 | 预览模式。true=只计算优惠金额(默认,安全优先);false=计算并生成可追溯的计算凭证,需同时传入 order_id |
{
"user_id": 1001,
"cart_items": [
{"sku": "SKU001", "quantity": 2, "price": "60.00", "category_id": "1"},
{"sku": "SKU002", "quantity": 1, "price": "100.00"}
],
"promotion_codes": ["PROMO001"],
"coupon_codes": ["CI-20260410-001"],
"context": {"channel": "pc", "user_group": "vip"},
"calculation_order": "optimal"
}{
"user_id": 1001,
"cart_items": [
{
"sku": "SKU001",
"quantity": 1,
"price": "199.00",
"cost_price": "80.00",
"member_price": "90.00"
}
],
"promotion_codes": ["PROMO001"],
"context": {"channel": "pc"}
}| 字段 | 类型 | 可能值 / 说明 |
|---|---|---|
| meta(响应元信息) | ||
meta.status | string | APPLIED 促销/券已生效;NO_MATCH 无匹配促销或券未满足条件;BLOCKED 价格保护拦截;ERROR 计算异常 |
meta.code | string | 见下方「meta.code 枚举值」 |
meta.message | string | 状态描述文本 |
meta.timestamp | int | 时间戳(毫秒) |
| data.summary(金额汇总) | ||
data.summary.original_amount | string | 原始金额 |
data.summary.total_discount | string | 总优惠金额(促销+券) |
data.summary.payable_amount | string | 应付金额 |
data.summary.available | boolean | true 有可用促销;false 无可用促销 |
data.summary.coupon_discount | string | 券抵扣总金额(仅当使用了券时返回) |
data.summary.reward_summary | object | 奖励汇总,如 {"coupon": 1, "gift": 2}(仅当有奖励时返回) |
| data.applied_promotions[](应用的促销列表) | ||
...promotion_code | string | 促销编码 |
...strategy_type | string | 见下方「strategy_type 枚举值」 |
...strategy_type_display | string | 策略类型显示名(如"满减") |
...benefit_type | string | direct_discount 直接金额折扣;reward 赠送权益(券/礼物/积分);mixed 既有折扣又有赠品;free_shipping 仅免运费;no_benefit 无实际优惠 |
...discount | string | 优惠金额 |
...applied_items | array | 应用的商品SKU列表 |
...rewards | array | 奖励列表(赠品、优惠券等) |
...message | string | 提示信息 |
| data.item_discounts[](商品级优惠分摊明细) | ||
...sku | string | 商品SKU |
...quantity | int | 数量 |
...original_price | string | 该商品小计原价 |
...allocated_discount | string | 分摊到该商品的优惠金额 |
...payable | string | 该商品分摊后应付金额 |
| 其他 data 字段 | ||
data.used_coupons | array | 实际参与抵扣的券明细(仅当使用了券时返回)。每项含 code、coupon_type、deducted_amount、status(固定值 applied) |
data.price_protection | object | 价格保护信息(仅当触发价格保护时返回)。含 triggered、violations[] 等 |
data.optimization_tips | array | 自动凑单提示(仅当有可用建议时返回) |
data.not_found_codes | array | 未找到的促销编码列表(仅当传入的 promotion_codes 中有不存在编码时返回) |
| trace(链路追踪) | ||
trace.trace_id | string | 计算链路追踪ID(预览模式 preview=true 时不生成) |
trace.logs | array | 诊断日志(如未找到编码的警告) |
trace.diagnosis | object | 结构化诊断信息(调试用途) |
| 值 | 触发场景 | 业务含义 |
|---|---|---|
PROMOTIONS_APPLIED | 有匹配促销且折扣生效 | 促销已生效 |
COUPONS_APPLIED | 仅传券、券抵扣成功 | 券抵扣已生效 |
NO_MATCHING_PROMOTIONS | 有匹配促销规则但条件不满足 | 没有匹配的促销规则 |
COUPONS_NOT_APPLIED | 仅传券、券条件不满足 | 券不满足使用条件,未产生优惠 |
NO_INPUT | 未传 promotion_codes 和 coupon 相关参数 | 未指定促销编码和券编码,未进行任何计算 |
PRICE_PROTECTION_BLOCKED | 价格保护规则拦截 | 部分商品触发价格保护拦截,无法下单 |
CALCULATION_ERROR | 计算异常 | 计算过程中发生错误 |
SERVICE_UNAVAILABLE | 系统异常降级 | 服务暂时不可用,已返回降级结果 |
| 值 | 说明 |
|---|---|
full_reduction | 满减(满X元减Y元) |
percentage_discount | 百分比折扣(如打8折) |
fixed_amount_reduction | 固定金额减免 |
special_price | 特价(指定商品特价) |
seckill | 秒杀(限时特价) |
first_order | 首单优惠 |
free_shipping | 免运费 |
full_gift | 满赠(满额赠送礼品) |
full_discount | 满折(满X元打Y折) |
n_discount_m | N件M折(如2件8折) |
tiered_price | 阶梯价(按数量阶梯定价) |
tiered_amount | 阶梯优惠(按金额阶梯优惠) |
coupon | 优惠券相关策略 |
{
"meta": {
"status": "APPLIED",
"code": "PROMOTIONS_APPLIED",
"message": "促销已生效",
"timestamp": 1776515805191
},
"data": {
"summary": {
"original_amount": "220.00",
"total_discount": "10.00",
"payable_amount": "210.00",
"available": true
},
"applied_promotions": [
{
"promotion_code": "PROMO001",
"strategy_type": "full_reduction",
"strategy_type_display": "满减",
"benefit_type": "direct_discount",
"discount": "10.00",
"applied_items": ["SKU001", "SKU002"],
"rewards": [],
"message": ""
}
],
"item_discounts": [
{
"sku": "SKU001",
"quantity": 2,
"original_price": "120.00",
"allocated_discount": "5.45",
"payable": "114.55"
},
{
"sku": "SKU002",
"quantity": 1,
"original_price": "100.00",
"allocated_discount": "4.55",
"payable": "95.45"
}
]
},
"trace": {"trace_id": "trace-xxx"},
"messages": []
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | SUCCESS | 计算成功,返回 trace_id |
| 400 | VALIDATION_ERROR | 参数校验失败 |
| 429 | TOO_MANY_REQUESTS | 请求过于频繁(超过租户配额) |
| 500 | CALCULATION_ERROR | 计算服务异常 |
| 503 | SERVICE_UNAVAILABLE | 服务暂时不可用 |
按指定标签下的促销规则进行计算。请求参数和响应结构与 /promotions/calculate/ 一致,系统自动将标签关联的促销编码传入计算。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
tag_code | string | 是 | 路径参数,标签编码,如 new_user |
| 请求体同标准计算(user_id / cart_items / context 等) | |||
POST /api/v1/promotions/tags/new_user/calculate/
{
"user_id": 1001,
"cart_items": [
{"sku": "SKU001", "quantity": 1, "price": "199.00"}
],
"context": {"channel": "app"}
}同标准计算响应结构。额外在 data 中返回标签匹配信息(测试中心模式)。
| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | SUCCESS | 计算成功 |
| 400 | BAD_REQUEST | JSON 格式错误或参数校验失败 |
| 500 | CALCULATION_ERROR | 计算服务异常 |
智能促销计算,支持三种模式自动匹配最佳促销组合。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
promotion_select | object | 否 | 促销选择配置(不传时退化为标准计算) |
promotion_select.mode | string | 是 | 选择模式:auto / tags / explicit |
promotion_select.config | object | 否 | 模式配置 |
promotion_select.config.include_tags | array | 否 | tags 模式:包含的标签编码列表 |
promotion_select.config.exclude_codes | array | 否 | 排除的促销编码列表 |
| 其余请求体字段同标准计算(cart_items / context / coupon_codes 等) | |||
{
"user_id": 1001,
"cart_items": [
{"sku": "SKU001", "quantity": 2, "price": "60.00"}
],
"promotion_select": {
"mode": "auto",
"config": {
"exclude_codes": ["PROMO999"]
}
},
"context": {"channel": "pc"}
}{
"user_id": 1001,
"cart_items": [
{"sku": "SKU001", "quantity": 1, "price": "199.00"}
],
"promotion_select": {
"mode": "tags",
"config": {
"include_tags": ["new_user", "flash_sale"],
"exclude_codes": []
}
}
}同标准计算响应结构。
| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | SUCCESS | 计算成功 |
| 400 | BAD_REQUEST | JSON 格式错误或参数校验失败 |
| 500 | CALCULATION_ERROR | 计算服务异常 |
促销标签
通过标签体系对促销进行分组管理,便于前端按场景快速调用。
获取可用标签列表,含每个标签关联的促销规则数量。
{
"count": 5,
"results": [
{
"tag_code": "new_user",
"name": "新人专享",
"promotion_count": 3,
"description": "新注册用户可用"
},
{
"tag_code": "flash_sale",
"name": "限时抢购",
"promotion_count": 2,
"description": "限时特价活动"
}
]
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | OK | 成功返回标签列表 |
获取标签详情及关联的促销规则列表。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
tag_code | string | 是 | 路径参数,标签编码,如 new_user |
{
"success": true,
"data": {
"tag_code": "new_user",
"name": "新人专享",
"description": "新注册用户可用",
"promotions": [
{
"promotion_code": "PROMO001",
"name": "新人首单8折",
"strategy_type": "percentage_discount",
"priority": 20
}
]
}
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | OK | 成功返回标签详情 |
| 404 | NOT_FOUND | 标签不存在 |
使用留痕 & 退款
促销使用留痕用于锁定优惠明细,退款计算确保下单与退款金额完全一致,支持审计追溯。
订单支付完成后上报促销使用记录,生成计算凭证用于后续退款核对。仅需传入 trace_id,order_id 及其余数据自动从 trace 快照读取。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
trace_id | string | 是 | 计算时返回的 trace_id |
status | string | 否 | 记录状态,默认 pendingpending — 待确认。订单已支付但尚未最终成交,促销优惠已预占但未正式生效。核心 KPI(GMV/订单数/用户数)不计入此状态。confirmed — 已确认。订单已发货或超过可取消期限,促销优惠正式生效。核心 KPI只统计此状态。cancelled — 已取消。订单在发货前被取消,促销使用作废。需业务系统主动调用。退款数据由退款执行接口自动记录,无需上报 |
{
"trace_id": "trace-xxx",
"status": "confirmed"
}
说明:user_id、original_amount、payable_amount、applied_promotions、context_snapshot 等字段会自动从 trace 快照中读取,无需在请求中传入。
•
pending / confirmed / cancelled:调用方无需额外处理,引擎自动以 order_id:promotion_code 为键保证幂等,支持状态流转覆盖。• 退款数据由退款执行接口自动记录,无需通过 usage 接口上报。
•
pending 记录超过 7 天未更新为 confirmed 或 cancelled,引擎自动标记为 confirmed• 设计 rationale:大多数订单支付后最终都会成交,默认乐观确认可减少业务系统接入负担
• 业务系统只需在订单取消时主动调用 usage 接口更新为
cancelled;正常成交的订单无需二次确认
• 核心 KPI(带动 GMV、实付总额、优惠总金额、渗透率、覆盖用户/订单):仅统计
confirmed 状态• 转化漏斗(pending / confirmed / cancelled / refunded):展示全部生命周期状态流转
• 损耗分析:损耗订单数 =
cancelled 记录数 + 已执行退款的订单数(去重);退款金额 = RefundExecution 已执行金额合计;损耗率 = 损耗订单数 ÷ 全部 usage 记录总数;损耗优惠金额 = cancelled 状态的 discount 合计
{
"status": "success",
"message": "记录已保存",
"data": {"total_records": 1}
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | SUCCESS | 记录已保存 |
| 400 | VALIDATION_ERROR | 参数校验失败或快照缺少 order_id |
| 404 | TRACE_NOT_FOUND | trace_id 不存在、已过期或快照数据缺失 |
| 429 | TOO_MANY_REQUESTS | 请求过于频繁(超过租户配额) |
| 500 | SAVE_ERROR | 服务器内部错误 |
退款计算(计算凭证模式 v4.0)。根据原单锁定的优惠明细直接读取计算退款金额,无需重新算价,确保下单与退款完全一致。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
trace_id | string | 是 | 原单计算时返回的 trace_id |
refund_items | array | 条件 | 退款商品列表。与 refund_all 至少填一个 |
refund_items[].sku | string | 是 | 商品SKU |
refund_items[].quantity | int | 是 | 退款数量 |
refund_all | boolean | 否 | 整单退开关。传 true 时系统从快照自动读取所有剩余未退商品,无需传入 refund_items |
refund_no | string | 否 | 退款单号(由业务系统传入,会回显到响应中) |
{
"trace_id": "trace-xxx",
"refund_items": [
{"sku": "SKU001", "quantity": 1}
],
"refund_no": "REFUND-001"
}{
"trace_id": "trace-xxx",
"refund_all": true,
"refund_no": "REFUND-001"
}| 字段 | 类型 | 可能值 / 说明 |
|---|---|---|
| meta(响应元信息) | ||
meta.status | string | APPLIED 退款计算成功;BLOCKED 触发兜底人工处理;ERROR 参数错误或计算异常;NO_MATCH 无剩余可退金额 |
meta.code | string | 见下方「meta.code 枚举值」 |
meta.message | string | 状态描述文本 |
meta.trace_id | string | 原单 trace_id |
| data(退款计算结果) | ||
data.refund_amount | string | 本次应退金额 |
data.remaining_payable | string | 退款后剩余应付金额 |
data.refunded_total | string | 该订单累计已退款金额 |
data.refund_items | array | 商品级退款明细 |
...sku | string | 商品SKU |
...quantity | int | 退款数量 |
...original_price | string | 该商品对应原价 |
...allocated_discount | string | 该商品需收回的优惠分摊 |
...refund_amount | string | 该商品实际退款金额 |
...capped | boolean | 是否因超出剩余可退金额而被截断(仅当被截断时返回) |
data.fallback | object | 兜底信息。triggered 是否触发;handler_code 处理编码;handler_note 处理说明;reason 触发原因 |
data.refund_strategy | array | 各促销的退款策略(如 proportional / full_refund_discount) |
data.applied_promotions | array | 促销退款策略明细。每项含 promotion_code、refund_strategy、rewards[](赠品/券回收策略) |
data.refund_to | string | 固定值 user |
data.coupon_refund_advice | array | 券退券建议(仅当订单使用了券时返回)。每项含 coupon_id、coupon_type、current_status、can_return、advice、return_to |
data.refund_no | string | 退款单号(业务系统传入时回显) |
data.capped | boolean | 整体截断标记(仅当退款金额被截断时返回) |
data.cap_reason | string | 截断原因(仅当被截断时返回) |
| 值 | 触发场景 | 业务含义 |
|---|---|---|
REFUND_CALCULATED | 正常计算成功 | 退款计算成功 |
FALLBACK_TO_MANUAL | 触发兜底规则(如退款金额超过阈值) | 该退款触发人工处理流程 |
MISSING_TRACE_ID | 请求未传 trace_id | trace_id 必填 |
MISSING_REFUND_ITEMS | 请求未传 refund_items 且未传 refund_all | refund_items 和 refund_all 至少填一个 |
TRACE_NOT_FOUND | trace_id 不存在或已过期 | trace_id 不存在 |
ITEM_NOT_IN_SNAPSHOT | 退款商品不在原单快照中 | 商品不在快照中 |
NO_REMAINING_REFUND | 已无可退金额 | 无剩余可退金额 |
CALCULATION_ERROR | 计算异常 | 退款计算失败 |
{
"meta": {
"status": "APPLIED",
"code": "REFUND_CALCULATED",
"message": "退款计算成功",
"trace_id": "trace-xxx"
},
"data": {
"refund_amount": "114.55",
"remaining_payable": "95.45",
"refunded_total": "0.00",
"refund_items": [
{
"sku": "SKU001",
"quantity": 1,
"original_price": "120.00",
"allocated_discount": "5.45",
"refund_amount": "114.55"
}
],
"fallback": {"triggered": false},
"refund_strategy": ["proportional"],
"refund_to": "user"
}
}{
"meta": {
"status": "BLOCKED",
"code": "FALLBACK_TO_MANUAL",
"message": "该退款触发人工处理流程",
"trace_id": "trace-xxx"
},
"data": {
"refund_amount": "114.55",
"refund_items": [...],
"fallback": {
"triggered": true,
"handler_code": "MANUAL_REVIEW",
"handler_note": "退款金额超过阈值,需人工审核",
"reason": "refund_amount_exceeds_threshold"
}
}
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | REFUND_CALCULATED | 退款计算成功 |
| 200 | NO_REMAINING_REFUND | 无剩余可退商品 |
| 200 | FALLBACK_TO_MANUAL | 触发人工处理流程 |
| 400 | MISSING_TRACE_ID | 缺少 trace_id |
| 400 | MISSING_REFUND_ITEMS | refund_items 和 refund_all 至少填一个 |
| 400 | ITEM_NOT_IN_SNAPSHOT | 商品不在原始订单快照中 |
| 404 | TRACE_NOT_FOUND | trace_id 不存在或已过期 |
| 500 | CALCULATION_ERROR | 退款计算异常 |
退款执行,确认退款并记录退款凭证。同一 refund_no 重复请求返回已有结果。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
trace_id | string | 是 | 原单 trace_id |
refund_items | array | 条件 | 退款商品列表。与 refund_all 至少填一个;若同时传入 refund_all,以此为准 |
refund_amount | string | 否 | 执行退款金额。不传时系统自动根据 refund_items 计算;传入时以传入值为准(不能超过剩余可退余额) |
refund_no | string | 是 | 退款单号(由业务系统传入,全局唯一) |
refund_all | boolean | 否 | 整单退开关。传 true 时系统从快照自动读取所有剩余未退商品并自动计算退款金额 |
extra_data | object | 否 | 额外数据 |
order_id | string | 否 | 订单号(用于关联外部订单系统) |
{
"trace_id": "trace-xxx",
"refund_items": [{"sku": "SKU001", "quantity": 1}],
"refund_amount": "114.55",
"refund_no": "REFUND-001"
}{
"trace_id": "trace-xxx",
"refund_all": true,
"refund_no": "REFUND-001"
}| 字段 | 类型 | 可能值 / 说明 |
|---|---|---|
| meta(响应元信息) | ||
meta.status | string | APPLIED 退款执行成功;BLOCKED 触发兜底人工处理;ERROR 参数错误或执行异常 |
meta.code | string | 见下方「meta.code 枚举值」 |
meta.message | string | 状态描述文本 |
meta.trace_id | string | 原单 trace_id |
| data(退款执行结果) | ||
data.refund_no | string | 退款单号(业务系统传入) |
data.refund_amount | string | 本次退款金额 |
data.refunded_total | string | 该订单累计已退款金额 |
data.status | string | pending 待执行;executed 已执行;failed 失败 |
data.executed_at | string | 执行时间(ISO 8601 格式) |
| 值 | 触发场景 | 业务含义 |
|---|---|---|
REFUND_EXECUTED | 正常执行成功 | 退款执行成功 |
REFUND_DUPLICATE | 同一 refund_no 重复请求 | 重复请求,返回已有结果 |
MISSING_PARAM | 未传 trace_id 或 refund_no | trace_id 和 refund_no 必填 |
TRACE_NOT_FOUND | trace_id 不存在 | trace 不存在 |
FALLBACK_TO_MANUAL | 触发兜底规则 | 该退款已触发人工处理,无法自动执行 |
EXECUTE_ERROR | 执行异常 | 退款执行失败 |
{
"meta": {
"status": "APPLIED",
"code": "REFUND_EXECUTED",
"message": "退款执行成功",
"trace_id": "trace-xxx"
},
"data": {
"refund_no": "R202605020001",
"refund_amount": "114.55",
"refunded_total": "114.55",
"status": "executed",
"executed_at": "2026-05-02T10:30:00+08:00"
}
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | REFUND_EXECUTED | 退款执行成功 |
| 200 | REFUND_DUPLICATE | 重复请求,返回已有结果 |
| 200 | FALLBACK_TO_MANUAL | 触发人工处理,无法自动执行 |
| 400 | MISSING_PARAM | trace_id 和 refund_no 必填 |
| 400 | MISSING_REFUND_ITEMS | refund_items 和 refund_all 至少填一个 |
| 404 | TRACE_NOT_FOUND | trace_id 不存在或已过期 |
| 500 | EXECUTE_ERROR | 退款执行异常 |
计算凭证查询
返回 trace 完整数据链条:计算前购物数据 → 促销计算结果(含用券) → 售后退款记录。支持整单退和多次部分退的完整审计。查询链路:Redis → MySQL 热库 → OSS 归档存储。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
trace_id | string | 是 | 路径参数,Trace ID,如 trace-xxx |
{
"meta": {"status": "APPLIED", "code": "TRACE_FOUND"},
"data": {
"trace_id": "trace-xxx",
"order_id": "ORDER12345",
"status": "ACTIVE",
"storage_location": "hot",
"created_at": "2026-05-02T10:00:00+08:00",
"expires_at": "2026-06-02T10:00:00+08:00",
"original_request": {
"user_id": "USER001",
"order_id": "ORDER12345",
"cart_items": [{"sku": "SKU001", "quantity": 2, "sale_price": "300.00"}],
"promotion_codes": ["PROMO001"],
"coupon_template_ids": ["COUPON001"],
"preview": false
},
"promotion_result": {
"summary": {"original_amount": "600.00", "discount_amount": "100.00", "payable_amount": "500.00"},
"applied_promotions": [{"code": "PROMO001", "name": "满500减100", "discount_amount": "100.00"}],
"item_discounts": [{"sku": "SKU001", "quantity": 2, "original_price": "300.00", "allocated_discount": "100.00", "payable": "500.00"}],
"used_coupons": [{"template_outer_id": "COUPON001", "coupon_type": "full_reduction", "discount_value": "20.00"}],
"total_paid": "500.00",
"currency": "CNY"
},
"refund_history": [
{
"refund_no": "REF_20260501_001",
"refund_items": [{"sku": "SKU001", "quantity": 1}],
"refund_amount": "250.00",
"status": "executed",
"source": "external",
"created_at": "2026-05-01T14:30:00+08:00",
"executed_at": "2026-05-01T14:30:05+08:00"
}
]
}
}| 数据块 | 来源 | 说明 |
|---|---|---|
original_request | 促销计算原始请求 | 计算前购物数据:cart_items、promotion_codes、coupon_template_ids 等 |
promotion_result | 促销计算响应 | 计算结果:summary、applied_promotions、item_discounts(分摊明细)、used_coupons(用券明细) |
refund_history | 退款执行记录 | 售后退款流水,支持整单退和多次部分退,按时间顺序排列 |
{
"meta": {
"status": "ERROR",
"code": "TRACE_NOT_FOUND",
"message": "trace_id 不存在或已过期"
}
}| HTTP | 业务码 | 说明 |
|---|---|---|
| 200 | TRACE_FOUND | 命中,返回完整数据链条 |
| 404 | TRACE_NOT_FOUND | trace_id 不存在或已过期 |
API 多语言 SDK
以下示例覆盖促销计算、退款计算与退款执行三个核心接口。请将 ACCESS_KEY 和 ACCESS_SECRET 替换为实际凭证。
Python 3.9+ | requests 2.28+
import requests, hmac, hashlib, time, json class MyPromotionClient: def __init__(self, access_key, access_secret, base_url="https://api.example.com"): self.base_url = base_url.rstrip("/") self.access_secret = access_secret self.headers = { "Content-Type": "application/json", "X-Access-Key": access_key, "X-Access-Secret": access_secret, } def _request(self, method, path, params=None, json=None): resp = requests.request(method, f"{self.base_url}{path}", headers=self.headers, params=params, json=json, timeout=10) resp.raise_for_status() return resp.json() # ===== 查询类 ===== def health_check(self): return self._request("GET", "/api/v1/health/") def list_promotions(self, page=1, page_size=20): return self._request("GET", "/api/v1/promotions/", params={"page": page, "page_size": page_size}) def get_promotion_metadata(self, promotion_code): return self._request("GET", f"/api/v1/promotions/{promotion_code}/metadata/") def get_promotion_detail(self, promotion_code): return self._request("GET", f"/api/v1/promotions/{promotion_code}/detail/") def list_tags(self): return self._request("GET", "/api/v1/promotions/tags/") def get_tag(self, tag_code): return self._request("GET", f"/api/v1/promotions/tags/{tag_code}/") def get_trace(self, trace_id): return self._request("GET", f"/api/v1/traces/{trace_id}/") # ===== 计算类 ===== def promo_calculate(self, user_id, cart_items, promotion_codes, order_id, preview=False): return self._request("POST", "/api/v1/promotions/calculate/", json={ "user_id": user_id, "cart_items": cart_items, "promotion_codes": promotion_codes, "preview": preview, "order_id": order_id, }) def tag_calculate(self, tag_code, user_id, cart_items, order_id, preview=False): return self._request("POST", f"/api/v1/promotions/tags/{tag_code}/calculate/", json={ "user_id": user_id, "cart_items": cart_items, "preview": preview, "order_id": order_id, }) def smart_calculate(self, user_id, cart_items, order_id, preview=False): return self._request("POST", "/api/v1/promotions/calculate/smart/", json={ "user_id": user_id, "cart_items": cart_items, "preview": preview, "order_id": order_id, }) # ===== 交易类 ===== def record_usage(self, trace_id, order_id, total_paid, status="completed"): return self._request("POST", "/api/v1/promotions/usage/", json={ "trace_id": trace_id, "order_id": order_id, "total_paid": total_paid, "status": status, }) def refund_calculate(self, trace_id, refund_items, refund_no): return self._request("POST", "/api/v1/refund/calculate/", json={ "trace_id": trace_id, "refund_items": refund_items, "refund_no": refund_no, }) def refund_execute(self, trace_id, refund_items, refund_amount, refund_no): return self._request("POST", "/api/v1/refund/execute/", json={ "trace_id": trace_id, "refund_items": refund_items, "refund_amount": refund_amount, "refund_no": refund_no, }) if __name__ == "__main__": client = MyPromotionClient("your_access_key", "your_access_secret") result = client.promo_calculate( user_id="USER001", cart_items=[{"sku": "SKU001", "quantity": 2, "price": "100.00"}], promotion_codes=["PROMO001"], order_id="ORDER_20260504_001", ) print(result)
Java 1.8+ | Jackson 2.15+
import com.fasterxml.jackson.databind.ObjectMapper; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; import java.util.*; public class MyPromotionClient { private final String baseUrl; private final String accessKey; private final String accessSecret; private final ObjectMapper mapper = new ObjectMapper(); public MyPromotionClient(String accessKey, String accessSecret, String baseUrl) { this.accessKey = accessKey; this.accessSecret = accessSecret; this.baseUrl = baseUrl.replaceAll("/+$", ""); } public MyPromotionClient(String accessKey, String accessSecret) { this(accessKey, accessSecret, "https://api.example.com"); } /* ===== 核心请求辅助 ===== */ private Map<String, Object> request(String method, String path, Map<String, String> query, Map<String, Object> body) throws Exception { String url = baseUrl + path; if (query != null && !query.isEmpty()) { StringBuilder qs = new StringBuilder("?"); for (Map.Entry<String, String> e : query.entrySet()) qs.append(e.getKey()).append("=").append(URLEncoder.encode(e.getValue(), "UTF-8")).append("&"); url += qs.toString(); } HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod(method); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("X-Access-Key", accessKey); conn.setRequestProperty("X-Access-Secret", accessSecret); if (body != null) { conn.setDoOutput(true); OutputStream os = conn.getOutputStream(); os.write(mapper.writeValueAsString(body).getBytes(StandardCharsets.UTF_8)); os.flush(); os.close(); } int code = conn.getResponseCode(); InputStream stream = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); BufferedReader in = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); String line; while ((line = in.readLine()) != null) sb.append(line); in.close(); if (code < 200 || code >= 300) { throw new RuntimeException("HTTP " + code + ": " + sb); } return mapper.readValue(sb.toString(), Map.class); } /* ===== 查询类 ===== */ public Map<String, Object> healthCheck() throws Exception { return request("GET", "/api/v1/health/", null, null); } public Map<String, Object> listPromotions(int page, int pageSize) throws Exception { Map<String, String> q = new HashMap<>(); q.put("page", String.valueOf(page)); q.put("page_size", String.valueOf(pageSize)); return request("GET", "/api/v1/promotions/", q, null); } public Map<String, Object> getPromotionMetadata(String promotionCode) throws Exception { return request("GET", "/api/v1/promotions/" + promotionCode + "/metadata/", null, null); } public Map<String, Object> getPromotionDetail(String promotionCode) throws Exception { return request("GET", "/api/v1/promotions/" + promotionCode + "/detail/", null, null); } public Map<String, Object> listTags() throws Exception { return request("GET", "/api/v1/promotions/tags/", null, null); } public Map<String, Object> getTag(String tagCode) throws Exception { return request("GET", "/api/v1/promotions/tags/" + tagCode + "/", null, null); } public Map<String, Object> getTrace(String traceId) throws Exception { return request("GET", "/api/v1/traces/" + traceId + "/", null, null); } /* ===== 计算类 ===== */ public Map<String, Object> promoCalculate(String userId, List<Map<String, Object>> cartItems, List<String> promotionCodes, String orderId, boolean preview) throws Exception { Map<String, Object> body = new HashMap<>(); body.put("user_id", userId); body.put("cart_items", cartItems); body.put("promotion_codes", promotionCodes); body.put("preview", preview); body.put("order_id", orderId); return request("POST", "/api/v1/promotions/calculate/", null, body); } public Map<String, Object> tagCalculate(String tagCode, String userId, List<Map<String, Object>> cartItems, String orderId, boolean preview) throws Exception { Map<String, Object> body = new HashMap<>(); body.put("user_id", userId); body.put("cart_items", cartItems); body.put("preview", preview); body.put("order_id", orderId); return request("POST", "/api/v1/promotions/tags/" + tagCode + "/calculate/", null, body); } public Map<String, Object> smartCalculate(String userId, List<Map<String, Object>> cartItems, String orderId, boolean preview) throws Exception { Map<String, Object> body = new HashMap<>(); body.put("user_id", userId); body.put("cart_items", cartItems); body.put("preview", preview); body.put("order_id", orderId); return request("POST", "/api/v1/promotions/calculate/smart/", null, body); } /* ===== 交易类 ===== */ public Map<String, Object> recordUsage(String traceId, String orderId, String totalPaid, String status) throws Exception { Map<String, Object> body = new HashMap<>(); body.put("trace_id", traceId); body.put("order_id", orderId); body.put("total_paid", totalPaid); body.put("status", status); return request("POST", "/api/v1/promotions/usage/", null, body); } public Map<String, Object> refundCalculate(String traceId, List<Map<String, Object>> refundItems, String refundNo) throws Exception { Map<String, Object> body = new HashMap<>(); body.put("trace_id", traceId); body.put("refund_items", refundItems); body.put("refund_no", refundNo); return request("POST", "/api/v1/refund/calculate/", null, body); } public Map<String, Object> refundExecute(String traceId, List<Map<String, Object>> refundItems, String refundAmount, String refundNo) throws Exception { Map<String, Object> body = new HashMap<>(); body.put("trace_id", traceId); body.put("refund_items", refundItems); body.put("refund_amount", refundAmount); body.put("refund_no", refundNo); return request("POST", "/api/v1/refund/execute/", null, body); } }
Go 1.21+
package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" ) type MyPromotionClient struct { BaseURL string AccessKey string AccessSecret string client *http.Client } func NewMyPromotionClient(key, secret string) *MyPromotionClient { return &MyPromotionClient{ BaseURL: "https://api.example.com", AccessKey: key, AccessSecret: secret, client: &http.Client{Timeout: 10 * time.Second}, } } func (c *MyPromotionClient) request(method, path string, query map[string]string, body map[string]interface{}) (map[string]interface{}, error) { url := c.BaseURL + path if len(query) > 0 { qs := "?" for k, v := range query { qs += k + "=" + v + "&" } url += qs } var bodyReader *bytes.Reader if body != nil { b, _ := json.Marshal(body) bodyReader = bytes.NewReader(b) } else { bodyReader = bytes.NewReader([]byte{}) } req, _ := http.NewRequest(method, url, bodyReader) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Access-Key", c.AccessKey) req.Header.Set("X-Access-Secret", c.AccessSecret) resp, err := c.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(b)) } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return result, nil } // ===== 查询类 ===== func (c *MyPromotionClient) HealthCheck() (map[string]interface{}, error) { return c.request("GET", "/api/v1/health/", nil, nil) } func (c *MyPromotionClient) ListPromotions(page, pageSize int) (map[string]interface{}, error) { return c.request("GET", "/api/v1/promotions/", map[string]string{"page": strconv.Itoa(page), "page_size": strconv.Itoa(pageSize)}, nil) } func (c *MyPromotionClient) GetPromotionMetadata(promotionCode string) (map[string]interface{}, error) { return c.request("GET", "/api/v1/promotions/"+promotionCode+"/metadata/", nil, nil) } func (c *MyPromotionClient) GetPromotionDetail(promotionCode string) (map[string]interface{}, error) { return c.request("GET", "/api/v1/promotions/"+promotionCode+"/detail/", nil, nil) } func (c *MyPromotionClient) ListTags() (map[string]interface{}, error) { return c.request("GET", "/api/v1/promotions/tags/", nil, nil) } func (c *MyPromotionClient) GetTag(tagCode string) (map[string]interface{}, error) { return c.request("GET", "/api/v1/promotions/tags/"+tagCode+"/", nil, nil) } func (c *MyPromotionClient) GetTrace(traceID string) (map[string]interface{}, error) { return c.request("GET", "/api/v1/traces/"+traceID+"/", nil, nil) } // ===== 计算类 ===== func (c *MyPromotionClient) PromoCalculate(userID string, cartItems []map[string]interface{}, promotionCodes []string, orderID string, preview bool) (map[string]interface{}, error) { return c.request("POST", "/api/v1/promotions/calculate/", nil, map[string]interface{}{ "user_id": userID, "cart_items": cartItems, "promotion_codes": promotionCodes, "preview": preview, "order_id": orderID, }) } func (c *MyPromotionClient) TagCalculate(tagCode, userID string, cartItems []map[string]interface{}, orderID string, preview bool) (map[string]interface{}, error) { return c.request("POST", "/api/v1/promotions/tags/"+tagCode+"/calculate/", nil, map[string]interface{}{ "user_id": userID, "cart_items": cartItems, "preview": preview, "order_id": orderID, }) } func (c *MyPromotionClient) SmartCalculate(userID string, cartItems []map[string]interface{}, orderID string, preview bool) (map[string]interface{}, error) { return c.request("POST", "/api/v1/promotions/calculate/smart/", nil, map[string]interface{}{ "user_id": userID, "cart_items": cartItems, "preview": preview, "order_id": orderID, }) } // ===== 交易类 ===== func (c *MyPromotionClient) RecordUsage(traceID, orderID, totalPaid, status string) (map[string]interface{}, error) { return c.request("POST", "/api/v1/promotions/usage/", nil, map[string]interface{}{ "trace_id": traceID, "order_id": orderID, "total_paid": totalPaid, "status": status, }) } func (c *MyPromotionClient) RefundCalculate(traceID string, refundItems []map[string]interface{}, refundNo string) (map[string]interface{}, error) { return c.request("POST", "/api/v1/refund/calculate/", nil, map[string]interface{}{ "trace_id": traceID, "refund_items": refundItems, "refund_no": refundNo, }) } func (c *MyPromotionClient) RefundExecute(traceID string, refundItems []map[string]interface{}, refundAmount, refundNo string) (map[string]interface{}, error) { return c.request("POST", "/api/v1/refund/execute/", nil, map[string]interface{}{ "trace_id": traceID, "refund_items": refundItems, "refund_amount": refundAmount, "refund_no": refundNo, }) } }
PHP 8.1+ | ext-curl
<?php class MyPromotionClient { private string $baseUrl; private string $accessKey; private string $accessSecret; public function __construct(string $key, string $secret, string $baseUrl = "https://api.example.com") { $this->accessKey = $key; $this->accessSecret = $secret; $this->baseUrl = rtrim($baseUrl, "/"); } private function request(string $method, string $path, array $query = [], array $body = null): array { $url = $this->baseUrl . $path; if (!empty($query)) $url .= "?" . http_build_query($query); $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); } curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Content-Type: application/json", "X-Access-Key: " . $this->accessKey, "X-Access-Secret: " . $this->accessSecret, ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $resp = curl_exec($ch); curl_close($ch); return json_decode($resp, true); } /* ===== 查询类 ===== */ public function healthCheck(): array { return $this->request("GET", "/api/v1/health/"); } public function listPromotions(int $page = 1, int $pageSize = 20): array { return $this->request("GET", "/api/v1/promotions/", ["page" => $page, "page_size" => $pageSize]); } public function getPromotionMetadata(string $promotionCode): array { return $this->request("GET", "/api/v1/promotions/{$promotionCode}/metadata/"); } public function getPromotionDetail(string $promotionCode): array { return $this->request("GET", "/api/v1/promotions/{$promotionCode}/detail/"); } public function listTags(): array { return $this->request("GET", "/api/v1/promotions/tags/"); } public function getTag(string $tagCode): array { return $this->request("GET", "/api/v1/promotions/tags/{$tagCode}/"); } public function getTrace(string $traceId): array { return $this->request("GET", "/api/v1/traces/{$traceId}/"); } /* ===== 计算类 ===== */ public function promoCalculate(string $userId, array $cartItems, array $promotionCodes, string $orderId, bool $preview = false): array { return $this->request("POST", "/api/v1/promotions/calculate/", [], [ "user_id" => $userId, "cart_items" => $cartItems, "promotion_codes" => $promotionCodes, "preview" => $preview, "order_id" => $orderId, ]); } public function tagCalculate(string $tagCode, string $userId, array $cartItems, string $orderId, bool $preview = false): array { return $this->request("POST", "/api/v1/promotions/tags/{$tagCode}/calculate/", [], [ "user_id" => $userId, "cart_items" => $cartItems, "preview" => $preview, "order_id" => $orderId, ]); } public function smartCalculate(string $userId, array $cartItems, string $orderId, bool $preview = false): array { return $this->request("POST", "/api/v1/promotions/calculate/smart/", [], [ "user_id" => $userId, "cart_items" => $cartItems, "preview" => $preview, "order_id" => $orderId, ]); } /* ===== 交易类 ===== */ public function recordUsage(string $traceId, string $orderId, string $totalPaid, string $status = "completed"): array { return $this->request("POST", "/api/v1/promotions/usage/", [], [ "trace_id" => $traceId, "order_id" => $orderId, "total_paid" => $totalPaid, "status" => $status, ]); } public function refundCalculate(string $traceId, array $refundItems, string $refundNo): array { return $this->request("POST", "/api/v1/refund/calculate/", [], [ "trace_id" => $traceId, "refund_items" => $refundItems, "refund_no" => $refundNo, ]); } public function refundExecute(string $traceId, array $refundItems, string $refundAmount, string $refundNo): array { return $this->request("POST", "/api/v1/refund/execute/", [], [ "trace_id" => $traceId, "refund_items" => $refundItems, "refund_amount" => $refundAmount, "refund_no" => $refundNo, ]); } } $client = new MyPromotionClient("your_access_key", "your_access_secret"); $result = $client->promoCalculate( "USER001", [["sku" => "SKU001", "quantity" => 2, "price" => "100.00"]], ["PROMO001"], "ORDER_20260504_001" ); echo $result["meta"]["trace_id"] ?? "no trace";
Node.js 18+ | axios 1.6+
const axios = require('axios'); class MyPromotionClient { constructor(accessKey, accessSecret, baseUrl = 'https://api.example.com') { this.baseUrl = baseUrl.replace(/\/$/, ''); this.headers = { 'Content-Type': 'application/json', 'X-Access-Key': accessKey, 'X-Access-Secret': accessSecret, }; } async _request(method, path, params = null, body = null) { const url = this.baseUrl + path + (params ? '?' + new URLSearchParams(params).toString() : ''); const resp = await axios({ method, url, headers: this.headers, data: body, timeout: 10000 }); return resp.data; } // ===== 查询类 ===== async healthCheck() { return this._request('GET', '/api/v1/health/'); } async listPromotions(page = 1, pageSize = 20) { return this._request('GET', '/api/v1/promotions/', { page, page_size: pageSize }); } async getPromotionMetadata(promotionCode) { return this._request('GET', `/api/v1/promotions/${promotionCode}/metadata/`); } async getPromotionDetail(promotionCode) { return this._request('GET', `/api/v1/promotions/${promotionCode}/detail/`); } async listTags() { return this._request('GET', '/api/v1/promotions/tags/'); } async getTag(tagCode) { return this._request('GET', `/api/v1/promotions/tags/${tagCode}/`); } async getTrace(traceId) { return this._request('GET', `/api/v1/traces/${traceId}/`); } // ===== 计算类 ===== async promoCalculate(userId, cartItems, promotionCodes, orderId, preview = false) { return this._request('POST', '/api/v1/promotions/calculate/', null, { user_id: userId, cart_items: cartItems, promotion_codes: promotionCodes, preview, order_id: orderId, }); } async tagCalculate(tagCode, userId, cartItems, orderId, preview = false) { return this._request('POST', `/api/v1/promotions/tags/${tagCode}/calculate/`, null, { user_id: userId, cart_items: cartItems, preview, order_id: orderId, }); } async smartCalculate(userId, cartItems, orderId, preview = false) { return this._request('POST', '/api/v1/promotions/calculate/smart/', null, { user_id: userId, cart_items: cartItems, preview, order_id: orderId, }); } // ===== 交易类 ===== async recordUsage(traceId, orderId, totalPaid, status = 'completed') { return this._request('POST', '/api/v1/promotions/usage/', null, { trace_id: traceId, order_id: orderId, total_paid: totalPaid, status, }); } async refundCalculate(traceId, refundItems, refundNo) { return this._request('POST', '/api/v1/refund/calculate/', null, { trace_id: traceId, refund_items: refundItems, refund_no: refundNo, }); } async refundExecute(traceId, refundItems, refundAmount, refundNo) { return this._request('POST', '/api/v1/refund/execute/', null, { trace_id: traceId, refund_items: refundItems, refund_amount: refundAmount, refund_no: refundNo, }); } } (async () => { const client = new MyPromotionClient('your_access_key', 'your_access_secret'); const result = await client.promoCalculate( 'USER001', [{ sku: 'SKU001', quantity: 2, price: '100.00' }], ['PROMO001'], 'ORDER_20260504_001', ); console.log(result); })();
cURL 7.68+ | OpenSSL 1.1.1+
curl -X GET "https://api.example.com/api/v1/health/" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret"
curl -X GET "https://api.example.com/api/v1/promotions/?page=1&page_size=20" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret"
curl -X GET "https://api.example.com/api/v1/promotions/PROMO001/metadata/" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret"
curl -X GET "https://api.example.com/api/v1/promotions/PROMO001/detail/" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret"
curl -X GET "https://api.example.com/api/v1/promotions/tags/" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret"
curl -X GET "https://api.example.com/api/v1/promotions/tags/TAG001/" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret"
curl -X POST "https://api.example.com/api/v1/promotions/calculate/" \ -H "Content-Type: application/json" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret" \ -d '{ "user_id": "USER001", "cart_items": [ {"sku": "SKU001", "quantity": 2, "price": "100.00"} ], "promotion_codes": ["PROMO001"], "preview": false, "order_id": "ORDER_20260504_001" }'
curl -X POST "https://api.example.com/api/v1/promotions/tags/TAG001/calculate/" \ -H "Content-Type: application/json" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret" \ -d '{ "user_id": "USER001", "cart_items": [ {"sku": "SKU001", "quantity": 2, "price": "100.00"} ], "preview": false, "order_id": "ORDER_20260504_001" }'
curl -X POST "https://api.example.com/api/v1/promotions/calculate/smart/" \ -H "Content-Type: application/json" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret" \ -d '{ "user_id": "USER001", "cart_items": [ {"sku": "SKU001", "quantity": 2, "price": "100.00"} ], "preview": false, "order_id": "ORDER_20260504_001" }'
curl -X POST "https://api.example.com/api/v1/promotions/usage/" \ -H "Content-Type: application/json" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret" \ -d '{ "trace_id": "trace-xxx", "order_id": "ORDER_20260504_001", "total_paid": "170.00", "status": "completed" }'
curl -X GET "https://api.example.com/api/v1/traces/trace-xxx/" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret"
curl -X POST "https://api.example.com/api/v1/refund/calculate/" \ -H "Content-Type: application/json" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret" \ -d '{ "trace_id": "trace-xxx", "refund_items": [ {"sku": "SKU001", "quantity": 1} ], "refund_no": "REF_20260504_001" }'
curl -X POST "https://api.example.com/api/v1/refund/execute/" \ -H "Content-Type: application/json" \ -H "X-Access-Key: your_access_key" \ -H "X-Access-Secret: your_access_secret" \ -d '{ "trace_id": "trace-xxx", "refund_items": [ {"sku": "SKU001", "quantity": 1} ], "refund_amount": "30.00", "refund_no": "REF_20260504_001" }'
Webhook 推送 SDK
外部系统向 MyPromotion 推送数据时使用。所有推送请求必须携带 HMAC-SHA256 签名。
Python 3.9+ | requests 2.28+
import hmac import hashlib import time import json import requests class MyPromotionClient: def __init__(self, access_secret, base_url="https://api.example.com"): self.base_url = base_url.rstrip("/") self.access_secret = access_secret def send_webhook(self, data_type, config_key, payload_dict): url = f"{self.base_url}/webhook/v1/{data_type}/{config_key}/" payload = json.dumps(payload_dict, separators=(',', ':')) timestamp = str(int(time.time())) sign_content = f"{timestamp}.{payload}" signature = hmac.new( self.access_secret.encode('utf-8'), sign_content.encode('utf-8'), hashlib.sha256 ).hexdigest() resp = requests.post(url, data=payload, headers={ 'Content-Type': 'application/json', 'X-Webhook-Signature': signature, 'X-Webhook-Timestamp': timestamp, }) return resp.json() if __name__ == "__main__": client = MyPromotionClient("your_webhook_secret_key") result = client.send_webhook( data_type="product", config_key="your_config_key", payload_dict={"action": "modify", "items": [{"outer_id": "SKU001"}]} ) print(result)
Java 1.8+
import com.fasterxml.jackson.databind.ObjectMapper; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; public class MyPromotionClient { private final String baseUrl; private final String accessSecret; private final ObjectMapper mapper = new ObjectMapper(); public MyPromotionClient(String accessSecret, String baseUrl) { this.accessSecret = accessSecret; this.baseUrl = baseUrl.replaceAll("/+$", ""); } public MyPromotionClient(String accessSecret) { this(accessSecret, "https://api.example.com"); } private String sign(String payload, String ts) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(accessSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] b = mac.doFinal((ts + "." + payload).getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte c : b) sb.append(String.format("%02x", c)); return sb.toString(); } public String sendWebhook(String dataType, String configKey, Object payloadDict) throws Exception { String url = baseUrl + "/webhook/v1/" + dataType + "/" + configKey + "/"; String payload = mapper.writeValueAsString(payloadDict); String ts = String.valueOf(System.currentTimeMillis() / 1000); String sig = sign(payload, ts); HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("X-Webhook-Signature", sig); conn.setRequestProperty("X-Webhook-Timestamp", ts); conn.setDoOutput(true); try (OutputStream os = conn.getOutputStream()) { os.write(payload.getBytes(StandardCharsets.UTF_8)); } int code = conn.getResponseCode(); InputStream stream = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); BufferedReader in = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); String line; while ((line = in.readLine()) != null) sb.append(line); in.close(); if (code < 200 || code >= 300) { throw new RuntimeException("HTTP " + code + ": " + sb); } return sb.toString(); } public static void main(String[] args) throws Exception { MyPromotionClient client = new MyPromotionClient("your_webhook_secret_key"); String result = client.sendWebhook( "product", "your_config_key", java.util.Collections.singletonMap("action", "modify") ); System.out.println(result); } }
Go 1.21+
package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type MyPromotionClient struct { BaseURL string AccessSecret string client *http.Client } func NewMyPromotionClient(secret string) *MyPromotionClient { return &MyPromotionClient{ BaseURL: "https://api.example.com", AccessSecret: secret, client: http.DefaultClient, } } func (c *MyPromotionClient) sign(payload, ts string) string { mac := hmac.New(sha256.New, []byte(c.AccessSecret)) mac.Write([]byte(ts + "." + payload)) return hex.EncodeToString(mac.Sum(nil)) } func (c *MyPromotionClient) SendWebhook(dataType, configKey string, payloadDict map[string]interface{}) (map[string]interface{}, error) { url := fmt.Sprintf("%s/webhook/v1/%s/%s/", c.BaseURL, dataType, configKey) payloadBytes, _ := json.Marshal(payloadDict) payload := string(payloadBytes) ts := fmt.Sprintf("%d", time.Now().Unix()) sig := c.sign(payload, ts) req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Webhook-Signature", sig) req.Header.Set("X-Webhook-Timestamp", ts) resp, err := c.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(b)) } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return result, nil } func main() { client := NewMyPromotionClient("your_webhook_secret_key") result, err := client.SendWebhook("product", "your_key", map[string]interface{}{ "action": "modify", "items": []map[string]interface{}{{"outer_id": "SKU001"}}, }) if err != nil { panic(err) } fmt.Println(result) }
PHP 8.1+ | ext-curl
<?php class MyPromotionClient { private string $baseUrl = "https://api.example.com"; private string $accessSecret; public function __construct(string $secret) { $this->accessSecret = $secret; } private function sign(string $payload, string $ts): string { return hash_hmac('sha256', $ts . "." . $payload, $this->accessSecret); } public function sendWebhook(string $dataType, string $configKey, array $payloadDict): string { $url = $this->baseUrl . "/webhook/v1/{$dataType}/{$configKey}/"; $payload = json_encode($payloadDict); $ts = (string)time(); $sig = $this->sign($payload, $ts); $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Content-Type: application/json", "X-Webhook-Signature: " . $sig, "X-Webhook-Timestamp: " . $ts, ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); curl_close($ch); return $result; } } $client = new MyPromotionClient("your_webhook_secret_key"); $result = $client->sendWebhook( "product", "your_key", ["action" => "modify", "items" => [["outer_id" => "SKU001"]]] ); echo $result;
Node.js 18+
const crypto = require('crypto'); class MyPromotionClient { constructor(accessSecret, baseUrl = 'https://api.example.com') { this.baseUrl = baseUrl; this.accessSecret = accessSecret; } sign(payload, ts) { return crypto.createHmac('sha256', this.accessSecret) .update(`${ts}.${payload}`) .digest('hex'); } async sendWebhook(dataType, configKey, payloadDict) { const payload = JSON.stringify(payloadDict); const ts = Math.floor(Date.now() / 1000).toString(); const signature = this.sign(payload, ts); const resp = await fetch(`${this.baseUrl}/webhook/v1/${dataType}/${configKey}/`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Webhook-Signature': signature, 'X-Webhook-Timestamp': ts, }, body: payload, }); return resp.json(); } } const client = new MyPromotionClient('your_webhook_secret_key'); client.sendWebhook('product', 'your_config_key', { action: 'modify', items: [{ outer_id: 'SKU001', name: '商品1' }], }).then(console.log);
cURL 7.68+ | OpenSSL 1.1.1+
# 签名字符串:{timestamp}.{json_payload} # 算法:HMAC-SHA256,密钥:your_webhook_secret_key # 输出:hex 编码的签名值 curl -X POST "https://api.example.com/webhook/v1/{data_type}/{config_key}/" \ -H "Content-Type: application/json" \ -H "X-Webhook-Signature: a1b2c3d4e5f6..." \ -H "X-Webhook-Timestamp: 1710423456" \ -d '{"action":"modify","items":[{"outer_id":"SKU001","name":"商品1"}]}'