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 .valuations import router as valuations_router
from .invoice.invoice import invoice_router from .invoice.invoice import invoice_router
from .transactions.transactions import transactions_router from .transactions.transactions import transactions_router
from .app_invoices.app_invoices import app_invoices_router
from .sms.sms import router as sms_router from .sms.sms import router as sms_router
v1_router = APIRouter() v1_router = APIRouter()
@ -50,6 +51,7 @@ v1_router.include_router(
tags=["admin-内置接口"], tags=["admin-内置接口"],
) )
v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission], 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(transactions_router, prefix="/transactions", dependencies=[DependAuth, DependPermission], tags=["admin-交易管理"])
v1_router.include_router(sms_router, prefix="/sms", tags=["app-短信服务"]) 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 typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse 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.controllers.app_user import app_user_controller
from app.models.user import AppUser, AppUserQuotaLog from app.models.user import AppUser, AppUserQuotaLog
from app.core.dependency import DependAuth, DependPermission, AuthControl from app.core.dependency import DependAuth, DependPermission, AuthControl
@ -52,9 +52,9 @@ async def update_quota(payload: AppUserQuotaUpdateSchema, operator=Depends(AuthC
) )
if not user: if not user:
raise HTTPException(status_code=404, detail="用户不存在") raise HTTPException(status_code=404, detail="用户不存在")
if payload.remark is not None: # if payload.remark is not None:
user.notes = payload.remark # user.notes = payload.remark
await user.save() # await user.save()
return Success(data={"user_id": user.id, "remaining_quota": user.remaining_quota}, msg="调整成功") return Success(data={"user_id": user.id, "remaining_quota": user.remaining_quota}, msg="调整成功")
@ -78,3 +78,24 @@ async def quota_logs(user_id: int, page: int = Query(1, ge=1), page_size: int =
] ]
data_items = [m.model_dump() for m in models] 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, AppUserQuotaOut,
) )
from app.schemas.app_user import AppUserRegisterOut, TokenValidateOut 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 ( from app.utils.app_user_jwt import (
create_app_user_access_token, create_app_user_access_token,
get_current_app_user, get_current_app_user,
@ -24,7 +24,7 @@ from app.controllers.invoice import invoice_controller
router = APIRouter() router = APIRouter()
@router.post("/register", response_model=BasicResponse[AppUserRegisterOut], summary="用户注册") @router.post("/register", response_model=BasicResponse[dict], summary="用户注册")
async def register( async def register(
register_data: AppUserRegisterSchema register_data: AppUserRegisterSchema
): ):
@ -34,20 +34,16 @@ async def register(
""" """
try: try:
user = await app_user_controller.register(register_data) user = await app_user_controller.register(register_data)
return { return Success(data={
"code": 200,
"msg": "注册成功",
"data": {
"user_id": user.id, "user_id": user.id,
"phone": user.phone, "phone": user.phone,
"default_password": register_data.phone[-6:] "default_password": register_data.phone[-6:]
} })
}
except Exception as e: except Exception as e:
raise HTTPException(status_code=200, detail=str(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( async def login(
login_data: AppUserLoginSchema login_data: AppUserLoginSchema
): ):
@ -67,30 +63,46 @@ async def login(
# 生成访问令牌 # 生成访问令牌
access_token = create_app_user_access_token(user.id, user.phone) access_token = create_app_user_access_token(user.id, user.phone)
return AppUserJWTOut( return Success(data={
access_token=access_token, "access_token": access_token,
token_type="bearer", "token_type": "bearer",
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60 "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)): async def logout(current_user: AppUser = Depends(get_current_app_user)):
""" """
用户登出客户端需要删除本地token 用户登出客户端需要删除本地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)): 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)): 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) pending_invoices = await invoice_controller.count_pending_for_user(current_user.id)
except Exception: except Exception:
pending_invoices = 0 pending_invoices = 0
# 剩余估值次数(占位,可从用户扩展字段或配额表获取) # 剩余估值次数
remaining_quota = 0 remaining_quota = current_user.remaining_quota
return AppUserDashboardOut(remaining_quota=remaining_quota, latest_valuation=latest_out, pending_invoices=pending_invoices) 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)): 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 次与用户类型占位 - 当前实现返回默认 0 次与用户类型占位
- 若后续接入配额系统可从数据库中读取真实值 - 若后续接入配额系统可从数据库中读取真实值
""" """
remaining_count = 0 remaining_count = current_user.remaining_quota
user_type = "体验用户" 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( async def update_profile(
update_data: AppUserUpdateSchema, update_data: AppUserUpdateSchema,
current_user: AppUser = Depends(get_current_app_user) current_user: AppUser = Depends(get_current_app_user)
@ -145,10 +157,26 @@ async def update_profile(
if not updated_user: if not updated_user:
raise HTTPException(status_code=404, detail="用户不存在") 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( async def change_password(
password_data: AppUserChangePasswordSchema, password_data: AppUserChangePasswordSchema,
current_user: AppUser = Depends(get_current_app_user) current_user: AppUser = Depends(get_current_app_user)
@ -165,19 +193,12 @@ async def change_password(
if not success: if not success:
raise HTTPException(status_code=400, detail="原密码错误") 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)): async def validate_token(current_user: AppUser = Depends(get_current_app_user)):
""" """
验证token是否有效 验证token是否有效
""" """
return { return Success(data={"user_id": current_user.id, "phone": current_user.phone})
"code": 200,
"msg": "token有效",
"data": {
"user_id": current_user.id,
"phone": current_user.phone
}
}

View File

@ -18,7 +18,7 @@ from app.schemas.valuation import (
UserValuationOut, UserValuationOut,
UserValuationDetail 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.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 import FinalValueACalculator
# from app.utils.calculation_engine.cultural_value_b2.sub_formulas.living_heritage_b21 import cross_border_depth_dict # 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": "估值计算任务已提交,正在后台处理中", "message": "估值计算任务已提交,正在后台处理中",
"user_id": user_id, "user_id": user_id,
"asset_name": getattr(data, 'asset_name', None) "asset_name": getattr(data, 'asset_name', None)
}, }
msg="估值计算任务已启动"
) )
except Exception as e: 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( async def get_my_valuations(
query: UserValuationQuery = Depends(), query: UserValuationQuery = Depends(),
current_user: AppUser = Depends(get_current_app_user) current_user: AppUser = Depends(get_current_app_user)
@ -641,13 +640,14 @@ async def get_my_valuations(
# 使用model_dump_json()来正确序列化datetime然后解析为dict列表 # 使用model_dump_json()来正确序列化datetime然后解析为dict列表
import json import json
serialized_items = [json.loads(item.model_dump_json()) for item in result.items] serialized_items = [json.loads(item.model_dump_json()) for item in result.items]
return SuccessExtra( return Success(
data=serialized_items, data={
total=result.total, "items": serialized_items,
page=result.page, "total": result.total,
page_size=result.size, "page": result.page,
pages=result.pages, "page_size": result.size,
msg="获取估值评估列表成功" "pages": result.pages,
}
) )
except Exception as e: except Exception as e:
raise HTTPException( 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( async def get_valuation_detail(
valuation_id: int, valuation_id: int,
current_user: AppUser = Depends(get_current_app_user) current_user: AppUser = Depends(get_current_app_user)
@ -679,7 +679,7 @@ async def get_valuation_detail(
# 使用model_dump_json()来正确序列化datetime然后解析为dict # 使用model_dump_json()来正确序列化datetime然后解析为dict
import json import json
result_dict = json.loads(result.model_dump_json()) result_dict = json.loads(result.model_dump_json())
return Success(data=result_dict, msg="获取估值评估详情成功") return Success(data=result_dict)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
@ -700,7 +700,7 @@ async def get_my_valuation_statistics(
result = await user_valuation_controller.get_user_valuation_statistics( result = await user_valuation_controller.get_user_valuation_statistics(
user_id=current_user.id user_id=current_user.id
) )
return Success(data=result, msg="获取统计信息成功") return Success(data=result)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -728,7 +728,7 @@ async def delete_valuation(
detail="估值评估记录不存在或已被删除" detail="估值评估记录不存在或已被删除"
) )
return Success(data={"deleted": True}, msg="删除估值评估成功") return Success(data={"deleted": True})
except HTTPException: except HTTPException:
raise raise
except Exception as e: 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 typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
@ -8,6 +8,7 @@ from app.schemas.invoice import (
UpdateStatus, UpdateStatus,
UpdateType, UpdateType,
InvoiceHeaderCreate, InvoiceHeaderCreate,
InvoiceHeaderUpdate,
PaymentReceiptCreate, PaymentReceiptCreate,
InvoiceOut, InvoiceOut,
InvoiceList, InvoiceList,
@ -15,12 +16,15 @@ from app.schemas.invoice import (
PaymentReceiptOut, PaymentReceiptOut,
) )
from app.controllers.invoice import invoice_controller 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 = 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( async def list_invoices(
phone: Optional[str] = Query(None), phone: Optional[str] = Query(None),
company_name: Optional[str] = Query(None), company_name: Optional[str] = Query(None),
@ -28,6 +32,7 @@ async def list_invoices(
status: Optional[str] = Query(None), status: Optional[str] = Query(None),
ticket_type: Optional[str] = Query(None), ticket_type: Optional[str] = Query(None),
invoice_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: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100), page_size: int = Query(10, ge=1, le=100),
): ):
@ -45,13 +50,14 @@ async def list_invoices(
status=status, status=status,
ticket_type=ticket_type, ticket_type=ticket_type,
invoice_type=invoice_type, invoice_type=invoice_type,
app_user_id=user_id,
) )
return SuccessExtra( return SuccessExtra(
data=result.items, total=result.total, page=result.page, page_size=result.page_size, msg="获取成功" 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(...)): async def invoice_detail(id: int = Query(...)):
""" """
根据ID获取发票详情 根据ID获取发票详情
@ -62,7 +68,7 @@ async def invoice_detail(id: int = Query(...)):
return Success(data=out, msg="获取成功") 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): async def create_invoice(data: InvoiceCreate):
""" """
创建发票记录 创建发票记录
@ -72,7 +78,7 @@ async def create_invoice(data: InvoiceCreate):
return Success(data=out, msg="创建成功") 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(...)): 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 "未找到") 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(...)): 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 "未找到") 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): async def update_invoice_status(data: UpdateStatus):
""" """
更新发票状态pending|invoiced|rejected|refunded 更新发票状态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): async def upload_payment_receipt(id: int, data: PaymentReceiptCreate):
""" """
上传对公转账付款凭证 上传对公转账付款凭证
""" """
receipt = await invoice_controller.create_receipt(id, data) 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)): 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) headers = await invoice_controller.get_headers(user_id=app_user_id)
return Success(data=headers, msg="获取成功") 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): 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 "未找到") 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)): 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="创建成功") 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): 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 "未找到") 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.core.dependency import DependAuth
from app.log import logger from app.log import logger
from app.schemas.app_user import AppUserInfoOut, AppUserJWTOut from app.schemas.app_user import AppUserInfoOut, AppUserJWTOut
from app.schemas.base import BasicResponse, Success
class SendCodeRequest(BaseModel): class SendCodeRequest(BaseModel):
@ -44,8 +45,8 @@ rate_limiter = PhoneRateLimiter(60)
router = APIRouter(tags=["短信服务"]) router = APIRouter(tags=["短信服务"])
@router.post("/send-code", response_model=SendResponse, summary="验证码发送") @router.post("/send-code", response_model=BasicResponse[dict], summary="验证码发送")
async def send_code(payload: SendCodeRequest) -> SendResponse: async def send_code(payload: SendCodeRequest) -> BasicResponse[dict]:
"""发送验证码短信 """发送验证码短信
Args: Args:
@ -68,7 +69,13 @@ async def send_code(payload: SendCodeRequest) -> SendResponse:
rid = res.get("RequestId") or res.get("MessageId") rid = res.get("RequestId") or res.get("MessageId")
if code == "OK": if code == "OK":
logger.info("sms.send_code success phone={} request_id={}", payload.phone, rid) 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" msg = res.get("Message") or res.get("ResponseDescription") or "error"
logger.warning("sms.send_code fail phone={} code={} msg={}", payload.phone, code, msg) 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)) 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="短信服务异常") raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="短信服务异常")
@router.post("/send-report", response_model=SendResponse, summary="报告通知发送", dependencies=[DependAuth]) @router.post("/send-report", response_model=BasicResponse[dict], summary="报告通知发送", dependencies=[DependAuth])
async def send_report(payload: SendReportRequest) -> SendResponse: async def send_report(payload: SendReportRequest) -> BasicResponse[dict]:
"""发送报告通知短信 """发送报告通知短信
Args: Args:
@ -98,7 +105,13 @@ async def send_report(payload: SendReportRequest) -> SendResponse:
rid = res.get("RequestId") or res.get("MessageId") rid = res.get("RequestId") or res.get("MessageId")
if code == "OK": if code == "OK":
logger.info("sms.send_report success phone={} request_id={}", payload.phone, rid) 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" msg = res.get("Message") or res.get("ResponseDescription") or "error"
logger.warning("sms.send_report fail phone={} code={} msg={}", payload.phone, code, msg) 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)) 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="短信服务异常") raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="短信服务异常")
@router.post("/verify-code", summary="验证码验证", response_model=VerifyResponse) @router.post("/verify-code", summary="验证码验证", response_model=BasicResponse[dict])
async def verify_code(payload: VerifyCodeRequest) -> VerifyResponse: async def verify_code(payload: VerifyCodeRequest) -> BasicResponse[dict]:
"""验证验证码 """验证验证码
Args: Args:
@ -137,7 +150,7 @@ async def verify_code(payload: VerifyCodeRequest) -> VerifyResponse:
store.clear_code(payload.phone) store.clear_code(payload.phone)
store.reset_failures(payload.phone) store.reset_failures(payload.phone)
logger.info("sms.verify_code success phone={}", 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): class SMSLoginRequest(BaseModel):
@ -146,8 +159,8 @@ class SMSLoginRequest(BaseModel):
device_id: Optional[str] = Field(None) device_id: Optional[str] = Field(None)
@router.post("/login", summary="短信验证码登录", response_model=SMSLoginResponse) @router.post("/login", summary="短信验证码登录", response_model=BasicResponse[dict])
async def sms_login(payload: SMSLoginRequest) -> SMSLoginResponse: async def sms_login(payload: SMSLoginRequest) -> BasicResponse[dict]:
ok, reason = store.can_verify(payload.phone_number) ok, reason = store.can_verify(payload.phone_number)
if not ok: if not ok:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=str(reason)) 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( user_info = AppUserInfoOut(
id=user.id, id=user.id,
phone=user.phone, phone=user.phone,
nickname=user.nickname, nickname=getattr(user, "alias", None),
avatar=user.avatar, avatar=None,
company_name=user.company_name, company_name=user.company_name,
company_address=user.company_address, company_address=user.company_address,
company_contact=user.company_contact, company_contact=user.company_contact,
@ -192,9 +205,10 @@ async def sms_login(payload: SMSLoginRequest) -> SMSLoginResponse:
last_login=user.last_login, last_login=user.last_login,
created_at=user.created_at, created_at=user.created_at,
updated_at=user.updated_at, updated_at=user.updated_at,
remaining_quota=user.remaining_quota,
) )
token_out = AppUserJWTOut(access_token=access_token, expires_in=ACCESS_TOKEN_EXPIRE_MINUTES) 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): class VerifyCodeRequest(BaseModel):
phone: str = Field(...) phone: str = Field(...)
code: str = Field(...) code: str = Field(...)

View File

@ -1,22 +1,11 @@
from fastapi import APIRouter, UploadFile, File from fastapi import APIRouter, UploadFile, File
from app.controllers.upload import UploadController from app.controllers.upload import UploadController
from app.schemas.upload import ImageUploadResponse, FileUploadResponse from app.schemas.upload import ImageUploadResponse, FileUploadResponse
from app.schemas.base import BasicResponse, Success
router = APIRouter() router = APIRouter()
@router.post("/image", response_model=ImageUploadResponse, summary="上传图片") @router.post("/file", response_model=BasicResponse[dict], summary="统一上传接口")
async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse: 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})
: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)

View File

@ -87,6 +87,11 @@ async def get_valuations(
heritage_level: Optional[str] = Query(None, description="非遗等级"), heritage_level: Optional[str] = Query(None, description="非遗等级"),
status: Optional[str] = Query(None, description="评估状态"), status: Optional[str] = Query(None, description="评估状态"),
is_active: Optional[bool] = 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="页码"), page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量") size: int = Query(10, ge=1, le=100, description="每页数量")
): ):
@ -98,6 +103,11 @@ async def get_valuations(
heritage_level=heritage_level, heritage_level=heritage_level,
status=status, status=status,
is_active=is_active, is_active=is_active,
phone=phone,
submitted_start=submitted_start,
submitted_end=submitted_end,
audited_start=audited_start,
audited_end=audited_end,
page=page, page=page,
size=size size=size
) )

View File

@ -86,6 +86,9 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
# 更新字段 # 更新字段
update_dict = update_data.model_dump(exclude_unset=True) 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(): for field, value in update_dict.items():
setattr(user, field, value) setattr(user, field, value)

View File

@ -9,6 +9,7 @@ from app.schemas.invoice import (
InvoiceOut, InvoiceOut,
InvoiceList, InvoiceList,
InvoiceHeaderCreate, InvoiceHeaderCreate,
InvoiceHeaderUpdate,
InvoiceHeaderOut, InvoiceHeaderOut,
UpdateStatus, UpdateStatus,
UpdateType, UpdateType,
@ -60,6 +61,22 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
header = await InvoiceHeader.filter(id=id_).first() header = await InvoiceHeader.filter(id=id_).first()
return InvoiceHeaderOut.model_validate(header) if header else None 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: 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"]) qs = qs.filter(ticket_type=filters["ticket_type"])
if filters.get("invoice_type"): if filters.get("invoice_type"):
qs = qs.filter(invoice_type=filters["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() total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size) 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: for r in rows:
inv = await r.invoice inv = await r.invoice
items.append({ items.append({
"id": r.id,
"invoice_id": getattr(inv, "id", None),
"submitted_at": r.created_at.isoformat() if r.created_at else "", "submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": { "receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
"receipts": [
{
"id": r.id, "id": r.id,
"url": r.url, "url": r.url,
"note": r.note, "note": r.note,
"verified": r.verified, "verified": r.verified,
}, }
],
"phone": inv.phone, "phone": inv.phone,
"wechat": inv.wechat, "wechat": inv.wechat,
"company_name": inv.company_name, "company_name": inv.company_name,
@ -267,13 +291,18 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
return None return None
inv = await r.invoice inv = await r.invoice
return { return {
"id": r.id,
"invoice_id": getattr(inv, "id", None),
"submitted_at": r.created_at.isoformat() if r.created_at else "", "submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": { "receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
"receipts": [
{
"id": r.id, "id": r.id,
"url": r.url, "url": r.url,
"note": r.note, "note": r.note,
"verified": r.verified, "verified": r.verified,
}, }
],
"phone": inv.phone, "phone": inv.phone,
"wechat": inv.wechat, "wechat": inv.wechat,
"company_name": inv.company_name, "company_name": inv.company_name,

View File

@ -15,8 +15,9 @@ class UploadController:
:param file: 上传的图片文件 :param file: 上传的图片文件
:return: 图片URL和文件名 :return: 图片URL和文件名
""" """
# 检查文件类型 ext = os.path.splitext(file.filename or "")[1].lower()
if not file.content_type.startswith('image/'): image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
if not (file.content_type.startswith('image/') or ext in image_exts):
raise ValueError("只支持上传图片文件") raise ValueError("只支持上传图片文件")
# 获取项目根目录 # 获取项目根目录
@ -61,8 +62,32 @@ class UploadController:
"application/vnd.ms-excel", "application/vnd.ms-excel",
"application/zip", "application/zip",
"application/x-zip-compressed", "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("不支持的文件类型") raise ValueError("不支持的文件类型")
base_dir = Path(__file__).resolve().parent.parent base_dir = Path(__file__).resolve().parent.parent
@ -95,7 +120,9 @@ class UploadController:
统一上传入口自动识别图片与非图片类型 统一上传入口自动识别图片与非图片类型
返回统一结构url, filename, content_type 返回统一结构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) img = await UploadController.upload_image(file)
return FileUploadResponse(url=img.url, filename=img.filename, content_type=file.content_type or "image") return FileUploadResponse(url=img.url, filename=img.filename, content_type=file.content_type or "image")
# 非图片类型复用原文件上传校验 # 非图片类型复用原文件上传校验

View File

@ -114,7 +114,73 @@ class UserValuationController:
async def _to_user_out(self, valuation: ValuationAssessment) -> UserValuationOut: 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: async def _to_user_detail(self, valuation: ValuationAssessment) -> UserValuationDetail:
"""转换为用户端详细模型""" """转换为用户端详细模型"""

View File

@ -13,6 +13,7 @@ from app.schemas.valuation import (
ValuationCalculationStepCreate, ValuationCalculationStepCreate,
ValuationCalculationStepOut ValuationCalculationStepOut
) )
from app.models.user import AppUser
class ValuationController: class ValuationController:
@ -76,13 +77,15 @@ class ValuationController:
create_data = data.model_dump() create_data = data.model_dump()
create_data['user_id'] = user_id create_data['user_id'] = user_id
valuation = await self.model.create(**create_data) 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]: async def get_by_id(self, valuation_id: int) -> Optional[ValuationAssessmentOut]:
"""根据ID获取估值评估""" """根据ID获取估值评估"""
valuation = await self.model.filter(id=valuation_id, is_active=True).first() valuation = await self.model.filter(id=valuation_id, is_active=True).first()
if valuation: if valuation:
return ValuationAssessmentOut.model_validate(valuation) out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
return None return None
async def update(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]: 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) update_data = data.model_dump(exclude_unset=True)
if update_data: 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.update_from_dict(update_data)
await valuation.save() 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: async def delete(self, valuation_id: int) -> bool:
"""软删除估值评估""" """软删除估值评估"""
@ -121,6 +128,7 @@ class ValuationController:
# 转换为输出模型 # 转换为输出模型
items = [ValuationAssessmentOut.model_validate(v) for v in valuations] items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
items = await self._attach_user_phone_bulk(items)
# 计算总页数 # 计算总页数
pages = (total + query.size - 1) // query.size pages = (total + query.size - 1) // query.size
@ -155,6 +163,37 @@ class ValuationController:
if hasattr(query, 'status') and query.status: if hasattr(query, 'status') and query.status:
queryset = queryset.filter(status=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 return queryset
async def get_statistics(self) -> dict: async def get_statistics(self) -> dict:
@ -195,6 +234,7 @@ class ValuationController:
# 转换为输出模型 # 转换为输出模型
items = [ValuationAssessmentOut.model_validate(v) for v in valuations] items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
items = await self._attach_user_phone_bulk(items)
# 计算总页数 # 计算总页数
pages = (total + size - 1) // size pages = (total + size - 1) // size
@ -213,12 +253,14 @@ class ValuationController:
if not valuation: if not valuation:
return None return None
update_data = {"status": "approved"} from datetime import datetime
update_data = {"status": "approved", "audited_at": datetime.now()}
if admin_notes: if admin_notes:
update_data["admin_notes"] = admin_notes update_data["admin_notes"] = admin_notes
await valuation.update_from_dict(update_data).save() 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]: async def reject_valuation(self, valuation_id: int, admin_notes: Optional[str] = None) -> Optional[ValuationAssessmentOut]:
"""审核拒绝估值评估""" """审核拒绝估值评估"""
@ -226,12 +268,14 @@ class ValuationController:
if not valuation: if not valuation:
return None return None
update_data = {"status": "rejected"} from datetime import datetime
update_data = {"status": "rejected", "audited_at": datetime.now()}
if admin_notes: if admin_notes:
update_data["admin_notes"] = admin_notes update_data["admin_notes"] = admin_notes
await valuation.update_from_dict(update_data).save() 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]: async def update_admin_notes(self, valuation_id: int, admin_notes: str) -> Optional[ValuationAssessmentOut]:
"""更新管理员备注""" """更新管理员备注"""
@ -240,7 +284,23 @@ class ValuationController:
return None return None
await valuation.update_from_dict({"admin_notes": admin_notes}).save() 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
# 创建控制器实例 # 创建控制器实例

View File

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

View File

@ -86,6 +86,7 @@ class ValuationAssessment(Model):
admin_notes = fields.TextField(null=True, description="管理员备注") admin_notes = fields.TextField(null=True, description="管理员备注")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间") created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间") updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
audited_at = fields.DatetimeField(null=True, description="审核时间")
is_active = fields.BooleanField(default=True, description="是否激活") is_active = fields.BooleanField(default=True, description="是否激活")
class Meta: class Meta:

View File

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

View File

@ -23,6 +23,16 @@ class InvoiceHeaderOut(BaseModel):
email: EmailStr 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): class InvoiceCreate(BaseModel):
ticket_type: str = Field(..., pattern=r"^(electronic|paper)$") ticket_type: str = Field(..., pattern=r"^(electronic|paper)$")
invoice_type: str = Field(..., pattern=r"^(special|normal)$") invoice_type: str = Field(..., pattern=r"^(special|normal)$")

View File

@ -134,6 +134,9 @@ class ValuationAssessmentOut(ValuationAssessmentBase):
"""估值评估输出模型""" """估值评估输出模型"""
id: int = Field(..., description="主键ID") id: int = Field(..., description="主键ID")
user_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="评估状态") status: str = Field(..., description="评估状态")
admin_notes: Optional[str] = Field(None, description="管理员备注") admin_notes: Optional[str] = Field(None, description="管理员备注")
created_at: datetime = Field(..., description="创建时间") created_at: datetime = Field(..., description="创建时间")
@ -159,6 +162,8 @@ class UserValuationOut(ValuationAssessmentBase):
"""用户端估值评估输出模型""" """用户端估值评估输出模型"""
id: int = Field(..., description="主键ID") id: int = Field(..., description="主键ID")
user_id: Optional[int] = Field(None, 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="评估状态") status: str = Field(..., description="评估状态")
admin_notes: Optional[str] = Field(None, description="管理员备注") admin_notes: Optional[str] = Field(None, description="管理员备注")
created_at: datetime = Field(..., description="创建时间") created_at: datetime = Field(..., description="创建时间")
@ -176,6 +181,8 @@ class UserValuationOut(ValuationAssessmentBase):
class UserValuationDetail(ValuationAssessmentBase): class UserValuationDetail(ValuationAssessmentBase):
"""用户端详细估值评估模型""" """用户端详细估值评估模型"""
id: int = Field(..., description="主键ID") 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="评估状态") status: str = Field(..., description="评估状态")
admin_notes: Optional[str] = Field(None, description="管理员备注") admin_notes: Optional[str] = Field(None, description="管理员备注")
created_at: datetime = Field(..., description="创建时间") created_at: datetime = Field(..., description="创建时间")
@ -230,6 +237,11 @@ class ValuationAssessmentQuery(BaseModel):
heritage_level: Optional[str] = Field(None, description="非遗等级") heritage_level: Optional[str] = Field(None, description="非遗等级")
status: Optional[str] = Field(None, description="评估状态: pending(待审核), approved(已通过), rejected(已拒绝)") status: Optional[str] = Field(None, description="评估状态: pending(待审核), approved(已通过), rejected(已拒绝)")
is_active: Optional[bool] = Field(None, description="是否激活") 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="页码") page: int = Field(1, ge=1, description="页码")
size: int = Field(10, ge=1, le=100, description="每页数量") size: int = Field(10, ge=1, le=100, description="每页数量")

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 export DOCKER_DEFAULT_PLATFORM=linux/amd64
docker build -t zfc931912343/guzhi-fastapi-admin:v1.5 . docker build -t zfc931912343/guzhi-fastapi-admin:v1.8 .
docker push zfc931912343/guzhi-fastapi-admin:v1.5 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.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