feat(all): 同步最新进度

This commit is contained in:
xingc
2025-04-03 17:21:29 +08:00
parent d6098c8b87
commit 70e0e5b034
24 changed files with 806 additions and 184 deletions

View File

@@ -25,3 +25,6 @@ OVERSEAS_PROXY = http://127.0.0.1:7890
# 微信 # 微信
APPID = wx3d30771f3987cc92 APPID = wx3d30771f3987cc92
APP_SECRET = 8e57b1b5344d1dbd058cd45f3b2c4d3c APP_SECRET = 8e57b1b5344d1dbd058cd45f3b2c4d3c
PAY_MCH_ID =
PAY_API_KEY =
PAY_NOTIFY_URL =

6
associated_script.py Normal file
View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/3/8 下午11:23
# @File : associated_script.py
# @Software : PyCharm
# @Author : xingc
# @Desc : 协同清理脚本

View File

@@ -62,6 +62,9 @@ class Settings(BaseSettings):
# 微信 # 微信
APPID: str = '' APPID: str = ''
APP_SECRET: str = '' APP_SECRET: str = ''
PAY_MCH_ID: str = ''
PAY_API_KEY: str = ''
PAY_NOTIFY_URL: str = ''
model_config = SettingsConfigDict(env_file=f'.env.{ENVIRONMENT}', env_file_encoding='utf-8') model_config = SettingsConfigDict(env_file=f'.env.{ENVIRONMENT}', env_file_encoding='utf-8')
@@ -75,6 +78,8 @@ class ProductionSettings(Settings):
ACCESS_SECRET_KEY: str = '1f790219840ccd10b5d8b33d7f595b7fe0eed262403689f6fa1cab80149b00f2' ACCESS_SECRET_KEY: str = '1f790219840ccd10b5d8b33d7f595b7fe0eed262403689f6fa1cab80149b00f2'
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 1 # 1days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 1 # 1days
model_config = SettingsConfigDict(env_file=f'.env.{ENVIRONMENT}', env_file_encoding='utf-8')
class DevelopmentSettings(Settings): class DevelopmentSettings(Settings):
# 日志配置 # 日志配置

View File

@@ -6,10 +6,16 @@
# @Desc : # @Desc :
import logging import logging
from fastapi import FastAPI, Request, Response import redis.asyncio as aioredis
from fastapi import FastAPI, Request, Response, status
from fastapi.responses import UJSONResponse
from fastapi.exceptions import HTTPException, RequestValidationError
from fastapi_limiter import FastAPILimiter
from config import settings from config import settings
from core.routers import routers from core.db.engine import get_cache
from core.routers.route import routers
from core.routers.chat.handlers import socket_manager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -29,7 +35,7 @@ async def exception_log(request: Request, exc):
def create_app() -> FastAPI: def create_app() -> FastAPI:
app = FastAPI( app = FastAPI(
debug=settings.DEBUG, debug=settings.DEBUG,
openapi_url='/openapi.json' if settings.DEBUG else None, openapi_url='/openapi.json' if settings.DEBUG else None
) )
for router in routers: for router in routers:
@@ -41,15 +47,49 @@ def create_app() -> FastAPI:
@app.on_event('startup') @app.on_event('startup')
async def startup_event(): async def startup_event():
logger.info(f'Application <<{settings.NAME}>> is started') redis_conn = await get_cache()
await FastAPILimiter.init(redis_conn)
logger.info(f'Application ``{settings.NAME}`` is started')
@app.on_event('shutdown') @app.on_event('shutdown')
async def shutdown_event(): async def shutdown_event():
logger.info(f'Application <<{settings.NAME}>> is closed') logger.info(f'Application ``{settings.NAME}`` is closed')
await socket_manager.pubsub_client.disconnect()
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
await exception_log(request, exc)
return UJSONResponse(
status_code=exc.status_code,
content={
'status': exc.status_code,
'data': None,
'message': exc.detail
}
)
@app.exception_handler(RequestValidationError)
async def http_exception_handler(request: Request, exc: RequestValidationError):
await exception_log(request, exc)
return UJSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
'status': status.HTTP_422_UNPROCESSABLE_ENTITY,
'data': exc.errors(),
'message': 'Validation error'
}
)
# @app.exception_handler(Exception) # @app.exception_handler(Exception)
# async def http_exception_handler(request: Request, exc: Exception): # async def http_exception_handler(request: Request, exc: Exception):
# await exception_log(request, exc) # await exception_log(request, exc)
# return base_response.error(status_code=500, message='Server error') # return UJSONResponse(
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
# content={
# 'status': status.HTTP_500_INTERNAL_SERVER_ERROR,
# 'data': None,
# 'message': 'Internal server error'
# }
# )
return app return app

View File

@@ -27,3 +27,8 @@ class SearchCardRetry(ScraperError):
class WechatApiError(CardBookException): class WechatApiError(CardBookException):
"""微信开放服务调用api错误""" """微信开放服务调用api错误"""
pass pass
class WebsocketTooManyRequests(CardBookException):
"""websocket请求太多"""
pass

View File

@@ -47,9 +47,28 @@ class PlatformSourceType(enum.Enum):
class MessageType(enum.Enum): class MessageType(enum.Enum):
TEXT = 'text' TEXT = 'text'
IMAGE = 'image' IMAGE = 'image'
READ = 'read'
class CardCategoryType(enum.Enum): class CardCategoryType(enum.Enum):
SPORTS = 'sports' SPORTS = 'sports'
ANIME = 'anime' ANIME = 'anime'
OTHER = 'other' OTHER = 'other'
class ProductCategory(enum.Enum):
VIP = 'vip'
POINTS = 'points'
class ProductAction(enum.Enum):
VIP_RENEWAL = 'vip_renewal'
RECHARGE_POINTS = 'recharge_points'
class OrderStatusType(enum.Enum):
INIT = 'init'
FAIL = 'fail'
OK = 'ok'
WAIT_PAYMENT = 'wait_payment'
CANCEL = 'cancel'

View File

@@ -21,20 +21,24 @@ class CRUD:
self.session = session self.session = session
self.model = model self.model = model
async def get(self, filters: dict) -> Optional[Model]: async def get(self, filters: dict, model: Model = None) -> Optional[Model]:
model = model or self.model
async with self.session.begin(): async with self.session.begin():
result = await self.session.execute(select(self.model).filter( result = await self.session.execute(select(model).filter(
*self.build_filter_conditions(filters)) *self.build_filter_conditions(filters, model))
) )
record = result.scalar_one_or_none() record = result.scalar_one_or_none()
return record return record
async def get_paginated(self, filters: dict, page: int = 1, page_size: int = 10) -> Tuple[List[Model], int]: async def get_paginated(
self, filters: dict, page: int = 1, page_size: int = 10, model: Model = None
) -> Tuple[List[Model], int]:
model = model or self.model
async with self.session.begin(): async with self.session.begin():
filter_conditions = self.build_filter_conditions(filters) if filters else [] filter_conditions = self.build_filter_conditions(filters) if filters else []
query = select(self.model).filter(*filter_conditions).order_by(desc(self.model.created_at)) query = select(model).filter(*filter_conditions).order_by(desc(model.created_at))
total_query = select(func.count()).select_from(self.model).filter(*filter_conditions) total_query = select(func.count()).select_from(model).filter(*filter_conditions)
total_result = await self.session.execute(total_query) total_result = await self.session.execute(total_query)
total = total_result.scalar() total = total_result.scalar()
@@ -44,9 +48,12 @@ class CRUD:
return records, total return records, total
async def get_grouped_count(self, group_by_field: str, filters: Dict[str, Any] = None) -> Dict[Any, int]: async def get_grouped_count(
self, group_by_field: str, filters: Dict[str, Any] = None, model: Model = None
) -> Dict[Any, int]:
model = model or self.model
filter_conditions = self.build_filter_conditions(filters) if filters else [] filter_conditions = self.build_filter_conditions(filters) if filters else []
group_by = getattr(self.model, group_by_field) group_by = getattr(model, group_by_field)
query = select(group_by, func.count()).filter(*filter_conditions).group_by(group_by) query = select(group_by, func.count()).filter(*filter_conditions).group_by(group_by)
async with self.session.begin(): async with self.session.begin():
@@ -55,32 +62,35 @@ class CRUD:
return {record[0]: record[1] for record in records} return {record[0]: record[1] for record in records}
async def create(self, data: dict, filters: dict) -> Union[Model, bool]: async def create(self, data: dict, filters: dict, model: Model = None) -> Union[Model, bool]:
model = model or self.model
async with self.session.begin(): async with self.session.begin():
result = await self.session.execute(select(self.model).filter( result = await self.session.execute(select(model).filter(
*self.build_filter_conditions(filters)) *self.build_filter_conditions(filters, model))
) )
if result.scalars().first() is not None: if result.scalars().first() is not None:
return False return False
record = self.model(**data) record = model(**data)
self.session.add(record) self.session.add(record)
await self.session.commit() await self.session.commit()
return record return record
async def get_all(self, filters: dict = None) -> List[Model]: async def get_all(self, filters: dict = None, model: Model = None) -> List[Model]:
model = model or self.model
async with self.session.begin(): async with self.session.begin():
result = await self.session.execute( result = await self.session.execute(
select(self.model) if filters is None else select(self.model).filter( select(model) if filters is None else select(model).filter(
*self.build_filter_conditions(filters)).order_by(desc(self.model.insert_time)) *self.build_filter_conditions(filters, model)).order_by(desc(model.insert_time))
) )
records = result.scalars().all() records = result.scalars().all()
return records return records
async def update(self, data: dict, filters: dict) -> Optional[Model]: async def update(self, data: dict, filters: dict, model: Model = None) -> Optional[Model]:
model = model or self.model
async with self.session.begin(): async with self.session.begin():
result = await self.session.execute(select(self.model).filter( result = await self.session.execute(select(model).filter(
*self.build_filter_conditions(filters)) *self.build_filter_conditions(filters, model))
) )
record = result.scalars().first() record = result.scalars().first()
@@ -101,10 +111,11 @@ class CRUD:
await self.session.commit() await self.session.commit()
return record return record
async def updates(self, data: dict, filters: dict) -> Optional[Model]: async def updates(self, data: dict, filters: dict, model: Model = None) -> Optional[Model]:
model = model or self.model
async with self.session.begin(): async with self.session.begin():
result = await self.session.execute(select(self.model).filter( result = await self.session.execute(select(model).filter(
*self.build_filter_conditions(filters)) *self.build_filter_conditions(filters, model))
) )
records = result.scalars().all() records = result.scalars().all()
@@ -125,10 +136,11 @@ class CRUD:
await self.session.commit() await self.session.commit()
return records return records
async def delete(self, filters: dict) -> bool: async def delete(self, filters: dict, model: Model = None) -> bool:
model = model or self.model
async with self.session.begin(): async with self.session.begin():
result = await self.session.execute(select(self.model).filter( result = await self.session.execute(select(model).filter(
*self.build_filter_conditions(filters)) *self.build_filter_conditions(filters, model))
) )
record = result.scalars().first() record = result.scalars().first()
@@ -139,7 +151,7 @@ class CRUD:
await self.session.commit() await self.session.commit()
return True return True
def build_filter_conditions(self, filters: Dict[str, Any]) -> tuple: def build_filter_conditions(self, filters: Dict[str, Any], model: Model) -> tuple:
conditions = [] conditions = []
for key, value in filters.items(): for key, value in filters.items():
@@ -158,7 +170,7 @@ class CRUD:
else: else:
field_name = key field_name = key
field = getattr(self.model, field_name) field = getattr(model, field_name)
if operator == 'like': # 模糊匹配 if operator == 'like': # 模糊匹配
conditions.append(field.like(f'%{value}%')) conditions.append(field.like(f'%{value}%'))

View File

@@ -5,15 +5,15 @@
# @Author : xingc # @Author : xingc
# @Desc : # @Desc :
import enum import enum
import random
import uuid import uuid
from datetime import datetime, date from datetime import datetime, date
from typing import List
import ujson import ujson
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, String, Text, Date, Column, Integer, String, Text, Date,
ForeignKey, DECIMAL, CheckConstraint, ForeignKey, DECIMAL, CheckConstraint,
UniqueConstraint, Index, Sequence, JSON, UUID, Enum, Boolean UniqueConstraint, Index, Sequence, JSON, Enum, Boolean, UUID, Table
) )
from sqlalchemy.orm import ( from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, relationship DeclarativeBase, Mapped, mapped_column, relationship
@@ -57,10 +57,11 @@ class DateModel:
class User(BaseModel, DateModel): class User(BaseModel, DateModel):
__tablename__ = 'system_users' __tablename__ = 'system_users'
__table_args__ = ( __table_args__ = (
Index('wechat_openid', 'wechat_openid'), Index('idx_wechat_openid', 'wechat_openid'),
UniqueConstraint('user_id', name='uniq_user_id'), UniqueConstraint('user_id', name='uniq_user_id'),
{'comment': '用户表'} {'comment': '用户表'}
) )
id = None id = None
user_id: Mapped[int] = mapped_column( user_id: Mapped[int] = mapped_column(
Integer, Sequence('non_primary_id_seq', start=100000, increment=1), primary_key=True, autoincrement=True, Integer, Sequence('non_primary_id_seq', start=100000, increment=1), primary_key=True, autoincrement=True,
@@ -75,22 +76,28 @@ class User(BaseModel, DateModel):
vip_expire_date: Mapped[date] = mapped_column(Date, default=datetime.now().date, comment='会员到期时间') vip_expire_date: Mapped[date] = mapped_column(Date, default=datetime.now().date, comment='会员到期时间')
points: Mapped[int] = mapped_column(Integer, default=0, comment='会员积分') points: Mapped[int] = mapped_column(Integer, default=0, comment='会员积分')
collections: Mapped[list["UserCollection"]] = relationship( collections: Mapped[list['UserCollection']] = relationship(
back_populates="user", back_populates='user',
cascade="all, delete-orphan" cascade='all, delete-orphan'
) )
# 关注者 # 关注者
following: Mapped[list["UserFollows"]] = relationship( following: Mapped[list['UserFollows']] = relationship(
'UserFollows', 'UserFollows',
foreign_keys='UserFollows.follower_id', foreign_keys='UserFollows.follower_id',
cascade="all, delete-orphan" cascade='all, delete-orphan'
) )
# 定义被关注关系 # 定义被关注关系
followers: Mapped[list["UserFollows"]] = relationship( followers: Mapped[list['UserFollows']] = relationship(
'UserFollows', 'UserFollows',
foreign_keys='UserFollows.followed_id', foreign_keys='UserFollows.followed_id',
cascade="all, delete-orphan" cascade='all, delete-orphan'
) )
chats: Mapped[List['Chat']] = relationship(secondary='chat_participant', back_populates='users')
messages: Mapped[List['Message']] = relationship(back_populates='user')
read_statuses: Mapped[List['ReadStatus']] = relationship(back_populates='user')
def __str__(self):
return f'{self.user_id}'
class UserFollows(BaseModel, DateModel): class UserFollows(BaseModel, DateModel):
@@ -100,24 +107,23 @@ class UserFollows(BaseModel, DateModel):
) )
follower_id: Mapped[int] = mapped_column( follower_id: Mapped[int] = mapped_column(
Integer, ForeignKey('system_users.user_id', ondelete="CASCADE"), comment='关注者的用户id' Integer, ForeignKey('system_users.user_id', ondelete='CASCADE'), comment='关注者的用户id'
) )
followed_id: Mapped[int] = mapped_column( followed_id: Mapped[int] = mapped_column(
Integer, ForeignKey('system_users.user_id', ondelete="CASCADE"), comment='被关注者的用户id' Integer, ForeignKey('system_users.user_id', ondelete='CASCADE'), comment='被关注者的用户id'
) )
updated_at = None
follower = relationship('User', foreign_keys=[follower_id], back_populates='following') follower = relationship('User', foreign_keys=[follower_id], back_populates='following')
followed = relationship('User', foreign_keys=[followed_id], back_populates='followers') followed = relationship('User', foreign_keys=[followed_id], back_populates='followers')
def __repr__(self): def __repr__(self):
return f"<UserFollows(follower_id={self.follower_id}, followed_id={self.followed_id})>" return f'<UserFollows(follower_id={self.follower_id}, followed_id={self.followed_id})>'
class Card(BaseModel, DateModel): class Card(BaseModel, DateModel):
__tablename__ = 'system_cards' __tablename__ = 'system_cards'
__table_args__ = ( __table_args__ = (
Index('cert_number_category_source', 'cert_number', 'category', 'source'), Index('idx_cert_number_category_source', 'cert_number', 'category', 'source'),
{'comment': '卡片表'} {'comment': '卡片表'}
) )
@@ -129,28 +135,28 @@ class Card(BaseModel, DateModel):
card_name: Mapped[str] = mapped_column(String(225), nullable=True, comment='卡片名称') card_name: Mapped[str] = mapped_column(String(225), nullable=True, comment='卡片名称')
card_number: Mapped[str] = mapped_column(String(225), nullable=True, comment='卡片编号') card_number: Mapped[str] = mapped_column(String(225), nullable=True, comment='卡片编号')
card_type: Mapped[str] = mapped_column(String(20), nullable=True, comment='卡片类型') card_type: Mapped[str] = mapped_column(String(20), nullable=True, comment='卡片类型')
card_picture: Mapped[dict] = mapped_column(JSON, default=dict, comment='卡片正、反图片(本地)') card_picture: Mapped[dict] = mapped_column(JSON, default=lambda: dict(), comment='卡片正、反图片(本地)')
card_spare_picture: Mapped[dict] = mapped_column(JSON, default=dict, comment='卡片正、反图片(远程)') card_spare_picture: Mapped[dict] = mapped_column(JSON, default=lambda: dict(), comment='卡片正、反图片(远程)')
create_user_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='创建者') create_user_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='创建者')
perfect_user_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='完善者') perfect_user_id: Mapped[int] = mapped_column(Integer, nullable=True, comment='完善者')
category: Mapped[str] = mapped_column( category: Mapped[str] = mapped_column(
Enum(base_schema.CardCategoryType, inherit_schema=True), Enum(base_schema.CardCategoryType, inherit_schema=True),
nullable=True, default=base_schema.CardCategoryType.OTHER, comment='卡片分类' nullable=True, server_default=base_schema.CardCategoryType.OTHER.value, comment='卡片分类'
) )
source: Mapped[str] = mapped_column( source: Mapped[str] = mapped_column(
Enum(base_schema.PlatformSourceType, inherit_schema=True), Enum(base_schema.PlatformSourceType, inherit_schema=True),
comment='来源PSA、BGS、CGC、GBTC、CCG、BCTC :采集 、OTHER自定义' comment='来源PSA、BGS、CGC、GBTC、CCG、BCTC :采集 、OTHER自定义'
) )
collections: Mapped[list["UserCollection"]] = relationship( collections: Mapped[list['UserCollection']] = relationship(
back_populates="card", back_populates='card',
cascade="all, delete-orphan" cascade='all, delete-orphan'
) )
class UserCollection(BaseModel, DateModel): class UserCollection(BaseModel, DateModel):
__tablename__ = "system_user_collections" __tablename__ = 'system_user_collections'
__table_args__ = ( __table_args__ = (
UniqueConstraint('user_id', 'card_id', name='uniq_user_card'), UniqueConstraint('user_id', 'card_id', name='uniq_user_card'),
CheckConstraint('purchase_price >= 0', name='check_purchase_price'), CheckConstraint('purchase_price >= 0', name='check_purchase_price'),
@@ -175,16 +181,15 @@ class UserCollection(BaseModel, DateModel):
displayed: Mapped[bool] = mapped_column( displayed: Mapped[bool] = mapped_column(
Boolean, default=True Boolean, default=True
) )
updated_at = None
user: Mapped["User"] = relationship(back_populates='collections') user: Mapped['User'] = relationship(back_populates='collections')
card: Mapped["Card"] = relationship(back_populates='collections') card: Mapped['Card'] = relationship(back_populates='collections')
class SearchCardCache(BaseModel, DateModel): class SearchCardCache(BaseModel, DateModel):
__tablename__ = 'system_cache_cards' __tablename__ = 'system_cache_cards'
__table_args__ = ( __table_args__ = (
Index('cert_and_card_number_source', 'cert_number', 'source'), Index('idx_cert_and_card_number_source', 'cert_number', 'source'),
{'comment': '搜索卡片信息缓存表'} {'comment': '搜索卡片信息缓存表'}
) )
@@ -196,38 +201,105 @@ class SearchCardCache(BaseModel, DateModel):
comment='数据来源PSA、BGS、CGC、GBTC、CCG、BCTC' comment='数据来源PSA、BGS、CGC、GBTC、CCG、BCTC'
) )
# class Product(BaseModel, DateModel):
# __tablename__ = 'system_products'
# __table_args__ = {'comment': '产品信息表'}
#
# name: Mapped[str] = mapped_column(String(50), comment='产品名')
# price: Mapped[float] = mapped_column(DECIMAL(10, 2), comment='价格')
# action: Mapped[str] = mapped_column(String(10), comment='付款后的动作会员续期vip_renewal 积分recharge_points')
# number: Mapped[int] = mapped_column(Integer, comment='执行动作的增量值')
# description: Mapped[str] = mapped_column(Text, comment='商品描述')
#
#
# class Order(BaseModel, DateModel):
# __tablename__ = 'system_orders'
# __table_args__ = {'comment': '产品信息表'}
#
# order_id: Mapped[int] = mapped_column(String(20), comment='订单id')
# user_id: Mapped[int] = mapped_column(Integer, comment='用户id')
# product_id: Mapped[int] = mapped_column(Integer, comment='产品id')
# price: Mapped[float] = mapped_column(DECIMAL(10, 2), comment='价格')
# pay_url: Mapped[str] = mapped_column(String(255), comment='支付地址')
# status: Mapped[int] = mapped_column(Integer, comment='订单状态,-1初始化 0失败 1完成 2待支付')
# class Product(BaseModel, DateModel):
# class Message(BaseModel, DateModel): __tablename__ = 'system_products'
# __tablename__ = 'system_messages' __table_args__ = {'comment': '产品信息表'}
# __table_args__ = {'comment': '聊天消息表'}
# name: Mapped[str] = mapped_column(String(20), comment='产品名')
# guid: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), default=uuid.uuid4) category: Mapped[str] = mapped_column(
# message_type: Mapped[str] = mapped_column( Enum(base_schema.ProductCategory, inherit_schema=True), comment='商品分类, vip-会员服务 points-积分服务'
# Enum(base_schema.MessageType, inherit_schema=True), default=base_schema.MessageType.TEXT )
# ) price: Mapped[int] = mapped_column(Integer, comment='价格,不带小数点 单位为分')
# content: Mapped[str] = mapped_column(String(5000), comment='文本消息') action: Mapped[str] = mapped_column(String(10), comment='付款后的动作会员续期vip_renewal 积分recharge_points')
# send_timestamp: Mapped[int] = mapped_column(Integer, comment='发送时间') number: Mapped[int] = mapped_column(Integer, comment='执行动作的增量值')
# description: Mapped[str] = mapped_column(Text, nullable=True, comment='商品描述')
# user: Mapped["User"] = relationship(back_populates="system_messages")
class Order(BaseModel, DateModel):
__tablename__ = 'system_orders'
__table_args__ = (
Index('idx_order_on_order_id', 'order_id'),
{'comment': '产品订单表'}
)
order_id: Mapped[str] = mapped_column(String(32), comment='订单id')
transaction_id: Mapped[str] = mapped_column(String(32), nullable=True, comment='微信订单id')
user_id: Mapped[int] = mapped_column(Integer, comment='用户id')
product_id: Mapped[int] = mapped_column(Integer, comment='产品id')
total_fee: Mapped[int] = mapped_column(Integer, nullable=True, comment='发起的总金额,单位为分')
notify_total_fee: Mapped[int] = mapped_column(Integer, nullable=True, comment='回调成功支付的总金额,单位为分')
pay_url: Mapped[str] = mapped_column(String(255), nullable=True, comment='支付地址')
status: Mapped[str] = mapped_column(
Enum(base_schema.OrderStatusType, inherit_schema=True),
server_default=base_schema.OrderStatusType.INIT.value,
comment='订单状态init初始化 fail失败 ok完成 wait payment待支付 cancel. 取消'
)
class Chat(BaseModel, DateModel):
__tablename__ = 'system_chats'
__table_args__ = (
Index('idx_chat_on_guid', 'guid'),
{'comment': '聊天表'}
)
guid: Mapped[uuid.UUID] = mapped_column(String(36), nullable=True, default=lambda: str(uuid.uuid4()))
users: Mapped[List['User']] = relationship(secondary='chat_participant', back_populates='chats')
messages: Mapped[List['Message']] = relationship(back_populates='chat', cascade='all,delete')
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
read_statuses: Mapped[List['ReadStatus']] = relationship(back_populates='chat', cascade='all,delete')
class Message(BaseModel, DateModel):
__tablename__ = 'system_messages'
__table_args__ = (
Index('idx_message_on_user_id', 'user_id'),
Index('idx_message_on_chat_id', 'chat_id'),
Index('idx_message_on_user_id_chat_id', 'chat_id', 'user_id'),
{'comment': '聊天消息表'}
)
guid: Mapped[uuid.UUID] = mapped_column(String(36), nullable=True, default=lambda: str(uuid.uuid4()))
user_id: Mapped[int] = mapped_column(ForeignKey('system_users.user_id'))
chat_id: Mapped[int] = mapped_column(ForeignKey('system_chats.id'))
message_type: Mapped[str] = mapped_column(
Enum(base_schema.MessageType, inherit_schema=True), default=base_schema.MessageType.TEXT
)
content: Mapped[str] = mapped_column(String(5000), nullable=True, comment='文本消息')
chat: Mapped['Chat'] = relationship(back_populates='messages')
user: Mapped['User'] = relationship(back_populates='messages')
def __str__(self):
return f'{self.content}'
class ReadStatus(BaseModel, DateModel):
__tablename__ = 'system_read_status'
__table_args__ = (
Index('idx_read_status_on_chat_id', 'chat_id'),
Index('idx_read_status_on_user_id', 'user_id'),
{'comment': '聊天消息读取状态表'}
)
last_read_message_id: Mapped[int] = mapped_column(nullable=True)
user_id: Mapped[int] = mapped_column(ForeignKey('system_users.user_id'))
chat_id: Mapped[int] = mapped_column(ForeignKey('system_chats.id'))
chat: Mapped['Chat'] = relationship(back_populates='read_statuses')
user: Mapped['User'] = relationship(back_populates='read_statuses')
def __str__(self):
return f'User: {self.user_id}, Message: {self.last_read_message_id}'
chat_participant = Table(
'chat_participant',
BaseModel.metadata,
Column('user_id', Integer, ForeignKey('system_users.user_id'), primary_key=True),
Column('chat_id', Integer, ForeignKey('system_chats.id'), primary_key=True),
comment='聊天参与者关联表'
)

View File

@@ -4,10 +4,3 @@
# @Software : PyCharm # @Software : PyCharm
# @Author : xingc # @Author : xingc
# @Desc : # @Desc :
from .auth.routes import auth_router, test_auth_router
from .card.routes import card_router
from .im.routes import im_router
from .file.routes import file_router
from .user.routes import user_router
routers = [auth_router, test_auth_router, card_router, im_router, file_router, user_router]

View File

@@ -91,8 +91,9 @@ async def wechat_login(
session_key = wechat_session.get('session_key') session_key = wechat_session.get('session_key')
union_id = wechat_session.get('union_id') union_id = wechat_session.get('union_id')
try: try:
profile = await decrypt_sensitive_data(session_key, payload.encrypted_data, payload.iv) profile = decrypt_sensitive_data(session_key, payload.encrypted_data, payload.iv)
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
logger.exception(f"[wechat_login] Exception, User information decryption failed, detail: {e}")
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail='请重试登录操作') raise HTTPException(status.HTTP_400_BAD_REQUEST, detail='请重试登录操作')
logger.info(f"decrypt plaintext: {wechat_session}") logger.info(f"decrypt plaintext: {wechat_session}")

View File

@@ -4,7 +4,6 @@
# @Software : PyCharm # @Software : PyCharm
# @Author : xingc # @Author : xingc
# @Desc : # @Desc :
import re
import logging import logging
from typing import List, Literal from typing import List, Literal

View File

@@ -12,7 +12,7 @@ from pydantic import BaseModel, constr, condecimal, field_validator
from core.base.schema import SupportPlatformType, PlatformSourceType, CardCategoryType from core.base.schema import SupportPlatformType, PlatformSourceType, CardCategoryType
class CardPrice(BaseModel): class CardPicture(BaseModel):
front_picture: Optional[str] = None front_picture: Optional[str] = None
reverse_picture: Optional[str] = None reverse_picture: Optional[str] = None
@@ -29,13 +29,13 @@ class Card(BaseModel):
cert_number: str cert_number: str
card_year: Optional[constr(pattern=r'^\d{4}([-~]\d{1,4})?$')] card_year: Optional[constr(pattern=r'^\d{4}([-~]\d{1,4})?$')]
card_brand: Optional[str] = None card_brand: Optional[str] = None
card_score: Optional[float] = None card_score: Optional[condecimal(max_digits=10, decimal_places=1)] = None
auto_score: Optional[float] = None auto_score: Optional[condecimal(max_digits=10, decimal_places=1)] = None
card_name: str card_name: str
card_number: Optional[str] = None card_number: Optional[str] = None
card_type: Optional[str] = None card_type: Optional[str] = None
card_picture: Optional[CardPrice] = {} card_picture: Optional[CardPicture] = CardPicture()
card_spare_picture: Optional[dict] = {} card_spare_picture: Optional[dict] = CardPicture()
source: SupportPlatformType source: SupportPlatformType

276
core/routers/chat/routes.py Normal file
View File

@@ -0,0 +1,276 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:53
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
import logging
from typing import List, Dict
from json.decoder import JSONDecodeError
from fastapi import APIRouter, Query, Depends, WebSocket, WebSocketDisconnect, HTTPException, status
from fastapi_limiter.depends import WebSocketRateLimiter
import redis.asyncio as aioredis
from sqlalchemy import select, func, and_, exists
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from core.base import schema as base_schema
from core.db.crud import CRUD
from core.db.engine import get_async_session, get_cache
from core.db.models import Message, Chat, User, ReadStatus, chat_participant
from core.routers.auth.schema import LoginUser
from core.routers.auth.services import get_current_user
from core.base.exceptions import WebsocketTooManyRequests
from . import schema
from .handlers import socket_manager
from .services import websocket_callback, get_user_active_direct_chats, get_unread_messages_per_chat
logger = logging.getLogger(__name__)
chat_router = APIRouter(prefix='/chat', tags=['聊天模块'])
@chat_router.get(
'/all/', summary='获取所有聊天',
response_model=base_schema.BaseResponse[
base_schema.Paginated[
List[schema.ChatUnreadOut]
]
]
)
async def get_all_chats(
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session),
page: int = Query(default=1, gt=0),
page_size: int = Query(default=15, gt=0, le=200),
):
filter_conditions = (
exists().where(
(chat_participant.c.chat_id == Chat.id) &
(chat_participant.c.user_id == login_user.user_id)
) &
Chat.is_deleted.is_(False)
)
query = (
select(Chat)
.where(filter_conditions).order_by(Chat.updated_at.desc())
)
total_query = select(func.count()).select_from(Chat).filter(filter_conditions)
total_result = await db_session.execute(total_query)
total = total_result.scalar()
query = query.limit(page_size).offset((page - 1) * page_size)
result = await db_session.execute(query)
chats = result.scalars().all()
unread_messages_chats_count = await get_unread_messages_per_chat(db_session, chats, login_user)
return {
'data': {
'list': unread_messages_chats_count,
'total': total,
'page': page,
'page_size': page_size,
}
}
@chat_router.get(
'/{chat_guid}/messages/', summary='获取单个聊天的历史消息',
response_model=base_schema.BaseResponse[
base_schema.Paginated[
List[Dict]
]
]
)
async def get_single_chat_messages(
chat_guid: str,
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session),
page: int = Query(default=1, gt=0),
page_size: int = Query(default=15, gt=0, le=200),
last_read_message_id: int = Query(default=0, ge=0),
):
db_crud = CRUD(db_session, Chat)
chat: Chat | None = await db_crud.get(filters={'guid': chat_guid, 'is_deleted': False})
if not chat:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='您访问的聊天不存在')
chat_user_ids = [user.user_id for user in await chat.awaitable_attrs.users]
if login_user.user_id not in chat_user_ids:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='您无权访问此聊天')
# 组装查询条件
conditions = and_(
Message.chat_id == chat.id,
Message.id > last_read_message_id
)
# 记录总数查询
total_stmt = (
select(func.count()).where(
conditions
)
)
total = await db_session.execute(total_stmt)
# 分页记录查询
stmt = (
select(Message)
.where(conditions)
.order_by(Message.created_at.desc())
.limit(page_size).offset((page - 1) * page_size)
.options(selectinload(Message.user), selectinload(Message.chat))
)
records = await db_session.execute(stmt)
return {
'data': {
'list': [
{
**card.dict(),
**user_collection.dict()
} for card, user_collection in records
],
'total': total.scalar(),
'page': page,
'page_size': page_size,
},
}
@chat_router.post('/create', summary='创建新聊天', response_model=base_schema.BaseResponse[schema.Chat])
async def create_new_chat(
payload: schema.ChatCreateIn,
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session),
):
if payload.recipient_user_id == login_user.user_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='操作禁止')
recipient_user: User | None = await CRUD(db_session, User).get(filters={'user_id': payload.recipient_user_id})
if recipient_user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='收件人不存在')
initiator_user: User = await CRUD(db_session, User).get(filters={'user_id': login_user.user_id})
# 查询是否已存在 对应且未关闭的聊天
query = (
select(Chat)
.where(
exists().where(
(chat_participant.c.chat_id == Chat.id) &
(chat_participant.c.user_id == login_user.user_id)
) &
exists().where(
(chat_participant.c.chat_id == Chat.id) &
(chat_participant.c.user_id == recipient_user.user_id)
) &
Chat.is_deleted == False
).order_by(Chat.created_at.desc())
)
result = await db_session.execute(query)
chat = result.scalar_one_or_none()
if chat is None:
try:
chat = Chat()
chat.users.append(initiator_user)
chat.users.append(recipient_user)
db_session.add(chat)
await db_session.flush()
initiator_read_status = ReadStatus(chat_id=chat.id, user_id=initiator_user.user_id, last_read_message_id=0)
recipient_read_status = ReadStatus(chat_id=chat.id, user_id=recipient_user.user_id, last_read_message_id=0)
db_session.add_all([initiator_read_status, recipient_read_status])
await db_session.commit()
except Exception as exc_info:
await db_session.rollback()
raise exc_info
return {
'data': {
'chat_guid': chat.guid,
'friend_user': recipient_user,
}
}
@chat_router.delete('/{chat_guid}/', summary='删除聊天', response_model=base_schema.BaseResponse)
async def delete_single_chat(
chat_guid: str,
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session),
):
chat = await CRUD(db_session, Chat).update(
data={'is_deleted': True},
filters={'chat_guid': chat_guid, 'user_id': login_user.user_id}
)
if chat is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='聊天不存在')
return {'message': '聊天已删除'}
@chat_router.websocket('/ws')
async def get_chat_websocket(
websocket: WebSocket,
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session),
redis_conn: aioredis.Redis = Depends(get_cache)
):
current_user = await CRUD(db_session, User).get(filters={'user_id': login_user.user_id})
await socket_manager.connect_socket(websocket=websocket)
logger.info('Websocket connection is established')
ratelimit = WebSocketRateLimiter(times=50, seconds=10, callback=websocket_callback)
user_id = str(login_user.user_id)
await socket_manager.add_user_socket_connection(user_id, websocket)
if chats := await get_user_active_direct_chats(db_session, current_user=current_user):
for chat_guid in chats:
await socket_manager.add_user_to_chat(chat_guid, websocket)
else:
chats = dict()
try:
while True:
try:
incoming_message = await websocket.receive_json()
await ratelimit(websocket)
try:
message = schema.Message(**incoming_message)
except ValueError:
await socket_manager.send_error(websocket, 'The message type or format you sent is invalid')
continue
handler = socket_manager.handlers.get(message.type)
if not handler:
logger.error(f'No handler [{message.type}] exists')
await socket_manager.send_error(websocket, f'Type: {message.type} was not found')
continue
await handler(
websocket=websocket,
db_session=db_session,
cache=redis_conn,
current_user=current_user,
chats=chats,
message=message,
)
except (JSONDecodeError, AttributeError) as exc:
logger.exception(f'Websocket error, detail: {exc}')
await websocket.send_json({'status': 'error', 'message': 'Wrong message format'})
except ValueError as exc:
logger.exception(f'Websocket error, detail: {exc}')
await websocket.send_json({'status': 'error', 'message': 'Could not validate incoming message'})
except WebsocketTooManyRequests:
logger.exception(f'User: {login_user} sent too many ws requests')
await websocket.send_json({'status': 'error', 'message': 'You have sent too many requests'})
except WebSocketDisconnect:
logging.info('Websocket is disconnected')

View File

@@ -70,9 +70,9 @@ async def upload_file(
return {'data': file_info} return {'data': file_info}
except Exception as e: except Exception as e:
logger.error(f'Upload failed: {str(e)}') logger.error(f'Upload failed: {e}')
logger.exception(e) logger.exception(e)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'File upload failed' detail='Internal server error'
) )

View File

@@ -1,33 +0,0 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午4:53
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from typing import Annotated
from fastapi import APIRouter, Query
im_router = APIRouter(prefix='/im', tags=['聊天模块'])
# @im_router.get('/chat/{user_id}/messages/')
# def get_single_chat_messages(
# user_id: int,
# limit: Annotated[int, Query(ge=1, le=200)] = 20,
# cursor: int = -1,
# ):
# pass
#
#
# @im_router.get('/chats/messages/')
# def get_chats_messages(
# limit: Annotated[int, Query(ge=1, le=200)] = 100,
# cursor: int = -1,
# ):
# pass
#
#
# @im_router.websocket('/chat')
# def get_chat():
# pass

View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/24 上午9:03
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc :

109
core/routers/pay/routes.py Normal file
View File

@@ -0,0 +1,109 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/24 上午9:03
# @File : routes.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
import logging
import xmltodict
from fastapi import APIRouter, Request, HTTPException, status, Depends, BackgroundTasks
from sqlalchemy.ext.asyncio import AsyncSession
from core.base import schema as base_schema
from core.db.crud import CRUD
from core.db.engine import get_async_session
from core.db.models import Order
from core.routers.auth.schema import LoginUser
from core.routers.auth.services import get_current_user
from core.utils.helper import generate_keyname, generate_order_number
from core.wechat.api import wechat_pay_api
from . import schema
from .actions import actions_after_payment
logger = logging.getLogger(__name__)
pay_router = APIRouter(prefix='/pay', tags=['支付模块'])
@pay_router.post(
'/order/create',
summary='订单创建',
response_model=base_schema.BaseResponse[schema.CreateOrderOut]
)
async def create_order(
request: Request,
payload: schema.CreateOrderIn,
login_user: LoginUser = Depends(get_current_user),
db_session: AsyncSession = Depends(get_async_session),
):
client_ip = (
request.headers.get('X-Real-IP') or
request.headers.get('x-real-ip') or
request.client.host
)
out_trade_no = generate_order_number()
prepay_id = wechat_pay_api.create_unified_order(
**payload.dict(exclude_none=True),
out_trade_no=out_trade_no,
spbill_create_ip=client_ip
)
payment_params = wechat_pay_api.generate_payment_params(prepay_id)
db_crud = CRUD(db_session, Order)
order: Order = await db_crud.create(
data={
'order_id': out_trade_no,
'user_id': login_user.user_id,
'product_id': payload.product_id,
'total_fee': payload.total_fee,
'status': base_schema.OrderStatusType.WAIT_PAYMENT
},
filters={
'out_trade_no': out_trade_no,
}
)
return {'data': {'payment_params': payment_params, 'order_id': order.order_id}}
@pay_router.post('/wechat/notify', summary='处理支付结果通知', response_model=base_schema.BaseResponse)
async def wechat_notify(
request: Request,
background_tasks: BackgroundTasks,
db_session: AsyncSession = Depends(get_async_session)
):
body = await request.body()
logger.debug(body.decode())
result = xmltodict.parse(body.decode())
# 验证签名
params = result['xml']
sign = params.pop('sign')
local_sign = wechat_pay_api.generate_sign(params)
if sign != local_sign:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='签名验证失败'
)
# 处理支付结果
db_crud = CRUD(db_session, Order)
record = {
'out_trade_no': params['out_trade_no'],
'transaction_id': params['transaction_id']
}
if params['return_code'] == 'SUCCESS' and params['result_code'] == 'SUCCESS':
record.update({
'notify_total_fee': params['total_fee'],
'status': base_schema.OrderStatusType.OK
})
result = {'code': 200, 'message': 'OK'}
background_tasks.add_task(actions_after_payment, notify_params=params)
else:
result = {'code': 400, 'message': '支付失败'}
record.update({
'status': base_schema.OrderStatusType.FAIL,
})
await db_crud.update(data=record, filters={'order_id': record['out_trade_no']})
return result

View File

@@ -0,0 +1,28 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/24 下午9:51
# @File : schema.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from pydantic import BaseModel
class CreateOrderIn(BaseModel):
product_id: int
openid: str
total_fee: int
body: str
class PaymentParamsOut(BaseModel):
appId: str
timeStamp: str
nonceStr: str
package: str
package: str
signType: str
class CreateOrderOut(BaseModel):
payment_params: PaymentParamsOut
order_id: str

View File

@@ -33,7 +33,7 @@ class LiteUser(BaseModel):
displayed_card_total: Optional[int] = 0 displayed_card_total: Optional[int] = 0
class UserFollows(Schema): class UserFollows(BaseModel):
nickname: str nickname: str
user_id: int user_id: int
gender: Optional[int] = None gender: Optional[int] = None

View File

@@ -6,6 +6,8 @@
# @Desc : # @Desc :
import asyncio import asyncio
import functools import functools
import random
import string
from datetime import datetime from datetime import datetime
from typing import Callable from typing import Callable
@@ -57,3 +59,17 @@ def check_picture_for_empty(picture: dict, or_: bool = False) -> bool:
picture.get('reverse_picture') is None picture.get('reverse_picture') is None
] ]
return any(results) if or_ else all(results) return any(results) if or_ else all(results)
def generate_order_number():
random_part = ''.join(random.choices(string.digits, k=18))
order_number = f"{datetime.now().strftime('%Y%m%d%H%M%S')}{random_part}"
return order_number
def format_date(date: datetime | None) -> str | None:
if date is None:
return date
else:
return date.strftime('%Y-%m-%d %H:%M:%S')

View File

@@ -11,8 +11,11 @@ import time
import httpx import httpx
import ujson import ujson
import xmltodict
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad from Crypto.Util.Padding import unpad
from fastapi import HTTPException
from simple_spider_tool import md5, jsonpath
from config import settings from config import settings
from core.base.exceptions import WechatApiError from core.base.exceptions import WechatApiError
@@ -22,22 +25,10 @@ from core.db.engine import get_cache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WechatApi: class BaseClient:
def __init__(self, appid, secret): def __init__(self):
self.appid = appid
self.secret = secret
self._access_token = None
self._access_token_expire_time = -1
self.session = httpx.AsyncClient() self.session = httpx.AsyncClient()
async def access_token(self):
if any([
self._access_token is None,
self._access_token_expire_time < math.floor(time.time()),
]):
await self.get_access_token()
return self._access_token
async def request(self, method, url, headers=None, params=None, data=None, json=None, **kwargs): async def request(self, method, url, headers=None, params=None, data=None, json=None, **kwargs):
r = await self.session.request( r = await self.session.request(
method, url, headers=headers, params=params, data=data, json=json, **kwargs method, url, headers=headers, params=params, data=data, json=json, **kwargs
@@ -62,6 +53,23 @@ class WechatApi:
'POST', url, headers=headers, params=params, data=data, json=json, **kwargs 'POST', url, headers=headers, params=params, data=data, json=json, **kwargs
) )
class WechatApi(BaseClient):
def __init__(self, appid, secret):
self.appid = appid
self.secret = secret
self._access_token = None
self._access_token_expire_time = -1
super().__init__()
async def access_token(self):
if any([
self._access_token is None,
self._access_token_expire_time < math.floor(time.time()),
]):
await self.get_access_token()
return self._access_token
async def get_access_token(self): async def get_access_token(self):
redis_conn = await get_cache() redis_conn = await get_cache()
result_str = await redis_conn.get('access_token') result_str = await redis_conn.get('access_token')
@@ -113,39 +121,96 @@ class WechatApi:
wechat_api = WechatApi(appid=settings.APPID, secret=settings.APP_SECRET) wechat_api = WechatApi(appid=settings.APPID, secret=settings.APP_SECRET)
async def decrypt_sensitive_data(session_key: str, encrypted_data: str, iv: str) -> dict: def decrypt_sensitive_data(session_key: str, encrypted_data: str, iv: str) -> dict:
"""解密敏感数据""" """解密敏感数据"""
session_key_bytes = base64.b64decode(session_key) session_key_bytes = base64.b64decode(session_key)
iv_bytes = base64.b64decode(iv) iv_bytes = base64.b64decode(iv)
encrypted_data_bytes = base64.b64decode(encrypted_data) encrypted_data_bytes = base64.b64decode(encrypted_data)
cipher = AES.new(session_key_bytes, AES.MODE_CBC, iv_bytes) cipher = AES.new(session_key_bytes, AES.MODE_CBC, iv_bytes)
# decrypted = cipher.decrypt(encrypted_data_bytes)
# pad = decrypted[-1]
# decrypted = decrypted[:-pad]
decrypted = unpad(cipher.decrypt(encrypted_data_bytes), AES.block_size) decrypted = unpad(cipher.decrypt(encrypted_data_bytes), AES.block_size)
return ujson.decode(decrypted.decode('utf-8')) return ujson.decode(decrypted.decode('utf-8'))
def decrypt_phone_number(encrypted_data: str, session_key: str, iv: str) -> dict: class WechatPayApi(BaseClient):
try: def __init__(self, appid, mch_id, app_key, notify_url, debug: bool = False):
# Base64 解码 """
session_key_bytes = base64.b64decode(session_key) :param appid: 小程序id
encrypted_bytes = base64.b64decode(encrypted_data) :param mch_id: 商户id
iv_bytes = base64.b64decode(iv) :param app_key: 接口密钥
:param debug:
"""
self.appid = appid
self.mch_id = mch_id
self.appi_key = app_key
self.notify_url = notify_url
self.base_url = 'https://api.mch.weixin.qq.com'
super().__init__()
# AES 解密 @staticmethod
cipher = AES.new(session_key_bytes, AES.MODE_CBC, iv_bytes) def generate_nonce_str() -> str:
decrypted = unpad(cipher.decrypt(encrypted_bytes), AES.block_size) """生成随机字符串"""
decrypted_data = ujson.loads(decrypted.decode('utf-8')) return md5(f'{time.time()}')
# 验证数据 def generate_sign(self, params: dict) -> str:
if decrypted_data.get("watermark", {}).get("appid") != "your-appid": """生成签名"""
raise ValueError("Invalid appid in decrypted data") plaintext = '&'.join([f'{k}={params[k]}' for k in sorted(params.keys())])
return decrypted_data return md5(f'{plaintext}&key={self.appi_key}').upper()
except Exception as e:
logger.info(e)
# raise HTTPException(status_code=400, detail=f"解密失败: {str(e)}")
def generate_payment_params(self, prepay_id) -> dict:
"""生成小程序支付参数"""
params = {
"appId": self.appid,
"timeStamp": f'{int(time.time())}',
"nonceStr": self.generate_nonce_str(),
"package": f"prepay_id={prepay_id}",
"signType": "MD5",
}
params["paySign"] = self.generate_sign(params)
return params
async def create_unified_order(
self,
openid: str,
body: str,
out_trade_no: str,
total_fee: int,
create_ip: str,
):
url = self.base_url + '/pay/unifiedorder'
params = {
'appid': self.appid,
'mch_id': self.mch_id,
'nonce_str': self.generate_nonce_str(),
'body': body,
'out_trade_no': out_trade_no,
'total_fee': total_fee,
'spbill_create_ip': create_ip,
'notify_url': self.notify_url,
'trade_type': 'JSAPI',
'openid': openid,
}
params['sign'] = self.generate_sign(params)
headers = {
'Content-Type': 'application/xml',
}
data = xmltodict.unparse({'xml': params}, full_document=False)
r = await self.post(url, headers=headers, data=data)
if r.status_code != 200:
raise HTTPException(status_code=500, detail='微信支付接口请求失败')
# 解析返回的 XML
result = xmltodict.parse(r.text)
return_code = jsonpath(result, '$.xml.return_code', first=True)
if return_code != 'SUCCESS':
return_msg = jsonpath(result, '$.xml.return_msg', first=True)
raise HTTPException(status_code=500, detail=return_msg)
return result['xml']['prepay_id']
wechat_pay_api = WechatPayApi(
appid=settings.APPID, mch_id=settings.PAY_MCH_ID, app_key=settings.PAY_API_KEY, notify_url=settings.PAY_NOTIFY_URL
)
if __name__ == '__main__': if __name__ == '__main__':
import asyncio import asyncio

View File

@@ -4,12 +4,12 @@
# @Software : PyCharm # @Software : PyCharm
# @Author : xingc # @Author : xingc
# @Desc : # @Desc :
import logging import logging.config
from config import LOGGING_CONFIG from config import LOGGING_CONFIG
from core.app import create_app from core.app import create_app
# logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
app = create_app() app = create_app()

View File

@@ -20,9 +20,9 @@ SQLAlchemy[asyncio]
SQLAlchemy[aiomysql] SQLAlchemy[aiomysql]
ujson ujson
uvicorn[standard] uvicorn[standard]
pycryptodome
xmltodict
fastapi-limiter