Skip to content
Merged

sync #66

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
- id: check-toml

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.14.10
rev: v0.14.13
hooks:
- id: ruff-check
args:
Expand All @@ -21,7 +21,7 @@ repos:
- id: ruff-format

- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.9.18
rev: 0.9.26
hooks:
- id: uv-lock
- id: uv-export
Expand Down
1 change: 1 addition & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ ignore = [
"RUF003",
"RUF006",
"RUF012",
"RUF067",
"TRY400",
"TRY003",
"TRY301"
Expand Down
3 changes: 1 addition & 2 deletions backend/app/admin/api/v1/sys/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,11 @@ async def get_userinfo(
)
async def get_users_paginated(
db: CurrentSession,
dept: Annotated[int | None, Query(description='部门 ID')] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
phone: Annotated[str | None, Query(description='手机号')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
page_data = await user_service.get_list(db=db, dept=dept, username=username, phone=phone, status=status)
page_data = await user_service.get_list(db=db, username=username, phone=phone, status=status)
return response_base.success(data=page_data)


Expand Down
5 changes: 1 addition & 4 deletions backend/app/admin/crud/crud_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,17 @@ async def check_email(self, db: AsyncSession, email: str) -> User | None:
"""
return await self.select_model_by_column(db, email=email)

async def get_select(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
async def get_select(self, username: str | None, phone: str | None, status: int | None) -> Select:
"""
获取用户列表查询表达式

:param dept: 部门 ID
:param username: 用户名
:param phone: 电话号码
:param status: 用户状态
:return:
"""
filters = {}

if dept:
filters['dept_id'] = dept
if username:
filters['username__like'] = f'%{username}%'
if phone:
Expand Down
5 changes: 2 additions & 3 deletions backend/app/admin/service/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,17 @@ async def get_userinfo(*, db: AsyncSession, pk: int | None = None, username: str
return user

@staticmethod
async def get_list(*, db: AsyncSession, dept: int, username: str, phone: str, status: int) -> dict[str, Any]:
async def get_list(*, db: AsyncSession, username: str, phone: str, status: int) -> dict[str, Any]:
"""
获取用户列表

:param db: 数据库会话
:param dept: 部门 ID
:param username: 用户名
:param phone: 手机号
:param status: 状态
:return:
"""
user_select = await user_dao.get_select(dept=dept, username=username, phone=phone, status=status)
user_select = await user_dao.get_select(username=username, phone=phone, status=status)
data = await paging_data(db, user_select)
if data['items']:
serialized_items = select_join_serialize(data['items'])
Expand Down
11 changes: 10 additions & 1 deletion backend/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from rich.text import Text
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
from watchfiles import PythonFilter
from watchfiles import Change, PythonFilter

from backend import __version__
from backend.common.enums import DataBaseType, PrimaryKeyType
Expand All @@ -29,6 +29,7 @@
ENV_FILE_PATH,
MYSQL_SCRIPT_DIR,
POSTGRESQL_SCRIPT_DIR,
RELOAD_LOCK_FILE,
)
from backend.database.db import (
async_db_session,
Expand All @@ -51,6 +52,11 @@ class CustomReloadFilter(PythonFilter):
def __init__(self) -> None:
super().__init__(extra_extensions=['.json', '.yaml', '.yml'])

def __call__(self, change: Change, path: str) -> bool:
if RELOAD_LOCK_FILE.exists():
return False
return super().__call__(change, path)


def setup_env_file() -> bool:
if not ENV_EXAMPLE_FILE_PATH.exists():
Expand Down Expand Up @@ -307,6 +313,9 @@ async def install_plugin(
db_type: DataBaseType,
pk_type: PrimaryKeyType,
) -> None:
if settings.ENVIRONMENT != 'dev':
raise cappa.Exit('插件安装仅在开发环境可用', code=1)

if not path and not repo_url:
raise cappa.Exit('path 或 repo_url 必须指定其中一项', code=1)
if path and repo_url:
Expand Down
7 changes: 7 additions & 0 deletions backend/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ class FileType(StrEnum):
video = 'video'


class PluginLevelType(StrEnum):
"""插件级别类型"""

app = 'app'
extend = 'extend'


class PluginType(StrEnum):
"""插件类型"""

Expand Down
17 changes: 15 additions & 2 deletions backend/core/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from typing import Any, Literal

from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict

from backend.core.path_conf import ENV_EXAMPLE_FILE_PATH, ENV_FILE_PATH
from backend.plugin.settings_source import PluginSettingsSource


class Settings(BaseSettings):
Expand All @@ -16,10 +17,22 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=ENV_FILE_PATH,
env_file_encoding='utf-8',
extra='ignore',
extra='allow',
case_sensitive=True,
)

@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
"""自定义配置源优先级"""
return env_settings, dotenv_settings, PluginSettingsSource(settings_cls)

# .env 当前环境
ENVIRONMENT: Literal['dev', 'prod']

Expand Down
3 changes: 3 additions & 0 deletions backend/core/path_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@

# PostgreSQL 脚本目录
POSTGRESQL_SCRIPT_DIR = BASE_PATH / 'sql' / 'postgresql'

# 热重载锁文件
RELOAD_LOCK_FILE = BASE_PATH / '.reload.lock'
4 changes: 2 additions & 2 deletions backend/plugin/config/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## 参数配置
# Config

内置插件,可直接使用
参数配置插件,通常用于动态配置系统参数和前端工程数据展示
4 changes: 3 additions & 1 deletion backend/plugin/config/plugin.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
[plugin]
summary = '参数配置'
version = '0.0.2'
description = '通常用于动态配置系统参数/前端工程数据展示'
description = '通常用于动态配置系统参数和前端工程数据展示'
author = 'wu-clan'
tags = ['other']
database = ['mysql', 'postgresql']

[app]
extend = 'admin'
Expand Down
2 changes: 1 addition & 1 deletion backend/plugin/config/sql/mysql/init.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
insert into sys_config (id, name, type, `key`, value, is_frontend, remark, created_time, updated_time)
values
(1, '状态', 'EMAIL', 'EMAIL_STATUS', '1', false, null, now(), null),
(1, '状态', 'EMAIL', 'EMAIL_CONFIG_STATUS', '1', false, null, now(), null),
(2, '服务器地址', 'EMAIL', 'EMAIL_HOST', 'smtp.qq.com', false, null, now(), null),
(3, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(4, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
Expand Down
2 changes: 1 addition & 1 deletion backend/plugin/config/sql/mysql/init_snowflake.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
insert into sys_config (id, name, type, `key`, value, is_frontend, remark, created_time, updated_time)
values
(2069061886627938304, '状态', 'EMAIL', 'EMAIL_STATUS', '1', false, null, now(), null),
(2069061886627938304, '状态', 'EMAIL', 'EMAIL_CONFIG_STATUS', '1', false, null, now(), null),
(2069061886627938305, '服务器地址', 'EMAIL', 'EMAIL_HOST', 'smtp.qq.com', false, null, now(), null),
(2069061886627938306, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(2069061886627938307, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
Expand Down
2 changes: 1 addition & 1 deletion backend/plugin/config/sql/postgresql/init.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
insert into sys_config (id, name, type, "key", value, is_frontend, remark, created_time, updated_time)
values
(1, '状态', 'EMAIL', 'EMAIL_STATUS', '1', false, null, now(), null),
(1, '状态', 'EMAIL', 'EMAIL_CONFIG_STATUS', '1', false, null, now(), null),
(2, '服务器地址', 'EMAIL', 'EMAIL_HOST', 'smtp.qq.com', false, null, now(), null),
(3, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(4, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
Expand Down
2 changes: 1 addition & 1 deletion backend/plugin/config/sql/postgresql/init_snowflake.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
insert into sys_config (id, name, type, "key", value, is_frontend, remark, created_time, updated_time)
values
(2069061886627938304, '状态', 'EMAIL', 'EMAIL_STATUS', '1', false, null, now(), null),
(2069061886627938304, '状态', 'EMAIL', 'EMAIL_CONFIG_STATUS', '1', false, null, now(), null),
(2069061886627938305, '服务器地址', 'EMAIL', 'EMAIL_HOST', 'smtp.qq.com', false, null, now(), null),
(2069061886627938306, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(2069061886627938307, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
Expand Down
31 changes: 6 additions & 25 deletions backend/plugin/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,18 @@

from fastapi import APIRouter, Depends, Request

from backend.common.enums import DataBaseType, PrimaryKeyType, StatusType
from backend.common.enums import DataBaseType, PluginLevelType, PrimaryKeyType, StatusType
from backend.common.exception import errors
from backend.common.log import log
from backend.core.conf import settings
from backend.core.path_conf import PLUGIN_DIR
from backend.database.redis import RedisCli, redis_client
from backend.plugin.errors import PluginConfigError, PluginInjectError
from backend.plugin.validator import validate_plugin_config
from backend.utils.async_helper import run_await
from backend.utils.dynamic_import import get_model_objects, import_module_cached


class PluginConfigError(Exception):
"""插件信息错误"""


class PluginInjectError(Exception):
"""插件注入错误"""


@lru_cache
def get_plugins() -> list[str]:
"""获取插件列表"""
Expand Down Expand Up @@ -105,7 +99,6 @@ def load_plugin_config(plugin: str) -> dict[str, Any]:

def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""解析插件配置"""

extend_plugins = []
app_plugins = []

Expand All @@ -123,32 +116,20 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:

for plugin in plugins:
data = load_plugin_config(plugin)
plugin_type = validate_plugin_config(plugin, data)

plugin_info = data.get('plugin')
if not plugin_info:
raise PluginConfigError(f'插件 {plugin} 配置文件缺少 plugin 配置')

required_fields = ['summary', 'version', 'description', 'author']
missing_fields = [field for field in required_fields if field not in plugin_info]
if missing_fields:
raise PluginConfigError(f'插件 {plugin} 配置文件缺少必要字段: {", ".join(missing_fields)}')

if data.get('api'):
if not data.get('app', {}).get('extend'):
raise PluginConfigError(f'扩展级插件 {plugin} 配置文件缺少 app.extend 配置')
if plugin_type == PluginLevelType.extend:
extend_plugins.append(data)
else:
if not data.get('app', {}).get('router'):
raise PluginConfigError(f'应用级插件 {plugin} 配置文件缺少 app.router 配置')
app_plugins.append(data)

# 补充插件信息
data['plugin']['name'] = plugin
plugin_cache_info = run_await(current_redis_client.get)(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
if plugin_cache_info:
data['plugin']['enable'] = json.loads(plugin_cache_info)['plugin']['enable']
else:
data['plugin']['enable'] = str(StatusType.enable.value)
data['plugin']['name'] = plugin

# 缓存最新插件信息
run_await(current_redis_client.set)(
Expand Down
10 changes: 10 additions & 0 deletions backend/plugin/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class PluginConfigError(Exception):
"""插件信息错误"""


class PluginInjectError(Exception):
"""插件注入错误"""


class PluginInstallError(Exception):
"""插件安装错误"""
Loading