Compare commits

...

2 Commits

Author SHA1 Message Date
c15f3e9925 fix(invoice): 移除发票创建模型中不必要的必填限制
refactor(valuations): 添加估值计算前的用户配额检查逻辑

docs: 添加发票抬头必填字段的修改方案文档
2025-11-26 18:17:47 +08:00
27b6276cdc feat(交易管理): 新增多文件上传及邮件发送事务支持
- 新增多文件上传接口及发票附件存储
- 邮件发送流程增加事务保障及完整日志记录
- 用户管理备注支持分维度更新并记录操作日志
- 覆盖单元测试与集成测试用例
2025-11-26 18:17:07 +08:00
4 changed files with 191 additions and 5 deletions

View File

@ -0,0 +1,131 @@
## 目标概述
* 加强“交易管理-发票附件与发送邮箱”能力,完善数据记录与事务保障。
* 改进“开票弹窗附件上传”支持多文件上传(后端存储结构)。
* 优化“用户管理备注与操作记录”,区分备注维度并完善日志。
* 覆盖单元/集成测试、数据库迁移、API文档与审计日志。
## 数据库与模型变更
* EmailSendLog
* 新增:`extra: JSONField(null=True)` 完整记录发送邮箱的请求数据(收件人、主题、正文、附件列表、重试信息等)。
* Invoice或与开票弹窗相关的业务模型
* 新增:`attachments: JSONField(null=True)` 支持多个附件URL与弹窗上传对应
* <br />
* 迁移脚本:创建/修改上述字段;保留历史数据不丢失。
## 事务与原子性
* 发送邮箱流程(交易管理)
* 封装在 `tortoise.transactions.in_transaction()`邮件发送、EmailSendLog写入含attachments/extra原子提交失败回滚。
* extra写入内容完整`SendEmailRequest`收件人、主题、正文、附件URL/文件名、重试次数、客户端UA等
* 多文件上传至发票附件(开票弹窗)
* 更新发票的 `attachments` 字段在同一事务内写入如任一URL校验失败则回滚。
## 后端接口改造
* 上传组件(后端)
* 新增:`POST /api/v1/upload/files` 接收 `files: List[UploadFile]`,统一返回 `urls: string[]`;保留现有单文件接口向后兼容。
* 交易管理(管理员)
* `POST /api/v1/transactions/send-email` 扩展:
* 入参支持 `file_urls: string[]`(与/或单文件),服务端聚合附件;
* 在事务中记录 EmailSendLog含attachments与extra返回log\_id与状态
* 回显接口(详情/列表)新增 `extra` 字段完整展示发送记录。
* 开票弹窗附件(管理员/或对应端)
* 新增/改造:`PUT /api/v1/invoice/{id}/attachments` 入参 `urls: string[]`,更新发票 `attachments`
* 列表/详情回显 `attachments: string[]`
* 用户管理备注优化(管理员端)
* 新接口:`PUT /api/v1/app-user-admin/{user_id}/notes`
* 入参:`system_notes?: string``user_notes?: string`(可选择性更新)
* 逻辑:仅更新提供的字段;不影响其他字段。
* 修复:`POST /api/v1/app-user-admin/quota` 仅调整次数,不再自动写入 `user.notes`
* 操作日志:在调整配额、更新备注、停用启用等操作时写入 `AppUserOperationLog`
## 前端改造(要点)
* 多文件上传组件
* 改为多选/拖拽支持;对每个文件显示上传进度与失败重试;
* 成功后收集URL数组写入发票 `attachments` 或作为邮件附件来源;
* 兼容旧接口若后端仅返回单URL前端仍正常显示降级为单文件模式
* 开票弹窗
* 支持附件列表预览与移除;提交时调用 `PUT /invoice/{id}/attachments`
* 邮件发送弹窗
* 选择附件来源(已上传的附件/本地文件再上传);提交后在详情页面完整回显 `extra`(含附件清单与正文等)。
## 审计与日志
* 关键操作:邮件发送、发票附件更新、用户备注更新、配额调整
* 统一调用审计记录(路由中间件已存在,补充结构化日志:`logger.info()` + DB审计表/操作日志表写入)。
## 测试方案
* 单元测试
* Email发送控制器事务成功/失败回滚(模拟抛错)
* 多文件上传文件类型校验、URL数组返回、尾随空白处理
* 备注更新:选择性字段更新、不影响其他字段
* 集成测试FastAPI + httpx.AsyncClient
* 发送邮箱请求→持久化校验attachments/extra→回显接口校验
* 附件上传:批量上传、更新发票、列表/详情回显
* 用户备注接口调用→DB值校验→操作日志存在
## 迁移与兼容
* 使用现有迁移工具如Aerich生成并应用迁移新增JSON/Text字段
* 前端保留旧接口兼容若上传仍走单文件后端返回数组长度1
* API文档OpenAPI
* 补充/更新以上端点的schema说明、示例请求/响应、错误码约定;
## 实施步骤与交付
1. 数据模型与迁移脚本编写与应用EmailSendLog、Invoice、AppUser/OperationLog
2. 后端接口改造与事务封装(邮件发送、多文件上传、发票附件更新、备注接口)。
3. 前端组件与弹窗改造多文件上传、进度与错误处理、回显extra
4. 审计日志与结构化日志补充。
5. 单元与集成测试编写,覆盖核心路径。
6. 更新接口文档与部署回归验证。

View File

@ -0,0 +1,34 @@
## 目标
* 添加抬头时将 `公司名称``公司税号``电子邮箱`设为必填,其他字段可为空。
* 现状确认(代码引用)
- 后端必填:`app/schemas/invoice.py:612``InvoiceHeaderCreate` 已要求 `company_name``tax_number` 为必填,`email: EmailStr` 为必填。
- API 入口:`app/api/v1/app_invoices/app_invoices.py:5558` 新增抬头接口使用 `InvoiceHeaderCreate`,后端将严格校验三项必填。
## 修改方案
1. 统一前端校验文案
* 统一三项必填的错误提示为简洁中文,如:“请输入公司名称 / 公司税号 / 电子邮箱”。
* 邮箱格式提示统一为:“请输入有效的电子邮箱”。
2. 后端校验与返回确认
* 保持 `InvoiceHeaderCreate` 的必填与格式限制不变(`app/schemas/invoice.py:612`)。
* 确认更新接口 `InvoiceHeaderUpdate``app/schemas/invoice.py:3239`)允许局部更新、但不影响创建必填逻辑。
3. 验证与测试
* 后端接口验证:对 `POST /app-invoices/headers``app/api/v1/app_invoices/app_invoices.py:5558`)进行用例:缺失任一必填字段应返回 422全部正确应 200/201。
* 可补充最小化单元测试Pydantic 校验用例覆盖必填与格式。
## 交付内容。
* 完成基本交互与接口验证,确保行为符合预期。

View File

@ -301,7 +301,28 @@ async def calculate_valuation(
"""
try:
# 添加后台任务
from app.models.user import AppUser, AppUserQuotaLog
user = await AppUser.filter(id=user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if (user.remaining_quota or 0) < 1:
raise HTTPException(status_code=400, detail="估值次数不足")
before = user.remaining_quota or 0
user.remaining_quota = before - 1
await user.save()
try:
await AppUserQuotaLog.create(
app_user_id=user_id,
operator_id=user_id,
operator_name=user.alias or user.username or user.phone or "",
before_count=before,
after_count=before - 1,
op_type="consume",
remark="发起估值"
)
except Exception:
pass
background_tasks.add_task(_perform_valuation_calculation, user_id, data)
logger.info("valuation.task_queued user_id={} asset_name={} industry={}",

View File

@ -46,10 +46,10 @@ class InvoiceCreate(BaseModel):
email: EmailStr
company_name: str = Field(..., min_length=1, max_length=128)
tax_number: str = Field(..., min_length=1, max_length=32)
register_address: str = Field(..., min_length=1, max_length=256)
register_phone: str = Field(..., min_length=1, max_length=32)
bank_name: str = Field(..., min_length=1, max_length=128)
bank_account: str = Field(..., min_length=1, max_length=64)
register_address: str = Field(..., max_length=256)
register_phone: str = Field(..., max_length=32)
bank_name: str = Field(..., max_length=128)
bank_account: str = Field(..., max_length=64)
app_user_id: Optional[int] = None
header_id: Optional[int] = None
wechat: Optional[str] = None