feat: 新增发票管理模块和用户端接口

refactor: 优化响应格式和错误处理

fix: 修复文件上传类型校验和删除无用PDF文件

perf: 添加估值评估审核时间字段和查询条件

docs: 更新Docker镜像版本至v1.8

test: 添加响应格式检查脚本

style: 统一API响应数据结构

chore: 清理无用静态文件和更新构建脚本
This commit is contained in:
邹方成 2025-11-24 16:39:53 +08:00
parent 1dd9a313e6
commit c690a95cab
30 changed files with 658 additions and 166 deletions

View File

@ -22,6 +22,7 @@ from .users import users_router
from .valuations import router as valuations_router
from .invoice.invoice import invoice_router
from .transactions.transactions import transactions_router
from .app_invoices.app_invoices import app_invoices_router
from .sms.sms import router as sms_router
v1_router = APIRouter()
@ -50,6 +51,7 @@ v1_router.include_router(
tags=["admin-内置接口"],
)
v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission], tags=["admin-估值评估"])
v1_router.include_router(invoice_router, prefix="/invoice", dependencies=[DependAuth, DependPermission], tags=["admin-发票管理"])
v1_router.include_router(invoice_router, prefix="/invoice", tags=["admin-发票管理"])
v1_router.include_router(transactions_router, prefix="/transactions", dependencies=[DependAuth, DependPermission], tags=["admin-交易管理"])
v1_router.include_router(sms_router, prefix="/sms", tags=["app-短信服务"])
v1_router.include_router(app_invoices_router, prefix="/app-invoices", tags=["app-发票管理"])

View File

@ -0,0 +1,77 @@
from fastapi import APIRouter, Query, Depends
from typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
from app.schemas.invoice import InvoiceOut, InvoiceHeaderOut, InvoiceHeaderCreate, InvoiceHeaderUpdate
from app.controllers.invoice import invoice_controller
from app.utils.app_user_jwt import get_current_app_user
from app.models.user import AppUser
app_invoices_router = APIRouter(tags=["app-发票管理"])
@app_invoices_router.get("/list", summary="我的发票列表", response_model=PageResponse[InvoiceOut])
async def get_my_invoices(
status: Optional[str] = Query(None),
ticket_type: Optional[str] = Query(None),
invoice_type: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
current_user: AppUser = Depends(get_current_app_user),
):
result = await invoice_controller.list(
page=page,
page_size=page_size,
status=status,
ticket_type=ticket_type,
invoice_type=invoice_type,
app_user_id=current_user.id,
)
return SuccessExtra(
data=result.items,
total=result.total,
page=result.page,
page_size=result.page_size,
msg="获取成功",
)
@app_invoices_router.get("/headers", summary="我的发票抬头", response_model=BasicResponse[list[InvoiceHeaderOut]])
async def get_my_headers(current_user: AppUser = Depends(get_current_app_user)):
headers = await invoice_controller.get_headers(user_id=current_user.id)
return Success(data=headers, msg="获取成功")
@app_invoices_router.get("/headers/{id}", summary="我的发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut])
async def get_my_header_by_id(id: int, current_user: AppUser = Depends(get_current_app_user)):
header = await invoice_controller.get_header_by_id(id)
if not header or getattr(header, "id", None) is None:
return Success(data={}, msg="未找到")
# 仅允许访问属于自己的抬头
if getattr(header, "app_user_id", None) not in (current_user.id, None):
return Success(data={}, msg="未找到")
return Success(data=header, msg="获取成功")
@app_invoices_router.post("/headers", summary="新增我的发票抬头", response_model=BasicResponse[InvoiceHeaderOut])
async def create_my_header(data: InvoiceHeaderCreate, current_user: AppUser = Depends(get_current_app_user)):
header = await invoice_controller.create_header(user_id=current_user.id, data=data)
return Success(data=header, msg="创建成功")
@app_invoices_router.put("/headers/{id}", summary="更新我的发票抬头", response_model=BasicResponse[InvoiceHeaderOut])
async def update_my_header(id: int, data: InvoiceHeaderUpdate, current_user: AppUser = Depends(get_current_app_user)):
existing = await invoice_controller.get_header_by_id(id)
if not existing or getattr(existing, "id", None) is None:
return Success(data={}, msg="未找到")
if getattr(existing, "app_user_id", None) != current_user.id:
return Success(data={}, msg="未找到")
header = await invoice_controller.update_header(id, data)
return Success(data=header or {}, msg="更新成功" if header else "未找到")
@app_invoices_router.delete("/headers/{id}", summary="删除我的发票抬头", response_model=BasicResponse[dict])
async def delete_my_header(id: int, current_user: AppUser = Depends(get_current_app_user)):
existing = await invoice_controller.get_header_by_id(id)
if not existing or getattr(existing, "id", None) is None:
return Success(data={"deleted": False}, msg="未找到")
if getattr(existing, "app_user_id", None) != current_user.id:
return Success(data={"deleted": False}, msg="未找到")
ok = await invoice_controller.delete_header(id)
return Success(data={"deleted": ok}, msg="删除成功" if ok else "未找到")

View File

@ -2,7 +2,7 @@ from fastapi import APIRouter, Query, Depends, HTTPException
from typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
from app.schemas.app_user import AppUserQuotaUpdateSchema, AppUserQuotaLogOut
from app.schemas.app_user import AppUserQuotaUpdateSchema, AppUserQuotaLogOut, AppUserUpdateSchema
from app.controllers.app_user import app_user_controller
from app.models.user import AppUser, AppUserQuotaLog
from app.core.dependency import DependAuth, DependPermission, AuthControl
@ -52,9 +52,9 @@ async def update_quota(payload: AppUserQuotaUpdateSchema, operator=Depends(AuthC
)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if payload.remark is not None:
user.notes = payload.remark
await user.save()
# if payload.remark is not None:
# user.notes = payload.remark
# await user.save()
return Success(data={"user_id": user.id, "remaining_quota": user.remaining_quota}, msg="调整成功")
@ -77,4 +77,25 @@ async def quota_logs(user_id: int, page: int = Query(1, ge=1), page_size: int =
) for r in rows
]
data_items = [m.model_dump() for m in models]
return SuccessExtra(data=data_items, total=total, page=page, page_size=page_size, msg="获取成功")
return SuccessExtra(data=data_items, total=total, page=page, page_size=page_size, msg="获取成功")
@admin_app_users_router.put("/{user_id}", summary="更新App用户信息", response_model=BasicResponse[dict])
async def update_app_user(user_id: int, data: AppUserUpdateSchema):
user = await app_user_controller.update_user_info(user_id, data)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return Success(data={
"id": user.id,
"phone": user.phone,
"wechat": getattr(user, "alias", None),
"company_name": getattr(user, "company_name", None),
"company_address": getattr(user, "company_address", None),
"company_contact": getattr(user, "company_contact", None),
"company_phone": getattr(user, "company_phone", None),
"company_email": getattr(user, "company_email", None),
"is_active": user.is_active,
"created_at": user.created_at.isoformat() if user.created_at else "",
"updated_at": user.updated_at.isoformat() if user.updated_at else "",
"remaining_quota": int(getattr(user, "remaining_quota", 0) or 0),
}, msg="更新成功")

View File

@ -11,7 +11,7 @@ from app.schemas.app_user import (
AppUserQuotaOut,
)
from app.schemas.app_user import AppUserRegisterOut, TokenValidateOut
from app.schemas.base import BasicResponse, MessageOut
from app.schemas.base import BasicResponse, MessageOut, Success
from app.utils.app_user_jwt import (
create_app_user_access_token,
get_current_app_user,
@ -24,7 +24,7 @@ from app.controllers.invoice import invoice_controller
router = APIRouter()
@router.post("/register", response_model=BasicResponse[AppUserRegisterOut], summary="用户注册")
@router.post("/register", response_model=BasicResponse[dict], summary="用户注册")
async def register(
register_data: AppUserRegisterSchema
):
@ -34,20 +34,16 @@ async def register(
"""
try:
user = await app_user_controller.register(register_data)
return {
"code": 200,
"msg": "注册成功",
"data": {
"user_id": user.id,
"phone": user.phone,
"default_password": register_data.phone[-6:]
}
}
return Success(data={
"user_id": user.id,
"phone": user.phone,
"default_password": register_data.phone[-6:]
})
except Exception as e:
raise HTTPException(status_code=200, detail=str(e))
@router.post("/login", response_model=AppUserJWTOut, summary="用户登录")
@router.post("/login", response_model=BasicResponse[dict], summary="用户登录")
async def login(
login_data: AppUserLoginSchema
):
@ -67,30 +63,46 @@ async def login(
# 生成访问令牌
access_token = create_app_user_access_token(user.id, user.phone)
return AppUserJWTOut(
access_token=access_token,
token_type="bearer",
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
return Success(data={
"access_token": access_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
})
@router.post("/logout", summary="用户登出", response_model=BasicResponse[MessageOut])
@router.post("/logout", summary="用户登出", response_model=BasicResponse[dict])
async def logout(current_user: AppUser = Depends(get_current_app_user)):
"""
用户登出客户端需要删除本地token
"""
return {"code": 200, "msg": "OK", "data": {"message": "登出成功"}}
return Success(data={"message": "登出成功"})
@router.get("/profile", response_model=AppUserInfoOut, summary="获取用户信息")
@router.get("/profile", response_model=BasicResponse[dict], summary="获取用户信息")
async def get_profile(current_user: AppUser = Depends(get_current_app_user)):
"""
获取当前用户信息
"""
return current_user
user_info = AppUserInfoOut(
id=current_user.id,
phone=current_user.phone,
nickname=getattr(current_user, "alias", None),
avatar=None,
company_name=current_user.company_name,
company_address=current_user.company_address,
company_contact=current_user.company_contact,
company_phone=current_user.company_phone,
company_email=current_user.company_email,
is_active=current_user.is_active,
last_login=current_user.last_login,
created_at=current_user.created_at,
updated_at=current_user.updated_at,
remaining_quota=current_user.remaining_quota,
)
return Success(data=user_info.model_dump())
@router.get("/dashboard", response_model=AppUserDashboardOut, summary="用户首页摘要")
@router.get("/dashboard", response_model=BasicResponse[dict], summary="用户首页摘要")
async def get_dashboard(current_user: AppUser = Depends(get_current_app_user)):
"""
用户首页摘要
@ -115,12 +127,12 @@ async def get_dashboard(current_user: AppUser = Depends(get_current_app_user)):
pending_invoices = await invoice_controller.count_pending_for_user(current_user.id)
except Exception:
pending_invoices = 0
# 剩余估值次数(占位,可从用户扩展字段或配额表获取)
remaining_quota = 0
return AppUserDashboardOut(remaining_quota=remaining_quota, latest_valuation=latest_out, pending_invoices=pending_invoices)
# 剩余估值次数
remaining_quota = current_user.remaining_quota
return Success(data={"remaining_quota": remaining_quota, "latest_valuation": latest_out, "pending_invoices": pending_invoices})
@router.get("/quota", response_model=AppUserQuotaOut, summary="剩余估值次数")
@router.get("/quota", response_model=BasicResponse[dict], summary="剩余估值次数")
async def get_quota(current_user: AppUser = Depends(get_current_app_user)):
"""
剩余估值次数查询
@ -128,12 +140,12 @@ async def get_quota(current_user: AppUser = Depends(get_current_app_user)):
- 当前实现返回默认 0 次与用户类型占位
- 若后续接入配额系统可从数据库中读取真实值
"""
remaining_count = 0
remaining_count = current_user.remaining_quota
user_type = "体验用户"
return AppUserQuotaOut(remaining_count=remaining_count, user_type=user_type)
return Success(data={"remaining_count": remaining_count, "user_type": user_type})
@router.put("/profile", response_model=AppUserInfoOut, summary="更新用户信息")
@router.put("/profile", response_model=BasicResponse[dict], summary="更新用户信息")
async def update_profile(
update_data: AppUserUpdateSchema,
current_user: AppUser = Depends(get_current_app_user)
@ -145,10 +157,26 @@ async def update_profile(
if not updated_user:
raise HTTPException(status_code=404, detail="用户不存在")
return updated_user
user_info = AppUserInfoOut(
id=updated_user.id,
phone=updated_user.phone,
nickname=getattr(updated_user, "alias", None),
avatar=None,
company_name=updated_user.company_name,
company_address=updated_user.company_address,
company_contact=updated_user.company_contact,
company_phone=updated_user.company_phone,
company_email=updated_user.company_email,
is_active=updated_user.is_active,
last_login=updated_user.last_login,
created_at=updated_user.created_at,
updated_at=updated_user.updated_at,
remaining_quota=updated_user.remaining_quota,
)
return Success(data=user_info.model_dump())
@router.post("/change-password", summary="修改密码", response_model=BasicResponse[MessageOut])
@router.post("/change-password", summary="修改密码", response_model=BasicResponse[dict])
async def change_password(
password_data: AppUserChangePasswordSchema,
current_user: AppUser = Depends(get_current_app_user)
@ -165,19 +193,12 @@ async def change_password(
if not success:
raise HTTPException(status_code=400, detail="原密码错误")
return {"code": 200, "msg": "OK", "data": {"message": "密码修改成功"}}
return Success(data={"message": "密码修改成功"})
@router.get("/validate-token", summary="验证token", response_model=BasicResponse[TokenValidateOut])
@router.get("/validate-token", summary="验证token", response_model=BasicResponse[dict])
async def validate_token(current_user: AppUser = Depends(get_current_app_user)):
"""
验证token是否有效
"""
return {
"code": 200,
"msg": "token有效",
"data": {
"user_id": current_user.id,
"phone": current_user.phone
}
}
return Success(data={"user_id": current_user.id, "phone": current_user.phone})

View File

@ -18,7 +18,7 @@ from app.schemas.valuation import (
UserValuationOut,
UserValuationDetail
)
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
from app.schemas.base import Success, BasicResponse
from app.utils.app_user_jwt import get_current_app_user_id, get_current_app_user
from app.utils.calculation_engine import FinalValueACalculator
# from app.utils.calculation_engine.cultural_value_b2.sub_formulas.living_heritage_b21 import cross_border_depth_dict
@ -313,8 +313,7 @@ async def calculate_valuation(
"message": "估值计算任务已提交,正在后台处理中",
"user_id": user_id,
"asset_name": getattr(data, 'asset_name', None)
},
msg="估值计算任务已启动"
}
)
except Exception as e:
@ -624,7 +623,7 @@ async def _extract_calculation_params_c(data: UserValuationCreate) -> Dict[str,
}
@app_valuations_router.get("/", summary="获取我的估值评估列表", response_model=PageResponse[UserValuationOut])
@app_valuations_router.get("/", summary="获取我的估值评估列表", response_model=BasicResponse[dict])
async def get_my_valuations(
query: UserValuationQuery = Depends(),
current_user: AppUser = Depends(get_current_app_user)
@ -641,13 +640,14 @@ async def get_my_valuations(
# 使用model_dump_json()来正确序列化datetime然后解析为dict列表
import json
serialized_items = [json.loads(item.model_dump_json()) for item in result.items]
return SuccessExtra(
data=serialized_items,
total=result.total,
page=result.page,
page_size=result.size,
pages=result.pages,
msg="获取估值评估列表成功"
return Success(
data={
"items": serialized_items,
"total": result.total,
"page": result.page,
"page_size": result.size,
"pages": result.pages,
}
)
except Exception as e:
raise HTTPException(
@ -656,7 +656,7 @@ async def get_my_valuations(
)
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情", response_model=BasicResponse[UserValuationDetail])
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情", response_model=BasicResponse[dict])
async def get_valuation_detail(
valuation_id: int,
current_user: AppUser = Depends(get_current_app_user)
@ -679,7 +679,7 @@ async def get_valuation_detail(
# 使用model_dump_json()来正确序列化datetime然后解析为dict
import json
result_dict = json.loads(result.model_dump_json())
return Success(data=result_dict, msg="获取估值评估详情成功")
return Success(data=result_dict)
except HTTPException:
raise
except Exception as e:
@ -700,7 +700,7 @@ async def get_my_valuation_statistics(
result = await user_valuation_controller.get_user_valuation_statistics(
user_id=current_user.id
)
return Success(data=result, msg="获取统计信息成功")
return Success(data=result)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -728,7 +728,7 @@ async def delete_valuation(
detail="估值评估记录不存在或已被删除"
)
return Success(data={"deleted": True}, msg="删除估值评估成功")
return Success(data={"deleted": True})
except HTTPException:
raise
except Exception as e:

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Query
from fastapi import APIRouter, Query, Depends, Header, HTTPException
from typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
@ -8,6 +8,7 @@ from app.schemas.invoice import (
UpdateStatus,
UpdateType,
InvoiceHeaderCreate,
InvoiceHeaderUpdate,
PaymentReceiptCreate,
InvoiceOut,
InvoiceList,
@ -15,12 +16,15 @@ from app.schemas.invoice import (
PaymentReceiptOut,
)
from app.controllers.invoice import invoice_controller
from app.utils.app_user_jwt import get_current_app_user
from app.core.dependency import DependAuth, DependPermission
from app.models.user import AppUser
invoice_router = APIRouter(tags=["发票管理"])
@invoice_router.get("/list", summary="获取发票列表", response_model=PageResponse[InvoiceOut])
@invoice_router.get("/list", summary="获取发票列表", response_model=PageResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def list_invoices(
phone: Optional[str] = Query(None),
company_name: Optional[str] = Query(None),
@ -28,6 +32,7 @@ async def list_invoices(
status: Optional[str] = Query(None),
ticket_type: Optional[str] = Query(None),
invoice_type: Optional[str] = Query(None),
user_id: Optional[int] = Query(None, description="按App用户ID过滤"),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
):
@ -45,13 +50,14 @@ async def list_invoices(
status=status,
ticket_type=ticket_type,
invoice_type=invoice_type,
app_user_id=user_id,
)
return SuccessExtra(
data=result.items, total=result.total, page=result.page, page_size=result.page_size, msg="获取成功"
)
@invoice_router.get("/detail", summary="发票详情", response_model=BasicResponse[InvoiceOut])
@invoice_router.get("/detail", summary="发票详情", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def invoice_detail(id: int = Query(...)):
"""
根据ID获取发票详情
@ -62,7 +68,7 @@ async def invoice_detail(id: int = Query(...)):
return Success(data=out, msg="获取成功")
@invoice_router.post("/create", summary="创建发票", response_model=BasicResponse[InvoiceOut])
@invoice_router.post("/create", summary="创建发票", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def create_invoice(data: InvoiceCreate):
"""
创建发票记录
@ -72,7 +78,7 @@ async def create_invoice(data: InvoiceCreate):
return Success(data=out, msg="创建成功")
@invoice_router.post("/update", summary="更新发票", response_model=BasicResponse[InvoiceOut])
@invoice_router.post("/update", summary="更新发票", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def update_invoice(data: InvoiceUpdate, id: int = Query(...)):
"""
更新发票记录
@ -82,7 +88,7 @@ async def update_invoice(data: InvoiceUpdate, id: int = Query(...)):
return Success(data=out or {}, msg="更新成功" if updated else "未找到")
@invoice_router.delete("/delete", summary="删除发票", response_model=BasicResponse[MessageOut])
@invoice_router.delete("/delete", summary="删除发票", response_model=BasicResponse[MessageOut], dependencies=[DependAuth, DependPermission])
async def delete_invoice(id: int = Query(...)):
"""
删除发票记录
@ -95,7 +101,7 @@ async def delete_invoice(id: int = Query(...)):
return Success(data={"deleted": ok}, msg="删除成功" if ok else "未找到")
@invoice_router.post("/update-status", summary="更新发票状态", response_model=BasicResponse[InvoiceOut])
@invoice_router.post("/update-status", summary="更新发票状态", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def update_invoice_status(data: UpdateStatus):
"""
更新发票状态pending|invoiced|rejected|refunded
@ -106,25 +112,26 @@ async def update_invoice_status(data: UpdateStatus):
@invoice_router.post("/{id}/receipt", summary="上传付款凭证", response_model=BasicResponse[PaymentReceiptOut])
@invoice_router.post("/{id}/receipt", summary="上传付款凭证", response_model=BasicResponse[dict], dependencies=[DependAuth, DependPermission])
async def upload_payment_receipt(id: int, data: PaymentReceiptCreate):
"""
上传对公转账付款凭证
"""
receipt = await invoice_controller.create_receipt(id, data)
return Success(data=receipt, msg="上传成功")
detail = await invoice_controller.get_receipt_by_id(receipt.id)
return Success(data=detail, msg="上传成功")
@invoice_router.get("/headers", summary="发票抬头列表", response_model=BasicResponse[list[InvoiceHeaderOut]])
@invoice_router.get("/headers", summary="发票抬头列表", response_model=BasicResponse[list[InvoiceHeaderOut]], dependencies=[DependAuth, DependPermission])
async def get_invoice_headers(app_user_id: Optional[int] = Query(None)):
"""
获取发票抬头列表可按 AppUser 过滤
管理端抬头列表管理员token允许按 app_user_id 过滤为空则返回全部
"""
headers = await invoice_controller.get_headers(user_id=app_user_id)
return Success(data=headers, msg="获取成功")
@invoice_router.get("/headers/{id}", summary="发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut])
@invoice_router.get("/headers/{id}", summary="发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
async def get_invoice_header_by_id(id: int):
"""
获取发票抬头详情
@ -133,7 +140,7 @@ async def get_invoice_header_by_id(id: int):
return Success(data=header or {}, msg="获取成功" if header else "未找到")
@invoice_router.post("/headers", summary="新增发票抬头", response_model=BasicResponse[InvoiceHeaderOut])
@invoice_router.post("/headers", summary="新增发票抬头", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
async def create_invoice_header(data: InvoiceHeaderCreate, app_user_id: Optional[int] = Query(None)):
"""
新增发票抬头
@ -142,7 +149,7 @@ async def create_invoice_header(data: InvoiceHeaderCreate, app_user_id: Optional
return Success(data=header, msg="创建成功")
@invoice_router.put("/{id}/type", summary="更新发票类型", response_model=BasicResponse[InvoiceOut])
@invoice_router.put("/{id}/type", summary="更新发票类型", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def update_invoice_type(id: int, data: UpdateType):
"""
更新发票的电子/纸质与专票/普票类型
@ -151,4 +158,46 @@ async def update_invoice_type(id: int, data: UpdateType):
return Success(data=out or {}, msg="更新成功" if out else "未找到")
# 对公转账记录接口在 transactions 路由中统一暴露
@invoice_router.delete("/headers/{id}", summary="删除发票抬头", response_model=BasicResponse[MessageOut], dependencies=[DependAuth, DependPermission])
async def delete_invoice_header(id: int):
ok = await invoice_controller.delete_header(id)
return Success(msg="删除成功" if ok else "未找到")
@invoice_router.put("/headers/{id}", summary="更新发票抬头", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
async def update_invoice_header(id: int, data: InvoiceHeaderUpdate):
header = await invoice_controller.update_header(id, data)
return Success(data=header or {}, msg="更新成功" if header else "未找到")
# 用户端我的发票列表使用App用户token
@invoice_router.get("/app-list", summary="我的发票列表", response_model=PageResponse[InvoiceOut])
async def list_my_invoices(
status: Optional[str] = Query(None),
ticket_type: Optional[str] = Query(None),
invoice_type: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
current_user: AppUser = Depends(get_current_app_user),
):
result = await invoice_controller.list(
page=page,
page_size=page_size,
status=status,
ticket_type=ticket_type,
invoice_type=invoice_type,
app_user_id=current_user.id,
)
return SuccessExtra(
data=result.items,
total=result.total,
page=result.page,
page_size=result.page_size,
msg="获取成功",
)
# 用户端我的发票抬头使用App用户token
@invoice_router.get("/app-headers", summary="我的发票抬头", response_model=BasicResponse[list[InvoiceHeaderOut]])
async def get_my_invoice_headers(current_user: AppUser = Depends(get_current_app_user)):
headers = await invoice_controller.get_headers(user_id=current_user.id)
return Success(data=headers, msg="获取成功")

View File

@ -9,6 +9,7 @@ from app.services.sms_store import store
from app.core.dependency import DependAuth
from app.log import logger
from app.schemas.app_user import AppUserInfoOut, AppUserJWTOut
from app.schemas.base import BasicResponse, Success
class SendCodeRequest(BaseModel):
@ -44,8 +45,8 @@ rate_limiter = PhoneRateLimiter(60)
router = APIRouter(tags=["短信服务"])
@router.post("/send-code", response_model=SendResponse, summary="验证码发送")
async def send_code(payload: SendCodeRequest) -> SendResponse:
@router.post("/send-code", response_model=BasicResponse[dict], summary="验证码发送")
async def send_code(payload: SendCodeRequest) -> BasicResponse[dict]:
"""发送验证码短信
Args:
@ -68,7 +69,13 @@ async def send_code(payload: SendCodeRequest) -> SendResponse:
rid = res.get("RequestId") or res.get("MessageId")
if code == "OK":
logger.info("sms.send_code success phone={} request_id={}", payload.phone, rid)
return SendResponse(status="OK", message="sent", request_id=str(rid) if rid else None)
return Success(
data={
"status": "OK",
"message": "sent",
"request_id": str(rid) if rid else None,
}
)
msg = res.get("Message") or res.get("ResponseDescription") or "error"
logger.warning("sms.send_code fail phone={} code={} msg={}", payload.phone, code, msg)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(msg))
@ -79,8 +86,8 @@ async def send_code(payload: SendCodeRequest) -> SendResponse:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="短信服务异常")
@router.post("/send-report", response_model=SendResponse, summary="报告通知发送", dependencies=[DependAuth])
async def send_report(payload: SendReportRequest) -> SendResponse:
@router.post("/send-report", response_model=BasicResponse[dict], summary="报告通知发送", dependencies=[DependAuth])
async def send_report(payload: SendReportRequest) -> BasicResponse[dict]:
"""发送报告通知短信
Args:
@ -98,7 +105,13 @@ async def send_report(payload: SendReportRequest) -> SendResponse:
rid = res.get("RequestId") or res.get("MessageId")
if code == "OK":
logger.info("sms.send_report success phone={} request_id={}", payload.phone, rid)
return SendResponse(status="OK", message="sent", request_id=str(rid) if rid else None)
return Success(
data={
"status": "OK",
"message": "sent",
"request_id": str(rid) if rid else None,
}
)
msg = res.get("Message") or res.get("ResponseDescription") or "error"
logger.warning("sms.send_report fail phone={} code={} msg={}", payload.phone, code, msg)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(msg))
@ -109,8 +122,8 @@ async def send_report(payload: SendReportRequest) -> SendResponse:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="短信服务异常")
@router.post("/verify-code", summary="验证码验证", response_model=VerifyResponse)
async def verify_code(payload: VerifyCodeRequest) -> VerifyResponse:
@router.post("/verify-code", summary="验证码验证", response_model=BasicResponse[dict])
async def verify_code(payload: VerifyCodeRequest) -> BasicResponse[dict]:
"""验证验证码
Args:
@ -137,7 +150,7 @@ async def verify_code(payload: VerifyCodeRequest) -> VerifyResponse:
store.clear_code(payload.phone)
store.reset_failures(payload.phone)
logger.info("sms.verify_code success phone={}", payload.phone)
return VerifyResponse(status="OK", message="verified")
return Success(data={"status": "OK", "message": "verified"})
class SMSLoginRequest(BaseModel):
@ -146,8 +159,8 @@ class SMSLoginRequest(BaseModel):
device_id: Optional[str] = Field(None)
@router.post("/login", summary="短信验证码登录", response_model=SMSLoginResponse)
async def sms_login(payload: SMSLoginRequest) -> SMSLoginResponse:
@router.post("/login", summary="短信验证码登录", response_model=BasicResponse[dict])
async def sms_login(payload: SMSLoginRequest) -> BasicResponse[dict]:
ok, reason = store.can_verify(payload.phone_number)
if not ok:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=str(reason))
@ -181,8 +194,8 @@ async def sms_login(payload: SMSLoginRequest) -> SMSLoginResponse:
user_info = AppUserInfoOut(
id=user.id,
phone=user.phone,
nickname=user.nickname,
avatar=user.avatar,
nickname=getattr(user, "alias", None),
avatar=None,
company_name=user.company_name,
company_address=user.company_address,
company_contact=user.company_contact,
@ -192,9 +205,10 @@ async def sms_login(payload: SMSLoginRequest) -> SMSLoginResponse:
last_login=user.last_login,
created_at=user.created_at,
updated_at=user.updated_at,
remaining_quota=user.remaining_quota,
)
token_out = AppUserJWTOut(access_token=access_token, expires_in=ACCESS_TOKEN_EXPIRE_MINUTES)
return SMSLoginResponse(user=user_info, token=token_out)
return Success(data={"user": user_info.model_dump(), "token": token_out.model_dump()})
class VerifyCodeRequest(BaseModel):
phone: str = Field(...)
code: str = Field(...)

View File

@ -1,22 +1,11 @@
from fastapi import APIRouter, UploadFile, File
from app.controllers.upload import UploadController
from app.schemas.upload import ImageUploadResponse, FileUploadResponse
from app.schemas.base import BasicResponse, Success
router = APIRouter()
@router.post("/image", response_model=ImageUploadResponse, summary="上传图片")
async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
"""
上传图片接口
:param file: 图片文件
:return: 图片URL和文件名
"""
return await UploadController.upload_image(file)
@router.post("/file", response_model=FileUploadResponse, summary="上传文件")
async def upload_file(file: UploadFile = File(...)) -> FileUploadResponse:
return await UploadController.upload_file(file)
@router.post("/upload", response_model=FileUploadResponse, summary="统一上传接口")
async def upload(file: UploadFile = File(...)) -> FileUploadResponse:
return await UploadController.upload_any(file)
@router.post("/file", response_model=BasicResponse[dict], summary="统一上传接口")
async def upload(file: UploadFile = File(...)) -> BasicResponse[dict]:
res = await UploadController.upload_any(file)
return Success(data={"url": res.url, "filename": res.filename, "content_type": res.content_type})

View File

@ -87,6 +87,11 @@ async def get_valuations(
heritage_level: Optional[str] = Query(None, description="非遗等级"),
status: Optional[str] = Query(None, description="评估状态"),
is_active: Optional[bool] = Query(None, description="是否激活"),
phone: Optional[str] = Query(None, description="手机号模糊查询"),
submitted_start: Optional[str] = Query(None, description="提交时间开始毫秒或ISO"),
submitted_end: Optional[str] = Query(None, description="提交时间结束毫秒或ISO"),
audited_start: Optional[str] = Query(None, description="审核时间开始证书修改时间毫秒或ISO"),
audited_end: Optional[str] = Query(None, description="审核时间结束证书修改时间毫秒或ISO"),
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量")
):
@ -98,6 +103,11 @@ async def get_valuations(
heritage_level=heritage_level,
status=status,
is_active=is_active,
phone=phone,
submitted_start=submitted_start,
submitted_end=submitted_end,
audited_start=audited_start,
audited_end=audited_end,
page=page,
size=size
)
@ -200,4 +210,4 @@ async def update_admin_notes(valuation_id: int, data: ValuationAdminNotesUpdate)
if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在")
import json
return Success(data=json.loads(result.model_dump_json()), msg="管理员备注更新成功")
return Success(data=json.loads(result.model_dump_json()), msg="管理员备注更新成功")

View File

@ -86,6 +86,9 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
# 更新字段
update_dict = update_data.model_dump(exclude_unset=True)
if "nickname" in update_dict:
update_dict["alias"] = update_dict.pop("nickname")
update_dict.pop("avatar", None)
for field, value in update_dict.items():
setattr(user, field, value)
@ -145,4 +148,4 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
# 创建控制器实例
app_user_controller = AppUserController()
app_user_controller = AppUserController()

View File

@ -9,6 +9,7 @@ from app.schemas.invoice import (
InvoiceOut,
InvoiceList,
InvoiceHeaderCreate,
InvoiceHeaderUpdate,
InvoiceHeaderOut,
UpdateStatus,
UpdateType,
@ -60,6 +61,22 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
header = await InvoiceHeader.filter(id=id_).first()
return InvoiceHeaderOut.model_validate(header) if header else None
async def delete_header(self, id_: int) -> bool:
header = await InvoiceHeader.filter(id=id_).first()
if not header:
return False
await header.delete()
return True
async def update_header(self, id_: int, data: InvoiceHeaderUpdate) -> Optional[InvoiceHeaderOut]:
header = await InvoiceHeader.filter(id=id_).first()
if not header:
return None
update_data = data.model_dump(exclude_unset=True)
if update_data:
await header.update_from_dict(update_data).save()
return InvoiceHeaderOut.model_validate(header)
async def list(self, page: int = 1, page_size: int = 10, **filters) -> InvoiceList:
"""
获取发票列表支持筛选与分页
@ -83,6 +100,8 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
qs = qs.filter(ticket_type=filters["ticket_type"])
if filters.get("invoice_type"):
qs = qs.filter(invoice_type=filters["invoice_type"])
if filters.get("app_user_id"):
qs = qs.filter(app_user_id=filters["app_user_id"])
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
@ -231,13 +250,18 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
for r in rows:
inv = await r.invoice
items.append({
"id": r.id,
"invoice_id": getattr(inv, "id", None),
"submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": {
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
},
"receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
"receipts": [
{
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
}
],
"phone": inv.phone,
"wechat": inv.wechat,
"company_name": inv.company_name,
@ -267,13 +291,18 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
return None
inv = await r.invoice
return {
"id": r.id,
"invoice_id": getattr(inv, "id", None),
"submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": {
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
},
"receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
"receipts": [
{
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
}
],
"phone": inv.phone,
"wechat": inv.wechat,
"company_name": inv.company_name,
@ -319,4 +348,4 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
)
invoice_controller = InvoiceController()
invoice_controller = InvoiceController()

View File

@ -15,8 +15,9 @@ class UploadController:
:param file: 上传的图片文件
:return: 图片URL和文件名
"""
# 检查文件类型
if not file.content_type.startswith('image/'):
ext = os.path.splitext(file.filename or "")[1].lower()
image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
if not (file.content_type.startswith('image/') or ext in image_exts):
raise ValueError("只支持上传图片文件")
# 获取项目根目录
@ -61,8 +62,32 @@ class UploadController:
"application/vnd.ms-excel",
"application/zip",
"application/x-zip-compressed",
"application/octet-stream",
"text/plain",
"text/csv",
"application/json",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/x-rar-compressed",
"application/x-7z-compressed",
}
if file.content_type not in allowed:
allowed_exts = {
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".zip",
".rar",
".7z",
".txt",
".csv",
".ppt",
".pptx",
".json",
}
ext = os.path.splitext(file.filename or "")[1].lower()
if (file.content_type not in allowed) and (ext not in allowed_exts):
raise ValueError("不支持的文件类型")
base_dir = Path(__file__).resolve().parent.parent
@ -95,8 +120,10 @@ class UploadController:
统一上传入口自动识别图片与非图片类型
返回统一结构url, filename, content_type
"""
if file.content_type and file.content_type.startswith("image/"):
image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
ext = os.path.splitext(file.filename or "")[1].lower()
if (file.content_type and file.content_type.startswith("image/")) or (ext in image_exts):
img = await UploadController.upload_image(file)
return FileUploadResponse(url=img.url, filename=img.filename, content_type=file.content_type or "image")
# 非图片类型复用原文件上传校验
return await UploadController.upload_file(file)
return await UploadController.upload_file(file)

View File

@ -114,7 +114,73 @@ class UserValuationController:
async def _to_user_out(self, valuation: ValuationAssessment) -> UserValuationOut:
"""转换为用户端输出模型"""
return UserValuationOut.model_validate(valuation)
return UserValuationOut(
id=valuation.id,
asset_name=valuation.asset_name,
institution=valuation.institution,
industry=valuation.industry,
annual_revenue=valuation.annual_revenue,
rd_investment=valuation.rd_investment,
three_year_income=valuation.three_year_income,
funding_status=valuation.funding_status,
inheritor_level=valuation.inheritor_level,
inheritor_ages=valuation.inheritor_ages,
inheritor_age_count=valuation.inheritor_age_count,
inheritor_certificates=valuation.inheritor_certificates,
heritage_level=valuation.heritage_level,
heritage_asset_level=valuation.heritage_asset_level,
patent_application_no=valuation.patent_application_no,
patent_remaining_years=valuation.patent_remaining_years,
historical_evidence=valuation.historical_evidence,
patent_certificates=valuation.patent_certificates,
pattern_images=valuation.pattern_images,
report_url=valuation.report_url,
certificate_url=valuation.certificate_url,
application_maturity=valuation.application_maturity,
implementation_stage=valuation.implementation_stage,
application_coverage=valuation.application_coverage,
coverage_area=valuation.coverage_area,
cooperation_depth=valuation.cooperation_depth,
collaboration_type=valuation.collaboration_type,
offline_activities=valuation.offline_activities,
offline_teaching_count=valuation.offline_teaching_count,
online_accounts=valuation.online_accounts,
platform_accounts=valuation.platform_accounts,
sales_volume=valuation.sales_volume,
link_views=valuation.link_views,
circulation=valuation.circulation,
scarcity_level=valuation.scarcity_level,
last_market_activity=valuation.last_market_activity,
market_activity_time=valuation.market_activity_time,
monthly_transaction=valuation.monthly_transaction,
monthly_transaction_amount=valuation.monthly_transaction_amount,
price_fluctuation=valuation.price_fluctuation,
price_range=valuation.price_range,
market_price=valuation.market_price,
credit_code_or_id=valuation.credit_code_or_id,
biz_intro=valuation.biz_intro,
infringement_record=valuation.infringement_record,
patent_count=valuation.patent_count,
esg_value=valuation.esg_value,
policy_matching=valuation.policy_matching,
online_course_views=valuation.online_course_views,
pattern_complexity=valuation.pattern_complexity,
normalized_entropy=valuation.normalized_entropy,
legal_risk=valuation.legal_risk,
base_pledge_rate=valuation.base_pledge_rate,
flow_correction=valuation.flow_correction,
model_value_b=valuation.model_value_b,
market_value_c=valuation.market_value_c,
final_value_ab=valuation.final_value_ab,
dynamic_pledge_rate=valuation.dynamic_pledge_rate,
calculation_result=valuation.calculation_result,
calculation_input=valuation.calculation_input,
status=valuation.status,
admin_notes=valuation.admin_notes,
created_at=valuation.created_at,
updated_at=valuation.updated_at,
is_active=valuation.is_active,
)
async def _to_user_detail(self, valuation: ValuationAssessment) -> UserValuationDetail:
"""转换为用户端详细模型"""
@ -188,4 +254,4 @@ class UserValuationController:
# 创建控制器实例
user_valuation_controller = UserValuationController()
user_valuation_controller = UserValuationController()

View File

@ -13,6 +13,7 @@ from app.schemas.valuation import (
ValuationCalculationStepCreate,
ValuationCalculationStepOut
)
from app.models.user import AppUser
class ValuationController:
@ -76,13 +77,15 @@ class ValuationController:
create_data = data.model_dump()
create_data['user_id'] = user_id
valuation = await self.model.create(**create_data)
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def get_by_id(self, valuation_id: int) -> Optional[ValuationAssessmentOut]:
"""根据ID获取估值评估"""
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
if valuation:
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
return None
async def update(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]:
@ -93,10 +96,14 @@ class ValuationController:
update_data = data.model_dump(exclude_unset=True)
if update_data:
if 'certificate_url' in update_data and update_data.get('certificate_url'):
from datetime import datetime
update_data['audited_at'] = datetime.now()
await valuation.update_from_dict(update_data)
await valuation.save()
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def delete(self, valuation_id: int) -> bool:
"""软删除估值评估"""
@ -121,6 +128,7 @@ class ValuationController:
# 转换为输出模型
items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
items = await self._attach_user_phone_bulk(items)
# 计算总页数
pages = (total + query.size - 1) // query.size
@ -154,7 +162,38 @@ class ValuationController:
# 添加状态筛选
if hasattr(query, 'status') and query.status:
queryset = queryset.filter(status=query.status)
if getattr(query, 'phone', None):
queryset = queryset.filter(user__phone__icontains=query.phone)
def _parse_time(v: Optional[str]):
if not v:
return None
try:
iv = int(v)
from datetime import datetime
return datetime.fromtimestamp(iv / 1000)
except Exception:
try:
from datetime import datetime
return datetime.fromisoformat(v)
except Exception:
return None
s_dt = _parse_time(getattr(query, 'submitted_start', None))
e_dt = _parse_time(getattr(query, 'submitted_end', None))
if s_dt:
queryset = queryset.filter(created_at__gte=s_dt)
if e_dt:
queryset = queryset.filter(created_at__lte=e_dt)
a_s_dt = _parse_time(getattr(query, 'audited_start', None))
a_e_dt = _parse_time(getattr(query, 'audited_end', None))
if a_s_dt:
queryset = queryset.filter(audited_at__isnull=False, audited_at__gte=a_s_dt)
if a_e_dt:
queryset = queryset.filter(audited_at__isnull=False, audited_at__lte=a_e_dt)
return queryset
async def get_statistics(self) -> dict:
@ -195,6 +234,7 @@ class ValuationController:
# 转换为输出模型
items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
items = await self._attach_user_phone_bulk(items)
# 计算总页数
pages = (total + size - 1) // size
@ -213,12 +253,14 @@ class ValuationController:
if not valuation:
return None
update_data = {"status": "approved"}
from datetime import datetime
update_data = {"status": "approved", "audited_at": datetime.now()}
if admin_notes:
update_data["admin_notes"] = admin_notes
await valuation.update_from_dict(update_data).save()
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def reject_valuation(self, valuation_id: int, admin_notes: Optional[str] = None) -> Optional[ValuationAssessmentOut]:
"""审核拒绝估值评估"""
@ -226,23 +268,41 @@ class ValuationController:
if not valuation:
return None
update_data = {"status": "rejected"}
from datetime import datetime
update_data = {"status": "rejected", "audited_at": datetime.now()}
if admin_notes:
update_data["admin_notes"] = admin_notes
await valuation.update_from_dict(update_data).save()
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def update_admin_notes(self, valuation_id: int, admin_notes: str) -> Optional[ValuationAssessmentOut]:
"""更新管理员备注"""
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
if not valuation:
return None
await valuation.update_from_dict({"admin_notes": admin_notes}).save()
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def _attach_user_phone(self, out: ValuationAssessmentOut) -> ValuationAssessmentOut:
user = await AppUser.filter(id=out.user_id).first()
out.user_phone = getattr(user, "phone", None) if user else None
return out
async def _attach_user_phone_bulk(self, items: List[ValuationAssessmentOut]) -> List[ValuationAssessmentOut]:
ids = list({item.user_id for item in items if item.user_id})
if not ids:
return items
users = await AppUser.filter(id__in=ids).values("id", "phone")
phone_map = {u["id"]: u["phone"] for u in users}
for item in items:
item.user_phone = phone_map.get(item.user_id)
return items
# 创建控制器实例
valuation_controller = ValuationController()
from app.log import logger
from app.log import logger

View File

@ -16,6 +16,7 @@ async def DoesNotExistHandle(req: Request, exc: DoesNotExist) -> JSONResponse:
content = dict(
code=404,
msg=f"Object has not found, exc: {exc}, query_params: {req.query_params}",
data={},
)
return JSONResponse(content=content, status_code=404)
@ -24,20 +25,21 @@ async def IntegrityHandle(_: Request, exc: IntegrityError) -> JSONResponse:
content = dict(
code=500,
msg=f"IntegrityError{exc}",
data={},
)
return JSONResponse(content=content, status_code=500)
async def HttpExcHandle(_: Request, exc: HTTPException) -> JSONResponse:
content = dict(code=exc.status_code, msg=exc.detail, data=None)
content = dict(code=exc.status_code, msg=exc.detail, data={})
return JSONResponse(content=content, status_code=exc.status_code)
async def RequestValidationHandle(_: Request, exc: RequestValidationError) -> JSONResponse:
content = dict(code=422, msg=f"RequestValidationError, {exc}")
content = dict(code=422, msg=f"RequestValidationError, {exc}", data={})
return JSONResponse(content=content, status_code=422)
async def ResponseValidationHandle(_: Request, exc: ResponseValidationError) -> JSONResponse:
content = dict(code=500, msg=f"ResponseValidationError, {exc}")
content = dict(code=500, msg=f"ResponseValidationError, {exc}", data={})
return JSONResponse(content=content, status_code=500)

View File

@ -86,6 +86,7 @@ class ValuationAssessment(Model):
admin_notes = fields.TextField(null=True, description="管理员备注")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
audited_at = fields.DatetimeField(null=True, description="审核时间")
is_active = fields.BooleanField(default=True, description="是否激活")
class Meta:
@ -115,4 +116,4 @@ class ValuationCalculationStep(Model):
ordering = ["step_order"]
def __str__(self):
return f"估值ID {self.valuation_id} - 步骤 {self.step_order}: {self.step_name}"
return f"估值ID {self.valuation_id} - 步骤 {self.step_order}: {self.step_name}"

View File

@ -3,6 +3,7 @@ from pydantic import BaseModel, Field
from pydantic.generics import GenericModel
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
class Success(JSONResponse):
@ -13,9 +14,9 @@ class Success(JSONResponse):
data: Optional[Any] = None,
**kwargs,
):
content = {"code": code, "msg": msg, "data": data}
content = {"code": code, "msg": msg, "data": ({} if data is None else data)}
content.update(kwargs)
super().__init__(content=content, status_code=code)
super().__init__(content=jsonable_encoder(content), status_code=code)
class Fail(JSONResponse):
@ -26,9 +27,9 @@ class Fail(JSONResponse):
data: Optional[Any] = None,
**kwargs,
):
content = {"code": code, "msg": msg, "data": data}
content = {"code": code, "msg": msg, "data": ({} if data is None else data)}
content.update(kwargs)
super().__init__(content=content, status_code=code)
super().__init__(content=jsonable_encoder(content), status_code=code)
class SuccessExtra(JSONResponse):
@ -51,7 +52,7 @@ class SuccessExtra(JSONResponse):
"page_size": page_size,
}
content.update(kwargs)
super().__init__(content=content, status_code=code)
super().__init__(content=jsonable_encoder(content), status_code=code)
T = TypeVar("T")

View File

@ -23,6 +23,16 @@ class InvoiceHeaderOut(BaseModel):
email: EmailStr
class InvoiceHeaderUpdate(BaseModel):
company_name: Optional[str] = Field(None, min_length=1, max_length=128)
tax_number: Optional[str] = Field(None, min_length=1, max_length=32)
register_address: Optional[str] = Field(None, min_length=1, max_length=256)
register_phone: Optional[str] = Field(None, min_length=1, max_length=32)
bank_name: Optional[str] = Field(None, min_length=1, max_length=128)
bank_account: Optional[str] = Field(None, min_length=1, max_length=64)
email: Optional[EmailStr] = None
class InvoiceCreate(BaseModel):
ticket_type: str = Field(..., pattern=r"^(electronic|paper)$")
invoice_type: str = Field(..., pattern=r"^(special|normal)$")
@ -99,4 +109,4 @@ class PaymentReceiptOut(BaseModel):
url: str
note: Optional[str]
verified: bool
created_at: str
created_at: str

View File

@ -134,6 +134,9 @@ class ValuationAssessmentOut(ValuationAssessmentBase):
"""估值评估输出模型"""
id: int = Field(..., description="主键ID")
user_id: int = Field(..., description="用户ID")
user_phone: Optional[str] = Field(None, description="用户手机号")
report_url: Optional[str] = Field(None, description="评估报告URL")
certificate_url: Optional[str] = Field(None, description="证书URL")
status: str = Field(..., description="评估状态")
admin_notes: Optional[str] = Field(None, description="管理员备注")
created_at: datetime = Field(..., description="创建时间")
@ -159,6 +162,8 @@ class UserValuationOut(ValuationAssessmentBase):
"""用户端估值评估输出模型"""
id: int = Field(..., description="主键ID")
user_id: Optional[int] = Field(None, description="用户ID")
report_url: Optional[str] = Field(None, description="评估报告URL")
certificate_url: Optional[str] = Field(None, description="证书URL")
status: str = Field(..., description="评估状态")
admin_notes: Optional[str] = Field(None, description="管理员备注")
created_at: datetime = Field(..., description="创建时间")
@ -176,6 +181,8 @@ class UserValuationOut(ValuationAssessmentBase):
class UserValuationDetail(ValuationAssessmentBase):
"""用户端详细估值评估模型"""
id: int = Field(..., description="主键ID")
report_url: Optional[str] = Field(None, description="评估报告URL")
certificate_url: Optional[str] = Field(None, description="证书URL")
status: str = Field(..., description="评估状态")
admin_notes: Optional[str] = Field(None, description="管理员备注")
created_at: datetime = Field(..., description="创建时间")
@ -230,6 +237,11 @@ class ValuationAssessmentQuery(BaseModel):
heritage_level: Optional[str] = Field(None, description="非遗等级")
status: Optional[str] = Field(None, description="评估状态: pending(待审核), approved(已通过), rejected(已拒绝)")
is_active: Optional[bool] = Field(None, description="是否激活")
phone: Optional[str] = Field(None, description="手机号模糊查询")
submitted_start: Optional[str] = Field(None, description="提交时间开始毫秒时间戳或ISO字符串")
submitted_end: Optional[str] = Field(None, description="提交时间结束毫秒时间戳或ISO字符串")
audited_start: Optional[str] = Field(None, description="审核时间开始证书修改时间毫秒时间戳或ISO字符串")
audited_end: Optional[str] = Field(None, description="审核时间结束证书修改时间毫秒时间戳或ISO字符串")
page: int = Field(1, ge=1, description="页码")
size: int = Field(10, ge=1, le=100, description="每页数量")
@ -284,4 +296,4 @@ class ValuationCalculationStepOut(ValuationCalculationStepBase):
json_encoders = {
datetime: lambda v: v.isoformat(),
Decimal: lambda v: float(v)
}
}

View File

@ -1,2 +0,0 @@
%PDF-1.4
%粤マモ

View File

@ -1,2 +0,0 @@
%PDF-1.4
%粤マモ

View File

@ -1,2 +0,0 @@
%PDF-1.4
%粤マモ

View File

@ -1,2 +0,0 @@
%PDF-1.4
%粤マモ

View File

@ -1 +0,0 @@
%PDF-1.4

View File

@ -1 +0,0 @@
%PDF-1.4

View File

@ -1 +0,0 @@
%PDF-1.4

View File

@ -1 +0,0 @@
%PDF-1.4

View File

@ -0,0 +1,2 @@
"id" "asset_name" "institution" "industry" "annual_revenue" "rd_investment" "three_year_income" "funding_status" "inheritor_level" "inheritor_ages" "inheritor_age_count" "inheritor_certificates" "heritage_asset_level" "patent_application_no" "patent_remaining_years" "historical_evidence" "patent_certificates" "pattern_images" "implementation_stage" "application_coverage" "cooperation_depth" "offline_activities" "platform_accounts" "sales_volume" "link_views" "circulation" "scarcity_level" "last_market_activity" "market_activity_time" "monthly_transaction_amount" "price_fluctuation" "price_range" "market_price" "infringement_record" "patent_count" "esg_value" "policy_matching" "online_course_views" "pattern_complexity" "normalized_entropy" "legal_risk" "base_pledge_rate" "flow_correction" "model_value_b" "market_value_c" "final_value_ab" "dynamic_pledge_rate" "calculation_result" "calculation_input" "status" "admin_notes" "created_at" "updated_at" "is_active" "user_id"
"19" "蜀锦" "成都古蜀蜀锦研究所" "纺织业" "169" "32" "[169,169,169]" "无资助" "省级传承人" "[0,0,2]" "[0,0,2]" "[]" "国家级非遗" "" "{""artifacts"":2,""ancient_literature"":5,""inheritor_testimony"":5,""modern_research"":6}" "[]" "[]" "成熟应用" "1" "1" "50" "{""douyin"":{""account"":""huguangjing3691"",""likes"":""67000"",""comments"":""800"",""shares"":""500""}}" "5000" "296000" "限量:总发行份数 ≤100份" "限量:总发行份数 ≤100份" "0" "近一周" "月交易额100万500万" "[1580,3980]" "success" "2025-11-17 18:13:17.435287+08:00" "2025-11-17 18:13:17.435322+08:00" "1" "30"

View File

@ -0,0 +1,104 @@
import json
from typing import Dict, Any, List, Tuple
from fastapi import FastAPI
from app import create_app
def load_openapi(app: FastAPI) -> Dict[str, Any]:
return app.openapi()
def is_object_schema(schema: Dict[str, Any]) -> bool:
return schema.get("type") == "object"
def get_schema_props(schema: Dict[str, Any]) -> Dict[str, Any]:
return schema.get("properties", {}) if schema else {}
def check_success_schema(props: Dict[str, Any]) -> Tuple[bool, List[str]]:
issues: List[str] = []
code_prop = props.get("code")
msg_prop = props.get("msg")
data_prop = props.get("data")
if code_prop is None:
issues.append("缺少字段: code")
elif code_prop.get("type") != "integer":
issues.append(f"code类型错误: {code_prop.get('type')}")
if msg_prop is None:
issues.append("缺少字段: msg")
elif msg_prop.get("type") != "string":
issues.append(f"msg类型错误: {msg_prop.get('type')}")
if data_prop is None:
issues.append("缺少字段: data")
else:
tp = data_prop.get("type")
if tp != "object":
issues.append(f"data类型错误: {tp}")
return (len(issues) == 0, issues)
def check_paths(openapi: Dict[str, Any]) -> Dict[str, Any]:
paths = openapi.get("paths", {})
compliant: List[Dict[str, Any]] = []
non_compliant: List[Dict[str, Any]] = []
for path, ops in paths.items():
for method, meta in ops.items():
op_id = meta.get("operationId")
tags = meta.get("tags", [])
responses = meta.get("responses", {})
success = responses.get("200") or responses.get("201")
if not success:
non_compliant.append({
"path": path,
"method": method.upper(),
"operationId": op_id,
"tags": tags,
"issues": ["无成功响应模型(200/201)"],
})
continue
content = success.get("content", {}).get("application/json", {})
schema = content.get("schema")
if not schema:
non_compliant.append({
"path": path,
"method": method.upper(),
"operationId": op_id,
"tags": tags,
"issues": ["成功响应未声明JSON Schema"],
})
continue
props = get_schema_props(schema)
ok, issues = check_success_schema(props)
rec = {
"path": path,
"method": method.upper(),
"operationId": op_id,
"tags": tags,
}
if ok:
compliant.append(rec)
else:
non_compliant.append({**rec, "issues": issues})
total = len(compliant) + len(non_compliant)
rate = 0 if total == 0 else round(len(compliant) / total * 100, 2)
return {
"compliant": compliant,
"non_compliant": non_compliant,
"stats": {"total": total, "compliant": len(compliant), "non_compliant": len(non_compliant), "rate": rate},
}
def main() -> None:
app = create_app()
openapi = load_openapi(app)
result = check_paths(openapi)
print(json.dumps(result, ensure_ascii=False, indent=2))
with open("scripts/response_format_report.json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()

View File

@ -37,8 +37,8 @@
export DOCKER_DEFAULT_PLATFORM=linux/amd64
docker build -t zfc931912343/guzhi-fastapi-admin:v1.5 .
docker push zfc931912343/guzhi-fastapi-admin:v1.5
docker build -t zfc931912343/guzhi-fastapi-admin:v1.8 .
docker push zfc931912343/guzhi-fastapi-admin:v1.8
# 运行容器
@ -71,4 +71,8 @@ docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4 &&
docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4 && docker rm -f guzhi && docker run -itd --name=guzhi -p 8080:80 -v ~/guzhi-data/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped --memory=2g --cpus=1.0 -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4
docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.5 && docker rm -f guzhi_dev && docker run -itd --name=guzhi_dev -p 9990:80 -v ~/guzhi-data-dev/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.5
docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.7 && docker rm -f guzhi_dev && docker run -itd --name=guzhi_dev -p 9990:80 -v ~/guzhi-data-dev/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.7