refactor: 优化API路由和响应模型 feat(admin): 添加App用户管理接口 feat(sms): 实现阿里云短信服务集成 feat(email): 添加SMTP邮件发送功能 feat(upload): 支持文件上传接口 feat(rate-limiter): 实现手机号限流器 fix: 修复计算步骤入库问题 docs: 更新API文档和测试计划 chore: 更新依赖和配置
286 lines
10 KiB
Python
286 lines
10 KiB
Python
from typing import Optional, List
|
||
from tortoise.queryset import QuerySet
|
||
|
||
from app.core.crud import CRUDBase
|
||
from app.models.invoice import Invoice, InvoiceHeader, PaymentReceipt
|
||
from app.schemas.invoice import (
|
||
InvoiceCreate,
|
||
InvoiceUpdate,
|
||
InvoiceOut,
|
||
InvoiceList,
|
||
InvoiceHeaderCreate,
|
||
InvoiceHeaderOut,
|
||
UpdateStatus,
|
||
UpdateType,
|
||
PaymentReceiptCreate,
|
||
PaymentReceiptOut,
|
||
)
|
||
|
||
|
||
class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||
"""发票控制器"""
|
||
|
||
def __init__(self):
|
||
super().__init__(model=Invoice)
|
||
|
||
async def create_header(self, user_id: Optional[int], data: InvoiceHeaderCreate) -> InvoiceHeaderOut:
|
||
"""
|
||
创建发票抬头
|
||
参数:
|
||
user_id: 关联的 AppUser ID(可选)
|
||
data: 发票抬头创建数据
|
||
返回:
|
||
InvoiceHeaderOut: 抬头输出对象
|
||
"""
|
||
header = await InvoiceHeader.create(app_user_id=user_id, **data.model_dump())
|
||
return InvoiceHeaderOut.model_validate(header)
|
||
|
||
async def get_headers(self, user_id: Optional[int] = None) -> List[InvoiceHeaderOut]:
|
||
"""
|
||
获取发票抬头列表
|
||
参数:
|
||
user_id: 可筛选 AppUser 的抬头
|
||
返回:
|
||
List[InvoiceHeaderOut]: 抬头列表
|
||
"""
|
||
qs = InvoiceHeader.all()
|
||
if user_id is not None:
|
||
qs = qs.filter(app_user_id=user_id)
|
||
headers = await qs.order_by("-created_at")
|
||
return [InvoiceHeaderOut.model_validate(h) for h in headers]
|
||
|
||
async def get_header_by_id(self, id_: int) -> Optional[InvoiceHeaderOut]:
|
||
"""
|
||
根据ID获取抬头
|
||
参数:
|
||
id_: 抬头ID
|
||
返回:
|
||
InvoiceHeaderOut 或 None
|
||
"""
|
||
header = await InvoiceHeader.filter(id=id_).first()
|
||
return InvoiceHeaderOut.model_validate(header) if header else None
|
||
|
||
async def list(self, page: int = 1, page_size: int = 10, **filters) -> InvoiceList:
|
||
"""
|
||
获取发票列表(支持筛选与分页)
|
||
参数:
|
||
page: 页码
|
||
page_size: 每页数量
|
||
**filters: phone、company_name、tax_number、status、ticket_type、invoice_type、时间范围等
|
||
返回:
|
||
InvoiceList: 分页结果
|
||
"""
|
||
qs: QuerySet = self.model.all()
|
||
if filters.get("phone"):
|
||
qs = qs.filter(phone__icontains=filters["phone"])
|
||
if filters.get("company_name"):
|
||
qs = qs.filter(company_name__icontains=filters["company_name"])
|
||
if filters.get("tax_number"):
|
||
qs = qs.filter(tax_number__icontains=filters["tax_number"])
|
||
if filters.get("status"):
|
||
qs = qs.filter(status=filters["status"])
|
||
if filters.get("ticket_type"):
|
||
qs = qs.filter(ticket_type=filters["ticket_type"])
|
||
if filters.get("invoice_type"):
|
||
qs = qs.filter(invoice_type=filters["invoice_type"])
|
||
|
||
total = await qs.count()
|
||
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
|
||
|
||
items = [
|
||
InvoiceOut(
|
||
id=row.id,
|
||
created_at=row.created_at.isoformat() if row.created_at else "",
|
||
ticket_type=row.ticket_type,
|
||
invoice_type=row.invoice_type,
|
||
phone=row.phone,
|
||
email=row.email,
|
||
company_name=row.company_name,
|
||
tax_number=row.tax_number,
|
||
register_address=row.register_address,
|
||
register_phone=row.register_phone,
|
||
bank_name=row.bank_name,
|
||
bank_account=row.bank_account,
|
||
status=row.status,
|
||
app_user_id=row.app_user_id,
|
||
header_id=row.header_id,
|
||
wechat=row.wechat,
|
||
)
|
||
for row in rows
|
||
]
|
||
|
||
return InvoiceList(items=items, total=total, page=page, page_size=page_size)
|
||
|
||
async def update_status(self, data: UpdateStatus) -> Optional[InvoiceOut]:
|
||
"""
|
||
更新发票状态
|
||
参数:
|
||
data: 包含 id 与目标状态
|
||
返回:
|
||
更新后的发票输出或 None
|
||
"""
|
||
inv = await self.model.filter(id=data.id).first()
|
||
if not inv:
|
||
return None
|
||
inv.status = data.status
|
||
await inv.save()
|
||
return await self.get_out(inv.id)
|
||
|
||
async def update_type(self, id_: int, data: UpdateType) -> Optional[InvoiceOut]:
|
||
"""
|
||
更新发票类型(电子/纸质、专票/普票)
|
||
参数:
|
||
id_: 发票ID
|
||
data: 类型更新数据
|
||
返回:
|
||
更新后的发票输出或 None
|
||
"""
|
||
inv = await self.model.filter(id=id_).first()
|
||
if not inv:
|
||
return None
|
||
inv.ticket_type = data.ticket_type
|
||
inv.invoice_type = data.invoice_type
|
||
await inv.save()
|
||
return await self.get_out(inv.id)
|
||
|
||
async def create_receipt(self, invoice_id: int, data: PaymentReceiptCreate) -> PaymentReceiptOut:
|
||
"""
|
||
上传付款凭证
|
||
参数:
|
||
invoice_id: 发票ID
|
||
data: 凭证创建数据
|
||
返回:
|
||
PaymentReceiptOut
|
||
"""
|
||
receipt = await PaymentReceipt.create(invoice_id=invoice_id, **data.model_dump())
|
||
return PaymentReceiptOut(
|
||
id=receipt.id,
|
||
url=receipt.url,
|
||
note=receipt.note,
|
||
verified=receipt.verified,
|
||
created_at=receipt.created_at.isoformat() if receipt.created_at else "",
|
||
)
|
||
|
||
async def list_receipts(self, page: int = 1, page_size: int = 10, **filters) -> dict:
|
||
"""
|
||
对公转账记录列表
|
||
参数:
|
||
page: 页码
|
||
page_size: 每页数量
|
||
**filters: 提交时间范围、手机号、微信号、公司名称/税号、状态、开票类型等
|
||
返回:
|
||
dict: { items, total, page, page_size }
|
||
"""
|
||
qs = PaymentReceipt.all().prefetch_related("invoice")
|
||
# 通过关联发票进行筛选
|
||
if filters.get("phone"):
|
||
qs = qs.filter(invoice__phone__icontains=filters["phone"])
|
||
if filters.get("wechat"):
|
||
qs = qs.filter(invoice__wechat__icontains=filters["wechat"])
|
||
if filters.get("company_name"):
|
||
qs = qs.filter(invoice__company_name__icontains=filters["company_name"])
|
||
if filters.get("tax_number"):
|
||
qs = qs.filter(invoice__tax_number__icontains=filters["tax_number"])
|
||
if filters.get("status"):
|
||
qs = qs.filter(invoice__status=filters["status"])
|
||
if filters.get("ticket_type"):
|
||
qs = qs.filter(invoice__ticket_type=filters["ticket_type"])
|
||
if filters.get("invoice_type"):
|
||
qs = qs.filter(invoice__invoice_type=filters["invoice_type"])
|
||
|
||
total = await qs.count()
|
||
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
|
||
|
||
items = []
|
||
for r in rows:
|
||
inv = await r.invoice
|
||
items.append({
|
||
"submitted_at": r.created_at.isoformat() if r.created_at else "",
|
||
"receipt": {
|
||
"id": r.id,
|
||
"url": r.url,
|
||
"note": r.note,
|
||
"verified": r.verified,
|
||
},
|
||
"phone": inv.phone,
|
||
"wechat": inv.wechat,
|
||
"company_name": inv.company_name,
|
||
"tax_number": inv.tax_number,
|
||
"register_address": inv.register_address,
|
||
"register_phone": inv.register_phone,
|
||
"bank_name": inv.bank_name,
|
||
"bank_account": inv.bank_account,
|
||
"email": inv.email,
|
||
"ticket_type": inv.ticket_type,
|
||
"invoice_type": inv.invoice_type,
|
||
"status": inv.status,
|
||
})
|
||
|
||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||
|
||
async def get_receipt_by_id(self, id_: int) -> Optional[dict]:
|
||
"""
|
||
对公转账记录详情
|
||
参数:
|
||
id_: 付款凭证ID
|
||
返回:
|
||
dict 或 None
|
||
"""
|
||
r = await PaymentReceipt.filter(id=id_).first()
|
||
if not r:
|
||
return None
|
||
inv = await r.invoice
|
||
return {
|
||
"submitted_at": r.created_at.isoformat() if r.created_at else "",
|
||
"receipt": {
|
||
"id": r.id,
|
||
"url": r.url,
|
||
"note": r.note,
|
||
"verified": r.verified,
|
||
},
|
||
"phone": inv.phone,
|
||
"wechat": inv.wechat,
|
||
"company_name": inv.company_name,
|
||
"tax_number": inv.tax_number,
|
||
"register_address": inv.register_address,
|
||
"register_phone": inv.register_phone,
|
||
"bank_name": inv.bank_name,
|
||
"bank_account": inv.bank_account,
|
||
"email": inv.email,
|
||
"ticket_type": inv.ticket_type,
|
||
"invoice_type": inv.invoice_type,
|
||
"status": inv.status,
|
||
}
|
||
|
||
async def get_out(self, id_: int) -> Optional[InvoiceOut]:
|
||
"""
|
||
根据ID返回发票输出对象
|
||
参数:
|
||
id_: 发票ID
|
||
返回:
|
||
InvoiceOut 或 None
|
||
"""
|
||
inv = await self.model.filter(id=id_).first()
|
||
if not inv:
|
||
return None
|
||
return InvoiceOut(
|
||
id=inv.id,
|
||
created_at=inv.created_at.isoformat() if inv.created_at else "",
|
||
ticket_type=inv.ticket_type,
|
||
invoice_type=inv.invoice_type,
|
||
phone=inv.phone,
|
||
email=inv.email,
|
||
company_name=inv.company_name,
|
||
tax_number=inv.tax_number,
|
||
register_address=inv.register_address,
|
||
register_phone=inv.register_phone,
|
||
bank_name=inv.bank_name,
|
||
bank_account=inv.bank_account,
|
||
status=inv.status,
|
||
app_user_id=inv.app_user_id,
|
||
header_id=inv.header_id,
|
||
wechat=inv.wechat,
|
||
)
|
||
|
||
|
||
invoice_controller = InvoiceController() |