接口总览

所有接口以 /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 认证。接入申请审批通过后,平台将为您分配一组 AccessKeyAccessSecret,调用接口时在请求头中携带即可。

🔑 AccessKey 认证

在每个请求 Header 中携带 X-Access-KeyX-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 章节。

系统接口

GET /api/v1/health/
无需认证 限流:无

检查服务是否正常运行。

响应示例(200 OK)
{
    "status": "ok",
    "service": "promotion-platform",
    "version": "1.0.0"
}
状态码
HTTP业务码说明
200OK服务正常运行

促销规则查询

查询促销规则基本信息,支持分页、筛选、排序。

GET /api/v1/promotions/
需要认证 限流:1000/min
参数类型必填说明
statusstring状态筛选:active / pending / expired
strategy_typestring策略类型筛选
valid_nowboolean只返回当前有效的规则(true/false)
orderingstring排序字段,如 -priority,-created_at
pageint页码,默认 1
page_sizeint每页数量,默认 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业务码说明
200OK成功返回分页列表
401UNAUTHORIZED缺少或无效的认证信息
429TOO_MANY_REQUESTS请求过于频繁
GET /api/v1/promotions/{promotion_code}/metadata/
需要认证 限流:1000/min

获取单个促销规则的元信息(不含敏感配置)。

参数类型必填说明
promotion_codestring促销编码,如 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业务码说明
200OK成功返回促销元信息
401UNAUTHORIZED缺少或无效的认证信息
404NOT_FOUND促销规则不存在
429TOO_MANY_REQUESTS请求过于频繁
GET /api/v1/promotions/{promotion_code}/detail/
需要认证 限流:1000/min

获取促销规则完整信息(含条件、动作、范围配置),仅限内部使用。

参数类型必填说明
promotion_codestring促销编码
响应示例(含 conditions / actions / scopes)
{
    "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业务码说明
200OK成功返回规则详情
401UNAUTHORIZED缺少或无效的认证信息
404NOT_FOUND促销规则不存在
429TOO_MANY_REQUESTS请求过于频繁

促销计算

根据购物车商品和用户信息,计算适用的促销优惠。支持促销与券组合计算、价格保护、自动凑单提示。

POST /api/v1/promotions/calculate/
需要认证 限流:150/s(租户级)

标准促销计算,指定促销编码进行计算。

请求体
参数类型必填说明
user_idint / string用户ID(支持内部数字ID或外部系统字符串 outer_id),不传默认 0
order_idstring条件必填订单号。preview=false 时必须传入,将计算凭证与订单绑定,供后续退款追溯。preview=true(默认)时无需传入
cart_itemsarray购物车商品列表
cart_items[].skustring商品SKU
cart_items[].quantityint商品数量,≥1
cart_items[].pricedecimal商品单价,≥0.01
cart_items[].category_idstring品类ID(未传时自动从商品库补全)
cart_items[].brand_idstring品牌ID(未传时自动从商品库补全)
cart_items[].tagsarray商品标签列表(未传时自动从商品库补全)
cart_items[].cost_pricestring成本价(用于价格保护 POC 测试,未传时自动从商品库补全)
cart_items[].member_pricestring会员价(用于价格保护 POC 测试,未传时自动从商品库补全)
promotion_codesarray指定促销编码列表,如 ["PROMO001", "PROMO002"]
contextobject上下文信息(开放结构)
context.channelstring渠道:app / pc / miniapp / h5
context.user_groupstring用户群体:vip / member
context.shipping_methodstring配送方式
context.payment_methodstring支付方式
user_infoobject用户信息(用于生日等画像条件判断)
user_info.birthdaystring用户生日,格式 YYYY-MM-DD
coupon_codesarray券实例编码列表,系统按 outer_id 精确查询
coupon_template_idsarray券模板编码列表,系统自动匹配可用实例
used_couponsarray券详细信息列表(内部完整模式,直接参与抵扣)
calculation_orderarray / string计算顺序。默认 ["promotions","coupons"],支持 "optimal"(并行取最优)
previewboolean预览模式。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"
}
请求示例(价格保护 POC)
{
    "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.statusstringAPPLIED 促销/券已生效;NO_MATCH 无匹配促销或券未满足条件;BLOCKED 价格保护拦截;ERROR 计算异常
meta.codestring见下方「meta.code 枚举值」
meta.messagestring状态描述文本
meta.timestampint时间戳(毫秒)
data.summary(金额汇总)
data.summary.original_amountstring原始金额
data.summary.total_discountstring总优惠金额(促销+券)
data.summary.payable_amountstring应付金额
data.summary.availablebooleantrue 有可用促销;false 无可用促销
data.summary.coupon_discountstring券抵扣总金额(仅当使用了券时返回)
data.summary.reward_summaryobject奖励汇总,如 {"coupon": 1, "gift": 2}(仅当有奖励时返回)
data.applied_promotions[](应用的促销列表)
...promotion_codestring促销编码
...strategy_typestring见下方「strategy_type 枚举值」
...strategy_type_displaystring策略类型显示名(如"满减")
...benefit_typestringdirect_discount 直接金额折扣;reward 赠送权益(券/礼物/积分);mixed 既有折扣又有赠品;free_shipping 仅免运费;no_benefit 无实际优惠
...discountstring优惠金额
...applied_itemsarray应用的商品SKU列表
...rewardsarray奖励列表(赠品、优惠券等)
...messagestring提示信息
data.item_discounts[](商品级优惠分摊明细)
...skustring商品SKU
...quantityint数量
...original_pricestring该商品小计原价
...allocated_discountstring分摊到该商品的优惠金额
...payablestring该商品分摊后应付金额
其他 data 字段
data.used_couponsarray实际参与抵扣的券明细(仅当使用了券时返回)。每项含 codecoupon_typededucted_amountstatus(固定值 applied
data.price_protectionobject价格保护信息(仅当触发价格保护时返回)。含 triggeredviolations[]
data.optimization_tipsarray自动凑单提示(仅当有可用建议时返回)
data.not_found_codesarray未找到的促销编码列表(仅当传入的 promotion_codes 中有不存在编码时返回)
trace(链路追踪)
trace.trace_idstring计算链路追踪ID(预览模式 preview=true 时不生成)
trace.logsarray诊断日志(如未找到编码的警告)
trace.diagnosisobject结构化诊断信息(调试用途)
meta.code 枚举值
触发场景业务含义
PROMOTIONS_APPLIED有匹配促销且折扣生效促销已生效
COUPONS_APPLIED仅传券、券抵扣成功券抵扣已生效
NO_MATCHING_PROMOTIONS有匹配促销规则但条件不满足没有匹配的促销规则
COUPONS_NOT_APPLIED仅传券、券条件不满足券不满足使用条件,未产生优惠
NO_INPUT未传 promotion_codes 和 coupon 相关参数未指定促销编码和券编码,未进行任何计算
PRICE_PROTECTION_BLOCKED价格保护规则拦截部分商品触发价格保护拦截,无法下单
CALCULATION_ERROR计算异常计算过程中发生错误
SERVICE_UNAVAILABLE系统异常降级服务暂时不可用,已返回降级结果
strategy_type 枚举值
说明
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_mN件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业务码说明
200SUCCESS计算成功,返回 trace_id
400VALIDATION_ERROR参数校验失败
429TOO_MANY_REQUESTS请求过于频繁(超过租户配额)
500CALCULATION_ERROR计算服务异常
503SERVICE_UNAVAILABLE服务暂时不可用
POST /api/v1/promotions/tags/{tag_code}/calculate/
需要认证 限流:50/s(租户级)

按指定标签下的促销规则进行计算。请求参数和响应结构与 /promotions/calculate/ 一致,系统自动将标签关联的促销编码传入计算。

参数类型必填说明
tag_codestring路径参数,标签编码,如 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业务码说明
200SUCCESS计算成功
400BAD_REQUESTJSON 格式错误或参数校验失败
500CALCULATION_ERROR计算服务异常
POST /api/v1/promotions/calculate/smart/
需要认证 限流:30/s(租户级);optimal 模式额外 1/s

智能促销计算,支持三种模式自动匹配最佳促销组合。

参数类型必填说明
promotion_selectobject促销选择配置(不传时退化为标准计算)
promotion_select.modestring选择模式:auto / tags / explicit
promotion_select.configobject模式配置
promotion_select.config.include_tagsarraytags 模式:包含的标签编码列表
promotion_select.config.exclude_codesarray排除的促销编码列表
其余请求体字段同标准计算(cart_items / context / coupon_codes 等)
请求示例(auto 模式)
{
    "user_id": 1001,
    "cart_items": [
        {"sku": "SKU001", "quantity": 2, "price": "60.00"}
    ],
    "promotion_select": {
        "mode": "auto",
        "config": {
            "exclude_codes": ["PROMO999"]
        }
    },
    "context": {"channel": "pc"}
}
请求示例(tags 模式)
{
    "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业务码说明
200SUCCESS计算成功
400BAD_REQUESTJSON 格式错误或参数校验失败
500CALCULATION_ERROR计算服务异常

促销标签

通过标签体系对促销进行分组管理,便于前端按场景快速调用。

GET /api/v1/promotions/tags/
需要认证 限流:1000/min

获取可用标签列表,含每个标签关联的促销规则数量。

响应示例
{
    "count": 5,
    "results": [
        {
            "tag_code": "new_user",
            "name": "新人专享",
            "promotion_count": 3,
            "description": "新注册用户可用"
        },
        {
            "tag_code": "flash_sale",
            "name": "限时抢购",
            "promotion_count": 2,
            "description": "限时特价活动"
        }
    ]
}
状态码
HTTP业务码说明
200OK成功返回标签列表
GET /api/v1/promotions/tags/{tag_code}/
需要认证 限流:1000/min

获取标签详情及关联的促销规则列表。

参数类型必填说明
tag_codestring路径参数,标签编码,如 new_user
响应示例
{
    "success": true,
    "data": {
        "tag_code": "new_user",
        "name": "新人专享",
        "description": "新注册用户可用",
        "promotions": [
            {
                "promotion_code": "PROMO001",
                "name": "新人首单8折",
                "strategy_type": "percentage_discount",
                "priority": 20
            }
        ]
    }
}
状态码
HTTP业务码说明
200OK成功返回标签详情
404NOT_FOUND标签不存在

使用留痕 & 退款

促销使用留痕用于锁定优惠明细,退款计算确保下单与退款金额完全一致,支持审计追溯。

POST /api/v1/promotions/usage/
需要认证 限流:500/s(租户级)

订单支付完成后上报促销使用记录,生成计算凭证用于后续退款核对。仅需传入 trace_id,order_id 及其余数据自动从 trace 快照读取。

参数类型必填说明
trace_idstring计算时返回的 trace_id
statusstring记录状态,默认 pending
pending — 待确认。订单已支付但尚未最终成交,促销优惠已预占但未正式生效。核心 KPI(GMV/订单数/用户数)不计入此状态。
confirmed — 已确认。订单已发货或超过可取消期限,促销优惠正式生效。核心 KPI只统计此状态。
cancelled — 已取消。订单在发货前被取消,促销使用作废。需业务系统主动调用
退款数据由退款执行接口自动记录,无需上报
请求示例
{
    "trace_id": "trace-xxx",
    "status": "confirmed"
}

说明:user_idoriginal_amountpayable_amountapplied_promotionscontext_snapshot 等字段会自动从 trace 快照中读取,无需在请求中传入。

🛡️ 引擎层面幂等保证
pending / confirmed / cancelled:调用方无需额外处理,引擎自动以 order_id:promotion_code 为键保证幂等,支持状态流转覆盖。
• 退款数据由退款执行接口自动记录,无需通过 usage 接口上报。
⏰ 自动确认机制(默认乐观确认)
pending 记录超过 7 天未更新为 confirmedcancelled,引擎自动标记为 confirmed
• 设计 rationale:大多数订单支付后最终都会成交,默认乐观确认可减少业务系统接入负担
• 业务系统只需在订单取消时主动调用 usage 接口更新为 cancelled;正常成交的订单无需二次确认
📊 运营看板统计口径
核心 KPI(带动 GMV、实付总额、优惠总金额、渗透率、覆盖用户/订单):仅统计 confirmed 状态
转化漏斗(pending / confirmed / cancelled / refunded):展示全部生命周期状态流转
损耗分析:损耗订单数 = cancelled 记录数 + 已执行退款的订单数(去重);退款金额 = RefundExecution 已执行金额合计;损耗率 = 损耗订单数 ÷ 全部 usage 记录总数;损耗优惠金额 = cancelled 状态的 discount 合计
响应示例
{
    "status": "success",
    "message": "记录已保存",
    "data": {"total_records": 1}
}
状态码
HTTP业务码说明
200SUCCESS记录已保存
400VALIDATION_ERROR参数校验失败或快照缺少 order_id
404TRACE_NOT_FOUNDtrace_id 不存在、已过期或快照数据缺失
429TOO_MANY_REQUESTS请求过于频繁(超过租户配额)
500SAVE_ERROR服务器内部错误
POST /api/v1/refund/calculate/
需要认证 限流:100/s(租户级)

退款计算(计算凭证模式 v4.0)。根据原单锁定的优惠明细直接读取计算退款金额,无需重新算价,确保下单与退款完全一致。

参数类型必填说明
trace_idstring原单计算时返回的 trace_id
refund_itemsarray条件退款商品列表。与 refund_all 至少填一个
refund_items[].skustring商品SKU
refund_items[].quantityint退款数量
refund_allboolean整单退开关。传 true 时系统从快照自动读取所有剩余未退商品,无需传入 refund_items
refund_nostring退款单号(由业务系统传入,会回显到响应中)
请求示例(部分退)
{
    "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.statusstringAPPLIED 退款计算成功;BLOCKED 触发兜底人工处理;ERROR 参数错误或计算异常;NO_MATCH 无剩余可退金额
meta.codestring见下方「meta.code 枚举值」
meta.messagestring状态描述文本
meta.trace_idstring原单 trace_id
data(退款计算结果)
data.refund_amountstring本次应退金额
data.remaining_payablestring退款后剩余应付金额
data.refunded_totalstring该订单累计已退款金额
data.refund_itemsarray商品级退款明细
...skustring商品SKU
...quantityint退款数量
...original_pricestring该商品对应原价
...allocated_discountstring该商品需收回的优惠分摊
...refund_amountstring该商品实际退款金额
...cappedboolean是否因超出剩余可退金额而被截断(仅当被截断时返回)
data.fallbackobject兜底信息。triggered 是否触发;handler_code 处理编码;handler_note 处理说明;reason 触发原因
data.refund_strategyarray各促销的退款策略(如 proportional / full_refund_discount)
data.applied_promotionsarray促销退款策略明细。每项含 promotion_coderefund_strategyrewards[](赠品/券回收策略)
data.refund_tostring固定值 user
data.coupon_refund_advicearray券退券建议(仅当订单使用了券时返回)。每项含 coupon_idcoupon_typecurrent_statuscan_returnadvicereturn_to
data.refund_nostring退款单号(业务系统传入时回显)
data.cappedboolean整体截断标记(仅当退款金额被截断时返回)
data.cap_reasonstring截断原因(仅当被截断时返回)
meta.code 枚举值
触发场景业务含义
REFUND_CALCULATED正常计算成功退款计算成功
FALLBACK_TO_MANUAL触发兜底规则(如退款金额超过阈值)该退款触发人工处理流程
MISSING_TRACE_ID请求未传 trace_idtrace_id 必填
MISSING_REFUND_ITEMS请求未传 refund_items 且未传 refund_allrefund_items 和 refund_all 至少填一个
TRACE_NOT_FOUNDtrace_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"
    }
}
响应示例(Fallback 触发)
{
    "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业务码说明
200REFUND_CALCULATED退款计算成功
200NO_REMAINING_REFUND无剩余可退商品
200FALLBACK_TO_MANUAL触发人工处理流程
400MISSING_TRACE_ID缺少 trace_id
400MISSING_REFUND_ITEMSrefund_items 和 refund_all 至少填一个
400ITEM_NOT_IN_SNAPSHOT商品不在原始订单快照中
404TRACE_NOT_FOUNDtrace_id 不存在或已过期
500CALCULATION_ERROR退款计算异常
POST /api/v1/refund/execute/
需要认证 限流:50/s(租户级)

退款执行,确认退款并记录退款凭证。同一 refund_no 重复请求返回已有结果。

参数类型必填说明
trace_idstring原单 trace_id
refund_itemsarray条件退款商品列表。与 refund_all 至少填一个;若同时传入 refund_all,以此为准
refund_amountstring执行退款金额。不传时系统自动根据 refund_items 计算;传入时以传入值为准(不能超过剩余可退余额)
refund_nostring退款单号(由业务系统传入,全局唯一)
refund_allboolean整单退开关。传 true 时系统从快照自动读取所有剩余未退商品并自动计算退款金额
extra_dataobject额外数据
order_idstring订单号(用于关联外部订单系统)
请求示例(部分退执行)
{
    "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.statusstringAPPLIED 退款执行成功;BLOCKED 触发兜底人工处理;ERROR 参数错误或执行异常
meta.codestring见下方「meta.code 枚举值」
meta.messagestring状态描述文本
meta.trace_idstring原单 trace_id
data(退款执行结果)
data.refund_nostring退款单号(业务系统传入)
data.refund_amountstring本次退款金额
data.refunded_totalstring该订单累计已退款金额
data.statusstringpending 待执行;executed 已执行;failed 失败
data.executed_atstring执行时间(ISO 8601 格式)
meta.code 枚举值
触发场景业务含义
REFUND_EXECUTED正常执行成功退款执行成功
REFUND_DUPLICATE同一 refund_no 重复请求重复请求,返回已有结果
MISSING_PARAM未传 trace_id 或 refund_notrace_id 和 refund_no 必填
TRACE_NOT_FOUNDtrace_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业务码说明
200REFUND_EXECUTED退款执行成功
200REFUND_DUPLICATE重复请求,返回已有结果
200FALLBACK_TO_MANUAL触发人工处理,无法自动执行
400MISSING_PARAMtrace_id 和 refund_no 必填
400MISSING_REFUND_ITEMSrefund_items 和 refund_all 至少填一个
404TRACE_NOT_FOUNDtrace_id 不存在或已过期
500EXECUTE_ERROR退款执行异常

计算凭证查询

返回 trace 完整数据链条:计算前购物数据 → 促销计算结果(含用券) → 售后退款记录。支持整单退和多次部分退的完整审计。查询链路:Redis → MySQL 热库 → OSS 归档存储。

GET /api/v1/traces/{trace_id}/
需要认证 限流:1000/min
参数类型必填说明
trace_idstring路径参数,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业务码说明
200TRACE_FOUND命中,返回完整数据链条
404TRACE_NOT_FOUNDtrace_id 不存在或已过期

API 多语言 SDK

以下示例覆盖促销计算、退款计算与退款执行三个核心接口。请将 ACCESS_KEYACCESS_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+

完整 SDK 示例
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

完整 SDK 示例
<?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+

完整 SDK 示例
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+

Webhook 推送(HMAC-SHA256 签名)
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+

Webhook 推送(HMAC-SHA256 签名)
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+

Webhook 推送(HMAC-SHA256 签名)
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

Webhook 推送(HMAC-SHA256 签名)
<?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+

Webhook 推送(HMAC-SHA256 签名)
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+

Webhook 推送(HMAC-SHA256 签名)
# 签名字符串:{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"}]}'

📦 Payload JSON 示例下载

📦 商品数据 ⬇️ 下载 JSON
👤 用户画像 ⬇️ 下载 JSON
🎫 消费券模板 ⬇️ 下载 JSON
🎟️ 消费券实例 ⬇️ 下载 JSON