硬件销售订单 API(公开 + 用户)
实体硬件销售的公开下单 / 支付 / 查单、登录用户订单查询与好友码接口。与现有虚拟商品支付(/payment/*)完全隔离,独立 hw_sales_* 数据模型,不进 PaymentGateway。
基础信息
- 公开路由前缀:
/public/hardware-sales(独立hardwareSalesCors,支持 GET/POST/OPTIONS;IP + 手机号双维限流) - 手机号 OTP 前缀:
/auth/phone(公开)、/my/phone(登录态) - 登录用户订单前缀:
/hardware-sales(JWT Bearer 或 OAuthorders:checkout) - 金额单位:一律整数分;公开下单/试算不接受客户端金额字段,成交价由服务端按 SKU + 用户选择的
tierCode重算 - 身份前置:公开下单 / 发起支付必须携带 OAuth
orders:checkout、登录 JWT 或短期手机号验证令牌phoneToken,不允许匿名落单
客户可介入阶段
登录用户只能操作自己的硬件销售订单,非 owner 与订单不存在统一返回 404,避免泄露订单存在性。
| 能力 | 允许状态 | 限制 |
|---|---|---|
| 修改收货地址 | fulfillmentStatus ∈ {not_paid, awaiting_stock} | 订单进入 preparing 或已发货后不再允许自助修改,需联系客服发送邮件处理 |
| 标记签收 | fulfillmentStatus = shipped | 只能标记一次;已 delivered 再次提交返回 409 RECEIPT_ALREADY_MARKED |
端点
GET /public/hardware-sales/skus/:sku/config
公开 SKU 配置(全部可展示价格档位 + 标准价档位编码 + 币种)。
认证:无(公开)。
路径参数
| 参数 | 说明 |
|---|---|
sku | SKU 业务编码(如 aiboard_01_cn) |
响应:{ sku, title, available, status, salesMode, currency, standardTierCode, currentTier?, soldOut, tiers[] }。tiers 每项含 tierCode / title / priceCents / isStandard / tierKind / totalPriceCents / payNowPriceCents / topUpPriceCents / publicLimit / displayLimit。绝不返回 real_limit 或 SKU 内部成本价。
常见错误:404 SKU 不存在;429 限流;500 服务端异常。
POST /auth/phone/send-otp
发送手机号验证码(短信发送层当前为 mock provider,阿里云凭据就绪即切换)。
认证:无(公开)。内存级 IP 限流 + DB 维度手机号冷却 / IP 每小时限流。
请求:{ phone: string, lang?: string }。
响应:{ success: true, message, expiresIn, retryAfter }。
常见错误:400 手机号格式无效;429 限流(带 Retry-After);502 发送失败。
POST /auth/phone/verify
校验验证码 + 自动注册 / 登录 + 兜底默认 Team/Project + 签发会话与手机号令牌。
认证:无(公开)。
请求:{ phone: string, code: string, lang?: string }。
响应:{ success: true, data: { user, team, project, session: { accessToken, tokenType, expiresIn }, phoneToken, created } }。其中 phoneToken 为短期手机号验证令牌,供公开下单 / 发起支付使用。
常见错误:400 手机号 / 验证码无效或过期;429 尝试次数过多;500 服务端异常。
已登录用户绑定手机号
已登录用户补绑手机号(必须验证 OTP)。
本端点的权威定义见 POST /my/phone/bind;这里仅说明它在硬件公开下单链路中的用途,避免重复维护同一路由定义。
认证:JWT Bearer Token。
请求:{ phone: string, code: string }。
响应:{ success: true, data: { phone, phoneToken } }。
常见错误:400 手机号 / 验证码无效;401 未认证;409 手机号已被他人绑定;429 尝试过多。
POST /public/hardware-sales/orders
公开下单。金额服务端权威重算,body 中任何 amount / price / cents 字段一律拒绝。
认证:OAuth orders:checkout、登录 JWT 或 phoneToken(三选一,缺一不可)。IP + 手机号双维限流。
请求:{ sku, tierCode, discountCode?, phoneToken?, address: { receiverName, receiverPhone?, province, city, district?, addressLine, postalCode? }, channel?, lang?, utm? }。
响应:{ success: true, data: { orderNo, sku, tierCode, amountCents, originalAmountCents, discountCents, finalAmountCents, discountCodeApplied, currency, paymentStatus, fulfillmentStatus, expiresAt } }。tierCode 是真实成交档位;如果用户选择的档位不可用、过期或售罄,请求失败,不会静默换档。amountCents 与 finalAmountCents 均为实付金额;优惠只落订单级 discountCents,明细快照仍为档位原价。
常见错误:400 地址 / 参数非法 / 请求体包含金额字段;401 身份缺失或无效;404 SKU 不存在;409 档位不可用、售罄或优惠码不可用(无效 / 过期 / 不适用 / 已抢完 / 个人限用 / 未达门槛);429 限流;500 服务端异常。
POST /public/hardware-sales/orders/quote
公开订单试算。复用服务端优惠计算逻辑,不落单、不占库存名额、不占优惠码名额;下单仍会在锁内二次校验。
认证:可匿名调用;如果已带 OAuth orders:checkout、登录 JWT 或 phoneToken,后端会按该身份做试算。匿名试算只用于输入优惠码后的价格预览,不代表最终 checkout 一定成功;下单仍必须带 OAuth/JWT/phoneToken,并会在锁内按真实用户再次校验个人限次、库存和优惠名额。试算使用独立限流桶,默认 10/min,不消耗下单 / 支付额度;限流至少包含 IP,已认证请求会结合手机号,所有请求都会结合优惠码 hash。
请求:{ sku, tierCode, discountCode?, phoneToken? }。discountCode 为空时返回原价试算,不视为无效码。
响应:{ success: true, data: { status, sku, tierCode, originalAmountCents, discountCents, finalAmountCents, discountCodeApplied, currency, discountDetails? } }。
有效优惠码试算成功时(status=ok 且 discountCodeApplied=true),discountDetails 会返回下单前可公开展示的优惠条款:
{
"title": "朋友圈 F 码",
"refundPolicyNote": "退款后好友奖励同步失效",
"capacityLock": true,
"shippingNote": "预计 2026 年 8 月发货"
}title 来自优惠规则标题,refundPolicyNote 来自优惠规则退款条款,capacityLock 来自优惠规则产能锁定标记,shippingNote 来自当前成交档位发货说明。
status 可能值:ok / discount_not_applied / sku_not_found / sku_not_available / tier_not_available / sold_out。优惠码无效、过期、不适用、名额耗尽、个人限用、未达门槛等细因不会在公开试算响应中区分;此时 discountCents=0、discountCodeApplied=false、finalAmountCents=originalAmountCents。用户选择的档位不可用、过期或售罄时返回 tier_not_available,不会静默换档。
匿名试算无法判断当前用户是否已经达到个人使用上限;如果优惠码设置了个人限次,匿名结果只表示该码对当前 SKU/档位和全局名额仍可预览,最终以下单时的身份校验为准。
无效、过期、不适用、名额耗尽、个人限次、未达门槛或未输入优惠码时不会返回 discountDetails。公开响应不会包含 tierId、discountRuleId、discountCodeId、顶层 capacityLock、剩余额度或精确失败原因等内部字段。超过试算限流返回 429 RATE_LIMITED,带 Retry-After 与 X-RateLimit-* 响应头。
POST /public/hardware-sales/orders/:orderNo/pay
为指定订单生成微信支付信息。支持按客户端环境显式选择支付形态(不靠 User-Agent 推断):桌面扫码(Native QR)或微信内置浏览器直接调起收银台(JSAPI)。
认证:OAuth orders:checkout、登录 JWT 或 phoneToken,且身份必须与订单归属匹配(否则统一返回 ORDER_NOT_FOUND,不泄露存在性)。
路径参数
| 参数 | 说明 |
|---|---|
orderNo | 订单号 |
请求体(可选 JSON;不传 body 时保持 Native 扫码行为不变)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
clientPayScene | string | 否 | desktop_browser(默认,Native 扫码)/ wechat_in_app(微信内 JSAPI)/ mobile_browser(暂不支持,返回明确错误) |
clientId | string | wechat_in_app 必填 | OAuth App client_id,用于定位公众号凭据(与 JSSDK 签名同源) |
wechatAuthCode | string | wechat_in_app 必填 | 公众号网页授权 snsapi_base 返回的 code,服务端换取 openid |
returnUrl | string | 否 | 保留字段(H5 支付阶段使用),当前忽略 |
响应(Native 扫码,clientPayScene 缺省或 desktop_browser)
{ "success": true, "data": { "paymentNo": "HWSP...", "paymentMode": "native_qr", "codeUrl": "weixin://wxpay/bizpayurl?...", "qrCodeUrl": null, "paymentUrl": null, "jsapiPayParams": null, "amountCents": 29900, "currency": "CNY", "expiresAt": "..." } }响应(微信内 JSAPI,clientPayScene='wechat_in_app')
{ "success": true, "data": { "paymentNo": "HWSP...", "paymentMode": "wechat_jsapi", "jsapiPayParams": { "appId": "wx...", "timeStamp": "1779459004", "nonceStr": "...", "package": "prepay_id=wx...", "signType": "RSA", "paySign": "..." }, "amountCents": 29900, "currency": "CNY", "expiresAt": "..." } }jsapiPayParams 可直接传给 WeixinJSBridge.invoke('getBrandWCPayRequest', ...) 调起收银台;timeStamp 为秒级字符串,package 固定 prepay_id= 前缀。若走 JSSDK 的 wx.chooseWXPay(...),注意其入参字段名为小写 timestamp,需自行做 timeStamp → timestamp 映射(其余字段同名)。调起结果不可作为支付成功依据,仍以订单查询 / 回调推进为准。
常见错误:401 身份缺失;404 订单不存在或无权;409 订单状态不可支付;429 限流;400 INVALID_PAYMENT_SCENE 场景取值非法;400 PAYMENT_SCENE_NOT_SUPPORTED 普通手机浏览器暂不支持(请在微信中打开);400 WECHAT_AUTH_REQUIRED 微信内支付缺 clientId/wechatAuthCode;401 WECHAT_AUTH_FAILED 授权 code 失效(需重新授权);503 NOT_CONFIGURED 微信渠道未配置或公众号 AppID 与支付渠道不一致。
POST /public/hardware-sales/orders/lookup
手机号 + 订单号公开查单。必须同时匹配(手机号命中订单 phone 或收货 receiver_phone),错误手机号查不到。
认证:无(公开)。IP + 手机号 + 订单号维度限流。
请求:{ phone: string, orderNo: string }。
响应:{ success: true, data: { orderNo, currency, amountCents, paymentStatus, fulfillmentStatus, orderStatus, stage, stageLabel, createdAt, paidAt, carrier, trackingNo, shippedAt, address } }。物流字段仅发货后暴露;地址部分脱敏(无门牌 / 姓氏打码 / 无手机号)。
常见错误:400 参数非法;404(统一 ORDER_NOT_FOUND_OR_PHONE_MISMATCH,订单不存在与手机号不符返回完全相同错误);429 限流。
GET /hardware-sales/orders
登录用户订单列表(仅本人)。
认证:JWT Bearer Token 或 OAuth orders:checkout。
请求:Query limit / offset / paymentStatus / orderStatus(均可选)。
响应:{ success: true, data: { orders[], pagination: { total, limit, offset } } }。
orders[] 包含订单号、支付 / 履约状态、amountCents、discountCents、创建时间等用户本人可见摘要;不返回内部订单 UUID、优惠规则 ID、优惠码 ID 或 provider raw。
常见错误:401 未认证;500 服务端异常。
GET /hardware-sales/orders/:orderNo
登录用户订单详情(仅 owner,非 owner / 不存在统一 404)。
认证:JWT Bearer Token 或 OAuth orders:checkout。
路径参数
| 参数 | 说明 |
|---|---|
orderNo | 订单号 |
响应:订单 + 明细 + 地址(手机号脱敏)+ 支付状态 + 发货 / 物流 + 权益状态 + 用户可见事件时间线。
常见错误:400 缺少订单号;401 未认证;404 不存在或无权。
POST /hardware-sales/discount-codes/derive
登录用户基于自己的一笔已支付优惠订单生成好友码。好友码仍是普通硬件销售优惠码,可被公开 quote / create 订单流程消费。
认证:JWT Bearer Token 或 OAuth orders:checkout,且必须是来源订单 owner。
请求
{
"sourceOrderNo": "HWS202607010001",
"friendSharePercent": 20,
"tagline": "可选推荐语"
}friendSharePercent 必须为 10/20/.../100。来源订单优惠金额必须是整元;1 元优惠只能设置 friendSharePercent=100。同一来源订单只能生成一个好友码。
响应
{
"success": true,
"data": {
"code": "FRDABCD123",
"status": "active",
"sourceOrderNoMasked": "HWS...0001",
"baseDiscountCents": 10000,
"friendDiscountCents": 2000,
"selfRewardCents": 8000,
"currency": "CNY",
"tagline": "可选推荐语",
"expiresAt": "2026-09-29T00:00:00.000Z"
}
}好友码默认 maxUses=100、perPersonLimit=1、有效期 90 天。后端按创建时 split 固化 metadata,下单时复制到 discount_snapshot。
常见错误:400 参数非法;401 未认证;404 来源订单不存在或无权;409 来源订单未支付、未使用优惠、优惠不是整元、重复派生或朋友优惠不足 1 元。
GET /hardware-sales/discount-codes/mine
查询当前用户创建的好友码列表。
认证:JWT Bearer Token 或 OAuth orders:checkout。
响应:{ success: true, data: { codes[] } }。codes[] 含 code / status / sourceOrderNoMasked / baseDiscountCents / friendDiscountCents / selfRewardCents / maxUses / usedCount / paidCount / expiresAt / createdAt。
GET /hardware-sales/discount-codes/:code/orders
查询某个好友码产生的订单流转状态。仅好友码创建者可访问。
认证:JWT Bearer Token 或 OAuth orders:checkout。
响应:{ success: true, data: { code, orders[] } }。
orders[] 只返回脱敏流转字段:redemptionId / orderNoMasked / maskedBuyerPhone / paymentStatus / fulfillmentStatus / orderStatus / paidAt / deliveredAt / refundStatus / settlementEligibility / eligibleAt / friendDiscountCents / selfRewardCents / createdAt。
隐私约束:不返回完整手机号、姓名、地址、物流单号、内部 UUID、provider raw、discountRuleId、discountCodeId、capacityLock。
PATCH /hardware-sales/orders/:orderNo/address
登录用户修改本人硬件销售订单收货地址。仅允许在备货前修改,即 fulfillmentStatus ∈ {not_paid, awaiting_stock};进入 preparing / shipped / delivered / cancelled 后返回 409 ORDER_STATE_INVALID,需要联系客服发送邮件处理。
认证:JWT Bearer Token 或 OAuth orders:checkout,且必须是订单 owner。
路径参数
| 参数 | 说明 |
|---|---|
orderNo | 订单号 |
请求
{
"receiverName": "张三",
"receiverPhone": "13800001234",
"province": "广东省",
"city": "深圳市",
"district": "南山区",
"addressLine": "科技园路 1 号",
"postalCode": "518000"
}响应:{ success: true, data: { address } }。返回地址中的手机号仍按用户详情口径脱敏。
常见错误:400 地址参数非法;401 未认证;404 不存在或无权;409 ORDER_STATE_INVALID 当前状态不可自助改地址。
POST /hardware-sales/orders/:orderNo/receipt
登录用户将本人硬件销售订单标记为已签收。仅允许发货后操作,即 fulfillmentStatus = shipped;接口会原子推进到 delivered 并写入用户来源的 receipt_confirmed 审计事件。
认证:JWT Bearer Token 或 OAuth orders:checkout,且必须是订单 owner。
路径参数
| 参数 | 说明 |
|---|---|
orderNo | 订单号 |
请求:空 body。
curl -X POST "https://block2-api.wainao.chat/hardware-sales/orders/HWS202606250001/receipt" \
-H "Authorization: Bearer <access_token>"响应:{ success: true, data: { order } },其中 order.fulfillmentStatus='delivered',order.deliveredAt 为签收时间。
常见错误:401 未认证;404 不存在或无权;409 ORDER_STATE_INVALID 尚未发货或当前状态不可签收;409 RECEIPT_ALREADY_MARKED 已签收,不能重复标记。
POST /webhook/hardware-sales
微信 Native 支付成功 / 失败异步回调(硬件销售专用入口,out_trade_no 前缀 HWSP,绝不进 PaymentGateway)。
认证:微信 v3 验签(wechatpay-* header)。
请求:微信回调原始 JSON 报文。
响应:处理成功回 { code: 'SUCCESS' }(含已幂等 / 订单未找到);验签 / 校验失败回 { code: 'FAIL' } 让微信重试。严格校验金额(无容差)+ 币种 CNY;幂等推进 paid 并触发权益生成。
POST /webhook/hardware-sales-refund
微信退款异步回调(按 out_refund_no 的 HWSR 前缀路由,独立退款 webhook,不进 PaymentGateway)。
认证:微信 v3 验签。
请求:微信退款回调原始 JSON 报文。
响应:成功回 { code: 'SUCCESS' };失败回 { code: 'FAIL' }。幂等推进退款状态并回收对应权益。
相关文档
- 功能说明:
docs/features/hardware/10-sales-order.md - 后台运营 API:
apps/docs-site/docs/api/admin-hardware-sales.md