first init
This commit is contained in:
6
core/db/__init__.py
Normal file
6
core/db/__init__.py
Normal 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
185
core/db/crud.py
Normal 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
52
core/db/engine.py
Normal 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
197
core/db/models.py
Normal 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")
|
||||
Reference in New Issue
Block a user