From b1bf2030182566242d40559375f06c90a7dfcde9 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 16 Oct 2026 14:56:00 +0800 Subject: [PATCH 1/8] feat(avatar): route token recharge through Huihui org orders --- digital-avatar-app/backend/models.py | 1 + digital-avatar-app/backend/routers/tokens.py | 66 ++++++++++++++++ .../backend/services/huihui_org_payment.py | 48 ++++++++++++ .../backend/tests/test_token_billing.py | 31 ++++++++ .../docs/H5_PRODUCTION_DEPLOYMENT.md | 2 + digital-avatar-app/src/api/index.ts | 9 ++- digital-avatar-app/src/main.ts | 1 + digital-avatar-app/src/utils/uniapp-bridge.ts | 5 +- digital-avatar-app/src/views/TokenCharge.vue | 75 +++---------------- 9 files changed, 171 insertions(+), 67 deletions(-) create mode 100644 digital-avatar-app/backend/services/huihui_org_payment.py diff --git a/digital-avatar-app/backend/models.py b/digital-avatar-app/backend/models.py index 2b5a0b7..dbf539d 100644 --- a/digital-avatar-app/backend/models.py +++ b/digital-avatar-app/backend/models.py @@ -395,6 +395,7 @@ class TokenPaymentOrder(Base): "price": self.price_cents / 100, "status": self.status, "provider": self.provider, + "providerOrderNo": self.provider_order_no, "providerStatus": self.provider_status, "payMessage": self.pay_message, "failureReason": self.failure_reason, diff --git a/digital-avatar-app/backend/routers/tokens.py b/digital-avatar-app/backend/routers/tokens.py index ff4fbd4..6255828 100644 --- a/digital-avatar-app/backend/routers/tokens.py +++ b/digital-avatar-app/backend/routers/tokens.py @@ -25,6 +25,7 @@ from models import ( ) from responses import fail, ok from services.huihui_payment import HuihuiPaymentClient, HuihuiPaymentError +from services.huihui_org_payment import HuihuiOrgPaymentClient from services.token_billing import get_or_create_account from services.wechat_virtual_payment import ( PAYMENT_EVENTS as WECHAT_PAYMENT_EVENTS, @@ -270,6 +271,7 @@ def _callback_amount_cents(payload) -> int | None: "orderAmt", "amount", "totalAmount", + "chargeAmount", ) if value in (None, ""): return None @@ -405,6 +407,47 @@ def charge(payload: dict = Body(...), authorization: str = Header(None), db: Ses return ok(_payment_payload(order, get_or_create_account(db, user.id))) +@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() + if not plan: + return fail("套餐不存在", 404) + method = str(payload.get("paymentMethod") or "").lower() + if method not in PAYMENT_METHODS: + return fail("支付方式不正确", 400) + scene = str(payload.get("payScene") or "APP").upper() + if scene not in PAYMENT_SCENES: + return fail("支付场景不正确", 400) + pay_type = 6 if scene == "LITE" else (1 if method == "wechat" else 2) + if scene == "LITE" and method != "wechat": + return fail("微信小程序仅支持微信支付", 400) + cents = _price_cents(plan.price) + try: + result = HuihuiOrgPaymentClient().create_order( + 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}", pay_type=pay_type, + ) + except HuihuiPaymentError as exc: + return fail(str(exc), 502) + provider_no = str(result.get("orderNo") or "") + if not provider_no.isdigit() or int(result.get("payStatus") or 0) != 1 or int(result.get("chargeType") or 0) != 9: + return fail("会会返回的充值订单不正确", 502) + if _callback_amount_cents({"chargeAmount": result.get("chargeAmount")}) != cents: + return fail("会会返回的充值金额不匹配", 502) + order = TokenPaymentOrder( + order_no=f"AV{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:12].upper()}", + user_id=user.id, plan_id=plan.id, payment_method=method, + pay_type=PAYMENT_METHODS[method], pay_way=scene, points_amount=plan.amount, + price_cents=cents, status="pending", provider="huihui_org", + provider_order_no=provider_no, provider_status="1", + ) + db.add(order) + db.commit() + return ok(_payment_payload(order, get_or_create_account(db, user.id))) + + @router.get("/token/payment/{order_id}") def payment_status(order_id: str, authorization: str = Header(None), db: Session = Depends(get_db)): user = _require_user(authorization, db) @@ -414,6 +457,27 @@ def payment_status(order_id: str, authorization: str = Header(None), db: Session ).first() if not order: return fail("支付订单不存在", 404) + if order.provider == "huihui_org" and order.status == "pending": + try: + details = HuihuiOrgPaymentClient().get_order( + token=user.huihui_token, user_id=user.huihui_user_id, + order_no=order.provider_order_no, + ) + status = int(details.get("payStatus") or 0) + if (str(details.get("orderNo") or "") == order.provider_order_no + and str(details.get("userId") or "") == user.huihui_user_id + and int(details.get("chargeType") or 0) == 9 + and _callback_amount_cents({"chargeAmount": details.get("chargeAmount")}) == order.price_cents): + order.provider_status = str(status) + if status in (2, 3, 4): + _settle_paid_order(db, order, provider_status=f"ORG_{status}", + transaction_no=order.provider_order_no) + elif status == 5: + order.status = "closed" + db.commit() + db.refresh(order) + except (HuihuiPaymentError, TypeError, ValueError): + pass # 查询失败时保持待支付,不将未经核对的结果入账。 if order.provider == "wechat_virtual" and order.status == "pending" and user.wechat_mp_openid: try: provider_data = query_wechat_virtual_order( @@ -560,6 +624,8 @@ def admin_request_refund( return fail("订单不存在", 404) if order.status != "paid" or order.refund_status not in {"", "none", "failed"}: return fail("该订单当前不可退款", 409) + if order.provider == "huihui_org": + return fail("会会组织订单的原路退款接口尚未接入,请先在会会后台处理", 409) account = get_or_create_account(db, order.user_id) if int(account.balance or 0) < int(order.points_amount or 0): return fail("该订单发放的积分已使用,不能执行全额退款", 409) diff --git a/digital-avatar-app/backend/services/huihui_org_payment.py b/digital-avatar-app/backend/services/huihui_org_payment.py new file mode 100644 index 0000000..3296a1c --- /dev/null +++ b/digital-avatar-app/backend/services/huihui_org_payment.py @@ -0,0 +1,48 @@ +"""Huihui organization token-recharge order API.""" +import os + +import httpx + +from services.huihui_payment import HuihuiPaymentClient, HuihuiPaymentError + + +class HuihuiOrgPaymentClient(HuihuiPaymentClient): + def __init__(self): + super().__init__({ + "HUIHUI_PAYMENT_BASE_URL": os.getenv("HUIHUI_ORG_BASE_URL", ""), + "HUIHUI_APP_ID": os.getenv("HUIHUI_APP_ID", ""), + "HUIHUI_ACCESS_ID": os.getenv("HUIHUI_ACCESS_ID", ""), + "HUIHUI_ACCESS_SECRET": os.getenv("HUIHUI_ACCESS_SECRET", ""), + }) + + def _request(self, method: str, path: str, *, token: str, user_id: str, body=None) -> dict: + if not self.configured or not token or not user_id: + raise HuihuiPaymentError("会会订单服务或登录凭证未配置") + try: + response = httpx.request( + method, f"{self.base_url}{path}", + headers={"Authorization": f"Bearer {token}", "appId": self.app_id}, + params=self._signed_params(user_id), json=body, + timeout=self.timeout, follow_redirects=True, + ) + except httpx.HTTPError as exc: + raise HuihuiPaymentError("会会订单服务连接失败") from exc + payload = self._json(response) + if response.status_code >= 400 or payload.get("code") not in (0, 200, "0", "200"): + raise HuihuiPaymentError(str(payload.get("message") or "会会订单请求失败")) + data = payload.get("data") + if not isinstance(data, dict): + raise HuihuiPaymentError("会会订单信息缺失") + return data + + def create_order(self, *, token: str, user_id: str, name: str, phone: str, + avatar: str, amount: str, pay_type: int) -> dict: + body = {"chargeAmount": float(amount), "payType": pay_type, + "commodityName": "数字分身积分充值", "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) + + def get_order(self, *, token: str, user_id: str, order_no: str) -> dict: + if not order_no.isdigit(): + raise HuihuiPaymentError("会会订单号格式不正确") + return self._request("GET", f"/order/get/{order_no}", token=token, user_id=user_id) diff --git a/digital-avatar-app/backend/tests/test_token_billing.py b/digital-avatar-app/backend/tests/test_token_billing.py index d3f1592..ae9ba97 100644 --- a/digital-avatar-app/backend/tests/test_token_billing.py +++ b/digital-avatar-app/backend/tests/test_token_billing.py @@ -100,6 +100,37 @@ def test_charge_creates_huihui_order_without_early_points(authorization_context) assert "test-callback-secret-123456" not in callback_url +def test_org_recharge_only_credits_after_verified_provider_order(authorization_context): + seed() + context = authorization_context + _enable_huihui_payment_login(context) + provider_no = "261016111200090123456" + created_data = {"orderNo": provider_no, "chargeType": 9, + "chargeAmount": 10, "payStatus": 1} + with patch("routers.tokens.HuihuiOrgPaymentClient") as client_factory: + provider = client_factory.return_value + provider.create_order.return_value = created_data + created = client.post( + "/api/token/charge/org", headers=context["owner_headers"], + json={"planId": "1", "paymentMethod": "wechat", "payScene": "APP"}, + ).json()["data"] + assert created["providerOrderNo"] == provider_no + assert created["status"] == "pending" + assert created["balance"] == DEFAULT_TOKEN_GRANT + assert provider.create_order.call_args.kwargs["pay_type"] == 1 + + details = {**created_data, "payStatus": 2, + "userId": context["owner"].huihui_user_id} + provider.get_order.return_value = {**details, "chargeAmount": 9} + assert client.get(f"/api/token/payment/{created['id']}", headers=context["owner_headers"]).json()["data"]["status"] == "pending" + provider.get_order.return_value = details + paid = client.get(f"/api/token/payment/{created['id']}", headers=context["owner_headers"]).json()["data"] + assert paid["status"] == "paid" + assert paid["balance"] == DEFAULT_TOKEN_GRANT + 2_000_000 + duplicate = client.get(f"/api/token/payment/{created['id']}", headers=context["owner_headers"]).json()["data"] + assert duplicate["balance"] == paid["balance"] + + def test_success_callback_credits_once_and_status_is_user_scoped(authorization_context): context = authorization_context _enable_huihui_payment_login(context) diff --git a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md index 7341b7f..2881fcb 100644 --- a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md +++ b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md @@ -1,5 +1,7 @@ # 数字分身 H5 生产接入与部署 +测试环境入口为 `https://fat-digital.99hui.com/`;测试支付收银台使用 `https://fat-open.99hui.com/huihui-h5/`,生产支付收银台使用 `https://99hui.com/huihui-h5/`。两套环境的订单和验签配置必须隔离,密钥只保存在各自后端配置中,不写入前端或仓库。 + ## 1. 接入方式 生产会会在用户已登录后打开以下地址: diff --git a/digital-avatar-app/src/api/index.ts b/digital-avatar-app/src/api/index.ts index a3926a9..74d87cb 100644 --- a/digital-avatar-app/src/api/index.ts +++ b/digital-avatar-app/src/api/index.ts @@ -156,7 +156,8 @@ export interface TokenPaymentOrder { pointsAmount: number price: number status: 'pending' | 'paid' | 'failed' | 'closed' | 'refunded' - provider: 'huihui' | 'wechat_virtual' + provider: 'huihui' | 'wechat_virtual' | 'huihui_org' + providerOrderNo?: string providerStatus: string payMessage: string failureReason: string @@ -196,6 +197,12 @@ export const chargeToken = ( payScene: 'APP' | 'H5' | 'LITE' | 'JSAPI' ) => request.post('/token/charge', { planId, paymentMethod, payScene }) +export const chargeTokenOrg = ( + planId: string, + paymentMethod: 'wechat' | 'alipay', + payScene: 'APP' | 'H5' | 'LITE' | 'JSAPI' +) => request.post('/token/charge/org', { planId, paymentMethod, payScene }) + export const getTokenPaymentStatus = (orderId: string) => request.get(`/token/payment/${orderId}`) diff --git a/digital-avatar-app/src/main.ts b/digital-avatar-app/src/main.ts index c6910e9..e197567 100644 --- a/digital-avatar-app/src/main.ts +++ b/digital-avatar-app/src/main.ts @@ -15,6 +15,7 @@ app.use(pinia) // —— 混合架构:在挂载前注入 uniapp 壳传入的认证与会会资料 —— const params = getLaunchParams() +if (params.evn !== undefined) sessionStorage.setItem('hh_launch_evn', params.evn) const avatarStore = useAvatarStore(pinia) const userStore = useUserStore(pinia) diff --git a/digital-avatar-app/src/utils/uniapp-bridge.ts b/digital-avatar-app/src/utils/uniapp-bridge.ts index 2c17291..7b78f59 100644 --- a/digital-avatar-app/src/utils/uniapp-bridge.ts +++ b/digital-avatar-app/src/utils/uniapp-bridge.ts @@ -12,15 +12,16 @@ export interface UniLaunchParams { nickname?: string avatar?: string ts?: string + evn?: string } -const PARAM_KEYS: (keyof UniLaunchParams)[] = ['token', 'userId', 'nickname', 'avatar', 'ts'] +const PARAM_KEYS: (keyof UniLaunchParams)[] = ['token', 'userId', 'nickname', 'avatar', 'ts', 'evn'] function readParams(search: string, target: UniLaunchParams): void { const sp = new URLSearchParams(search) for (const key of PARAM_KEYS) { const value = sp.get(key) - if (value) target[key] = value + if (value || (key === 'evn' && sp.has(key))) target[key] = value ?? '' } } diff --git a/digital-avatar-app/src/views/TokenCharge.vue b/digital-avatar-app/src/views/TokenCharge.vue index 4a4c33e..da47455 100644 --- a/digital-avatar-app/src/views/TokenCharge.vue +++ b/digital-avatar-app/src/views/TokenCharge.vue @@ -110,7 +110,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import { useRouter } from 'vue-router' import { - chargeToken, + chargeTokenOrg, applyTokenInvoice, getRechargePlans, getTokenBalance, @@ -118,7 +118,7 @@ import { getTokenPaymentStatus, type TokenPaymentOrder } from '@/api' -import { isInUniWebView, onNativeMessage, UniEvents } from '@/utils/uniapp-bridge' +import { isInUniWebView, onNativeMessage } from '@/utils/uniapp-bridge' import wechatPayIcon from '@/assets/wechat-pay.svg' import alipayIcon from '@/assets/alipay.svg' @@ -235,71 +235,12 @@ const checkoutLabel = computed(() => { }) const payScene = () => { + if (paymentMethod.value === 'wechat' && /miniProgram/i.test(navigator.userAgent)) return 'LITE' as const if (isInUniWebView()) return 'APP' as const if (paymentMethod.value === 'wechat' && /MicroMessenger/i.test(navigator.userAgent)) return 'JSAPI' as const return 'H5' as const } -const parsePayMessage = (message: string) => { - const text = (message || '').trim() - if (!text) return null - try { - return JSON.parse(text) - } catch { - return text - } -} - -const findPaymentUrl = (value: any): string => { - if (typeof value === 'string' && /^https?:\/\//i.test(value)) return value - if (!value || typeof value !== 'object') return '' - for (const key of ['url', 'payUrl', 'mwebUrl', 'mweb_url', 'redirectUrl']) { - if (typeof value[key] === 'string' && /^https?:\/\//i.test(value[key])) return value[key] - } - return '' -} - -const invokeWechatJsapi = (params: Record) => new Promise((resolve, reject) => { - const invoke = () => { - const bridge = (window as any).WeixinJSBridge - if (!bridge?.invoke) { - reject(new Error('当前微信环境无法拉起支付')) - return - } - bridge.invoke('getBrandWCPayRequest', params, (result: any) => { - const message = String(result?.err_msg || result?.errMsg || '') - if (/\bok\b/i.test(message)) resolve() - else reject(new Error(/cancel/i.test(message) ? '支付已取消' : '微信支付未完成')) - }) - } - if ((window as any).WeixinJSBridge) invoke() - else document.addEventListener('WeixinJSBridgeReady', invoke, { once: true }) -}) - -const launchPayment = async (order: TokenPaymentOrder) => { - const parsed = parsePayMessage(order.payMessage) - if (order.payWay === 'JSAPI' && order.paymentMethod === 'wechat' && parsed && typeof parsed === 'object') { - await invokeWechatJsapi(parsed) - return - } - if (isInUniWebView() && UniEvents.payment({ - orderId: order.id, - orderNo: order.orderNo, - paymentMethod: order.paymentMethod, - payType: order.payType, - payWay: order.payWay, - payMessage: order.payMessage, - paymentParams: parsed - })) return - - const paymentUrl = findPaymentUrl(parsed) - if (paymentUrl) { - window.location.assign(paymentUrl) - return - } - throw new Error('请在会会 App 内完成支付') -} - const clearPendingOrder = () => { pendingOrderId.value = '' sessionStorage.removeItem('hh_pending_payment_order') @@ -349,12 +290,18 @@ const doCharge = async () => { charging.value = true paymentNotice.value = '' try { - const order = await chargeToken(selectedPlan.value.id, paymentMethod.value, payScene()) + const order = await chargeTokenOrg(selectedPlan.value.id, paymentMethod.value, payScene()) if (order.status === 'failed') throw new Error(order.failureReason || '支付订单创建失败') + if (!order.providerOrderNo || !/^\d+$/.test(order.providerOrderNo)) throw new Error('会会充值订单号缺失') paymentNoticeTone.value = 'pending' paymentNotice.value = '支付订单已创建,正在等待付款结果' startPolling(order.id) - await launchPayment(order) + const checkoutBase = window.location.hostname === 'digital.99hui.com' + ? 'https://99hui.com/huihui-h5/' + : 'https://fat-open.99hui.com/huihui-h5/' + const evn = sessionStorage.getItem('hh_launch_evn') + const evnPart = evn ? `&evn=${encodeURIComponent(evn)}` : '&evn' + window.location.assign(`${checkoutBase}#/payOrder?orderNo=${encodeURIComponent(order.providerOrderNo)}&isOrg=true${evnPart}`) } catch (error: any) { paymentNoticeTone.value = 'error' paymentNotice.value = error?.message || '充值失败,请重试' -- 2.54.0 From 09aebea7d1d1e90477c629b4b3804b6d78b76fbe Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 16 Oct 2026 15:03:03 +0800 Subject: [PATCH 2/8] fix(avatar): clear stale checkout context and closed orders --- digital-avatar-app/src/main.ts | 1 + digital-avatar-app/src/views/TokenCharge.vue | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/digital-avatar-app/src/main.ts b/digital-avatar-app/src/main.ts index e197567..ddd732d 100644 --- a/digital-avatar-app/src/main.ts +++ b/digital-avatar-app/src/main.ts @@ -16,6 +16,7 @@ app.use(pinia) // —— 混合架构:在挂载前注入 uniapp 壳传入的认证与会会资料 —— const params = getLaunchParams() if (params.evn !== undefined) sessionStorage.setItem('hh_launch_evn', params.evn) +else if (params.token) sessionStorage.removeItem('hh_launch_evn') const avatarStore = useAvatarStore(pinia) const userStore = useUserStore(pinia) diff --git a/digital-avatar-app/src/views/TokenCharge.vue b/digital-avatar-app/src/views/TokenCharge.vue index da47455..7eac1b7 100644 --- a/digital-avatar-app/src/views/TokenCharge.vue +++ b/digital-avatar-app/src/views/TokenCharge.vue @@ -260,9 +260,9 @@ const pollPayment = async () => { void loadData() return } - if (order.status === 'failed') { + if (order.status === 'failed' || order.status === 'closed') { paymentNoticeTone.value = 'error' - paymentNotice.value = order.failureReason || '支付未完成,请重新发起' + paymentNotice.value = order.status === 'closed' ? '订单已关闭,请重新发起支付' : (order.failureReason || '支付未完成,请重新发起') clearPendingOrder() return } -- 2.54.0 From c62233a7c2fb458d5cf2cea217376888b98a0a87 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 16 Oct 2026 15:17:21 +0800 Subject: [PATCH 3/8] feat(avatar): show recharge entry in uni-app webview --- digital-avatar-app/src/views/AvatarManage.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/digital-avatar-app/src/views/AvatarManage.vue b/digital-avatar-app/src/views/AvatarManage.vue index 2ec8398..67e7f04 100644 --- a/digital-avatar-app/src/views/AvatarManage.vue +++ b/digital-avatar-app/src/views/AvatarManage.vue @@ -20,7 +20,7 @@ - +
@@ -28,7 +28,7 @@ {{ tokenBalance.toLocaleString() }} 累计使用 {{ tokenConsumed.toLocaleString() }}
- +
@@ -89,14 +89,14 @@ import { useAvatarStore } from '@/store/avatar' import { useUserStore } from '@/store/user' import { createAvatarShareLink } from '@/api' import { isHuihuiEmbeddedMode } from '@/utils/embed-mode' +import { isInUniWebView } from '@/utils/uniapp-bridge' const router = useRouter() const avatarStore = useAvatarStore() const userStore = useUserStore() const isEmbedded = isHuihuiEmbeddedMode() -// 临时产品开关:余额卡片代码保留,后续改为 true 即可恢复展示。 -const SHOW_POINTS_BALANCE_CARD = false +const SHOW_POINTS_BALANCE_CARD = isInUniWebView() // 当前登录会会用户的资料(头像 / 昵称) const me = computed(() => userStore.user) -- 2.54.0 From 63a0c6692c17ff1d753b8a1ef2470c506c8a4eda Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 16 Oct 2026 15:31:45 +0800 Subject: [PATCH 4/8] fix(avatar): show recharge entry for all signed-in users --- digital-avatar-app/src/views/AvatarManage.vue | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/digital-avatar-app/src/views/AvatarManage.vue b/digital-avatar-app/src/views/AvatarManage.vue index 67e7f04..4e005ec 100644 --- a/digital-avatar-app/src/views/AvatarManage.vue +++ b/digital-avatar-app/src/views/AvatarManage.vue @@ -20,8 +20,8 @@ - -
+ +
积分余额 @@ -89,15 +89,12 @@ import { useAvatarStore } from '@/store/avatar' import { useUserStore } from '@/store/user' import { createAvatarShareLink } from '@/api' import { isHuihuiEmbeddedMode } from '@/utils/embed-mode' -import { isInUniWebView } from '@/utils/uniapp-bridge' const router = useRouter() const avatarStore = useAvatarStore() const userStore = useUserStore() const isEmbedded = isHuihuiEmbeddedMode() -const SHOW_POINTS_BALANCE_CARD = isInUniWebView() - // 当前登录会会用户的资料(头像 / 昵称) const me = computed(() => userStore.user) -- 2.54.0 From 643730f756d759538c12fde2ef504b59c8ac2c88 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 16 Oct 2026 16:16:11 +0800 Subject: [PATCH 5/8] fix(avatar): default checkout environment and simplify pay action --- digital-avatar-app/src/views/TokenCharge.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/digital-avatar-app/src/views/TokenCharge.vue b/digital-avatar-app/src/views/TokenCharge.vue index 7eac1b7..3ef8e3e 100644 --- a/digital-avatar-app/src/views/TokenCharge.vue +++ b/digital-avatar-app/src/views/TokenCharge.vue @@ -231,7 +231,7 @@ const checkoutLabel = computed(() => { if (!selectedPlan.value) return '请选择充值套餐' if (charging.value) return '正在创建支付订单…' if (pendingOrderId.value) return '等待支付结果…' - return `立即支付 ¥${selectedPlan.value.price}` + return '去支付' }) const payScene = () => { @@ -299,8 +299,8 @@ const doCharge = async () => { const checkoutBase = window.location.hostname === 'digital.99hui.com' ? 'https://99hui.com/huihui-h5/' : 'https://fat-open.99hui.com/huihui-h5/' - const evn = sessionStorage.getItem('hh_launch_evn') - const evnPart = evn ? `&evn=${encodeURIComponent(evn)}` : '&evn' + const evn = sessionStorage.getItem('hh_launch_evn') || 'wx' + const evnPart = `&evn=${encodeURIComponent(evn)}` window.location.assign(`${checkoutBase}#/payOrder?orderNo=${encodeURIComponent(order.providerOrderNo)}&isOrg=true${evnPart}`) } catch (error: any) { paymentNoticeTone.value = 'error' -- 2.54.0 From da5822ecfe0abc9f83aa99375017f1782015908a Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 16 Oct 2026 16:34:22 +0800 Subject: [PATCH 6/8] feat(finance): manage token plans and simplify recharge checkout --- backend/app/api/endpoints/finance.py | 24 ++++++++++++ backend/app/services/finance_service.py | 17 +++++++++ digital-avatar-app/src/views/TokenCharge.vue | 26 +------------ frontend/src/api/index.js | 2 + frontend/src/views/Finance.vue | 39 ++++++++++++++++---- 5 files changed, 76 insertions(+), 32 deletions(-) diff --git a/backend/app/api/endpoints/finance.py b/backend/app/api/endpoints/finance.py index ee6edbb..7ee069f 100644 --- a/backend/app/api/endpoints/finance.py +++ b/backend/app/api/endpoints/finance.py @@ -29,6 +29,30 @@ def summary(): db.close() +@router.get("/plans") +def token_plans(): + db = _session() + try: + return ApiResponse(data=finance_service.list_token_plans(db)) + finally: + db.close() + + +@router.patch("/plans/{plan_id}") +def update_token_plan(plan_id: str, body: dict = Body(...)): + db = _session() + try: + finance_service.update_token_plan( + db, plan_id, amount=int(body.get("amount") or 0), price=float(body.get("price") or 0), + name=str(body.get("name") or ""), badge=str(body.get("badge") or ""), desc=str(body.get("desc") or ""), + ) + return ApiResponse(message="充值套餐已更新") + except (ValueError, FinanceServiceError) as exc: + _raise(exc if isinstance(exc, FinanceServiceError) else FinanceServiceError("积分和价格格式不正确")) + finally: + db.close() + + @router.get("/orders") def orders( page: int = Query(1, ge=1), diff --git a/backend/app/services/finance_service.py b/backend/app/services/finance_service.py index 519629c..479dbc8 100644 --- a/backend/app/services/finance_service.py +++ b/backend/app/services/finance_service.py @@ -28,6 +28,23 @@ def _money(cents): class FinanceService: + @staticmethod + def list_token_plans(db): + rows = db.execute(text('SELECT id,name,amount,price,badge,"desc",virtual_product_id FROM token_plans ORDER BY price ASC')).fetchall() + return [dict(row._mapping) for row in rows] + + @staticmethod + def update_token_plan(db, plan_id: str, *, amount: int, price: float, name: str, badge: str, desc: str): + if amount <= 0 or price < 0 or not name.strip(): + raise FinanceServiceError("套餐名称、正数积分和非负价格不正确") + result = db.execute(text('''UPDATE token_plans SET name=:name,amount=:amount,price=:price,badge=:badge,"desc"=:desc WHERE id=:id'''), { + "id": plan_id, "name": name.strip()[:80], "amount": amount, "price": price, + "badge": badge.strip()[:80], "desc": desc.strip()[:200], + }) + if result.rowcount == 0: + raise FinanceServiceError("充值套餐不存在", 404) + db.commit() + @staticmethod def _tables_ready(db) -> bool: names = { diff --git a/digital-avatar-app/src/views/TokenCharge.vue b/digital-avatar-app/src/views/TokenCharge.vue index 3ef8e3e..1b5fe94 100644 --- a/digital-avatar-app/src/views/TokenCharge.vue +++ b/digital-avatar-app/src/views/TokenCharge.vue @@ -37,29 +37,7 @@
-
-

支付方式

-
-
- - 微信支付 - ✓ -
-
- - 支付宝 - ✓ -
-

{{ paymentNotice }}

@@ -119,8 +97,6 @@ import { type TokenPaymentOrder } from '@/api' import { isInUniWebView, onNativeMessage } from '@/utils/uniapp-bridge' -import wechatPayIcon from '@/assets/wechat-pay.svg' -import alipayIcon from '@/assets/alipay.svg' const router = useRouter() @@ -141,7 +117,7 @@ const plans = ref(null) // 支付方式 -const paymentMethod = ref<'wechat' | 'alipay'>('wechat') +const paymentMethod = 'wechat' const paymentNotice = ref('') const paymentNoticeTone = ref<'pending' | 'success' | 'error'>('pending') const pendingOrderId = ref(sessionStorage.getItem('hh_pending_payment_order') || '') diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index ca8c925..c6d3949 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -96,6 +96,8 @@ export const uploadAvatarPhoto = (id, formData) => request.post(`/avatars/${id}/ // Finance (数字分身积分订单) export const getFinanceSummary = () => request.get('/finance/summary') +export const getFinancePlans = () => request.get('/finance/plans') +export const updateFinancePlan = (id, data) => request.patch(`/finance/plans/${id}`, data) export const getFinanceOrders = (params) => request.get('/finance/orders', { params }) export const updateFinanceOrderStatus = (orderNo, data) => request.patch(`/finance/orders/${orderNo}/status`, data) export const requestFinanceRefund = (orderNo, data) => request.post(`/finance/orders/${orderNo}/refund`, data) diff --git a/frontend/src/views/Finance.vue b/frontend/src/views/Finance.vue index c7b718b..d5bc11a 100644 --- a/frontend/src/views/Finance.vue +++ b/frontend/src/views/Finance.vue @@ -16,6 +16,16 @@ + + + + + + + + + +
@@ -23,13 +33,13 @@ - + 查询
- - + + @@ -87,25 +97,37 @@ + + + + + + + + + + diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index c6d3949..c880554 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -100,9 +100,6 @@ export const getFinancePlans = () => request.get('/finance/plans') export const updateFinancePlan = (id, data) => request.patch(`/finance/plans/${id}`, data) export const getFinanceOrders = (params) => request.get('/finance/orders', { params }) export const updateFinanceOrderStatus = (orderNo, data) => request.patch(`/finance/orders/${orderNo}/status`, data) -export const requestFinanceRefund = (orderNo, data) => request.post(`/finance/orders/${orderNo}/refund`, data) -export const getFinanceRefunds = (params) => request.get('/finance/refunds', { params }) -export const confirmFinanceRefund = (refundNo, data) => request.post(`/finance/refunds/${refundNo}/confirm`, data) export const getFinanceInvoices = (params) => request.get('/finance/invoices', { params }) export const updateFinanceInvoice = (invoiceId, data) => request.patch(`/finance/invoices/${invoiceId}`, data) diff --git a/frontend/src/views/Finance.vue b/frontend/src/views/Finance.vue index d5bc11a..7a91c18 100644 --- a/frontend/src/views/Finance.vue +++ b/frontend/src/views/Finance.vue @@ -11,7 +11,6 @@
实收金额
¥{{ money(summary.paid_revenue) }}
{{ summary.paid_orders }} 笔已支付
待支付订单
{{ summary.pending_orders }}
可关闭或标记失败
-
处理中退款
{{ summary.processing_refunds }}
等待支付渠道确认
待开发票
{{ summary.pending_invoices }}
等待财务处理
@@ -49,33 +48,16 @@
- - - - - - - - - - - - - - - @@ -113,13 +95,13 @@