feat: 新增发票管理模块和用户端接口
refactor: 优化响应格式和错误处理 fix: 修复文件上传类型校验和删除无用PDF文件 perf: 添加估值评估审核时间字段和查询条件 docs: 更新Docker镜像版本至v1.8 test: 添加响应格式检查脚本 style: 统一API响应数据结构 chore: 清理无用静态文件和更新构建脚本
This commit is contained in:
parent
1dd9a313e6
commit
c690a95cab
@ -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-发票管理"])
|
||||
|
||||
77
app/api/v1/app_invoices/app_invoices.py
Normal file
77
app/api/v1/app_invoices/app_invoices.py
Normal 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 "未找到")
|
||||
@ -2,7 +2,7 @@ from fastapi import APIRouter, Query, Depends, HTTPException
|
||||
from typing import Optional
|
||||
|
||||
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
|
||||
from app.schemas.app_user import AppUserQuotaUpdateSchema, AppUserQuotaLogOut
|
||||
from app.schemas.app_user import AppUserQuotaUpdateSchema, AppUserQuotaLogOut, AppUserUpdateSchema
|
||||
from app.controllers.app_user import app_user_controller
|
||||
from app.models.user import AppUser, AppUserQuotaLog
|
||||
from app.core.dependency import DependAuth, DependPermission, AuthControl
|
||||
@ -52,9 +52,9 @@ async def update_quota(payload: AppUserQuotaUpdateSchema, operator=Depends(AuthC
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if payload.remark is not None:
|
||||
user.notes = payload.remark
|
||||
await user.save()
|
||||
# if payload.remark is not None:
|
||||
# user.notes = payload.remark
|
||||
# await user.save()
|
||||
return Success(data={"user_id": user.id, "remaining_quota": user.remaining_quota}, msg="调整成功")
|
||||
|
||||
|
||||
@ -77,4 +77,25 @@ async def quota_logs(user_id: int, page: int = Query(1, ge=1), page_size: int =
|
||||
) for r in rows
|
||||
]
|
||||
data_items = [m.model_dump() for m in models]
|
||||
return SuccessExtra(data=data_items, total=total, page=page, page_size=page_size, msg="获取成功")
|
||||
return SuccessExtra(data=data_items, total=total, page=page, page_size=page_size, msg="获取成功")
|
||||
|
||||
|
||||
@admin_app_users_router.put("/{user_id}", summary="更新App用户信息", response_model=BasicResponse[dict])
|
||||
async def update_app_user(user_id: int, data: AppUserUpdateSchema):
|
||||
user = await app_user_controller.update_user_info(user_id, data)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
return Success(data={
|
||||
"id": user.id,
|
||||
"phone": user.phone,
|
||||
"wechat": getattr(user, "alias", None),
|
||||
"company_name": getattr(user, "company_name", None),
|
||||
"company_address": getattr(user, "company_address", None),
|
||||
"company_contact": getattr(user, "company_contact", None),
|
||||
"company_phone": getattr(user, "company_phone", None),
|
||||
"company_email": getattr(user, "company_email", None),
|
||||
"is_active": user.is_active,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else "",
|
||||
"updated_at": user.updated_at.isoformat() if user.updated_at else "",
|
||||
"remaining_quota": int(getattr(user, "remaining_quota", 0) or 0),
|
||||
}, msg="更新成功")
|
||||
|
||||
@ -11,7 +11,7 @@ from app.schemas.app_user import (
|
||||
AppUserQuotaOut,
|
||||
)
|
||||
from app.schemas.app_user import AppUserRegisterOut, TokenValidateOut
|
||||
from app.schemas.base import BasicResponse, MessageOut
|
||||
from app.schemas.base import BasicResponse, MessageOut, Success
|
||||
from app.utils.app_user_jwt import (
|
||||
create_app_user_access_token,
|
||||
get_current_app_user,
|
||||
@ -24,7 +24,7 @@ from app.controllers.invoice import invoice_controller
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/register", response_model=BasicResponse[AppUserRegisterOut], summary="用户注册")
|
||||
@router.post("/register", response_model=BasicResponse[dict], summary="用户注册")
|
||||
async def register(
|
||||
register_data: AppUserRegisterSchema
|
||||
):
|
||||
@ -34,20 +34,16 @@ async def register(
|
||||
"""
|
||||
try:
|
||||
user = await app_user_controller.register(register_data)
|
||||
return {
|
||||
"code": 200,
|
||||
"msg": "注册成功",
|
||||
"data": {
|
||||
"user_id": user.id,
|
||||
"phone": user.phone,
|
||||
"default_password": register_data.phone[-6:]
|
||||
}
|
||||
}
|
||||
return Success(data={
|
||||
"user_id": user.id,
|
||||
"phone": user.phone,
|
||||
"default_password": register_data.phone[-6:]
|
||||
})
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=200, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/login", response_model=AppUserJWTOut, summary="用户登录")
|
||||
@router.post("/login", response_model=BasicResponse[dict], summary="用户登录")
|
||||
async def login(
|
||||
login_data: AppUserLoginSchema
|
||||
):
|
||||
@ -67,30 +63,46 @@ async def login(
|
||||
# 生成访问令牌
|
||||
access_token = create_app_user_access_token(user.id, user.phone)
|
||||
|
||||
return AppUserJWTOut(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
)
|
||||
return Success(data={
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
})
|
||||
|
||||
|
||||
@router.post("/logout", summary="用户登出", response_model=BasicResponse[MessageOut])
|
||||
@router.post("/logout", summary="用户登出", response_model=BasicResponse[dict])
|
||||
async def logout(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
用户登出(客户端需要删除本地token)
|
||||
"""
|
||||
return {"code": 200, "msg": "OK", "data": {"message": "登出成功"}}
|
||||
return Success(data={"message": "登出成功"})
|
||||
|
||||
|
||||
@router.get("/profile", response_model=AppUserInfoOut, summary="获取用户信息")
|
||||
@router.get("/profile", response_model=BasicResponse[dict], summary="获取用户信息")
|
||||
async def get_profile(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
获取当前用户信息
|
||||
"""
|
||||
return current_user
|
||||
user_info = AppUserInfoOut(
|
||||
id=current_user.id,
|
||||
phone=current_user.phone,
|
||||
nickname=getattr(current_user, "alias", None),
|
||||
avatar=None,
|
||||
company_name=current_user.company_name,
|
||||
company_address=current_user.company_address,
|
||||
company_contact=current_user.company_contact,
|
||||
company_phone=current_user.company_phone,
|
||||
company_email=current_user.company_email,
|
||||
is_active=current_user.is_active,
|
||||
last_login=current_user.last_login,
|
||||
created_at=current_user.created_at,
|
||||
updated_at=current_user.updated_at,
|
||||
remaining_quota=current_user.remaining_quota,
|
||||
)
|
||||
return Success(data=user_info.model_dump())
|
||||
|
||||
|
||||
@router.get("/dashboard", response_model=AppUserDashboardOut, summary="用户首页摘要")
|
||||
@router.get("/dashboard", response_model=BasicResponse[dict], summary="用户首页摘要")
|
||||
async def get_dashboard(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
用户首页摘要
|
||||
@ -115,12 +127,12 @@ async def get_dashboard(current_user: AppUser = Depends(get_current_app_user)):
|
||||
pending_invoices = await invoice_controller.count_pending_for_user(current_user.id)
|
||||
except Exception:
|
||||
pending_invoices = 0
|
||||
# 剩余估值次数(占位,可从用户扩展字段或配额表获取)
|
||||
remaining_quota = 0
|
||||
return AppUserDashboardOut(remaining_quota=remaining_quota, latest_valuation=latest_out, pending_invoices=pending_invoices)
|
||||
# 剩余估值次数
|
||||
remaining_quota = current_user.remaining_quota
|
||||
return Success(data={"remaining_quota": remaining_quota, "latest_valuation": latest_out, "pending_invoices": pending_invoices})
|
||||
|
||||
|
||||
@router.get("/quota", response_model=AppUserQuotaOut, summary="剩余估值次数")
|
||||
@router.get("/quota", response_model=BasicResponse[dict], summary="剩余估值次数")
|
||||
async def get_quota(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
剩余估值次数查询
|
||||
@ -128,12 +140,12 @@ async def get_quota(current_user: AppUser = Depends(get_current_app_user)):
|
||||
- 当前实现返回默认 0 次与用户类型占位
|
||||
- 若后续接入配额系统,可从数据库中读取真实值
|
||||
"""
|
||||
remaining_count = 0
|
||||
remaining_count = current_user.remaining_quota
|
||||
user_type = "体验用户"
|
||||
return AppUserQuotaOut(remaining_count=remaining_count, user_type=user_type)
|
||||
return Success(data={"remaining_count": remaining_count, "user_type": user_type})
|
||||
|
||||
|
||||
@router.put("/profile", response_model=AppUserInfoOut, summary="更新用户信息")
|
||||
@router.put("/profile", response_model=BasicResponse[dict], summary="更新用户信息")
|
||||
async def update_profile(
|
||||
update_data: AppUserUpdateSchema,
|
||||
current_user: AppUser = Depends(get_current_app_user)
|
||||
@ -145,10 +157,26 @@ async def update_profile(
|
||||
if not updated_user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
return updated_user
|
||||
user_info = AppUserInfoOut(
|
||||
id=updated_user.id,
|
||||
phone=updated_user.phone,
|
||||
nickname=getattr(updated_user, "alias", None),
|
||||
avatar=None,
|
||||
company_name=updated_user.company_name,
|
||||
company_address=updated_user.company_address,
|
||||
company_contact=updated_user.company_contact,
|
||||
company_phone=updated_user.company_phone,
|
||||
company_email=updated_user.company_email,
|
||||
is_active=updated_user.is_active,
|
||||
last_login=updated_user.last_login,
|
||||
created_at=updated_user.created_at,
|
||||
updated_at=updated_user.updated_at,
|
||||
remaining_quota=updated_user.remaining_quota,
|
||||
)
|
||||
return Success(data=user_info.model_dump())
|
||||
|
||||
|
||||
@router.post("/change-password", summary="修改密码", response_model=BasicResponse[MessageOut])
|
||||
@router.post("/change-password", summary="修改密码", response_model=BasicResponse[dict])
|
||||
async def change_password(
|
||||
password_data: AppUserChangePasswordSchema,
|
||||
current_user: AppUser = Depends(get_current_app_user)
|
||||
@ -165,19 +193,12 @@ async def change_password(
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="原密码错误")
|
||||
|
||||
return {"code": 200, "msg": "OK", "data": {"message": "密码修改成功"}}
|
||||
return Success(data={"message": "密码修改成功"})
|
||||
|
||||
|
||||
@router.get("/validate-token", summary="验证token", response_model=BasicResponse[TokenValidateOut])
|
||||
@router.get("/validate-token", summary="验证token", response_model=BasicResponse[dict])
|
||||
async def validate_token(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
验证token是否有效
|
||||
"""
|
||||
return {
|
||||
"code": 200,
|
||||
"msg": "token有效",
|
||||
"data": {
|
||||
"user_id": current_user.id,
|
||||
"phone": current_user.phone
|
||||
}
|
||||
}
|
||||
return Success(data={"user_id": current_user.id, "phone": current_user.phone})
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Query, Depends, Header, HTTPException
|
||||
from typing import Optional
|
||||
|
||||
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
|
||||
@ -8,6 +8,7 @@ from app.schemas.invoice import (
|
||||
UpdateStatus,
|
||||
UpdateType,
|
||||
InvoiceHeaderCreate,
|
||||
InvoiceHeaderUpdate,
|
||||
PaymentReceiptCreate,
|
||||
InvoiceOut,
|
||||
InvoiceList,
|
||||
@ -15,12 +16,15 @@ from app.schemas.invoice import (
|
||||
PaymentReceiptOut,
|
||||
)
|
||||
from app.controllers.invoice import invoice_controller
|
||||
from app.utils.app_user_jwt import get_current_app_user
|
||||
from app.core.dependency import DependAuth, DependPermission
|
||||
from app.models.user import AppUser
|
||||
|
||||
|
||||
invoice_router = APIRouter(tags=["发票管理"])
|
||||
|
||||
|
||||
@invoice_router.get("/list", summary="获取发票列表", response_model=PageResponse[InvoiceOut])
|
||||
@invoice_router.get("/list", summary="获取发票列表", response_model=PageResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
|
||||
async def list_invoices(
|
||||
phone: Optional[str] = Query(None),
|
||||
company_name: Optional[str] = Query(None),
|
||||
@ -28,6 +32,7 @@ async def list_invoices(
|
||||
status: Optional[str] = Query(None),
|
||||
ticket_type: Optional[str] = Query(None),
|
||||
invoice_type: Optional[str] = Query(None),
|
||||
user_id: Optional[int] = Query(None, description="按App用户ID过滤"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100),
|
||||
):
|
||||
@ -45,13 +50,14 @@ async def list_invoices(
|
||||
status=status,
|
||||
ticket_type=ticket_type,
|
||||
invoice_type=invoice_type,
|
||||
app_user_id=user_id,
|
||||
)
|
||||
return SuccessExtra(
|
||||
data=result.items, total=result.total, page=result.page, page_size=result.page_size, msg="获取成功"
|
||||
)
|
||||
|
||||
|
||||
@invoice_router.get("/detail", summary="发票详情", response_model=BasicResponse[InvoiceOut])
|
||||
@invoice_router.get("/detail", summary="发票详情", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
|
||||
async def invoice_detail(id: int = Query(...)):
|
||||
"""
|
||||
根据ID获取发票详情
|
||||
@ -62,7 +68,7 @@ async def invoice_detail(id: int = Query(...)):
|
||||
return Success(data=out, msg="获取成功")
|
||||
|
||||
|
||||
@invoice_router.post("/create", summary="创建发票", response_model=BasicResponse[InvoiceOut])
|
||||
@invoice_router.post("/create", summary="创建发票", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
|
||||
async def create_invoice(data: InvoiceCreate):
|
||||
"""
|
||||
创建发票记录
|
||||
@ -72,7 +78,7 @@ async def create_invoice(data: InvoiceCreate):
|
||||
return Success(data=out, msg="创建成功")
|
||||
|
||||
|
||||
@invoice_router.post("/update", summary="更新发票", response_model=BasicResponse[InvoiceOut])
|
||||
@invoice_router.post("/update", summary="更新发票", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
|
||||
async def update_invoice(data: InvoiceUpdate, id: int = Query(...)):
|
||||
"""
|
||||
更新发票记录
|
||||
@ -82,7 +88,7 @@ async def update_invoice(data: InvoiceUpdate, id: int = Query(...)):
|
||||
return Success(data=out or {}, msg="更新成功" if updated else "未找到")
|
||||
|
||||
|
||||
@invoice_router.delete("/delete", summary="删除发票", response_model=BasicResponse[MessageOut])
|
||||
@invoice_router.delete("/delete", summary="删除发票", response_model=BasicResponse[MessageOut], dependencies=[DependAuth, DependPermission])
|
||||
async def delete_invoice(id: int = Query(...)):
|
||||
"""
|
||||
删除发票记录
|
||||
@ -95,7 +101,7 @@ async def delete_invoice(id: int = Query(...)):
|
||||
return Success(data={"deleted": ok}, msg="删除成功" if ok else "未找到")
|
||||
|
||||
|
||||
@invoice_router.post("/update-status", summary="更新发票状态", response_model=BasicResponse[InvoiceOut])
|
||||
@invoice_router.post("/update-status", summary="更新发票状态", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
|
||||
async def update_invoice_status(data: UpdateStatus):
|
||||
"""
|
||||
更新发票状态(pending|invoiced|rejected|refunded)
|
||||
@ -106,25 +112,26 @@ async def update_invoice_status(data: UpdateStatus):
|
||||
|
||||
|
||||
|
||||
@invoice_router.post("/{id}/receipt", summary="上传付款凭证", response_model=BasicResponse[PaymentReceiptOut])
|
||||
@invoice_router.post("/{id}/receipt", summary="上传付款凭证", response_model=BasicResponse[dict], dependencies=[DependAuth, DependPermission])
|
||||
async def upload_payment_receipt(id: int, data: PaymentReceiptCreate):
|
||||
"""
|
||||
上传对公转账付款凭证
|
||||
"""
|
||||
receipt = await invoice_controller.create_receipt(id, data)
|
||||
return Success(data=receipt, msg="上传成功")
|
||||
detail = await invoice_controller.get_receipt_by_id(receipt.id)
|
||||
return Success(data=detail, msg="上传成功")
|
||||
|
||||
|
||||
@invoice_router.get("/headers", summary="发票抬头列表", response_model=BasicResponse[list[InvoiceHeaderOut]])
|
||||
@invoice_router.get("/headers", summary="发票抬头列表", response_model=BasicResponse[list[InvoiceHeaderOut]], dependencies=[DependAuth, DependPermission])
|
||||
async def get_invoice_headers(app_user_id: Optional[int] = Query(None)):
|
||||
"""
|
||||
获取发票抬头列表,可按 AppUser 过滤
|
||||
管理端抬头列表(管理员token):允许按 app_user_id 过滤;为空则返回全部。
|
||||
"""
|
||||
headers = await invoice_controller.get_headers(user_id=app_user_id)
|
||||
return Success(data=headers, msg="获取成功")
|
||||
|
||||
|
||||
@invoice_router.get("/headers/{id}", summary="发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut])
|
||||
@invoice_router.get("/headers/{id}", summary="发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
|
||||
async def get_invoice_header_by_id(id: int):
|
||||
"""
|
||||
获取发票抬头详情
|
||||
@ -133,7 +140,7 @@ async def get_invoice_header_by_id(id: int):
|
||||
return Success(data=header or {}, msg="获取成功" if header else "未找到")
|
||||
|
||||
|
||||
@invoice_router.post("/headers", summary="新增发票抬头", response_model=BasicResponse[InvoiceHeaderOut])
|
||||
@invoice_router.post("/headers", summary="新增发票抬头", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
|
||||
async def create_invoice_header(data: InvoiceHeaderCreate, app_user_id: Optional[int] = Query(None)):
|
||||
"""
|
||||
新增发票抬头
|
||||
@ -142,7 +149,7 @@ async def create_invoice_header(data: InvoiceHeaderCreate, app_user_id: Optional
|
||||
return Success(data=header, msg="创建成功")
|
||||
|
||||
|
||||
@invoice_router.put("/{id}/type", summary="更新发票类型", response_model=BasicResponse[InvoiceOut])
|
||||
@invoice_router.put("/{id}/type", summary="更新发票类型", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
|
||||
async def update_invoice_type(id: int, data: UpdateType):
|
||||
"""
|
||||
更新发票的电子/纸质与专票/普票类型
|
||||
@ -151,4 +158,46 @@ async def update_invoice_type(id: int, data: UpdateType):
|
||||
return Success(data=out or {}, msg="更新成功" if out else "未找到")
|
||||
|
||||
|
||||
# 对公转账记录接口在 transactions 路由中统一暴露
|
||||
@invoice_router.delete("/headers/{id}", summary="删除发票抬头", response_model=BasicResponse[MessageOut], dependencies=[DependAuth, DependPermission])
|
||||
async def delete_invoice_header(id: int):
|
||||
ok = await invoice_controller.delete_header(id)
|
||||
return Success(msg="删除成功" if ok else "未找到")
|
||||
|
||||
|
||||
@invoice_router.put("/headers/{id}", summary="更新发票抬头", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
|
||||
async def update_invoice_header(id: int, data: InvoiceHeaderUpdate):
|
||||
header = await invoice_controller.update_header(id, data)
|
||||
return Success(data=header or {}, msg="更新成功" if header else "未找到")
|
||||
|
||||
|
||||
# 用户端:我的发票列表(使用App用户token)
|
||||
@invoice_router.get("/app-list", summary="我的发票列表", response_model=PageResponse[InvoiceOut])
|
||||
async def list_my_invoices(
|
||||
status: Optional[str] = Query(None),
|
||||
ticket_type: Optional[str] = Query(None),
|
||||
invoice_type: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100),
|
||||
current_user: AppUser = Depends(get_current_app_user),
|
||||
):
|
||||
result = await invoice_controller.list(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
status=status,
|
||||
ticket_type=ticket_type,
|
||||
invoice_type=invoice_type,
|
||||
app_user_id=current_user.id,
|
||||
)
|
||||
return SuccessExtra(
|
||||
data=result.items,
|
||||
total=result.total,
|
||||
page=result.page,
|
||||
page_size=result.page_size,
|
||||
msg="获取成功",
|
||||
)
|
||||
|
||||
# 用户端:我的发票抬头(使用App用户token)
|
||||
@invoice_router.get("/app-headers", summary="我的发票抬头", response_model=BasicResponse[list[InvoiceHeaderOut]])
|
||||
async def get_my_invoice_headers(current_user: AppUser = Depends(get_current_app_user)):
|
||||
headers = await invoice_controller.get_headers(user_id=current_user.id)
|
||||
return Success(data=headers, msg="获取成功")
|
||||
|
||||
@ -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(...)
|
||||
|
||||
@ -1,22 +1,11 @@
|
||||
from fastapi import APIRouter, UploadFile, File
|
||||
from app.controllers.upload import UploadController
|
||||
from app.schemas.upload import ImageUploadResponse, FileUploadResponse
|
||||
from app.schemas.base import BasicResponse, Success
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/image", response_model=ImageUploadResponse, summary="上传图片")
|
||||
async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
|
||||
"""
|
||||
上传图片接口
|
||||
:param file: 图片文件
|
||||
:return: 图片URL和文件名
|
||||
"""
|
||||
return await UploadController.upload_image(file)
|
||||
|
||||
@router.post("/file", response_model=FileUploadResponse, summary="上传文件")
|
||||
async def upload_file(file: UploadFile = File(...)) -> FileUploadResponse:
|
||||
return await UploadController.upload_file(file)
|
||||
|
||||
@router.post("/upload", response_model=FileUploadResponse, summary="统一上传接口")
|
||||
async def upload(file: UploadFile = File(...)) -> FileUploadResponse:
|
||||
return await UploadController.upload_any(file)
|
||||
@router.post("/file", response_model=BasicResponse[dict], summary="统一上传接口")
|
||||
async def upload(file: UploadFile = File(...)) -> BasicResponse[dict]:
|
||||
res = await UploadController.upload_any(file)
|
||||
return Success(data={"url": res.url, "filename": res.filename, "content_type": res.content_type})
|
||||
|
||||
@ -87,6 +87,11 @@ async def get_valuations(
|
||||
heritage_level: Optional[str] = Query(None, description="非遗等级"),
|
||||
status: Optional[str] = Query(None, description="评估状态"),
|
||||
is_active: Optional[bool] = Query(None, description="是否激活"),
|
||||
phone: Optional[str] = Query(None, description="手机号模糊查询"),
|
||||
submitted_start: Optional[str] = Query(None, description="提交时间开始(毫秒或ISO)"),
|
||||
submitted_end: Optional[str] = Query(None, description="提交时间结束(毫秒或ISO)"),
|
||||
audited_start: Optional[str] = Query(None, description="审核时间开始(证书修改时间,毫秒或ISO)"),
|
||||
audited_end: Optional[str] = Query(None, description="审核时间结束(证书修改时间,毫秒或ISO)"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
size: int = Query(10, ge=1, le=100, description="每页数量")
|
||||
):
|
||||
@ -98,6 +103,11 @@ async def get_valuations(
|
||||
heritage_level=heritage_level,
|
||||
status=status,
|
||||
is_active=is_active,
|
||||
phone=phone,
|
||||
submitted_start=submitted_start,
|
||||
submitted_end=submitted_end,
|
||||
audited_start=audited_start,
|
||||
audited_end=audited_end,
|
||||
page=page,
|
||||
size=size
|
||||
)
|
||||
@ -200,4 +210,4 @@ async def update_admin_notes(valuation_id: int, data: ValuationAdminNotesUpdate)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="估值评估记录不存在")
|
||||
import json
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="管理员备注更新成功")
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="管理员备注更新成功")
|
||||
|
||||
@ -86,6 +86,9 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
|
||||
|
||||
# 更新字段
|
||||
update_dict = update_data.model_dump(exclude_unset=True)
|
||||
if "nickname" in update_dict:
|
||||
update_dict["alias"] = update_dict.pop("nickname")
|
||||
update_dict.pop("avatar", None)
|
||||
for field, value in update_dict.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
@ -145,4 +148,4 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
|
||||
|
||||
|
||||
# 创建控制器实例
|
||||
app_user_controller = AppUserController()
|
||||
app_user_controller = AppUserController()
|
||||
|
||||
@ -9,6 +9,7 @@ from app.schemas.invoice import (
|
||||
InvoiceOut,
|
||||
InvoiceList,
|
||||
InvoiceHeaderCreate,
|
||||
InvoiceHeaderUpdate,
|
||||
InvoiceHeaderOut,
|
||||
UpdateStatus,
|
||||
UpdateType,
|
||||
@ -60,6 +61,22 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
header = await InvoiceHeader.filter(id=id_).first()
|
||||
return InvoiceHeaderOut.model_validate(header) if header else None
|
||||
|
||||
async def delete_header(self, id_: int) -> bool:
|
||||
header = await InvoiceHeader.filter(id=id_).first()
|
||||
if not header:
|
||||
return False
|
||||
await header.delete()
|
||||
return True
|
||||
|
||||
async def update_header(self, id_: int, data: InvoiceHeaderUpdate) -> Optional[InvoiceHeaderOut]:
|
||||
header = await InvoiceHeader.filter(id=id_).first()
|
||||
if not header:
|
||||
return None
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if update_data:
|
||||
await header.update_from_dict(update_data).save()
|
||||
return InvoiceHeaderOut.model_validate(header)
|
||||
|
||||
async def list(self, page: int = 1, page_size: int = 10, **filters) -> InvoiceList:
|
||||
"""
|
||||
获取发票列表(支持筛选与分页)
|
||||
@ -83,6 +100,8 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
qs = qs.filter(ticket_type=filters["ticket_type"])
|
||||
if filters.get("invoice_type"):
|
||||
qs = qs.filter(invoice_type=filters["invoice_type"])
|
||||
if filters.get("app_user_id"):
|
||||
qs = qs.filter(app_user_id=filters["app_user_id"])
|
||||
|
||||
total = await qs.count()
|
||||
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
|
||||
@ -231,13 +250,18 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
for r in rows:
|
||||
inv = await r.invoice
|
||||
items.append({
|
||||
"id": r.id,
|
||||
"invoice_id": getattr(inv, "id", None),
|
||||
"submitted_at": r.created_at.isoformat() if r.created_at else "",
|
||||
"receipt": {
|
||||
"id": r.id,
|
||||
"url": r.url,
|
||||
"note": r.note,
|
||||
"verified": r.verified,
|
||||
},
|
||||
"receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
|
||||
"receipts": [
|
||||
{
|
||||
"id": r.id,
|
||||
"url": r.url,
|
||||
"note": r.note,
|
||||
"verified": r.verified,
|
||||
}
|
||||
],
|
||||
"phone": inv.phone,
|
||||
"wechat": inv.wechat,
|
||||
"company_name": inv.company_name,
|
||||
@ -267,13 +291,18 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
return None
|
||||
inv = await r.invoice
|
||||
return {
|
||||
"id": r.id,
|
||||
"invoice_id": getattr(inv, "id", None),
|
||||
"submitted_at": r.created_at.isoformat() if r.created_at else "",
|
||||
"receipt": {
|
||||
"id": r.id,
|
||||
"url": r.url,
|
||||
"note": r.note,
|
||||
"verified": r.verified,
|
||||
},
|
||||
"receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
|
||||
"receipts": [
|
||||
{
|
||||
"id": r.id,
|
||||
"url": r.url,
|
||||
"note": r.note,
|
||||
"verified": r.verified,
|
||||
}
|
||||
],
|
||||
"phone": inv.phone,
|
||||
"wechat": inv.wechat,
|
||||
"company_name": inv.company_name,
|
||||
@ -319,4 +348,4 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
)
|
||||
|
||||
|
||||
invoice_controller = InvoiceController()
|
||||
invoice_controller = InvoiceController()
|
||||
|
||||
@ -15,8 +15,9 @@ class UploadController:
|
||||
:param file: 上传的图片文件
|
||||
:return: 图片URL和文件名
|
||||
"""
|
||||
# 检查文件类型
|
||||
if not file.content_type.startswith('image/'):
|
||||
ext = os.path.splitext(file.filename or "")[1].lower()
|
||||
image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
|
||||
if not (file.content_type.startswith('image/') or ext in image_exts):
|
||||
raise ValueError("只支持上传图片文件")
|
||||
|
||||
# 获取项目根目录
|
||||
@ -61,8 +62,32 @@ class UploadController:
|
||||
"application/vnd.ms-excel",
|
||||
"application/zip",
|
||||
"application/x-zip-compressed",
|
||||
"application/octet-stream",
|
||||
"text/plain",
|
||||
"text/csv",
|
||||
"application/json",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-7z-compressed",
|
||||
}
|
||||
if file.content_type not in allowed:
|
||||
allowed_exts = {
|
||||
".pdf",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".zip",
|
||||
".rar",
|
||||
".7z",
|
||||
".txt",
|
||||
".csv",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".json",
|
||||
}
|
||||
ext = os.path.splitext(file.filename or "")[1].lower()
|
||||
if (file.content_type not in allowed) and (ext not in allowed_exts):
|
||||
raise ValueError("不支持的文件类型")
|
||||
|
||||
base_dir = Path(__file__).resolve().parent.parent
|
||||
@ -95,8 +120,10 @@ class UploadController:
|
||||
统一上传入口,自动识别图片与非图片类型。
|
||||
返回统一结构:url, filename, content_type
|
||||
"""
|
||||
if file.content_type and file.content_type.startswith("image/"):
|
||||
image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
|
||||
ext = os.path.splitext(file.filename or "")[1].lower()
|
||||
if (file.content_type and file.content_type.startswith("image/")) or (ext in image_exts):
|
||||
img = await UploadController.upload_image(file)
|
||||
return FileUploadResponse(url=img.url, filename=img.filename, content_type=file.content_type or "image")
|
||||
# 非图片类型复用原文件上传校验
|
||||
return await UploadController.upload_file(file)
|
||||
return await UploadController.upload_file(file)
|
||||
|
||||
@ -114,7 +114,73 @@ class UserValuationController:
|
||||
|
||||
async def _to_user_out(self, valuation: ValuationAssessment) -> UserValuationOut:
|
||||
"""转换为用户端输出模型"""
|
||||
return UserValuationOut.model_validate(valuation)
|
||||
return UserValuationOut(
|
||||
id=valuation.id,
|
||||
asset_name=valuation.asset_name,
|
||||
institution=valuation.institution,
|
||||
industry=valuation.industry,
|
||||
annual_revenue=valuation.annual_revenue,
|
||||
rd_investment=valuation.rd_investment,
|
||||
three_year_income=valuation.three_year_income,
|
||||
funding_status=valuation.funding_status,
|
||||
inheritor_level=valuation.inheritor_level,
|
||||
inheritor_ages=valuation.inheritor_ages,
|
||||
inheritor_age_count=valuation.inheritor_age_count,
|
||||
inheritor_certificates=valuation.inheritor_certificates,
|
||||
heritage_level=valuation.heritage_level,
|
||||
heritage_asset_level=valuation.heritage_asset_level,
|
||||
patent_application_no=valuation.patent_application_no,
|
||||
patent_remaining_years=valuation.patent_remaining_years,
|
||||
historical_evidence=valuation.historical_evidence,
|
||||
patent_certificates=valuation.patent_certificates,
|
||||
pattern_images=valuation.pattern_images,
|
||||
report_url=valuation.report_url,
|
||||
certificate_url=valuation.certificate_url,
|
||||
application_maturity=valuation.application_maturity,
|
||||
implementation_stage=valuation.implementation_stage,
|
||||
application_coverage=valuation.application_coverage,
|
||||
coverage_area=valuation.coverage_area,
|
||||
cooperation_depth=valuation.cooperation_depth,
|
||||
collaboration_type=valuation.collaboration_type,
|
||||
offline_activities=valuation.offline_activities,
|
||||
offline_teaching_count=valuation.offline_teaching_count,
|
||||
online_accounts=valuation.online_accounts,
|
||||
platform_accounts=valuation.platform_accounts,
|
||||
sales_volume=valuation.sales_volume,
|
||||
link_views=valuation.link_views,
|
||||
circulation=valuation.circulation,
|
||||
scarcity_level=valuation.scarcity_level,
|
||||
last_market_activity=valuation.last_market_activity,
|
||||
market_activity_time=valuation.market_activity_time,
|
||||
monthly_transaction=valuation.monthly_transaction,
|
||||
monthly_transaction_amount=valuation.monthly_transaction_amount,
|
||||
price_fluctuation=valuation.price_fluctuation,
|
||||
price_range=valuation.price_range,
|
||||
market_price=valuation.market_price,
|
||||
credit_code_or_id=valuation.credit_code_or_id,
|
||||
biz_intro=valuation.biz_intro,
|
||||
infringement_record=valuation.infringement_record,
|
||||
patent_count=valuation.patent_count,
|
||||
esg_value=valuation.esg_value,
|
||||
policy_matching=valuation.policy_matching,
|
||||
online_course_views=valuation.online_course_views,
|
||||
pattern_complexity=valuation.pattern_complexity,
|
||||
normalized_entropy=valuation.normalized_entropy,
|
||||
legal_risk=valuation.legal_risk,
|
||||
base_pledge_rate=valuation.base_pledge_rate,
|
||||
flow_correction=valuation.flow_correction,
|
||||
model_value_b=valuation.model_value_b,
|
||||
market_value_c=valuation.market_value_c,
|
||||
final_value_ab=valuation.final_value_ab,
|
||||
dynamic_pledge_rate=valuation.dynamic_pledge_rate,
|
||||
calculation_result=valuation.calculation_result,
|
||||
calculation_input=valuation.calculation_input,
|
||||
status=valuation.status,
|
||||
admin_notes=valuation.admin_notes,
|
||||
created_at=valuation.created_at,
|
||||
updated_at=valuation.updated_at,
|
||||
is_active=valuation.is_active,
|
||||
)
|
||||
|
||||
async def _to_user_detail(self, valuation: ValuationAssessment) -> UserValuationDetail:
|
||||
"""转换为用户端详细模型"""
|
||||
@ -188,4 +254,4 @@ class UserValuationController:
|
||||
|
||||
|
||||
# 创建控制器实例
|
||||
user_valuation_controller = UserValuationController()
|
||||
user_valuation_controller = UserValuationController()
|
||||
|
||||
@ -13,6 +13,7 @@ from app.schemas.valuation import (
|
||||
ValuationCalculationStepCreate,
|
||||
ValuationCalculationStepOut
|
||||
)
|
||||
from app.models.user import AppUser
|
||||
|
||||
|
||||
class ValuationController:
|
||||
@ -76,13 +77,15 @@ class ValuationController:
|
||||
create_data = data.model_dump()
|
||||
create_data['user_id'] = user_id
|
||||
valuation = await self.model.create(**create_data)
|
||||
return ValuationAssessmentOut.model_validate(valuation)
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
|
||||
async def get_by_id(self, valuation_id: int) -> Optional[ValuationAssessmentOut]:
|
||||
"""根据ID获取估值评估"""
|
||||
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
|
||||
if valuation:
|
||||
return ValuationAssessmentOut.model_validate(valuation)
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
return None
|
||||
|
||||
async def update(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]:
|
||||
@ -93,10 +96,14 @@ class ValuationController:
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if update_data:
|
||||
if 'certificate_url' in update_data and update_data.get('certificate_url'):
|
||||
from datetime import datetime
|
||||
update_data['audited_at'] = datetime.now()
|
||||
await valuation.update_from_dict(update_data)
|
||||
await valuation.save()
|
||||
|
||||
return ValuationAssessmentOut.model_validate(valuation)
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
|
||||
async def delete(self, valuation_id: int) -> bool:
|
||||
"""软删除估值评估"""
|
||||
@ -121,6 +128,7 @@ class ValuationController:
|
||||
|
||||
# 转换为输出模型
|
||||
items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
|
||||
items = await self._attach_user_phone_bulk(items)
|
||||
# 计算总页数
|
||||
pages = (total + query.size - 1) // query.size
|
||||
|
||||
@ -154,7 +162,38 @@ class ValuationController:
|
||||
# 添加状态筛选
|
||||
if hasattr(query, 'status') and query.status:
|
||||
queryset = queryset.filter(status=query.status)
|
||||
|
||||
|
||||
if getattr(query, 'phone', None):
|
||||
queryset = queryset.filter(user__phone__icontains=query.phone)
|
||||
|
||||
def _parse_time(v: Optional[str]):
|
||||
if not v:
|
||||
return None
|
||||
try:
|
||||
iv = int(v)
|
||||
from datetime import datetime
|
||||
return datetime.fromtimestamp(iv / 1000)
|
||||
except Exception:
|
||||
try:
|
||||
from datetime import datetime
|
||||
return datetime.fromisoformat(v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
s_dt = _parse_time(getattr(query, 'submitted_start', None))
|
||||
e_dt = _parse_time(getattr(query, 'submitted_end', None))
|
||||
if s_dt:
|
||||
queryset = queryset.filter(created_at__gte=s_dt)
|
||||
if e_dt:
|
||||
queryset = queryset.filter(created_at__lte=e_dt)
|
||||
|
||||
a_s_dt = _parse_time(getattr(query, 'audited_start', None))
|
||||
a_e_dt = _parse_time(getattr(query, 'audited_end', None))
|
||||
if a_s_dt:
|
||||
queryset = queryset.filter(audited_at__isnull=False, audited_at__gte=a_s_dt)
|
||||
if a_e_dt:
|
||||
queryset = queryset.filter(audited_at__isnull=False, audited_at__lte=a_e_dt)
|
||||
|
||||
return queryset
|
||||
|
||||
async def get_statistics(self) -> dict:
|
||||
@ -195,6 +234,7 @@ class ValuationController:
|
||||
|
||||
# 转换为输出模型
|
||||
items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
|
||||
items = await self._attach_user_phone_bulk(items)
|
||||
|
||||
# 计算总页数
|
||||
pages = (total + size - 1) // size
|
||||
@ -213,12 +253,14 @@ class ValuationController:
|
||||
if not valuation:
|
||||
return None
|
||||
|
||||
update_data = {"status": "approved"}
|
||||
from datetime import datetime
|
||||
update_data = {"status": "approved", "audited_at": datetime.now()}
|
||||
if admin_notes:
|
||||
update_data["admin_notes"] = admin_notes
|
||||
|
||||
await valuation.update_from_dict(update_data).save()
|
||||
return ValuationAssessmentOut.model_validate(valuation)
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
|
||||
async def reject_valuation(self, valuation_id: int, admin_notes: Optional[str] = None) -> Optional[ValuationAssessmentOut]:
|
||||
"""审核拒绝估值评估"""
|
||||
@ -226,23 +268,41 @@ class ValuationController:
|
||||
if not valuation:
|
||||
return None
|
||||
|
||||
update_data = {"status": "rejected"}
|
||||
from datetime import datetime
|
||||
update_data = {"status": "rejected", "audited_at": datetime.now()}
|
||||
if admin_notes:
|
||||
update_data["admin_notes"] = admin_notes
|
||||
|
||||
await valuation.update_from_dict(update_data).save()
|
||||
return ValuationAssessmentOut.model_validate(valuation)
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
|
||||
async def update_admin_notes(self, valuation_id: int, admin_notes: str) -> Optional[ValuationAssessmentOut]:
|
||||
"""更新管理员备注"""
|
||||
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
|
||||
if not valuation:
|
||||
return None
|
||||
|
||||
|
||||
await valuation.update_from_dict({"admin_notes": admin_notes}).save()
|
||||
return ValuationAssessmentOut.model_validate(valuation)
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
|
||||
async def _attach_user_phone(self, out: ValuationAssessmentOut) -> ValuationAssessmentOut:
|
||||
user = await AppUser.filter(id=out.user_id).first()
|
||||
out.user_phone = getattr(user, "phone", None) if user else None
|
||||
return out
|
||||
|
||||
async def _attach_user_phone_bulk(self, items: List[ValuationAssessmentOut]) -> List[ValuationAssessmentOut]:
|
||||
ids = list({item.user_id for item in items if item.user_id})
|
||||
if not ids:
|
||||
return items
|
||||
users = await AppUser.filter(id__in=ids).values("id", "phone")
|
||||
phone_map = {u["id"]: u["phone"] for u in users}
|
||||
for item in items:
|
||||
item.user_phone = phone_map.get(item.user_id)
|
||||
return items
|
||||
|
||||
|
||||
# 创建控制器实例
|
||||
valuation_controller = ValuationController()
|
||||
from app.log import logger
|
||||
from app.log import logger
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -86,6 +86,7 @@ class ValuationAssessment(Model):
|
||||
admin_notes = fields.TextField(null=True, description="管理员备注")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
|
||||
audited_at = fields.DatetimeField(null=True, description="审核时间")
|
||||
is_active = fields.BooleanField(default=True, description="是否激活")
|
||||
|
||||
class Meta:
|
||||
@ -115,4 +116,4 @@ class ValuationCalculationStep(Model):
|
||||
ordering = ["step_order"]
|
||||
|
||||
def __str__(self):
|
||||
return f"估值ID {self.valuation_id} - 步骤 {self.step_order}: {self.step_name}"
|
||||
return f"估值ID {self.valuation_id} - 步骤 {self.step_order}: {self.step_name}"
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -23,6 +23,16 @@ class InvoiceHeaderOut(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class InvoiceHeaderUpdate(BaseModel):
|
||||
company_name: Optional[str] = Field(None, min_length=1, max_length=128)
|
||||
tax_number: Optional[str] = Field(None, min_length=1, max_length=32)
|
||||
register_address: Optional[str] = Field(None, min_length=1, max_length=256)
|
||||
register_phone: Optional[str] = Field(None, min_length=1, max_length=32)
|
||||
bank_name: Optional[str] = Field(None, min_length=1, max_length=128)
|
||||
bank_account: Optional[str] = Field(None, min_length=1, max_length=64)
|
||||
email: Optional[EmailStr] = None
|
||||
|
||||
|
||||
class InvoiceCreate(BaseModel):
|
||||
ticket_type: str = Field(..., pattern=r"^(electronic|paper)$")
|
||||
invoice_type: str = Field(..., pattern=r"^(special|normal)$")
|
||||
@ -99,4 +109,4 @@ class PaymentReceiptOut(BaseModel):
|
||||
url: str
|
||||
note: Optional[str]
|
||||
verified: bool
|
||||
created_at: str
|
||||
created_at: str
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
@ -1,2 +0,0 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
@ -1,2 +0,0 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
@ -1,2 +0,0 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
@ -1 +0,0 @@
|
||||
%PDF-1.4
|
||||
@ -1 +0,0 @@
|
||||
%PDF-1.4
|
||||
@ -1 +0,0 @@
|
||||
%PDF-1.4
|
||||
@ -1 +0,0 @@
|
||||
%PDF-1.4
|
||||
2
app/static/files/valuation_assessments.txt
Normal file
2
app/static/files/valuation_assessments.txt
Normal 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"
|
||||
104
scripts/response_format_check.py
Normal file
104
scripts/response_format_check.py
Normal 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()
|
||||
10
估值字段.txt
10
估值字段.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
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user