mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 04:39:41 +00:00
chore: 批量优化项目代码,修复多处细节问题
本次提交包含多项优化和修复: 1. 修复CRUD初始化参数传递、搜索参数处理逻辑 2. 更新环境配置中的大模型相关参数 3. 重构部分服务方法命名,统一代码风格 4. 新增多个枚举类型,补充模型关联关系和加载选项 5. 优化查询参数类实现,完善字段校验逻辑 6. 调整Pydantic模型字段注释和类型定义 7. 简化并移除冗余的CRUD方法实现 8. 新增超级管理员权限装饰器 9. 修复邮件日志模型的租户关联和字段定义
This commit is contained in:
@@ -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": []}
|
||||
|
||||
Reference in New Issue
Block a user