upload project source code
This commit is contained in:
2
后端源码/yifan.action-ai.cn/api/app/scripts/__init__.py
Normal file
2
后端源码/yifan.action-ai.cn/api/app/scripts/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
30
后端源码/yifan.action-ai.cn/api/app/scripts/check_db_sys_menu.py
Normal file
30
后端源码/yifan.action-ai.cn/api/app/scripts/check_db_sys_menu.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.database import async_db_session
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with async_db_session() as session:
|
||||
db = (await session.execute(text("SELECT DATABASE()"))).scalar()
|
||||
exists = (
|
||||
await session.execute(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM information_schema.tables "
|
||||
"WHERE table_schema = DATABASE() AND table_name = 'sys_menu'"
|
||||
)
|
||||
)
|
||||
).scalar()
|
||||
|
||||
print("DATABASE() =", db)
|
||||
print("sys_menu exists count =", exists)
|
||||
if exists:
|
||||
rows = (await session.execute(text("SELECT COUNT(*) FROM sys_menu"))).scalar()
|
||||
print("sys_menu rows =", rows)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
一次性初始化:会员功能菜单(目录 + 页面菜单 + 按钮权限点)
|
||||
|
||||
推荐用法(在已配置好 env/.env.dev 或运行环境变量的情况下):
|
||||
|
||||
python -m app.scripts.init_yifan_membership_menus
|
||||
|
||||
该脚本是幂等的:重复执行会跳过已存在的菜单/按钮。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import sqlalchemy as sa
|
||||
from app.core.database import async_db_session
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_system.menu.crud import MenuCRUD
|
||||
from app.api.v1.module_system.menu.schema import MenuCreateSchema
|
||||
from app.core.logger import log
|
||||
|
||||
# 关键:预先导入 RoleModel,避免 MenuModel 关系解析失败
|
||||
# MenuModel.roles -> "RoleModel"(字符串引用),若未导入会导致 mapper 初始化报错
|
||||
from app.api.v1.module_system.role.model import RoleModel, RoleMenusModel # noqa: F401
|
||||
from app.api.v1.module_system.position.model import PositionModel # noqa: F401
|
||||
from app.api.v1.module_system.dept.model import DeptModel # noqa: F401
|
||||
from app.api.v1.module_system.user.model import UserModel, UserRolesModel # noqa: F401
|
||||
|
||||
|
||||
# 固定父节点(用户提供):壹梵后台管理
|
||||
ROOT_PARENT_ID = 131
|
||||
|
||||
|
||||
async def _create_if_not_exists(auth: AuthSchema, data: dict, *, unique_by: dict) -> int:
|
||||
"""
|
||||
幂等创建菜单:
|
||||
- unique_by: 用于判重的字段(如 {"route_path": "/x", "type": 2} 或 {"permission": "...", "type": 3})
|
||||
"""
|
||||
crud = MenuCRUD(auth)
|
||||
# 禁用默认预加载,避免在脚本上下文触发复杂关系初始化
|
||||
exists = await crud.get(preload=[], **unique_by)
|
||||
if exists:
|
||||
return exists.id
|
||||
created = await crud.create(data=MenuCreateSchema(**data))
|
||||
return created.id
|
||||
|
||||
|
||||
async def init_yifan_membership_menus(parent_id: int = ROOT_PARENT_ID) -> dict:
|
||||
"""
|
||||
初始化会员功能菜单(挂到 parent_id 下)。
|
||||
|
||||
返回创建结果的 id 汇总。
|
||||
"""
|
||||
async with async_db_session() as session:
|
||||
async with session.begin():
|
||||
# 0) 运行前检查:sys_menu 是否存在(否则无法初始化菜单)
|
||||
conn = await session.connection()
|
||||
insp = sa.inspect(conn.sync_connection)
|
||||
if not insp.has_table("sys_menu"):
|
||||
raise RuntimeError(
|
||||
"当前数据库缺少表 sys_menu,无法初始化菜单。请先初始化数据库结构(运行项目初始化/迁移脚本),"
|
||||
"确保 sys_menu/sys_role/sys_user 等核心表已创建。"
|
||||
)
|
||||
|
||||
auth = AuthSchema(db=session, check_data_scope=False, user=None)
|
||||
|
||||
# 1) 创建父目录:会员功能(type=1)
|
||||
membership_dir = {
|
||||
"name": "yifan_membership",
|
||||
"type": 1,
|
||||
"icon": "menu",
|
||||
"order": 10,
|
||||
"permission": None,
|
||||
"route_name": "YifanMembership",
|
||||
"route_path": "/yifan/membership",
|
||||
"component_path": None,
|
||||
"redirect": "/yifan/membership/members",
|
||||
"parent_id": parent_id,
|
||||
"keep_alive": True,
|
||||
"hidden": False,
|
||||
"always_show": True,
|
||||
"title": "会员功能",
|
||||
"params": [],
|
||||
"affix": False,
|
||||
"status": "0",
|
||||
"description": "脚本自动创建",
|
||||
}
|
||||
PARENT_ID = await _create_if_not_exists(
|
||||
auth,
|
||||
membership_dir,
|
||||
unique_by={"route_path": "/yifan/membership", "type": 1},
|
||||
)
|
||||
|
||||
# 2) 三个页面菜单(type=2)
|
||||
discount_menu = {
|
||||
"name": "yifan_membership_discount",
|
||||
"type": 2,
|
||||
"icon": "setting",
|
||||
"order": 1,
|
||||
"permission": "module_yifan:yifan_membership_discount:query",
|
||||
"route_name": "YifanMembershipDiscount",
|
||||
"route_path": "/yifan/membership/discount",
|
||||
"component_path": "module_yifan/yifan_membership_discount/index",
|
||||
"redirect": "",
|
||||
"parent_id": PARENT_ID,
|
||||
"keep_alive": True,
|
||||
"hidden": False,
|
||||
"always_show": False,
|
||||
"title": "会员折扣设置",
|
||||
"params": [],
|
||||
"affix": False,
|
||||
"status": "0",
|
||||
"description": "脚本自动创建",
|
||||
}
|
||||
DISCOUNT_MENU_ID = await _create_if_not_exists(
|
||||
auth,
|
||||
discount_menu,
|
||||
unique_by={"route_path": "/yifan/membership/discount", "type": 2},
|
||||
)
|
||||
|
||||
member_menu = {
|
||||
"name": "yifan_membership_member",
|
||||
"type": 2,
|
||||
"icon": "user",
|
||||
"order": 2,
|
||||
"permission": "module_yifan:yifan_membership_member:query",
|
||||
"route_name": "YifanMembershipMember",
|
||||
"route_path": "/yifan/membership/members",
|
||||
"component_path": "module_yifan/yifan_membership_member/index",
|
||||
"redirect": "",
|
||||
"parent_id": PARENT_ID,
|
||||
"keep_alive": True,
|
||||
"hidden": False,
|
||||
"always_show": False,
|
||||
"title": "会员列表",
|
||||
"params": [],
|
||||
"affix": False,
|
||||
"status": "0",
|
||||
"description": "脚本自动创建",
|
||||
}
|
||||
MEMBER_MENU_ID = await _create_if_not_exists(
|
||||
auth,
|
||||
member_menu,
|
||||
unique_by={"route_path": "/yifan/membership/members", "type": 2},
|
||||
)
|
||||
|
||||
user_center_menu = {
|
||||
"name": "yifan_membership_user_center",
|
||||
"type": 2,
|
||||
"icon": "user",
|
||||
"order": 3,
|
||||
"permission": "module_yifan:yifan_membership_user_center:query",
|
||||
"route_name": "YifanMembershipUserCenter",
|
||||
"route_path": "/yifan/membership/user-center",
|
||||
"component_path": "module_yifan/yifan_membership_user_center/index",
|
||||
"redirect": "",
|
||||
"parent_id": PARENT_ID,
|
||||
"keep_alive": True,
|
||||
"hidden": False,
|
||||
"always_show": False,
|
||||
"title": "用户中心(会员权益)",
|
||||
"params": [],
|
||||
"affix": False,
|
||||
"status": "0",
|
||||
"description": "脚本自动创建",
|
||||
}
|
||||
USER_CENTER_MENU_ID = await _create_if_not_exists(
|
||||
auth,
|
||||
user_center_menu,
|
||||
unique_by={"route_path": "/yifan/membership/user-center", "type": 2},
|
||||
)
|
||||
|
||||
# 3) 按钮权限点(type=3,挂到对应页面菜单下)
|
||||
discount_btn = {
|
||||
"name": "修改折扣",
|
||||
"type": 3,
|
||||
"icon": None,
|
||||
"order": 1,
|
||||
"permission": "module_yifan:yifan_membership_discount:update",
|
||||
"route_name": None,
|
||||
"route_path": None,
|
||||
"component_path": None,
|
||||
"redirect": "",
|
||||
"parent_id": DISCOUNT_MENU_ID,
|
||||
"keep_alive": False,
|
||||
"hidden": True,
|
||||
"always_show": False,
|
||||
"title": "修改折扣",
|
||||
"params": [],
|
||||
"affix": False,
|
||||
"status": "0",
|
||||
"description": "脚本自动创建",
|
||||
}
|
||||
DISCOUNT_UPDATE_BTN_ID = await _create_if_not_exists(
|
||||
auth,
|
||||
discount_btn,
|
||||
unique_by={"permission": "module_yifan:yifan_membership_discount:update", "type": 3},
|
||||
)
|
||||
|
||||
quota_btn = {
|
||||
"name": "名额调整",
|
||||
"type": 3,
|
||||
"icon": None,
|
||||
"order": 1,
|
||||
"permission": "module_yifan:yifan_membership_member:quota_adjust",
|
||||
"route_name": None,
|
||||
"route_path": None,
|
||||
"component_path": None,
|
||||
"redirect": "",
|
||||
"parent_id": MEMBER_MENU_ID,
|
||||
"keep_alive": False,
|
||||
"hidden": True,
|
||||
"always_show": False,
|
||||
"title": "名额调整",
|
||||
"params": [],
|
||||
"affix": False,
|
||||
"status": "0",
|
||||
"description": "脚本自动创建",
|
||||
}
|
||||
MEMBER_QUOTA_BTN_ID = await _create_if_not_exists(
|
||||
auth,
|
||||
quota_btn,
|
||||
unique_by={"permission": "module_yifan:yifan_membership_member:quota_adjust", "type": 3},
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
|
||||
return {
|
||||
"parent_dir_id": PARENT_ID,
|
||||
"discount_menu_id": DISCOUNT_MENU_ID,
|
||||
"member_menu_id": MEMBER_MENU_ID,
|
||||
"user_center_menu_id": USER_CENTER_MENU_ID,
|
||||
"discount_update_button_id": DISCOUNT_UPDATE_BTN_ID,
|
||||
"member_quota_adjust_button_id": MEMBER_QUOTA_BTN_ID,
|
||||
}
|
||||
|
||||
|
||||
async def _amain() -> None:
|
||||
result = await init_yifan_membership_menus()
|
||||
log.info(f"✅ 会员功能菜单初始化完成: {result}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
asyncio.run(_amain())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
189
后端源码/yifan.action-ai.cn/api/app/scripts/initialize.py
Normal file
189
后端源码/yifan.action-ai.cn/api/app/scripts/initialize.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config.path_conf import SCRIPT_DIR
|
||||
from app.core.logger import log
|
||||
from app.core.database import async_db_session, async_engine
|
||||
from app.core.base_model import MappedBase
|
||||
|
||||
from app.api.v1.module_system.user.model import UserModel, UserRolesModel
|
||||
from app.api.v1.module_system.role.model import RoleModel
|
||||
from app.api.v1.module_system.dept.model import DeptModel
|
||||
from app.api.v1.module_system.menu.model import MenuModel
|
||||
from app.api.v1.module_system.params.model import ParamsModel
|
||||
from app.api.v1.module_system.dict.model import DictTypeModel, DictDataModel
|
||||
|
||||
|
||||
class InitializeData:
|
||||
"""
|
||||
初始化数据库和基础数据
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
初始化数据库和基础数据
|
||||
"""
|
||||
# 按照依赖关系排序:先创建基础表,再创建关联表
|
||||
self.prepare_init_models = [
|
||||
MenuModel,
|
||||
ParamsModel,
|
||||
DeptModel,
|
||||
RoleModel,
|
||||
DictTypeModel,
|
||||
DictDataModel,
|
||||
UserModel,
|
||||
UserRolesModel,
|
||||
]
|
||||
|
||||
async def __init_create_table(self) -> None:
|
||||
"""
|
||||
初始化表结构(第一阶段)
|
||||
"""
|
||||
try:
|
||||
# 使用引擎创建所有表
|
||||
async with async_engine.begin() as conn:
|
||||
await conn.run_sync(MappedBase.metadata.create_all)
|
||||
log.info("✅️ 数据库表结构初始化完成")
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
log.error("❌️ 数据库表结构初始化超时")
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"❌️ 数据库表结构初始化失败: {str(e)}")
|
||||
raise
|
||||
|
||||
async def __init_data(self, db: AsyncSession) -> None:
|
||||
"""
|
||||
初始化基础数据
|
||||
|
||||
参数:
|
||||
- db (AsyncSession): 异步数据库会话。
|
||||
"""
|
||||
# 存储字典类型数据的映射,用于后续字典数据的初始化
|
||||
dict_type_mapping = {}
|
||||
|
||||
for model in self.prepare_init_models:
|
||||
table_name = model.__tablename__
|
||||
|
||||
# 检查表中是否已经有数据
|
||||
count_result = await db.execute(select(func.count()).select_from(model))
|
||||
existing_count = count_result.scalar()
|
||||
if existing_count and existing_count > 0:
|
||||
log.warning(f"⚠️ 跳过 {table_name} 表数据初始化(表已存在 {existing_count} 条记录)")
|
||||
continue
|
||||
|
||||
data = await self.__get_data(table_name)
|
||||
if not data:
|
||||
log.warning(f"⚠️ 跳过 {table_name} 表,无初始化数据")
|
||||
continue
|
||||
|
||||
try:
|
||||
# 特殊处理具有嵌套 children 数据的表
|
||||
if table_name in ["sys_dept", "sys_menu"]:
|
||||
# 获取对应的模型类
|
||||
model_class = DeptModel if table_name == "sys_dept" else MenuModel
|
||||
objs = self.__create_objects_with_children(data, model_class)
|
||||
# 处理字典类型表,保存类型映射
|
||||
elif table_name == "sys_dict_type":
|
||||
objs = []
|
||||
for item in data:
|
||||
obj = model(**item)
|
||||
objs.append(obj)
|
||||
dict_type_mapping[item['dict_type']] = obj
|
||||
# 处理字典数据表,添加dict_type_id关联
|
||||
elif table_name == "sys_dict_data":
|
||||
objs = []
|
||||
for item in data:
|
||||
dict_type = item.get('dict_type')
|
||||
if dict_type in dict_type_mapping:
|
||||
# 添加dict_type_id关联
|
||||
item['dict_type_id'] = dict_type_mapping[dict_type].id
|
||||
else:
|
||||
log.warning(f"⚠️ 未找到字典类型 {dict_type},跳过该字典数据")
|
||||
continue
|
||||
objs.append(model(**item))
|
||||
else:
|
||||
# 表为空,直接插入全部数据
|
||||
objs = [model(**item) for item in data]
|
||||
|
||||
db.add_all(objs)
|
||||
await db.flush()
|
||||
log.info(f"✅️ 已向 {table_name} 表写入初始化数据")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"❌️ 初始化 {table_name} 表数据失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def __create_objects_with_children(self, data: list[dict], model_class) -> list:
|
||||
"""
|
||||
通用递归创建对象函数,处理嵌套的 children 数据
|
||||
|
||||
参数:
|
||||
- data (list[dict]): 包含嵌套 children 数据的列表。
|
||||
- model_class: 对应的 SQLAlchemy 模型类。
|
||||
|
||||
返回:
|
||||
- list: 包含创建的对象的列表。
|
||||
"""
|
||||
objs = []
|
||||
|
||||
def create_object(obj_data: dict):
|
||||
# 分离 children 数据
|
||||
children_data = obj_data.pop('children', [])
|
||||
|
||||
# 创建当前对象
|
||||
obj = model_class(**obj_data)
|
||||
|
||||
# 递归处理子对象
|
||||
if children_data:
|
||||
obj.children = [create_object(child) for child in children_data]
|
||||
|
||||
return obj
|
||||
|
||||
for item in data:
|
||||
objs.append(create_object(item))
|
||||
|
||||
return objs
|
||||
|
||||
async def __get_data(self, filename: str) -> list[dict]:
|
||||
"""
|
||||
读取初始化数据文件
|
||||
|
||||
参数:
|
||||
- filename (str): 文件名(不包含扩展名)。
|
||||
|
||||
返回:
|
||||
- list[dict]: 解析后的 JSON 数据列表。
|
||||
"""
|
||||
json_path = SCRIPT_DIR / f'{filename}.json'
|
||||
if not json_path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
return json.loads(f.read())
|
||||
except json.JSONDecodeError as e:
|
||||
log.error(f"❌️ 解析 {json_path} 失败: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"❌️ 读取 {json_path} 失败: {str(e)}")
|
||||
raise
|
||||
|
||||
async def init_db(self) -> None:
|
||||
"""
|
||||
执行完整初始化流程
|
||||
"""
|
||||
# 先创建表结构
|
||||
await self.__init_create_table()
|
||||
|
||||
# 再初始化数据
|
||||
async with async_db_session() as session:
|
||||
async with session.begin():
|
||||
await self.__init_data(session)
|
||||
# session.add_all(objs)
|
||||
# 确保提交事务
|
||||
await session.commit()
|
||||
|
||||
Reference in New Issue
Block a user