diff --git a/Dockerfile b/Dockerfile index b5af105..ac6a314 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,14 @@ -FROM node:18.12.0-alpine3.16 AS web +FROM node:18-alpine AS web WORKDIR /opt/vue-fastapi-admin COPY /web ./web -RUN npm install -g pnpm && cd /opt/vue-fastapi-admin/web && pnpm install --registry=https://registry.npmmirror.com && pnpm run build + +# 安装pnpm并设置配置 +RUN npm install -g pnpm && \ + cd /opt/vue-fastapi-admin/web && \ + pnpm config set registry https://registry.npmmirror.com && \ + pnpm install && \ + pnpm run build FROM python:3.11-slim-bullseye diff --git a/app/api/v1/app_invoices/app_invoices.py b/app/api/v1/app_invoices/app_invoices.py index e644370..ece7824 100644 --- a/app/api/v1/app_invoices/app_invoices.py +++ b/app/api/v1/app_invoices/app_invoices.py @@ -117,13 +117,23 @@ async def create_with_receipt(payload: AppCreateInvoiceWithReceipt, current_user ) inv = await invoice_controller.create(inv_data) if payload.receipt_urls: - receipts = [] - for url in payload.receipt_urls: - receipt = await invoice_controller.create_receipt(inv.id, PaymentReceiptCreate(url=url, note=payload.note)) - detail = await invoice_controller.get_receipt_by_id(receipt.id) - if detail: - receipts.append(detail) - return Success(data={"invoice_id": inv.id, "receipts": receipts}, msg="创建并上传成功") + urls = payload.receipt_urls + main_url = urls[0] if isinstance(urls, list) and urls else None + receipt = await invoice_controller.create_receipt( + inv.id, + PaymentReceiptCreate(url=main_url, note=payload.note, extra=urls) + ) + detail = await invoice_controller.get_receipt_by_id(receipt.id) + return Success(data={"invoice_id": inv.id, "receipts": [detail] if detail else []}, msg="创建并上传成功") + if isinstance(payload.receipt_url, list) and payload.receipt_url: + urls = payload.receipt_url + main_url = urls[0] + receipt = await invoice_controller.create_receipt( + inv.id, + PaymentReceiptCreate(url=main_url, note=payload.note, extra=urls) + ) + detail = await invoice_controller.get_receipt_by_id(receipt.id) + return Success(data={"invoice_id": inv.id, "receipts": [detail] if detail else []}, msg="创建并上传成功") if payload.receipt_url: receipt = await invoice_controller.create_receipt(inv.id, PaymentReceiptCreate(url=payload.receipt_url, note=payload.note)) detail = await invoice_controller.get_receipt_by_id(receipt.id) diff --git a/app/api/v1/app_valuations/app_valuations.py b/app/api/v1/app_valuations/app_valuations.py index 7701356..71a9414 100644 --- a/app/api/v1/app_valuations/app_valuations.py +++ b/app/api/v1/app_valuations/app_valuations.py @@ -166,7 +166,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate # 步骤1:立即更新计算输入参数(不管后续是否成功) try: - await valuation_controller.update( + await valuation_controller.update_calc( valuation_id, ValuationAssessmentUpdate( calculation_input=input_data, @@ -212,7 +212,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate # api_calc_fields["flow_correction"] = None if api_calc_fields: - await valuation_controller.update( + await valuation_controller.update_calc( valuation_id, ValuationAssessmentUpdate(**api_calc_fields) ) @@ -225,7 +225,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate # 步骤2:更新计算结果字段(模型估值B、市场估值C、最终估值AB、完整计算结果) try: - await valuation_controller.update( + await valuation_controller.update_calc( valuation_id, ValuationAssessmentUpdate( model_value_b=calculation_result.get('model_value_b'), @@ -280,7 +280,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate base_pledge_rate_value = "0.5" # 固定值:基础质押率 = 0.5 flow_correction_value = "0.3" # 固定值:流量修正系数 = 0.3 - await valuation_controller.update( + await valuation_controller.update_calc( valuation_id, ValuationAssessmentUpdate( dynamic_pledge_rate=drp_result, @@ -307,19 +307,13 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate except Exception: pass - # 步骤4:最后更新状态为成功 + # 步骤4:计算完成,保持状态为 pending,等待后台审核 try: - result = await valuation_controller.update( - valuation_id, - ValuationAssessmentUpdate( - status='success' - ) - ) - logger.info("valuation.status_updated valuation_id={} status=success", valuation_id) - except Exception as e: - logger.warning("valuation.failed_to_update_status valuation_id={} err={}", valuation_id, repr(e)) - # 即使状态更新失败,也尝试获取结果用于日志 result = await valuation_controller.get_by_id(valuation_id) + logger.info("valuation.calc_finished valuation_id={} status=pending", valuation_id) + except Exception as e: + logger.warning("valuation.failed_to_fetch_after_calc valuation_id={} err={}", valuation_id, repr(e)) + result = None logger.info("valuation.background_calc_success user_id={} valuation_id={}", user_id, valuation_id) @@ -362,7 +356,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate fail_update_fields.update(api_calc_fields) try: - await valuation_controller.update( + await valuation_controller.update_calc( valuation_id, ValuationAssessmentUpdate(**fail_update_fields) ) @@ -372,7 +366,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate # 如果保存失败,至少更新状态 try: fail_update = ValuationAssessmentUpdate(status='rejected') - await valuation_controller.update(valuation_id, fail_update) + await valuation_controller.update_calc(valuation_id, fail_update) except Exception: pass else: diff --git a/app/api/v1/transactions/transactions.py b/app/api/v1/transactions/transactions.py index 10e84ce..1bd6170 100644 --- a/app/api/v1/transactions/transactions.py +++ b/app/api/v1/transactions/transactions.py @@ -85,6 +85,11 @@ async def send_email(payload: SendEmailRequest = Body(...)): raise HTTPException(status_code=400, detail="收件方地址域名不可用或未正确解析") if payload.file_urls: urls.extend([u.strip().strip('`') for u in payload.file_urls if isinstance(u, str)]) + if payload.file_url: + if isinstance(payload.file_url, str): + urls.append(payload.file_url.strip().strip('`')) + elif isinstance(payload.file_url, list): + urls.extend([u.strip().strip('`') for u in payload.file_url if isinstance(u, str)]) if urls: try: async with httpx.AsyncClient(timeout=10) as client: @@ -122,20 +127,25 @@ async def send_email(payload: SendEmailRequest = Body(...)): else: logger.error("transactions.email_send_fail email={} err={}", payload.email, error) - if status == "OK" and payload.receipt_id: + if payload.receipt_id: try: r = await PaymentReceipt.filter(id=payload.receipt_id).first() if r: - r.extra = (r.extra or {}) | payload.model_dump() - await r.save() try: inv = await r.invoice if inv: - inv.status = "invoiced" + s = str(payload.status or "").lower() + if s in {"invoiced", "success"}: + target = "invoiced" + elif s in {"refunded", "rejected", "pending"}: + target = s + else: + target = "invoiced" + inv.status = target await inv.save() - logger.info("transactions.invoice_mark_invoiced receipt_id={} invoice_id={}", payload.receipt_id, inv.id) + logger.info("transactions.invoice_status_updated receipt_id={} invoice_id={} status={}", payload.receipt_id, inv.id, target) except Exception as e2: - logger.warning("transactions.invoice_mark_invoiced_fail receipt_id={} err={}", payload.receipt_id, str(e2)) + logger.warning("transactions.invoice_status_update_fail receipt_id={} err={}", payload.receipt_id, str(e2)) except Exception as e: logger.error("transactions.email_extra_save_fail id={} err={}", payload.receipt_id, str(e)) diff --git a/app/controllers/invoice.py b/app/controllers/invoice.py index f7b1f7b..a525eff 100644 --- a/app/controllers/invoice.py +++ b/app/controllers/invoice.py @@ -271,20 +271,23 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]): items = [] for r in rows: inv = await r.invoice + urls = [] + if isinstance(r.extra, list): + urls = [str(u) for u in r.extra if u] + elif isinstance(r.extra, dict): + v = r.extra.get("urls") + if isinstance(v, list): + urls = [str(u) for u in v if u] + if not urls: + urls = [r.url] if r.url else [] + receipts = [{"id": r.id, "url": u, "note": r.note, "verified": r.verified} for u in urls] items.append({ "id": r.id, "invoice_id": getattr(inv, "id", None), "submitted_at": r.created_at.isoformat() if r.created_at else "", "receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "", "extra": r.extra, - "receipts": [ - { - "id": r.id, - "url": r.url, - "note": r.note, - "verified": r.verified, - } - ], + "receipts": receipts, "phone": inv.phone, "wechat": inv.wechat, "company_name": inv.company_name, @@ -313,20 +316,23 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]): if not r: return None inv = await r.invoice + urls = [] + if isinstance(r.extra, list): + urls = [str(u) for u in r.extra if u] + elif isinstance(r.extra, dict): + v = r.extra.get("urls") + if isinstance(v, list): + urls = [str(u) for u in v if u] + if not urls: + urls = [r.url] if r.url else [] + receipts = [{"id": r.id, "url": u, "note": r.note, "verified": r.verified} for u in urls] return { "id": r.id, "invoice_id": getattr(inv, "id", None), "submitted_at": r.created_at.isoformat() if r.created_at else "", "receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "", "extra": r.extra, - "receipts": [ - { - "id": r.id, - "url": r.url, - "note": r.note, - "verified": r.verified, - } - ], + "receipts": receipts, "phone": inv.phone, "wechat": inv.wechat, "company_name": inv.company_name, diff --git a/app/controllers/user_valuation.py b/app/controllers/user_valuation.py index 343b88c..dd77aad 100644 --- a/app/controllers/user_valuation.py +++ b/app/controllers/user_valuation.py @@ -127,7 +127,7 @@ class UserValuationController: inheritor_ages=valuation.inheritor_ages, inheritor_age_count=valuation.inheritor_age_count, inheritor_certificates=valuation.inheritor_certificates, - heritage_level=valuation.heritage_level, + heritage_level=getattr(valuation, "heritage_level", None), heritage_asset_level=valuation.heritage_asset_level, patent_application_no=valuation.patent_application_no, patent_remaining_years=valuation.patent_remaining_years, @@ -197,7 +197,7 @@ class UserValuationController: inheritor_ages=valuation.inheritor_ages, inheritor_age_count=valuation.inheritor_age_count, inheritor_certificates=valuation.inheritor_certificates, - heritage_level=valuation.heritage_level, + heritage_level=getattr(valuation, "heritage_level", None), heritage_asset_level=valuation.heritage_asset_level, patent_application_no=valuation.patent_application_no, patent_remaining_years=valuation.patent_remaining_years, diff --git a/app/controllers/valuation.py b/app/controllers/valuation.py index b8d396e..023b9e3 100644 --- a/app/controllers/valuation.py +++ b/app/controllers/valuation.py @@ -813,7 +813,7 @@ class ValuationController: return None from datetime import datetime - update_data = {"status": "pending", "audited_at": datetime.now(), "updated_at": datetime.now()} + update_data = {"status": "success", "audited_at": datetime.now(), "updated_at": datetime.now()} if admin_notes: update_data["admin_notes"] = admin_notes @@ -847,6 +847,18 @@ class ValuationController: out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) + async def update_calc(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]: + valuation = await self.model.filter(id=valuation_id, is_active=True).first() + if not valuation: + return None + update_data = data.model_dump(exclude_unset=True) + valuation.status ="pending" + if update_data: + await valuation.update_from_dict(update_data) + await valuation.save() + 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 diff --git a/app/schemas/invoice.py b/app/schemas/invoice.py index bc02a28..5102fd6 100644 --- a/app/schemas/invoice.py +++ b/app/schemas/invoice.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional, List, Union, Dict, Any from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator @@ -115,7 +115,7 @@ class UpdateType(BaseModel): class PaymentReceiptCreate(BaseModel): url: str = Field(..., min_length=1, max_length=512) note: Optional[str] = Field(None, max_length=256) - extra: Optional[dict] = None + extra: Optional[Union[List[str], Dict[str, Any]]] = None class PaymentReceiptOut(BaseModel): @@ -124,7 +124,7 @@ class PaymentReceiptOut(BaseModel): note: Optional[str] verified: bool created_at: str - extra: Optional[dict] = None + extra: Optional[Union[List[str], Dict[str, Any]]] = None class AppCreateInvoiceWithReceipt(BaseModel): @@ -133,7 +133,7 @@ class AppCreateInvoiceWithReceipt(BaseModel): invoice_type: Optional[str] = Field(None, pattern=r"^(special|normal)$") # 兼容前端索引字段:"0"→normal,"1"→special invoiceTypeIndex: Optional[str] = None - receipt_url: Optional[str] = Field(None, max_length=512) + receipt_url: Optional[Union[str, List[str]]] = Field(None) receipt_urls: Optional[List[str]] = None note: Optional[str] = Field(None, max_length=256) @@ -145,14 +145,26 @@ class AppCreateInvoiceWithReceipt(BaseModel): @field_validator('receipt_url', mode='before') @classmethod def _clean_receipt_url(cls, v): - if isinstance(v, list) and v: - v = v[0] + if isinstance(v, list): + cleaned: List[str] = [] + for item in v: + if isinstance(item, str): + s = item.strip() + if s.startswith('`') and s.endswith('`'): + s = s[1:-1].strip() + while s.endswith('\\'): + s = s[:-1].strip() + if s: + cleaned.append(s) + return cleaned or None if isinstance(v, str): s = v.strip() if s.startswith('`') and s.endswith('`'): s = s[1:-1].strip() + while s.endswith('\\'): + s = s[:-1].strip() return s or None - return v + return None @field_validator('receipt_urls', mode='before') @classmethod @@ -162,10 +174,18 @@ class AppCreateInvoiceWithReceipt(BaseModel): if isinstance(v, str): v = [v] if isinstance(v, list): + seen = set() cleaned = [] for item in v: - if isinstance(item, str) and item.strip(): - cleaned.append(item.strip()) + if isinstance(item, str): + s = item.strip() + if s.startswith('`') and s.endswith('`'): + s = s[1:-1].strip() + while s.endswith('\\'): + s = s[:-1].strip() + if s and s not in seen: + seen.add(s) + cleaned.append(s) return cleaned or None return None diff --git a/app/schemas/transactions.py b/app/schemas/transactions.py index 3111cd5..847a65e 100644 --- a/app/schemas/transactions.py +++ b/app/schemas/transactions.py @@ -8,6 +8,8 @@ class SendEmailRequest(BaseModel): subject: Optional[str] = Field(None, description="邮件主题") body: str = Field(..., description="文案内容") file_urls: Optional[List[str]] = Field(None, description="附件URL列表") + file_url: Optional[Union[str, List[str]]] = Field(None, description="附件URL或列表(兼容前端传参)") + status: Optional[str] = Field(None, description="开票状态标记: success|invoiced|rejected|refunded") class SendEmailBody(BaseModel): diff --git a/app/schemas/valuation.py b/app/schemas/valuation.py index 916e3e1..4a96f9c 100644 --- a/app/schemas/valuation.py +++ b/app/schemas/valuation.py @@ -193,7 +193,7 @@ class ValuationAssessmentOut(ValuationAssessmentBase): status: str = Field(..., description="评估状态") admin_notes: Optional[str] = Field(None, description="管理员备注") created_at: datetime = Field(..., description="创建时间") - updated_at: datetime = Field(..., description="更新时间") + updated_at: Optional[datetime] = Field(None, description="更新时间") audited_at: Optional[datetime] = Field(None, description="审核时间") is_active: bool = Field(..., description="是否激活") @@ -246,7 +246,7 @@ class UserValuationOut(ValuationAssessmentBase): status: str = Field(..., description="评估状态") admin_notes: Optional[str] = Field(None, description="管理员备注") created_at: datetime = Field(..., description="创建时间") - updated_at: datetime = Field(..., description="更新时间") + updated_at: Optional[datetime] = Field(None, description="更新时间") is_active: Optional[bool] = Field(None, description="是否激活") class Config: @@ -290,7 +290,7 @@ class UserValuationDetail(ValuationAssessmentBase): status: str = Field(..., description="评估状态") admin_notes: Optional[str] = Field(None, description="管理员备注") created_at: datetime = Field(..., description="创建时间") - updated_at: datetime = Field(..., description="更新时间") + updated_at: Optional[datetime] = Field(None, description="更新时间") class Config: from_attributes = True @@ -422,7 +422,7 @@ class ValuationCalculationStepOut(ValuationCalculationStepBase): id: int = Field(..., description="主键ID") valuation_id: int = Field(..., description="关联的估值评估ID") created_at: datetime = Field(..., description="创建时间") - updated_at: datetime = Field(..., description="更新时间") + updated_at: Optional[datetime] = Field(None, description="更新时间") class Config: from_attributes = True diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh index 3aec06e..aa297d4 100644 --- a/deploy/entrypoint.sh +++ b/deploy/entrypoint.sh @@ -1,5 +1,6 @@ #!/bin/sh set -e -nginx +# nginx + python run.py \ No newline at end of file diff --git a/web1/dist.zip b/web1/dist.zip new file mode 100644 index 0000000..7ef9a8d Binary files /dev/null and b/web1/dist.zip differ diff --git a/估值字段.txt b/估值字段.txt index 4246f48..8566973 100644 --- a/估值字段.txt +++ b/估值字段.txt @@ -37,8 +37,8 @@ export DOCKER_DEFAULT_PLATFORM=linux/amd64 -docker build -t zfc931912343/guzhi-fastapi-admin:v2.6 . -docker push zfc931912343/guzhi-fastapi-admin:v2.6 +docker build -t zfc931912343/guzhi-fastapi-admin:v3.1 . +docker push zfc931912343/guzhi-fastapi-admin:v3.1 # 运行容器 @@ -73,6 +73,7 @@ docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4 && 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 +1 - - docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v2.5 && docker rm -f guzhi_pro && docker run -itd --name=guzhi_pro -p 8080:80 -v ~/guzhi-data/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v2.5 \ No newline at end of file + docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.1 && docker rm -f guzhi_pro && docker run -itd --name=guzhi_pro -p 8080:80 -v ~/guzhi-data/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.1 + docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.2 && docker rm -f guzhi_dev && docker run -itd --name=guzhi_dev -p 9990:9999 -v ~/guzhi-data/static:/opt/vue-fastapi-admin/app/static --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.2 \ No newline at end of file