From 3379d5a1b2559ce9a2320fd3c0ed9d1d298b515e Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Mon, 19 Oct 2026 14:00:06 +0800 Subject: [PATCH 1/2] Align avatar recharge plan IDs with WeChat virtual products --- digital-avatar-app/backend/main.py | 26 +++++++++++----- digital-avatar-app/backend/routers/tokens.py | 11 +++++-- .../services/wechat_virtual_payment.py | 4 +-- .../backend/tests/test_token_billing.py | 31 +++++++++++++++++++ .../tests/test_virtual_payment_flow.py | 14 ++++----- .../tests/test_wechat_virtual_payment.py | 8 ++--- .../docs/H5_PRODUCTION_DEPLOYMENT.md | 9 ++---- .../docs/HUIHUI_VIRTUAL_CALLBACK.md | 2 +- 8 files changed, 73 insertions(+), 32 deletions(-) diff --git a/digital-avatar-app/backend/main.py b/digital-avatar-app/backend/main.py index 3f1211f..e70649c 100644 --- a/digital-avatar-app/backend/main.py +++ b/digital-avatar-app/backend/main.py @@ -9,7 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.interval import IntervalTrigger 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 import routers.avatars import routers.tokens @@ -90,18 +90,28 @@ def seed(): db = SessionLocal() try: plan_specs = [ - {"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": "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": "Basic", "legacy_id": "1", "name": "基础套餐", "amount": 2_000_000, "price": 10, "badge": "", "desc": "2M 积分"}, + {"id": "Standard", "legacy_id": "2", "name": "标准套餐", "amount": 20_000_000, "price": 100, "badge": "常用", "desc": "20M 积分"}, + {"id": "Professional", "legacy_id": "3", "name": "专业套餐", "amount": 250_000_000, "price": 1000, "badge": "加赠25%", "desc": "250M 积分"}, + {"id": "Enterprise", "legacy_id": "4", "name": "企业套餐", "amount": 2_500_000_000, "price": 10000, "badge": "企业推荐", "desc": "2500M 积分"}, ] for spec in plan_specs: 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: - 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: - for key, value in spec.items(): - setattr(plan, key, value) + plan.name = spec["name"] + plan.virtual_product_id = spec["id"] for user in db.query(User).all(): account = db.query(TokenAccount).filter(TokenAccount.user_id == user.id).first() diff --git a/digital-avatar-app/backend/routers/tokens.py b/digital-avatar-app/backend/routers/tokens.py index 4efa01d..2b75db0 100644 --- a/digital-avatar-app/backend/routers/tokens.py +++ b/digital-avatar-app/backend/routers/tokens.py @@ -46,6 +46,13 @@ router = APIRouter(tags=["Token"]) PAYMENT_METHODS = {"wechat": "WECHAT", "alipay": "ALIPAY"} 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"} 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") def charge(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_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: return fail("套餐不存在", 404) @@ -410,7 +417,7 @@ def charge(payload: dict = Body(...), authorization: str = Header(None), db: Ses @router.post("/token/charge/org") def charge_via_huihui_org(payload: dict = Body(...), authorization: str = Header(None), db: Session = Depends(get_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: return fail("套餐不存在", 404) method = str(payload.get("paymentMethod") or "").lower() diff --git a/digital-avatar-app/backend/services/wechat_virtual_payment.py b/digital-avatar-app/backend/services/wechat_virtual_payment.py index b656c70..bd1fa25 100644 --- a/digital-avatar-app/backend/services/wechat_virtual_payment.py +++ b/digital-avatar-app/backend/services/wechat_virtual_payment.py @@ -49,9 +49,7 @@ def _offer_id() -> str: def product_id_for_plan(plan) -> str: - configured = str(getattr(plan, "virtual_product_id", "") or "").strip() - if not configured: - configured = os.getenv(f"WECHAT_VIRTUAL_PRODUCT_{plan.id}", "").strip() + configured = str(getattr(plan, "id", "") or "").strip() if not configured: raise WechatVirtualPaymentError(f"套餐 {plan.id} 尚未配置微信虚拟支付商品 ID") if len(configured) > 64 or not all(ch.isalnum() or ch in "_-" for ch in configured): diff --git a/digital-avatar-app/backend/tests/test_token_billing.py b/digital-avatar-app/backend/tests/test_token_billing.py index 14535a2..480b133 100644 --- a/digital-avatar-app/backend/tests/test_token_billing.py +++ b/digital-avatar-app/backend/tests/test_token_billing.py @@ -57,6 +57,10 @@ def test_seed_synchronizes_requested_recharge_plans(): db = SessionLocal() try: 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] == [ (10, 2_000_000), (100, 20_000_000), @@ -67,6 +71,33 @@ def test_seed_synchronizes_requested_recharge_plans(): 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): context = authorization_context _enable_huihui_payment_login(context) diff --git a/digital-avatar-app/backend/tests/test_virtual_payment_flow.py b/digital-avatar-app/backend/tests/test_virtual_payment_flow.py index 6b0dd1d..cf17f3d 100644 --- a/digital-avatar-app/backend/tests/test_virtual_payment_flow.py +++ b/digital-avatar-app/backend/tests/test_virtual_payment_flow.py @@ -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.wechat_mp_openid = "openid-huihui-callback" user.wechat_mp_session_key = "session-huihui-callback" - plan = db.query(TokenPlan).filter(TokenPlan.id == "1").one() - plan.virtual_product_id = "points_plan_1" + plan = db.query(TokenPlan).filter(TokenPlan.id == "Basic").one() db.commit() finally: db.close() @@ -47,14 +46,14 @@ def test_huihui_virtual_callback_requires_signature_and_exact_order_details(auth created = client.post( "/api/token/charge", headers=context["owner_headers"], - json={"planId": "1", "paymentMethod": "wechat", "payScene": "LITE"}, + json={"planId": "Basic", "paymentMethod": "wechat", "payScene": "LITE"}, ).json()["data"] payload = { "orderNo": created["orderNo"], "paymentNo": f"huihui-virtual-payment-{context['suffix']}", "status": "SUCCESS", "amountCents": 1000, - "productId": "points_plan_1", + "productId": "Basic", "openId": "openid-huihui-callback", "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.wechat_mp_openid = "openid-flow" user.wechat_mp_session_key = "session-flow" - plan = db.query(TokenPlan).filter(TokenPlan.id == "1").one() - plan.virtual_product_id = "points_plan_1" + plan = db.query(TokenPlan).filter(TokenPlan.id == "Basic").one() db.commit() finally: db.close() @@ -108,7 +106,7 @@ def test_virtual_payment_callback_and_refund_are_idempotent(authorization_contex created = client.post( "/api/token/charge", headers=context["owner_headers"], - json={"planId": "1", "paymentMethod": "wechat", "payScene": "LITE"}, + json={"planId": "Basic", "paymentMethod": "wechat", "payScene": "LITE"}, ).json()["data"] assert created["provider"] == "wechat_virtual" params = json.loads(created["payMessage"]) @@ -120,7 +118,7 @@ def test_virtual_payment_callback_and_refund_are_idempotent(authorization_contex "OutTradeNo": created["orderNo"], "OpenId": "openid-flow", "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"}), } query = {"timestamp": "100", "nonce": "nonce", "signature": _signature("callback-token", "100", "nonce")} diff --git a/digital-avatar-app/backend/tests/test_wechat_virtual_payment.py b/digital-avatar-app/backend/tests/test_wechat_virtual_payment.py index e333b6b..ae49c4d 100644 --- a/digital-avatar-app/backend/tests/test_wechat_virtual_payment.py +++ b/digital-avatar-app/backend/tests/test_wechat_virtual_payment.py @@ -9,8 +9,8 @@ from services import wechat_virtual_payment as virtual def test_build_payment_params_signs_the_exact_compact_payload(): - order = SimpleNamespace(order_no="AV202609080001", plan_id="plan-1", price_cents=1000) - plan = SimpleNamespace(id="plan-1", virtual_product_id="points_plan_1") + order = SimpleNamespace(order_no="AV202609080001", plan_id="Basic", price_cents=1000) + plan = SimpleNamespace(id="Basic", virtual_product_id="stale-product-id") env = { "WECHAT_VIRTUAL_ENV": "sandbox", "WECHAT_VIRTUAL_SANDBOX_APP_KEY": "sandbox-key", @@ -25,10 +25,10 @@ def test_build_payment_params_signs_the_exact_compact_payload(): "buyQuantity": 1, "env": 1, "currencyType": "CNY", - "productId": "points_plan_1", + "productId": "Basic", "goodsPrice": 1000, "outTradeNo": "AV202609080001", - "attach": '{"orderNo":"AV202609080001","planId":"plan-1"}', + "attach": '{"orderNo":"AV202609080001","planId":"Basic"}', }, ensure_ascii=False, separators=(",", ":")) assert result["paySig"] == hmac.new( b"sandbox-key", f"requestVirtualPayment&{sign_data}".encode(), hashlib.sha256 diff --git a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md index 2881fcb..ee144b7 100644 --- a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md +++ b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md @@ -58,10 +58,7 @@ WECHAT_VIRTUAL_SANDBOX_APP_KEY=<沙箱AppKey> WECHAT_VIRTUAL_APP_KEY=<正式AppKey> WECHAT_VIRTUAL_OFFER_ID= WECHAT_VIRTUAL_CALLBACK_TOKEN=<回调校验Token> -WECHAT_VIRTUAL_PRODUCT_1=<10元套餐商品ID> -WECHAT_VIRTUAL_PRODUCT_2=<100元套餐商品ID> -WECHAT_VIRTUAL_PRODUCT_3=<1000元套餐商品ID> -WECHAT_VIRTUAL_PRODUCT_4=<10000元套餐商品ID> +# 商品 ID 直接使用套餐 ID:Basic、Standard、Professional、Enterprise AVATAR_DB_DIR=/srv/digital-avatar/data/db 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`。 -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`;即使接口代码部署到生产,在协议确认和联调验收前也不得将其视作已上线的支付渠道。 diff --git a/digital-avatar-app/docs/HUIHUI_VIRTUAL_CALLBACK.md b/digital-avatar-app/docs/HUIHUI_VIRTUAL_CALLBACK.md index 712c9c5..e6530e8 100644 --- a/digital-avatar-app/docs/HUIHUI_VIRTUAL_CALLBACK.md +++ b/digital-avatar-app/docs/HUIHUI_VIRTUAL_CALLBACK.md @@ -14,7 +14,7 @@ "paymentNo": "会会侧唯一支付流水号", "status": "SUCCESS", "amountCents": 1000, - "productId": "points_plan_1", + "productId": "Basic", "openId": "微信小程序用户 OpenID", "env": 1 } -- 2.54.0 From 643abd13ea499688ebacc9dd6ca510bf8fd0a9ab Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Mon, 19 Oct 2026 14:12:43 +0800 Subject: [PATCH 2/2] Pass recharge product ID to Huihui order API --- digital-avatar-app/backend/routers/tokens.py | 1 + .../backend/services/huihui_org_payment.py | 5 ++-- .../backend/tests/test_huihui_payment.py | 29 +++++++++++++++++++ .../backend/tests/test_token_billing.py | 1 + .../docs/H5_PRODUCTION_DEPLOYMENT.md | 2 +- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/digital-avatar-app/backend/routers/tokens.py b/digital-avatar-app/backend/routers/tokens.py index 2b75db0..2fd39a4 100644 --- a/digital-avatar-app/backend/routers/tokens.py +++ b/digital-avatar-app/backend/routers/tokens.py @@ -434,6 +434,7 @@ def charge_via_huihui_org(payload: dict = Body(...), authorization: str = Header token=user.huihui_token, user_id=user.huihui_user_id, name=user.nickname, phone=user.phone, avatar=user.avatar_url, amount=f"{cents / 100:.2f}", + product_id=plan.id, ) except HuihuiPaymentError as exc: return fail(str(exc), 502) diff --git a/digital-avatar-app/backend/services/huihui_org_payment.py b/digital-avatar-app/backend/services/huihui_org_payment.py index c981b6f..315e262 100644 --- a/digital-avatar-app/backend/services/huihui_org_payment.py +++ b/digital-avatar-app/backend/services/huihui_org_payment.py @@ -36,9 +36,10 @@ class HuihuiOrgPaymentClient(HuihuiPaymentClient): return data def create_order(self, *, token: str, user_id: str, name: str, phone: str, - avatar: str, amount: str) -> dict: + avatar: str, amount: str, product_id: str) -> dict: body = {"chargeAmount": float(amount), - "commodityName": "数字分身积分充值", "userId": user_id, + "commodityName": "数字分身积分充值", "productId": product_id, + "userId": user_id, "name": name or "", "phoneNumber": phone or "", "avatar": avatar or ""} return self._request("POST", "/order/tokenRecharge", token=token, user_id=user_id, body=body) diff --git a/digital-avatar-app/backend/tests/test_huihui_payment.py b/digital-avatar-app/backend/tests/test_huihui_payment.py index 98b9f52..eb3734a 100644 --- a/digital-avatar-app/backend/tests/test_huihui_payment.py +++ b/digital-avatar-app/backend/tests/test_huihui_payment.py @@ -1,6 +1,7 @@ import os from unittest.mock import Mock, patch +from services.huihui_org_payment import HuihuiOrgPaymentClient from services.huihui_payment import HuihuiPaymentClient @@ -81,3 +82,31 @@ def test_request_refund_uses_configured_huihui_endpoint_without_exposing_secret( "refundReason": "用户申请", } assert "accessSecret" not in post.call_args.kwargs["params"] + + +def test_org_recharge_order_sends_package_product_id_as_string(): + env = { + "HUIHUI_ORG_BASE_URL": "https://fat-open.example/api/huihuiorg", + "HUIHUI_APP_ID": "app-id", + "HUIHUI_ACCESS_ID": "access-id", + "HUIHUI_ACCESS_SECRET": "access-secret", + } + response = Mock(status_code=200) + response.json.return_value = {"code": 200, "data": {"orderNo": "261016111200090123456"}} + + with patch.dict(os.environ, env), patch( + "services.huihui_org_payment.httpx.request", return_value=response + ) as request: + result = HuihuiOrgPaymentClient().create_order( + token="user-token", user_id="user-id", name="测试用户", + phone="", avatar="", amount="10.00", product_id="Basic", + ) + + assert result["orderNo"] == "261016111200090123456" + assert request.call_args.args == ( + "POST", "https://fat-open.example/api/huihuiorg/order/tokenRecharge" + ) + body = request.call_args.kwargs["json"] + assert body["chargeAmount"] == 10.0 + assert body["productId"] == "Basic" + assert isinstance(body["productId"], str) diff --git a/digital-avatar-app/backend/tests/test_token_billing.py b/digital-avatar-app/backend/tests/test_token_billing.py index 480b133..e8926e2 100644 --- a/digital-avatar-app/backend/tests/test_token_billing.py +++ b/digital-avatar-app/backend/tests/test_token_billing.py @@ -152,6 +152,7 @@ def test_org_recharge_only_credits_after_verified_provider_order(authorization_c assert created["balance"] == DEFAULT_TOKEN_GRANT assert created["checkoutToken"] == f"huihui-payment-{context['suffix']}" assert "pay_type" not in provider.create_order.call_args.kwargs + assert provider.create_order.call_args.kwargs["product_id"] == "Basic" details = {**created_data, "payStatus": 2, "userId": context["owner"].huihui_user_id} diff --git a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md index ee144b7..32a5dec 100644 --- a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md +++ b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md @@ -89,7 +89,7 @@ CHAT_ATTACHMENT_CLEANUP_MINUTES=60 `EMBEDDING_API_URL` 同时支持 OpenAI 兼容基础地址(如上面的 `/v1`)和完整的 `/v1/embeddings` 地址,后端会统一请求 `/embeddings`。发布后必须在后端容器内执行一次最小向量探针,确认返回向量数量和维度,而不能只检查 `/api/health`。 -当前 H5 积分充值调用 `/api/token/charge/org` 创建会会组织充值订单,再用会会返回的订单号打开 `#/payOrder?id=<会会订单号>&isOrgUrl=true&evn=<入口环境>`;测试环境额外透传会会登录 token,生产环境不传。`id` 是收银台查询订单的编号,不能替换为套餐/道具 ID。普通微信 H5 走 JSAPI,App 和其他 H5 沿用会会普通收银台。支付回调确认状态成功且金额与套餐价格完全一致后才增加积分,重复回调不会重复到账。 +当前 H5 积分充值调用 `/api/token/charge/org` 创建会会组织充值订单。数字分身后端请求会会 `/order/tokenRecharge` 时,把所选套餐 ID 作为字符串 `productId` 传入(`Basic`、`Standard`、`Professional`、`Enterprise`),再用会会返回的订单号打开 `#/payOrder?id=<会会订单号>&isOrgUrl=true&evn=<入口环境>`;测试环境额外透传会会登录 token,生产环境不传。`id` 是收银台查询订单的编号,不能替换为套餐/道具 ID。普通微信 H5 走 JSAPI,App 和其他 H5 沿用会会普通收银台。支付回调确认状态成功且金额与套餐价格完全一致后才增加积分,重复回调不会重复到账。 微信小程序虚拟支付的服务端接口为:小程序先通过 `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 均不得下发前端或写日志。 -- 2.54.0