From 5ca0152c558d43ec1258da1020f3c7385c457e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=96=B9=E6=88=90?= Date: Thu, 27 Nov 2025 15:04:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=82=AE?= =?UTF-8?q?=E4=BB=B6=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=92=8C=E8=AF=84=E4=BC=B0?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复邮件发送时的收件方地址验证问题,添加域名解析检查 更新评估状态字段值从"approved"为"pending"以保持一致性 修改发票创建接口以支持无凭证上传的情况 添加用户管理接口的时间范围查询功能 更新SMTP和短信服务的默认配置 --- app/api/v1/app_invoices/app_invoices.py | 10 +++++++--- app/api/v1/app_users/admin_manage.py | 25 +++++++++++++++++++++++- app/api/v1/transactions/transactions.py | 17 ++++++++++++---- app/api/v1/valuations/valuations.py | 10 ++++++---- app/controllers/valuation.py | 7 ++++++- app/models/valuation.py | 2 +- app/schemas/invoice.py | 4 ++-- app/schemas/transactions.py | 4 ++-- app/schemas/valuation.py | 2 +- app/services/email_client.py | 2 ++ app/services/sms_client.py | 4 ++-- app/settings/config.py | 14 ++++++------- requirements.txt | Bin 2904 -> 2926 bytes smtp_test_output.txt | 0 估值字段.txt | 4 ++-- 15 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 smtp_test_output.txt diff --git a/app/api/v1/app_invoices/app_invoices.py b/app/api/v1/app_invoices/app_invoices.py index 56adea2..a696baa 100644 --- a/app/api/v1/app_invoices/app_invoices.py +++ b/app/api/v1/app_invoices/app_invoices.py @@ -116,9 +116,13 @@ async def create_with_receipt(payload: AppCreateInvoiceWithReceipt, current_user wechat=getattr(current_user, "alias", None), ) inv = await invoice_controller.create(inv_data) - 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) - return Success(data=detail, 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) + return Success(data=detail, msg="创建并上传成功") + else: + out = await invoice_controller.get_out(inv.id) + return Success(data=out.model_dump() if out else {}, msg="创建成功,未上传凭证") @app_invoices_router.get("/headers/list", summary="我的抬头列表(分页)", response_model=PageResponse[InvoiceHeaderOut]) async def get_my_headers_paged(page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), current_user: AppUser = Depends(get_current_app_user)): qs = invoice_controller.model_header.filter(app_user_id=current_user.id) if hasattr(invoice_controller, "model_header") else None diff --git a/app/api/v1/app_users/admin_manage.py b/app/api/v1/app_users/admin_manage.py index 8b0bede..bbaf216 100644 --- a/app/api/v1/app_users/admin_manage.py +++ b/app/api/v1/app_users/admin_manage.py @@ -17,6 +17,8 @@ async def list_app_users( phone: Optional[str] = Query(None), wechat: Optional[str] = Query(None), id: Optional[str] = Query(None), + created_start: Optional[str] = Query(None), + created_end: Optional[str] = Query(None), created_at: Optional[List[int]] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), @@ -28,7 +30,28 @@ async def list_app_users( qs = qs.filter(phone__icontains=phone) if wechat: qs = qs.filter(alias__icontains=wechat) - if created_at and len(created_at) == 2: + if created_start or created_end: + def _parse_dt(s: Optional[str]): + if not s: + return None + s = s.replace('+', ' ').strip() + try: + return datetime.strptime(s, "%Y-%m-%d %H:%M:%S") + except Exception: + try: + ms = float(s) + return datetime.fromtimestamp(ms / 1000) + except Exception: + return None + start_dt = _parse_dt(created_start) + end_dt = _parse_dt(created_end) + if start_dt and end_dt: + qs = qs.filter(created_at__gte=start_dt, created_at__lte=end_dt) + elif start_dt: + qs = qs.filter(created_at__gte=start_dt) + elif end_dt: + qs = qs.filter(created_at__lte=end_dt) + elif created_at and len(created_at) == 2: start_dt = datetime.fromtimestamp(created_at[0] / 1000) end_dt = datetime.fromtimestamp(created_at[1] / 1000) qs = qs.filter(created_at__gte=start_dt, created_at__lte=end_dt) diff --git a/app/api/v1/transactions/transactions.py b/app/api/v1/transactions/transactions.py index 620908c..142290d 100644 --- a/app/api/v1/transactions/transactions.py +++ b/app/api/v1/transactions/transactions.py @@ -74,6 +74,15 @@ async def send_email(payload: SendEmailRequest = Body(...)): attachments = [] urls = [] + try: + domain = payload.email.split("@")[-1] + import dns.resolver + try: + dns.resolver.resolve(domain, "MX") + except Exception: + dns.resolver.resolve(domain, "A") + except Exception: + 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 urls: @@ -113,14 +122,14 @@ async def send_email(payload: SendEmailRequest = Body(...)): else: logger.error("transactions.email_send_fail email={} err={}", payload.email, error) - if status == "OK" and data.receipt_id: + if status == "OK" and payload.receipt_id: try: - r = await PaymentReceipt.filter(id=data.receipt_id).first() + r = await PaymentReceipt.filter(id=payload.receipt_id).first() if r: - r.extra = (r.extra or {}) | data.model_dump() + r.extra = (r.extra or {}) | payload.model_dump() await r.save() except Exception as e: - logger.error("transactions.email_extra_save_fail id={} err={}", data.receipt_id, str(e)) + logger.error("transactions.email_extra_save_fail id={} err={}", payload.receipt_id, str(e)) return Success(data={"status": status, "log_id": log.id, "error": error}, msg="发送成功" if status == "OK" else "发送失败") diff --git a/app/api/v1/valuations/valuations.py b/app/api/v1/valuations/valuations.py index b813419..cf2ffc5 100644 --- a/app/api/v1/valuations/valuations.py +++ b/app/api/v1/valuations/valuations.py @@ -120,7 +120,8 @@ async def get_valuations( 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="每页数量") + size: int = Query(10, ge=1, le=100, description="每页数量"), + page_size: Optional[int] = Query(None, alias="page_size", ge=1, le=100, description="每页数量") ): """获取估值评估列表,支持筛选和分页""" query = ValuationAssessmentQuery( @@ -136,7 +137,7 @@ async def get_valuations( audited_start=audited_start, audited_end=audited_end, page=page, - size=size + size=page_size if page_size is not None else size ) result = await valuation_controller.get_list(query) import json @@ -155,10 +156,11 @@ async def get_valuations( async def search_valuations( keyword: str = Query(..., description="搜索关键词"), page: int = Query(1, ge=1, description="页码"), - size: int = Query(10, ge=1, le=100, description="每页数量") + size: int = Query(10, ge=1, le=100, description="每页数量"), + page_size: Optional[int] = Query(None, alias="page_size", ge=1, le=100, description="每页数量") ): """根据关键词搜索估值评估记录""" - result = await valuation_controller.search(keyword, page, size) + result = await valuation_controller.search(keyword, page, page_size if page_size is not None else size) import json items = [json.loads(item.model_dump_json()) for item in result.items] return SuccessExtra( diff --git a/app/controllers/valuation.py b/app/controllers/valuation.py index 6a715ac..bf04d16 100644 --- a/app/controllers/valuation.py +++ b/app/controllers/valuation.py @@ -490,6 +490,11 @@ class ValuationController: update_data['audited_at'] = datetime.now() await valuation.update_from_dict(update_data) await valuation.save() + from datetime import datetime + valuation.status = "success" + if not getattr(valuation, "audited_at", None): + valuation.audited_at = datetime.now() + await valuation.save() out = ValuationAssessmentOut.model_validate(valuation) return await self._attach_user_phone(out) @@ -643,7 +648,7 @@ class ValuationController: return None from datetime import datetime - update_data = {"status": "approved", "audited_at": datetime.now()} + update_data = {"status": "pending", "audited_at": datetime.now()} if admin_notes: update_data["admin_notes"] = admin_notes diff --git a/app/models/valuation.py b/app/models/valuation.py index 9e88093..1323faf 100644 --- a/app/models/valuation.py +++ b/app/models/valuation.py @@ -82,7 +82,7 @@ class ValuationAssessment(Model): # 系统字段 user = fields.ForeignKeyField("models.AppUser", related_name="valuations", description="提交用户") - status = fields.CharField(max_length=20, default="success", description="评估状态: pending(待审核), success(已通过), fail(已拒绝)") + status = fields.CharField(max_length=20, default="pending", description="评估状态: pending(待审核), success(已通过), rejected(已拒绝)") admin_notes = fields.TextField(null=True, description="管理员备注") created_at = fields.DatetimeField(auto_now_add=True, description="创建时间") updated_at = fields.DatetimeField(auto_now=True, description="更新时间") diff --git a/app/schemas/invoice.py b/app/schemas/invoice.py index 70765aa..ee5d376 100644 --- a/app/schemas/invoice.py +++ b/app/schemas/invoice.py @@ -126,7 +126,7 @@ class AppCreateInvoiceWithReceipt(BaseModel): invoice_type: Optional[str] = Field(None, pattern=r"^(special|normal)$") # 兼容前端索引字段:"0"→normal,"1"→special invoiceTypeIndex: Optional[str] = None - receipt_url: str = Field(..., min_length=1, max_length=512) + receipt_url: Optional[str] = Field(None, max_length=512) note: Optional[str] = Field(None, max_length=256) @field_validator('ticket_type', mode='before') @@ -143,7 +143,7 @@ class AppCreateInvoiceWithReceipt(BaseModel): s = v.strip() if s.startswith('`') and s.endswith('`'): s = s[1:-1].strip() - return s + return s or None return v @model_validator(mode='after') diff --git a/app/schemas/transactions.py b/app/schemas/transactions.py index 43d7b82..3111cd5 100644 --- a/app/schemas/transactions.py +++ b/app/schemas/transactions.py @@ -1,10 +1,10 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, EmailStr from typing import Optional, List, Union class SendEmailRequest(BaseModel): receipt_id: Optional[int] = Field(None, description="付款凭证ID") - email: str = Field(..., description="邮箱地址") + email: EmailStr = Field(..., description="邮箱地址") subject: Optional[str] = Field(None, description="邮件主题") body: str = Field(..., description="文案内容") file_urls: Optional[List[str]] = Field(None, description="附件URL列表") diff --git a/app/schemas/valuation.py b/app/schemas/valuation.py index 5b1524c..916e3e1 100644 --- a/app/schemas/valuation.py +++ b/app/schemas/valuation.py @@ -362,7 +362,7 @@ class ValuationAssessmentQuery(BaseModel): institution: Optional[str] = Field(None, description="所属机构") industry: Optional[str] = Field(None, description="所属行业") heritage_level: Optional[str] = Field(None, description="非遗等级") - status: Optional[str] = Field(None, description="评估状态: pending(待审核), approved(已通过), rejected(已拒绝)") + status: Optional[str] = Field(None, description="评估状态: pending(待审核), success(已通过), rejected(已拒绝)") is_active: Optional[bool] = Field(None, description="是否激活") phone: Optional[str] = Field(None, description="手机号模糊查询") submitted_start: Optional[str] = Field(None, description="提交时间开始(毫秒时间戳或ISO字符串)") diff --git a/app/services/email_client.py b/app/services/email_client.py index 8ae2ea4..d4364c6 100644 --- a/app/services/email_client.py +++ b/app/services/email_client.py @@ -51,6 +51,8 @@ class EmailClient: server.quit() except Exception: pass + if isinstance(e, smtplib.SMTPRecipientsRefused): + return {"status": "FAIL", "error": "收件方地址不存在或暂时不可用"} return {"status": "FAIL", "error": str(e)} def send_many(self, to_email: str, subject: Optional[str], body: str, attachments: Optional[List[Tuple[bytes, str]]] = None) -> dict: diff --git a/app/services/sms_client.py b/app/services/sms_client.py index 6093a60..604005d 100644 --- a/app/services/sms_client.py +++ b/app/services/sms_client.py @@ -86,7 +86,7 @@ class SMSClient: 返回体映射字典 """ key = settings.ALIYUN_SMS_TEMPLATE_PARAM_CODE_KEY or "code" - template = settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY or "SMS_498190229" + template = settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY logger.info("sms.send_code using key={} template={} phone={}", key, template, phone) return self.send_by_template(phone, template, {key: code}) @@ -99,7 +99,7 @@ class SMSClient: Returns: 返回体映射字典 """ - template = settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT or "SMS_498140213" + template = settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT logger.info("sms.send_report using template={} phone={}", template, phone) return self.send_by_template(phone, template, {}) diff --git a/app/settings/config.py b/app/settings/config.py index a54b0c8..e14f569 100644 --- a/app/settings/config.py +++ b/app/settings/config.py @@ -100,18 +100,18 @@ class Settings(BaseSettings): ALIYUN_SMS_SIGN_NAME: typing.Optional[str] = "成都文化产权交易所" ALIYUN_SMS_ENDPOINT: str = "dysmsapi.aliyuncs.com" ALIYUN_SMS_TEMPLATE_CODE_VERIFY: typing.Optional[str] = "SMS_498140213" - ALIYUN_SMS_TEMPLATE_CODE_REPORT: typing.Optional[str] = "SMS_49190229" + ALIYUN_SMS_TEMPLATE_CODE_REPORT: typing.Optional[str] = "SMS_498190229" SMS_CODE_DIGITS: int = 6 SMS_DEBUG_LOG_CODE: bool = True ALIYUN_USE_DEFAULT_CREDENTIALS: bool = False ALIYUN_SMS_TEMPLATE_PARAM_CODE_KEY: typing.Optional[str] = "code" - SMTP_HOST: typing.Optional[str] = None - SMTP_PORT: typing.Optional[int] = None - SMTP_USERNAME: typing.Optional[str] = None - SMTP_PASSWORD: typing.Optional[str] = None - SMTP_TLS: bool = True - SMTP_FROM: typing.Optional[str] = None + SMTP_HOST: typing.Optional[str] = "smtp.qiye.aliyun.com" + SMTP_PORT: typing.Optional[int] = 465 + SMTP_USERNAME: typing.Optional[str] = "value@cdcee.net" + SMTP_PASSWORD: typing.Optional[str] = "PPXbILdGlRCn2VOx" + SMTP_TLS: bool = False + SMTP_FROM: typing.Optional[str] = "value@cdcee.net" settings = Settings() diff --git a/requirements.txt b/requirements.txt index 88e4123891ad400c494e0da1549b2fe50868713c..b6180adac10f97ceae0f415f1d7ba3f9a8847e27 100644 GIT binary patch delta 30 kcmca1_D*a=1h-fsLlHwdLq0 Date: Thu, 27 Nov 2025 16:35:36 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BC=B0=E5=80=BC?= =?UTF-8?q?=E4=B8=8E=E4=BA=A4=E6=98=93=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/app_users/app_users.py | 3 +-- app/api/v1/app_valuations/app_valuations.py | 2 +- app/api/v1/transactions/transactions.py | 10 +++++++++- app/controllers/valuation.py | 2 +- .../sub_formulas/basic_value_b11.py | 4 ++-- .../sub_formulas/traffic_factor_b12.py | 16 +++++++++++++++- 估值字段.txt | 4 ++-- 7 files changed, 31 insertions(+), 10 deletions(-) diff --git a/app/api/v1/app_users/app_users.py b/app/api/v1/app_users/app_users.py index 88d9062..aa7f32a 100644 --- a/app/api/v1/app_users/app_users.py +++ b/app/api/v1/app_users/app_users.py @@ -141,8 +141,7 @@ async def get_quota(current_user: AppUser = Depends(get_current_app_user)): - 若后续接入配额系统,可从数据库中读取真实值 """ remaining_count = current_user.remaining_quota - user_type = "体验用户" - return Success(data={"remaining_count": remaining_count, "user_type": user_type}) + return Success(data={"remaining_count": remaining_count}) @router.put("/profile", response_model=BasicResponse[dict], summary="更新用户信息") diff --git a/app/api/v1/app_valuations/app_valuations.py b/app/api/v1/app_valuations/app_valuations.py index 0e0e949..7701356 100644 --- a/app/api/v1/app_valuations/app_valuations.py +++ b/app/api/v1/app_valuations/app_valuations.py @@ -232,6 +232,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate market_value_c=calculation_result.get('market_value_c'), final_value_ab=calculation_result.get('final_value_ab'), calculation_result=calculation_result, + status='pending', ) ) logger.info( @@ -468,7 +469,6 @@ async def calculate_valuation( operator_name=user.alias or user.username or user.phone or "", before_count=before, after_count=before - 1, - op_type="consume", remark="发起估值" ) except Exception: diff --git a/app/api/v1/transactions/transactions.py b/app/api/v1/transactions/transactions.py index 142290d..10e84ce 100644 --- a/app/api/v1/transactions/transactions.py +++ b/app/api/v1/transactions/transactions.py @@ -103,7 +103,7 @@ async def send_email(payload: SendEmailRequest = Body(...)): except Exception as e: result = {"status": "FAIL", "error": str(e)} - body_summary = payload.body[:500] + body_summary = payload.body status = result.get("status") error = result.get("error") first_name = attachments[0][1] if attachments else None @@ -128,6 +128,14 @@ async def send_email(payload: SendEmailRequest = Body(...)): if r: r.extra = (r.extra or {}) | payload.model_dump() await r.save() + try: + inv = await r.invoice + if inv: + inv.status = "invoiced" + await inv.save() + logger.info("transactions.invoice_mark_invoiced receipt_id={} invoice_id={}", payload.receipt_id, inv.id) + except Exception as e2: + logger.warning("transactions.invoice_mark_invoiced_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/valuation.py b/app/controllers/valuation.py index bf04d16..2a640ad 100644 --- a/app/controllers/valuation.py +++ b/app/controllers/valuation.py @@ -491,7 +491,7 @@ class ValuationController: await valuation.update_from_dict(update_data) await valuation.save() from datetime import datetime - valuation.status = "success" + valuation.status = update_data.get("status", "pending") if not getattr(valuation, "audited_at", None): valuation.audited_at = datetime.now() await valuation.save() diff --git a/app/utils/calculation_engine/economic_value_b1/sub_formulas/basic_value_b11.py b/app/utils/calculation_engine/economic_value_b1/sub_formulas/basic_value_b11.py index 1d670ff..f1e549f 100644 --- a/app/utils/calculation_engine/economic_value_b1/sub_formulas/basic_value_b11.py +++ b/app/utils/calculation_engine/economic_value_b1/sub_formulas/basic_value_b11.py @@ -106,7 +106,7 @@ class BasicValueB11Calculator: # 使用两个增长率的平均值 avg_growth_rate = (growth_rate_1 + growth_rate_2) / 2 - return avg_growth_rate + return max(avg_growth_rate, 0.0) def calculate_legal_strength_l(self, patent_score: float, @@ -379,4 +379,4 @@ if __name__ == "__main__": # print(f"增长率: {growth_rate*100}%") # print(f"(1+14%)^5 = {growth_factor:.4f}") # print(f"2333 × {growth_factor:.4f} = {initial_value * growth_factor:.2f}") - # print(f"再除以5: {initial_value * growth_factor:.2f} ÷ 5 = {result:.2f}") \ No newline at end of file + # print(f"再除以5: {initial_value * growth_factor:.2f} ÷ 5 = {result:.2f}") diff --git a/app/utils/calculation_engine/economic_value_b1/sub_formulas/traffic_factor_b12.py b/app/utils/calculation_engine/economic_value_b1/sub_formulas/traffic_factor_b12.py index 74a203f..add801b 100644 --- a/app/utils/calculation_engine/economic_value_b1/sub_formulas/traffic_factor_b12.py +++ b/app/utils/calculation_engine/economic_value_b1/sub_formulas/traffic_factor_b12.py @@ -48,6 +48,20 @@ class TrafficFactorB12Calculator: traffic_factor = (math.log(search_index_s1 / industry_average_s2) * 0.3 + social_media_spread_s3 * 0.7) + + """ +为什么需要 + +- 经济价值 B1 的公式是 B1 = B11 × (1 + B12) × B13 (app/utils/calculation_engine/economic_value_b1/economic_value_b1.py:34-45)。 +- 如果 B12 < -1 ,则 (1 + B12) 会变成负数,导致 B1 翻成负值并把模型估值 B(final_value_ab/model_value_b.py:48-50)拉到巨负。 +- 通过设置 B12 ≥ -0.9 ,确保 (1 + B12) ≥ 0.1 ,即乘数始终为正且不至于过小。 +直观示例 + +- 原始计算得到 B12 = -1.8 (例如 ln(S1/S2) 很大负、社交传播度 S3 又很低),则 (1 + B12) = -0.8 ,会让 B1 变负。 +- 裁剪后 B12 = -0.9 ,则 (1 + B12) = 0.1 , B1 保持为正,避免最终估值出现大幅负值。 + """ + if traffic_factor < -0.9: + traffic_factor = -0.9 return traffic_factor @@ -346,4 +360,4 @@ if __name__ == "__main__": print(f"覆盖人群指数: {coverage_index:.4f}") print(f"转化效率: {conversion_efficiency:.4f}") print(f"社交媒体传播度S3: {social_media_spread_s3:.4f}") - print(f"流量因子B12: {traffic_factor:.4f}") \ No newline at end of file + print(f"流量因子B12: {traffic_factor:.4f}") diff --git a/估值字段.txt b/估值字段.txt index 4c11ced..dda01a9 100644 --- a/估值字段.txt +++ b/估值字段.txt @@ -37,8 +37,8 @@ export DOCKER_DEFAULT_PLATFORM=linux/amd64 -docker build -t zfc931912343/guzhi-fastapi-admin:v2.0 . -docker push zfc931912343/guzhi-fastapi-admin:v2.0 +docker build -t zfc931912343/guzhi-fastapi-admin:v2.1 . +docker push zfc931912343/guzhi-fastapi-admin:v2.1 # 运行容器