first init

This commit is contained in:
xingc
2025-02-18 14:26:47 +08:00
commit 48a02974fc
41 changed files with 2297 additions and 0 deletions

6
core/db/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午12:17
# @File : __init__.py.py
# @Software : PyCharm
# @Author : xingc
# @Desc :

185
core/db/crud.py Normal file
View File

@@ -0,0 +1,185 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午12:18
# @File : crud.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from datetime import datetime
from typing import List, Optional, Union, NewType, Dict, Any, Tuple
from sqlalchemy import func, desc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from .models import User, Card, SearchCardCache
Model = NewType('Model', Union[User, Card, SearchCardCache])
class CRUD:
def __init__(self, session: AsyncSession, model: Model):
self.session = session
self.model = model
async def get(self, filters: dict) -> Optional[Model]:
async with self.session.begin():
result = await self.session.execute(select(self.model).filter(
*self.build_filter_conditions(filters))
)
record = result.scalar_one_or_none()
return record
async def get_paginated(self, filters: dict, page: int = 1, page_size: int = 10) -> Tuple[List[Model], int]:
async with self.session.begin():
filter_conditions = self.build_filter_conditions(filters) if filters else []
query = select(self.model).filter(*filter_conditions).order_by(desc(self.model.insert_time))
total_query = select(func.count()).select_from(self.model).filter(*filter_conditions)
total_result = await self.session.execute(total_query)
total = total_result.scalar()
query = query.limit(page_size).offset((page - 1) * page_size)
result = await self.session.execute(query)
records = result.scalars().all()
return records, total
async def get_grouped_count(self, group_by_field: str, filters: Dict[str, Any] = None) -> Dict[Any, int]:
# 构建过滤条件
filter_conditions = self.build_filter_conditions(filters) if filters else []
# 确定分组字段
group_by = getattr(self.model, group_by_field)
# 构建查询
query = select(group_by, func.count()).filter(*filter_conditions).group_by(group_by)
# 执行查询并获取结果
async with self.session.begin():
result = await self.session.execute(query)
records = result.all()
return {record[0]: record[1] for record in records}
async def create(self, data: dict, filters: dict) -> Union[Model, bool]:
async with self.session.begin():
result = await self.session.execute(select(self.model).filter(
*self.build_filter_conditions(filters))
)
if result.scalars().first() is not None:
return False
record = self.model(**data)
self.session.add(record)
await self.session.commit()
return record
async def get_all(self, filters: dict = None) -> List[Model]:
async with self.session.begin():
result = await self.session.execute(
select(self.model) if filters is None else select(self.model).filter(
*self.build_filter_conditions(filters)).order_by(desc(self.model.insert_time))
)
records = result.scalars().all()
return records
async def update(self, data: dict, filters: dict) -> Optional[Model]:
async with self.session.begin():
result = await self.session.execute(select(self.model).filter(
*self.build_filter_conditions(filters))
)
record = result.scalars().first()
if not record:
return None
count = 0
for key, value in data.items():
if any([
value is None,
value == getattr(record, key),
key in ['insert_time', 'update_time']
]):
continue
setattr(record, key, value)
count += 1
if count > 0:
await self.session.commit()
return record
async def updates(self, data: dict, filters: dict) -> Optional[Model]:
async with self.session.begin():
result = await self.session.execute(select(self.model).filter(
*self.build_filter_conditions(filters))
)
records = result.scalars().all()
if len(records) == 0:
return None
count = 0
for record in records:
for key, value in data.items():
if any([
value is None,
value == getattr(record, key),
key in ['insert_time', 'update_time']
]):
continue
setattr(record, key, value)
count += 1
if count > 0:
await self.session.commit()
return records
async def delete(self, filters: dict) -> bool:
async with self.session.begin():
result = await self.session.execute(select(self.model).filter(
*self.build_filter_conditions(filters))
)
record = result.scalars().first()
if not record:
return False
await self.session.delete(record)
await self.session.commit()
return True
def build_filter_conditions(self, filters: Dict[str, Any]) -> tuple:
conditions = []
for key, value in filters.items():
if any([
value is None,
value == '',
]):
continue
if isinstance(value, datetime) and value.tzinfo is not None:
value = value.astimezone()
# 解析字段名和操作符
field_name, operator = key, None
if '__' in key:
field_name, operator = key.split('__', 1)
else:
field_name = key
field = getattr(self.model, field_name)
if operator == 'like': # 模糊匹配
conditions.append(field.like(f'%{value}%'))
elif operator == 'gt': # 大于
conditions.append(field > value)
elif operator == 'lt': # 小于
conditions.append(field < value)
elif operator == 'eq': # 等于
conditions.append(field == value)
elif operator == 'ne': # 不等于
conditions.append(field != value)
elif operator == 'in': # 在集合中
conditions.append(field.in_(value))
else:
# 如果没有匹配的操作符,默认为等于
conditions.append(field == value)
return tuple(conditions)

52
core/db/engine.py Normal file
View File

@@ -0,0 +1,52 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午12:19
# @File : engine.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
from typing import AsyncGenerator
import redis.asyncio as aioredis
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
engine = create_async_engine(
(
settings.DB_URL or
'{scheme}://{user}:{password}@{host}:{port}/{db}'.format(
scheme=settings.DB_SCHEMA,
user=settings.DB_USER,
password=settings.DB_PASSWORD,
host=settings.DB_HOST,
port=settings.DB_PORT,
db=settings.DB_NAME,
)
),
pool_size=settings.DB_POOL_SIZE,
pool_recycle=1800,
pool_timeout=30,
connect_args={'charset': 'utf8mb4', "connect_timeout": 5},
echo=settings.DEBUG
)
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with session_maker() as session:
yield session
def create_redis_pool():
return aioredis.ConnectionPool(
host=settings.REDIS_HOST, port=settings.REDIS_PORT, password=settings.REDIS_PASSWORD, db=settings.REDIS_DB,
decode_responses=True
)
redis_pool = create_redis_pool()
async def get_cache() -> aioredis.Redis:
return aioredis.Redis(connection_pool=redis_pool)

197
core/db/models.py Normal file
View File

@@ -0,0 +1,197 @@
# -*- coding = utf-8 -*-
# @Time : 2025/2/13 下午12:17
# @File : models.py
# @Software : PyCharm
# @Author : xingc
# @Desc :
import enum
import uuid
from datetime import datetime
import ujson
from sqlalchemy import (
Column, Integer, String, Text, Date,
ForeignKey, DECIMAL, CheckConstraint,
UniqueConstraint, Index, Sequence, JSON, UUID, Enum, Boolean
)
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, relationship
)
from sqlalchemy.sql import func
from sqlalchemy.ext.asyncio import AsyncAttrs
from core.base import schema as base_schema
class BaseModel(DeclarativeBase, AsyncAttrs):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True, comment='自增id'
)
def dict(self, exclude: set = None):
data = {}
for column in self.__table__.columns:
if exclude is not None and column.name in exclude:
continue
value = getattr(self, column.name)
if isinstance(value, datetime):
data[column.name] = value.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(value, enum.Enum):
data[column.name] = value.value
else:
data[column.name] = value
return data
def json(self, exclude: set = None):
return ujson.dumps(self.dict(exclude=exclude))
class DateModel:
created_at: Mapped[datetime] = mapped_column(server_default=func.current_timestamp(), comment='创建时间')
updated_at: Mapped[datetime] = mapped_column(server_default=func.current_timestamp(),
onupdate=func.current_timestamp(), comment='更新时间')
class User(BaseModel, DateModel):
__tablename__ = 'system_users'
__table_args__ = (
UniqueConstraint('user_id', name='uniq_user_id'),
{'comment': '用户表'}
)
nickname: Mapped[str] = mapped_column(String(20), nullable=False, comment='昵称')
user_id: Mapped[int] = mapped_column(
Integer, Sequence('non_primary_id_seq', start=100000, increment=2), comment='用户id'
)
head_avatar: Mapped[str] = mapped_column(String(500), comment='头像链接')
wechat_openid = mapped_column(String(100), comment='微信openid')
wechat_union_id = mapped_column(String(100), comment='微信unionid')
phone: Mapped[int] = mapped_column(Integer, comment='手机号码')
vip_expire_date: Mapped[datetime] = mapped_column(Date, comment='会员到期时间')
integral: Mapped[int] = mapped_column(Integer, default=0, comment='会员积分')
card_total: Mapped[int] = Column(Integer, default=0, comment='卡牌总数')
card_show_total: Mapped[int] = Column(Integer, default=0, comment='卡片展示总数')
collections: Mapped[list["UserCollection"]] = relationship(
back_populates="user",
cascade="all, delete-orphan"
)
class Card(BaseModel, DateModel):
__tablename__ = 'system_cards'
__table_args__ = (
Index('cert_number_category_source', 'cert_number', 'category', 'source'),
{'comment': '卡片表'}
)
cert_number: Mapped[str] = mapped_column(String(50), comment='评级卡号')
card_year: Mapped[int] = mapped_column(Integer, comment='卡片年份')
card_brand: Mapped[str] = mapped_column(Integer, comment='卡片品牌')
card_score: Mapped[float] = mapped_column(DECIMAL(10, 1), comment='卡片评分')
auto_score: Mapped[float] = mapped_column(DECIMAL(10, 1), comment='签字评分')
card_name: Mapped[str] = mapped_column(String(225), comment='卡片名称')
card_number: Mapped[str] = mapped_column(String(225), comment='卡片编号')
card_type: Mapped[str] = mapped_column(String(20), comment='卡片类型')
card_picture: Mapped[dict] = mapped_column(JSON, default=dict, comment='卡片正、反图片(本地)')
card_spare_picture: Mapped[dict] = mapped_column(JSON, default=dict, comment='卡片正、反图片(远程)')
create_user_id: Mapped[int] = mapped_column(Integer, comment='创建者')
category: Mapped[str] = mapped_column(
Enum(base_schema.CardCategoryType, inherit_schema=True),
default=base_schema.CardCategoryType.OTHER, comment='卡片分类'
)
source: Mapped[str] = mapped_column(
Enum(base_schema.PlatformSourceType, inherit_schema=True),
comment='来源PSA、BGS、CGC、GBTC、CCG、BCTC :采集 、OTHER自定义'
)
collections: Mapped[list["UserCollection"]] = relationship(
back_populates="card",
cascade="all, delete-orphan"
)
class UserCollection(BaseModel, DateModel):
__tablename__ = "system_user_collections"
__table_args__ = (
UniqueConstraint('user_id', 'card_id', name='uniq_user_card'),
CheckConstraint('purchase_price >= 0', name='check_purchase_price'),
CheckConstraint('display_value >= 0', name='check_display_price')
)
user_id: Mapped[int] = mapped_column(
Integer, ForeignKey("system_users.id", ondelete="CASCADE"),
nullable=False
)
card_id: Mapped[int] = mapped_column(
Integer, ForeignKey("system_cards.id", ondelete="CASCADE"),
nullable=False
)
purchase_price: Mapped[float] = mapped_column(
DECIMAL(10, 2), nullable=False
)
display_price: Mapped[float] = mapped_column(
DECIMAL(10, 2), nullable=False
)
displayed: Mapped[bool] = mapped_column(
Boolean, default=True
)
updated_at: Mapped[bool] = None
user: Mapped["User"] = relationship(back_populates="collections")
card: Mapped["Card"] = relationship(back_populates="collections")
class SearchCardCache(BaseModel, DateModel):
__tablename__ = 'system_cache_cards'
__table_args__ = (
Index('cert_and_card_number_source', 'cert_number', 'card_number', 'source'),
{'comment': '搜索卡片信息缓存表'}
)
cert_number: Mapped[str] = mapped_column(String(50), comment='评级卡号')
card_number: Mapped[str] = mapped_column(String(50), comment='卡片编号')
data: Mapped[dict] = mapped_column(JSON, comment='标准数据')
raw_data: Mapped[dict] = mapped_column(JSON, comment='原始数据')
source: Mapped[str] = mapped_column(
Enum(base_schema.SupportPlatformType, inherit_schema=True),
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 Message(BaseModel, DateModel):
# __tablename__ = 'system_messages'
# __table_args__ = {'comment': '聊天消息表'}
#
# guid: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), default=uuid.uuid4)
# 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), comment='文本消息')
# send_timestamp: Mapped[int] = mapped_column(Integer, comment='发送时间')
#
# user: Mapped["User"] = relationship(back_populates="system_messages")