From c690a95cab4d43b34da9db39e1d55919bfe90234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=96=B9=E6=88=90?= Date: Mon, 24 Nov 2025 16:39:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=8F=91=E7=A5=A8?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 优化响应格式和错误处理 fix: 修复文件上传类型校验和删除无用PDF文件 perf: 添加估值评估审核时间字段和查询条件 docs: 更新Docker镜像版本至v1.8 test: 添加响应格式检查脚本 style: 统一API响应数据结构 chore: 清理无用静态文件和更新构建脚本 --- app/api/v1/__init__.py | 4 +- app/api/v1/app_invoices/app_invoices.py | 77 +++++++++++++++ app/api/v1/app_users/admin_manage.py | 31 +++++- app/api/v1/app_users/app_users.py | 103 +++++++++++-------- app/api/v1/app_valuations/app_valuations.py | 30 +++--- app/api/v1/invoice/invoice.py | 79 ++++++++++++--- app/api/v1/sms/sms.py | 42 +++++--- app/api/v1/upload/upload.py | 21 +--- app/api/v1/valuations/valuations.py | 12 ++- app/controllers/app_user.py | 5 +- app/controllers/invoice.py | 55 ++++++++--- app/controllers/upload.py | 37 ++++++- app/controllers/user_valuation.py | 70 ++++++++++++- app/controllers/valuation.py | 82 ++++++++++++--- app/core/exceptions.py | 8 +- app/models/valuation.py | 3 +- app/schemas/base.py | 11 ++- app/schemas/invoice.py | 12 ++- app/schemas/valuation.py | 14 ++- app/static/files/demo.pdf | 2 - app/static/files/demo_1.pdf | 2 - app/static/files/demo_1_2.pdf | 2 - app/static/files/demo_1_2_3.pdf | 2 - app/static/files/test.pdf | 1 - app/static/files/test_1.pdf | 1 - app/static/files/test_1_2.pdf | 1 - app/static/files/test_1_2_3.pdf | 1 - app/static/files/valuation_assessments.txt | 2 + scripts/response_format_check.py | 104 ++++++++++++++++++++ 估值字段.txt | 10 +- 30 files changed, 658 insertions(+), 166 deletions(-) create mode 100644 app/api/v1/app_invoices/app_invoices.py delete mode 100644 app/static/files/demo.pdf delete mode 100644 app/static/files/demo_1.pdf delete mode 100644 app/static/files/demo_1_2.pdf delete mode 100644 app/static/files/demo_1_2_3.pdf delete mode 100644 app/static/files/test.pdf delete mode 100644 app/static/files/test_1.pdf delete mode 100644 app/static/files/test_1_2.pdf delete mode 100644 app/static/files/test_1_2_3.pdf create mode 100644 app/static/files/valuation_assessments.txt create mode 100644 scripts/response_format_check.py diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index f6f2711..99f0fec 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -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-发票管理"]) diff --git a/app/api/v1/app_invoices/app_invoices.py b/app/api/v1/app_invoices/app_invoices.py new file mode 100644 index 0000000..cfbf0e7 --- /dev/null +++ b/app/api/v1/app_invoices/app_invoices.py @@ -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 "未找到") diff --git a/app/api/v1/app_users/admin_manage.py b/app/api/v1/app_users/admin_manage.py index 967db15..a251305 100644 --- a/app/api/v1/app_users/admin_manage.py +++ b/app/api/v1/app_users/admin_manage.py @@ -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="获取成功") \ No newline at end of file + 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="更新成功") diff --git a/app/api/v1/app_users/app_users.py b/app/api/v1/app_users/app_users.py index 67fd06c..88d9062 100644 --- a/app/api/v1/app_users/app_users.py +++ b/app/api/v1/app_users/app_users.py @@ -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 - } - } \ No newline at end of file + return Success(data={"user_id": current_user.id, "phone": current_user.phone}) diff --git a/app/api/v1/app_valuations/app_valuations.py b/app/api/v1/app_valuations/app_valuations.py index 495b461..1288593 100644 --- a/app/api/v1/app_valuations/app_valuations.py +++ b/app/api/v1/app_valuations/app_valuations.py @@ -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: diff --git a/app/api/v1/invoice/invoice.py b/app/api/v1/invoice/invoice.py index 2706ba2..c054854 100644 --- a/app/api/v1/invoice/invoice.py +++ b/app/api/v1/invoice/invoice.py @@ -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 路由中统一暴露 \ No newline at end of file +@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="获取成功") diff --git a/app/api/v1/sms/sms.py b/app/api/v1/sms/sms.py index 5614ece..01322e9 100644 --- a/app/api/v1/sms/sms.py +++ b/app/api/v1/sms/sms.py @@ -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(...) diff --git a/app/api/v1/upload/upload.py b/app/api/v1/upload/upload.py index b1db2f6..824c251 100644 --- a/app/api/v1/upload/upload.py +++ b/app/api/v1/upload/upload.py @@ -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) \ No newline at end of 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}) diff --git a/app/api/v1/valuations/valuations.py b/app/api/v1/valuations/valuations.py index 49c46ce..75c5422 100644 --- a/app/api/v1/valuations/valuations.py +++ b/app/api/v1/valuations/valuations.py @@ -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="管理员备注更新成功") \ No newline at end of file + return Success(data=json.loads(result.model_dump_json()), msg="管理员备注更新成功") diff --git a/app/controllers/app_user.py b/app/controllers/app_user.py index 563b45c..c753f64 100644 --- a/app/controllers/app_user.py +++ b/app/controllers/app_user.py @@ -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() \ No newline at end of file +app_user_controller = AppUserController() diff --git a/app/controllers/invoice.py b/app/controllers/invoice.py index a57ff99..eceb208 100644 --- a/app/controllers/invoice.py +++ b/app/controllers/invoice.py @@ -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() \ No newline at end of file +invoice_controller = InvoiceController() diff --git a/app/controllers/upload.py b/app/controllers/upload.py index 772f9c0..bd60337 100644 --- a/app/controllers/upload.py +++ b/app/controllers/upload.py @@ -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) \ No newline at end of file + return await UploadController.upload_file(file) diff --git a/app/controllers/user_valuation.py b/app/controllers/user_valuation.py index f1dfc18..343b88c 100644 --- a/app/controllers/user_valuation.py +++ b/app/controllers/user_valuation.py @@ -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() \ No newline at end of file +user_valuation_controller = UserValuationController() diff --git a/app/controllers/valuation.py b/app/controllers/valuation.py index a2e95ed..a94bc7e 100644 --- a/app/controllers/valuation.py +++ b/app/controllers/valuation.py @@ -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 \ No newline at end of file +from app.log import logger diff --git a/app/core/exceptions.py b/app/core/exceptions.py index b1ba744..2d0d1be 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -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) diff --git a/app/models/valuation.py b/app/models/valuation.py index 6c3f886..f3f355e 100644 --- a/app/models/valuation.py +++ b/app/models/valuation.py @@ -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}" \ No newline at end of file + return f"估值ID {self.valuation_id} - 步骤 {self.step_order}: {self.step_name}" diff --git a/app/schemas/base.py b/app/schemas/base.py index 57e7e9d..d04079a 100644 --- a/app/schemas/base.py +++ b/app/schemas/base.py @@ -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") diff --git a/app/schemas/invoice.py b/app/schemas/invoice.py index 22ca22a..20154bb 100644 --- a/app/schemas/invoice.py +++ b/app/schemas/invoice.py @@ -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 \ No newline at end of file + created_at: str diff --git a/app/schemas/valuation.py b/app/schemas/valuation.py index adac318..9e58d1d 100644 --- a/app/schemas/valuation.py +++ b/app/schemas/valuation.py @@ -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) - } \ No newline at end of file + } diff --git a/app/static/files/demo.pdf b/app/static/files/demo.pdf deleted file mode 100644 index be5830c..0000000 --- a/app/static/files/demo.pdf +++ /dev/null @@ -1,2 +0,0 @@ -%PDF-1.4 -% diff --git a/app/static/files/demo_1.pdf b/app/static/files/demo_1.pdf deleted file mode 100644 index be5830c..0000000 --- a/app/static/files/demo_1.pdf +++ /dev/null @@ -1,2 +0,0 @@ -%PDF-1.4 -% diff --git a/app/static/files/demo_1_2.pdf b/app/static/files/demo_1_2.pdf deleted file mode 100644 index be5830c..0000000 --- a/app/static/files/demo_1_2.pdf +++ /dev/null @@ -1,2 +0,0 @@ -%PDF-1.4 -% diff --git a/app/static/files/demo_1_2_3.pdf b/app/static/files/demo_1_2_3.pdf deleted file mode 100644 index be5830c..0000000 --- a/app/static/files/demo_1_2_3.pdf +++ /dev/null @@ -1,2 +0,0 @@ -%PDF-1.4 -% diff --git a/app/static/files/test.pdf b/app/static/files/test.pdf deleted file mode 100644 index 7e761f9..0000000 --- a/app/static/files/test.pdf +++ /dev/null @@ -1 +0,0 @@ -%PDF-1.4 \ No newline at end of file diff --git a/app/static/files/test_1.pdf b/app/static/files/test_1.pdf deleted file mode 100644 index 7e761f9..0000000 --- a/app/static/files/test_1.pdf +++ /dev/null @@ -1 +0,0 @@ -%PDF-1.4 \ No newline at end of file diff --git a/app/static/files/test_1_2.pdf b/app/static/files/test_1_2.pdf deleted file mode 100644 index 7e761f9..0000000 --- a/app/static/files/test_1_2.pdf +++ /dev/null @@ -1 +0,0 @@ -%PDF-1.4 \ No newline at end of file diff --git a/app/static/files/test_1_2_3.pdf b/app/static/files/test_1_2_3.pdf deleted file mode 100644 index 7e761f9..0000000 --- a/app/static/files/test_1_2_3.pdf +++ /dev/null @@ -1 +0,0 @@ -%PDF-1.4 \ No newline at end of file diff --git a/app/static/files/valuation_assessments.txt b/app/static/files/valuation_assessments.txt new file mode 100644 index 0000000..5a988c4 --- /dev/null +++ b/app/static/files/valuation_assessments.txt @@ -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" diff --git a/scripts/response_format_check.py b/scripts/response_format_check.py new file mode 100644 index 0000000..a5a6e28 --- /dev/null +++ b/scripts/response_format_check.py @@ -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() diff --git a/估值字段.txt b/估值字段.txt index d515e8c..83f782e 100644 --- a/估值字段.txt +++ b/估值字段.txt @@ -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 \ No newline at end of file + 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 + + + + \ No newline at end of file