Files
cozsweet-frontend-nextjs/FRONTEND_GIFT_PRODUCTS_API.md

15 KiB
Raw Permalink Blame History

虚拟礼物商品与付款后文案 API

一、用途

前端按当前角色一次获取全部启用的礼物品类和商品,在本地完成品类切换与商品筛选。页面不得写死咖啡、鲜花、服装等品类,也不得根据 planId 推算名称、图片或价格。付款成功后,前端使用订单号获取稳定的角色感谢文案。

三、推荐接入流程

  1. 当前角色确定后,只请求一次 GET /api/payment/gift-products?characterId=<CHARACTER_ID>
  2. 使用 data.categories 渲染品类栏,使用 data.plans 保存完整商品目录。
  3. 用户切换品类时只在前端按 product.category 本地筛选,不再请求后端。
  4. 使用商品自身的 planNamedescriptionimageUrlamountCentscurrency 渲染通用商品卡片。
  5. 用户选择商品后,使用该商品原始 planId 创建订单。
  6. 用户切换商品时清除旧 orderIdclientSecret 和支付组件状态,再创建新订单。
  7. Stripe 使用本次订单返回的 payParams.clientSecret 挂载 Payment Element。
  8. 每 3 至 5 秒轮询订单状态,直到状态变成 paidfailedexpired
  9. 状态为 paid 后调用 POST /api/payment/tip-message,展示 data.message

amountCentscurrencyplanId 必须来自同一条商品数据。后端下单时会再次按 planId 校验真实价格,前端展示值不得作为收费依据。


四、一次获取完整礼物目录

4.1 请求

GET /api/payment/gift-products?characterId=elio
  • 完整地址:https://proapi.banlv-ai.com/api/payment/gift-products
  • 兼容别名:GET /api/payment/tip-plans
  • 登录鉴权:不需要
  • 请求格式:Query String

4.2 查询参数

字段 类型 必填 可空 示例 说明
characterId string 前端必传 elio 当前收礼角色 ID。后端仍兼容省略,但多角色前端不得省略。
category string coffee 兼容旧的按品类懒加载方式。新版前端首屏不传此字段。

4.3 请求示例

curl 'https://proapi.banlv-ai.com/api/payment/gift-products?characterId=elio'

4.4 成功响应

{
  "code": 200,
  "message": "success",
  "success": true,
  "data": {
    "characterId": "elio",
    "categories": [
      {
        "category": "coffee",
        "name": "Coffee",
        "productCount": 3,
        "imageUrl": null
      },
      {
        "category": "flowers",
        "name": "Flowers",
        "productCount": 2,
        "imageUrl": "https://example.com/flowers.jpg"
      }
    ],
    "plans": [
      {
        "planId": "tip_coffee_usd_4_99",
        "planName": "Velvet Espresso",
        "orderType": "tip",
        "tipType": "coffee_small",
        "category": "coffee",
        "characterId": "elio",
        "description": "Buy Elio a small coffee",
        "imageUrl": null,
        "amountCents": 499,
        "currency": "USD",
        "autoRenew": false,
        "isFirstRechargeOffer": false,
        "firstRechargeDiscountPercent": 0,
        "promotionType": null
      }
    ]
  }
}

4.5 响应字段

字段 类型 可空 说明
data.characterId string 本次查询的角色;省略请求参数时为 null
data.categories array 当前结果中的品类,顺序与 Manager 商品排序一致。
categories[].category string 稳定品类值,只用于关联和筛选。
categories[].name string 品类展示名称。
categories[].productCount number(整数) 当前品类中的启用商品数量。
categories[].imageUrl string 该品类第一张可用商品图;没有图片时为 null
data.plans array 当前角色全部启用商品;没有商品时为空数组。
plans[].planId string 商品唯一标识,创建订单时必须原样传回。
plans[].planName string 商品展示名称。
plans[].orderType string enum 礼物固定为 tip
plans[].tipType string 支付成功事件使用的礼物细分类型。
plans[].category string 所属品类,与 categories[].category 对应。
plans[].characterId string 收礼角色 ID。
plans[].description string 商品说明,可能为空字符串。
plans[].imageUrl string 完整公开图片 URL;没有图片时为 null
plans[].amountCents number(整数) 展示金额的百分之一;USD 下 499 表示 $4.99
plans[].currency string 三位大写币种代码,例如 USD
plans[].autoRenew boolean 礼物固定为 false
plans[].isFirstRechargeOffer boolean 礼物固定为 false
plans[].firstRechargeDiscountPercent number(整数) 礼物固定为 0
plans[].promotionType string 礼物没有促销时为 null

五、前端通用品类与商品显示逻辑

5.1 建议类型

interface GiftCategory {
  category: string;
  name: string;
  productCount: number;
  imageUrl: string | null;
}

interface GiftProduct {
  planId: string;
  planName: string;
  orderType: "tip";
  tipType: string;
  category: string;
  characterId: string;
  description: string;
  imageUrl: string | null;
  amountCents: number;
  currency: string;
  autoRenew: false;
}

interface GiftCatalog {
  characterId: string | null;
  categories: GiftCategory[];
  plans: GiftProduct[];
}

5.2 状态和筛选

const [catalog, setCatalog] = useState<GiftCatalog | null>(null);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);

const visibleProducts = (catalog?.plans ?? []).filter(
  product => product.category === selectedCategory,
);
  • 角色变化时重新请求,并以 characterId 作为缓存键,不能复用另一个角色的目录。
  • 请求完成后默认选择 categories[0].category;深链指定的品类存在时优先选择深链值。
  • 切换品类只更新 selectedCategory,不请求第二个接口。
  • 只有一个品类时可以隐藏品类切换栏。
  • 新增未知品类时仍使用同一套通用商品卡片,禁止编写 if (category === "coffee") 一类业务分支。
  • 商品图片优先级:product.imageUrl → 当前 category.imageUrl → 通用礼物占位图。
  • 商品名称和说明使用 planNamedescription;不要使用前端本地咖啡常量覆盖。
  • 价格使用 Intl.NumberFormat,金额为 amountCents / 100
const displayPrice = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: product.currency,
}).format(product.amountCents / 100);

5.3 页面状态

状态 前端行为
加载中 显示稳定尺寸的商品骨架,不显示旧角色缓存。
categories=[]plans=[] 显示“该角色暂时没有可用礼物”,不要回退到写死咖啡。
某品类没有商品 自动切换到第一个有商品的品类;全部为空时显示空状态。
imageUrl=null 使用品类图或通用占位图。
请求失败 显示重试操作,不使用可能过期的价格创建订单。
角色切换 取消旧请求,清空品类、商品、选中商品、订单和 Stripe 状态。

5.4 兼容接口

  • GET /api/payment/gift-categories 保留给只需要品类的旧客户端,新版前端主流程不调用。
  • GET /api/payment/gift-products?characterId=elio&category=coffee 仍支持按品类过滤,适用于未来商品数很大时懒加载。
  • GET /api/payment/tip-plans/gift-products 返回同一协议,旧前端继续读取 data.plans 不受影响。
  • 新增的 data.characterIddata.categories 是向后兼容字段。

六、创建礼物支付订单

6.1 请求

POST /api/payment/create-order
  • 完整地址:https://proapi.banlv-ai.com/api/payment/create-order
  • 登录鉴权:需要
  • HeaderAuthorization: Bearer <USER_TOKEN>
  • Content-Typeapplication/json

6.2 请求字段

字段 类型 必填 可空 示例 说明
planId string tip_coffee_usd_4_99 必须使用本次商品接口返回的原始值。
payChannel enum string stripe 可选值:stripeezpay
autoRenew boolean false 礼物是一次性付款,固定发送 false
recipientCharacterId string elio 可省略;省略时使用商品所属角色。传入其他角色会被拒绝。

6.3 请求示例

curl -X POST 'https://proapi.banlv-ai.com/api/payment/create-order' \
  -H 'Authorization: Bearer <USER_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{
    "planId": "tip_coffee_usd_4_99",
    "payChannel": "stripe",
    "autoRenew": false,
    "recipientCharacterId": "elio"
  }'

6.4 Stripe 成功响应

{
  "code": 200,
  "message": "success",
  "success": true,
  "data": {
    "orderId": "pay_xxx",
    "payParams": {
      "clientSecret": "<STRIPE_CLIENT_SECRET>",
      "provider": "stripe",
      "automaticRenewal": false,
      "firstChargeAmountCents": 499,
      "renewalAmountCents": null
    },
    "expiresAt": "2026-07-20T10:30:00+00:00",
    "expiresInSeconds": 1800
  }
}

6.5 Stripe 前端处理要求

  • 使用 data.payParams.clientSecret 挂载 Stripe Payment Element。
  • firstChargeAmountCents 应与所选商品的 amountCents 一致,可用于提交支付前的防错校验。
  • 用户切换商品或重新创建订单后,必须销毁旧 Payment Element,并使用新的 orderId + clientSecret 重新挂载。
  • 不要复用上一件商品的 clientSecret,否则页面商品名称、显示价格和实际 PaymentIntent 可能不属于同一订单。
  • 礼物不会增加积分或 VIP,不参加首充折扣,也不会自动续费。

EzPay 响应可能在 payParams 中返回 cashierUrlpayData,前端按对应支付渠道处理。


七、轮询订单状态

7.1 请求

GET /api/payment/order-status?order_id=<ORDER_ID>
  • 完整地址:https://proapi.banlv-ai.com/api/payment/order-status
  • 登录鉴权:需要,必须与创建订单的用户一致
  • HeaderAuthorization: Bearer <USER_TOKEN>
  • 查询字段兼容性说明:当前字段名是 order_id,不是 orderId

7.2 请求示例

curl 'https://proapi.banlv-ai.com/api/payment/order-status?order_id=<ORDER_ID>' \
  -H 'Authorization: Bearer <USER_TOKEN>'

7.3 关键响应字段

{
  "code": 200,
  "message": "success",
  "success": true,
  "data": {
    "orderId": "pay_xxx",
    "status": "paid",
    "orderType": "tip",
    "planId": "tip_coffee_usd_4_99",
    "creditsAdded": 0
  }
}
字段 类型 可空 说明
orderId string 当前订单号。
status enum string pendingpaidfailedexpired
orderType string 礼物订单为 tip
planId string 当前订单对应的商品 planId
creditsAdded number(整数) 礼物订单固定为 0

建议每 3 至 5 秒轮询一次;出现 paidfailedexpired 后立即停止。


八、获取付款后感谢文案

8.1 请求

POST /api/payment/tip-message

仅在订单状态已经变为 paid 后调用。

  • 完整地址:https://proapi.banlv-ai.com/api/payment/tip-message
  • 登录鉴权:需要,必须是订单所属用户
  • HeaderAuthorization: Bearer <USER_TOKEN>
  • Content-Typeapplication/json

8.2 请求字段

字段 类型 必填 可空 示例 说明
orderId string pay_xxx 当前用户已经支付成功的礼物订单号。

8.3 请求示例

curl -X POST 'https://proapi.banlv-ai.com/api/payment/tip-message' \
  -H 'Authorization: Bearer <USER_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"orderId":"<PAID_ORDER_ID>"}'

8.4 成功响应

{
  "code": 200,
  "message": "success",
  "success": true,
  "data": {
    "orderId": "pay_xxx",
    "characterId": "elio",
    "planId": "tip_coffee_usd_4_99",
    "productName": "Velvet Espresso",
    "tipCount": 1,
    "poolIndex": 37,
    "message": "This is the 1st time you've sent me \"Velvet Espresso\". You have a knack for making me smile."
  }
}

8.5 响应字段

字段 类型 可空 说明
orderId string 已支付订单号。
characterId string 收礼角色 ID。
planId string 本次实际支付商品的 planId
productName string 本次实际支付商品名称。
tipCount number(整数) 当前用户对同一 planId 的成功支付次数。
poolIndex number(整数) 本次选中的预生成文案下标,范围为 099
message string 可直接展示的完整英文文案。

每个角色在 Manager 中预先保存正好 100 条英文感谢语。后端使用 characterId + orderId 稳定选择一条,因此同一个 orderId 重复请求时,poolIndexmessage 保持一致。该接口不会在请求时调用 AI。


十、前端验收步骤

  1. 在预发请求 Elio 的品类,确认页面根据接口返回结果渲染,没有写死咖啡品类。
  2. 请求选中品类的商品,确认商品名称、planId、图片、amountCents 和币种全部来自接口。
  3. 分别选择 $4.99$9.99 商品,确认创建订单请求发送的是对应商品自己的 planId
  4. 创建订单后校验 payParams.firstChargeAmountCents === product.amountCents;不一致时阻止支付并重新创建订单。
  5. 切换商品后确认旧 Stripe Payment Element 已销毁,新的组件使用新 clientSecret
  6. 使用预发测试账号完成一笔允许的支付,轮询状态直到 paid
  7. 连续两次调用付款后文案接口,确认同一订单返回内容完全一致。
  8. 再次购买同一商品,确认 tipCount 增加 1。
  9. 确认礼物付款后没有增加积分、VIP 或自动续费。

十一、注意事项

  • 前端不要缓存并跨商品复用 orderIdclientSecretpayParams
  • planId 是商品身份,amountCents 是商品价格;二者必须来自同一次商品接口响应。
  • 品类或商品为空时展示空状态,不要回退到写死的旧咖啡商品。
  • imageUrl=null 是合法结果,应显示本地占位图。
  • 本接口没有新增数据库迁移,也没有改变现有公开字段命名。