refactor: 统一项目代码风格并修复多处类型与调用问题

本次提交包含多项优化:
1.  移除大量冗余的文件头注释与过时的from __future__导入
2.  将CRUD的list方法统一重命名为get_list保持接口一致
3.  修复前后端状态字段类型不匹配问题,将string类型status改为number
4.  修正前端文案错别字,将"代办事项"修正为标准写法
5.  更新sqlalchemy版本并调整依赖配置
6.  新增缓存工具类替代fastapi-cache2,重构缓存调用逻辑
7.  新增开源授权函生成相关工具与数据库字段支持
8.  为多个业务模块添加防重复提交loading状态
9.  修复邮件模型的外键关联缺失问题
10. 优化pdf生成工具的导入时机与文档注释
This commit is contained in:
zhangtao
2026-06-21 17:34:11 +08:00
parent 3169ef2355
commit 7d2367e34e
106 changed files with 1968 additions and 966 deletions
+4
View File
@@ -116,6 +116,10 @@ ruff check --watch
uv run ruff check
uv run ruff check --fix
uv run ruff check --watch
# 生成开源授权函 JSON 文件
#uv run --with pip-licenses pip-licenses --format=json \
> app/api/v1/module_platform/invoice/oss_licenses.json
```
## 后端约定(日期与序列化)
@@ -1,7 +1,3 @@
"""健康检查 Schema"""
from __future__ import annotations
from pydantic import BaseModel, Field
@@ -1,3 +1,4 @@
import platform
import socket
import time
@@ -95,7 +95,13 @@ class EmailLogModel(ModelMixin, TenantMixin, UserMixin):
biz_type: Mapped[str] = mapped_column(String(50), nullable=False, default="other", comment="业务类型(register/reset_password/invite/expiry_warning/ticket_reply/other")
error_msg: Mapped[str | None] = mapped_column(Text, nullable=True, default=None, comment="失败原因")
retry_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="重试次数")
tenant_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True, comment="关联租户 ID(可为空,如平台注册邮件)")
tenant_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("platform_tenant.id", ondelete="SET NULL", onupdate="CASCADE"),
nullable=True,
index=True,
comment="关联租户 ID(可为空,如平台注册邮件)",
)
sent_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None, comment="实际发送时间")
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
@@ -67,7 +67,7 @@ class EmailConfigService:
if not ids:
raise CustomException(msg="删除对象不能为空")
crud = EmailConfigCRUD(self.auth)
configs = await crud.list(search={"id": ("in", ids)})
configs = await crud.get_list(search={"id": ("in", ids)})
for obj in configs:
if obj.is_default:
raise CustomException(msg=f"配置「{obj.name}」是默认配置,请先将其他配置设为默认后再删除")
@@ -42,13 +42,26 @@ async def invoice_list_my_controller(
)
return SuccessResponse(data=result, msg="查询成功")
@TenantInvoiceRouter.get("/{id}/download", summary="下载发票PDF", response_model=ResponseSchema[dict])
@TenantInvoiceRouter.get("/{id}/download", summary="下载发票PDF与授权函", response_model=ResponseSchema[dict])
async def invoice_download_controller(
id: Annotated[int, Path(ge=1)],
auth: Annotated[AuthSchema, Depends(AuthPermission(["*:*:*"]))],
) -> JSONResponse:
pdf_url = await InvoiceTenantService.download(auth, id, auth.tenant_id)
return SuccessResponse(msg="下载地址", data={"pdf_url": pdf_url})
oss_license_pdf_url = await InvoiceTenantService.download_license(auth, id, auth.tenant_id)
return SuccessResponse(
msg="下载地址",
data={"pdf_url": pdf_url, "oss_license_pdf_url": oss_license_pdf_url},
)
@TenantInvoiceRouter.get("/{id}/license/download", summary="下载开源授权函PDF", response_model=ResponseSchema[dict])
async def invoice_license_download_controller(
id: Annotated[int, Path(ge=1)],
auth: Annotated[AuthSchema, Depends(AuthPermission(["*:*:*"]))],
) -> JSONResponse:
oss_license_pdf_url = await InvoiceTenantService.download_license(auth, id, auth.tenant_id)
return SuccessResponse(msg="授权函下载地址", data={"oss_license_pdf_url": oss_license_pdf_url})
PlatformInvoiceRouter = APIRouter(prefix="/invoice", route_class=OperationLogRoute, tags=["平台管理", "发票管理"])
@@ -78,6 +91,7 @@ async def invoice_issue_controller(
id,
data.pdf_url or "",
data.api_response or "",
data.oss_license_pdf_url or "",
)
return SuccessResponse(data=result, msg="发票开具成功")
@@ -29,7 +29,8 @@ class InvoiceModel(ModelMixin, TenantMixin, UserMixin):
address_info: Mapped[str | None] = mapped_column(Text, nullable=True, comment="注册地址及电话")
amount: Mapped[int] = mapped_column(Integer, nullable=False, comment="发票金额(分)")
tax_amount: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="税额(分)")
pdf_url: Mapped[str | None] = mapped_column(String(500), nullable=True, comment="PDF下载地址")
pdf_url: Mapped[str | None] = mapped_column(String(500), nullable=True, comment="发票PDF下载地址")
oss_license_pdf_url: Mapped[str | None] = mapped_column(String(500), nullable=True, comment="开源授权函PDF下载地址")
api_response: Mapped[str | None] = mapped_column(Text, nullable=True, comment="第三方API响应")
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:待开票 1:已开票 2:开票失败 3:已作废)", index=True)
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
@@ -0,0 +1,527 @@
[
{
"License": "MIT License",
"Name": "APScheduler",
"Version": "3.11.0"
},
{
"License": "BSD-3-Clause",
"Name": "GitPython",
"Version": "3.1.46"
},
{
"License": "BSD License",
"Name": "Jinja2",
"Version": "3.1.6"
},
{
"License": "MIT License",
"Name": "Mako",
"Version": "1.3.10"
},
{
"License": "BSD-3-Clause",
"Name": "MarkupSafe",
"Version": "3.0.3"
},
{
"License": "MIT",
"Name": "PyJWT",
"Version": "2.13.0"
},
{
"License": "MIT",
"Name": "PyMySQL",
"Version": "1.2.0"
},
{
"License": "MIT License",
"Name": "PyYAML",
"Version": "6.0.3"
},
{
"License": "BSD License",
"Name": "Pygments",
"Version": "2.19.2"
},
{
"License": "MIT",
"Name": "SQLAlchemy",
"Version": "2.0.45"
},
{
"License": "Apache Software License",
"Name": "agno",
"Version": "2.5.8"
},
{
"License": "Apache Software License",
"Name": "aiofiles",
"Version": "24.1.0"
},
{
"License": "MIT License",
"Name": "aioredis",
"Version": "2.0.1"
},
{
"License": "MIT",
"Name": "aiosmtplib",
"Version": "3.0.2"
},
{
"License": "MIT License",
"Name": "aiosqlite",
"Version": "0.22.1"
},
{
"License": "MIT",
"Name": "alembic",
"Version": "1.18.4"
},
{
"License": "MIT",
"Name": "annotated-doc",
"Version": "0.0.4"
},
{
"License": "MIT License",
"Name": "annotated-types",
"Version": "0.7.0"
},
{
"License": "MIT",
"Name": "anyio",
"Version": "4.12.1"
},
{
"License": "Apache Software License",
"Name": "async-timeout",
"Version": "5.0.1"
},
{
"License": "Apache-2.0",
"Name": "asyncmy",
"Version": "0.2.11"
},
{
"License": "Apache-2.0",
"Name": "asyncpg",
"Version": "0.31.0"
},
{
"License": "Apache Software License",
"Name": "bleach",
"Version": "6.4.0"
},
{
"License": "MIT License",
"Name": "blinker",
"Version": "1.9.0"
},
{
"License": "MIT",
"Name": "brotli",
"Version": "1.2.0"
},
{
"License": "Mozilla Public License 2.0 (MPL 2.0)",
"Name": "certifi",
"Version": "2026.1.4"
},
{
"License": "MIT",
"Name": "cffi",
"Version": "2.0.0"
},
{
"License": "BSD License",
"Name": "click",
"Version": "8.1.7"
},
{
"License": "MIT License",
"Name": "croniter",
"Version": "4.0.0"
},
{
"License": "Apache-2.0 OR BSD-3-Clause",
"Name": "cryptography",
"Version": "49.0.0"
},
{
"License": "BSD License",
"Name": "cssselect2",
"Version": "0.9.0"
},
{
"License": "Apache Software License",
"Name": "distro",
"Version": "1.9.0"
},
{
"License": "ISC License (ISCL)",
"Name": "dnspython",
"Version": "2.8.0"
},
{
"License": "MIT License",
"Name": "docstring_parser",
"Version": "0.17.0"
},
{
"License": "The Unlicense (Unlicense)",
"Name": "email-validator",
"Version": "2.3.0"
},
{
"License": "MIT License",
"Name": "et_xmlfile",
"Version": "2.0.0"
},
{
"License": "BSD-3-Clause",
"Name": "fakeredis",
"Version": "2.34.1"
},
{
"License": "MIT",
"Name": "fastapi",
"Version": "0.138.0"
},
{
"License": "Apache Software License",
"Name": "fastapi-cache2",
"Version": "0.1.8"
},
{
"License": "Other/Proprietary License",
"Name": "fastapi-limiter",
"Version": "0.1.6"
},
{
"License": "MIT License",
"Name": "fastapi-mail",
"Version": "1.5.1"
},
{
"License": "MIT",
"Name": "fonttools",
"Version": "4.63.0"
},
{
"License": "BSD License",
"Name": "gitdb",
"Version": "4.0.12"
},
{
"License": "MIT AND PSF-2.0",
"Name": "greenlet",
"Version": "3.5.2"
},
{
"License": "MIT License",
"Name": "h11",
"Version": "0.16.0"
},
{
"License": "MIT License",
"Name": "h2",
"Version": "4.3.0"
},
{
"License": "MIT License",
"Name": "hpack",
"Version": "4.1.0"
},
{
"License": "BSD-3-Clause",
"Name": "httpcore",
"Version": "1.0.9"
},
{
"License": "BSD License",
"Name": "httpx",
"Version": "0.28.1"
},
{
"License": "MIT License",
"Name": "hyperframe",
"Version": "6.1.0"
},
{
"License": "BSD-3-Clause",
"Name": "idna",
"Version": "3.11"
},
{
"License": "MIT",
"Name": "iniconfig",
"Version": "2.3.0"
},
{
"License": "MIT License",
"Name": "jiter",
"Version": "0.12.0"
},
{
"License": "MIT License",
"Name": "loguru",
"Version": "0.7.3"
},
{
"License": "MIT License",
"Name": "markdown-it-py",
"Version": "4.0.0"
},
{
"License": "MIT License",
"Name": "mdurl",
"Version": "0.1.2"
},
{
"License": "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0",
"Name": "numpy",
"Version": "2.4.1"
},
{
"License": "Apache Software License",
"Name": "openai",
"Version": "2.28.0"
},
{
"License": "MIT License",
"Name": "openpyxl",
"Version": "3.1.5"
},
{
"License": "Apache Software License; BSD License",
"Name": "packaging",
"Version": "25.0"
},
{
"License": "BSD License",
"Name": "pandas",
"Version": "3.0.3"
},
{
"License": "MIT License",
"Name": "pendulum",
"Version": "3.2.0"
},
{
"License": "MIT-CMU",
"Name": "pillow",
"Version": "12.2.0"
},
{
"License": "MIT License",
"Name": "pluggy",
"Version": "1.6.0"
},
{
"License": "BSD-3-Clause",
"Name": "psutil",
"Version": "7.2.2"
},
{
"License": "LGPL-3.0-only",
"Name": "psycopg",
"Version": "3.3.2"
},
{
"License": "LGPL-3.0-only",
"Name": "psycopg-binary",
"Version": "3.3.2"
},
{
"License": "BSD License",
"Name": "pycparser",
"Version": "2.23"
},
{
"License": "MIT",
"Name": "pydantic",
"Version": "2.12.5"
},
{
"License": "MIT",
"Name": "pydantic-settings",
"Version": "2.14.1"
},
{
"License": "MIT",
"Name": "pydantic_core",
"Version": "2.41.5"
},
{
"License": "BSD License",
"Name": "pydyf",
"Version": "0.12.1"
},
{
"License": "GNU General Public License v2 or later (GPLv2+); GNU Lesser General Public License v2 or later (LGPLv2+); Mozilla Public License 1.1 (MPL 1.1)",
"Name": "pyphen",
"Version": "0.17.2"
},
{
"License": "MIT",
"Name": "pytest",
"Version": "9.0.2"
},
{
"License": "Apache-2.0",
"Name": "pytest-asyncio",
"Version": "1.4.0"
},
{
"License": "Apache Software License; BSD License",
"Name": "python-dateutil",
"Version": "2.9.0.post0"
},
{
"License": "BSD-3-Clause",
"Name": "python-dotenv",
"Version": "1.2.1"
},
{
"License": "Apache-2.0",
"Name": "python-multipart",
"Version": "0.0.32"
},
{
"License": "MIT License",
"Name": "pytz",
"Version": "2025.2"
},
{
"License": "MIT",
"Name": "redis",
"Version": "7.1.0"
},
{
"License": "MIT License",
"Name": "rich",
"Version": "15.0.0"
},
{
"License": "MIT License",
"Name": "ruff",
"Version": "0.14.13"
},
{
"License": "ISC License (ISCL)",
"Name": "shellingham",
"Version": "1.5.4"
},
{
"License": "MIT License",
"Name": "six",
"Version": "1.17.0"
},
{
"License": "BSD License",
"Name": "smmap",
"Version": "5.0.3"
},
{
"License": "Apache Software License; MIT License",
"Name": "sniffio",
"Version": "1.3.1"
},
{
"License": "Apache Software License",
"Name": "sortedcontainers",
"Version": "2.4.0"
},
{
"License": "UNKNOWN",
"Name": "sqlglot",
"Version": "27.8.0"
},
{
"License": "MIT License",
"Name": "sqlglotrs",
"Version": "0.6.1"
},
{
"License": "BSD-3-Clause",
"Name": "starlette",
"Version": "0.52.1"
},
{
"License": "BSD License",
"Name": "tinycss2",
"Version": "1.5.1"
},
{
"License": "MIT License",
"Name": "tinyhtml5",
"Version": "2.1.0"
},
{
"License": "MIT License; Mozilla Public License 2.0 (MPL 2.0)",
"Name": "tqdm",
"Version": "4.67.1"
},
{
"License": "MIT",
"Name": "typer",
"Version": "0.26.7"
},
{
"License": "MIT",
"Name": "typing-inspection",
"Version": "0.4.2"
},
{
"License": "PSF-2.0",
"Name": "typing_extensions",
"Version": "4.15.0"
},
{
"License": "Apache-2.0",
"Name": "tzdata",
"Version": "2025.3"
},
{
"License": "MIT License",
"Name": "tzlocal",
"Version": "5.3.1"
},
{
"License": "Apache-2.0",
"Name": "ua-parser",
"Version": "1.0.2"
},
{
"License": "Apache Software License",
"Name": "ua-parser-builtins",
"Version": "202601"
},
{
"License": "BSD-3-Clause",
"Name": "uvicorn",
"Version": "0.49.0"
},
{
"License": "BSD License",
"Name": "weasyprint",
"Version": "69.0"
},
{
"License": "BSD License",
"Name": "webencodings",
"Version": "0.5.1"
},
{
"License": "BSD-3-Clause",
"Name": "websockets",
"Version": "16.0"
},
{
"License": "Apache Software License",
"Name": "zopfli",
"Version": "0.4.3"
}
]
@@ -0,0 +1,51 @@
"""辅助函数 — 把 pip-licenses 输出的原始 JSON 转换成模板友好的精简数据。
为什么不直接渲染原始 JSON:
- LicenseText 全文可能数 MB
- 模板只需要摘要信息 (Name/Version/License)
- 按 License 分组便于阅读
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import TypedDict
class PackageInfo(TypedDict):
name: str
version: str
license: str
class LicenseGroup(TypedDict):
license: str
packages: list[PackageInfo]
_LICENSES_JSON_PATH = Path(__file__).parent / "oss_licenses.json"
def load_oss_licenses() -> list[LicenseGroup]:
"""
加载项目依赖的开源许可证清单,按许可证类型分组。
返回:
- list[LicenseGroup]: 形如 [{"license": "MIT", "packages": [{"name": ..., "version": ...}, ...]}, ...]
"""
raw = json.loads(_LICENSES_JSON_PATH.read_text(encoding="utf-8"))
groups: dict[str, list[PackageInfo]] = {}
for pkg in raw:
license_name = pkg.get("License") or "Unknown"
groups.setdefault(license_name, []).append(
PackageInfo(
name=pkg["Name"],
version=pkg.get("Version", "-"),
license=license_name,
)
)
sorted_groups: list[LicenseGroup] = []
for license_name in sorted(groups.keys(), key=str.lower):
pkgs = sorted(groups[license_name], key=lambda p: p["name"].lower())
sorted_groups.append(LicenseGroup(license=license_name, packages=pkgs))
return sorted_groups
@@ -11,6 +11,7 @@ from datetime import datetime
from app.config.path_conf import INVOICE_DIR, TEMPLATE_DIR
from app.utils.pdf_generator import amount_to_cn_uppercase, amount_to_yuan, generate_pdf_from_template
from .oss_licenses_helper import load_oss_licenses
from .schema import InvoiceOutSchema
_INVOICE_TYPE_LABEL = {
@@ -73,9 +74,42 @@ def _render_invoice_pdf(invoice: InvoiceOutSchema) -> str:
output_dir = INVOICE_DIR / str(invoice.tenant_id)
output_path = output_dir / f"{invoice.invoice_no}.pdf"
generate_pdf_from_template(
template_name="invoice.jinja2",
template_name="invoice/invoice.jinja2",
template_dir=TEMPLATE_DIR,
variables=variables,
output_path=output_path,
)
return f"/static/invoice/{invoice.tenant_id}/{invoice.invoice_no}.pdf"
def _render_oss_license_pdf(invoice: InvoiceOutSchema) -> str:
"""
渲染并保存开源项目授权声明函 PDF(与发票 PDF 独立存储)
参数:
- invoice (InvoiceOutSchema): 发票对象(用于在授权函中展示关联发票号)
返回:
- str: PDF 的相对 URL 路径(形如 /static/invoice/{tenant_id}/{invoice_no}_license.pdf
"""
groups = load_oss_licenses()
total_packages = sum(len(g["packages"]) for g in groups)
variables = {
"invoice_no": invoice.invoice_no,
"invoice_date": datetime.now().strftime("%Y-%m-%d"),
"product_version": "v1.0.0",
"groups": groups,
"total_packages": total_packages,
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
output_dir = INVOICE_DIR / str(invoice.tenant_id)
output_path = output_dir / f"{invoice.invoice_no}_license.pdf"
generate_pdf_from_template(
template_name="invoice/oss_license.jinja2",
template_dir=TEMPLATE_DIR,
variables=variables,
output_path=output_path,
)
return f"/static/invoice/{invoice.tenant_id}/{invoice.invoice_no}_license.pdf"
@@ -29,7 +29,8 @@ class InvoiceUpdateSchema(BaseModel):
"""更新发票(内部使用)"""
status: int | None = Field(default=None, ge=0, le=3, description="状态(0:待开票 1:已开票 2:开票失败 3:已作废)")
pdf_url: str | None = Field(default=None, max_length=500, description="PDF 下载地址")
pdf_url: str | None = Field(default=None, max_length=500, description="发票 PDF 下载地址")
oss_license_pdf_url: str | None = Field(default=None, max_length=500, description="开源授权函 PDF 下载地址")
api_response: str | None = Field(default=None, description="第三方 API 响应")
description: str | None = Field(default=None, description="备注")
@@ -50,7 +51,8 @@ class InvoiceIssueSchema(BaseModel):
"""超管开票"""
api_response: str | None = Field(default=None, description="第三方 API 响应(手动填入)")
pdf_url: str | None = Field(default=None, max_length=500, description="PDF 下载地址")
pdf_url: str | None = Field(default=None, max_length=500, description="发票 PDF 下载地址")
oss_license_pdf_url: str | None = Field(default=None, max_length=500, description="开源授权函 PDF 下载地址")
class InvoiceVoidSchema(BaseModel):
@@ -64,7 +66,8 @@ class InvoiceOutSchema(InvoiceCreateSchema, BaseSchema, UserBySchema, TenantBySc
model_config = ConfigDict(from_attributes=True)
pdf_url: str | None = Field(default=None, description="PDF 下载地址")
pdf_url: str | None = Field(default=None, description="发票 PDF 下载地址")
oss_license_pdf_url: str | None = Field(default=None, description="开源授权函 PDF 下载地址")
api_response: str | None = Field(default=None, description="第三方 API 响应")
@@ -8,7 +8,7 @@ from app.core.exceptions import CustomException
from app.core.logger import logger
from .crud import InvoiceCRUD
from .pdf_helper import _render_invoice_pdf
from .pdf_helper import _render_invoice_pdf, _render_oss_license_pdf
from .schema import (
InvoiceApplySchema,
InvoiceCreateSchema,
@@ -134,6 +134,16 @@ class InvoiceTenantService:
raise CustomException(msg="发票未开具或无PDF")
return invoice.pdf_url
@classmethod
async def download_license(cls, auth: AuthSchema, invoice_id: int, tenant_id: int) -> str:
crud = InvoiceCRUD(auth)
invoice = await crud.get_or_404(id=invoice_id, msg="发票不存在")
if hasattr(invoice, "tenant_id") and invoice.tenant_id != tenant_id:
raise CustomException(msg="发票不存在")
if not invoice.oss_license_pdf_url:
raise CustomException(msg="开源授权函 PDF 不存在,请先开票")
return invoice.oss_license_pdf_url
class InvoicePlatformService:
"""平台端发票服务"""
@@ -166,7 +176,14 @@ class InvoicePlatformService:
)
@classmethod
async def issue(cls, auth: AuthSchema, invoice_id: int, pdf_url: str, api_response: str) -> InvoiceOutSchema:
async def issue(
cls,
auth: AuthSchema,
invoice_id: int,
pdf_url: str,
api_response: str,
oss_license_pdf_url: str = "",
) -> InvoiceOutSchema:
"""
平台开具发票
@@ -175,6 +192,7 @@ class InvoicePlatformService:
- invoice_id (int): 发票 ID
- pdf_url (str): PDF 下载地址
- api_response (str): 第三方 API 响应
- oss_license_pdf_url (str): 授权函 PDF 地址(手动模式可空,自动模式自动渲染)
返回:
- InvoiceOutSchema: 发票信息
@@ -186,9 +204,10 @@ class InvoicePlatformService:
if invoice.status != 0:
raise CustomException(msg="仅待开票状态可操作")
# 调用开票服务:本地 WeasyPrint 渲染 PDF(对接百望云/票通时替换为远程调用
# 调用开票服务:本地 WeasyPrint 渲染发票 PDF + 开源授权函 PDF(独立两个文件
try:
pdf_url_result = _render_invoice_pdf(invoice)
oss_license_pdf_url_result = _render_oss_license_pdf(invoice)
api_response_result = json.dumps(
{
"code": "SUCCESS",
@@ -216,6 +235,7 @@ class InvoicePlatformService:
InvoiceUpdateSchema(
status=1,
pdf_url=pdf_url or pdf_url_result,
oss_license_pdf_url=oss_license_pdf_url or oss_license_pdf_url_result,
api_response=api_response or api_response_result,
),
)
@@ -2,11 +2,11 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from app.common.response import ResponseSchema, SuccessResponse
from app.core import cache_util
from app.core.base_schema import AuthSchema, BatchSetAvailable
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
@@ -56,7 +56,7 @@ async def create_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:create"]))],
) -> JSONResponse:
result_dict = await MenuService(auth).create(data=data)
await FastAPICache.clear(namespace=_MENU_NS)
await cache_util.clear(namespace=_MENU_NS)
return SuccessResponse(data=result_dict, msg="创建菜单成功")
@@ -71,7 +71,7 @@ async def update_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:update"]))],
) -> JSONResponse:
result_dict = await MenuService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_MENU_NS)
await cache_util.clear(namespace=_MENU_NS)
return SuccessResponse(data=result_dict, msg="修改菜单成功")
@@ -85,7 +85,7 @@ async def delete_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:delete"]))],
) -> JSONResponse:
await MenuService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_MENU_NS)
await cache_util.clear(namespace=_MENU_NS)
return SuccessResponse(msg="删除菜单成功")
@@ -99,5 +99,5 @@ async def batch_set_available_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:menu:patch"]))],
) -> JSONResponse:
await MenuService(auth).set_available(data=data)
await FastAPICache.clear(namespace=_MENU_NS)
await cache_util.clear(namespace=_MENU_NS)
return SuccessResponse(msg="批量修改菜单状态成功")
@@ -129,7 +129,7 @@ class MenuService:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
all_menus = await MenuCRUD(self.auth).list()
all_menus = await MenuCRUD(self.auth).get_list()
child_id_map = get_child_id_map(model_list=all_menus)
delete_ids_set = set()
@@ -142,7 +142,7 @@ class MenuService:
@require_superadmin
async def set_available(self, data: BatchSetAvailable) -> None:
menu_list = await MenuCRUD(self.auth).list()
menu_list = await MenuCRUD(self.auth).get_list()
total_ids = []
if data.status == 0:
@@ -1,5 +1,3 @@
"""订单与支付 Controller"""
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query, Request
@@ -1,5 +1,3 @@
"""订单与支付 Model"""
from datetime import datetime
from typing import TYPE_CHECKING
@@ -1,7 +1,3 @@
"""订单与支付 Schema"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Literal
@@ -99,14 +95,13 @@ class OrderCreateSchema(BaseModel):
return v
@model_validator(mode="after")
def check_target(self) -> OrderCreateSchema:
def check_target(self) -> None:
if self.order_type == "plugin":
if not self.plugin_id or self.plugin_id <= 0:
raise ValueError("插件订单必须指定 plugin_id")
else:
if not self.package_id or self.package_id <= 0:
raise ValueError("套餐订单必须指定 package_id")
return self
class OrderOutSchema(BaseSchema, TenantBySchema):
@@ -1,5 +1,3 @@
"""订单与支付 Service"""
import random
from datetime import datetime, timedelta
@@ -474,7 +472,7 @@ class PaymentService:
raise CustomException(msg=f"降级失败:当前租户已有 {current}{label},超过目标套餐限额 {limit}")
@classmethod
async def _send_order_email(cls, auth: AuthSchema, order: "OrderModel", product: object, tenant: object, order_type_label: str = "") -> None:
async def _send_order_email(cls, auth: AuthSchema, order: OrderModel, product: object, tenant: object, order_type_label: str = "") -> None:
"""
发送购买确认邮件失败静默降级
@@ -2,12 +2,12 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from app.common.response import ResponseSchema, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
@@ -66,7 +66,7 @@ async def create_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:create"]))],
) -> JSONResponse:
result_dict = await PackageService(auth).create(data=data)
await FastAPICache.clear(namespace=_PKG_NS)
await cache_util.clear(namespace=_PKG_NS)
return SuccessResponse(data=result_dict, msg="创建成功")
@PackageRouter.put(
@@ -80,7 +80,7 @@ async def update_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))],
) -> JSONResponse:
result_dict = await PackageService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_PKG_NS)
await cache_util.clear(namespace=_PKG_NS)
return SuccessResponse(data=result_dict, msg="更新成功")
@PackageRouter.delete(
@@ -93,7 +93,7 @@ async def delete_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:delete"]))],
) -> JSONResponse:
await PackageService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_PKG_NS)
await cache_util.clear(namespace=_PKG_NS)
return SuccessResponse(msg="删除成功")
@PackageRouter.patch(
@@ -107,7 +107,7 @@ async def set_available_controller(
) -> JSONResponse:
for id in data.ids:
await PackageService(auth).update(id=id, data=PackageUpdateSchema(status=data.status))
await FastAPICache.clear(namespace=_PKG_NS)
await cache_util.clear(namespace=_PKG_NS)
return SuccessResponse(msg="状态设置成功")
@PackageRouter.get(
@@ -1,3 +1,4 @@
import sqlalchemy as sa
from sqlalchemy import func, select
@@ -2,12 +2,12 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query
from fastapi.responses import JSONResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from app.common.response import ResponseSchema, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
@@ -67,7 +67,7 @@ async def plugin_create_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:create"]))],
) -> JSONResponse:
r = await PluginService(auth).create(data=data)
await FastAPICache.clear(namespace=_PLUGIN_NS)
await cache_util.clear(namespace=_PLUGIN_NS)
return SuccessResponse(data=r, msg="创建成功")
@@ -82,7 +82,7 @@ async def plugin_update_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:update"]))],
) -> JSONResponse:
r = await PluginService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_PLUGIN_NS)
await cache_util.clear(namespace=_PLUGIN_NS)
return SuccessResponse(data=r, msg="更新成功")
@@ -96,7 +96,7 @@ async def plugin_delete_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:delete"]))],
) -> JSONResponse:
await PluginService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_PLUGIN_NS)
await cache_util.clear(namespace=_PLUGIN_NS)
return SuccessResponse(msg="删除成功")
@@ -125,7 +125,7 @@ async def plugin_install_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:install"]))],
) -> JSONResponse:
await PluginService(auth).install(plugin_id=data.plugin_id)
await FastAPICache.clear(namespace=_PLUGIN_NS)
await cache_util.clear(namespace=_PLUGIN_NS)
return SuccessResponse(msg="安装成功")
@@ -139,7 +139,7 @@ async def plugin_uninstall_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:uninstall"]))],
) -> JSONResponse:
await PluginService(auth).uninstall(plugin_id=data.plugin_id)
await FastAPICache.clear(namespace=_PLUGIN_NS)
await cache_util.clear(namespace=_PLUGIN_NS)
return SuccessResponse(msg="卸载成功")
@@ -153,7 +153,7 @@ async def plugin_toggle_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:toggle"]))],
) -> JSONResponse:
await PluginService(auth).toggle(plugin_id=data.plugin_id)
await FastAPICache.clear(namespace=_PLUGIN_NS)
await cache_util.clear(namespace=_PLUGIN_NS)
return SuccessResponse(msg="操作成功")
@@ -178,5 +178,5 @@ async def plugin_reload_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_platform:plugin:reload"]))],
) -> JSONResponse:
msg = PluginService.reload()
await FastAPICache.clear(namespace=_PLUGIN_NS)
await cache_util.clear(namespace=_PLUGIN_NS)
return SuccessResponse(data=msg, msg="重载成功")
@@ -1,5 +1,3 @@
"""租户自助服务 Controller — 对应 PRD §20.20"""
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
@@ -1,5 +1,3 @@
"""租户自助服务 Schema"""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
@@ -1,4 +1,3 @@
"""租户自助服务 Service"""
from datetime import datetime, timedelta
@@ -2,13 +2,13 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query
from fastapi.responses import JSONResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from redis.asyncio.client import Redis
from app.common.response import ResponseSchema, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission, redis_getter
from app.core.router_class import OperationLogRoute
@@ -74,7 +74,7 @@ async def create_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:create"]))],
) -> JSONResponse:
result_dict = await TenantService(auth).create(data=data)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(data=result_dict, msg="创建租户成功")
@TenantRouter.put(
@@ -88,7 +88,7 @@ async def update_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))],
) -> JSONResponse:
result_dict = await TenantService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(data=result_dict, msg="修改租户成功")
@TenantRouter.delete(
@@ -101,7 +101,7 @@ async def delete_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:delete"]))],
) -> JSONResponse:
await TenantService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(msg="删除租户成功")
@TenantRouter.patch(
@@ -114,7 +114,7 @@ async def batch_set_available_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:patch"]))],
) -> JSONResponse:
await TenantService(auth).set_available(data=data)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(msg="批量修改租户状态成功")
@TenantRouter.put(
@@ -127,7 +127,7 @@ async def toggle_tenant_status_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:patch"]))],
) -> JSONResponse:
await TenantService(auth).toggle_status(id=id)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(msg="修改租户状态成功")
@TenantRouter.get(
@@ -154,7 +154,7 @@ async def add_tenant_user_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:create"]))],
) -> JSONResponse:
await TenantService(auth).add_tenant_user(tenant_id=id, data=data)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(msg="添加用户成功")
@TenantRouter.delete(
@@ -168,7 +168,7 @@ async def remove_tenant_user_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:delete"]))],
) -> JSONResponse:
await TenantService(auth).remove_tenant_user(tenant_id=id, user_id=uid)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(msg="移除用户成功")
@TenantRouter.get(
@@ -207,7 +207,7 @@ async def update_tenant_config_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))],
) -> JSONResponse:
result = await TenantService(auth).update_config(redis=redis, tenant_id=id, config=data)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(data=result, msg="更新租户配置成功")
@TenantRouter.put(
@@ -221,7 +221,7 @@ async def renew_tenant_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:update"]))],
) -> JSONResponse:
result = await TenantService(auth).renew(tenant_id=id, end_time=data.end_time)
await FastAPICache.clear(namespace=_TENANT_NS)
await cache_util.clear(namespace=_TENANT_NS)
return SuccessResponse(data=result, msg="租户续期成功")
@TenantRouter.get(
@@ -1,3 +1,4 @@
import json
import random
import string
@@ -206,13 +207,13 @@ class TenantService:
for tid in ids:
reasons: list[str] = []
if await UserCRUD(self.auth).list(search={"tenant_id": tid}):
if await UserCRUD(self.auth).get_list(search={"tenant_id": tid}):
reasons.append("用户")
if await DeptCRUD(self.auth).list(search={"tenant_id": tid}):
if await DeptCRUD(self.auth).get_list(search={"tenant_id": tid}):
reasons.append("部门")
if await RoleCRUD(self.auth).list(search={"tenant_id": tid}):
if await RoleCRUD(self.auth).get_list(search={"tenant_id": tid}):
reasons.append("角色")
if await PositionCRUD(self.auth).list(search={"tenant_id": tid}):
if await PositionCRUD(self.auth).get_list(search={"tenant_id": tid}):
reasons.append("岗位")
if reasons:
raise CustomException(msg=f"租户下已存在{'/'.join(reasons)},操作失败")
@@ -4,19 +4,19 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from redis.asyncio.client import Redis
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import ErrorResponse, ResponseSchema, SuccessResponse
from app.config.setting import settings
from app.core import cache_util
from app.core.base_schema import (
AuthSchema,
JWTOutSchema,
LogoutPayloadSchema,
RefreshTokenPayloadSchema,
)
from app.core.cache_util import cache
from app.core.dependencies import db_getter, get_current_user, redis_getter
from app.core.exceptions import CustomException
from app.core.logger import logger
@@ -180,7 +180,7 @@ async def select_tenant_controller(
redis: Annotated[Redis, Depends(redis_getter)],
) -> JSONResponse:
result = await LoginService(auth).select_tenant(request=request, redis=redis, tenant_id=data.tenant_id)
await FastAPICache.clear(namespace=_AUTH_TENANTS_NS)
await cache_util.clear(namespace=_AUTH_TENANTS_NS)
return SuccessResponse(data=result, msg="租户切换成功")
@@ -8,8 +8,6 @@
环境变量见 Settings OAUTH_* 字段
"""
from __future__ import annotations
import json
import secrets
from typing import Any, Literal
@@ -1,3 +1,4 @@
import json
import secrets
import uuid
@@ -2,11 +2,11 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from app.common.response import ResponseSchema, SuccessResponse
from app.core import cache_util
from app.core.base_schema import AuthSchema, BatchSetAvailable
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
@@ -53,7 +53,7 @@ async def create_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:create"]))],
) -> JSONResponse:
result_dict = await DeptService(auth).create(data=data)
await FastAPICache.clear(namespace=_DEPT_NS)
await cache_util.clear(namespace=_DEPT_NS)
return SuccessResponse(data=result_dict, msg="创建部门成功")
@DeptRouter.put(
@@ -67,7 +67,7 @@ async def update_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:update"]))],
) -> JSONResponse:
result_dict = await DeptService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_DEPT_NS)
await cache_util.clear(namespace=_DEPT_NS)
return SuccessResponse(data=result_dict, msg="修改部门成功")
@DeptRouter.delete(
@@ -80,7 +80,7 @@ async def delete_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:delete"]))],
) -> JSONResponse:
await DeptService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_DEPT_NS)
await cache_util.clear(namespace=_DEPT_NS)
return SuccessResponse(msg="删除部门成功")
@DeptRouter.patch(
@@ -93,5 +93,5 @@ async def batch_set_available_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dept:patch"]))],
) -> JSONResponse:
await DeptService(auth).batch_set_available(data=data)
await FastAPICache.clear(namespace=_DEPT_NS)
await cache_util.clear(namespace=_DEPT_NS)
return SuccessResponse(msg="批量修改部门状态成功")
@@ -1,3 +1,4 @@
from app.core.base_schema import AuthSchema, BatchSetAvailable
from app.core.exceptions import CustomException
from app.utils.common_util import (
@@ -83,7 +84,7 @@ class DeptService:
raise CustomException(msg="删除失败,删除对象不能为空")
# 获取所有部门列表,用于构建树形关系
all_depts = await DeptCRUD(self.auth).list()
all_depts = await DeptCRUD(self.auth).get_list()
# 构建子部门ID映射
child_id_map = get_child_id_map(model_list=all_depts)
@@ -95,7 +96,7 @@ class DeptService:
await DeptCRUD(self.auth).delete(ids=ids)
async def batch_set_available(self, data: BatchSetAvailable) -> None:
dept_list = await DeptCRUD(self.auth).list()
dept_list = await DeptCRUD(self.auth).get_list()
total_ids = []
if data.status == 0:
@@ -2,13 +2,13 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from redis.asyncio.client import Redis
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission, redis_getter
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
@@ -68,7 +68,7 @@ async def get_type_list_controller(
async def get_type_optionselect_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:query"]))],
) -> JSONResponse:
result_dict_list = await DictTypeService(auth).list()
result_dict_list = await DictTypeService(auth).get_list()
return SuccessResponse(data=result_dict_list, msg="获取字典类型列表成功")
@DictRouter.post(
@@ -82,7 +82,7 @@ async def create_type_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:create"]))],
) -> JSONResponse:
result_dict = await DictTypeService(auth).create(redis=redis, data=data)
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
await cache_util.clear(namespace=_DICT_TYPE_NS)
return SuccessResponse(data=result_dict, msg="创建字典类型成功")
@DictRouter.put(
@@ -97,7 +97,7 @@ async def update_type_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:update"]))],
) -> JSONResponse:
result_dict = await DictTypeService(auth).update(redis=redis, id=id, data=data)
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
await cache_util.clear(namespace=_DICT_TYPE_NS)
return SuccessResponse(data=result_dict, msg="修改字典类型成功")
@DictRouter.delete(
@@ -111,7 +111,7 @@ async def delete_type_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:delete"]))],
) -> JSONResponse:
await DictTypeService(auth).delete(redis=redis, ids=ids)
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
await cache_util.clear(namespace=_DICT_TYPE_NS)
return SuccessResponse(msg="删除字典类型成功")
@DictRouter.patch(
@@ -124,7 +124,7 @@ async def batch_set_available_dict_type_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:patch"]))],
) -> JSONResponse:
await DictTypeService(auth).set_available(data=data)
await FastAPICache.clear(namespace=_DICT_TYPE_NS)
await cache_util.clear(namespace=_DICT_TYPE_NS)
return SuccessResponse(msg="批量修改字典类型状态成功")
@DictRouter.post(
@@ -137,7 +137,7 @@ async def export_type_list_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_type:export"]))],
) -> StreamingResponse:
# 获取全量数据并转为dict列表
result_dict_list = await DictTypeService(auth).list(search=search)
result_dict_list = await DictTypeService(auth).get_list(search=search)
export_data = [item.model_dump() for item in result_dict_list]
export_result = DictTypeService.export(data_list=export_data)
@@ -242,7 +242,7 @@ async def export_data_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:dict_data:export"]))],
) -> StreamingResponse:
result_dict_list = await DictDataService(auth).list(search=search, order_by=page.order_by)
result_dict_list = await DictDataService(auth).get_list(search=search, order_by=page.order_by)
export_data = [item.model_dump() for item in result_dict_list]
export_result = DictDataService.export(data_list=export_data)
@@ -54,7 +54,7 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
- int: 删除的记录数量
"""
if exclude_system:
system_data = await self.list(
system_data = await self.get_list(
search={
"id__in": ids,
"remark__contains": "系统默认",
@@ -81,4 +81,4 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
search = {"dict_type": dict_type}
if status is not None:
search["status"] = status
return await self.list(search=search, order_by=[{"id": "asc"}])
return await self.get_list(search=search, order_by=[{"id": "asc"}])
@@ -47,7 +47,7 @@ class DictTypeService:
"""
return await DictTypeCRUD(self.auth).get_or_404(id=id, out_schema=DictTypeOutSchema)
async def list(
async def get_list(
self,
search: DictTypeQueryParam | None = None,
order_by: list[dict] | None = None,
@@ -62,7 +62,7 @@ class DictTypeService:
返回:
- list[DictTypeOutSchema]: 字典类型响应模型列表
"""
obj_list = await DictTypeCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
obj_list = await DictTypeCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [DictTypeOutSchema.model_validate(obj) for obj in obj_list]
async def page(
@@ -149,7 +149,7 @@ class DictTypeService:
# 如果字典类型修改或状态变更,则修改对应字典数据的类型和状态
if exist_obj.dict_type != data.dict_type or exist_obj.status != data.status:
exist_obj_type_list = await DictDataCRUD(self.auth).list(search={"dict_type": exist_obj.dict_type})
exist_obj_type_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": exist_obj.dict_type})
if exist_obj_type_list:
for item in exist_obj_type_list:
item.dict_type = data.dict_type
@@ -174,7 +174,7 @@ class DictTypeService:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}"
try:
# 获取当前字典类型的所有字典数据,确保包含最新状态
dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": data.dict_type})
dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": data.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
@@ -203,14 +203,14 @@ class DictTypeService:
"""
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
existing = await DictTypeCRUD(self.auth).list(search={"id": ("in", ids)})
existing = await DictTypeCRUD(self.auth).get_list(search={"id": ("in", ids)})
existing_map = {obj.id: obj for obj in existing}
for nid in ids:
if nid not in existing_map:
raise CustomException(msg="删除失败,该数据不存在")
exist_obj = existing_map[nid]
# 检查是否有字典数据
exist_obj_type_list = await DictDataCRUD(self.auth).list(search={"dict_type": exist_obj.dict_type})
exist_obj_type_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": exist_obj.dict_type})
if len(exist_obj_type_list) > 0:
# 如果有字典数据,不能删除
raise CustomException(msg="删除失败,该数据字典类型下存在字典数据")
@@ -290,7 +290,7 @@ class DictDataService:
"""
return await DictDataCRUD(self.auth).get_or_404(id=id, out_schema=DictDataOutSchema)
async def list(
async def get_list(
self,
search: DictDataQueryParam | None = None,
order_by: list[dict] | None = None,
@@ -305,7 +305,7 @@ class DictDataService:
返回:
- list[DictDataOutSchema]: 字典数据响应模型列表
"""
obj_list = await DictDataCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
obj_list = await DictDataCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [DictDataOutSchema.model_validate(obj) for obj in obj_list]
async def page(
@@ -351,7 +351,7 @@ class DictDataService:
async with async_db_session() as session:
async with session.begin():
init_auth = AuthSchema(db=session, check_data_scope=False)
obj_list = await DictTypeCRUD(init_auth).list()
obj_list = await DictTypeCRUD(init_auth).get_list()
if not obj_list:
logger.warning("未找到任何字典类型数据")
return
@@ -360,7 +360,7 @@ class DictDataService:
dict_type = obj.dict_type
tenant_id = obj.tenant_id
try:
dict_data_list = await DictDataCRUD(init_auth).list(
dict_data_list = await DictDataCRUD(init_auth).get_list(
search={"dict_type": dict_type, "tenant_id": tenant_id}
)
dict_data = [
@@ -452,7 +452,7 @@ class DictDataService:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}"
try:
# 获取当前字典类型的所有字典数据
dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": data.dict_type})
dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": data.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
@@ -505,7 +505,7 @@ class DictDataService:
if dict_type:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{dict_type.dict_type}"
try:
dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": dict_type.dict_type})
dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": dict_type.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
@@ -522,7 +522,7 @@ class DictDataService:
# 刷新新字典类型缓存
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{data.dict_type}"
try:
dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": data.dict_type})
dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": data.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
@@ -550,7 +550,7 @@ class DictDataService:
"""
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
existing = await DictDataCRUD(self.auth).list(search={"id": ("in", ids)})
existing = await DictDataCRUD(self.auth).get_list(search={"id": ("in", ids)})
existing_map = {obj.id: obj for obj in existing}
for nid in ids:
if nid not in existing_map:
@@ -560,7 +560,7 @@ class DictDataService:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{self.auth.user.tenant_id}:{exist_obj.dict_type}"
try:
# 重新拉取该类型所有字典数据并写回缓存(保持一致)
dict_data_list = await DictDataCRUD(self.auth).list(search={"dict_type": exist_obj.dict_type})
dict_data_list = await DictDataCRUD(self.auth).get_list(search={"dict_type": exist_obj.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
@@ -1,3 +1,4 @@
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
@@ -49,7 +50,7 @@ class LoginLogService:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
existing = await LoginLogCRUD(self.auth).list(search={"id": ("in", ids)})
existing = await LoginLogCRUD(self.auth).get_list(search={"id": ("in", ids)})
existing_map = {obj.id for obj in existing}
for nid in ids:
if nid not in existing_map:
@@ -128,7 +129,7 @@ class OperationLogService:
async def delete(self, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
existing = await OperationLogCRUD(self.auth).list(search={"id": ("in", ids)})
existing = await OperationLogCRUD(self.auth).get_list(search={"id": ("in", ids)})
existing_map = {obj.id for obj in existing}
for nid in ids:
if nid not in existing_map:
@@ -2,12 +2,12 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission, get_current_user
from app.core.logger import logger
from app.core.router_class import OperationLogRoute
@@ -66,7 +66,7 @@ async def create_notice_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:create"]))],
) -> JSONResponse:
result_dict = await NoticeService(auth).create(data=data)
await FastAPICache.clear(namespace=_NOTICE_NS)
await cache_util.clear(namespace=_NOTICE_NS)
return SuccessResponse(data=result_dict, msg="创建公告成功")
@NoticeRouter.put(
@@ -80,7 +80,7 @@ async def update_notice_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:update"]))],
) -> JSONResponse:
result_dict = await NoticeService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_NOTICE_NS)
await cache_util.clear(namespace=_NOTICE_NS)
return SuccessResponse(data=result_dict, msg="修改公告成功")
@NoticeRouter.delete(
@@ -93,7 +93,7 @@ async def delete_notice_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:delete"]))],
) -> JSONResponse:
await NoticeService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_NOTICE_NS)
await cache_util.clear(namespace=_NOTICE_NS)
return SuccessResponse(msg="删除公告成功")
@NoticeRouter.patch(
@@ -106,7 +106,7 @@ async def batch_set_available_notice_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:patch"]))],
) -> JSONResponse:
await NoticeService(auth).set_available(data=data)
await FastAPICache.clear(namespace=_NOTICE_NS)
await cache_util.clear(namespace=_NOTICE_NS)
return SuccessResponse(msg="批量修改公告状态成功")
@NoticeRouter.post(
@@ -117,7 +117,7 @@ async def export_notice_list_controller(
search: Annotated[NoticeQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:notice:export"]))],
) -> StreamingResponse:
result_dict_list = await NoticeService(auth).list(search=search)
result_dict_list = await NoticeService(auth).get_list(search=search)
export_data = [item.model_dump() for item in result_dict_list]
export_result = NoticeService.export(notice_list=export_data)
@@ -163,7 +163,7 @@ async def mark_read_controller(
) -> JSONResponse:
"""标记已读。通过 `sys_notice_read` 表记录已读时间。"""
await NoticeService(auth).mark_read(notice_id=id)
await FastAPICache.clear(namespace=_NOTICE_NS)
await cache_util.clear(namespace=_NOTICE_NS)
logger.info(f"用户[{auth.user.id}]标记通知[{id}]已读")
return SuccessResponse(msg="标记已读成功")
@@ -177,7 +177,7 @@ async def mark_all_read_controller(
) -> JSONResponse:
"""全部标记已读。返回本次操作标记的数量。"""
count = await NoticeService(auth).mark_all_read()
await FastAPICache.clear(namespace=_NOTICE_NS)
await cache_util.clear(namespace=_NOTICE_NS)
logger.info(f"用户[{auth.user.id}]全部已读, 数量={count}")
return SuccessResponse(data=count, msg=f"全部标记已读成功,共标记 {count}")
@@ -28,12 +28,12 @@ class NoticeService:
async def detail(self, id: int) -> NoticeOutSchema:
return await NoticeCRUD(self.auth).get_or_404(id=id, out_schema=NoticeOutSchema)
async def list(
async def get_list(
self,
search: NoticeQueryParam | None = None,
order_by: list[dict] | None = None,
) -> list[NoticeOutSchema]:
notice_obj_list = await NoticeCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
notice_obj_list = await NoticeCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [NoticeOutSchema.model_validate(notice_obj) for notice_obj in notice_obj_list]
async def page(
@@ -79,7 +79,7 @@ class NoticeService:
async def delete(self, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
notices = await NoticeCRUD(self.auth).list(search={"id": ("in", ids)})
notices = await NoticeCRUD(self.auth).get_list(search={"id": ("in", ids)})
notice_map = {n.id: n for n in notices}
for nid in ids:
if nid not in notice_map:
@@ -117,7 +117,7 @@ async def delete_param_controller(
)
async def batch_set_status_controller(
ids: Annotated[list[int], Body(description="参数ID列表")],
status: Annotated[str, Body(description="状态值")],
status: Annotated[int, Body(description="状态值")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:patch"]))],
) -> JSONResponse:
await ParamsService(auth).batch_set_status(ids=ids, status=status)
@@ -132,7 +132,7 @@ async def export_param_list_controller(
search: Annotated[ParamsQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:param:export"]))],
) -> StreamingResponse:
result_dict_list = await ParamsService(auth).list(search=search)
result_dict_list = await ParamsService(auth).get_list(search=search)
export_data = [item.model_dump() for item in result_dict_list]
export_result = ParamsService.export(data_list=export_data)
@@ -78,7 +78,7 @@ class ParamsService:
raise CustomException(msg="该数据不存在")
return obj.config_value
async def list(
async def get_list(
self,
search: ParamsQueryParam | None = None,
order_by: list[dict] | None = None,
@@ -93,7 +93,7 @@ class ParamsService:
返回:
- list[ParamsOutSchema]: 参数响应模型列表
"""
obj_list = await ParamsCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
obj_list = await ParamsCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [ParamsOutSchema.model_validate(obj) for obj in obj_list]
async def page(
@@ -215,7 +215,7 @@ class ParamsService:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
# 批量校验参数存在性
objs = await ParamsCRUD(self.auth).list(search={"id": ("in", ids)})
objs = await ParamsCRUD(self.auth).get_list(search={"id": ("in", ids)})
obj_map = {o.id: o for o in objs}
for pid in ids:
obj = obj_map.get(pid)
@@ -235,7 +235,7 @@ class ParamsService:
logger.error(f"删除系统配置失败: {e}")
raise CustomException(msg="同步删除缓存失败") from e
async def batch_set_status(self, ids: list[int], status: str) -> None:
async def batch_set_status(self, ids: list[int], status: int) -> None:
"""
批量设置系统参数状态
@@ -297,7 +297,7 @@ class ParamsService:
async with async_db_session() as session:
async with session.begin():
init_auth = AuthSchema(db=session, check_data_scope=False)
config_obj = await ParamsCRUD(init_auth).list()
config_obj = await ParamsCRUD(init_auth).get_list()
if not config_obj:
raise CustomException(msg="该数据不存在")
try:
@@ -349,7 +349,7 @@ class ParamsService:
async with async_db_session() as session:
async with session.begin():
init_auth = AuthSchema(db=session, check_data_scope=False)
config_obj = await ParamsCRUD(init_auth).list()
config_obj = await ParamsCRUD(init_auth).get_list()
if config_obj:
try:
for config in config_obj:
@@ -2,12 +2,12 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
@@ -68,7 +68,7 @@ async def create_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:create"]))],
) -> JSONResponse:
result_dict = await PositionService(auth).create(data=data)
await FastAPICache.clear(namespace=_POS_NS)
await cache_util.clear(namespace=_POS_NS)
return SuccessResponse(data=result_dict, msg="创建岗位成功")
@PositionRouter.put(
@@ -82,7 +82,7 @@ async def update_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:update"]))],
) -> JSONResponse:
result_dict = await PositionService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_POS_NS)
await cache_util.clear(namespace=_POS_NS)
return SuccessResponse(data=result_dict, msg="修改岗位成功")
@PositionRouter.delete(
@@ -95,7 +95,7 @@ async def delete_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:delete"]))],
) -> JSONResponse:
await PositionService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_POS_NS)
await cache_util.clear(namespace=_POS_NS)
return SuccessResponse(msg="删除岗位成功")
@PositionRouter.patch(
@@ -108,7 +108,7 @@ async def batch_set_available_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:patch"]))],
) -> JSONResponse:
await PositionService(auth).set_available(data=data)
await FastAPICache.clear(namespace=_POS_NS)
await cache_util.clear(namespace=_POS_NS)
return SuccessResponse(msg="批量修改岗位状态成功")
@PositionRouter.get(
@@ -120,7 +120,7 @@ async def export_obj_list_controller(
search: Annotated[PositionQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:position:export"]))],
) -> StreamingResponse:
position_query_result = await PositionService(auth).list(search=search)
position_query_result = await PositionService(auth).get_list(search=search)
position_export_result = PositionService.export_list(position_list=position_query_result)
return StreamResponse(
@@ -24,12 +24,12 @@ class PositionService:
async def detail(self, id: int) -> PositionOutSchema:
return await PositionCRUD(self.auth).get_or_404(id=id, out_schema=PositionOutSchema)
async def list(
async def get_list(
self,
search: PositionQueryParam | None = None,
order_by: list[dict] | None = None,
) -> list[PositionOutSchema]:
position_list = await PositionCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
position_list = await PositionCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [PositionOutSchema.model_validate(position) for position in position_list]
async def page(
@@ -66,7 +66,7 @@ class PositionService:
async def delete(self, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
positions = await PositionCRUD(self.auth).list(search={"id": ("in", ids)})
positions = await PositionCRUD(self.auth).get_list(search={"id": ("in", ids)})
position_map = {p.id: p for p in positions}
for pid in ids:
if pid not in position_map:
@@ -74,7 +74,7 @@ class PositionService:
await PositionCRUD(self.auth).delete(ids=ids)
async def set_available(self, data: BatchSetAvailable) -> None:
positions = await PositionCRUD(self.auth).list(search={"id": ("in", data.ids)})
positions = await PositionCRUD(self.auth).get_list(search={"id": ("in", data.ids)})
position_map = {p.id: p for p in positions}
for pid in data.ids:
if pid not in position_map:
@@ -2,12 +2,12 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
@@ -69,7 +69,7 @@ async def create_role_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:create"]))],
) -> JSONResponse:
result_dict = await RoleService(auth).create(data=data)
await FastAPICache.clear(namespace=_ROLE_NS)
await cache_util.clear(namespace=_ROLE_NS)
return SuccessResponse(data=result_dict, msg="创建角色成功")
@RoleRouter.put(
@@ -83,7 +83,7 @@ async def update_role_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:update"]))],
) -> JSONResponse:
result_dict = await RoleService(auth).update(id=id, data=data)
await FastAPICache.clear(namespace=_ROLE_NS)
await cache_util.clear(namespace=_ROLE_NS)
return SuccessResponse(data=result_dict, msg="修改角色成功")
@RoleRouter.delete(
@@ -96,7 +96,7 @@ async def delete_role_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:delete"]))],
) -> JSONResponse:
await RoleService(auth).delete(ids=ids)
await FastAPICache.clear(namespace=_ROLE_NS)
await cache_util.clear(namespace=_ROLE_NS)
return SuccessResponse(msg="删除角色成功")
@RoleRouter.patch(
@@ -109,7 +109,7 @@ async def batch_set_available_role_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:patch"]))],
) -> JSONResponse:
await RoleService(auth).set_available(data=data)
await FastAPICache.clear(namespace=_ROLE_NS)
await cache_util.clear(namespace=_ROLE_NS)
return SuccessResponse(msg="批量修改角色状态成功")
@RoleRouter.put(
@@ -122,7 +122,7 @@ async def set_role_permission_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:permission"]))],
) -> JSONResponse:
await RoleService(auth).set_permission(data=data)
await FastAPICache.clear(namespace=_ROLE_NS)
await cache_util.clear(namespace=_ROLE_NS)
return SuccessResponse(msg="授权角色成功")
@RoleRouter.get(
@@ -134,7 +134,7 @@ async def export_role_list_controller(
search: Annotated[RoleQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:role:export"]))],
) -> StreamingResponse:
role_query_result = await RoleService(auth).list(search=search)
role_query_result = await RoleService(auth).get_list(search=search)
role_export_result = RoleService.export_list(role_list=role_query_result)
return StreamResponse(
@@ -25,8 +25,8 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
返回:
- None
"""
roles = await self.list(search={"id": ("in", role_ids)})
menus = [] if not menu_ids else await MenuCRUD(self.auth).list(search={"id": ("in", menu_ids)})
roles = await self.get_list(search={"id": ("in", role_ids)})
menus = [] if not menu_ids else await MenuCRUD(self.auth).get_list(search={"id": ("in", menu_ids)})
from app.api.v1.module_platform.package.service import PackageService
@@ -54,8 +54,8 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
返回:
- None
"""
roles = await self.list(search={"id": ("in", role_ids)})
depts = [] if not dept_ids else await DeptCRUD(self.auth).list(search={"id": ("in", dept_ids)})
roles = await self.get_list(search={"id": ("in", role_ids)})
depts = [] if not dept_ids else await DeptCRUD(self.auth).get_list(search={"id": ("in", dept_ids)})
for obj in roles:
relationship = obj.depts
@@ -37,7 +37,7 @@ class RoleService:
"""
return await RoleCRUD(self.auth).get_or_404(id=id, out_schema=RoleOutSchema)
async def list(
async def get_list(
self,
search: RoleQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
@@ -52,7 +52,7 @@ class RoleService:
返回:
- list[RoleOutSchema]: 角色响应模型列表
"""
role_list = await RoleCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
role_list = await RoleCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [RoleOutSchema.model_validate(role) for role in role_list]
async def page(
@@ -141,7 +141,7 @@ class RoleService:
raise CustomException(msg="删除失败,删除对象不能为空")
# 批量校验角色存在性
roles = await RoleCRUD(self.auth).list(search={"id": ("in", ids)})
roles = await RoleCRUD(self.auth).get_list(search={"id": ("in", ids)})
if len(roles) != len(ids):
raise CustomException(msg="删除失败,部分ID不存在")
@@ -179,7 +179,7 @@ class RoleService:
返回:
- None
"""
roles = await RoleCRUD(self.auth).list(search={"id": ("in", data.ids)})
roles = await RoleCRUD(self.auth).get_list(search={"id": ("in", data.ids)})
role_map = {r.id: r for r in roles}
for rid in data.ids:
if rid not in role_map:
@@ -1,3 +1,4 @@
from sqlalchemy import select
from app.api.v1.module_system.user.model import UserModel
@@ -212,7 +212,7 @@ async def export_user_list_controller(
search: Annotated[UserQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:user:export"]))],
) -> StreamingResponse:
user_list = await UserService(auth).list(search=search, order_by=page.order_by)
user_list = await UserService(auth).get_list(search=search, order_by=page.order_by)
user_export_result = UserService.export_list(user_list=user_list)
return StreamResponse(
@@ -38,9 +38,9 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
返回:
- None
"""
user_objs = await self.list(search={"id": ("in", user_ids)})
user_objs = await self.get_list(search={"id": ("in", user_ids)})
if role_ids:
role_objs = await RoleCRUD(self.auth).list(search={"id": ("in", role_ids)})
role_objs = await RoleCRUD(self.auth).get_list(search={"id": ("in", role_ids)})
else:
role_objs = []
@@ -61,9 +61,9 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
返回:
- None
"""
user_objs = await self.list(search={"id": ("in", user_ids)})
user_objs = await self.get_list(search={"id": ("in", user_ids)})
if position_ids:
position_objs = await PositionCRUD(self.auth).list(search={"id": ("in", position_ids)})
position_objs = await PositionCRUD(self.auth).get_list(search={"id": ("in", position_ids)})
else:
position_objs = []
@@ -46,12 +46,12 @@ class UserService:
result.dept_name = dept.name if dept else None
return result
async def list(
async def get_list(
self,
search: UserQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[UserOutSchema]:
user_list = await UserCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
user_list = await UserCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [UserOutSchema.model_validate(user) for user in user_list]
async def page(
@@ -125,7 +125,7 @@ class UserService:
new_user = await UserCRUD(self.auth).update(id=id, data=data)
if data.role_ids and len(data.role_ids) > 0:
roles = await RoleCRUD(self.auth).list(search={"id": ("in", data.role_ids)})
roles = await RoleCRUD(self.auth).get_list(search={"id": ("in", data.role_ids)})
if len(roles) != len(data.role_ids):
raise CustomException(msg="更新失败,部分角色不存在")
if not all(role.status == 0 for role in roles):
@@ -133,7 +133,7 @@ class UserService:
await UserCRUD(self.auth).set_user_roles(user_ids=[id], role_ids=data.role_ids)
if data.position_ids and len(data.position_ids) > 0:
positions = await PositionCRUD(self.auth).list(search={"id": ("in", data.position_ids)})
positions = await PositionCRUD(self.auth).get_list(search={"id": ("in", data.position_ids)})
if len(positions) != len(data.position_ids):
raise CustomException(msg="更新失败,部分岗位不存在")
if not all(position.status == 0 for position in positions):
@@ -145,7 +145,7 @@ class UserService:
async def delete(self, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
users = await UserCRUD(self.auth).list(search={"id": ("in", ids)})
users = await UserCRUD(self.auth).get_list(search={"id": ("in", ids)})
user_map = {u.id: u for u in users}
for uid in ids:
user = user_map.get(uid)
+31 -23
View File
@@ -47,6 +47,14 @@ from app.core.logger import logger
from app.plugin.module_task.cronjob.node.model import NodeModel
from app.utils.cron_util import CronUtil
# 任务状态常量(与 JobModel.status 注释保持一致:0:待执行 1:执行中 2:成功 3:失败 4:超时 5:已取消)
JOB_STATUS_PENDING = 0
JOB_STATUS_RUNNING = 1
JOB_STATUS_SUCCESS = 2
JOB_STATUS_FAILED = 3
JOB_STATUS_TIMEOUT = 4
JOB_STATUS_CANCELLED = 5
scheduler = AsyncIOScheduler()
scheduler.configure(
jobstores={
@@ -149,7 +157,7 @@ class SchedulerUtil:
if trigger_type in ("cron", "interval"):
cls._update_job_log(
job_id=job_id,
status="running",
status=JOB_STATUS_RUNNING,
)
else:
# 一次性任务(manual/date):创建新的 running 状态日志
@@ -157,7 +165,7 @@ class SchedulerUtil:
job_id=job_id,
job_name=job.name,
trigger_type=trigger_type,
status="running",
status=JOB_STATUS_RUNNING,
)
else:
# 任务可能已经被移除(一次性任务执行完毕后自动移除)
@@ -174,7 +182,7 @@ class SchedulerUtil:
job_id=original_job_id,
job_name=job_name,
trigger_type="manual",
status="running",
status=JOB_STATUS_RUNNING,
)
if result:
logger.info(f"任务 {original_job_id} 日志创建成功,id={result}")
@@ -199,7 +207,7 @@ class SchedulerUtil:
# 更新执行日志
cls._update_latest_job_log(
job_id=job_id,
status="success",
status=JOB_STATUS_SUCCESS,
result=str(retval) if retval else None,
)
@@ -212,7 +220,7 @@ class SchedulerUtil:
job_id=job_id,
job_name=job.name,
trigger_type=trigger_type,
status="pending",
status=JOB_STATUS_PENDING,
)
logger.debug(f"任务 {job_id} 已创建新的 pending 状态日志,等待下次执行")
@@ -235,7 +243,7 @@ class SchedulerUtil:
# 更新执行日志
cls._update_latest_job_log(
job_id=job_id,
status="failed",
status=JOB_STATUS_FAILED,
result="failed",
error=str(exception) if exception else "未知错误",
)
@@ -249,7 +257,7 @@ class SchedulerUtil:
job_id=job_id,
job_name=job.name,
trigger_type=trigger_type,
status="pending",
status=JOB_STATUS_PENDING,
)
logger.debug(f"任务 {job_id} 已创建新的 pending 状态日志,等待下次执行")
@@ -268,7 +276,7 @@ class SchedulerUtil:
# 更新执行日志
cls._update_latest_job_log(
job_id=job_id,
status="timeout",
status=JOB_STATUS_TIMEOUT,
result="timeout",
error="任务错过执行时间",
)
@@ -281,7 +289,7 @@ class SchedulerUtil:
job_id=job_id,
job_name=job.name,
trigger_type=trigger_type,
status="pending",
status=JOB_STATUS_PENDING,
)
logger.debug(f"任务 {job_id} 已创建新的 pending 状态日志,等待下次执行")
@@ -335,7 +343,7 @@ class SchedulerUtil:
job_id=job_id,
job_name=job.name,
trigger_type=trigger_type,
status="pending",
status=JOB_STATUS_PENDING,
)
logger.info(f"任务 {job_id} 已创建初始 pending 状态日志")
else:
@@ -609,8 +617,8 @@ class SchedulerUtil:
from app.plugin.module_task.cronjob.job.model import JobModel
with Session(engine) as session:
session.query(JobModel).filter(JobModel.status == "pending").update({
"status": "cancelled"
session.query(JobModel).filter(JobModel.status == JOB_STATUS_PENDING).update({
"status": JOB_STATUS_CANCELLED
})
session.commit()
logger.info("所有待执行任务日志已标记为已取消")
@@ -871,7 +879,7 @@ class SchedulerUtil:
with Session(engine) as session:
deleted = (
session.query(JobModel)
.filter(JobModel.job_id == job_id, JobModel.status == "pending")
.filter(JobModel.job_id == job_id, JobModel.status == JOB_STATUS_PENDING)
.delete(synchronize_session=False)
)
session.commit()
@@ -886,7 +894,7 @@ class SchedulerUtil:
job_id: str,
job_name: str | None = None,
trigger_type: str = "manual",
status: str = "running",
status: int = JOB_STATUS_RUNNING,
) -> int | None:
"""
创建执行日志
@@ -922,7 +930,7 @@ class SchedulerUtil:
@classmethod
def _update_job_log(
cls, job_id: str, status: str, result: str | None = None, error: str | None = None
cls, job_id: str, status: int, result: str | None = None, error: str | None = None
) -> None:
"""
更新执行日志更新该 job_id 最新的 pending 状态日志
@@ -940,7 +948,7 @@ class SchedulerUtil:
job_log = (
session
.query(JobModel)
.filter(JobModel.job_id == job_id, JobModel.status == "pending")
.filter(JobModel.job_id == job_id, JobModel.status == JOB_STATUS_PENDING)
.order_by(JobModel.created_time.desc())
.first()
)
@@ -960,7 +968,7 @@ class SchedulerUtil:
@classmethod
def _update_latest_job_log(
cls, job_id: str, status: str, result: str | None = None, error: str | None = None
cls, job_id: str, status: int, result: str | None = None, error: str | None = None
) -> None:
"""
更新最新的执行日志更新该 job_id 最新的一条日志
@@ -980,7 +988,7 @@ class SchedulerUtil:
job_log = (
session
.query(JobModel)
.filter(JobModel.job_id == job_id, JobModel.status == "running")
.filter(JobModel.job_id == job_id, JobModel.status == JOB_STATUS_RUNNING)
.order_by(JobModel.created_time.desc())
.first()
)
@@ -1003,7 +1011,7 @@ class SchedulerUtil:
job_log = (
session
.query(JobModel)
.filter(JobModel.job_id == job_id, JobModel.status == "cancelled")
.filter(JobModel.job_id == job_id, JobModel.status == JOB_STATUS_CANCELLED)
.order_by(JobModel.created_time.desc())
.first()
)
@@ -1076,12 +1084,12 @@ class SchedulerUtil:
job_log = (
session
.query(JobModel)
.filter(JobModel.job_id == job_id, JobModel.status.in_(["pending", "running"]))
.filter(JobModel.job_id == job_id, JobModel.status.in_([JOB_STATUS_PENDING, JOB_STATUS_RUNNING]))
.order_by(JobModel.created_time.desc())
.first()
)
if job_log:
job_log.status = "cancelled"
job_log.status = JOB_STATUS_CANCELLED
session.commit()
logger.info(f"任务 {job_id} 的执行日志已标记为已取消")
@@ -1501,7 +1509,7 @@ class SchedulerUtil:
existing_log = (
session
.query(JobModel)
.filter(JobModel.job_id == str(job.id), JobModel.status == "pending")
.filter(JobModel.job_id == str(job.id), JobModel.status == JOB_STATUS_PENDING)
.first()
)
if not existing_log:
@@ -1509,7 +1517,7 @@ class SchedulerUtil:
job_id=str(job.id),
job_name=job.name,
trigger_type=cls._get_trigger_type(str(job.id)),
status="pending",
status=JOB_STATUS_PENDING,
next_run_time=str(job.next_run_time) if job.next_run_time else None,
job_state=cls._get_job_state(job),
)
+17 -18
View File
@@ -15,7 +15,6 @@
super().__init__(model=OrderModel, session=session)
"""
import builtins
from collections.abc import Sequence
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, TypeVar
@@ -193,7 +192,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
except Exception as e:
raise CustomException(msg=f"统计失败: {e!s}")
async def list(
async def get_list(
self,
search: dict | None = None,
order_by: list[dict[str, str]] | None = None,
@@ -227,9 +226,9 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
async def tree_list(
self,
search: dict | None = None,
order_by: builtins.list[dict[str, str]] | None = None,
order_by: list[dict[str, str]] | None = None,
children_attr: str | None = None,
preload: builtins.list[str | Any] | None = None,
preload: list[str | Any] | None = None,
) -> Sequence[ModelType]:
"""
获取树形结构数据列表复用请求级事务会话
@@ -271,10 +270,10 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
self,
offset: int,
limit: int,
order_by: builtins.list[dict[str, str]],
order_by: list[dict[str, str]],
search: dict,
out_schema: type[OutSchemaType] | None = None,
preload: builtins.list[str | Any] | None = None,
preload: list[str | Any] | None = None,
) -> PageResultSchema:
"""
获取分页数据复用请求级事务会话count data 共享同一会话
@@ -413,7 +412,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
except Exception as e:
raise CustomException(msg=f"更新失败: {e!s}")
async def delete(self, ids: builtins.list[int]) -> None:
async def delete(self, ids: list[int]) -> None:
"""软删除对象(有认证时填充删除人 + 租户隔离)"""
try:
pk = self._get_pk_col()
@@ -449,7 +448,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
except Exception as e:
raise CustomException(msg=f"清空失败: {e!s}")
async def set(self, ids: builtins.list[int], **kwargs) -> None:
async def set(self, ids: list[int], **kwargs) -> None:
"""批量更新字段(带租户隔离)"""
try:
pk = self._get_pk_col()
@@ -461,7 +460,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
except Exception as e:
raise CustomException(msg=f"批量更新失败: {e!s}")
async def restore(self, ids: builtins.list[int]) -> None:
async def restore(self, ids: list[int]) -> None:
"""恢复软删除对象(带租户隔离)"""
try:
if not self._supports_soft_delete:
@@ -487,7 +486,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
filter_obj = Permission(model=self.model, auth=self.auth)
return await filter_obj.filter_query(sql)
def _platform_shared_conditions(self) -> builtins.list[ColumnElement]:
def _platform_shared_conditions(self) -> list[ColumnElement]:
if not self.auth or not self.auth.user:
return []
tid = self.auth.user.tenant_id
@@ -507,8 +506,8 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
return sql.where(getattr(self.model, "tenant_id") == tid)
return sql
async def __build_conditions(self, **kwargs) -> builtins.list[ColumnElement]:
conditions: builtins.list[ColumnElement] = []
async def __build_conditions(self, **kwargs) -> list[ColumnElement]:
conditions: list[ColumnElement] = []
if hasattr(self.model, "is_deleted"):
conditions.append(getattr(self.model, "is_deleted") == False) # noqa: E712
@@ -572,8 +571,8 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
return conditions
def _parse_order(
self, order: builtins.list[dict[str, str]]
) -> builtins.list[ColumnElement]:
self, order: list[dict[str, str]]
) -> list[ColumnElement]:
"""
解析排序参数
@@ -583,7 +582,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
返回:
- 排序表达式列表
"""
columns: builtins.list[ColumnElement] = []
columns: list[ColumnElement] = []
for item in order:
for field, direction in item.items():
column = getattr(self.model, field)
@@ -591,8 +590,8 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
return columns
def __loader_options(
self, preload: builtins.list[str | Any] | None = None
) -> builtins.list[Any]:
self, preload: list[str | Any] | None = None
) -> list[Any]:
"""
构建预加载选项
@@ -602,7 +601,7 @@ class CRUDBase[ModelType: MappedBase, CreateSchemaType: BaseModel, UpdateSchemaT
返回:
- 预加载选项列表
"""
options: builtins.list[Any] = []
options: list[Any] = []
model_loader_options = getattr(self.model, "__loader_options__", [])
all_preloads: set[str | Any] = set(model_loader_options)
+54
View File
@@ -0,0 +1,54 @@
"""轻量 Redis 缓存工具(替代 fastapi-cache2,兼容 redis-py"""
import hashlib
import json
from collections.abc import Callable
from functools import wraps
from typing import Any
from redis.asyncio.client import Redis
_ENABLE: bool = True
_EXPIRE: int = 300
_PREFIX: str = "fastapi-admin-cache"
_REDIS: Redis | None = None
async def init(redis: Redis, prefix: str = "fastapi-admin-cache", expire: int = 300, enable: bool = True) -> None:
global _REDIS, _PREFIX, _EXPIRE, _ENABLE
_REDIS = redis
_PREFIX = prefix
_EXPIRE = expire
_ENABLE = enable
def _build_key(namespace: str, func: Callable, *args: Any, **kwargs: Any) -> str:
raw = f"{func.__module__}:{func.__qualname__}:{args}:{kwargs}"
return f"{_PREFIX}:{namespace}:{hashlib.md5(raw.encode()).hexdigest()}"
def cache(expire: int | None = None, namespace: str = "default"):
def decorator(func: Callable):
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
if not _ENABLE or _REDIS is None:
return await func(*args, **kwargs)
key = _build_key(namespace, func, *args, **kwargs)
cached = await _REDIS.get(key)
if cached:
return json.loads(cached)
result = await func(*args, **kwargs)
await _REDIS.set(key, json.dumps(result), ex=expire or _EXPIRE)
return result
return wrapper
return decorator
async def clear(namespace: str | None = None) -> None:
if _REDIS is None:
return
pattern = f"{_PREFIX}:{namespace}:*" if namespace else f"{_PREFIX}:*"
keys = [key async for key in _REDIS.scan_iter(match=pattern)]
if keys:
await _REDIS.delete(*keys)
-4
View File
@@ -1,7 +1,3 @@
"""请求级上下文"""
from __future__ import annotations
from contextvars import ContextVar, Token
from dataclasses import dataclass
from typing import Any
+6 -6
View File
@@ -6,11 +6,11 @@ from fastapi.concurrency import asynccontextmanager
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
from fastapi_limiter import FastAPILimiter
from fastapi_limiter.depends import RateLimiter, WebSocketRateLimiter
from app.core import cache_util
from .config.setting import settings
from .core.exceptions import handle_exception
from .core.http_limit import http_limit_callback, ws_limit_callback
@@ -40,6 +40,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
logger.info("✅ Redis租户配置初始化完成")
await SchedulerUtil.init_scheduler(redis=app.state.redis)
logger.info("✅ 定时任务调度器初始化完成")
await cache_util.init(redis=app.state.redis)
logger.info("✅ fastapi-admin-cache 初始化完成")
await FastAPILimiter.init(
redis=app.state.redis,
prefix=settings.REQUEST_LIMITER_REDIS_PREFIX,
@@ -47,8 +49,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
ws_callback=ws_limit_callback,
)
logger.info("✅ 请求限流器初始化完成")
FastAPICache.init(backend=RedisBackend(app.state.redis), prefix="fastapi-admin-cache", expire=300, enable=True)
logger.info("✅ fastapi-cache2 初始化完成")
console_start(
host=settings.SERVER_HOST, port=settings.SERVER_PORT,
@@ -65,8 +65,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
try:
await SchedulerUtil.shutdown(wait=True)
logger.info("✅ 定时任务调度器已关闭")
await FastAPICache.clear()
logger.info("✅ fastapi-cache2 已关闭")
await cache_util.clear()
logger.info("✅ fastapi-admin-cache 已关闭")
await FastAPILimiter.close()
logger.info("✅ 请求限制器已关闭")
await import_modules_async(modules=settings.EVENT_LIST, desc="全局事件", app=app, status=False)
@@ -1,3 +1,4 @@
from collections.abc import AsyncGenerator
from datetime import datetime
from typing import Any
@@ -117,7 +117,7 @@ async def export_obj_list_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:export"]))],
) -> StreamingResponse:
service = DemoService(auth)
result_dict_list = await service.list(search=search)
result_dict_list = await service.get_list(search=search)
export_result = DemoService.batch_export(obj_list=result_dict_list)
return StreamResponse(
@@ -30,12 +30,12 @@ class DemoService:
raise CustomException(msg="该数据不存在")
return DemoOutSchema.model_validate(obj)
async def list(
async def get_list(
self,
search: DemoQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[DemoOutSchema]:
obj_list = await DemoCRUD(self.auth).list(search=vars(search) if search else None, order_by=order_by)
obj_list = await DemoCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by)
return [DemoOutSchema.model_validate(obj) for obj in obj_list]
async def page(
@@ -76,7 +76,7 @@ class DemoService:
async def delete(self, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
objs = await DemoCRUD(self.auth).list(search={"id": ("in", ids)})
objs = await DemoCRUD(self.auth).get_list(search={"id": ("in", ids)})
obj_map = {o.id: o for o in objs}
for id_ in ids:
if id_ not in obj_map:
@@ -70,7 +70,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- Sequence[GenTableModel]: 所有业务表信息列表
"""
return await self.list(preload=preload)
return await self.get_list(preload=preload)
async def get_gen_table_list(
self,
@@ -87,7 +87,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回:
- Sequence[GenTableModel]: 业务表列表信息
"""
return await self.list(
return await self.get_list(
search=vars(search) if search else None,
order_by=[{"created_time": "desc"}],
preload=preload,
@@ -511,7 +511,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
返回:
- Sequence[GenTableColumnModel]: 业务表字段列表信息对象序列
"""
return await self.list(search={"table_id": table_id}, order_by=order_by, preload=preload)
return await self.get_list(search={"table_id": table_id}, order_by=order_by, preload=preload)
async def get_gen_db_table_columns_by_name(self, table_name: str | None) -> list[GenTableColumnOutSchema]:
"""
@@ -560,7 +560,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
返回:
- Sequence[GenTableColumnModel]: 业务表字段列表信息对象序列
"""
return await self.list(search=search, order_by=order_by, preload=preload)
return await self.get_list(search=search, order_by=order_by, preload=preload)
async def create_gen_table_column_crud(self, data: GenTableColumnSchema) -> GenTableColumnModel | None:
"""创建业务表字段。
@@ -1,3 +1,4 @@
import io
import os
import re
@@ -26,6 +27,8 @@ from app.config.setting import settings
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.gen_util import GenUtils
from app.utils.jinja2_template_util import Jinja2TemplateUtil
from .crud import GenTableColumnCRUD, GenTableCRUD
from .schema import (
@@ -37,8 +40,6 @@ from .schema import (
GenTableQueryParam,
GenTableSchema,
)
from .tools.gen_util import GenUtils
from .tools.jinja2_template_util import Jinja2TemplateUtil
def handle_service_exception(func: Callable) -> Callable:
@@ -51,7 +51,7 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
返回:
- Sequence[JobModel]: 执行日志模型序列
"""
return await self.list(search=search, order_by=order_by, preload=preload)
return await self.get_list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: JobCreateSchema) -> JobModel | None:
"""
@@ -1,3 +1,4 @@
from app.core.ap_scheduler import SchedulerUtil
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
@@ -146,7 +146,7 @@ async def execute_job_controller(
)
async def batch_set_status_controller(
ids: Annotated[list[int], Body(description="节点ID列表")],
status: Annotated[str, Body(description="状态值")],
status: Annotated[int, Body(description="状态值")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:cronjob:node:update"]))],
) -> JSONResponse:
service = NodeService(auth)
@@ -53,7 +53,7 @@ class NodeCRUD(CRUDBase[NodeModel, NodeCreateSchema, NodeUpdateSchema]):
返回:
- Sequence[NodeModel]: 节点模型序列
"""
return await self.list(search=search, order_by=order_by, preload=preload)
return await self.get_list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: NodeCreateSchema) -> NodeModel | None:
"""
@@ -39,7 +39,7 @@ class NodeService:
obj = await NodeCRUD(self.auth).get_obj_by_id_crud(id=id)
return NodeOutSchema.model_validate(obj)
async def list(
async def get_list(
self,
search: NodeQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
@@ -141,7 +141,7 @@ class NodeService:
return {"job_id": id, "status": "executed", "trigger": trigger}
async def batch_set_status(self, ids: list[int], status: str) -> None:
async def batch_set_status(self, ids: list[int], status: int) -> None:
if not ids:
raise CustomException(msg="请选择要操作的数据")
@@ -53,7 +53,7 @@ class WorkflowCRUD(CRUDBase[WorkflowModel, WorkflowCreateSchema, WorkflowUpdateS
返回:
- Sequence[WorkflowModel]: 工作流列表
"""
return await self.list(search=search, order_by=order_by, preload=preload)
return await self.get_list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: WorkflowCreateSchema) -> WorkflowModel | None:
"""
@@ -1,3 +1,4 @@
import asyncio
from typing import Any
@@ -1,7 +1,3 @@
"""工作流 DAG 执行引擎 — 拓扑分层 + 并行执行"""
from __future__ import annotations
import json
from collections import defaultdict, deque
from concurrent.futures import ThreadPoolExecutor
@@ -54,7 +54,7 @@ class WorkflowNodeTypeCRUD(CRUDBase[WorkflowNodeTypeModel, WorkflowNodeTypeCreat
返回:
- Sequence[WorkflowNodeTypeModel]: 列表
"""
return await self.list(search=search, order_by=order_by, preload=preload)
return await self.get_list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeModel | None:
"""
@@ -1,3 +1,4 @@
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
@@ -11,6 +11,9 @@
"tax_amount": 4485,
"status": 1,
"tenant_id": 3,
"pdf_url": "/static/invoice/3/INV20260101001.pdf",
"oss_license_pdf_url": "/static/invoice/3/INV20260101001_license.pdf",
"api_response": null,
"description": "星辰科技-标准版年付发票(已开具)"
},
{
@@ -25,6 +28,9 @@
"tax_amount": 1485,
"status": 1,
"tenant_id": 3,
"pdf_url": "/static/invoice/3/INV20260315001.pdf",
"oss_license_pdf_url": "/static/invoice/3/INV20260315001_license.pdf",
"api_response": null,
"description": "星辰科技-AI助手发票(已开具)"
},
{
@@ -39,6 +45,9 @@
"tax_amount": 4485,
"status": 0,
"tenant_id": 4,
"pdf_url": null,
"oss_license_pdf_url": null,
"api_response": null,
"description": "创新工坊-标准版月付发票(待开具)"
},
{
@@ -53,6 +62,9 @@
"tax_amount": 735,
"status": 0,
"tenant_id": 4,
"pdf_url": null,
"oss_license_pdf_url": null,
"api_response": null,
"description": "创新工坊-数据大屏发票(待开具)"
}
]
+1 -1
View File
@@ -11,8 +11,8 @@ from app.plugin.module_generator.gencode.schema import (
GenTableColumnOutSchema,
GenTableOutSchema,
)
from app.plugin.module_generator.gencode.tools.gen_util import GenUtils
from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil
from app.utils.gen_util import GenUtils
from app.utils.string_util import StringUtil
-4
View File
@@ -1,7 +1,3 @@
"""支付网关 — 抽象基类 + 支付宝 + Mock + 工厂"""
from __future__ import annotations
import base64
import json
import uuid
+4 -1
View File
@@ -9,7 +9,6 @@ from pathlib import Path
from typing import Any
from jinja2 import Environment, FileSystemLoader, StrictUndefined
from weasyprint import CSS, HTML
def render_html_template(
@@ -37,6 +36,8 @@ def html_to_pdf(html_str: str, css_str: str | None = None) -> bytes:
"""
HTML 字符串转换为 PDF 字节流
需要 weasyprint + 系统 libgobject安装: brew install glib pango
参数:
- html_str (str): HTML 内容
- css_str (str | None): 可选的内联 CSS 字符串
@@ -44,6 +45,8 @@ def html_to_pdf(html_str: str, css_str: str | None = None) -> bytes:
返回:
- bytes: PDF 字节流
"""
from weasyprint import CSS, HTML
html = HTML(string=html_str, base_url=".")
if css_str:
return html.write_pdf(stylesheets=[CSS(string=css_str)])
+2 -2
View File
@@ -35,16 +35,16 @@ dependencies = [
"python-multipart==0.0.32", # request.form() 对表单进行「解析」时安装
"redis==7.1.0", # redis 同/异步操作数据库(用户celery配套使用)redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本
"rich==15.0.0", # 终端打印美化
"sqlalchemy==2.0.45", # 数据库ORM
"sqlalchemy>=2.0.51,<2.1", # 数据库ORM
"sqlglot[rs]==27.8.0", # sql 解析
"typer==0.26.7", # 命令行工具
"ua-parser==1.0.2", # 解析 User-Agent 获取 OS/浏览器
"uvicorn==0.49.0", # uvicorn web 框架
"websockets>=16.0,<17.0", # websocket 通信
"fastapi-cache2[redis]>=0.1.8",
"fastapi-mail==1.5.1", # 邮件发送(简化 EmailSendService 底层)
"tinycss2==1.5.1",
"weasyprint==69.0",
"fastapi-cache2[redis]>=0.1.8",
]
[dependency-groups]
+1 -1
View File
@@ -32,7 +32,7 @@ pymysql==1.2.0 # MySQL 异步操作数据库
python-multipart==0.0.32 # request.form() 对表单进行「解析」时安装
redis==7.1.0 # Redis 异步操作数据库
rich==15.0.0 # 终端打印美化
sqlalchemy==2.0.45 # 数据库ORM
sqlalchemy==2.0.51 # 数据库ORM
sqlglot[rs]==27.8.0 # sql 解析
typer==0.26.7 # 命令行工具
ua-parser==1.0.2 # 获取用户UA
-378
View File
@@ -1,378 +0,0 @@
#!/bin/bash
set -e
# 固定脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# ========== 彩色输出定义 ==========
if [[ -t 1 ]]; then
tty_red="\033[0;31m"
tty_green="\033[0;32m"
tty_yellow="\033[0;33m"
tty_blue="\033[0;34m"
tty_cyan="\033[0;36m"
tty_purple="\033[0;35m"
tty_bold="\033[1m"
tty_reset="\033[0m"
else
tty_red="" tty_green="" tty_yellow="" tty_blue="" tty_cyan="" tty_purple="" tty_bold="" tty_reset=""
fi
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[38;5;226m[!]'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
LIGHT_GRAY='\033[0;37m'
PURPLE='\033[0;35m'
BOLD='\033[1m'
RESET='\033[0m'
# ========== 彩色输出函数 ==========
function info() {
echo -e "${tty_green}$1${tty_reset}"
}
function warn() {
echo -e "${tty_yellow}⚠️ $1${tty_reset}"
}
function error() {
echo -e "${tty_red}$1${tty_reset}"
}
function pause() {
read -n1 -r -p "按任意键继续..." key
}
# 判断执行是否成功
JudgeSuccess() {
if [ $? -ne 0 ]; then
error "步骤失败: $1"
exit 1
else
info "步骤成功: $1"
fi
}
# 分割线输出函数
print_separator() {
printf "${LIGHT_GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
}
show_banner() {
echo ""
echo -e "╭──────────────────────────────────────────────╮"
echo -e "│ \033[1;34m👋 欢迎使用 FastAPI 初始化脚本\033[0m │"
echo -e "╰──────────────────────────────────────────────╯"
echo "版本: 3.0.0"
echo "作者:coderxslee"
echo ""
}
# ========== 从 .env.dev 文件中读取数据库配置 ==========
function load_db_config() {
local env_file="$SCRIPT_DIR/env/.env.dev"
if [ ! -f "$env_file" ]; then
error "未找到 $env_file 文件"
return 1
fi
# 读取数据库配置(忽略注释和空行)
# 使用 sed 去掉注释部分,然后提取值
DATABASE_HOST=$(grep "^DATABASE_HOST" "$env_file" | cut -d'=' -f2 | sed 's/#.*//' | xargs)
DATABASE_PORT=$(grep "^DATABASE_PORT" "$env_file" | cut -d'=' -f2 | sed 's/#.*//' | xargs)
DATABASE_USER=$(grep "^DATABASE_USER" "$env_file" | cut -d'=' -f2 | sed 's/#.*//' | xargs)
DATABASE_PASSWORD=$(grep "^DATABASE_PASSWORD" "$env_file" | cut -d'=' -f2 | sed 's/#.*//' | xargs)
DATABASE_NAME=$(grep "^DATABASE_NAME" "$env_file" | cut -d'=' -f2 | sed 's/#.*//' | xargs)
DATABASE_TYPE=$(grep "^DATABASE_TYPE" "$env_file" | cut -d'=' -f2 | sed 's/#.*//' | xargs)
# 验证必要的配置
if [[ -z "$DATABASE_HOST" ]] || [[ -z "$DATABASE_PORT" ]] || [[ -z "$DATABASE_USER" ]] || [[ -z "$DATABASE_NAME" ]]; then
error "数据库配置不完整,请检查 $env_file 文件"
return 1
fi
return 0
}
# ========== FastAPI 功能函数 ==========
# 1. 启动开发服务器
function start_dev_server() {
print_separator
echo -e "${tty_cyan}🚀 启动(uv run dev...${tty_reset}"
load_db_config || return 1
echo -e "${tty_blue}🗄️ 检查并创建数据库...${tty_reset}"
# 检查数据库是否存在
DB_EXISTS=$(PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "postgres" -tAc "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME';" 2>/dev/null || echo "")
if [ "$DB_EXISTS" != "1" ]; then
warn "数据库 '$DATABASE_NAME' 不存在,正在创建..."
CREATE_DB_OUTPUT=$(PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "postgres" -c "CREATE DATABASE \"$DATABASE_NAME\" WITH ENCODING 'UTF8' LC_COLLATE 'C.UTF-8' LC_CTYPE 'C.UTF-8' TEMPLATE template0 OWNER $DATABASE_USER;" 2>&1)
if [ $? -ne 0 ]; then
error "数据库创建失败"
echo -e "${tty_red}错误详情:${tty_reset}"
echo -e "${tty_red}$CREATE_DB_OUTPUT${tty_reset}"
return 1
fi
info "数据库 '$DATABASE_NAME' 创建成功 (UTF8 编码, C.UTF-8 排序规则)"
else
info "数据库 '$DATABASE_NAME' 已存在"
fi
print_separator
cd "$SCRIPT_DIR"
uv run main.py run --env=dev
JudgeSuccess "开发服务器启动"
echo -e "${tty_green}🎉 开发服务器启动完成!${tty_reset}"
print_separator
}
# 2. 生成迁移文件
function create_migration() {
print_separator
echo -e "${tty_cyan}📝 生成迁移文件(模型变更后)...${tty_reset}"
cd "$SCRIPT_DIR"
uv run main.py revision --env=dev
JudgeSuccess "迁移文件生成"
echo -e "${tty_green}🎉 迁移文件生成完成!${tty_reset}"
print_separator
}
# 3. 应用迁移
function apply_migration() {
print_separator
echo -e "${tty_cyan}⚡ 应用迁移...${tty_reset}"
cd "$SCRIPT_DIR"
uv run main.py upgrade --env=dev
JudgeSuccess "迁移应用"
echo -e "${tty_green}🎉 迁移应用完成!${tty_reset}"
print_separator
}
# 4. 重置数据库中的迁移记录
function reset_migration_records() {
print_separator
echo -e "${tty_cyan}🔄 重置数据库中的迁移记录...${tty_reset}"
echo -e "${tty_yellow}⚠️ 警告:此操作将重置数据库中的迁移记录!${tty_reset}"
read -p "确认继续吗?(y/N): " confirm
if [[ $confirm != [yY] ]]; then
echo -e "${tty_yellow}操作已取消${tty_reset}"
return
fi
load_db_config || return 1
cd "$SCRIPT_DIR"
echo -e "${tty_blue}🔄 正在重置迁移记录...${tty_reset}"
# 使用 psql 连接到数据库并重置迁移记录
PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_NAME" -c "DELETE FROM alembic_version;" 2>/dev/null
JudgeSuccess "迁移记录重置"
echo -e "${tty_green}🎉 迁移记录重置完成!${tty_reset}"
print_separator
}
# 5. 清理数据库(删除所有表)
function clean_database() {
print_separator
echo -e "${tty_cyan}🗄️ 清理数据库(删除所有表)...${tty_reset}"
echo -e "${tty_yellow}⚠️ 警告:此操作将删除数据库中的所有数据!${tty_reset}"
read -p "确认继续吗?(y/N): " confirm
if [[ $confirm != [yY] ]]; then
echo -e "${tty_yellow}操作已取消${tty_reset}"
return
fi
load_db_config || return 1
cd "$SCRIPT_DIR"
echo -e "${tty_blue}🗄️ 清理数据库,删除所有现有的表...${tty_reset}"
# 使用 psql 连接到数据库并删除所有表
PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_NAME" -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" 2>/dev/null
JudgeSuccess "数据库清理"
echo -e "${tty_green}🎉 数据库清理完成!${tty_reset}"
print_separator
}
# 6. 删除数据库
function drop_database() {
print_separator
echo -e "${tty_cyan}🗑️ 删除数据库...${tty_reset}"
# 获取数据库名称
echo -e "${tty_blue}📝 请输入要删除的数据库名称:${tty_reset}"
read -p "数据库名称: " db_name
# 验证数据库名称不为空
if [[ -z "$db_name" ]]; then
error "数据库名称不能为空!"
return
fi
# 显示警告信息
echo -e "${tty_red}⚠️ 警告:此操作将永久删除数据库 '$db_name' 及其所有数据!${tty_reset}"
echo -e "${tty_yellow}此操作不可撤销!${tty_reset}"
# 第一次确认
read -p "确认要删除数据库 '$db_name' 吗?(y/N): " confirm1
if [[ $confirm1 != [yY] ]]; then
echo -e "${tty_yellow}操作已取消${tty_reset}"
return
fi
# 第二次确认
echo -e "${tty_red}⚠️ 最后确认:您真的要删除数据库 '$db_name' 吗?${tty_reset}"
read -p "请输入 'DELETE' 来确认删除: " confirm2
if [[ $confirm2 != "DELETE" && $confirm2 != "delete" ]]; then
echo -e "${tty_yellow}操作已取消${tty_reset}"
return
fi
load_db_config || return 1
cd "$SCRIPT_DIR"
echo -e "${tty_blue}🗑️ 正在删除数据库 '$db_name'...${tty_reset}"
# 先断开所有连接到该数据库的连接
PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "postgres" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$db_name' AND pid <> pg_backend_pid();" 2>/dev/null
# 删除数据库
PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "postgres" -c "DROP DATABASE IF EXISTS \"$db_name\";" 2>/dev/null
JudgeSuccess "数据库删除"
echo -e "${tty_green}🎉 数据库 '$db_name' 删除完成!${tty_reset}"
print_separator
}
# 7. 初始化数据(执行 sql 目录下脚本)
function init_sql_data() {
print_separator
echo -e "${tty_cyan}🧰 初始化数据...${tty_reset}"
local sql_dir
sql_dir="$REPO_ROOT/backend/sql/postgres/init_data"
if [ ! -d "$sql_dir" ]; then
error "未找到 SQL 目录: $sql_dir"
return 1
fi
local sql_files=()
while IFS= read -r file; do
sql_files+=("$file")
done < <(find "$sql_dir" -maxdepth 1 -type f -name "*.sql" 2>/dev/null | sort)
if [ ${#sql_files[@]} -eq 0 ]; then
warn "SQL 目录下没有可执行的 .sql 文件: $sql_dir"
return 0
fi
echo -e "${tty_purple}可用 SQL 文件:${tty_reset}"
echo -e "${tty_yellow}0. 执行全部 SQL 文件${tty_reset}"
local i
for i in "${!sql_files[@]}"; do
echo "$((i + 1)). $(basename "${sql_files[$i]}")"
done
local choice
read -p "请选择要初始化的 SQL(输入序号): " choice
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
error "输入无效,请输入数字序号"
return 1
fi
load_db_config || return 1
if [ "$choice" -eq 0 ]; then
echo -e "${tty_blue}🚀 开始执行全部 SQL 文件...${tty_reset}"
local file
for file in "${sql_files[@]}"; do
echo -e "${tty_blue}执行: $(basename "$file")${tty_reset}"
if PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_NAME" -f "$file"; then
info "执行成功: $(basename "$file")"
else
error "执行失败: $(basename "$file")"
return 1
fi
done
info "全部 SQL 文件执行完成"
else
local index=$((choice - 1))
if [ "$index" -lt 0 ] || [ "$index" -ge ${#sql_files[@]} ]; then
error "序号超出范围"
return 1
fi
local selected_file="${sql_files[$index]}"
echo -e "${tty_blue}执行: $(basename "$selected_file")${tty_reset}"
if PGPASSWORD="$DATABASE_PASSWORD" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_NAME" -f "$selected_file"; then
info "执行成功: $(basename "$selected_file")"
info "SQL 初始化完成"
else
error "执行失败: $(basename "$selected_file")"
return 1
fi
fi
print_separator
}
# 主菜单
function main_menu() {
clear
show_banner
echo -e "\033[1;34m📦 请选择要执行的操作:\033[0m"
echo ""
echo -e "\033[1;33m1. 🚀 启动(uv run dev\033[0m"
echo -e "\033[1;32m2. 📝 生成迁移文件(模型变更后)\033[0m"
echo -e "\033[1;36m3. ⚡ 应用迁移\033[0m"
echo -e "\033[1;36m4. 🔄 重置数据库中的迁移记录\033[0m"
echo -e "\033[1;31m5. 🗄️ 清理数据库(删除所有表)\033[0m"
echo -e "\033[1;31m6. 🗑️ 删除数据库\033[0m"
echo -e "\033[1;36m7. 🧰 初始化数据(执行 sql 脚本)\033[0m"
echo -e "\033[1;31m0. ❌ 退出\033[0m"
echo ""
read -p "请选择你要执行的操作: " option
case $option in
1) start_dev_server && pause ;;
2) create_migration && pause ;;
3) apply_migration && pause ;;
4) reset_migration_records && pause ;;
5) clean_database && pause ;;
6) drop_database && pause ;;
7) init_sql_data && pause ;;
0) exit 0 ;;
*) error "未知选项: $option" && pause ;;
esac
}
# 非交互模式入口
if [[ "$1" == "--start-dev" ]]; then
start_dev_server
exit $?
fi
# 默认进入交互菜单
main_menu
-267
View File
@@ -1,267 +0,0 @@
@echo off
setlocal EnableDelayedExpansion
set "SCRIPT_DIR=%~dp0"
set "REPO_ROOT=%SCRIPT_DIR%.."
set "tty_red="
set "tty_green="
set "tty_yellow="
set "tty_blue="
set "tty_cyan="
set "tty_purple="
set "tty_bold="
set "tty_reset="
set "RED=[0;31m"
set "GREEN=[0;32m"
set "YELLOW=[33m"
set "BLUE=[0;34m"
set "CYAN=[0;36m"
set "LIGHT_GRAY=[0;37m"
set "PURPLE=[0;35m"
set "BOLD=[1m"
set "RESET=[0m"
set "ERROR_COUNT=0"
goto :main_menu
:info
echo !tty_green!OK: %~1!tty_reset!
goto :eof
:warn
echo !tty_yellow!WARNING: %~1!tty_reset!
goto :eof
:error
echo !tty_red!ERROR: %~1!tty_reset!
goto :eof
:pause
pause
goto :eof
:print_separator
echo !LIGHT_GRAY!---------------------------------------------------------------!RESET!
goto :eof
:show_banner
echo.
echo Welcome to FastAPI Init Script
echo Version: 1.0.0
echo Author: coderxslee
echo.
goto :eof
:load_db_config
set "env_file=%SCRIPT_DIR%env\.env.dev"
if not exist "%env_file%" (
call :error "Not found: %env_file%"
exit /b 1
)
for /f "usebackq tokens=1,* delims==" %%a in ("%env_file%") do (
set "line=%%a"
set "value=%%b"
set "line=!line:~0,13!"
if "!line!"=="DATABASE_HOST" set "DATABASE_HOST=%%b"
if "!line!"=="DATABASE_PORT" set "DATABASE_PORT=%%b"
if "!line!"=="DATABASE_USER" set "DATABASE_USER=%%b"
if "!line!"=="DATABASE_PASSWORD" set "DATABASE_PASSWORD=%%b"
if "!line!"=="DATABASE_NAME" set "DATABASE_NAME=%%b"
if "!line!"=="DATABASE_TYPE" set "DATABASE_TYPE=%%b"
)
if "!DATABASE_HOST!"=="" (
call :error "Database configuration incomplete"
exit /b 1
)
exit /b 0
:start_dev_server
call :print_separator
echo Starting dev server...
call :load_db_config
if errorlevel 1 exit /b 1
echo Checking database...
echo Database host: !DATABASE_HOST!
echo Database port: !DATABASE_PORT!
echo Database user: !DATABASE_USER!
echo Database name: !DATABASE_NAME!
call :print_separator
cd /d "%SCRIPT_DIR%"
uv run main.py run --env=dev
exit /b 0
:create_migration
call :print_separator
echo Generating migration files...
cd /d "%SCRIPT_DIR%"
uv run main.py revision --env=dev
exit /b 0
:apply_migration
call :print_separator
echo Applying migrations...
cd /d "%SCRIPT_DIR%"
uv run main.py upgrade --env=dev
exit /b 0
:reset_migration_records
call :print_separator
echo Resetting migration records...
echo WARNING: This will reset migration records in the database!
set /p confirm="Continue? (y/N): "
if /i not "!confirm!"=="y" (
echo Operation cancelled
exit /b 0
)
call :load_db_config
if errorlevel 1 exit /b 1
cd /d "%SCRIPT_DIR%"
echo Resetting migration records...
echo DELETE FROM alembic_version; | PGPASSWORD=%DATABASE_PASSWORD% psql -h %DATABASE_HOST% -p %DATABASE_PORT% -U %DATABASE_USER% -d %DATABASE_NAME%
exit /b 0
:clean_database
call :print_separator
echo Cleaning database...
echo WARNING: This will delete all tables in the database!
set /p confirm="Continue? (y/N): "
if /i not "!confirm!"=="y" (
echo Operation cancelled
exit /b 0
)
call :load_db_config
if errorlevel 1 exit /b 1
cd /d "%SCRIPT_DIR%"
echo Cleaning database, dropping all tables...
echo DROP SCHEMA public CASCADE; CREATE SCHEMA public; | PGPASSWORD=%DATABASE_PASSWORD% psql -h %DATABASE_HOST% -p %DATABASE_PORT% -U %DATABASE_USER% -d %DATABASE_NAME%
exit /b 0
:drop_database
call :print_separator
echo Deleting database...
set /p db_name="Enter database name: "
if "!db_name!"=="" (
call :error "Database name cannot be empty"
exit /b 1
)
echo WARNING: This will permanently delete database '!db_name!' and all its data!
echo This action cannot be undone!
set /p confirm1="Confirm deletion of '!db_name!'? (y/N): "
if /i not "!confirm1!"=="y" (
echo Operation cancelled
exit /b 0
)
set /p confirm2="Enter 'DELETE' to confirm: "
if not "!confirm2!"=="DELETE" (
echo Operation cancelled
exit /b 0
)
call :load_db_config
if errorlevel 1 exit /b 1
cd /d "%SCRIPT_DIR%"
echo Dropping database '!db_name!'...
echo SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '!db_name!' AND pid <> pg_backend_pid(); | PGPASSWORD=%DATABASE_PASSWORD% psql -h %DATABASE_HOST% -p %DATABASE_PORT% -U %DATABASE_USER% -d postgres
echo DROP DATABASE IF EXISTS "!db_name!"; | PGPASSWORD=%DATABASE_PASSWORD% psql -h %DATABASE_HOST% -p %DATABASE_PORT% -U %DATABASE_USER% -d postgres
exit /b 0
:init_sql_data
call :print_separator
echo Initializing SQL data...
set "sql_dir=%REPO_ROOT%\backend\sql\postgres\init_data"
if not exist "%sql_dir%" (
call :error "SQL directory not found: %sql_dir%"
exit /b 1
)
echo Available SQL files:
echo 0. Execute all SQL files
set "i=0"
set "file_count=0"
for %%f in ("%sql_dir%\*.sql") do (
set /a file_count+=1
echo !file_count!. %%~nxf
)
set /p choice="Select SQL to initialize (enter number): "
call :load_db_config
if errorlevel 1 exit /b 1
if "!choice!"=="0" (
echo Executing all SQL files...
for %%f in ("%sql_dir%\*.sql") do (
echo Executing: %%~nxf
PGPASSWORD=%DATABASE_PASSWORD% psql -h %DATABASE_HOST% -p %DATABASE_PORT% -U %DATABASE_USER% -d %DATABASE_NAME% -f "%%f"
)
) else (
set /a index=choice-1
set "selected_file="
set "j=0"
for %%f in ("%sql_dir%\*.sql") do (
if "!j!"=="!index!" (
set "selected_file=%%f"
)
set /a j+=1
)
if "!selected_file!"=="" (
call :error "Invalid selection"
exit /b 1
)
echo Executing: !selected_file!
PGPASSWORD=%DATABASE_PASSWORD% psql -h %DATABASE_HOST% -p %DATABASE_PORT% -U %DATABASE_USER% -d %DATABASE_NAME% -f "!selected_file!"
)
exit /b 0
:main_menu
cls
call :show_banner
echo Please select an operation:
echo.
echo 1. Start dev server (uv run dev)
echo 2. Generate migration files
echo 3. Apply migrations
echo 4. Reset migration records
echo 5. Clean database (drop all tables)
echo 6. Delete database
echo 7. Initialize SQL data
echo 0. Exit
echo.
set /p option="Select operation: "
if "!option!"=="1" call :start_dev_server
if "!option!"=="2" call :create_migration
if "!option!"=="3" call :apply_migration
if "!option!"=="4" call :reset_migration_records
if "!option!"=="5" call :clean_database
if "!option!"=="6" call :drop_database
if "!option!"=="7" call :init_sql_data
if "!option!"=="0" exit /b 0
call :pause
goto :main_menu
:end
endlocal
exit /b 0
@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>开源项目授权声明 - {{ invoice_no }}</title>
<style>
@page {
size: A4;
margin: 14mm 16mm;
}
* { box-sizing: border-box; }
body {
font-family: "Noto Sans CJK SC", "Source Han Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 11px;
color: #1a1a2e;
line-height: 1.6;
margin: 0;
}
.doc {
border: 1.5px solid #1a1a2e;
padding: 22px 26px;
}
h1 {
text-align: center;
font-size: 18px;
margin: 0 0 4px 0;
letter-spacing: 2px;
}
.subtitle {
text-align: center;
color: #555;
font-size: 10px;
margin-bottom: 18px;
}
.meta {
background: #f4f6fa;
border-left: 3px solid #3454d1;
padding: 8px 12px;
margin-bottom: 14px;
font-size: 10.5px;
}
.meta table { border-collapse: collapse; width: 100%; }
.meta td { padding: 2px 6px; }
.meta td:first-child { color: #555; width: 90px; }
.section-title {
font-size: 12px;
font-weight: bold;
margin: 16px 0 6px 0;
padding-left: 8px;
border-left: 4px solid #3454d1;
}
.declaration {
text-indent: 2em;
font-size: 11px;
line-height: 1.8;
margin: 6px 0;
}
.license-group {
margin-bottom: 14px;
page-break-inside: avoid;
}
.license-group-header {
background: #1a1a2e;
color: #fff;
padding: 4px 10px;
font-size: 11px;
font-weight: bold;
display: flex;
justify-content: space-between;
}
.license-group-header .count {
color: #c5cae9;
font-weight: normal;
}
table.pkg-table {
width: 100%;
border-collapse: collapse;
font-size: 10.5px;
}
table.pkg-table th, table.pkg-table td {
border: 1px solid #c5c9d6;
padding: 4px 8px;
text-align: left;
}
table.pkg-table th {
background: #eef0f5;
font-weight: bold;
}
table.pkg-table td.col-name { width: 50%; }
table.pkg-table td.col-version { width: 20%; font-family: "Menlo", "Consolas", monospace; }
table.pkg-table td.col-pkgs { width: 30%; text-align: right; color: #666; }
.footer {
margin-top: 20px;
padding-top: 10px;
border-top: 1px dashed #888;
font-size: 9.5px;
color: #555;
text-align: center;
}
</style>
</head>
<body>
<div class="doc">
<h1>开源项目授权声明函</h1>
<div class="subtitle">Open Source Components Authorization Statement</div>
<div class="meta">
<table>
<tr>
<td>关联发票号</td><td><b>{{ invoice_no }}</b></td>
<td>开票日期</td><td>{{ invoice_date }}</td>
</tr>
<tr>
<td>产品名称</td><td colspan="3">FastapiAdmin 企业管理后台</td>
</tr>
<tr>
<td>产品版本</td><td colspan="3">{{ product_version }}</td>
</tr>
<tr>
<td>许可证总数</td><td>{{ groups | length }} 类</td>
<td>依赖包总数</td><td>{{ total_packages }} 个</td>
</tr>
</table>
</div>
<div class="section-title">一、声明</div>
<p class="declaration">
兹声明,本产品 <b>FastapiAdmin 企业管理后台</b> 在开发与部署过程中使用了 {{ total_packages }} 个第三方开源软件包,
涵盖 {{ groups | length }} 类许可证。本产品严格遵循各开源许可证的条款要求,
在使用、修改、分发相关开源组件时保留原始版权声明及许可证文本。
本声明函随同电子发票一同提供给客户,便于客户进行开源合规审计与软件资产管理。
</p>
<div class="section-title">二、许可证分类清单</div>
{% for group in groups %}
<div class="license-group">
<div class="license-group-header">
<span>{{ group.license }}</span>
<span class="count">共 {{ group.packages | length }} 个包</span>
</div>
<table class="pkg-table">
<thead>
<tr>
<th>包名 (Package)</th>
<th>版本 (Version)</th>
</tr>
</thead>
<tbody>
{% for pkg in group.packages %}
<tr>
<td class="col-name">{{ pkg.name }}</td>
<td class="col-version">{{ pkg.version }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
<div class="section-title">三、通用合规承诺</div>
<p class="declaration">
1. 上述所有开源组件均通过官方包管理器(PyPI)合法获取,并保留其原始许可证文本;<br>
2. 本产品未对 GPL/AGPL 等强 copyleft 协议组件进行源码闭源分发;<br>
3. 各许可证全文可访问各组件官方仓库或开源许可证标准文本;<br>
4. 如客户在二次开发或再分发过程中对许可证合规有进一步要求,本平台可提供完整的 LicenseText 文本。
</p>
<div class="footer">
本声明函由 FastapiAdmin 平台自动生成 · 生成时间 {{ generated_at }}<br>
本文件为电子授权声明,与发票具有同等合规效力
</div>
</div>
</body>
</html>
@@ -0,0 +1,97 @@
"""开发辅助脚本:为 platform_invoice.json 中所有已开票(status=1)的种子发票生成示例 PDF。
使用场景
- 首次启动开发环境后初始化数据已导入 PDF 文件并不存在
- 前端访问 /static/invoice/...pdf 404
- 跑此脚本批量生成发票 PDF + 授权函 PDF路径与 JSON pdf_url / oss_license_pdf_url 完全一致
用法
cd backend
uv run python tests/scripts/seed_invoice_pdfs.py
依赖
- weasyprintpyproject.toml 已包含
- macOS 上需先安装系统库brew install glib pango libffiLinux 部署环境正常
"""
import asyncio
import json
import sys
from pathlib import Path
BACKEND_DIR = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(BACKEND_DIR))
from app.api.v1.module_platform.invoice.pdf_helper import ( # noqa: E402
_render_invoice_pdf,
_render_oss_license_pdf,
)
from app.api.v1.module_platform.invoice.schema import InvoiceOutSchema # noqa: E402
SEED_JSON = BACKEND_DIR / "app" / "scripts" / "data" / "platform_invoice.json"
def _to_out_schema(record: dict) -> InvoiceOutSchema:
"""把 JSON 字典转为 InvoiceOutSchema,缺失的字段用合理默认填充"""
return InvoiceOutSchema(
id=record.get("id", 0),
invoice_no=record["invoice_no"],
order_id=record["order_id"],
tenant_id=record["tenant_id"],
invoice_type=record["invoice_type"],
title=record["title"],
tax_no=record.get("tax_no"),
bank_info=record.get("bank_info"),
address_info=record.get("address_info"),
amount=record["amount"],
tax_amount=record.get("tax_amount", 0),
status=record.get("status", 1),
description=record.get("description"),
created_time=record.get("created_time", "2026-01-01 00:00:00"),
updated_time=record.get("updated_time", "2026-01-01 00:00:00"),
created_by=record.get("created_by", {"id": 1, "name": "admin"}),
updated_by=record.get("updated_by", {"id": 1, "name": "admin"}),
)
async def main() -> int:
if not SEED_JSON.exists():
print(f"[ERR] 找不到种子数据: {SEED_JSON}")
return 1
records = json.loads(SEED_JSON.read_text(encoding="utf-8"))
issued = [r for r in records if r.get("status") == 1]
print(f"种子数据共 {len(records)} 条,其中 status=1(已开票){len(issued)}\n")
if not issued:
print("无需生成")
return 0
success = 0
failed = 0
for rec in issued:
invoice_no = rec["invoice_no"]
try:
invoice = _to_out_schema(rec)
invoice_url = _render_invoice_pdf(invoice)
license_url = _render_oss_license_pdf(invoice)
inv_full = BACKEND_DIR / invoice_url.lstrip("/")
lic_full = BACKEND_DIR / license_url.lstrip("/")
print(f"{invoice_no}")
print(f" 发票 PDF: {invoice_url} ({inv_full.stat().st_size} bytes)")
print(f" 授权函 PDF: {license_url} ({lic_full.stat().st_size} bytes)")
success += 1
except Exception as e:
print(f"{invoice_no}: {type(e).__name__}: {e}")
failed += 1
print(f"\n=== 完成:成功 {success} 条,失败 {failed} 条 ===")
if failed:
print("\n如果提示缺少 libgobject/pango")
print(" macOS: brew install glib pango libffi")
print(" Ubuntu: sudo apt-get install libpango-1.0-0 libpangoft2-1.0-0")
return 0 if failed == 0 else 2
if __name__ == "__main__":
sys.exit(asyncio.run(main()))
+249
View File
@@ -0,0 +1,249 @@
"""验证 scripts/data/*.json 与数据库表名一致性。
核心规则只要求 1
- JSON 文件名 .json= 数据库表名Model.__tablename__
- platform_invoice.json InvoiceModel.__tablename__ = "platform_invoice"
补充检查每对 JSON-Model
- JSON 出现的字段必须存在于 Model 列中多余字段 = ERROR
- JSON 缺失必填无默认字段 = ERROR
违规时 fail
用法uv run python tests/test_init_data_integrity.py
"""
import importlib
import inspect
import json
import sys
from pathlib import Path
from typing import Any
BACKEND_ROOT = Path(__file__).resolve().parent.parent
SCRIPT_DIR = BACKEND_ROOT / "app" / "scripts" / "data"
# 非种子数据文件,校验脚本跳过
_EXCLUDED_JSON: set[str] = {"oss_licenses.json"}
# 运行时产生数据的表,无需种子 JSON(白名单)
# 业务说明:以下表的数据由系统运行产生,不在 initialize 时导入
_NO_SEED_TABLES: set[str] = {
"gen_table", # 代码生成器-业务表(用户在线创建)
"gen_table_column", # 代码生成器-字段表(随 gen_table 产生)
"platform_email_log", # 邮件发送日志(运行时累计)
"platform_package_plugin", # 套餐-插件关联(运行时由超管配置)
"sys_role_depts", # 角色-部门关联(运行时由超管配置)
"sys_role_menus", # 角色-菜单关联(运行时由超管配置)
"sys_user_positions", # 用户-岗位关联(运行时由 HR 配置)
"task_job", # 定时任务(运行时由用户配置)
"task_workflow", # 工作流(运行时由用户配置)
}
# 已知会被 initialize.py 特殊处理的字段(树形结构 children)
# initialize.py 的 _RECURSIVE_TABLES = {"platform_menu", "sys_dept"} 会 pop children 再传给 Model
_ALLOWED_EXTRA_FIELDS: set[str] = {"children"}
sys.path.insert(0, str(BACKEND_ROOT))
# ────────────────────────────────────────────────────────────
# 1. 扫描所有 Model,自动建立"表名 → Model 类"映射
# ────────────────────────────────────────────────────────────
def _discover_table_to_model() -> dict[str, type]:
"""
扫描 app/ 下所有 model.py提取 *Model 类的 __tablename__
返回: {tablename: ModelClass}
"""
table_to_model: dict[str, type] = {}
for model_file in sorted(BACKEND_ROOT.rglob("model.py")):
rel = model_file.relative_to(BACKEND_ROOT)
if "app" not in rel.parts:
continue
module_path = ".".join(rel.with_suffix("").parts)
try:
mod = importlib.import_module(module_path)
except Exception as e: # noqa: BLE001
print(f"[IMPORT ERR] {module_path}: {type(e).__name__}: {e}")
continue
for name, obj in vars(mod).items():
if not inspect.isclass(obj):
continue
if obj.__module__ != module_path:
continue
if not name.endswith("Model"):
continue
if name.startswith("_"):
continue
table_name = getattr(obj, "__tablename__", None)
if not table_name:
continue
if table_name in table_to_model:
# 同名表名不应存在多个 Model
print(f"[WARN] 表名 {table_name!r} 被多个 Model 声明: {obj}{table_to_model[table_name]}")
continue
table_to_model[table_name] = obj
return table_to_model
# ────────────────────────────────────────────────────────────
# 2. 字段对应检查
# ────────────────────────────────────────────────────────────
def _model_columns(model: type) -> dict[str, dict[str, Any]]:
cols: dict[str, dict[str, Any]] = {}
for col in model.__table__.columns:
cols[col.name] = {
"type": col.type.__class__.__name__,
"nullable": col.nullable,
"default": col.default.arg if col.default else None,
"server_default": col.server_default.arg if col.server_default else None,
"primary_key": col.primary_key,
}
return cols
def _check_field_compat(json_path: Path, model: type) -> dict:
"""检查 JSON 字段 vs Model 列的兼容性"""
cols = _model_columns(model)
data = json.loads(json_path.read_text(encoding="utf-8"))
if isinstance(data, dict):
records = [data]
else:
records = data
extra_fields: set[str] = set()
missing_required: set[str] = set()
type_mismatches: list[str] = []
sample = records[0] if records else {}
for k in sample.keys():
if k not in cols and k not in _ALLOWED_EXTRA_FIELDS:
extra_fields.add(k)
for col_name, info in cols.items():
if info["primary_key"]:
continue
if info["nullable"] or info["default"] is not None or info["server_default"] is not None:
continue
for rec in records:
if col_name not in rec:
missing_required.add(col_name)
break
type_map = {
"INTEGER": int,
"BIGINTEGER": int,
"SMALLINTEGER": int,
"VARCHAR": str,
"String": str,
"TEXT": str,
"JSON": (dict, list),
"BOOLEAN": bool,
"DATETIME": str,
"DATE": str,
"TIME": str,
"FLOAT": (int, float),
"NUMERIC": (int, float),
}
for col_name, info in cols.items():
py_type = type_map.get(info["type"])
if py_type is None:
continue
for rec in records[:5]:
v = rec.get(col_name)
if v is None:
continue
if not isinstance(v, py_type):
type_mismatches.append(f"{col_name}({info['type']}): 实际 {type(v).__name__}={v!r}")
break
return {
"json_count": len(records),
"extra_in_json": sorted(extra_fields),
"missing_required": sorted(missing_required),
"type_mismatches": type_mismatches,
"ok": not extra_fields and not missing_required and not type_mismatches,
}
# ────────────────────────────────────────────────────────────
# 3. 主流程:表名对应 + 字段对应
# ────────────────────────────────────────────────────────────
def main() -> int:
table_to_model = _discover_table_to_model()
print(f"扫描到 {len(table_to_model)} 个 Model 表\n")
# 收集所有 JSON 文件
json_files = sorted(p for p in SCRIPT_DIR.glob("*.json") if p.name not in _EXCLUDED_JSON)
json_table_names = {p.stem for p in json_files}
model_table_names = set(table_to_model.keys())
# 规则 1: JSON 文件名(去 .json)必须 = Model.__tablename__
orphan_jsons = json_table_names - model_table_names
orphan_tables = model_table_names - json_table_names
# 规则 2: 字段对应
print("" * 70)
print("【规则 1】JSON 文件名 ↔ 数据库表名对应")
print("" * 70)
bad = 0
if orphan_jsons:
print("✗ 下列 JSON 文件找不到对应 Model 表:")
for name in sorted(orphan_jsons):
print(f" - {name}.json")
bad += len(orphan_jsons)
if orphan_tables:
# 过滤掉白名单内的"运行时产生数据"表
real_orphan = orphan_tables - _NO_SEED_TABLES
whitelisted = orphan_tables & _NO_SEED_TABLES
if real_orphan:
print("✗ 下列 Model 表没有对应 JSON 种子数据(initialize 时会被跳过):")
for name in sorted(real_orphan):
print(f" - {name} ({table_to_model[name].__name__})")
bad += len(real_orphan)
if whitelisted:
print(f" ⊙ 跳过白名单表(运行时产生数据): {len(whitelisted)}")
for name in sorted(whitelisted):
print(f" - {name}")
if not orphan_jsons and not orphan_tables:
print(f"✓ 全部 {len(json_table_names)} 个 JSON 都对应了 Model 表")
print()
print("" * 70)
print("【规则 2】JSON 字段 ↔ Model 列兼容性")
print("" * 70)
field_bad = 0
for json_path in json_files:
table_name = json_path.stem
if table_name not in table_to_model:
continue # 上面已报
model = table_to_model[table_name]
r = _check_field_compat(json_path, model)
mark = "" if r["ok"] else ""
print(f" {mark} {json_path.name} ({r['json_count']} 条) ↔ {model.__name__}")
if r["extra_in_json"]:
print(f" JSON 多余字段: {r['extra_in_json']}")
if r["missing_required"]:
print(f" 缺失必填字段: {r['missing_required']}")
for t in r["type_mismatches"]:
print(f" 类型不匹配: {t}")
if not r["ok"]:
field_bad += 1
print()
total_bad = bad + field_bad
if total_bad:
print(f"=== 失败:表名不符 {bad} 个 + 字段不符 {field_bad} 个 ===")
return 1
print(f"=== 全部通过:{len(json_table_names)} 个 JSON ↔ {len(model_table_names)} 个 Model 表 ===")
return 0
if __name__ == "__main__":
sys.exit(main())
+32 -26
View File
@@ -296,7 +296,7 @@ requires-dist = [
{ name = "python-multipart", specifier = "==0.0.32" },
{ name = "redis", specifier = "==7.1.0" },
{ name = "rich", specifier = "==15.0.0" },
{ name = "sqlalchemy", specifier = "==2.0.45" },
{ name = "sqlalchemy", specifier = ">=2.0.51,<2.1" },
{ name = "sqlglot", extras = ["rs"], specifier = "==27.8.0" },
{ name = "tinycss2", specifier = "==1.5.1" },
{ name = "typer", specifier = "==0.26.7" },
@@ -1827,37 +1827,43 @@ wheels = [
[[package]]
name = "sqlalchemy"
version = "2.0.45"
version = "2.0.51"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
]
[[package]]
+1 -1
View File
@@ -30,7 +30,7 @@ const TicketAPI = {
responseType: "blob",
});
},
batchTicket(body: { ids: number[]; status: string }) {
batchTicket(body: { ids: number[]; status: number }) {
return request<ApiResponse>({ url: `${API_PATH}/batch`, method: "put", data: body });
},
};
@@ -90,6 +90,7 @@
@click="handleSubmit"
v-ripple
:disabled="disabledSubmit"
:loading="loading"
>
{{ t("table.form.submit") }}
</ElButton>
@@ -177,6 +178,7 @@
@click="handleSubmit"
v-ripple
:disabled="disabledSubmit"
:loading="loading"
>
{{ t("table.form.submit") }}
</ElButton>
@@ -296,6 +298,8 @@ interface Props {
showSubmit?: boolean;
/** 是否禁用提交按钮 */
disabledSubmit?: boolean;
/** 提交按钮 loading(防止重复提交) */
loading?: boolean;
/** 提交时是否清洗空值 */
sanitizeOutput?: Partial<SanitizeOutputOptions>;
/** 是否需要内置 ElScrollbar 包裹 */
@@ -329,6 +333,7 @@ const props = withDefaults(defineProps<Props>(), {
showReset: true,
showSubmit: true,
disabledSubmit: false,
loading: false,
sanitizeOutput: () => ({}),
scrollbar: false,
maxHeight: "75vh",
@@ -21,6 +21,7 @@
v-hasPerm="permCreate"
type="primary"
:icon="Plus"
:loading="createLoading"
@click="$emit('add')"
plain
>
@@ -63,7 +64,7 @@
批量删除
</ElButton>
<ElDropdown v-if="permPatch" v-hasPerm="permPatch" trigger="click">
<ElButton type="default" :disabled="removeIds.length === 0 || moreDisabled">
<ElButton type="default" :disabled="removeIds.length === 0 || moreDisabled" :loading="moreLoading">
<template #icon>
<ArrowDown />
</template>
@@ -71,8 +72,8 @@
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem icon="Check" @click="$emit('more', '0')">批量启用</ElDropdownItem>
<ElDropdownItem icon="CircleClose" @click="$emit('more', '1')">
<ElDropdownItem icon="Check" @click="$emit('more', 0)">批量启用</ElDropdownItem>
<ElDropdownItem icon="CircleClose" @click="$emit('more', 1)">
批量停用
</ElDropdownItem>
</ElDropdownMenu>
@@ -112,6 +113,10 @@ interface Props {
permPatch?: string | string[];
/** 批量删除中(按钮 loading,并禁用「更多」) */
deleteLoading?: boolean;
/** 新增按钮 loading(防止重复点击触发多次创建) */
createLoading?: boolean;
/** 「更多」下拉项(启用/停用)loading */
moreLoading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
@@ -119,6 +124,8 @@ const props = withDefaults(defineProps<Props>(), {
deleteLoading: false,
importLoading: false,
exportLoading: false,
createLoading: false,
moreLoading: false,
});
interface Emits {
@@ -128,10 +135,10 @@ interface Emits {
import: [];
export: [];
delete: [];
more: [value: string];
more: [value: number];
}
defineEmits<Emits>();
const moreDisabled = computed(() => props.removeIds.length === 0 || props.deleteLoading);
const moreDisabled = computed(() => props.removeIds.length === 0 || props.deleteLoading || props.moreLoading);
</script>
+2 -2
View File
@@ -23,8 +23,8 @@ export async function confirmBatchDelete(count: number): Promise<void> {
}
/** 状态切换确认 */
export async function confirmToggleStatus(status: string): Promise<void> {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
export async function confirmToggleStatus(status: number): Promise<void> {
await ElMessageBox.confirm(`确认${status === 0 ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
+147
View File
@@ -0,0 +1,147 @@
/**
* useLoading / loading
*
* 1
* ```ts
* const { withLoading, isLoading } = useLoading();
*
* async function handleDelete(id: number) {
* await SomeAPI.delete(id);
* ElMessage.success("删除成功");
* }
*
* const deleteBtn = withLoading("delete", handleDelete);
*
* // template:
* // <ElButton :loading="isLoading('delete')" @click="deleteBtn">删除</ElButton>
* ```
*
* 2
* ```ts
* const { loading, run } = useLoading("submit");
*
* async function handleSubmit() {
* await run(async () => {
* await SomeAPI.save(data);
* });
* }
*
* // <ElButton :loading="loading" @click="handleSubmit">保存</ElButton>
* ```
*
* 3 ref
* ```ts
* const { loading, withLoading } = useLoading();
*
* const handleDelete = withLoading(async (id: number) => {
* await SomeAPI.delete(id);
* });
*
* // <ElButton :loading="loading" @click="handleDelete(1)">删除</ElButton>
* ```
*/
import { computed, ref, type ComputedRef, type Ref } from "vue";
export interface UseLoadingReturn {
/** 任意 key 是否在 loadingkey-less 模式下表示唯一 loading */
loading: ComputedRef<boolean>;
/** 当前 loading map(多 key 模式) */
loadingMap: Readonly<Ref<Record<string, boolean>>>;
/** 判断指定 key 是否 loading */
isLoading: (key: string) => boolean;
/** 包装异步函数(key-less 模式) */
withLoading: <T extends (...args: any[]) => Promise<any>>(fn: T) => T;
/** 包装异步函数并绑定 key(多 key 模式) */
withKeyLoading: <T extends (...args: any[]) => Promise<any>>(key: string, fn: T) => T;
/** 手动 run(多 key 模式) */
run: <T>(key: string, fn: () => Promise<T>) => Promise<T>;
/** 重置所有 loading(通常不需要) */
reset: () => void;
}
/**
* loading
*
* @param defaultKey key key ref
*/
export function useLoading(defaultKey?: string): UseLoadingReturn {
const loadingMap = ref<Record<string, boolean>>({});
function setLoading(key: string, val: boolean): void {
loadingMap.value = { ...loadingMap.value, [key]: val };
}
function isLoading(key: string): boolean {
return !!loadingMap.value[key];
}
/**
* key-less loading computed key loading true
* - defaultKey loading key
* - ref map[defaultKey]
*/
const loading = computed(() => {
if (defaultKey) {
return !!loadingMap.value[defaultKey];
}
return Object.values(loadingMap.value).some(Boolean);
});
/**
* key-less defaultKey
*/
function withLoading<T extends (...args: any[]) => Promise<any>>(fn: T): T {
const key = defaultKey ?? "__default__";
return (async (...args: Parameters<T>) => {
setLoading(key, true);
try {
return await fn(...args);
} finally {
setLoading(key, false);
}
}) as T;
}
/**
* + key
*/
function withKeyLoading<T extends (...args: any[]) => Promise<any>>(
key: string,
fn: T,
): T {
return (async (...args: Parameters<T>) => {
setLoading(key, true);
try {
return await fn(...args);
} finally {
setLoading(key, false);
}
}) as T;
}
/**
* run key
*/
async function run<T>(key: string, fn: () => Promise<T>): Promise<T> {
setLoading(key, true);
try {
return await fn();
} finally {
setLoading(key, false);
}
}
function reset(): void {
loadingMap.value = {};
}
return {
loading,
loadingMap,
isLoading,
withLoading,
withKeyLoading,
run,
reset,
};
}
+1 -1
View File
@@ -309,7 +309,7 @@
"notice": {
"title": "通知",
"btnRead": "标为已读",
"bar": ["通知", "消息", "办"],
"bar": ["通知", "消息", "办"],
"text": ["暂无"],
"viewAll": "查看全部"
},
+1 -1
View File
@@ -190,7 +190,7 @@ declare global {
*/
interface BatchType {
ids: number[];
status: string;
status: number;
}
/**
@@ -2,7 +2,7 @@
<div class="fa-card p-5 h-134 overflow-hidden">
<div class="fa-card-header">
<div class="title">
<h4>办事项</h4>
<h4>办事项</h4>
<p>
待处理
<span class="text-danger">3</span>
@@ -34,7 +34,8 @@
:perm-create="['module_ai:chat:create']"
:perm-delete="['module_ai:chat:delete']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
@add="handleAdd"
@delete="handleBatchDelete"
/>
</template>
@@ -247,6 +248,8 @@ const editingTitle = ref("");
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const { selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection<ChatSession>();
const createLoading = ref(false);
async function deleteSessionRow(id: string) {
try {
await confirmDelete();
@@ -437,6 +440,15 @@ async function handleCloseDialog() {
await resetForm();
}
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
async function handleOpenDialog(type: "create" | "detail", id?: string) {
await resetForm();
dialogVisible.type = type;
@@ -38,7 +38,8 @@
:perm-delete="['module_example:demo:delete']"
:perm-patch="['module_example:demo:patch']"
:delete-loading="batchDeleting"
@add="openEditDialog('add')"
:create-loading="createLoading"
@add="handleAdd"
@import="openImport"
@export="openExport"
@delete="handleBatchDelete"
@@ -250,6 +251,8 @@ const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(n
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DemoTable>();
const createLoading = ref(false);
const {
columns,
columnChecks,
@@ -580,6 +583,15 @@ async function openDetailDialog(row: DemoTable) {
dialogVisible.visible = true;
}
async function handleAdd() {
createLoading.value = true;
try {
await openEditDialog("add");
} finally {
createLoading.value = false;
}
}
async function openEditDialog(type: "add" | "edit", row?: DemoTable) {
dialogVisible.type = type === "add" ? "create" : "update";
if (type === "add") {
@@ -679,7 +691,7 @@ async function handleBatchDelete() {
}
}
async function runBatchStatus(status: string) {
async function runBatchStatus(status: number) {
const ids = selectedIds.value;
if (ids.length === 0) {
ElMessage.warning("请先在列表中勾选数据");
@@ -687,7 +699,7 @@ async function runBatchStatus(status: string) {
}
try {
await confirmAction(
`确认对选中的 ${ids.length} 条数据${status === "0" ? "启用" : "停用"}`,
`确认对选中的 ${ids.length} 条数据${status === 0 ? "启用" : "停用"}`,
"批量设置"
);
await DemoAPI.batchDemo({ ids, status });
@@ -36,7 +36,8 @@
:perm-create="['module_platform:email:update']"
:perm-delete="['module_platform:email:update']"
:delete-loading="configBatchDeleting"
@add="openConfigDialog('create')"
:create-loading="configCreateLoading"
@add="handleConfigAdd"
@delete="handleConfigBatchDelete"
/>
</template>
@@ -89,7 +90,8 @@
:perm-create="['module_platform:email:update']"
:perm-delete="['module_platform:email:update']"
:delete-loading="templateBatchDeleting"
@add="openTemplateDialog('create')"
:create-loading="templateCreateLoading"
@add="handleTemplateAdd"
@delete="handleTemplateBatchDelete"
/>
</template>
@@ -324,6 +326,8 @@ const {
onTableSelectionChange: onConfigSelectionChange,
} = useTableSelection<EmailConfigTable>();
const configCreateLoading = ref(false);
const {
columns: configColumns,
columnChecks: configColumnChecks,
@@ -448,6 +452,8 @@ const {
onTableSelectionChange: onTemplateSelectionChange,
} = useTableSelection<EmailTemplateTable>();
const templateCreateLoading = ref(false);
const {
columns: templateColumns,
columnChecks: templateColumnChecks,
@@ -699,6 +705,15 @@ const configDialogFormItems = computed<FormItem[]>(() => [
},
]);
async function handleConfigAdd() {
configCreateLoading.value = true;
try {
await openConfigDialog("create");
} finally {
configCreateLoading.value = false;
}
}
async function openConfigDialog(type: "create" | "update", row?: EmailConfigTable) {
configDialogVisible.type = type;
editingConfigId.value = row?.id ?? null;
@@ -832,6 +847,15 @@ const templateDialogFormItems = computed<FormItem[]>(() => [
},
]);
async function handleTemplateAdd() {
templateCreateLoading.value = true;
try {
await openTemplateDialog("create");
} finally {
templateCreateLoading.value = false;
}
}
async function openTemplateDialog(type: "create" | "update", row?: EmailTemplateTable) {
templateDialogVisible.type = type;
editingTemplateId.value = row?.id ?? null;
@@ -41,7 +41,9 @@
:perm-delete="['module_platform:menu:delete']"
:perm-patch="['module_platform:menu:patch']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
:more-loading="moreLoading"
@add="handleAdd"
@delete="handleBatchDelete"
@more="handleMoreClick"
/>
@@ -497,6 +499,8 @@ const selectedIds = computed(() =>
);
const batchDeleting = ref(false);
const submitLoading = ref(false);
const createLoading = ref(false);
const moreLoading = ref(false);
const menuOptions = ref<OptionType[]>([]);
const fullMenuTree = ref<MenuTable[]>([]);
@@ -1124,6 +1128,15 @@ async function handleCloseDialog() {
await resetForm();
}
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
async function handleOpenDialog(
type: "create" | "update" | "detail",
id?: number,
@@ -1222,13 +1235,13 @@ async function handleBatchDelete() {
}
}
async function handleMoreClick(status: string) {
async function handleMoreClick(status: number) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
return;
}
ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
ElMessageBox.confirm(`确认${status === 0 ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
@@ -1239,10 +1252,13 @@ async function handleMoreClick(status: string) {
cancelButtonText: "取消",
type: "warning",
});
moreLoading.value = true;
await MenuAPI.batchMenu({ ids, status });
await loadMenuData();
} catch {
//
} finally {
moreLoading.value = false;
}
}
@@ -35,7 +35,9 @@
:perm-delete="['module_package:package:delete']"
:perm-patch="['module_package:package:update']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
:more-loading="moreLoading"
@add="handleAdd"
@delete="handleBatchDelete"
@more="handleMoreClick"
/>
@@ -279,6 +281,9 @@ const pkgSearchItems = computed<SearchFormItem[]>(() => [
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const { selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection<PackageTable>();
const createLoading = ref(false);
const moreLoading = ref(false);
const opCtx = {
onDetail: (id: number) => void handleOpenDialog("detail", id),
onEdit: (id: number) => void handleOpenDialog("update", id),
@@ -591,6 +596,15 @@ const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
onUpdateSuccess: refreshCreate,
});
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
async function deletePkgRow(id: number) {
try {
await confirmDelete();
@@ -606,7 +620,7 @@ async function togglePkgStatus(row: PackageTable) {
const newStatus = row.status === 0 ? 1 : 0;
const label = newStatus === 0 ? "启用" : "禁用";
try {
await confirmToggleStatus(label);
await confirmToggleStatus(newStatus);
await PackageAPI.batchPackageStatus({ ids: [row.id!], status: Number(newStatus) });
ElMessage.success(`${label}成功`);
await refreshData();
@@ -34,7 +34,8 @@
:perm-create="['module_system:tenant:create']"
:perm-delete="['module_system:tenant:delete']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
@add="handleAdd"
@delete="handleBatchDelete"
/>
</template>
@@ -355,6 +356,8 @@ const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(n
const { selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection<TenantTable>();
const createLoading = ref(false);
async function deleteTenantRow(id: number) {
try {
await confirmDelete();
@@ -600,6 +603,15 @@ function brandCropBind(key: string) {
}
}
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
dialogVisible.type = type;
if (id) {
@@ -36,7 +36,9 @@
:perm-delete="['module_system:dept:delete']"
:perm-patch="['module_system:dept:patch']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
:more-loading="moreLoading"
@add="handleAdd"
@delete="handleBatchDelete"
@more="handleMoreClick"
/>
@@ -267,6 +269,9 @@ const deptOptions = ref<OptionType[]>([]);
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DeptTable>();
const createLoading = ref(false);
const moreLoading = ref(false);
async function loadDeptData() {
loading.value = true;
try {
@@ -378,6 +383,15 @@ const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } = use
},
});
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
const opCtx = {
onAddChild: (parentId: number) =>
void handleOpenDialog("create", undefined, { parent_id: parentId }),
@@ -513,7 +527,7 @@ async function handleBatchDelete() {
}
}
async function handleMoreClick(status: string) {
async function handleMoreClick(status: number) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
@@ -521,11 +535,14 @@ async function handleMoreClick(status: string) {
}
try {
await confirmToggleStatus(status);
moreLoading.value = true;
await DeptAPI.batchDept({ ids, status });
await loadDeptData();
await userStore.getUserInfo();
} catch {
//
} finally {
moreLoading.value = false;
}
}
@@ -41,7 +41,9 @@
:perm-delete="['module_system:dict_data:delete']"
:perm-patch="['module_system:dict_data:patch']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
:more-loading="moreLoading"
@add="handleAdd"
@export="openExport"
@delete="handleBatchDelete"
@more="handleMoreClick"
@@ -369,6 +371,9 @@ const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(n
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DictDataTable>();
const createLoading = ref(false);
const moreLoading = ref(false);
const {
columns,
columnChecks,
@@ -671,6 +676,15 @@ async function handleCloseDialog() {
await resetForm();
}
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
dialogVisible.type = type;
if (id) {
@@ -786,7 +800,7 @@ async function handleBatchDelete() {
}
}
async function handleMoreClick(status: string) {
async function handleMoreClick(status: number) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
@@ -794,12 +808,15 @@ async function handleMoreClick(status: string) {
}
try {
await confirmToggleStatus(status);
moreLoading.value = true;
await DictAPI.batchDictData({ ids, status });
await refreshData();
dictStore.clearDictData();
if (props.dictType) await dictStore.getDict([props.dictType]);
} catch {
//
} finally {
moreLoading.value = false;
}
}
</script>
@@ -36,7 +36,9 @@
:perm-delete="['module_system:dict_type:delete']"
:perm-patch="['module_system:dict_type:patch']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
:more-loading="moreLoading"
@add="handleAdd"
@export="openExport"
@delete="handleBatchDelete"
@more="handleMoreClick"
@@ -234,6 +236,9 @@ const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(n
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DictTable>();
const createLoading = ref(false);
const moreLoading = ref(false);
//
const { dialogVisible } = useCrudDialog();
@@ -306,6 +311,15 @@ const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } = use
},
});
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
const dictDialogFormItems = computed<FormItem[]>(() => [
{
label: "字典名称",
@@ -548,7 +562,7 @@ async function handleBatchDelete() {
}
}
async function handleMoreClick(status: string) {
async function handleMoreClick(status: number) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
@@ -556,6 +570,7 @@ async function handleMoreClick(status: string) {
}
try {
await confirmToggleStatus(status);
moreLoading.value = true;
await DictAPI.batchDictType({ ids, status });
await refreshData();
dictStore.clearDictData();
@@ -563,6 +578,8 @@ async function handleMoreClick(status: string) {
if (dictTypes.length > 0) await dictStore.getDict(dictTypes);
} catch {
//
} finally {
moreLoading.value = false;
}
}
</script>
@@ -45,7 +45,9 @@
:perm-delete="['module_system:notice:delete']"
:perm-patch="['module_system:notice:patch']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
:more-loading="moreLoading"
@add="handleAdd"
@export="openExport"
@delete="handleBatchDelete"
@more="handleMoreClick"
@@ -284,6 +286,9 @@ const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(n
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<NoticeTable>();
const createLoading = ref(false);
const moreLoading = ref(false);
//
const { dialogVisible } = useCrudDialog();
@@ -382,6 +387,15 @@ const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
},
});
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
const noticeDialogFormItems = computed<FormItem[]>(() => [
{
label: "标题",
@@ -648,7 +662,7 @@ async function handleBatchDelete() {
}
}
async function handleMoreClick(status: string) {
async function handleMoreClick(status: number) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
@@ -656,11 +670,14 @@ async function handleMoreClick(status: string) {
}
try {
await confirmToggleStatus(status);
moreLoading.value = true;
await NoticeAPI.batchNotice({ ids, status });
await refreshData();
await noticeStore.getNotice();
} catch {
ElMessage.info("操作取消");
//
} finally {
moreLoading.value = false;
}
}
@@ -35,7 +35,8 @@
:perm-export="['module_system:param:export']"
:perm-delete="['module_system:param:delete']"
:delete-loading="batchDeleting"
@add="handleOpenDialog('create')"
:create-loading="createLoading"
@add="handleAdd"
@export="openExport"
@delete="handleBatchDelete"
/>
@@ -233,6 +234,8 @@ const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(n
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<ConfigTable>();
const createLoading = ref(false);
//
const { dialogVisible } = useCrudDialog();
@@ -308,6 +311,15 @@ const { submitLoading, handleCloseDialog, handleOpenDialog, handleSubmit } =
},
});
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
const paramDialogFormItems = computed<FormItem[]>(() => [
{
label: "配置名称",

Some files were not shown because too many files have changed in this diff Show More