|
|
import random
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
from app.core.base_schema import AuthSchema
|
|
|
from app.core.exceptions import CustomException
|
|
|
from app.core.logger import logger
|
|
|
from app.utils.payment import create_payment_gateway
|
|
|
|
|
|
from .crud import OrderCRUD, PaymentRecordCRUD, RefundCRUD
|
|
|
from .model import OrderModel
|
|
|
from .schema import (
|
|
|
OrderCreateInternalSchema,
|
|
|
OrderCreateSchema,
|
|
|
OrderOutSchema,
|
|
|
OrderQueryParam,
|
|
|
OrderStatusMessage,
|
|
|
OrderUpdateInternalSchema,
|
|
|
PaymentCreateOut,
|
|
|
PaymentRecordCreateSchema,
|
|
|
PaymentRecordOutSchema,
|
|
|
PaymentStatusOut,
|
|
|
RefundApplySchema,
|
|
|
RefundCreateSchema,
|
|
|
RefundOutSchema,
|
|
|
RefundReviewSchema,
|
|
|
RefundUpdateSchema,
|
|
|
)
|
|
|
|
|
|
|
|
|
def _generate_order_no() -> str:
|
|
|
"""生成订单号:YYYYMMDD + 6位随机数"""
|
|
|
today = datetime.now().strftime("%Y%m%d")
|
|
|
suffix = str(random.randint(100000, 999999))
|
|
|
return f"{today}{suffix}"
|
|
|
|
|
|
|
|
|
def _generate_refund_no() -> str:
|
|
|
"""生成退款单号"""
|
|
|
today = datetime.now().strftime("%Y%m%d")
|
|
|
suffix = str(random.randint(100000, 999999))
|
|
|
return f"RF{today}{suffix}"
|
|
|
|
|
|
|
|
|
class OrderService:
|
|
|
"""
|
|
|
订单管理服务
|
|
|
"""
|
|
|
|
|
|
@classmethod
|
|
|
async def create_order(cls, auth: AuthSchema, data: OrderCreateSchema, amount: int | None = None) -> OrderOutSchema:
|
|
|
"""
|
|
|
创建订单
|
|
|
|
|
|
套餐订单:amount 从套餐价格自动计算
|
|
|
插件订单:amount 从插件价格自动计算
|
|
|
免费订单(amount=0):自动激活
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- data (OrderCreateSchema): 订单创建模型
|
|
|
- amount (int | None): 订单金额(分),None 时自动计算
|
|
|
|
|
|
返回:
|
|
|
- OrderOutSchema: 新创建的订单详情
|
|
|
"""
|
|
|
if amount is None:
|
|
|
if data.order_type == "plugin":
|
|
|
from app.api.v1.module_platform.plugin.model import PluginModel
|
|
|
|
|
|
plugin = await auth.db.get(PluginModel, data.plugin_id)
|
|
|
amount = plugin.price if plugin else 0
|
|
|
else:
|
|
|
from app.api.v1.module_platform.package.model import PackageModel
|
|
|
|
|
|
pkg = await auth.db.get(PackageModel, data.package_id)
|
|
|
amount = pkg.price if pkg else 0
|
|
|
|
|
|
order = await OrderCRUD(auth).create(
|
|
|
OrderCreateInternalSchema(
|
|
|
order_no=_generate_order_no(),
|
|
|
tenant_id=data.tenant_id,
|
|
|
package_id=data.package_id,
|
|
|
plugin_id=data.plugin_id,
|
|
|
order_type=data.order_type,
|
|
|
amount=amount,
|
|
|
expire_time=datetime.now() + timedelta(minutes=15),
|
|
|
)
|
|
|
)
|
|
|
|
|
|
# 免费订单自动激活
|
|
|
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)
|
|
|
await auth.db.refresh(order)
|
|
|
|
|
|
return OrderOutSchema.model_validate(order)
|
|
|
|
|
|
@classmethod
|
|
|
async def get_detail(cls, auth: AuthSchema, order_id: int) -> OrderOutSchema | None:
|
|
|
"""
|
|
|
订单详情
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- order_id (int): 订单ID
|
|
|
|
|
|
返回:
|
|
|
- OrderOutSchema | None: 订单详情,不存在时返回 None
|
|
|
"""
|
|
|
order = await OrderCRUD(auth).get_by_id(order_id)
|
|
|
return OrderOutSchema.model_validate(order) if order else None
|
|
|
|
|
|
@classmethod
|
|
|
async def get_list(
|
|
|
cls,
|
|
|
auth: AuthSchema,
|
|
|
page_no: int,
|
|
|
page_size: int,
|
|
|
search: OrderQueryParam,
|
|
|
order_by: list[dict[str, str]] | None = None,
|
|
|
) -> tuple[list, int]:
|
|
|
"""
|
|
|
订单列表
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- page_no (int): 当前页码
|
|
|
- page_size (int): 每页数量
|
|
|
- search (OrderQueryParam): 查询参数
|
|
|
- order_by (list[dict] | None): 排序字段
|
|
|
|
|
|
返回:
|
|
|
- tuple[list, int]: (订单列表, 总数)
|
|
|
"""
|
|
|
offset = (page_no - 1) * page_size
|
|
|
rows, total = await OrderCRUD(auth).query(
|
|
|
tenant_id=search.tenant_id,
|
|
|
status=search.status,
|
|
|
order_type=search.order_type,
|
|
|
offset=offset,
|
|
|
limit=page_size,
|
|
|
)
|
|
|
items = [OrderOutSchema.model_validate(r) for r in rows]
|
|
|
return items, total
|
|
|
|
|
|
@classmethod
|
|
|
async def cancel_order(cls, auth: AuthSchema, order_id: int) -> OrderStatusMessage:
|
|
|
"""
|
|
|
取消订单
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- order_id (int): 订单ID
|
|
|
|
|
|
返回:
|
|
|
- OrderStatusMessage: 取消结果
|
|
|
"""
|
|
|
crud = OrderCRUD(auth)
|
|
|
order = await crud.get_by_id(order_id)
|
|
|
if not order:
|
|
|
raise CustomException(msg="该数据不存在")
|
|
|
if order.status != 0:
|
|
|
raise CustomException(msg="仅待支付订单可取消")
|
|
|
await crud.update(order_id, OrderUpdateInternalSchema(status=2))
|
|
|
return OrderStatusMessage(id=order.id, status=2, message="已取消")
|
|
|
|
|
|
@classmethod
|
|
|
async def check_payment_status(cls, auth: AuthSchema, order_id: int) -> PaymentStatusOut:
|
|
|
"""
|
|
|
查询订单支付状态(供前端轮询用)
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- order_id (int): 订单ID
|
|
|
|
|
|
返回:
|
|
|
- PaymentStatusOut: 支付状态信息
|
|
|
"""
|
|
|
order = await OrderCRUD(auth).get_by_id(order_id)
|
|
|
if not order:
|
|
|
return PaymentStatusOut(exists=False)
|
|
|
return PaymentStatusOut(
|
|
|
exists=True,
|
|
|
order_id=order.id,
|
|
|
status=order.status,
|
|
|
paid=order.status == 1,
|
|
|
pay_method=order.pay_method,
|
|
|
pay_time=order.pay_time.isoformat() if order.pay_time else None,
|
|
|
)
|
|
|
|
|
|
@staticmethod
|
|
|
async def cancel_expired_orders() -> None:
|
|
|
from sqlalchemy import update as sa_update
|
|
|
|
|
|
from app.core.database import async_db_session
|
|
|
|
|
|
now = datetime.now()
|
|
|
async with async_db_session() as session:
|
|
|
async with session.begin():
|
|
|
result = await session.execute(
|
|
|
sa_update(OrderModel)
|
|
|
.where(OrderModel.status == 0)
|
|
|
.where(OrderModel.expire_time < now)
|
|
|
.where(OrderModel.is_deleted == False) # noqa: E712
|
|
|
.values(status=2)
|
|
|
)
|
|
|
logger.info(f"超时订单取消: 已取消 {result.rowcount} 条订单")
|
|
|
|
|
|
|
|
|
class PaymentService:
|
|
|
"""
|
|
|
支付管理服务
|
|
|
"""
|
|
|
|
|
|
@classmethod
|
|
|
async def create_payment(cls, auth: AuthSchema, order_id: int, method: str, notify_base_url: str) -> PaymentCreateOut:
|
|
|
"""
|
|
|
创建支付(调用支付网关)
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- order_id (int): 订单ID
|
|
|
- method (str): 支付方式(alipay/wxpay)
|
|
|
- notify_base_url (str): 回调基础URL
|
|
|
|
|
|
返回:
|
|
|
- PaymentCreateOut: 支付创建结果(支付URL/二维码)
|
|
|
"""
|
|
|
from app.api.v1.module_platform.package.model import PackageModel
|
|
|
|
|
|
order = await OrderCRUD(auth).get_by_id(order_id)
|
|
|
if not order:
|
|
|
raise CustomException(msg="该数据不存在")
|
|
|
if order.status != 0:
|
|
|
raise CustomException(msg="订单状态异常,无法支付")
|
|
|
if order.amount <= 0:
|
|
|
raise CustomException(msg="免费订单无需支付")
|
|
|
|
|
|
if order.order_type == "plugin":
|
|
|
from app.api.v1.module_platform.plugin.model import PluginModel
|
|
|
|
|
|
plugin = await auth.db.get(PluginModel, order.plugin_id)
|
|
|
subject = f"FastapiAdmin - 插件 {plugin.name}" if plugin else "FastapiAdmin 插件"
|
|
|
else:
|
|
|
pkg = await auth.db.get(PackageModel, order.package_id)
|
|
|
subject = f"FastapiAdmin - {pkg.name}" if pkg else "FastapiAdmin 套餐"
|
|
|
|
|
|
notify_url = f"{notify_base_url}/api/v1/platform/payment/callback/{method}" if method else ""
|
|
|
|
|
|
gateway = create_payment_gateway(method)
|
|
|
info = await gateway.create_payment(
|
|
|
order_no=order.order_no,
|
|
|
amount=order.amount,
|
|
|
subject=subject,
|
|
|
notify_url=notify_url,
|
|
|
)
|
|
|
return PaymentCreateOut(
|
|
|
pay_url=info.pay_url,
|
|
|
qr_code_url=info.qr_code_url,
|
|
|
trade_no=info.trade_no,
|
|
|
order_id=order.id,
|
|
|
order_no=order.order_no,
|
|
|
amount=order.amount,
|
|
|
)
|
|
|
|
|
|
@classmethod
|
|
|
async def handle_callback(cls, auth: AuthSchema, method: str, callback_data: dict) -> dict:
|
|
|
"""
|
|
|
处理支付回调
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- method (str): 支付方式
|
|
|
- callback_data (dict): 支付网关回调数据
|
|
|
|
|
|
返回:
|
|
|
- dict: 处理结果
|
|
|
"""
|
|
|
gateway = create_payment_gateway(method)
|
|
|
callback_result = await gateway.verify_callback(callback_data)
|
|
|
|
|
|
if not callback_result.verified:
|
|
|
logger.warning(f"支付回调验签失败: method={method} data={callback_data}")
|
|
|
raise CustomException(msg="支付回调验签失败")
|
|
|
|
|
|
order_no = callback_data.get("order_no") or callback_data.get("out_trade_no", "")
|
|
|
o_crud = OrderCRUD(auth)
|
|
|
order = None
|
|
|
if order_no:
|
|
|
order = await o_crud.get_by_order_no(order_no)
|
|
|
elif callback_result.order_id:
|
|
|
order = await o_crud.get_by_id(callback_result.order_id)
|
|
|
|
|
|
if not order:
|
|
|
raise CustomException(msg="该数据不存在")
|
|
|
if order.status != 0:
|
|
|
raise CustomException(msg="订单状态异常")
|
|
|
if order.amount != callback_result.amount and callback_result.amount > 0:
|
|
|
raise CustomException(msg="金额不一致")
|
|
|
|
|
|
pid = order.package_id
|
|
|
tid = order.tenant_id
|
|
|
otype = order.order_type
|
|
|
oid = order.id
|
|
|
|
|
|
await o_crud.update(
|
|
|
oid,
|
|
|
OrderUpdateInternalSchema(status=1, pay_method=method, pay_time=datetime.now()),
|
|
|
)
|
|
|
|
|
|
await PaymentRecordCRUD(auth).create(
|
|
|
PaymentRecordCreateSchema(
|
|
|
order_id=oid,
|
|
|
transaction_id=callback_result.transaction_id,
|
|
|
pay_method=method,
|
|
|
amount=callback_result.amount,
|
|
|
raw_response=str(callback_result.raw) if callback_result.raw else None,
|
|
|
pay_time=datetime.now(),
|
|
|
)
|
|
|
)
|
|
|
|
|
|
order.package_id = pid
|
|
|
order.tenant_id = tid
|
|
|
order.order_type = otype
|
|
|
await PaymentService._activate_tenant_package(auth, order)
|
|
|
|
|
|
logger.info(f"支付回调处理完成: order_id={oid} method={method} tenant_id={tid} type={otype}")
|
|
|
return {"order_id": oid, "status": 1, "message": "支付成功"}
|
|
|
|
|
|
@classmethod
|
|
|
async def _activate_tenant_package(cls, auth: AuthSchema, order: OrderModel) -> None:
|
|
|
"""
|
|
|
支付成功后激活套餐
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- order (OrderModel): 订单模型
|
|
|
|
|
|
返回:
|
|
|
- None
|
|
|
"""
|
|
|
from app.api.v1.module_platform.package.model import PackageModel
|
|
|
from app.api.v1.module_platform.tenant.model import TenantModel
|
|
|
|
|
|
if order.order_type == "plugin":
|
|
|
await PaymentService._activate_plugin(auth, order)
|
|
|
return
|
|
|
|
|
|
pkg = await auth.db.get(PackageModel, order.package_id)
|
|
|
if not pkg:
|
|
|
logger.warning(f"支付回调:套餐 {order.package_id} 不存在,跳过激活")
|
|
|
return
|
|
|
|
|
|
tenant = await auth.db.get(TenantModel, order.tenant_id)
|
|
|
if not tenant:
|
|
|
logger.warning(f"支付回调:租户 {order.tenant_id} 不存在,跳过激活")
|
|
|
return
|
|
|
|
|
|
now = datetime.now()
|
|
|
period_months = order.period_count or 1
|
|
|
duration = timedelta(days=30 * period_months)
|
|
|
|
|
|
if order.order_type == "new":
|
|
|
tenant.package_id = order.package_id
|
|
|
tenant.start_time = now
|
|
|
tenant.end_time = now + duration
|
|
|
tenant.status = 0
|
|
|
logger.info(f"租户[{tenant.name}]新开通 {pkg.name},有效期至 {tenant.end_time}")
|
|
|
|
|
|
elif order.order_type == "renew":
|
|
|
base = tenant.end_time if tenant.end_time and tenant.end_time > now else now
|
|
|
tenant.end_time = base + duration
|
|
|
tenant.status = 0
|
|
|
logger.info(f"租户[{tenant.name}]续费 {pkg.name},续至 {tenant.end_time}")
|
|
|
|
|
|
elif order.order_type in ("upgrade", "downgrade"):
|
|
|
if order.order_type == "downgrade":
|
|
|
await PaymentService._check_downgrade_quota(auth, order.tenant_id, pkg)
|
|
|
tenant.package_id = order.package_id
|
|
|
tenant.status = 0
|
|
|
logger.info(f"租户[{tenant.name}]套餐变更 {'升级' if order.order_type == 'upgrade' else '降级'} → {pkg.name}")
|
|
|
|
|
|
await auth.db.flush()
|
|
|
|
|
|
if tenant.contact_email:
|
|
|
await PaymentService._send_order_email(auth, order, pkg, tenant)
|
|
|
|
|
|
@classmethod
|
|
|
async def _activate_plugin(cls, auth: AuthSchema, order: OrderModel) -> None:
|
|
|
"""
|
|
|
支付成功后标记插件为已购买
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- order (OrderModel): 订单模型
|
|
|
|
|
|
返回:
|
|
|
- None
|
|
|
"""
|
|
|
from app.api.v1.module_platform.plugin.model import PluginModel, TenantPluginModel
|
|
|
|
|
|
plugin = await auth.db.get(PluginModel, order.plugin_id)
|
|
|
if not plugin:
|
|
|
logger.warning(f"支付回调:插件 {order.plugin_id} 不存在")
|
|
|
return
|
|
|
|
|
|
result = await auth.db.execute(
|
|
|
select(TenantPluginModel)
|
|
|
.where(
|
|
|
TenantPluginModel.tenant_id == order.tenant_id,
|
|
|
TenantPluginModel.plugin_id == order.plugin_id,
|
|
|
)
|
|
|
.limit(1)
|
|
|
)
|
|
|
existing = result.scalar_one_or_none()
|
|
|
if existing:
|
|
|
existing.purchased = "1"
|
|
|
else:
|
|
|
tp = TenantPluginModel(
|
|
|
tenant_id=order.tenant_id,
|
|
|
plugin_id=order.plugin_id,
|
|
|
purchased="1",
|
|
|
enabled="0",
|
|
|
installed_time=datetime.now(),
|
|
|
)
|
|
|
auth.db.add(tp)
|
|
|
|
|
|
await auth.db.flush()
|
|
|
logger.info(f"租户[{order.tenant_id}]已购买插件[{plugin.name}]")
|
|
|
|
|
|
from app.api.v1.module_platform.tenant.model import TenantModel
|
|
|
|
|
|
tenant = await auth.db.get(TenantModel, order.tenant_id)
|
|
|
if tenant and tenant.contact_email:
|
|
|
await PaymentService._send_order_email(auth, order, plugin, tenant, order_type_label="购买")
|
|
|
|
|
|
@classmethod
|
|
|
async def _check_downgrade_quota(cls, auth: AuthSchema, tenant_id: int, new_pkg: object) -> None:
|
|
|
"""
|
|
|
降级前检查:租户当前资源数是否超过新套餐限额
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- tenant_id (int): 租户ID
|
|
|
- new_pkg (object): 目标套餐
|
|
|
|
|
|
返回:
|
|
|
- None
|
|
|
"""
|
|
|
from sqlalchemy import func, select
|
|
|
|
|
|
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
|
|
|
|
|
|
checks = {
|
|
|
"用户": (UserModel, new_pkg.max_users),
|
|
|
"角色": (RoleModel, new_pkg.max_roles),
|
|
|
"部门": (DeptModel, new_pkg.max_depts),
|
|
|
}
|
|
|
|
|
|
for label, (model, limit) in checks.items():
|
|
|
if limit <= 0:
|
|
|
continue
|
|
|
count_stmt = (
|
|
|
select(func.count())
|
|
|
.select_from(model)
|
|
|
.where(
|
|
|
model.tenant_id == tenant_id,
|
|
|
model.is_deleted.is_(False),
|
|
|
)
|
|
|
)
|
|
|
result = await auth.db.execute(count_stmt)
|
|
|
current = result.scalar() or 0
|
|
|
if current > limit:
|
|
|
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:
|
|
|
"""
|
|
|
发送购买确认邮件(失败静默降级)
|
|
|
|
|
|
参数:
|
|
|
- order (OrderModel): 订单模型
|
|
|
- product (object): 商品(套餐/插件)
|
|
|
- tenant (object): 租户模型
|
|
|
- order_type_label (str): 订单类型文案
|
|
|
|
|
|
返回:
|
|
|
- None
|
|
|
"""
|
|
|
try:
|
|
|
from app.api.v1.module_platform.email.service import EmailSendService
|
|
|
|
|
|
if not order_type_label:
|
|
|
order_type_labels = {
|
|
|
"new": "开通",
|
|
|
"renew": "续费",
|
|
|
"upgrade": "升级",
|
|
|
"downgrade": "降级",
|
|
|
"plugin": "购买",
|
|
|
}
|
|
|
order_type_label = order_type_labels.get(order.order_type, order.order_type)
|
|
|
|
|
|
product_name = getattr(product, "name", "") if product else ""
|
|
|
amount_str = f"{order.amount / 100:.2f}" if order.amount else "0.00"
|
|
|
|
|
|
await EmailSendService(auth).send_by_template(
|
|
|
to_email=tenant.contact_email,
|
|
|
to_name=tenant.contact_name or tenant.name,
|
|
|
template_code="order_confirmation",
|
|
|
variables={
|
|
|
"tenant_name": tenant.name,
|
|
|
"order_no": order.order_no,
|
|
|
"package_name": product_name,
|
|
|
"order_type": order_type_label,
|
|
|
"amount": amount_str,
|
|
|
},
|
|
|
biz_type="order",
|
|
|
tenant_id=tenant.id,
|
|
|
)
|
|
|
except Exception:
|
|
|
pass # 邮件发送失败不阻塞业务流程
|
|
|
|
|
|
@classmethod
|
|
|
async def get_records(cls, auth: AuthSchema, offset: int, limit: int) -> tuple[list, int]:
|
|
|
"""
|
|
|
支付记录列表
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- offset (int): 偏移量
|
|
|
- limit (int): 每页数量
|
|
|
|
|
|
返回:
|
|
|
- tuple[list, int]: (支付记录列表, 总数)
|
|
|
"""
|
|
|
rows, total = await PaymentRecordCRUD(auth).query(offset, limit)
|
|
|
items = [PaymentRecordOutSchema.model_validate(r) for r in rows]
|
|
|
return items, total
|
|
|
|
|
|
|
|
|
class RefundService:
|
|
|
"""
|
|
|
退款管理服务
|
|
|
"""
|
|
|
|
|
|
@classmethod
|
|
|
async def apply(cls, auth: AuthSchema, data: RefundApplySchema, order_id: int) -> RefundOutSchema:
|
|
|
"""
|
|
|
申请退款
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- data (RefundApplySchema): 退款申请模型
|
|
|
- order_id (int): 订单ID
|
|
|
|
|
|
返回:
|
|
|
- RefundOutSchema: 退款记录详情
|
|
|
"""
|
|
|
o_crud = OrderCRUD(auth)
|
|
|
order = await o_crud.get_by_id(order_id)
|
|
|
if not order:
|
|
|
raise CustomException(msg="该数据不存在")
|
|
|
if order.status != 1:
|
|
|
raise CustomException(msg="仅已支付订单可退款")
|
|
|
if order.amount == 0:
|
|
|
raise CustomException(msg="免费套餐不支持退款")
|
|
|
if order.pay_time and (datetime.now() - order.pay_time).days > 7:
|
|
|
raise CustomException(msg="已超过 7 天退款时限")
|
|
|
existing = await RefundCRUD(auth).get_by_order_id(order_id)
|
|
|
if existing:
|
|
|
raise CustomException(msg="已存在退款申请")
|
|
|
refund = await RefundCRUD(auth).create(
|
|
|
RefundCreateSchema(
|
|
|
order_id=order.id,
|
|
|
refund_no=_generate_refund_no(),
|
|
|
amount=order.amount,
|
|
|
reason=data.reason,
|
|
|
)
|
|
|
)
|
|
|
return RefundOutSchema.model_validate(refund)
|
|
|
|
|
|
@classmethod
|
|
|
async def get_list(cls, auth: AuthSchema, status: int | None, offset: int, limit: int) -> tuple[list, int]:
|
|
|
"""
|
|
|
退款列表
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- status (int | None): 退款状态筛选
|
|
|
- offset (int): 偏移量
|
|
|
- limit (int): 每页数量
|
|
|
|
|
|
返回:
|
|
|
- tuple[list, int]: (退款列表, 总数)
|
|
|
"""
|
|
|
rows, total = await RefundCRUD(auth).query(status, offset, limit)
|
|
|
items = [RefundOutSchema.model_validate(r) for r in rows]
|
|
|
return items, total
|
|
|
|
|
|
@classmethod
|
|
|
async def approve(cls, auth: AuthSchema, refund_id: int, reviewer_id: int, operator_name: str = "") -> OrderStatusMessage:
|
|
|
"""
|
|
|
批准退款
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- refund_id (int): 退款ID
|
|
|
- reviewer_id (int): 审核人ID
|
|
|
- operator_name (str): 操作人名称
|
|
|
|
|
|
返回:
|
|
|
- OrderStatusMessage: 审核结果
|
|
|
"""
|
|
|
crud = RefundCRUD(auth)
|
|
|
refund = await crud.get_by_id(refund_id)
|
|
|
if not refund:
|
|
|
raise CustomException(msg="该数据不存在")
|
|
|
if refund.status != 1:
|
|
|
raise CustomException(msg="仅申请中可审核")
|
|
|
await crud.update(
|
|
|
refund_id,
|
|
|
RefundUpdateSchema(status=2, reviewer_id=reviewer_id, review_time=datetime.now()),
|
|
|
)
|
|
|
await OrderCRUD(auth).mark_refunded(refund.order_id)
|
|
|
return OrderStatusMessage(id=refund.id, status=2, message="已批准退款")
|
|
|
|
|
|
@classmethod
|
|
|
async def reject(
|
|
|
cls,
|
|
|
auth: AuthSchema,
|
|
|
refund_id: int,
|
|
|
reviewer_id: int,
|
|
|
data: RefundReviewSchema,
|
|
|
operator_name: str = "",
|
|
|
) -> OrderStatusMessage:
|
|
|
"""
|
|
|
驳回退款
|
|
|
|
|
|
参数:
|
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
- refund_id (int): 退款ID
|
|
|
- reviewer_id (int): 审核人ID
|
|
|
- data (RefundReviewSchema): 驳回原因
|
|
|
- operator_name (str): 操作人名称
|
|
|
|
|
|
返回:
|
|
|
- OrderStatusMessage: 审核结果
|
|
|
"""
|
|
|
crud = RefundCRUD(auth)
|
|
|
refund = await crud.get_by_id(refund_id)
|
|
|
if not refund:
|
|
|
raise CustomException(msg="该数据不存在")
|
|
|
if refund.status != 1:
|
|
|
raise CustomException(msg="仅申请中可审核")
|
|
|
await crud.update(
|
|
|
refund_id,
|
|
|
RefundUpdateSchema(
|
|
|
status=3,
|
|
|
reviewer_id=reviewer_id,
|
|
|
review_time=datetime.now(),
|
|
|
reject_reason=data.reject_reason,
|
|
|
),
|
|
|
)
|
|
|
return OrderStatusMessage(id=refund.id, status=3, message="已驳回")
|