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