diff --git a/backend/README.md b/backend/README.md index 48278233..61bef087 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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 ``` ## 后端约定(日期与序列化) diff --git a/backend/app/api/v1/module_common/monitoring/schema.py b/backend/app/api/v1/module_common/monitoring/schema.py index 3d999371..e628e827 100644 --- a/backend/app/api/v1/module_common/monitoring/schema.py +++ b/backend/app/api/v1/module_common/monitoring/schema.py @@ -1,7 +1,3 @@ -"""健康检查 Schema""" - -from __future__ import annotations - from pydantic import BaseModel, Field diff --git a/backend/app/api/v1/module_monitor/server/service.py b/backend/app/api/v1/module_monitor/server/service.py index bee5ec8a..6d3f27fb 100644 --- a/backend/app/api/v1/module_monitor/server/service.py +++ b/backend/app/api/v1/module_monitor/server/service.py @@ -1,3 +1,4 @@ + import platform import socket import time diff --git a/backend/app/api/v1/module_platform/email/model.py b/backend/app/api/v1/module_platform/email/model.py index c072cb19..94c81104 100644 --- a/backend/app/api/v1/module_platform/email/model.py +++ b/backend/app/api/v1/module_platform/email/model.py @@ -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="备注") diff --git a/backend/app/api/v1/module_platform/email/service.py b/backend/app/api/v1/module_platform/email/service.py index 7a8f483c..d10a0c4d 100644 --- a/backend/app/api/v1/module_platform/email/service.py +++ b/backend/app/api/v1/module_platform/email/service.py @@ -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}」是默认配置,请先将其他配置设为默认后再删除") diff --git a/backend/app/api/v1/module_platform/invoice/controller.py b/backend/app/api/v1/module_platform/invoice/controller.py index 4639bf2a..7a9818e8 100644 --- a/backend/app/api/v1/module_platform/invoice/controller.py +++ b/backend/app/api/v1/module_platform/invoice/controller.py @@ -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="发票开具成功") diff --git a/backend/app/api/v1/module_platform/invoice/model.py b/backend/app/api/v1/module_platform/invoice/model.py index f88933e1..54927134 100644 --- a/backend/app/api/v1/module_platform/invoice/model.py +++ b/backend/app/api/v1/module_platform/invoice/model.py @@ -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="备注") diff --git a/backend/app/api/v1/module_platform/invoice/oss_licenses.json b/backend/app/api/v1/module_platform/invoice/oss_licenses.json new file mode 100644 index 00000000..91e02fe2 --- /dev/null +++ b/backend/app/api/v1/module_platform/invoice/oss_licenses.json @@ -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" + } +] diff --git a/backend/app/api/v1/module_platform/invoice/oss_licenses_helper.py b/backend/app/api/v1/module_platform/invoice/oss_licenses_helper.py new file mode 100644 index 00000000..7ff00480 --- /dev/null +++ b/backend/app/api/v1/module_platform/invoice/oss_licenses_helper.py @@ -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 diff --git a/backend/app/api/v1/module_platform/invoice/pdf_helper.py b/backend/app/api/v1/module_platform/invoice/pdf_helper.py index c473b517..a120cc46 100644 --- a/backend/app/api/v1/module_platform/invoice/pdf_helper.py +++ b/backend/app/api/v1/module_platform/invoice/pdf_helper.py @@ -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" diff --git a/backend/app/api/v1/module_platform/invoice/schema.py b/backend/app/api/v1/module_platform/invoice/schema.py index 5733f705..38d47629 100644 --- a/backend/app/api/v1/module_platform/invoice/schema.py +++ b/backend/app/api/v1/module_platform/invoice/schema.py @@ -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 响应") diff --git a/backend/app/api/v1/module_platform/invoice/service.py b/backend/app/api/v1/module_platform/invoice/service.py index 61ede355..031398fb 100644 --- a/backend/app/api/v1/module_platform/invoice/service.py +++ b/backend/app/api/v1/module_platform/invoice/service.py @@ -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, ), ) diff --git a/backend/app/api/v1/module_platform/menu/controller.py b/backend/app/api/v1/module_platform/menu/controller.py index 7fe73575..d720b75b 100644 --- a/backend/app/api/v1/module_platform/menu/controller.py +++ b/backend/app/api/v1/module_platform/menu/controller.py @@ -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="批量修改菜单状态成功") diff --git a/backend/app/api/v1/module_platform/menu/service.py b/backend/app/api/v1/module_platform/menu/service.py index d90be7a0..f002ce00 100644 --- a/backend/app/api/v1/module_platform/menu/service.py +++ b/backend/app/api/v1/module_platform/menu/service.py @@ -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: diff --git a/backend/app/api/v1/module_platform/order/controller.py b/backend/app/api/v1/module_platform/order/controller.py index 293dff06..4dbdc6f2 100644 --- a/backend/app/api/v1/module_platform/order/controller.py +++ b/backend/app/api/v1/module_platform/order/controller.py @@ -1,5 +1,3 @@ -"""订单与支付 Controller""" - from typing import Annotated from fastapi import APIRouter, Body, Depends, Path, Query, Request diff --git a/backend/app/api/v1/module_platform/order/model.py b/backend/app/api/v1/module_platform/order/model.py index 2f1c490c..0a3b37f4 100644 --- a/backend/app/api/v1/module_platform/order/model.py +++ b/backend/app/api/v1/module_platform/order/model.py @@ -1,5 +1,3 @@ -"""订单与支付 Model""" - from datetime import datetime from typing import TYPE_CHECKING diff --git a/backend/app/api/v1/module_platform/order/schema.py b/backend/app/api/v1/module_platform/order/schema.py index 9b5c84df..4004702c 100644 --- a/backend/app/api/v1/module_platform/order/schema.py +++ b/backend/app/api/v1/module_platform/order/schema.py @@ -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): diff --git a/backend/app/api/v1/module_platform/order/service.py b/backend/app/api/v1/module_platform/order/service.py index 3dcf5a3b..ea6a5994 100644 --- a/backend/app/api/v1/module_platform/order/service.py +++ b/backend/app/api/v1/module_platform/order/service.py @@ -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: """ 发送购买确认邮件(失败静默降级) diff --git a/backend/app/api/v1/module_platform/package/controller.py b/backend/app/api/v1/module_platform/package/controller.py index 7d4d7103..a3581179 100644 --- a/backend/app/api/v1/module_platform/package/controller.py +++ b/backend/app/api/v1/module_platform/package/controller.py @@ -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( diff --git a/backend/app/api/v1/module_platform/package/service.py b/backend/app/api/v1/module_platform/package/service.py index b4c387d4..ee9f181d 100644 --- a/backend/app/api/v1/module_platform/package/service.py +++ b/backend/app/api/v1/module_platform/package/service.py @@ -1,3 +1,4 @@ + import sqlalchemy as sa from sqlalchemy import func, select diff --git a/backend/app/api/v1/module_platform/plugin/controller.py b/backend/app/api/v1/module_platform/plugin/controller.py index 3c01a16c..2db667ec 100644 --- a/backend/app/api/v1/module_platform/plugin/controller.py +++ b/backend/app/api/v1/module_platform/plugin/controller.py @@ -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="重载成功") diff --git a/backend/app/api/v1/module_platform/self_service/controller.py b/backend/app/api/v1/module_platform/self_service/controller.py index b543dd90..069d0038 100644 --- a/backend/app/api/v1/module_platform/self_service/controller.py +++ b/backend/app/api/v1/module_platform/self_service/controller.py @@ -1,5 +1,3 @@ -"""租户自助服务 Controller — 对应 PRD §20.20""" - from typing import Annotated from fastapi import APIRouter, Depends, Path, Query diff --git a/backend/app/api/v1/module_platform/self_service/schema.py b/backend/app/api/v1/module_platform/self_service/schema.py index 70de6b38..f1a5b205 100644 --- a/backend/app/api/v1/module_platform/self_service/schema.py +++ b/backend/app/api/v1/module_platform/self_service/schema.py @@ -1,5 +1,3 @@ -"""租户自助服务 Schema""" - from typing import Literal from pydantic import BaseModel, ConfigDict, Field diff --git a/backend/app/api/v1/module_platform/self_service/service.py b/backend/app/api/v1/module_platform/self_service/service.py index 6200f86c..d6cf8b3e 100644 --- a/backend/app/api/v1/module_platform/self_service/service.py +++ b/backend/app/api/v1/module_platform/self_service/service.py @@ -1,4 +1,3 @@ -"""租户自助服务 Service""" from datetime import datetime, timedelta diff --git a/backend/app/api/v1/module_platform/tenant/controller.py b/backend/app/api/v1/module_platform/tenant/controller.py index 211604a6..dabec466 100644 --- a/backend/app/api/v1/module_platform/tenant/controller.py +++ b/backend/app/api/v1/module_platform/tenant/controller.py @@ -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( diff --git a/backend/app/api/v1/module_platform/tenant/service.py b/backend/app/api/v1/module_platform/tenant/service.py index 8e18ff42..ea834db7 100644 --- a/backend/app/api/v1/module_platform/tenant/service.py +++ b/backend/app/api/v1/module_platform/tenant/service.py @@ -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)},操作失败") diff --git a/backend/app/api/v1/module_system/auth/controller.py b/backend/app/api/v1/module_system/auth/controller.py index 459e1b3b..c500235a 100644 --- a/backend/app/api/v1/module_system/auth/controller.py +++ b/backend/app/api/v1/module_system/auth/controller.py @@ -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="租户切换成功") diff --git a/backend/app/api/v1/module_system/auth/oauth_service.py b/backend/app/api/v1/module_system/auth/oauth_service.py index afd93e23..44ddc1c8 100644 --- a/backend/app/api/v1/module_system/auth/oauth_service.py +++ b/backend/app/api/v1/module_system/auth/oauth_service.py @@ -8,8 +8,6 @@ 环境变量见 Settings 中 OAUTH_* 字段。 """ -from __future__ import annotations - import json import secrets from typing import Any, Literal diff --git a/backend/app/api/v1/module_system/auth/service.py b/backend/app/api/v1/module_system/auth/service.py index 4c0388bf..a366efca 100644 --- a/backend/app/api/v1/module_system/auth/service.py +++ b/backend/app/api/v1/module_system/auth/service.py @@ -1,3 +1,4 @@ + import json import secrets import uuid diff --git a/backend/app/api/v1/module_system/dept/controller.py b/backend/app/api/v1/module_system/dept/controller.py index 6a6490ec..114f85c9 100644 --- a/backend/app/api/v1/module_system/dept/controller.py +++ b/backend/app/api/v1/module_system/dept/controller.py @@ -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="批量修改部门状态成功") diff --git a/backend/app/api/v1/module_system/dept/service.py b/backend/app/api/v1/module_system/dept/service.py index ec858aae..31d103ee 100644 --- a/backend/app/api/v1/module_system/dept/service.py +++ b/backend/app/api/v1/module_system/dept/service.py @@ -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: diff --git a/backend/app/api/v1/module_system/dict/controller.py b/backend/app/api/v1/module_system/dict/controller.py index 4106fa30..b1be6a8c 100644 --- a/backend/app/api/v1/module_system/dict/controller.py +++ b/backend/app/api/v1/module_system/dict/controller.py @@ -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) diff --git a/backend/app/api/v1/module_system/dict/crud.py b/backend/app/api/v1/module_system/dict/crud.py index 4d490ce2..6acc979f 100644 --- a/backend/app/api/v1/module_system/dict/crud.py +++ b/backend/app/api/v1/module_system/dict/crud.py @@ -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"}]) diff --git a/backend/app/api/v1/module_system/dict/service.py b/backend/app/api/v1/module_system/dict/service.py index 03cba9c9..ad6766e0 100644 --- a/backend/app/api/v1/module_system/dict/service.py +++ b/backend/app/api/v1/module_system/dict/service.py @@ -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( diff --git a/backend/app/api/v1/module_system/log/service.py b/backend/app/api/v1/module_system/log/service.py index 203e9ff7..23e11011 100644 --- a/backend/app/api/v1/module_system/log/service.py +++ b/backend/app/api/v1/module_system/log/service.py @@ -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: diff --git a/backend/app/api/v1/module_system/notice/controller.py b/backend/app/api/v1/module_system/notice/controller.py index 2f03fcb2..ce03f50b 100644 --- a/backend/app/api/v1/module_system/notice/controller.py +++ b/backend/app/api/v1/module_system/notice/controller.py @@ -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} 条") diff --git a/backend/app/api/v1/module_system/notice/service.py b/backend/app/api/v1/module_system/notice/service.py index b47aff32..5caa6976 100644 --- a/backend/app/api/v1/module_system/notice/service.py +++ b/backend/app/api/v1/module_system/notice/service.py @@ -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: diff --git a/backend/app/api/v1/module_system/params/controller.py b/backend/app/api/v1/module_system/params/controller.py index ef3d4654..e4e19e1e 100644 --- a/backend/app/api/v1/module_system/params/controller.py +++ b/backend/app/api/v1/module_system/params/controller.py @@ -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) diff --git a/backend/app/api/v1/module_system/params/service.py b/backend/app/api/v1/module_system/params/service.py index 23a5bf46..61619615 100644 --- a/backend/app/api/v1/module_system/params/service.py +++ b/backend/app/api/v1/module_system/params/service.py @@ -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: diff --git a/backend/app/api/v1/module_system/position/controller.py b/backend/app/api/v1/module_system/position/controller.py index 25735913..6849b8b8 100644 --- a/backend/app/api/v1/module_system/position/controller.py +++ b/backend/app/api/v1/module_system/position/controller.py @@ -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( diff --git a/backend/app/api/v1/module_system/position/service.py b/backend/app/api/v1/module_system/position/service.py index 3cda1c7e..fdd49342 100644 --- a/backend/app/api/v1/module_system/position/service.py +++ b/backend/app/api/v1/module_system/position/service.py @@ -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: diff --git a/backend/app/api/v1/module_system/role/controller.py b/backend/app/api/v1/module_system/role/controller.py index 5dc30bfa..9080ea7d 100644 --- a/backend/app/api/v1/module_system/role/controller.py +++ b/backend/app/api/v1/module_system/role/controller.py @@ -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( diff --git a/backend/app/api/v1/module_system/role/crud.py b/backend/app/api/v1/module_system/role/crud.py index a9a87fd3..08ad32d3 100644 --- a/backend/app/api/v1/module_system/role/crud.py +++ b/backend/app/api/v1/module_system/role/crud.py @@ -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 diff --git a/backend/app/api/v1/module_system/role/service.py b/backend/app/api/v1/module_system/role/service.py index 34f25db9..8e71a27e 100644 --- a/backend/app/api/v1/module_system/role/service.py +++ b/backend/app/api/v1/module_system/role/service.py @@ -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: diff --git a/backend/app/api/v1/module_system/ticket/service.py b/backend/app/api/v1/module_system/ticket/service.py index 458c9f8b..1df4ca67 100644 --- a/backend/app/api/v1/module_system/ticket/service.py +++ b/backend/app/api/v1/module_system/ticket/service.py @@ -1,3 +1,4 @@ + from sqlalchemy import select from app.api.v1.module_system.user.model import UserModel diff --git a/backend/app/api/v1/module_system/user/controller.py b/backend/app/api/v1/module_system/user/controller.py index 20e3f129..d2d30fbf 100644 --- a/backend/app/api/v1/module_system/user/controller.py +++ b/backend/app/api/v1/module_system/user/controller.py @@ -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( diff --git a/backend/app/api/v1/module_system/user/crud.py b/backend/app/api/v1/module_system/user/crud.py index 88f672cb..866f484c 100644 --- a/backend/app/api/v1/module_system/user/crud.py +++ b/backend/app/api/v1/module_system/user/crud.py @@ -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 = [] diff --git a/backend/app/api/v1/module_system/user/service.py b/backend/app/api/v1/module_system/user/service.py index 4290ad0b..88fcb156 100644 --- a/backend/app/api/v1/module_system/user/service.py +++ b/backend/app/api/v1/module_system/user/service.py @@ -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) diff --git a/backend/app/core/ap_scheduler.py b/backend/app/core/ap_scheduler.py index 5d0762cc..6353ba73 100644 --- a/backend/app/core/ap_scheduler.py +++ b/backend/app/core/ap_scheduler.py @@ -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), ) diff --git a/backend/app/core/base_crud.py b/backend/app/core/base_crud.py index ce15122f..6c062935 100644 --- a/backend/app/core/base_crud.py +++ b/backend/app/core/base_crud.py @@ -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) diff --git a/backend/app/core/cache_util.py b/backend/app/core/cache_util.py new file mode 100644 index 00000000..9a9f866e --- /dev/null +++ b/backend/app/core/cache_util.py @@ -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) diff --git a/backend/app/core/request_context.py b/backend/app/core/request_context.py index 50fb29fa..bc9f7642 100644 --- a/backend/app/core/request_context.py +++ b/backend/app/core/request_context.py @@ -1,7 +1,3 @@ -"""请求级上下文""" - -from __future__ import annotations - from contextvars import ContextVar, Token from dataclasses import dataclass from typing import Any diff --git a/backend/app/init_app.py b/backend/app/init_app.py index eb24e451..e937df2d 100644 --- a/backend/app/init_app.py +++ b/backend/app/init_app.py @@ -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) diff --git a/backend/app/plugin/module_ai/chat/service.py b/backend/app/plugin/module_ai/chat/service.py index e4b86521..f4b2c811 100644 --- a/backend/app/plugin/module_ai/chat/service.py +++ b/backend/app/plugin/module_ai/chat/service.py @@ -1,3 +1,4 @@ + from collections.abc import AsyncGenerator from datetime import datetime from typing import Any diff --git a/backend/app/plugin/module_example/demo/controller.py b/backend/app/plugin/module_example/demo/controller.py index e74e6295..ae33559f 100644 --- a/backend/app/plugin/module_example/demo/controller.py +++ b/backend/app/plugin/module_example/demo/controller.py @@ -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( diff --git a/backend/app/plugin/module_example/demo/service.py b/backend/app/plugin/module_example/demo/service.py index c0873d9d..ce7d8507 100644 --- a/backend/app/plugin/module_example/demo/service.py +++ b/backend/app/plugin/module_example/demo/service.py @@ -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: diff --git a/backend/app/plugin/module_generator/gencode/crud.py b/backend/app/plugin/module_generator/gencode/crud.py index 6d3ade9c..531a4505 100644 --- a/backend/app/plugin/module_generator/gencode/crud.py +++ b/backend/app/plugin/module_generator/gencode/crud.py @@ -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: """创建业务表字段。 diff --git a/backend/app/plugin/module_generator/gencode/service.py b/backend/app/plugin/module_generator/gencode/service.py index 9763c971..46e9b52b 100644 --- a/backend/app/plugin/module_generator/gencode/service.py +++ b/backend/app/plugin/module_generator/gencode/service.py @@ -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: diff --git a/backend/app/plugin/module_task/cronjob/job/crud.py b/backend/app/plugin/module_task/cronjob/job/crud.py index adcd43b3..54d45fa8 100644 --- a/backend/app/plugin/module_task/cronjob/job/crud.py +++ b/backend/app/plugin/module_task/cronjob/job/crud.py @@ -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: """ diff --git a/backend/app/plugin/module_task/cronjob/job/service.py b/backend/app/plugin/module_task/cronjob/job/service.py index 38a3a96b..74612ceb 100644 --- a/backend/app/plugin/module_task/cronjob/job/service.py +++ b/backend/app/plugin/module_task/cronjob/job/service.py @@ -1,3 +1,4 @@ + from app.core.ap_scheduler import SchedulerUtil from app.core.base_schema import AuthSchema from app.core.exceptions import CustomException diff --git a/backend/app/plugin/module_task/cronjob/node/controller.py b/backend/app/plugin/module_task/cronjob/node/controller.py index 76f538ac..dc602165 100644 --- a/backend/app/plugin/module_task/cronjob/node/controller.py +++ b/backend/app/plugin/module_task/cronjob/node/controller.py @@ -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) diff --git a/backend/app/plugin/module_task/cronjob/node/crud.py b/backend/app/plugin/module_task/cronjob/node/crud.py index c91e37da..9dd658f7 100644 --- a/backend/app/plugin/module_task/cronjob/node/crud.py +++ b/backend/app/plugin/module_task/cronjob/node/crud.py @@ -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: """ diff --git a/backend/app/plugin/module_task/cronjob/node/service.py b/backend/app/plugin/module_task/cronjob/node/service.py index f4574830..1f7f1bd2 100644 --- a/backend/app/plugin/module_task/cronjob/node/service.py +++ b/backend/app/plugin/module_task/cronjob/node/service.py @@ -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="请选择要操作的数据") diff --git a/backend/app/plugin/module_task/workflow/flows/crud.py b/backend/app/plugin/module_task/workflow/flows/crud.py index d6ca6fd6..03beb221 100644 --- a/backend/app/plugin/module_task/workflow/flows/crud.py +++ b/backend/app/plugin/module_task/workflow/flows/crud.py @@ -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: """ diff --git a/backend/app/plugin/module_task/workflow/flows/service.py b/backend/app/plugin/module_task/workflow/flows/service.py index fadd6e22..8534a6a2 100644 --- a/backend/app/plugin/module_task/workflow/flows/service.py +++ b/backend/app/plugin/module_task/workflow/flows/service.py @@ -1,3 +1,4 @@ + import asyncio from typing import Any diff --git a/backend/app/plugin/module_task/workflow/handlers/workflow_engine.py b/backend/app/plugin/module_task/workflow/handlers/workflow_engine.py index f74534ee..6eeafb35 100644 --- a/backend/app/plugin/module_task/workflow/handlers/workflow_engine.py +++ b/backend/app/plugin/module_task/workflow/handlers/workflow_engine.py @@ -1,7 +1,3 @@ -"""工作流 DAG 执行引擎 — 拓扑分层 + 并行执行""" - -from __future__ import annotations - import json from collections import defaultdict, deque from concurrent.futures import ThreadPoolExecutor diff --git a/backend/app/plugin/module_task/workflow/nodes/crud.py b/backend/app/plugin/module_task/workflow/nodes/crud.py index f30ca4db..804f9ad2 100644 --- a/backend/app/plugin/module_task/workflow/nodes/crud.py +++ b/backend/app/plugin/module_task/workflow/nodes/crud.py @@ -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: """ diff --git a/backend/app/plugin/module_task/workflow/nodes/service.py b/backend/app/plugin/module_task/workflow/nodes/service.py index a2699c18..86e2e737 100644 --- a/backend/app/plugin/module_task/workflow/nodes/service.py +++ b/backend/app/plugin/module_task/workflow/nodes/service.py @@ -1,3 +1,4 @@ + from app.core.base_schema import AuthSchema from app.core.exceptions import CustomException diff --git a/backend/app/scripts/data/platform_invoice.json b/backend/app/scripts/data/platform_invoice.json index fe15c023..8effc7b9 100644 --- a/backend/app/scripts/data/platform_invoice.json +++ b/backend/app/scripts/data/platform_invoice.json @@ -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": "创新工坊-数据大屏发票(待开具)" } ] diff --git a/backend/app/utils/jinja2_template_util.py b/backend/app/utils/jinja2_template_util.py index 7617391e..5f5e8565 100644 --- a/backend/app/utils/jinja2_template_util.py +++ b/backend/app/utils/jinja2_template_util.py @@ -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 diff --git a/backend/app/utils/payment.py b/backend/app/utils/payment.py index 5d4093fd..652b497b 100644 --- a/backend/app/utils/payment.py +++ b/backend/app/utils/payment.py @@ -1,7 +1,3 @@ -"""支付网关 — 抽象基类 + 支付宝 + Mock + 工厂""" - -from __future__ import annotations - import base64 import json import uuid diff --git a/backend/app/utils/pdf_generator.py b/backend/app/utils/pdf_generator.py index 3ec34ca3..bd3b1841 100644 --- a/backend/app/utils/pdf_generator.py +++ b/backend/app/utils/pdf_generator.py @@ -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)]) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index fdcf44a4..375615e6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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] diff --git a/backend/requirements.txt b/backend/requirements.txt index 6a84a07f..ab66ad46 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/backend/run_linux.sh b/backend/run_linux.sh deleted file mode 100755 index 302defce..00000000 --- a/backend/run_linux.sh +++ /dev/null @@ -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 diff --git a/backend/run_win.bat b/backend/run_win.bat deleted file mode 100755 index e22b5357..00000000 --- a/backend/run_win.bat +++ /dev/null @@ -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 diff --git a/backend/templates/invoice.jinja2 b/backend/templates/invoice/invoice.jinja2 similarity index 100% rename from backend/templates/invoice.jinja2 rename to backend/templates/invoice/invoice.jinja2 diff --git a/backend/templates/invoice/oss_license.jinja2 b/backend/templates/invoice/oss_license.jinja2 new file mode 100644 index 00000000..d4d45b30 --- /dev/null +++ b/backend/templates/invoice/oss_license.jinja2 @@ -0,0 +1,177 @@ + + +
+ ++ 兹声明,本产品 FastapiAdmin 企业管理后台 在开发与部署过程中使用了 {{ total_packages }} 个第三方开源软件包, + 涵盖 {{ groups | length }} 类许可证。本产品严格遵循各开源许可证的条款要求, + 在使用、修改、分发相关开源组件时保留原始版权声明及许可证文本。 + 本声明函随同电子发票一同提供给客户,便于客户进行开源合规审计与软件资产管理。 +
+ +| 包名 (Package) | +版本 (Version) | +
|---|---|
| {{ pkg.name }} | +{{ pkg.version }} | +
+ 1. 上述所有开源组件均通过官方包管理器(PyPI)合法获取,并保留其原始许可证文本;
+ 2. 本产品未对 GPL/AGPL 等强 copyleft 协议组件进行源码闭源分发;
+ 3. 各许可证全文可访问各组件官方仓库或开源许可证标准文本;
+ 4. 如客户在二次开发或再分发过程中对许可证合规有进一步要求,本平台可提供完整的 LicenseText 文本。
+
待处理
3
diff --git a/frontend/web/src/views/module_ai/memory/index.vue b/frontend/web/src/views/module_ai/memory/index.vue
index e2fba20f..83c03ccc 100644
--- a/frontend/web/src/views/module_ai/memory/index.vue
+++ b/frontend/web/src/views/module_ai/memory/index.vue
@@ -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"
/>
@@ -247,6 +248,8 @@ const editingTitle = ref("");
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const { selectedIds, batchDeleting, onTableSelectionChange } = useTableSelection