chore: 批量优化项目代码,修复多处细节问题

本次提交包含多项优化和修复:
1. 修复CRUD初始化参数传递、搜索参数处理逻辑
2. 更新环境配置中的大模型相关参数
3. 重构部分服务方法命名,统一代码风格
4. 新增多个枚举类型,补充模型关联关系和加载选项
5. 优化查询参数类实现,完善字段校验逻辑
6. 调整Pydantic模型字段注释和类型定义
7. 简化并移除冗余的CRUD方法实现
8. 新增超级管理员权限装饰器
9. 修复邮件日志模型的租户关联和字段定义
This commit is contained in:
zhangtao
2026-06-20 23:37:58 +08:00
parent bbe77930a8
commit 4e2b668d7b
96 changed files with 2914 additions and 1936 deletions
@@ -3,6 +3,7 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import JSONResponse
from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_params import PaginationQueryParam
@@ -20,14 +21,7 @@ from .schema import (
SelfOrderOut,
WorkspaceOut,
)
from .service import (
create_plugin_purchase_order,
create_self_order,
get_available_packages,
get_self_order_detail,
get_self_order_list,
preview_package_change,
)
from .service import SelfService
TenantSelfServiceRouter = APIRouter(
route_class=OperationLogRoute,
@@ -41,16 +35,16 @@ TenantSelfServiceRouter = APIRouter(
summary="可选套餐列表",
response_model=ResponseSchema[PackageAvailableOut],
)
async def package_available(
async def package_available_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:package:query"]))],
):
) -> JSONResponse:
"""
可选套餐列表
返回:
- SuccessResponse: 包含可用套餐列表的 JSON 响应。
- JSONResponse: 包含可用套餐列表的 JSON 响应。
"""
result = await get_available_packages(auth=auth, tenant_id=auth.tenant_id)
result = await SelfService.get_available_packages(auth=auth, tenant_id=auth.tenant_id)
return SuccessResponse(data=result, msg="查询成功")
@@ -59,10 +53,10 @@ async def package_available(
summary="套餐变更影响预览",
response_model=ResponseSchema[PackagePreviewOut],
)
async def package_preview(
async def package_preview_controller(
target_package_id: Annotated[int, Query(ge=1, description="目标套餐ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:package:query"]))],
):
) -> JSONResponse:
"""
套餐变更影响预览
@@ -70,9 +64,9 @@ async def package_preview(
- target_package_id (int): 目标套餐 ID。
返回:
- SuccessResponse: 包含套餐变更影响预览的 JSON 响应。
- JSONResponse: 包含套餐变更影响预览的 JSON 响应。
"""
result = await preview_package_change(auth=auth, tenant_id=auth.tenant_id, target_package_id=target_package_id)
result = await SelfService.preview_package_change(auth=auth, tenant_id=auth.tenant_id, target_package_id=target_package_id)
return SuccessResponse(data=result, msg="查询成功")
@@ -81,10 +75,10 @@ async def package_preview(
summary="创建自助订单",
response_model=ResponseSchema[SelfOrderOut],
)
async def order_create(
async def order_create_controller(
data: SelfOrderCreate,
auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:create"]))],
):
) -> JSONResponse:
"""
创建自助订单
@@ -92,9 +86,9 @@ async def order_create(
- data (SelfOrderCreate): 订单创建参数。
返回:
- SuccessResponse: 包含订单详情的 JSON 响应。
- JSONResponse: 包含订单详情的 JSON 响应。
"""
result = await create_self_order(auth=auth, tenant_id=auth.tenant_id, data=data)
result = await SelfService.create_self_order(auth=auth, tenant_id=auth.tenant_id, data=data)
return SuccessResponse(data=result, msg="订单创建成功")
@@ -103,10 +97,10 @@ async def order_create(
summary="购买付费插件",
response_model=ResponseSchema[SelfOrderOut],
)
async def plugin_purchase(
async def plugin_purchase_controller(
data: PluginPurchaseCreate,
auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:create"]))],
):
) -> JSONResponse:
"""
购买付费插件
@@ -114,9 +108,9 @@ async def plugin_purchase(
- data (PluginPurchaseCreate): 插件购买参数。
返回:
- SuccessResponse: 包含订单详情的 JSON 响应。
- JSONResponse: 包含订单详情的 JSON 响应。
"""
result = await create_plugin_purchase_order(auth=auth, tenant_id=auth.tenant_id, data=data)
result = await SelfService.create_plugin_purchase_order(auth=auth, tenant_id=auth.tenant_id, data=data)
return SuccessResponse(data=result, msg="插件订单创建成功")
@@ -125,10 +119,10 @@ async def plugin_purchase(
summary="我的订单列表",
response_model=ResponseSchema[SelfOrderListOut],
)
async def order_list(
async def order_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:query"]))],
):
) -> JSONResponse:
"""
我的订单列表
@@ -136,9 +130,9 @@ async def order_list(
- page (PaginationQueryParam): 分页查询参数。
返回:
- SuccessResponse: 包含分页订单列表的 JSON 响应。
- JSONResponse: 包含分页订单列表的 JSON 响应。
"""
result = await get_self_order_list(
result = await SelfService.get_self_order_list(
auth=auth,
tenant_id=auth.tenant_id,
page_no=page.page_no,
@@ -153,10 +147,10 @@ async def order_list(
summary="订单详情",
response_model=ResponseSchema[SelfOrderDetailOut],
)
async def order_detail(
async def order_detail_controller(
order_id: Annotated[int, Path(ge=1, description="订单ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["tenant:order:query"]))],
):
) -> JSONResponse:
"""
订单详情
@@ -164,9 +158,9 @@ async def order_detail(
- order_id (int): 订单 ID。
返回:
- SuccessResponse: 包含订单详情的 JSON 响应。
- JSONResponse: 包含订单详情的 JSON 响应。
"""
result = await get_self_order_detail(auth=auth, tenant_id=auth.tenant_id, order_id=order_id)
result = await SelfService.get_self_order_detail(auth=auth, tenant_id=auth.tenant_id, order_id=order_id)
return SuccessResponse(data=result, msg="查询成功")
@@ -175,16 +169,14 @@ async def order_detail(
summary="租户工作台概览",
response_model=ResponseSchema[WorkspaceOut],
)
async def tenant_workspace(
async def tenant_workspace_controller(
auth: Annotated[AuthSchema, Depends(get_current_user)],
):
) -> JSONResponse:
"""
租户工作台概览
返回:
- SuccessResponse: 包含工作台概览信息的 JSON 响应。
- JSONResponse: 包含工作台概览信息的 JSON 响应。
"""
from .service import get_workspace_data
result = await get_workspace_data(auth=auth, tenant_id=auth.tenant_id)
result = await SelfService.get_workspace_data(auth=auth, tenant_id=auth.tenant_id)
return SuccessResponse(data=result, msg="查询成功")
@@ -1,179 +1,231 @@
"""租户自助服务 Schema"""
from pydantic import BaseModel, Field
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
from app.common.enums import OrderTypeEnum
OrderType = OrderTypeEnum # 兼容旧代码的类型别名
PackageAction = Literal["buy", "renew", "upgrade", "downgrade"]
PayMethod = Literal["alipay", "wxpay", "free"]
class PackageAvailableItem(BaseModel):
"""可选套餐项"""
"""
可选套餐项
"""
id: int
name: str
price: int # 分
period: str # month/year
trial_days: int = 0
max_users: int = 0
max_roles: int = 0
max_depts: int = 0
max_storage_mb: int = 0
description: str | None = None
is_current: bool = False
available_actions: list[str] = [] # [buy, renew, upgrade, downgrade]
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="套餐ID")
name: str = Field(..., description="套餐名称")
price: int = Field(..., ge=0, description="价格(分)")
period: str = Field(..., description="计费周期(month/year)")
trial_days: int = Field(default=0, ge=0, description="试用天数")
max_users: int = Field(default=0, ge=0, description="最大用户数")
max_roles: int = Field(default=0, ge=0, description="最大角色数")
max_depts: int = Field(default=0, ge=0, description="最大部门数")
max_storage_mb: int = Field(default=0, ge=0, description="最大存储(MB)")
description: str | None = Field(default=None, description="套餐描述")
is_current: bool = Field(default=False, description="是否为当前套餐")
available_actions: list[PackageAction] = Field(default_factory=list, description="可执行操作列表")
class PackageAvailableOut(BaseModel):
"""可选套餐列表"""
"""
可选套餐列表
"""
current_package_id: int | None = None
packages: list[PackageAvailableItem]
model_config = ConfigDict(from_attributes=True)
class PackagePreviewQuery(BaseModel):
"""套餐变更预览请求"""
target_package_id: int = Field(..., ge=1)
current_package_id: int | None = Field(default=None, description="当前套餐ID")
packages: list[PackageAvailableItem] = Field(default_factory=list, description="可选套餐列表")
class PackagePreviewOut(BaseModel):
"""套餐变更预览结果"""
"""
套餐变更预览结果
"""
current_package: str = ""
target_package: str = ""
action: str = "" # upgrade/downgrade/buy/renew
amount: int = 0 # 分
period: str = ""
gained_menus: list[dict] = []
lost_menus: list[dict] = []
affected_roles: list[str] = []
affected_users: int = 0
model_config = ConfigDict(from_attributes=True)
current_package: str = Field(default="", description="当前套餐名称")
target_package: str = Field(default="", description="目标套餐名称")
action: PackageAction = Field(default="buy", description="操作类型")
amount: int = Field(default=0, ge=0, description="金额(分)")
period: str = Field(default="", description="计费周期")
gained_menus: list[dict] = Field(default_factory=list, description="新增菜单清单")
lost_menus: list[dict] = Field(default_factory=list, description="移除菜单清单")
affected_roles: list[str] = Field(default_factory=list, description="受影响的角色名")
affected_users: int = Field(default=0, ge=0, description="受影响用户数")
class SelfOrderCreate(BaseModel):
"""自助订单创建"""
"""
自助订单创建
"""
package_id: int = Field(..., ge=1)
order_type: str = Field(..., pattern="^(buy|renew|upgrade|downgrade)$")
package_id: int = Field(..., ge=1, description="套餐ID")
order_type: PackageAction = Field(..., description="订单类型(buy/renew/upgrade/downgrade)")
class PluginPurchaseCreate(BaseModel):
"""插件购买"""
"""
插件购买
"""
plugin_id: int = Field(..., ge=1, description="插件ID")
pay_method: str | None = Field(default=None, pattern=r"^(alipay|wxpay)?$")
# ─── 订单 ───
pay_method: PayMethod | None = Field(default=None, description="支付方式(alipay/wxpay/free)")
class SelfOrderOut(BaseModel):
"""自助订单创建结果"""
"""
自助订单创建结果
"""
order_id: int
order_no: str
amount: int
need_pay: bool
model_config = ConfigDict(from_attributes=True)
order_id: int = Field(..., description="订单ID")
order_no: str = Field(..., description="订单号")
amount: int = Field(..., ge=0, description="订单金额(分)")
need_pay: bool = Field(..., description="是否需要支付")
class SelfOrderListItem(BaseModel):
"""我的订单列表项"""
"""
我的订单列表项
"""
id: int
order_no: str
package_name: str = ""
order_type: str
amount: int
status: int
pay_method: str | None = None
pay_time: str | None = None
created_at: str | None = None
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="订单ID")
order_no: str = Field(..., description="订单号")
package_name: str = Field(default="", description="套餐名称")
order_type: OrderType = Field(..., description="订单类型")
amount: int = Field(..., ge=0, description="订单金额(分)")
status: int = Field(..., description="订单状态(0:待支付 1:已支付 2:已取消 3:已退款)")
pay_method: str | None = Field(default=None, description="支付方式")
pay_time: str | None = Field(default=None, description="支付时间")
created_at: str | None = Field(default=None, description="创建时间")
class SelfOrderListOut(BaseModel):
"""我的订单列表"""
"""
我的订单列表
"""
items: list[SelfOrderListItem]
total: int
page_no: int
page_size: int
model_config = ConfigDict(from_attributes=True)
items: list[SelfOrderListItem] = Field(default_factory=list, description="订单列表")
total: int = Field(..., ge=0, description="总记录数")
page_no: int = Field(..., ge=1, description="页码")
page_size: int = Field(..., ge=1, description="每页数量")
class SelfOrderDetailOut(BaseModel):
"""订单详情"""
"""
订单详情
"""
id: int
order_no: str
package_id: int | None = None
package_name: str = ""
amount: int
order_type: str
status: int
pay_method: str | None = None
pay_time: str | None = None
created_at: str | None = None
model_config = ConfigDict(from_attributes=True)
# ─── 工作台 ───
id: int = Field(..., description="订单ID")
order_no: str = Field(..., description="订单号")
package_id: int | None = Field(default=None, description="套餐ID")
package_name: str = Field(default="", description="套餐名称")
amount: int = Field(..., ge=0, description="订单金额(分)")
order_type: OrderType = Field(..., description="订单类型")
status: int = Field(..., description="订单状态(0:待支付 1:已支付 2:已取消 3:已退款)")
pay_method: str | None = Field(default=None, description="支付方式")
pay_time: str | None = Field(default=None, description="支付时间")
created_at: str | None = Field(default=None, description="创建时间")
class WorkspaceTenantInfo(BaseModel):
"""工作台-租户信息"""
"""
工作台-租户信息
"""
id: int
name: str
code: str
status: int
status_label: str
start_time: str | None = None
end_time: str | None = None
days_remaining: int = 0
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="租户ID")
name: str = Field(..., description="租户名称")
code: str = Field(..., description="租户编码")
status: int = Field(..., description="租户状态(0:正常 1:宽限期 2:已暂停 3:已冻结 4:已过期 5:已归档)")
status_label: str = Field(..., description="租户状态描述")
start_time: str | None = Field(default=None, description="开始时间")
end_time: str | None = Field(default=None, description="结束时间")
days_remaining: int = Field(default=0, description="剩余天数")
class WorkspacePackageInfo(BaseModel):
"""工作台-套餐信息"""
"""
工作台-套餐信息
"""
id: int
name: str
price: int
period: str
max_users: int
max_roles: int
max_depts: int
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="套餐ID")
name: str = Field(..., description="套餐名称")
price: int = Field(..., ge=0, description="价格(分)")
period: str = Field(..., description="计费周期")
max_users: int = Field(..., ge=0, description="最大用户数")
max_roles: int = Field(..., ge=0, description="最大角色数")
max_depts: int = Field(..., ge=0, description="最大部门数")
class WorkspaceUsagePercent(BaseModel):
"""工作台-用量百分比"""
"""
工作台-用量百分比
"""
users: float = 0
roles: float = 0
depts: float = 0
model_config = ConfigDict(from_attributes=True)
users: float = Field(default=0.0, ge=0, description="用户用量占比(%)")
roles: float = Field(default=0.0, ge=0, description="角色用量占比(%)")
depts: float = Field(default=0.0, ge=0, description="部门用量占比(%)")
class WorkspaceQuotaInfo(BaseModel):
"""工作台-配额用量"""
"""
工作台-配额用量
"""
max_users: int = 0
max_roles: int = 0
max_depts: int = 0
current_users: int = 0
current_roles: int = 0
current_depts: int = 0
usage_percent: WorkspaceUsagePercent = Field(default_factory=WorkspaceUsagePercent)
model_config = ConfigDict(from_attributes=True)
max_users: int = Field(default=0, ge=0, description="最大用户数")
max_roles: int = Field(default=0, ge=0, description="最大角色数")
max_depts: int = Field(default=0, ge=0, description="最大部门数")
current_users: int = Field(default=0, ge=0, description="当前用户数")
current_roles: int = Field(default=0, ge=0, description="当前角色数")
current_depts: int = Field(default=0, ge=0, description="当前部门数")
usage_percent: WorkspaceUsagePercent = Field(default_factory=WorkspaceUsagePercent, description="用量占比")
class WorkspaceOrderItem(BaseModel):
"""工作台-近期订单项"""
"""
工作台-近期订单项
"""
id: int
order_no: str
amount: int
order_type: str
status: int
created_at: str | None = None
model_config = ConfigDict(from_attributes=True)
id: int = Field(..., description="订单ID")
order_no: str = Field(..., description="订单号")
amount: int = Field(..., ge=0, description="订单金额(分)")
order_type: OrderType = Field(..., description="订单类型")
status: int = Field(..., description="订单状态(0:待支付 1:已支付 2:已取消 3:已退款)")
created_at: str | None = Field(default=None, description="创建时间")
class WorkspaceOut(BaseModel):
"""工作台概览"""
"""
工作台概览
"""
tenant: WorkspaceTenantInfo
package: WorkspacePackageInfo | None = None
quota: WorkspaceQuotaInfo
recent_orders: list[WorkspaceOrderItem] = []
model_config = ConfigDict(from_attributes=True)
tenant: WorkspaceTenantInfo = Field(..., description="租户信息")
package: WorkspacePackageInfo | None = Field(default=None, description="当前套餐信息")
quota: WorkspaceQuotaInfo = Field(..., description="配额用量")
recent_orders: list[WorkspaceOrderItem] = Field(default_factory=list, description="近期订单(最多5条)")
@@ -5,9 +5,24 @@ from datetime import datetime, timedelta
from sqlalchemy import func, select
from app.api.v1.module_platform.menu.model import MenuModel
from app.api.v1.module_platform.order.crud import OrderCRUD
from app.api.v1.module_platform.order.model import OrderModel
from app.api.v1.module_platform.order.schema import (
OrderCreateInternalSchema,
OrderCreateSchema,
OrderUpdateInternalSchema,
)
from app.api.v1.module_platform.order.service import (
OrderService,
PaymentService,
_generate_order_no,
)
from app.api.v1.module_platform.package.model import PackageMenuModel, PackageModel
from app.api.v1.module_platform.plugin.model import PluginModel, TenantPluginModel
from app.api.v1.module_platform.tenant.model import TenantModel
from app.api.v1.module_system.dept.model import DeptModel
from app.api.v1.module_system.role.model import RoleModel
from app.api.v1.module_system.user.model import UserModel
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
@@ -31,420 +46,484 @@ from .schema import (
)
async def get_available_packages(auth: AuthSchema, tenant_id: int) -> PackageAvailableOut:
"""获取可选套餐列表"""
tenant = await auth.db.get(TenantModel, tenant_id)
current_pkg_id = tenant.package_id if tenant else None
# 一次性获取当前套餐价格(可能未启用,不在后续结果中)
current_price: int | None = None
if current_pkg_id:
cp = await auth.db.get(PackageModel, current_pkg_id)
current_price = cp.price if cp else 0
stmt = select(PackageModel).where(PackageModel.status == 0).order_by(PackageModel.price)
result = await auth.db.execute(stmt)
packages = result.scalars().all()
items: list[PackageAvailableItem] = []
for pkg in packages:
is_current = pkg.id == current_pkg_id
actions: list[str] = []
if is_current:
actions = ["renew"]
elif current_pkg_id is None:
actions = ["buy"]
elif current_price is not None:
actions = ["upgrade"] if pkg.price > current_price else ["downgrade"]
items.append(
PackageAvailableItem(
id=pkg.id,
name=pkg.name,
price=pkg.price,
period=pkg.period,
trial_days=pkg.trial_days,
max_users=pkg.max_users,
max_roles=pkg.max_roles,
max_depts=pkg.max_depts,
max_storage_mb=pkg.max_storage_mb,
description=pkg.description,
is_current=is_current,
available_actions=actions,
)
)
return PackageAvailableOut(
current_package_id=current_pkg_id,
packages=items,
)
async def preview_package_change(auth: AuthSchema, tenant_id: int, target_package_id: int) -> PackagePreviewOut:
"""套餐变更影响预览"""
tenant = await auth.db.get(TenantModel, tenant_id)
current_pkg_id = tenant.package_id if tenant else None
target_pkg = await auth.db.get(PackageModel, target_package_id)
if not target_pkg:
raise CustomException(msg="目标套餐不存在")
current_pkg = None
if current_pkg_id:
current_pkg = await auth.db.get(PackageModel, current_pkg_id)
# 获取当前套餐的菜单
current_menus = set()
if current_pkg_id:
cm_stmt = select(MenuModel).join(PackageMenuModel, MenuModel.id == PackageMenuModel.menu_id).where(PackageMenuModel.package_id == current_pkg_id)
result = await auth.db.execute(cm_stmt)
current_menus = {m.id for m in result.scalars().all()}
# 获取目标套餐的菜单
tm_stmt = select(MenuModel).join(PackageMenuModel, MenuModel.id == PackageMenuModel.menu_id).where(PackageMenuModel.package_id == target_package_id)
result = await auth.db.execute(tm_stmt)
target_menus = {m.id for m in result.scalars().all()}
# 计算差异
gained_menus, lost_menus = [], []
if current_menus:
lost_ids = current_menus - target_menus
if lost_ids:
lost_stmt = select(MenuModel).where(MenuModel.id.in_(lost_ids))
lost = await auth.db.execute(lost_stmt)
lost_menus = [{"id": m.id, "name": m.name, "path": m.route_path} for m in lost.scalars().all()]
gained_ids = target_menus - (current_menus or set())
if gained_ids:
gain_stmt = select(MenuModel).where(MenuModel.id.in_(gained_ids))
gained = await auth.db.execute(gain_stmt)
gained_menus = [{"id": m.id, "name": m.name, "path": m.route_path} for m in gained.scalars().all()]
# 确定操作类型
if current_pkg_id is None:
action = "buy"
elif current_pkg and target_pkg.price > current_pkg.price:
action = "upgrade"
elif current_pkg and target_pkg.price < current_pkg.price:
action = "downgrade"
else:
action = "renew"
# 受影响角色
affected_roles = []
if current_pkg_id:
role_stmt = select(RoleModel).where(RoleModel.tenant_id == tenant_id)
r_result = await auth.db.execute(role_stmt)
affected_roles = [r.name for r in r_result.scalars().all()]
return PackagePreviewOut(
current_package=current_pkg.name if current_pkg else "",
target_package=target_pkg.name,
action=action,
amount=target_pkg.price if hasattr(target_pkg, "price") else 0,
period=target_pkg.period if hasattr(target_pkg, "period") else "month",
gained_menus=gained_menus,
lost_menus=lost_menus,
affected_roles=affected_roles,
affected_users=0,
)
async def create_self_order(auth: AuthSchema, tenant_id: int, data: SelfOrderCreate) -> SelfOrderOut:
"""创建自助订单"""
tenant = await auth.db.get(TenantModel, tenant_id)
if not tenant:
raise CustomException(msg="租户不存在")
if tenant.status not in (0, 1, 2):
raise CustomException(msg="租户状态不允许操作")
pkg = await auth.db.get(PackageModel, data.package_id)
if not pkg or pkg.status == 1:
raise CustomException(msg="套餐不可用")
amount = pkg.price if hasattr(pkg, "price") else 0
from app.api.v1.module_platform.order.crud import OrderCRUD
from app.api.v1.module_platform.order.schema import (
OrderCreateInternalSchema,
OrderUpdateInternalSchema,
)
from app.api.v1.module_platform.order.service import _generate_order_no
order = await OrderCRUD(auth).create(
OrderCreateInternalSchema(
order_no=_generate_order_no(),
tenant_id=tenant_id,
package_id=data.package_id,
order_type=data.order_type,
amount=amount,
expire_time=datetime.now() + timedelta(minutes=15),
)
)
await auth.db.flush()
# 免费订单自动激活
if amount == 0:
await OrderCRUD(auth).update(
order.id,
OrderUpdateInternalSchema(status=1, pay_method="free", pay_time=datetime.now()),
)
from app.api.v1.module_platform.order.service import PaymentService
await PaymentService._activate_tenant_package(auth, order)
logger.info(f"自助订单创建: order_no={order.order_no} tenant={tenant_id} amount={amount}")
return SelfOrderOut(
order_id=order.id,
order_no=order.order_no,
amount=amount,
need_pay=amount > 0,
)
async def create_plugin_purchase_order(auth: AuthSchema, tenant_id: int, data: PluginPurchaseCreate) -> SelfOrderOut:
"""创建插件购买订单(复用套餐订单流程)"""
from app.api.v1.module_platform.order.schema import OrderCreateSchema
from app.api.v1.module_platform.order.service import OrderService
from app.api.v1.module_platform.plugin.model import PluginModel, TenantPluginModel
# 校验插件存在且可用
plugin = await auth.db.get(PluginModel, data.plugin_id)
if not plugin or plugin.status == 1:
raise CustomException(msg="插件不可用")
if getattr(plugin, "price", 0) == 0:
raise CustomException(msg="免费插件可直接安装,无需购买")
# 校验未重复购买
existing = await auth.db.execute(
select(TenantPluginModel)
.where(
TenantPluginModel.tenant_id == tenant_id,
TenantPluginModel.plugin_id == data.plugin_id,
)
.limit(1)
)
tp = existing.scalar_one_or_none()
if tp and tp.purchased == "1":
raise CustomException(msg="您已购买此插件,可直接安装")
order_data = OrderCreateSchema(
tenant_id=tenant_id,
package_id=None,
plugin_id=data.plugin_id,
order_type="plugin",
pay_method=data.pay_method,
)
result = await OrderService.create_order(auth, order_data)
logger.info(f"插件购买订单创建: order_no={result.order_no} plugin={plugin.name}")
return SelfOrderOut(
order_id=result.id,
order_no=result.order_no,
amount=result.amount,
need_pay=result.amount > 0,
)
async def get_self_order_list(
auth: AuthSchema,
tenant_id: int,
page_no: int = 1,
page_size: int = 20,
order_by: list[dict] | None = None,
) -> SelfOrderListOut:
"""我的订单列表"""
from sqlalchemy import func as sa_func
from app.api.v1.module_platform.order.model import OrderModel
stmt = select(OrderModel).where(OrderModel.tenant_id == tenant_id).order_by(OrderModel.created_time.desc()).offset((page_no - 1) * page_size).limit(page_size)
result = await auth.db.execute(stmt)
orders = result.scalars().all()
# 查总数
total = (await auth.db.execute(select(sa_func.count(OrderModel.id)).where(OrderModel.tenant_id == tenant_id))).scalar() or 0
# 批量查询关联套餐,避免 N+1
package_ids = [o.package_id for o in orders if o.package_id]
pkg_map: dict[int, str] = {}
if package_ids:
pkg_result = await auth.db.execute(select(PackageModel.id, PackageModel.name).where(PackageModel.id.in_(package_ids)))
pkg_map = {row[0]: row[1] for row in pkg_result.all()}
items = []
for o in orders:
pkg_name = pkg_map.get(o.package_id, "") if o.package_id else ""
items.append(
SelfOrderListItem(
id=o.id,
order_no=o.order_no,
package_name=pkg_name,
order_type=o.order_type,
amount=o.amount,
status=o.status,
pay_method=o.pay_method,
pay_time=o.pay_time.isoformat() if o.pay_time else None,
created_at=o.created_time.isoformat() if o.created_time else None,
)
)
return SelfOrderListOut(
items=items,
total=total,
page_no=page_no,
page_size=page_size,
)
async def get_self_order_detail(auth: AuthSchema, tenant_id: int, order_id: int) -> SelfOrderDetailOut:
"""订单详情"""
from app.api.v1.module_platform.order.model import OrderModel
stmt = select(OrderModel).where(
OrderModel.id == order_id,
OrderModel.tenant_id == tenant_id,
)
result = await auth.db.execute(stmt)
order = result.scalar_one_or_none()
if not order:
raise CustomException(msg="订单不存在")
pkg_name = ""
if order.package_id:
p = await auth.db.get(PackageModel, order.package_id)
if p:
pkg_name = p.name
return SelfOrderDetailOut(
id=order.id,
order_no=order.order_no,
package_id=order.package_id,
package_name=pkg_name,
amount=order.amount,
order_type=order.order_type,
status=order.status,
pay_method=order.pay_method,
pay_time=order.pay_time.isoformat() if order.pay_time else None,
created_at=order.created_time.isoformat() if order.created_time else None,
)
async def get_workspace_data(auth: AuthSchema, tenant_id: int) -> WorkspaceOut:
"""获取租户工作台概览数据
聚合返回:租户信息、当前套餐、配额用量、近期订单。
class SelfService:
"""
租户自助服务
"""
from app.api.v1.module_platform.package.model import PackageModel
from app.api.v1.module_platform.tenant.model import TenantModel
from app.api.v1.module_system.dept.model import DeptModel
from app.api.v1.module_system.role.model import RoleModel
from app.api.v1.module_system.user.model import UserModel
tenant = await auth.db.get(TenantModel, tenant_id)
if not tenant:
return WorkspaceOut(
tenant=WorkspaceTenantInfo(id=0, name="", code="", status=0, status_label="未知"),
quota=WorkspaceQuotaInfo(),
@classmethod
async def get_available_packages(cls, auth: AuthSchema, tenant_id: int) -> PackageAvailableOut:
"""
获取可选套餐列表
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
返回:
- PackageAvailableOut: 可选套餐列表
"""
tenant = await auth.db.get(TenantModel, tenant_id)
current_pkg_id = tenant.package_id if tenant else None
# 一次性获取当前套餐价格(可能未启用,不在后续结果中)
current_price: int | None = None
if current_pkg_id:
cp = await auth.db.get(PackageModel, current_pkg_id)
current_price = cp.price if cp else 0
stmt = select(PackageModel).where(PackageModel.status == 0).order_by(PackageModel.price)
result = await auth.db.execute(stmt)
packages = result.scalars().all()
items: list[PackageAvailableItem] = []
for pkg in packages:
is_current = pkg.id == current_pkg_id
actions: list[str] = []
if is_current:
actions = ["renew"]
elif current_pkg_id is None:
actions = ["buy"]
elif current_price is not None:
actions = ["upgrade"] if pkg.price > current_price else ["downgrade"]
items.append(
PackageAvailableItem(
id=pkg.id,
name=pkg.name,
price=pkg.price,
period=pkg.period,
trial_days=pkg.trial_days,
max_users=pkg.max_users,
max_roles=pkg.max_roles,
max_depts=pkg.max_depts,
max_storage_mb=pkg.max_storage_mb,
description=pkg.description,
is_current=is_current,
available_actions=actions,
)
)
return PackageAvailableOut(
current_package_id=current_pkg_id,
packages=items,
)
# ── 套餐 ──
package = None
if tenant.package_id:
package = await auth.db.get(PackageModel, tenant.package_id)
@classmethod
async def preview_package_change(cls, auth: AuthSchema, tenant_id: int, target_package_id: int) -> PackagePreviewOut:
"""
套餐变更影响预览
# ── 用量计数 ──
async def _count(model_cls) -> int:
stmt = (
select(func.count())
.select_from(model_cls)
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
- target_package_id (int): 目标套餐ID
返回:
- PackagePreviewOut: 套餐变更预览结果
"""
tenant = await auth.db.get(TenantModel, tenant_id)
current_pkg_id = tenant.package_id if tenant else None
target_pkg = await auth.db.get(PackageModel, target_package_id)
if not target_pkg:
raise CustomException(msg="该数据不存在")
current_pkg = None
if current_pkg_id:
current_pkg = await auth.db.get(PackageModel, current_pkg_id)
# 获取当前套餐的菜单
current_menus = set()
if current_pkg_id:
cm_stmt = select(MenuModel).join(PackageMenuModel, MenuModel.id == PackageMenuModel.menu_id).where(PackageMenuModel.package_id == current_pkg_id)
result = await auth.db.execute(cm_stmt)
current_menus = {m.id for m in result.scalars().all()}
# 获取目标套餐的菜单
tm_stmt = select(MenuModel).join(PackageMenuModel, MenuModel.id == PackageMenuModel.menu_id).where(PackageMenuModel.package_id == target_package_id)
result = await auth.db.execute(tm_stmt)
target_menus = {m.id for m in result.scalars().all()}
# 计算差异
gained_menus, lost_menus = [], []
if current_menus:
lost_ids = current_menus - target_menus
if lost_ids:
lost_stmt = select(MenuModel).where(MenuModel.id.in_(lost_ids))
lost = await auth.db.execute(lost_stmt)
lost_menus = [{"id": m.id, "name": m.name, "path": m.route_path} for m in lost.scalars().all()]
gained_ids = target_menus - (current_menus or set())
if gained_ids:
gain_stmt = select(MenuModel).where(MenuModel.id.in_(gained_ids))
gained = await auth.db.execute(gain_stmt)
gained_menus = [{"id": m.id, "name": m.name, "path": m.route_path} for m in gained.scalars().all()]
# 确定操作类型
if current_pkg_id is None:
action = "buy"
elif current_pkg and target_pkg.price > current_pkg.price:
action = "upgrade"
elif current_pkg and target_pkg.price < current_pkg.price:
action = "downgrade"
else:
action = "renew"
# 受影响角色
affected_roles = []
if current_pkg_id:
role_stmt = select(RoleModel).where(RoleModel.tenant_id == tenant_id)
r_result = await auth.db.execute(role_stmt)
affected_roles = [r.name for r in r_result.scalars().all()]
return PackagePreviewOut(
current_package=current_pkg.name if current_pkg else "",
target_package=target_pkg.name,
action=action,
amount=target_pkg.price,
period=target_pkg.period,
gained_menus=gained_menus,
lost_menus=lost_menus,
affected_roles=affected_roles,
affected_users=0,
)
@classmethod
async def create_self_order(cls, auth: AuthSchema, tenant_id: int, data: SelfOrderCreate) -> SelfOrderOut:
"""
创建自助订单(套餐购买/续费/升级/降级;免费订单自动激活)
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
- data (SelfOrderCreate): 自助订单创建参数
返回:
- SelfOrderOut: 自助订单创建结果
"""
tenant = await auth.db.get(TenantModel, tenant_id)
if not tenant:
raise CustomException(msg="该数据不存在")
if tenant.status not in (0, 1, 2):
raise CustomException(msg="租户状态不允许操作")
pkg = await auth.db.get(PackageModel, data.package_id)
if not pkg or pkg.status == 1:
raise CustomException(msg="该数据不存在")
amount = pkg.price
order = await OrderCRUD(auth).create(
OrderCreateInternalSchema(
order_no=_generate_order_no(),
tenant_id=tenant_id,
package_id=data.package_id,
order_type=data.order_type,
amount=amount,
expire_time=datetime.now() + timedelta(minutes=15),
)
)
await auth.db.flush()
# 免费订单自动激活
if amount == 0:
await OrderCRUD(auth).update(
order.id,
OrderUpdateInternalSchema(status=1, pay_method="free", pay_time=datetime.now()),
)
await PaymentService._activate_tenant_package(auth, order)
logger.info(f"自助订单创建: order_no={order.order_no} tenant={tenant_id} amount={amount}")
return SelfOrderOut(
order_id=order.id,
order_no=order.order_no,
amount=amount,
need_pay=amount > 0,
)
@classmethod
async def create_plugin_purchase_order(cls, auth: AuthSchema, tenant_id: int, data: PluginPurchaseCreate) -> SelfOrderOut:
"""
创建插件购买订单(复用套餐订单流程)
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
- data (PluginPurchaseCreate): 插件购买参数
返回:
- SelfOrderOut: 订单创建结果
"""
# 校验插件存在且可用
plugin = await auth.db.get(PluginModel, data.plugin_id)
if not plugin or plugin.status == 1:
raise CustomException(msg="该数据不存在")
if plugin.price == 0:
raise CustomException(msg="免费插件可直接安装,无需购买")
# 校验未重复购买
existing = await auth.db.execute(
select(TenantPluginModel)
.where(
model_cls.tenant_id == tenant_id,
model_cls.is_deleted == False,
TenantPluginModel.tenant_id == tenant_id,
TenantPluginModel.plugin_id == data.plugin_id,
)
.limit(1)
)
return (await auth.db.execute(stmt)).scalar() or 0
tp = existing.scalar_one_or_none()
if tp and tp.purchased == "1":
raise CustomException(msg="您已购买此插件,可直接安装")
user_count = await _count(UserModel)
role_count = await _count(RoleModel)
dept_count = await _count(DeptModel)
order_data = OrderCreateSchema(
tenant_id=tenant_id,
package_id=None,
plugin_id=data.plugin_id,
order_type="plugin",
pay_method=data.pay_method,
)
result = await OrderService.create_order(auth=auth, data=order_data)
# ── 到期状态 ──
now = datetime.now()
days_remaining = (tenant.end_time - now).days if tenant.end_time else 0
logger.info(f"插件购买订单创建: order_no={result.order_no} plugin={plugin.name}")
return SelfOrderOut(
order_id=result.id,
order_no=result.order_no,
amount=result.amount,
need_pay=result.amount > 0,
)
status_labels = {
"0": "正常",
"1": "宽限期",
"2": "已暂停",
"3": "已冻结",
"4": "已过期",
"5": "已归档",
}
@classmethod
async def get_self_order_list(
cls,
auth: AuthSchema,
tenant_id: int,
page_no: int = 1,
page_size: int = 20,
order_by: list[dict] | None = None,
) -> SelfOrderListOut:
"""
我的订单列表
# ── 近期订单 ──
from app.api.v1.module_platform.order.model import OrderModel
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
- page_no (int): 页码
- page_size (int): 每页数量
- order_by (list[dict] | None): 排序参数
orders_stmt = select(OrderModel).where(OrderModel.tenant_id == tenant_id).order_by(OrderModel.created_time.desc()).limit(5)
orders_result = await auth.db.execute(orders_stmt)
recent_orders = []
for o in orders_result.scalars().all():
recent_orders.append(
WorkspaceOrderItem(
id=o.id,
order_no=o.order_no,
amount=o.amount,
order_type=o.order_type,
status=o.status,
created_at=o.created_time.isoformat() if o.created_time else None,
返回:
- SelfOrderListOut: 订单分页列表
"""
offset = (page_no - 1) * page_size
page_result = await OrderCRUD(auth).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"created_time": "desc"}],
search={"tenant_id": tenant_id},
)
# 批量查询关联套餐,避免 N+1
package_ids = [o.package_id for o in page_result.items if hasattr(o, "package_id") and o.package_id]
pkg_map: dict[int, str] = {}
if package_ids:
pkg_result = await auth.db.execute(select(PackageModel.id, PackageModel.name).where(PackageModel.id.in_(package_ids)))
pkg_map = {row[0]: row[1] for row in pkg_result.all()}
items = []
for o in page_result.items:
pkg_name = pkg_map.get(o.package_id, "") if hasattr(o, "package_id") and o.package_id else ""
items.append(
SelfOrderListItem(
id=o.id,
order_no=o.order_no,
package_name=pkg_name,
order_type=o.order_type,
amount=o.amount,
status=o.status,
pay_method=o.pay_method,
pay_time=o.pay_time.isoformat() if o.pay_time else None,
created_at=o.created_time.isoformat() if o.created_time else None,
)
)
return SelfOrderListOut(
items=items,
total=page_result.total,
page_no=page_no,
page_size=page_size,
)
return WorkspaceOut(
tenant=WorkspaceTenantInfo(
id=tenant.id,
name=tenant.name,
code=tenant.code,
status=tenant.status,
status_label=status_labels.get(str(tenant.status), "未知"),
start_time=tenant.start_time.isoformat() if tenant.start_time else None,
end_time=tenant.end_time.isoformat() if tenant.end_time else None,
days_remaining=max(days_remaining, 0),
),
package=WorkspacePackageInfo(
id=package.id,
name=package.name,
price=package.price,
period=package.period,
max_users=package.max_users,
max_roles=package.max_roles,
max_depts=package.max_depts,
@classmethod
async def get_self_order_detail(cls, auth: AuthSchema, tenant_id: int, order_id: int) -> SelfOrderDetailOut:
"""
订单详情
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
- order_id (int): 订单ID
返回:
- SelfOrderDetailOut: 订单详情
"""
order = await OrderCRUD(auth).get_or_404(id=order_id, msg="该数据不存在")
pkg_name = ""
if order.package_id:
p = await auth.db.get(PackageModel, order.package_id)
if p:
pkg_name = p.name
return SelfOrderDetailOut(
id=order.id,
order_no=order.order_no,
package_id=order.package_id,
package_name=pkg_name,
amount=order.amount,
order_type=order.order_type,
status=order.status,
pay_method=order.pay_method,
pay_time=order.pay_time.isoformat() if order.pay_time else None,
created_at=order.created_time.isoformat() if order.created_time else None,
)
if package
else None,
quota=WorkspaceQuotaInfo(
max_users=package.max_users if package else 0,
max_roles=package.max_roles if package else 0,
max_depts=package.max_depts if package else 0,
current_users=user_count,
current_roles=role_count,
current_depts=dept_count,
usage_percent=WorkspaceUsagePercent(
users=round(user_count / package.max_users * 100, 1) if package and package.max_users > 0 else 0,
roles=round(role_count / package.max_roles * 100, 1) if package and package.max_roles > 0 else 0,
depts=round(dept_count / package.max_depts * 100, 1) if package and package.max_depts > 0 else 0,
@classmethod
async def get_workspace_data(cls, auth: AuthSchema, tenant_id: int) -> WorkspaceOut:
"""
获取租户工作台概览(租户信息、套餐、配额用量、近期订单)
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
返回:
- WorkspaceOut: 工作台概览数据
"""
tenant = await auth.db.get(TenantModel, tenant_id)
if not tenant:
return WorkspaceOut(
tenant=WorkspaceTenantInfo(id=0, name="", code="", status=0, status_label="未知"),
quota=WorkspaceQuotaInfo(),
)
# ── 套餐 ──
package = None
if tenant.package_id:
package = await auth.db.get(PackageModel, tenant.package_id)
# ── 用量计数 ──
async def _count(model_cls) -> int:
stmt = (
select(func.count())
.select_from(model_cls)
.where(
model_cls.tenant_id == tenant_id,
model_cls.is_deleted.is_(False),
)
)
return (await auth.db.execute(stmt)).scalar() or 0
user_count = await _count(UserModel)
role_count = await _count(RoleModel)
dept_count = await _count(DeptModel)
# ── 到期状态 ──
now = datetime.now()
days_remaining = (tenant.end_time - now).days if tenant.end_time else 0
status_labels = {
"0": "正常",
"1": "宽限期",
"2": "已暂停",
"3": "已冻结",
"4": "已过期",
"5": "已归档",
}
# ── 近期订单 ──
orders_stmt = select(OrderModel).where(OrderModel.tenant_id == tenant_id).order_by(OrderModel.created_time.desc()).limit(5)
orders_result = await auth.db.execute(orders_stmt)
recent_orders = []
for o in orders_result.scalars().all():
recent_orders.append(
WorkspaceOrderItem(
id=o.id,
order_no=o.order_no,
amount=o.amount,
order_type=o.order_type,
status=o.status,
created_at=o.created_time.isoformat() if o.created_time else None,
)
)
return WorkspaceOut(
tenant=WorkspaceTenantInfo(
id=tenant.id,
name=tenant.name,
code=tenant.code,
status=tenant.status,
status_label=status_labels.get(str(tenant.status), "未知"),
start_time=tenant.start_time.isoformat() if tenant.start_time else None,
end_time=tenant.end_time.isoformat() if tenant.end_time else None,
days_remaining=max(days_remaining, 0),
),
),
recent_orders=recent_orders,
)
package=WorkspacePackageInfo(
id=package.id,
name=package.name,
price=package.price,
period=package.period,
max_users=package.max_users,
max_roles=package.max_roles,
max_depts=package.max_depts,
)
if package
else None,
quota=WorkspaceQuotaInfo(
max_users=package.max_users if package else 0,
max_roles=package.max_roles if package else 0,
max_depts=package.max_depts if package else 0,
current_users=user_count,
current_roles=role_count,
current_depts=dept_count,
usage_percent=WorkspaceUsagePercent(
users=round(user_count / package.max_users * 100, 1) if package and package.max_users > 0 else 0,
roles=round(role_count / package.max_roles * 100, 1) if package and package.max_roles > 0 else 0,
depts=round(dept_count / package.max_depts * 100, 1) if package and package.max_depts > 0 else 0,
),
),
recent_orders=recent_orders,
)
@classmethod
async def get_tenant_api_usage_today(cls, auth: AuthSchema, tenant_id: int) -> dict:
"""
获取租户当日 API 用量汇总(已移除 api_usage 模块)
async def get_tenant_api_usage_today(auth: AuthSchema, tenant_id: int) -> dict:
"""获取租户当日 API 用量汇总(已移除 api_usage 模块)"""
return {"today": {"request_count": 0, "error_count": 0, "total_duration_ms": 0}, "month": {"total_calls": 0}, "top_paths": []}
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
返回:
- dict: API 用量占位数据
"""
return {
"today": {"request_count": 0, "error_count": 0, "total_duration_ms": 0},
"month": {"total_calls": 0},
"top_paths": [],
}
async def get_tenant_api_usage_daily(auth: AuthSchema, tenant_id: int, days: int = 7) -> dict:
"""获取租户每日 API 用量(已移除 api_usage 模块)"""
return {"daily": []}
@classmethod
async def get_tenant_api_usage_daily(cls, auth: AuthSchema, tenant_id: int, days: int = 7) -> dict:
"""
获取租户每日 API 用量(已移除 api_usage 模块)
参数:
- auth (AuthSchema): 认证信息模型
- tenant_id (int): 租户ID
- days (int): 查询天数
返回:
- dict: 每日用量占位数据
"""
return {"daily": []}