feat(all): 同步最新进度
This commit is contained in:
@@ -24,4 +24,7 @@ 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
6
associated_script.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding = utf-8 -*-
|
||||||
|
# @Time : 2025/3/8 下午11:23
|
||||||
|
# @File : associated_script.py
|
||||||
|
# @Software : PyCharm
|
||||||
|
# @Author : xingc
|
||||||
|
# @Desc : 协同清理脚本
|
||||||
@@ -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):
|
||||||
# 日志配置
|
# 日志配置
|
||||||
|
|||||||
52
core/app.py
52
core/app.py
@@ -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
|
||||||
|
|||||||
@@ -27,3 +27,8 @@ class SearchCardRetry(ScraperError):
|
|||||||
class WechatApiError(CardBookException):
|
class WechatApiError(CardBookException):
|
||||||
"""微信开放服务调用api错误"""
|
"""微信开放服务调用api错误"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketTooManyRequests(CardBookException):
|
||||||
|
"""websocket请求太多"""
|
||||||
|
pass
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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}%'))
|
||||||
|
|||||||
@@ -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='聊天参与者关联表'
|
||||||
|
)
|
||||||
@@ -3,11 +3,4 @@
|
|||||||
# @File : __init__.py.py
|
# @File : __init__.py.py
|
||||||
# @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]
|
|
||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
276
core/routers/chat/routes.py
Normal 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')
|
||||||
@@ -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'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
|
||||||
6
core/routers/pay/__init__.py
Normal file
6
core/routers/pay/__init__.py
Normal 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
109
core/routers/pay/routes.py
Normal 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
|
||||||
28
core/routers/pay/schema.py
Normal file
28
core/routers/pay/schema.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
4
main.py
4
main.py
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ SQLAlchemy[asyncio]
|
|||||||
SQLAlchemy[aiomysql]
|
SQLAlchemy[aiomysql]
|
||||||
ujson
|
ujson
|
||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
|
pycryptodome
|
||||||
|
xmltodict
|
||||||
|
fastapi-limiter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user