add wxpay

This commit is contained in:
Felix
2025-12-03 18:02:12 +08:00
parent 9dd6f37c22
commit f029e0a5fe
13 changed files with 581 additions and 40 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ backend/static
backend/log/
backend/.env
backend/alembic/versions/
backend/app/admin/cert/
.venv/
.vscode/
.cloudbase/

View File

@@ -8,13 +8,15 @@ from backend.app.admin.api.v1.dict import router as dict_router
from backend.app.admin.api.v1.audit_log import router as audit_log_router
from backend.app.admin.api.v1.coupon import router as coupon_router
from backend.app.admin.api.v1.points import router as points_router
from backend.app.admin.api.v1.wxpay import router as wxpay_router
from backend.core.conf import settings
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
v1.include_router(wx_router, prefix='/wx', tags=['微信服务'])
v1.include_router(wxpay_router, prefix='/wxpay', tags=['微信支付'])
v1.include_router(file_router, prefix='/file', tags=['文件服务'])
v1.include_router(dict_router, prefix='/dict', tags=['字典服务'])
v1.include_router(audit_log_router, prefix='/audit', tags=['审计日志服务'])
v1.include_router(coupon_router, prefix='/coupon', tags=['兑换券服务'])
v1.include_router(points_router, prefix='/points', tags=['积分服务'])
v1.include_router(points_router, prefix='/points', tags=['积分服务'])

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends, Request
from fastapi_limiter.depends import RateLimiter
from backend.app.admin.schema.wxpay import (
CreateJsapiOrderRequest,
CreateJsapiOrderResponse,
QueryOrderResponse,
CloseOrderResponse,
CreateRefundRequest,
CreateRefundResponse,
QueryRefundResponse,
DownloadBillResponse,
)
from backend.app.admin.service.wxpay_service import wxpay_service
from backend.common.response.response_schema import response_base, ResponseSchemaModel
from backend.common.security.jwt import DependsJwtAuth
router = APIRouter()
@router.post('/order/jsapi', summary='JSAPI/小程序下单', dependencies=[DependsJwtAuth, Depends(RateLimiter(times=10, minutes=1))])
async def create_jsapi_order(
request: Request,
body: CreateJsapiOrderRequest,
) -> ResponseSchemaModel[CreateJsapiOrderResponse]:
result = await wxpay_service.create_jsapi_order(
user_id=request.user.id,
payer_openid=request.user.openid,
amount_cents=body.amount_cents,
description=body.description,
)
data = CreateJsapiOrderResponse(**result)
return response_base.success(data=data)
# @router.get('/order/{out_trade_no}', summary='查询订单', dependencies=[DependsJwtAuth])
# async def query_order(out_trade_no: str) -> ResponseSchemaModel[QueryOrderResponse]:
# result = await wxpay_service.query_order(out_trade_no)
# data = QueryOrderResponse(**result)
# return response_base.success(data=data)
# @router.post('/order/{out_trade_no}/close', summary='关闭订单', dependencies=[DependsJwtAuth])
# async def close_order(out_trade_no: str) -> ResponseSchemaModel[CloseOrderResponse]:
# result = await wxpay_service.close_order(out_trade_no)
# data = CloseOrderResponse(**result)
# return response_base.success(data=data)
@router.post('/refund', summary='申请退款', dependencies=[DependsJwtAuth])
async def create_refund(
request: Request,
body: CreateRefundRequest,
) -> ResponseSchemaModel[CreateRefundResponse]:
result = await wxpay_service.create_refund(
user_id=request.user.id,
out_trade_no=body.out_trade_no,
amount_cents=body.amount_cents,
reason=body.reason,
)
data = CreateRefundResponse(**result)
return response_base.success(data=data)
@router.get('/refund/{out_refund_no}', summary='查询退款', dependencies=[DependsJwtAuth])
async def query_refund(out_refund_no: str) -> ResponseSchemaModel[QueryRefundResponse]:
result = await wxpay_service.query_refund(out_refund_no)
data = QueryRefundResponse(**result)
return response_base.success(data=data)
@router.post('/notify', summary='支付回调通知')
async def pay_notify(request: Request) -> ResponseSchemaModel[dict]:
raw = await request.body()
timestamp = request.headers.get('Wechatpay-Timestamp', '')
nonce = request.headers.get('Wechatpay-Nonce', '')
signature = request.headers.get('Wechatpay-Signature', '')
serial = request.headers.get('Wechatpay-Serial', '')
result = await wxpay_service.handle_notify(raw, timestamp, nonce, signature, serial)
return response_base.success(data=result)
# @router.get('/bill', summary='下载交易账单', dependencies=[DependsJwtAuth])
# async def download_bill(bill_date: str, bill_type: str = 'ALL') -> ResponseSchemaModel[DownloadBillResponse]:
# result = await wxpay_service.download_bill(bill_date, bill_type)
# data = DownloadBillResponse(**result)
# return response_base.success(data=data)

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model.wx_pay import WxOrder
class WxOrderCRUD(CRUDPlus[WxOrder]):
async def add(self, db: AsyncSession, order: WxOrder) -> None:
db.add(order)
async def get_by_out_trade_no(self, db: AsyncSession, out_trade_no: str) -> WxOrder | None:
return await self.select_model_by_column(db, out_trade_no=out_trade_no)
async def set_prepay_id(self, db: AsyncSession, order_id: int, prepay_id: str) -> int:
return await self.update_model(db, order_id, {'prepay_id': prepay_id})
async def update_trade_state(self, db: AsyncSession, order_id: int, trade_state: str) -> int:
return await self.update_model(db, order_id, {'trade_state': trade_state})
async def set_transaction(self, db: AsyncSession, order_id: int, transaction_id: str) -> int:
return await self.update_model(db, order_id, {'transaction_id': transaction_id})
async def close(self, db: AsyncSession, out_trade_no: str) -> int:
stmt = update(self.model).where(self.model.out_trade_no == out_trade_no).values(trade_state='CLOSED')
result = await db.execute(stmt)
return result.rowcount
wx_order_dao: WxOrderCRUD = WxOrderCRUD(WxOrder)

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model.wx_pay import WxPayNotifyLog
class WxPayNotifyCRUD(CRUDPlus[WxPayNotifyLog]):
async def add(self, db: AsyncSession, log: WxPayNotifyLog) -> None:
db.add(log)
wx_pay_notify_dao: WxPayNotifyCRUD = WxPayNotifyCRUD(WxPayNotifyLog)

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model.wx_pay import WxRefund
class WxRefundCRUD(CRUDPlus[WxRefund]):
async def add(self, db: AsyncSession, refund: WxRefund) -> None:
db.add(refund)
async def get_by_out_refund_no(self, db: AsyncSession, out_refund_no: str) -> WxRefund | None:
return await self.select_model_by_column(db, out_refund_no=out_refund_no)
async def set_refund_id(self, db: AsyncSession, refund_id_pk: int, refund_id: str) -> int:
return await self.update_model(db, refund_id_pk, {'refund_id': refund_id})
async def update_status(self, db: AsyncSession, refund_id_pk: int, status: str, raw: dict | None = None) -> int:
payload = {'status': status}
if raw is not None:
payload['raw'] = raw
return await self.update_model(db, refund_id_pk, payload)
wx_refund_dao: WxRefundCRUD = WxRefundCRUD(WxRefund)

View File

@@ -6,4 +6,5 @@ from backend.app.admin.model.audit_log import AuditLog, DailySummary
from backend.app.admin.model.file import File
from backend.app.admin.model.coupon import Coupon, CouponUsage
from backend.app.admin.model.points import Points, PointsLog
from backend.app.ai.model import Image
from backend.app.ai.model import Image
from backend.app.admin.model.wx_pay import WxOrder, WxRefund, WxPayNotifyLog

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Optional
from sqlalchemy import String, BigInteger, Integer, Boolean, DateTime, Text, Index
from sqlalchemy.dialects.mysql import JSON as MySQLJSON
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.model import snowflake_id_key, Base
class WxOrder(Base):
__tablename__ = 'wx_order'
id: Mapped[snowflake_id_key] = mapped_column(BigInteger, init=False, primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True, nullable=False, comment='用户ID')
out_trade_no: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, comment='商户订单号')
description: Mapped[str] = mapped_column(String(128), nullable=False, comment='订单描述')
amount_cents: Mapped[int] = mapped_column(Integer, nullable=False, comment='订单金额(分)')
payer_openid: Mapped[Optional[str]] = mapped_column(String(64), default=None, index=True, comment='支付者openid')
prepay_id: Mapped[Optional[str]] = mapped_column(String(64), default=None, index=True, comment='预支付ID')
transaction_id: Mapped[Optional[str]] = mapped_column(String(64), default=None, index=True, comment='微信支付交易单号')
trade_state: Mapped[str] = mapped_column(String(16), default='NOTPAY', index=True, comment='订单状态')
__table_args__ = (
Index('idx_wx_order_user', 'user_id', 'trade_state'),
{'comment': '微信支付订单'}
)
class WxRefund(Base):
__tablename__ = 'wx_refund'
id: Mapped[snowflake_id_key] = mapped_column(BigInteger, init=False, primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True, nullable=False, comment='用户ID')
out_trade_no: Mapped[str] = mapped_column(String(64), nullable=False, comment='商户订单号')
out_refund_no: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, comment='商户退款单号')
amount_cents: Mapped[int] = mapped_column(Integer, nullable=False, comment='退款金额(分)')
refund_id: Mapped[Optional[str]] = mapped_column(String(64), default=None, index=True, comment='微信退款单号')
status: Mapped[str] = mapped_column(String(16), default='PROCESSING', index=True, comment='退款状态')
reason: Mapped[Optional[str]] = mapped_column(String(128), default=None, comment='退款原因')
raw: Mapped[Optional[dict]] = mapped_column(MySQLJSON, default=None, comment='原始返回数据')
__table_args__ = (
Index('idx_wx_refund_trade', 'out_trade_no', 'status'),
{'comment': '微信支付退款'}
)
class WxPayNotifyLog(Base):
__tablename__ = 'wx_pay_notify_log'
id: Mapped[snowflake_id_key] = mapped_column(BigInteger, init=False, primary_key=True)
out_trade_no: Mapped[str] = mapped_column(String(64), index=True, nullable=False, comment='商户订单号')
event_type: Mapped[str] = mapped_column(String(32), nullable=False, comment='事件类型')
verified: Mapped[bool] = mapped_column(Boolean, default=False, comment='签名是否验证通过')
raw_text: Mapped[Optional[str]] = mapped_column(Text, default=None, comment='回调原文')
raw_json: Mapped[Optional[dict]] = mapped_column(MySQLJSON, default=None, comment='回调JSON')
__table_args__ = (
Index('idx_wx_notify_trade', 'out_trade_no'),
{'comment': '微信支付回调日志'}
)

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field
from backend.common.schema import SchemaBase
class CreateJsapiOrderRequest(BaseModel):
amount_cents: int = Field(..., description='订单金额(分)')
description: str = Field(..., description='订单描述')
class CreateJsapiOrderResponse(BaseModel):
out_trade_no: str = Field(..., description='商户订单号')
prepay_id: str = Field(..., description='预支付ID')
trade_state: str = Field(..., description='订单状态')
appId: str = Field(..., description='小程序AppID')
timeStamp: str = Field(..., description='时间戳(秒)')
nonceStr: str = Field(..., description='随机串')
package: str = Field(..., description='prepay_id 包')
signType: str = Field('RSA', description='签名类型')
paySign: str = Field(..., description='签名')
class QueryOrderResponse(BaseModel):
out_trade_no: str = Field(..., description='商户订单号')
transaction_id: Optional[str] = Field(None, description='微信支付交易单号')
trade_state: str = Field(..., description='订单状态')
amount_cents: int = Field(..., description='订单金额(分)')
description: str = Field(..., description='订单描述')
class CloseOrderResponse(BaseModel):
out_trade_no: str = Field(..., description='商户订单号')
trade_state: str = Field(..., description='订单状态')
class CreateRefundRequest(BaseModel):
out_trade_no: str = Field(..., description='商户订单号')
amount_cents: int = Field(..., description='退款金额(分)')
reason: Optional[str] = Field(None, description='退款原因')
class CreateRefundResponse(BaseModel):
out_refund_no: str = Field(..., description='商户退款单号')
refund_id: Optional[str] = Field(None, description='微信退款单号')
status: str = Field(..., description='退款状态')
class QueryRefundResponse(CreateRefundResponse):
pass
class DownloadBillResponse(BaseModel):
bill_date: str = Field(..., description='账单日期')
content: str = Field(..., description='账单原文')

View File

@@ -1,37 +1,264 @@
from wechatpy.pay import WeChatPay
from backend.app.admin.model.order import Order
from backend.app.admin.service.usage_service import UsageService
from backend.core.conf import settings
from backend.app.admin.crud.order_crud import order_dao
from backend.database.db import async_db_session
wxpay = WeChatPay(
appid=settings.WECHAT_APP_ID,
api_key=settings.WECHAT_PAY_API_KEY,
mch_id=settings.WECHAT_MCH_ID,
mch_cert=settings.WECHAT_PAY_CERT_PATH,
mch_key=settings.WECHAT_PAY_KEY_PATH
)
class WxPayService:
@staticmethod
async def create_order(user_id: int, amount_cents: int, description: str):
async with async_db_session.begin() as db:
order = Order(
user_id=user_id,
order_type="purchase",
amount_cents=amount_cents,
amount_times=UsageService.calculate_purchase_times(amount_cents),
status="pending"
)
await order_dao.add(db, order)
result = wxpay.order.create(
trade_type="JSAPI",
body=description,
total_fee=amount_cents,
notify_url=settings.WECHAT_NOTIFY_URL,
out_trade_no=str(order.id)
)
return result
from typing import Optional
from datetime import datetime
import json
import logging
import httpx
from backend.app.admin.model.wx_pay import WxOrder, WxRefund, WxPayNotifyLog
from backend.app.admin.crud.wx_order_crud import wx_order_dao
from backend.app.admin.crud.wx_refund_crud import wx_refund_dao
from backend.app.admin.crud.wx_pay_notify_crud import wx_pay_notify_dao
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.utils.wechatpay_v3 import decrypt_resource, _verify_header_signature
from wechatpayv3 import WeChatPay, WeChatPayType
class WxPayService:
@staticmethod
def _build_wxpay_instance(notify_url: str) -> WeChatPay:
def _read(path: str) -> str:
with open(path, 'r') as f:
return f.read()
private_key = _read(settings.WX_PAY_KEY_PATH) if settings.WX_PAY_KEY_PATH else ""
public_key = _read(settings.WX_PAY_PLATFORM_CERT_PATH) if settings.WX_PAY_PLATFORM_CERT_PATH else None
return WeChatPay(
wechatpay_type=WeChatPayType.JSAPI,
mchid=settings.WX_PAY_MCH_ID or settings.WX_MCH_ID,
private_key=private_key,
cert_serial_no=settings.WX_PAY_SERIAL_NO,
apiv3_key=settings.WX_PAY_APIV3_KEY,
appid=settings.WX_SP_APPID or settings.WX_APPID,
notify_url=notify_url,
logger=logging.getLogger('wxpay'),
proxy=None,
timeout=(10, 30),
partner_mode=False,
public_key=public_key,
public_key_id=settings.WX_PAY_PUB_ID or None,
)
@staticmethod
def _parse_result(result: object) -> dict:
if isinstance(result, (str, bytes)):
try:
return json.loads(result)
except Exception:
return {}
if isinstance(result, dict):
msg = result.get('message')
if isinstance(msg, (str, bytes)):
try:
return json.loads(msg)
except Exception:
return {}
return result
return {}
@staticmethod
def _safe_call(func, *args, **kwargs):
try:
code, result = func(*args, **kwargs)
except Exception as e:
logging.error(f"WeChatPay call failed: {e}")
raise RuntimeError(f"wxpay_error: {str(e)}")
if code != 200:
# 尝试解析 message 以便返回更清晰的错误
parsed = WxPayService._parse_result(result)
logging.warning(f"WeChatPay non-200 response: code={code}, parsed={parsed}")
raise RuntimeError(f"wxpay_code_{code}: {parsed or result}")
return result
@staticmethod
async def create_jsapi_order(user_id: int, amount_cents: int, description: str, payer_openid: str) -> dict:
async with async_db_session.begin() as db:
from backend.utils.snowflake import snowflake
generated_id = snowflake.generate()
order = WxOrder(
user_id=user_id,
out_trade_no=str(generated_id),
description=description,
amount_cents=amount_cents,
payer_openid=payer_openid,
trade_state='NOTPAY',
)
order.id = generated_id
await wx_order_dao.add(db, order)
await db.flush()
await db.refresh(order)
# notify_url = f"{settings.SERVER_HOST}:{settings.SERVER_PORT}{settings.FASTAPI_API_V1_PATH}/wxpay/notify"
notify_url = f"https://app.xhzone.cn:{settings.SERVER_PORT}{settings.FASTAPI_API_V1_PATH}/wxpay/notify"
wxpay = WxPayService._build_wxpay_instance(notify_url)
payer = {"openid": payer_openid}
result = WxPayService._safe_call(
wxpay.pay,
description=description,
out_trade_no=str(order.id),
amount={"total": amount_cents, "currency": "CNY"},
pay_type=WeChatPayType.JSAPI,
payer=payer,
)
data = WxPayService._parse_result(result)
prepay_id = data.get('prepay_id') or ''
if prepay_id:
await wx_order_dao.set_prepay_id(db, order.id, prepay_id)
app_id = settings.WX_SP_APPID or settings.WX_APPID
timestamp = str(int(datetime.now().timestamp()))
nonce_str = str(order.id)
package = f"prepay_id={prepay_id}"
sign_type = "RSA"
pay_sign = wxpay.sign([app_id, timestamp, nonce_str, package])
return {
'out_trade_no': str(order.id),
'prepay_id': prepay_id,
'trade_state': order.trade_state,
'appId': app_id,
'timeStamp': timestamp,
'nonceStr': nonce_str,
'package': package,
'signType': sign_type,
'paySign': pay_sign,
}
@staticmethod
async def query_order(out_trade_no: str) -> dict:
async with async_db_session.begin() as db:
order = await wx_order_dao.get_by_out_trade_no(db, out_trade_no)
if not order:
return {}
notify_url = f"{settings.SERVER_HOST}:{settings.SERVER_PORT}{settings.FASTAPI_API_V1_PATH}/wxpay/notify"
wxpay = WxPayService._build_wxpay_instance(notify_url)
result = WxPayService._safe_call(wxpay.query, out_trade_no=out_trade_no)
data = WxPayService._parse_result(result)
trade_state = data.get('trade_state', 'NOTPAY')
transaction_id = data.get('transaction_id')
await wx_order_dao.update_trade_state(db, order.id, trade_state)
if transaction_id:
await wx_order_dao.set_transaction(db, order.id, transaction_id)
return {
'out_trade_no': out_trade_no,
'transaction_id': transaction_id,
'trade_state': trade_state,
'amount_cents': order.amount_cents,
'description': order.description,
}
@staticmethod
async def close_order(out_trade_no: str) -> dict:
async with async_db_session.begin() as db:
notify_url = f"{settings.SERVER_HOST}:{settings.SERVER_PORT}{settings.FASTAPI_API_V1_PATH}/wxpay/notify"
wxpay = WxPayService._build_wxpay_instance(notify_url)
try:
WxPayService._safe_call(wxpay.close, out_trade_no=out_trade_no)
except Exception:
# 关单失败不影响本地标记为 CLOSED
logging.warning(f"WeChatPay close order failed for {out_trade_no}")
await wx_order_dao.close(db, out_trade_no)
return {'out_trade_no': out_trade_no, 'trade_state': 'CLOSED'}
@staticmethod
async def create_refund(user_id: int, out_trade_no: str, amount_cents: int, reason: Optional[str] = None) -> dict:
async with async_db_session.begin() as db:
from backend.utils.snowflake import snowflake
# 使用退款记录主键 id 作为 out_refund_no
generated_id = snowflake.generate()
refund = WxRefund(
user_id=user_id,
out_trade_no=out_trade_no,
out_refund_no=str(generated_id),
amount_cents=amount_cents,
reason=reason,
)
refund.id = generated_id
await wx_refund_dao.add(db, refund)
await db.flush()
await db.refresh(refund)
notify_url = f"{settings.SERVER_HOST}:{settings.SERVER_PORT}{settings.FASTAPI_API_V1_PATH}/wxpay/notify"
wxpay = WxPayService._build_wxpay_instance(notify_url)
result = WxPayService._safe_call(
wxpay.refund,
out_trade_no=out_trade_no,
out_refund_no=str(refund.id),
amount={"refund": amount_cents, "total": amount_cents, "currency": "CNY"},
reason=reason or "",
)
data = WxPayService._parse_result(result)
refund_id = data.get('refund_id')
status = 'REFUND' if refund_id else 'PROCESSING'
await wx_refund_dao.update_status(db, refund.id, status, raw=data or {})
if refund_id:
await wx_refund_dao.set_refund_id(db, refund.id, refund_id)
return {
'out_refund_no': str(refund.id),
'refund_id': refund_id,
'status': status,
}
@staticmethod
async def query_refund(out_refund_no: str) -> dict:
async with async_db_session.begin() as db:
refund = await wx_refund_dao.get_by_out_refund_no(db, out_refund_no)
if not refund:
return {}
return {
'out_refund_no': refund.out_refund_no,
'refund_id': refund.refund_id,
'status': refund.status,
}
@staticmethod
async def handle_notify(raw_body: bytes, timestamp: str | None = None, nonce: str | None = None,
signature: str | None = None, serial: str | None = None) -> dict:
async with async_db_session.begin() as db:
text = raw_body.decode('utf-8') if isinstance(raw_body, (bytes, bytearray)) else str(raw_body)
verified = False
if timestamp and nonce and signature:
verified = _verify_header_signature(timestamp, nonce, text, signature, settings.WX_PAY_PLATFORM_CERT_PATH)
body_json = json.loads(text)
resource = body_json.get('resource') or {}
out_trade_no = ''
event_type = body_json.get('event_type') or 'UNKNOWN'
if verified and resource:
apiv3 = settings.WX_PAY_APIV3_KEY
decrypted = decrypt_resource(resource.get('associated_data'), resource.get('nonce'), resource.get('ciphertext'), apiv3)
out_trade_no = decrypted.get('out_trade_no') or ''
transaction_id = decrypted.get('transaction_id')
log = WxPayNotifyLog(
out_trade_no=out_trade_no,
event_type=event_type,
verified=verified,
raw_text=text,
raw_json=body_json,
)
await wx_pay_notify_dao.add(db, log)
if verified and out_trade_no:
order = await wx_order_dao.get_by_out_trade_no(db, out_trade_no)
if order:
await wx_order_dao.update_trade_state(db, order.id, 'SUCCESS')
if 'transaction_id' in locals() and transaction_id:
await wx_order_dao.set_transaction(db, order.id, transaction_id)
return {'verified': verified}
@staticmethod
async def download_bill(bill_date: str, bill_type: str = 'ALL') -> dict:
notify_url = f"{settings.SERVER_HOST}:{settings.SERVER_PORT}{settings.FASTAPI_API_V1_PATH}/wxpay/notify"
wxpay = WxPayService._build_wxpay_instance(notify_url)
try:
result = WxPayService._safe_call(wxpay.bill, bill_type='tradebill', bill_date=bill_date, tar_type='GZIP')
data = WxPayService._parse_result(result)
url = data.get('download_url')
except Exception as e:
logging.warning(f"WeChatPay bill fetch failed: {e}")
url = None
content = ''
if url:
async with httpx.AsyncClient() as client:
r = await client.get(url)
content = r.text
return {'bill_date': bill_date, 'content': content}
wxpay_service: WxPayService = WxPayService()

View File

@@ -37,8 +37,17 @@ class Settings(BaseSettings):
WX_APPID: str
WX_SECRET: str
WX_MCH_ID: str = ""
WX_PAY_MCH_ID: str = ""
WX_PAY_PUB_ID: str = ""
WX_PAY_CERT_PATH: str = ""
WX_PAY_KEY_PATH: str = ""
WX_PAY_API_KEY: str = ""
WX_SP_APPID: str = ""
WX_SUB_APPID: str = ""
WX_PAY_SERIAL_NO: str = ""
WX_PAY_APIV3_KEY: str = ""
WX_PAY_PLATFORM_SERIAL_NO: str = ""
WX_PAY_PLATFORM_CERT_PATH: str = ""
# model key
QWEN_API_KEY: str
@@ -103,6 +112,7 @@ class Settings(BaseSettings):
f'{FASTAPI_API_V1_PATH}/wx/login',
f'{FASTAPI_API_V1_PATH}/auth/login',
f'{FASTAPI_API_V1_PATH}/auth/logout',
f'{FASTAPI_API_V1_PATH}/wxpay/notify',
]
# JWT
@@ -202,4 +212,4 @@ def get_db_uri(settings: Settings):
# Changed from PostgresDsn.build to manual URL construction for MySQL
return f"mysql+asyncmy://{settings.DATABASE_USER}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOST}:{settings.DATABASE_PORT}/{settings.DATABASE_DB_NAME}"
settings = get_settings()
settings = get_settings()

View File

@@ -6,11 +6,27 @@ import uvicorn
from backend.middleware.cos_client import CosClient
from backend.core.registrar import register_app
from backend.core.conf import settings
from backend.app.admin.service.wxpay_service import WxPayService
from random import sample
from string import ascii_letters, digits
from wechatpayv3 import WeChatPay, WeChatPayType
app = register_app()
@app.get("/")
def read_root():
wxpay = WxPayService._build_wxpay_instance(f'https://app.xhzone.cn:80{settings.FASTAPI_API_V1_PATH}/wxpay/notif')
out_trade_no = ''.join(sample(ascii_letters + digits, 8))
description = 'demo-description'
amount = 100
code, message = wxpay.pay(
description=description,
out_trade_no=out_trade_no,
amount={'total': amount},
payer={"openid": 'onPN_1wptAzuLo16_0aO-iSHuxI0'},
pay_type=WeChatPayType.JSAPI
)
print({'code': code, 'message': message})
return {"Hello": f"World"}

View File

@@ -316,6 +316,7 @@ win32-setctime==1.2.0 ; sys_platform == 'win32'
# via loguru
wsproto==1.2.0
# via simple-websocket
wechatpayv3==2.0.1
zope-event==5.0
# via gevent
zope-interface==7.2