数字分身套餐 ID 与微信虚拟支付道具 ID 对齐 #40
@@ -9,7 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
|
|
||||||
from database import engine, init_db, SessionLocal
|
from database import engine, init_db, SessionLocal
|
||||||
from models import Avatar, Authorization, Organization, TokenAccount, TokenPlan, User
|
from models import Avatar, Authorization, Organization, TokenAccount, TokenPaymentOrder, TokenPlan, User
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
import routers.avatars
|
import routers.avatars
|
||||||
import routers.tokens
|
import routers.tokens
|
||||||
@@ -90,18 +90,28 @@ def seed():
|
|||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
plan_specs = [
|
plan_specs = [
|
||||||
{"id": "1", "name": "基础套餐", "amount": 2_000_000, "price": 10, "badge": "", "desc": "2M 积分"},
|
{"id": "Basic", "legacy_id": "1", "name": "基础套餐", "amount": 2_000_000, "price": 10, "badge": "", "desc": "2M 积分"},
|
||||||
{"id": "2", "name": "标准套餐", "amount": 20_000_000, "price": 100, "badge": "常用", "desc": "20M 积分"},
|
{"id": "Standard", "legacy_id": "2", "name": "标准套餐", "amount": 20_000_000, "price": 100, "badge": "常用", "desc": "20M 积分"},
|
||||||
{"id": "3", "name": "专业套餐", "amount": 250_000_000, "price": 1000, "badge": "加赠25%", "desc": "250M 积分"},
|
{"id": "Professional", "legacy_id": "3", "name": "专业套餐", "amount": 250_000_000, "price": 1000, "badge": "加赠25%", "desc": "250M 积分"},
|
||||||
{"id": "4", "name": "企业套餐", "amount": 2_500_000_000, "price": 10000, "badge": "企业推荐", "desc": "2500M 积分"},
|
{"id": "Enterprise", "legacy_id": "4", "name": "企业套餐", "amount": 2_500_000_000, "price": 10000, "badge": "企业推荐", "desc": "2500M 积分"},
|
||||||
]
|
]
|
||||||
for spec in plan_specs:
|
for spec in plan_specs:
|
||||||
plan = db.query(TokenPlan).filter(TokenPlan.id == spec["id"]).first()
|
plan = db.query(TokenPlan).filter(TokenPlan.id == spec["id"]).first()
|
||||||
|
legacy = db.query(TokenPlan).filter(TokenPlan.id == spec["legacy_id"]).first()
|
||||||
|
if legacy is not None and plan is None:
|
||||||
|
legacy.id = spec["id"]
|
||||||
|
plan = legacy
|
||||||
|
db.flush()
|
||||||
|
if legacy is not None and legacy is not plan:
|
||||||
|
db.delete(legacy)
|
||||||
|
db.query(TokenPaymentOrder).filter(TokenPaymentOrder.plan_id == spec["legacy_id"]).update(
|
||||||
|
{TokenPaymentOrder.plan_id: spec["id"]}, synchronize_session=False
|
||||||
|
)
|
||||||
if plan is None:
|
if plan is None:
|
||||||
db.add(TokenPlan(**spec))
|
db.add(TokenPlan(**{key: value for key, value in spec.items() if key != "legacy_id"}, virtual_product_id=spec["id"]))
|
||||||
else:
|
else:
|
||||||
for key, value in spec.items():
|
plan.name = spec["name"]
|
||||||
setattr(plan, key, value)
|
plan.virtual_product_id = spec["id"]
|
||||||
|
|
||||||
for user in db.query(User).all():
|
for user in db.query(User).all():
|
||||||
account = db.query(TokenAccount).filter(TokenAccount.user_id == user.id).first()
|
account = db.query(TokenAccount).filter(TokenAccount.user_id == user.id).first()
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ router = APIRouter(tags=["Token"])
|
|||||||
|
|
||||||
PAYMENT_METHODS = {"wechat": "WECHAT", "alipay": "ALIPAY"}
|
PAYMENT_METHODS = {"wechat": "WECHAT", "alipay": "ALIPAY"}
|
||||||
PAYMENT_SCENES = {"APP", "H5", "LITE", "JSAPI"}
|
PAYMENT_SCENES = {"APP", "H5", "LITE", "JSAPI"}
|
||||||
|
LEGACY_PLAN_IDS = {"1": "Basic", "2": "Standard", "3": "Professional", "4": "Enterprise"}
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_plan(db: Session, plan_id):
|
||||||
|
return db.query(TokenPlan).filter(TokenPlan.id == LEGACY_PLAN_IDS.get(str(plan_id), plan_id)).first()
|
||||||
|
|
||||||
|
|
||||||
SUCCESS_STATUSES = {"SUCCESS", "SUCCEEDED", "PAID", "COMPLETED", "TRADE_SUCCESS"}
|
SUCCESS_STATUSES = {"SUCCESS", "SUCCEEDED", "PAID", "COMPLETED", "TRADE_SUCCESS"}
|
||||||
FAILED_STATUSES = {"FAIL", "FAILED", "CLOSED", "CANCELLED", "CANCELED", "EXPIRED"}
|
FAILED_STATUSES = {"FAIL", "FAILED", "CLOSED", "CANCELLED", "CANCELED", "EXPIRED"}
|
||||||
|
|
||||||
@@ -307,7 +314,7 @@ def plans(authorization: str = Header(None), db: Session = Depends(get_db)):
|
|||||||
@router.post("/token/charge")
|
@router.post("/token/charge")
|
||||||
def charge(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db)):
|
def charge(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||||
user = _require_user(authorization, db)
|
user = _require_user(authorization, db)
|
||||||
plan = db.query(TokenPlan).filter(TokenPlan.id == payload.get("planId")).first()
|
plan = _selected_plan(db, payload.get("planId"))
|
||||||
if not plan:
|
if not plan:
|
||||||
return fail("套餐不存在", 404)
|
return fail("套餐不存在", 404)
|
||||||
|
|
||||||
@@ -410,7 +417,7 @@ def charge(payload: dict = Body(...), authorization: str = Header(None), db: Ses
|
|||||||
@router.post("/token/charge/org")
|
@router.post("/token/charge/org")
|
||||||
def charge_via_huihui_org(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db)):
|
def charge_via_huihui_org(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_db)):
|
||||||
user = _require_user(authorization, db)
|
user = _require_user(authorization, db)
|
||||||
plan = db.query(TokenPlan).filter(TokenPlan.id == payload.get("planId")).first()
|
plan = _selected_plan(db, payload.get("planId"))
|
||||||
if not plan:
|
if not plan:
|
||||||
return fail("套餐不存在", 404)
|
return fail("套餐不存在", 404)
|
||||||
method = str(payload.get("paymentMethod") or "").lower()
|
method = str(payload.get("paymentMethod") or "").lower()
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ def _offer_id() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def product_id_for_plan(plan) -> str:
|
def product_id_for_plan(plan) -> str:
|
||||||
configured = str(getattr(plan, "virtual_product_id", "") or "").strip()
|
configured = str(getattr(plan, "id", "") or "").strip()
|
||||||
if not configured:
|
|
||||||
configured = os.getenv(f"WECHAT_VIRTUAL_PRODUCT_{plan.id}", "").strip()
|
|
||||||
if not configured:
|
if not configured:
|
||||||
raise WechatVirtualPaymentError(f"套餐 {plan.id} 尚未配置微信虚拟支付商品 ID")
|
raise WechatVirtualPaymentError(f"套餐 {plan.id} 尚未配置微信虚拟支付商品 ID")
|
||||||
if len(configured) > 64 or not all(ch.isalnum() or ch in "_-" for ch in configured):
|
if len(configured) > 64 or not all(ch.isalnum() or ch in "_-" for ch in configured):
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ def test_seed_synchronizes_requested_recharge_plans():
|
|||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
plans = db.query(TokenPlan).order_by(TokenPlan.price.asc()).all()
|
plans = db.query(TokenPlan).order_by(TokenPlan.price.asc()).all()
|
||||||
|
assert {plan.id: plan.virtual_product_id for plan in plans} == {
|
||||||
|
"Basic": "Basic", "Standard": "Standard",
|
||||||
|
"Professional": "Professional", "Enterprise": "Enterprise",
|
||||||
|
}
|
||||||
assert [(plan.price, plan.amount) for plan in plans] == [
|
assert [(plan.price, plan.amount) for plan in plans] == [
|
||||||
(10, 2_000_000),
|
(10, 2_000_000),
|
||||||
(100, 20_000_000),
|
(100, 20_000_000),
|
||||||
@@ -67,6 +71,33 @@ def test_seed_synchronizes_requested_recharge_plans():
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_preserves_admin_price_and_migrates_old_order_plan_id():
|
||||||
|
seed()
|
||||||
|
db = SessionLocal()
|
||||||
|
order_no = f"TEST-MIGRATION-{uuid.uuid4().hex}"
|
||||||
|
try:
|
||||||
|
plan = db.query(TokenPlan).filter_by(id="Basic").one()
|
||||||
|
original_price = plan.price
|
||||||
|
plan.price = 0.01
|
||||||
|
db.add(TokenPlan(id="1", name="基础套餐", amount=2_000_000, price=10))
|
||||||
|
db.add(TokenPaymentOrder(
|
||||||
|
order_no=order_no, user_id="test-user", plan_id="1",
|
||||||
|
payment_method="wechat", pay_type="WECHAT", pay_way="JSAPI",
|
||||||
|
points_amount=2_000_000, price_cents=1000, status="pending",
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
seed()
|
||||||
|
db.expire_all()
|
||||||
|
assert db.query(TokenPlan).filter_by(id="1").first() is None
|
||||||
|
assert db.query(TokenPlan).filter_by(id="Basic").one().price == 0.01
|
||||||
|
assert db.query(TokenPaymentOrder).filter_by(order_no=order_no).one().plan_id == "Basic"
|
||||||
|
finally:
|
||||||
|
db.query(TokenPaymentOrder).filter_by(order_no=order_no).delete()
|
||||||
|
db.query(TokenPlan).filter_by(id="Basic").update({"price": original_price})
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
def test_charge_creates_huihui_order_without_early_points(authorization_context):
|
def test_charge_creates_huihui_order_without_early_points(authorization_context):
|
||||||
context = authorization_context
|
context = authorization_context
|
||||||
_enable_huihui_payment_login(context)
|
_enable_huihui_payment_login(context)
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ def test_huihui_virtual_callback_requires_signature_and_exact_order_details(auth
|
|||||||
user = db.query(User).filter(User.id == context["owner"].id).one()
|
user = db.query(User).filter(User.id == context["owner"].id).one()
|
||||||
user.wechat_mp_openid = "openid-huihui-callback"
|
user.wechat_mp_openid = "openid-huihui-callback"
|
||||||
user.wechat_mp_session_key = "session-huihui-callback"
|
user.wechat_mp_session_key = "session-huihui-callback"
|
||||||
plan = db.query(TokenPlan).filter(TokenPlan.id == "1").one()
|
plan = db.query(TokenPlan).filter(TokenPlan.id == "Basic").one()
|
||||||
plan.virtual_product_id = "points_plan_1"
|
|
||||||
db.commit()
|
db.commit()
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
@@ -47,14 +46,14 @@ def test_huihui_virtual_callback_requires_signature_and_exact_order_details(auth
|
|||||||
created = client.post(
|
created = client.post(
|
||||||
"/api/token/charge",
|
"/api/token/charge",
|
||||||
headers=context["owner_headers"],
|
headers=context["owner_headers"],
|
||||||
json={"planId": "1", "paymentMethod": "wechat", "payScene": "LITE"},
|
json={"planId": "Basic", "paymentMethod": "wechat", "payScene": "LITE"},
|
||||||
).json()["data"]
|
).json()["data"]
|
||||||
payload = {
|
payload = {
|
||||||
"orderNo": created["orderNo"],
|
"orderNo": created["orderNo"],
|
||||||
"paymentNo": f"huihui-virtual-payment-{context['suffix']}",
|
"paymentNo": f"huihui-virtual-payment-{context['suffix']}",
|
||||||
"status": "SUCCESS",
|
"status": "SUCCESS",
|
||||||
"amountCents": 1000,
|
"amountCents": 1000,
|
||||||
"productId": "points_plan_1",
|
"productId": "Basic",
|
||||||
"openId": "openid-huihui-callback",
|
"openId": "openid-huihui-callback",
|
||||||
"env": 1,
|
"env": 1,
|
||||||
}
|
}
|
||||||
@@ -91,8 +90,7 @@ def test_virtual_payment_callback_and_refund_are_idempotent(authorization_contex
|
|||||||
user = db.query(User).filter(User.id == context["owner"].id).one()
|
user = db.query(User).filter(User.id == context["owner"].id).one()
|
||||||
user.wechat_mp_openid = "openid-flow"
|
user.wechat_mp_openid = "openid-flow"
|
||||||
user.wechat_mp_session_key = "session-flow"
|
user.wechat_mp_session_key = "session-flow"
|
||||||
plan = db.query(TokenPlan).filter(TokenPlan.id == "1").one()
|
plan = db.query(TokenPlan).filter(TokenPlan.id == "Basic").one()
|
||||||
plan.virtual_product_id = "points_plan_1"
|
|
||||||
db.commit()
|
db.commit()
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
@@ -108,7 +106,7 @@ def test_virtual_payment_callback_and_refund_are_idempotent(authorization_contex
|
|||||||
created = client.post(
|
created = client.post(
|
||||||
"/api/token/charge",
|
"/api/token/charge",
|
||||||
headers=context["owner_headers"],
|
headers=context["owner_headers"],
|
||||||
json={"planId": "1", "paymentMethod": "wechat", "payScene": "LITE"},
|
json={"planId": "Basic", "paymentMethod": "wechat", "payScene": "LITE"},
|
||||||
).json()["data"]
|
).json()["data"]
|
||||||
assert created["provider"] == "wechat_virtual"
|
assert created["provider"] == "wechat_virtual"
|
||||||
params = json.loads(created["payMessage"])
|
params = json.loads(created["payMessage"])
|
||||||
@@ -120,7 +118,7 @@ def test_virtual_payment_callback_and_refund_are_idempotent(authorization_contex
|
|||||||
"OutTradeNo": created["orderNo"],
|
"OutTradeNo": created["orderNo"],
|
||||||
"OpenId": "openid-flow",
|
"OpenId": "openid-flow",
|
||||||
"Env": 1,
|
"Env": 1,
|
||||||
"GoodsInfo": json.dumps({"ProductId": "points_plan_1", "ActualPrice": 1000}),
|
"GoodsInfo": json.dumps({"ProductId": "Basic", "ActualPrice": 1000}),
|
||||||
"WeChatPayInfo": json.dumps({"TransactionId": "wx-transaction-1"}),
|
"WeChatPayInfo": json.dumps({"TransactionId": "wx-transaction-1"}),
|
||||||
}
|
}
|
||||||
query = {"timestamp": "100", "nonce": "nonce", "signature": _signature("callback-token", "100", "nonce")}
|
query = {"timestamp": "100", "nonce": "nonce", "signature": _signature("callback-token", "100", "nonce")}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from services import wechat_virtual_payment as virtual
|
|||||||
|
|
||||||
|
|
||||||
def test_build_payment_params_signs_the_exact_compact_payload():
|
def test_build_payment_params_signs_the_exact_compact_payload():
|
||||||
order = SimpleNamespace(order_no="AV202609080001", plan_id="plan-1", price_cents=1000)
|
order = SimpleNamespace(order_no="AV202609080001", plan_id="Basic", price_cents=1000)
|
||||||
plan = SimpleNamespace(id="plan-1", virtual_product_id="points_plan_1")
|
plan = SimpleNamespace(id="Basic", virtual_product_id="stale-product-id")
|
||||||
env = {
|
env = {
|
||||||
"WECHAT_VIRTUAL_ENV": "sandbox",
|
"WECHAT_VIRTUAL_ENV": "sandbox",
|
||||||
"WECHAT_VIRTUAL_SANDBOX_APP_KEY": "sandbox-key",
|
"WECHAT_VIRTUAL_SANDBOX_APP_KEY": "sandbox-key",
|
||||||
@@ -25,10 +25,10 @@ def test_build_payment_params_signs_the_exact_compact_payload():
|
|||||||
"buyQuantity": 1,
|
"buyQuantity": 1,
|
||||||
"env": 1,
|
"env": 1,
|
||||||
"currencyType": "CNY",
|
"currencyType": "CNY",
|
||||||
"productId": "points_plan_1",
|
"productId": "Basic",
|
||||||
"goodsPrice": 1000,
|
"goodsPrice": 1000,
|
||||||
"outTradeNo": "AV202609080001",
|
"outTradeNo": "AV202609080001",
|
||||||
"attach": '{"orderNo":"AV202609080001","planId":"plan-1"}',
|
"attach": '{"orderNo":"AV202609080001","planId":"Basic"}',
|
||||||
}, ensure_ascii=False, separators=(",", ":"))
|
}, ensure_ascii=False, separators=(",", ":"))
|
||||||
assert result["paySig"] == hmac.new(
|
assert result["paySig"] == hmac.new(
|
||||||
b"sandbox-key", f"requestVirtualPayment&{sign_data}".encode(), hashlib.sha256
|
b"sandbox-key", f"requestVirtualPayment&{sign_data}".encode(), hashlib.sha256
|
||||||
|
|||||||
@@ -58,10 +58,7 @@ WECHAT_VIRTUAL_SANDBOX_APP_KEY=<沙箱AppKey>
|
|||||||
WECHAT_VIRTUAL_APP_KEY=<正式AppKey>
|
WECHAT_VIRTUAL_APP_KEY=<正式AppKey>
|
||||||
WECHAT_VIRTUAL_OFFER_ID=<offer-id>
|
WECHAT_VIRTUAL_OFFER_ID=<offer-id>
|
||||||
WECHAT_VIRTUAL_CALLBACK_TOKEN=<回调校验Token>
|
WECHAT_VIRTUAL_CALLBACK_TOKEN=<回调校验Token>
|
||||||
WECHAT_VIRTUAL_PRODUCT_1=<10元套餐商品ID>
|
# 商品 ID 直接使用套餐 ID:Basic、Standard、Professional、Enterprise
|
||||||
WECHAT_VIRTUAL_PRODUCT_2=<100元套餐商品ID>
|
|
||||||
WECHAT_VIRTUAL_PRODUCT_3=<1000元套餐商品ID>
|
|
||||||
WECHAT_VIRTUAL_PRODUCT_4=<10000元套餐商品ID>
|
|
||||||
|
|
||||||
AVATAR_DB_DIR=/srv/digital-avatar/data/db
|
AVATAR_DB_DIR=/srv/digital-avatar/data/db
|
||||||
AVATAR_UPLOAD_DIR=/srv/digital-avatar/data/uploads
|
AVATAR_UPLOAD_DIR=/srv/digital-avatar/data/uploads
|
||||||
@@ -92,9 +89,9 @@ CHAT_ATTACHMENT_CLEANUP_MINUTES=60
|
|||||||
|
|
||||||
`EMBEDDING_API_URL` 同时支持 OpenAI 兼容基础地址(如上面的 `/v1`)和完整的 `/v1/embeddings` 地址,后端会统一请求 `/embeddings`。发布后必须在后端容器内执行一次最小向量探针,确认返回向量数量和维度,而不能只检查 `/api/health`。
|
`EMBEDDING_API_URL` 同时支持 OpenAI 兼容基础地址(如上面的 `/v1`)和完整的 `/v1/embeddings` 地址,后端会统一请求 `/embeddings`。发布后必须在后端容器内执行一次最小向量探针,确认返回向量数量和维度,而不能只检查 `/api/health`。
|
||||||
|
|
||||||
App 与 H5 积分充值使用会会支付体系的 `payment-v3/payment/pay`,渠道值为 `WECHAT` / `ALIPAY`;App 场景为 `APP`,普通浏览器为 `H5`,微信内 H5 为 `JSAPI`。`HUIHUI_PAYMENT_CALLBACK_SECRET` 只用于为每笔订单生成 HMAC 回调签名,不会发送到前端或直接出现在回调地址中。支付回调确认状态成功且金额与套餐价格完全一致后才增加积分,重复回调不会重复到账。
|
当前 H5 积分充值调用 `/api/token/charge/org` 创建会会组织充值订单,再用会会返回的订单号打开 `#/payOrder?id=<会会订单号>&isOrgUrl=true&evn=<入口环境>`;测试环境额外透传会会登录 token,生产环境不传。`id` 是收银台查询订单的编号,不能替换为套餐/道具 ID。普通微信 H5 走 JSAPI,App 和其他 H5 沿用会会普通收银台。支付回调确认状态成功且金额与套餐价格完全一致后才增加积分,重复回调不会重复到账。
|
||||||
|
|
||||||
微信小程序使用微信虚拟支付:小程序先通过 `POST /api/token/wechat/session` 交换临时登录码,再由 `POST /api/token/charge`(`payScene=LITE`)返回已签名的 `requestVirtualPayment` 参数。微信回调地址配置为 `https://digital.99hui.com/api/token/payment/wechat/virtual/notify`。回调会复核签名、OpenID、环境、商品 ID 与实付金额,退款回调确认后才扣回积分。AppKey、AppSecret、session_key 均不得下发前端或写日志。
|
微信小程序虚拟支付的服务端接口为:小程序先通过 `POST /api/token/wechat/session` 交换临时登录码,再由 `POST /api/token/charge`(`payScene=LITE`)返回已签名的 `requestVirtualPayment` 参数;其中 `productId` 直接采用套餐 ID(`Basic`、`Standard`、`Professional`、`Enterprise`)。小程序原生支付页仍需调用 `wx.requestVirtualPayment`,不能仅打开上述 H5 收银台 URL 来完成虚拟支付。微信回调地址配置为 `https://digital.99hui.com/api/token/payment/wechat/virtual/notify`。回调会复核签名、OpenID、环境、商品 ID 与实付金额,退款回调确认后才扣回积分。AppKey、AppSecret、session_key 均不得下发前端或写日志。
|
||||||
|
|
||||||
如由会会服务端转发微信小程序虚拟支付结果,另有待双方确认的固定回调契约,见 [HUIHUI_VIRTUAL_CALLBACK.md](HUIHUI_VIRTUAL_CALLBACK.md)。该接口使用独立的 `HUIHUI_VIRTUAL_CALLBACK_SECRET`;即使接口代码部署到生产,在协议确认和联调验收前也不得将其视作已上线的支付渠道。
|
如由会会服务端转发微信小程序虚拟支付结果,另有待双方确认的固定回调契约,见 [HUIHUI_VIRTUAL_CALLBACK.md](HUIHUI_VIRTUAL_CALLBACK.md)。该接口使用独立的 `HUIHUI_VIRTUAL_CALLBACK_SECRET`;即使接口代码部署到生产,在协议确认和联调验收前也不得将其视作已上线的支付渠道。
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"paymentNo": "会会侧唯一支付流水号",
|
"paymentNo": "会会侧唯一支付流水号",
|
||||||
"status": "SUCCESS",
|
"status": "SUCCESS",
|
||||||
"amountCents": 1000,
|
"amountCents": 1000,
|
||||||
"productId": "points_plan_1",
|
"productId": "Basic",
|
||||||
"openId": "微信小程序用户 OpenID",
|
"openId": "微信小程序用户 OpenID",
|
||||||
"env": 1
|
"env": 1
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user