diff --git a/.trae/documents/估值审核联动与估值次数扣减实现方案.md b/.trae/documents/估值审核联动与估值次数扣减实现方案.md deleted file mode 100644 index b87b3c2..0000000 --- a/.trae/documents/估值审核联动与估值次数扣减实现方案.md +++ /dev/null @@ -1,131 +0,0 @@ -## 目标概述 - -* 加强“交易管理-发票附件与发送邮箱”能力,完善数据记录与事务保障。 - -* 改进“开票弹窗附件上传”支持多文件上传(后端存储结构)。 - -* 优化“用户管理备注与操作记录”,区分备注维度并完善日志。 - -* 覆盖单元/集成测试、数据库迁移、API文档与审计日志。 - -## 数据库与模型变更 - -* EmailSendLog - - * 新增:`extra: JSONField(null=True)` 完整记录发送邮箱的请求数据(收件人、主题、正文、附件列表、重试信息等)。 - -* Invoice(或与开票弹窗相关的业务模型) - - * 新增:`attachments: JSONField(null=True)` 支持多个附件URL(与弹窗上传对应)。 - -*
- -* 迁移脚本:创建/修改上述字段;保留历史数据不丢失。 - -## 事务与原子性 - -* 发送邮箱流程(交易管理) - - * 封装在 `tortoise.transactions.in_transaction()` 中:邮件发送、EmailSendLog写入(含attachments/extra)原子提交;失败回滚。 - - * extra写入内容:完整`SendEmailRequest`(收件人、主题、正文、附件URL/文件名、重试次数、客户端UA等)。 - -* 多文件上传至发票附件(开票弹窗) - - * 更新发票的 `attachments` 字段在同一事务内写入;如任一URL校验失败则回滚。 - -## 后端接口改造 - -* 上传组件(后端) - - * 新增:`POST /api/v1/upload/files` 接收 `files: List[UploadFile]`,统一返回 `urls: string[]`;保留现有单文件接口向后兼容。 - -* 交易管理(管理员) - - * `POST /api/v1/transactions/send-email` 扩展: - - * 入参支持 `file_urls: string[]`(与/或单文件),服务端聚合附件; - - * 在事务中记录 EmailSendLog(含attachments与extra),返回log\_id与状态; - - * 回显接口(详情/列表)新增 `extra` 字段完整展示发送记录。 - -* 开票弹窗附件(管理员/或对应端) - - * 新增/改造:`PUT /api/v1/invoice/{id}/attachments` 入参 `urls: string[]`,更新发票 `attachments`。 - - * 列表/详情回显 `attachments: string[]`。 - -* 用户管理备注优化(管理员端) - - * 新接口:`PUT /api/v1/app-user-admin/{user_id}/notes` - - * 入参:`system_notes?: string`、`user_notes?: string`(可选择性更新) - - * 逻辑:仅更新提供的字段;不影响其他字段。 - - * 修复:`POST /api/v1/app-user-admin/quota` 仅调整次数,不再自动写入 `user.notes`。 - - * 操作日志:在调整配额、更新备注、停用启用等操作时写入 `AppUserOperationLog`。 - -## 前端改造(要点) - -* 多文件上传组件 - - * 改为多选/拖拽支持;对每个文件显示上传进度与失败重试; - - * 成功后收集URL数组写入发票 `attachments` 或作为邮件附件来源; - - * 兼容旧接口:若后端仅返回单URL,前端仍正常显示(降级为单文件模式)。 - -* 开票弹窗 - - * 支持附件列表预览与移除;提交时调用 `PUT /invoice/{id}/attachments`; - -* 邮件发送弹窗 - - * 选择附件来源(已上传的附件/本地文件再上传);提交后在详情页面完整回显 `extra`(含附件清单与正文等)。 - -## 审计与日志 - -* 关键操作:邮件发送、发票附件更新、用户备注更新、配额调整 - - * 统一调用审计记录(路由中间件已存在,补充结构化日志:`logger.info()` + DB审计表/操作日志表写入)。 - -## 测试方案 - -* 单元测试 - - * Email发送控制器:事务成功/失败回滚(模拟抛错) - - * 多文件上传:文件类型校验、URL数组返回、尾随空白处理 - - * 备注更新:选择性字段更新、不影响其他字段 - -* 集成测试(FastAPI + httpx.AsyncClient) - - * 发送邮箱:请求→持久化校验(attachments/extra)→回显接口校验 - - * 附件上传:批量上传、更新发票、列表/详情回显 - - * 用户备注:接口调用→DB值校验→操作日志存在 - -## 迁移与兼容 - -* 使用现有迁移工具(如Aerich)生成并应用迁移:新增JSON/Text字段; - -* 前端保留旧接口兼容:若上传仍走单文件,后端返回数组长度1; - -* API文档(OpenAPI) - - * 补充/更新以上端点的schema说明、示例请求/响应、错误码约定; - -## 实施步骤与交付 - -1. 数据模型与迁移脚本编写与应用(EmailSendLog、Invoice、AppUser/OperationLog)。 -2. 后端接口改造与事务封装(邮件发送、多文件上传、发票附件更新、备注接口)。 -3. 前端组件与弹窗改造(多文件上传、进度与错误处理、回显extra)。 -4. 审计日志与结构化日志补充。 -5. 单元与集成测试编写,覆盖核心路径。 -6. 更新接口文档与部署回归验证。 - diff --git a/.trae/documents/修复添加发票抬头必填项校验.md b/.trae/documents/修复添加发票抬头必填项校验.md deleted file mode 100644 index 6b90a13..0000000 --- a/.trae/documents/修复添加发票抬头必填项校验.md +++ /dev/null @@ -1,34 +0,0 @@ -## 目标 - -* 添加抬头时将 `公司名称`、`公司税号`、`电子邮箱`设为必填,其他字段可为空。 - -* 现状确认(代码引用) - -- 后端必填:`app/schemas/invoice.py:6–12` 中 `InvoiceHeaderCreate` 已要求 `company_name`、`tax_number` 为必填,`email: EmailStr` 为必填。 - -- API 入口:`app/api/v1/app_invoices/app_invoices.py:55–58` 新增抬头接口使用 `InvoiceHeaderCreate`,后端将严格校验三项必填。 - -## 修改方案 - -1. 统一前端校验文案 - - * 统一三项必填的错误提示为简洁中文,如:“请输入公司名称 / 公司税号 / 电子邮箱”。 - - * 邮箱格式提示统一为:“请输入有效的电子邮箱”。 - -2. 后端校验与返回确认 - - * 保持 `InvoiceHeaderCreate` 的必填与格式限制不变(`app/schemas/invoice.py:6–12`)。 - - * 确认更新接口 `InvoiceHeaderUpdate`(`app/schemas/invoice.py:32–39`)允许局部更新、但不影响创建必填逻辑。 - -3. 验证与测试 - - * 后端接口验证:对 `POST /app-invoices/headers`(`app/api/v1/app_invoices/app_invoices.py:55–58`)进行用例:缺失任一必填字段应返回 422;全部正确应 200/201。 - - * 可补充最小化单元测试:Pydantic 校验用例覆盖必填与格式。 - -## 交付内容。 - -* 完成基本交互与接口验证,确保行为符合预期。 - diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..dcbf81d --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,225 @@ +# 非遗资产估值系统 - 部署文档 + +## 项目概述 + +非遗资产估值系统是一个基于 Vue.js + FastAPI 的全栈应用,用于非物质文化遗产资产的价值评估。 + +- **前端**: Vue.js + Vite + pnpm +- **后端**: Python 3.11 + FastAPI + Tortoise ORM +- **数据库**: MySQL +- **容器化**: Docker + +--- + +## 目录结构 + +``` +youshu-guzhi/ +├── app/ # 后端 FastAPI 应用 +│ ├── api/ # API 路由 +│ ├── controllers/ # 业务控制器 +│ ├── models/ # 数据库模型 +│ ├── schemas/ # Pydantic 数据模型 +│ ├── settings/ # 配置文件 +│ └── utils/ # 工具函数和计算引擎 +├── web/ # 前端 Vue.js 应用 +├── deploy/ # 部署相关文件 +│ ├── entrypoint.sh # 容器启动脚本 +│ └── web.conf # Nginx 配置 +├── Dockerfile # Docker 构建文件 +├── requirements.txt # Python 依赖 +└── run.py # 应用启动入口 +``` + +--- + +## 环境配置 + +### 数据库配置 + +#### 使用 Docker 部署 MySQL + +```bash +# 创建数据目录 +mkdir -p ~/mysql-data + +# 启动 MySQL 容器 +docker run -d \ + --name mysql-valuation \ + -p 3306:3306 \ + -e MYSQL_ROOT_PASSWORD=your_password \ + -e MYSQL_DATABASE=valuation_service \ + -v ~/mysql-data:/var/lib/mysql \ + --restart=unless-stopped \ + mysql:8.0 +``` + +#### 应用配置 + +配置文件位置: `app/settings/config.py` + +```python +TORTOISE_ORM = { + "connections": { + "mysql": { + "engine": "tortoise.backends.mysql", + "credentials": { + "host": "your_mysql_host", # 数据库主机地址 + "port": 3306, # 数据库端口 + "user": "root", # 数据库用户名 + "password": "your_password", # 数据库密码 + "database": "valuation_service", # 数据库名称 + }, + }, + }, + ... +} +``` + +### 第三方服务配置 + +| 服务 | 配置项 | 说明 | +|-----|-------|------| +| 阿里云短信 | `ALIBABA_CLOUD_ACCESS_KEY_ID/SECRET` | 短信验证码发送 | +| 阿里云邮件 | `SMTP_*` | 邮件发送 | + +--- + +## 本地开发 + +### 1. 安装依赖 + +```bash +# 安装 Python 依赖 +pip install -r requirements.txt + +# 安装前端依赖 +cd web +pnpm install +``` + +### 2. 启动服务 + +```bash +# 启动后端 (端口 9999) +python run.py + +# 启动前端开发服务器 (另一个终端) +cd web +pnpm run dev +``` + +--- + +## Docker 部署 + +### 1. 构建镜像 + +```bash +# 设置平台 (M1/M2 Mac 需要) +export DOCKER_DEFAULT_PLATFORM=linux/amd64 + +# 构建镜像 +docker build -t zfc931912343/guzhi-fastapi-admin:v3.9 . + +# 推送到 Docker Hub +docker push zfc931912343/guzhi-fastapi-admin:v3.9 +``` + +### 2. 部署到服务器 + +#### 生产环境 + +```bash +# 创建数据目录 +mkdir -p ~/guzhi-data/static/images + +# 拉取并运行 +docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.9 \ + && docker rm -f guzhi_pro \ + && docker run -itd \ + --name=guzhi_pro \ + -p 8080:9999 \ + -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.9 +``` + +#### 开发/测试环境 + +```bash +docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.9 \ + && 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.9 +``` + +--- + +## 端口说明 + +| 环境 | 容器名 | 主机端口 | 容器端口 | +|-----|-------|---------|---------| +| 生产 | guzhi_pro | 8080 | 9999 | +| 开发 | guzhi_dev | 9990 | 9999 | + +--- + +## 数据持久化 + +容器挂载的数据目录: + +``` +~/guzhi-data/static/images -> /opt/vue-fastapi-admin/app/static/images +``` + +用于存储用户上传的图片文件(如非遗纹样图片、证书图片等)。 + +--- + +## 常用运维命令 + +```bash +# 查看容器日志 +docker logs -f guzhi_pro + +# 进入容器 +docker exec -it guzhi_pro bash + +# 重启容器 +docker restart guzhi_pro + +# 查看容器状态 +docker ps | grep guzhi +``` + +--- + +## API 接口说明 + +| 模块 | 路径前缀 | 说明 | +|-----|---------|------| +| 用户端估值 | `/api/v1/app-valuations/` | 用户提交估值请求 | +| 管理端估值 | `/api/v1/valuations/` | 管理后台查看/审核 | +| 计算报告 | `/api/v1/valuations/{id}/report` | 获取计算过程报告 | + +--- + +## 版本历史 + +| 版本 | 日期 | 说明 | +|-----|------|------| +| v3.9 | 2025-12-18 | 修复风险调整系数B3显示问题,添加计算过程详情 | +| v3.8 | 2025-12-18 | 修复历史传承度HI权重计算 | + +--- + +## 联系信息 + +如有问题,请联系项目负责人。 diff --git a/aaa.json b/aaa.json deleted file mode 100644 index 9af7f17..0000000 --- a/aaa.json +++ /dev/null @@ -1,29 +0,0 @@ -2025-11-17 18:13:15.766 | INFO | app.api.v1.app_valuations.app_valuations:calculate_valuation:284 - valuation.task_queued user_id=30 asset_name=蜀锦 industry=纺织业 -2025-11-17 18:13:15.768 | INFO | app.api.v1.app_valuations.app_valuations:_perform_valuation_calculation:44 - valuation.calc_start user_id=30 asset_name=蜀锦 industry=纺织业 -2025-11-17 18:13:15 - INFO - 14.145.4.28:0 - "POST /api/v1/app-valuations/ HTTP/1.0" 200 OK -2025-11-17 18:13:16,048 - INFO - API请求成功: dajiala.web_search -2025-11-17 18:13:16,049 - ERROR - 微信指数API返回错误: {'code': 20001, 'msg': '金额不足,请充值', 'data': ''} -2025-11-17 18:13:16,049 - WARNING - 没有指数数据用于计算平均值 -2025-11-17 18:13:16.049 | INFO | app.api.v1.app_valuations.app_valuations:_extract_calculation_params_b1:336 - 资产 '蜀锦' 的微信指数近30天平均值: 0.0 -2025-11-17 18:13:16,051 - INFO - 行业 纺织业 S2计算: S2=2200.0 -{'orderNo': '202511171813162420295', 'rc': '0001', 'msg': '查询成功,无数据'} -2025-11-17 18:13:16,827 - INFO - API请求成功: chinaz.judgement -{'orderNo': '202511171813169260297', 'rc': '0002', 'msg': '查询企业不存在,请检查后再试'} -2025-11-17 18:13:17,428 - INFO - API请求成功: chinaz.patent -2025-11-17 18:13:17.428 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:80 - final_value_a.calculation_start input_data_keys=['model_data', 'market_data'] model_data_keys=['economic_data', 'cultural_data', 'risky_data'] market_data_keys=['weighted_average_price', 'manual_bids', 'expert_valuations', 'daily_browse_volume', 'collection_count', 'issuance_level', 'recent_market_activity'] -2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:89 - final_value_a.economic_data 经济价值B1参数: 近三年机构收益=[169.0, 169.0, 169.0] 专利分=3.0 普及地域分=7.0 侵权分=0.0 创新投入比=18.93491124260355 ESG分=5.0 专利使用量=0.0 行业修正系数=-0.5 -2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:101 - final_value_a.cultural_data 文化价值B2参数: 传承人等级系数=0.7 跨境深度=0.3 线下教学次数=50.0 抖音浏览量=67000.0 快手浏览量=0 哔哩哔哩浏览量=0 结构复杂度=1.5 归一化信息熵=9 历史传承度=0.0 -2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:114 - final_value_a.risky_data 风险调整B3参数: 最高价=3980.0 最低价=1580.0 诉讼状态=0.0 传承人年龄=[0, 0, 2] -2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:122 - final_value_a.market_data 市场估值C参数: 平均交易价=None 手动出价=[3980.0, 1580.0, 2780.0] 专家估值=[] 日浏览量=296000.0 收藏数量=67000 发行等级=限量:总发行份数 ≤100份 最近市场活动=近一周 -2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:132 - final_value_a.calculating_model_value_b 开始计算模型估值B -2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:142 - final_value_a.model_value_b_calculated 模型估值B计算完成: 模型估值B=336.37180882339413万元 耗时=0ms 返回字段=['economic_value_b1', 'cultural_value_b2', 'risk_value_b3', 'model_value_b'] -2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:152 - final_value_a.calculating_market_value_c 开始计算市场估值C -浏览热度分: -2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:162 - final_value_a.market_value_c_calculated 市场估值C计算完成: 市场估值C=9452.0万元 耗时=0ms 返回字段=['market_bidding_c1', 'heat_coefficient_c2', 'scarcity_multiplier_c3', 'temporal_decay_c4', 'market_value_c'] -2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:172 - final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B=336.37180882339413万元 市场估值C=9452.0万元 -2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_final_value_a:50 - final_value_a.calculate_final_value_a 开始计算最终估值A: 模型估值B=336.37180882339413万元 市场估值C=9452.0万元 -2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_final_value_a:57 - final_value_a.weighted_values 加权计算: 模型估值B加权值=235.46026617637588万元(权重0.7) 市场估值C加权值=2835.6万元(权重0.3) -2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_final_value_a:62 - final_value_a.final_calculation 最终估值A计算: 模型估值B=336.37180882339413万元 市场估值C=9452.0万元 模型加权值=235.46026617637588万元 市场加权值=2835.6万元 最终估值AB=3071.060266176376万元 -2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:183 - final_value_a.calculation_completed 最终估值A计算完成: 最终估值AB=3071.060266176376万元 模型估值B=336.37180882339413万元 市场估值C=9452.0万元 总耗时=1ms 模型计算耗时=0ms 市场计算耗时=0ms -2025-11-17 18:13:17.430 | INFO | app.api.v1.app_valuations.app_valuations:_perform_valuation_calculation:160 - valuation.calc_done user_id=30 duration_ms=1662 model_value_b=336.37180882339413 market_value_c=9452.0 final_value_ab=3071.060266176376 -Traceback (most recent call last): \ No newline at end of file diff --git a/app/api/v1/app_users/admin_manage.py b/app/api/v1/app_users/admin_manage.py index bbaf216..456f1b0 100644 --- a/app/api/v1/app_users/admin_manage.py +++ b/app/api/v1/app_users/admin_manage.py @@ -16,6 +16,7 @@ admin_app_users_router = APIRouter(dependencies=[DependAuth, DependPermission], async def list_app_users( phone: Optional[str] = Query(None), wechat: Optional[str] = Query(None), + include_deleted: Optional[bool] = Query(False), id: Optional[str] = Query(None), created_start: Optional[str] = Query(None), created_end: Optional[str] = Query(None), @@ -24,6 +25,8 @@ async def list_app_users( page_size: int = Query(10, ge=1, le=100), ): qs = AppUser.filter() + if not include_deleted: + qs = qs.filter(is_deleted=False) if id is not None and id.strip().isdigit(): qs = qs.filter(id=int(id.strip())) if phone: @@ -133,3 +136,11 @@ async def update_app_user(user_id: int, data: AppUserUpdateSchema): "updated_at": user.updated_at.isoformat() if user.updated_at else "", "remaining_quota": int(getattr(user, "remaining_quota", 0) or 0), }, msg="更新成功") + + +@admin_app_users_router.delete("/{user_id}", summary="注销App用户", response_model=BasicResponse[dict]) +async def admin_delete_app_user(user_id: int): + ok = await app_user_controller.delete_user_account(user_id) + if not ok: + raise HTTPException(status_code=404, detail="用户不存在") + return Success(data={"user_id": user_id}, msg="账号已注销") diff --git a/app/api/v1/app_users/app_users.py b/app/api/v1/app_users/app_users.py index aa7f32a..b07d7af 100644 --- a/app/api/v1/app_users/app_users.py +++ b/app/api/v1/app_users/app_users.py @@ -15,11 +15,20 @@ from app.schemas.base import BasicResponse, MessageOut, Success from app.utils.app_user_jwt import ( create_app_user_access_token, get_current_app_user, - ACCESS_TOKEN_EXPIRE_MINUTES + ACCESS_TOKEN_EXPIRE_MINUTES, + verify_app_user_token ) from app.models.user import AppUser from app.controllers.user_valuation import user_valuation_controller from app.controllers.invoice import invoice_controller +from app.core.token_blacklist import add_to_blacklist +from fastapi import Header +from pydantic import BaseModel, Field +from typing import Optional +import time +from app.models.valuation import ValuationAssessment +from app.services.sms_store import store +from app.settings import settings router = APIRouter() @@ -78,6 +87,50 @@ async def logout(current_user: AppUser = Depends(get_current_app_user)): return Success(data={"message": "登出成功"}) +class DeleteAccountRequest(BaseModel): + code: Optional[str] = Field(None, description="短信验证码或绕过码") + + +@router.delete("/account", summary="注销用户信息", response_model=BasicResponse[dict]) +async def delete_account(current_user: AppUser = Depends(get_current_app_user), token: str = Header(None), payload: Optional[DeleteAccountRequest] = None): + if payload and payload.code: + if settings.SMS_BYPASS_CODE and payload.code == settings.SMS_BYPASS_CODE: + store.mark_verified(current_user.phone) + else: + ok, reason = store.can_verify(current_user.phone) + if not ok: + raise HTTPException(status_code=423, detail=str(reason)) + record = store.get_code(current_user.phone) + if not record: + raise HTTPException(status_code=400, detail="验证码已过期") + code_stored, expires_at = record + if time.time() > expires_at: + store.clear_code(current_user.phone) + raise HTTPException(status_code=400, detail="验证码已过期") + if payload.code != code_stored: + count, locked = store.record_verify_failure(current_user.phone) + if locked: + raise HTTPException(status_code=423, detail="尝试次数过多,已锁定") + raise HTTPException(status_code=401, detail="验证码错误") + store.clear_code(current_user.phone) + store.reset_failures(current_user.phone) + store.mark_verified(current_user.phone) + else: + if not store.is_recently_verified(current_user.phone): + raise HTTPException(status_code=403, detail="请先完成手机号验证码验证") + remaining_quota = int(getattr(current_user, "remaining_quota", 0) or 0) + if remaining_quota > 0: + raise HTTPException(status_code=400, detail="当前剩余估值次数大于0,无法注销账号") + ok = await app_user_controller.delete_user_account(current_user.id) + if token: + payload = verify_app_user_token(token) + exp = getattr(payload, "exp", None) if payload else None + await add_to_blacklist(token, current_user.id, exp) + if not ok: + raise HTTPException(status_code=404, detail="用户不存在") + return Success(data={"message": "账号已注销"}) + + @router.get("/profile", response_model=BasicResponse[dict], summary="获取用户信息") async def get_profile(current_user: AppUser = Depends(get_current_app_user)): """ diff --git a/app/api/v1/app_valuations/app_valuations.py b/app/api/v1/app_valuations/app_valuations.py index 71a9414..6bd27de 100644 --- a/app/api/v1/app_valuations/app_valuations.py +++ b/app/api/v1/app_valuations/app_valuations.py @@ -37,13 +37,13 @@ from app.utils.wechat_index_calculator import wechat_index_calculator app_valuations_router = APIRouter(tags=["用户端估值评估"]) -async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate): +async def _perform_valuation_calculation(user_id: int, valuation_id: int, data: UserValuationCreate): """ 后台任务:执行估值计算 """ try: start_ts = time.monotonic() - logger.info("valuation.calc_start user_id={} asset_name={} industry={}", user_id, + logger.info("valuation.calc_start user_id={} valuation_id={} asset_name={} industry={}", user_id, valuation_id, getattr(data, 'asset_name', None), getattr(data, 'industry', None)) # 根据行业查询 ESG 基准分(优先用行业名称匹配,如用的是行业代码就把 name 改成 code) @@ -84,25 +84,53 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate # 政策匹配度 input_data_by_b1["policy_match_score"] = policy_match_score - # 侵权分 默认 6 + # 法律风险/侵权记录:通过司法API查询诉讼状态 + # 评分规则:无诉讼(10分), 已解决诉讼(7分), 未解决诉讼(0分) + lawsuit_status_text = "无诉讼" # 默认无诉讼 + judicial_api_response = {} # 保存API原始返回用于日志 try: judicial_data = universal_api.query_judicial_data(data.institution) - _data = judicial_data["data"].get("target", None) # 诉讼标的 - if _data: - infringement_score = 0.0 + _data = judicial_data.get("data", {}) + judicial_api_response = _data # 保存原始返回 + target = _data.get("target", None) # 诉讼标的 + total = _data.get("total", 0) # 诉讼总数 + + if target or total > 0: + # 有诉讼记录,检查是否已解决 + settled = _data.get("settled", False) + if settled: + lawsuit_status_text = "已解决诉讼" + infringement_score = 7.0 + else: + lawsuit_status_text = "未解决诉讼" + infringement_score = 0.0 else: + lawsuit_status_text = "无诉讼" infringement_score = 10.0 - except: + + logger.info(f"法律风险查询结果: 机构={data.institution} 诉讼状态={lawsuit_status_text} 评分={infringement_score}") + except Exception as e: + logger.warning(f"法律风险查询失败: {e}") + lawsuit_status_text = "查询失败" infringement_score = 0.0 + judicial_api_response = {"error": str(e)} + input_data_by_b1["infringement_score"] = infringement_score + # 保存诉讼状态文本,用于前端展示 + lawsuit_status_for_display = lawsuit_status_text - # 获取专利信息 TODO 参数 + # 获取专利信息 + patent_api_response = {} # 保存API原始返回用于日志 + patent_matched_count = 0 + patent_years_total = 0 try: patent_data = universal_api.query_patent_info(data.industry) + patent_api_response = patent_data # 保存原始返回 except Exception as e: logger.warning("valuation.patent_api_error err={}", repr(e)) input_data_by_b1["patent_count"] = 0.0 input_data_by_b1["patent_score"] = 0.0 + patent_api_response = {"error": str(e)} patent_dict = patent_data if isinstance(patent_data, dict) else {} inner_data = patent_dict.get("data", {}) if isinstance(patent_dict.get("data", {}), dict) else {} @@ -113,16 +141,17 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate # 查询匹配申请号的记录集合 matched = [item for item in data_list if isinstance(item, dict) and item.get("SQH") == getattr(data, 'patent_application_no', None)] + patent_matched_count = len(matched) if matched: patent_count_score = min(len(matched) * 2.5, 10.0) input_data_by_b1["patent_count"] = float(patent_count_score) else: input_data_by_b1["patent_count"] = 0.0 - years_total = calculate_total_years(data_list) - if years_total > 10: + patent_years_total = calculate_total_years(data_list) + if patent_years_total > 10: patent_score = 10.0 - elif years_total >= 5: + elif patent_years_total >= 5: patent_score = 7.0 else: patent_score = 3.0 @@ -152,18 +181,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate } calculator = FinalValueACalculator() - # 先创建估值记录以获取ID,方便步骤落库关联 - initial_detail = await user_valuation_controller.create_valuation( - user_id=user_id, - data=data, - calculation_result=None, - calculation_input=None, - drp_result=None, - status='pending' - ) - valuation_id = initial_detail.id - logger.info("valuation.init_created user_id={} valuation_id={}", user_id, valuation_id) - + # 步骤1:立即更新计算输入参数(不管后续是否成功) try: await valuation_controller.update_calc( @@ -187,10 +205,9 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate # 政策匹配度 api_calc_fields["policy_matching"] = str(policy_match_score) if policy_match_score is not None else None - # 侵权记录/法律风险 - infringement_record_value = "有侵权记录" if infringement_score == 0.0 else "无侵权记录" - api_calc_fields["infringement_record"] = infringement_record_value - api_calc_fields["legal_risk"] = infringement_record_value + # 侵权记录/法律风险 - 使用实际查询到的诉讼状态 + api_calc_fields["infringement_record"] = lawsuit_status_for_display + api_calc_fields["legal_risk"] = lawsuit_status_for_display # 专利使用量 patent_count_value = input_data_by_b1.get("patent_count", 0.0) @@ -220,6 +237,130 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate except Exception as e: logger.warning("valuation.failed_to_update_api_calc_fields valuation_id={} err={}", valuation_id, repr(e)) + # 步骤1.6:记录所有API查询结果和参数映射(便于检查参数匹配) + try: + # 1. ESG评分查询记录 + await valuation_controller.log_formula_step( + valuation_id, "API_ESG_QUERY", + status="completed", + input_params={"industry": data.industry}, + output_result={"esg_score": esg_score, "source": "ESG表"} + ) + + # 2. 行业系数查询记录 + await valuation_controller.log_formula_step( + valuation_id, "API_INDUSTRY_QUERY", + status="completed", + input_params={"industry": data.industry}, + output_result={"industry_coefficient": fix_num_score, "source": "Industry表"} + ) + + # 3. 政策匹配度查询记录 + await valuation_controller.log_formula_step( + valuation_id, "API_POLICY_QUERY", + status="completed", + input_params={"industry": data.industry}, + output_result={"policy_match_score": policy_match_score, "source": "Policy表"} + ) + + # 4. 司法诉讼查询记录 + await valuation_controller.log_formula_step( + valuation_id, "API_JUDICIAL_QUERY", + status="completed", + input_params={"institution": data.institution}, + output_result={ + "api_response": judicial_api_response, # API原始返回 + "lawsuit_status": lawsuit_status_for_display, + "infringement_score": infringement_score, + "calculation": f"诉讼标的={judicial_api_response.get('target', '无')}, 诉讼总数={judicial_api_response.get('total', 0)} → {lawsuit_status_for_display} → {infringement_score}分", + "score_rule": "无诉讼:10分, 已解决:7分, 未解决:0分" + } + ) + + # 5. 专利信息查询记录 + await valuation_controller.log_formula_step( + valuation_id, "API_PATENT_QUERY", + status="completed", + input_params={ + "industry": data.industry, + "patent_application_no": data.patent_application_no + }, + output_result={ + "api_data_count": len(patent_api_response.get("data", {}).get("dataList", []) if isinstance(patent_api_response.get("data"), dict) else []), + "matched_count": patent_matched_count, + "years_total": patent_years_total, + "patent_count": input_data_by_b1.get("patent_count", 0), + "patent_score": input_data_by_b1.get("patent_score", 0), + "calculation": f"匹配专利数={patent_matched_count} → 专利数分={input_data_by_b1.get('patent_count', 0)}, 剩余年限合计={patent_years_total}年 → 专利分={patent_score}", + "score_rule": "剩余年限>10年:10分, 5-10年:7分, <5年:3分" + } + ) + + # 6. 微信指数查询记录 + await valuation_controller.log_formula_step( + valuation_id, "API_WECHAT_INDEX", + status="completed", + input_params={"asset_name": data.asset_name}, + output_result={ + "search_index_s1": input_data_by_b1.get("search_index_s1", 0), + "formula": "S1 = 微信指数 / 10" + } + ) + + # 7. 跨界合作深度映射记录 + await valuation_controller.log_formula_step( + valuation_id, "MAPPING_CROSS_BORDER_DEPTH", + status="completed", + input_params={ + "user_input": getattr(data, 'cooperation_depth', None), + "mapping": {"0":"无(0分)", "1":"品牌联名(3分)", "2":"科技载体(5分)", "3":"国家外交礼品(10分)"} + }, + output_result={"cross_border_depth": input_data_by_b2.get("cross_border_depth", 0)} + ) + + # 8. 传承人等级映射记录 + await valuation_controller.log_formula_step( + valuation_id, "MAPPING_INHERITOR_LEVEL", + status="completed", + input_params={ + "user_input": data.inheritor_level, + "mapping": {"国家级传承人":"10分", "省级传承人":"7分", "市级传承人及以下":"4分"} + }, + output_result={"inheritor_level_coefficient": input_data_by_b2.get("inheritor_level_coefficient", 0)} + ) + + # 9. 历史传承度HI计算记录 + await valuation_controller.log_formula_step( + valuation_id, "CALC_HISTORICAL_INHERITANCE", + status="completed", + input_params={ + "historical_evidence": data.historical_evidence, + "weights": {"出土实物":1.0, "古代文献":0.8, "传承人佐证":0.6, "现代研究":0.4} + }, + output_result={ + "historical_inheritance": input_data_by_b2.get("historical_inheritance", 0), + "formula": "HI = 出土实物×1.0 + 古代文献×0.8 + 传承人佐证×0.6 + 现代研究×0.4" + } + ) + + # 11. 市场风险价格波动记录 + await valuation_controller.log_formula_step( + valuation_id, "CALC_MARKET_RISK", + status="completed", + input_params={ + "price_fluctuation": data.price_fluctuation, + "highest_price": input_data_by_b3.get("highest_price", 0), + "lowest_price": input_data_by_b3.get("lowest_price", 0) + }, + output_result={ + "volatility_rule": "波动率≤5%:10分, 5-15%:5分, >15%:0分" + } + ) + + logger.info("valuation.param_mapping_logged valuation_id={}", valuation_id) + except Exception as e: + logger.warning("valuation.failed_to_log_param_mapping valuation_id={} err={}", valuation_id, repr(e)) + # 计算最终估值A(统一计算),传入估值ID以关联步骤落库 calculation_result = await calculator.calculate_complete_final_value_a(valuation_id, input_data) @@ -339,8 +480,12 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate api_calc_fields["esg_value"] = str(esg_score) if esg_score is not None else None if 'policy_match_score' in locals(): api_calc_fields["policy_matching"] = str(policy_match_score) if policy_match_score is not None else None - if 'infringement_score' in locals(): - infringement_record_value = "有侵权记录" if infringement_score == 0.0 else "无侵权记录" + if 'lawsuit_status_for_display' in locals(): + api_calc_fields["infringement_record"] = lawsuit_status_for_display + api_calc_fields["legal_risk"] = lawsuit_status_for_display + elif 'infringement_score' in locals(): + # 兼容旧逻辑 + infringement_record_value = "无诉讼" if infringement_score == 10.0 else ("已解决诉讼" if infringement_score == 7.0 else "未解决诉讼") api_calc_fields["infringement_record"] = infringement_record_value api_calc_fields["legal_risk"] = infringement_record_value if 'input_data_by_b1' in locals(): @@ -468,17 +613,30 @@ async def calculate_valuation( except Exception: pass - background_tasks.add_task(_perform_valuation_calculation, user_id, data) + # 先创建估值记录以获取ID,方便用户查询 + initial_detail = await user_valuation_controller.create_valuation( + user_id=user_id, + data=data, + calculation_result=None, + calculation_input=None, + drp_result=None, + status='pending' + ) + valuation_id = initial_detail.id - logger.info("valuation.task_queued user_id={} asset_name={} industry={}", - user_id, getattr(data, 'asset_name', None), getattr(data, 'industry', None)) + background_tasks.add_task(_perform_valuation_calculation, user_id, valuation_id, data) + + logger.info("valuation.task_queued user_id={} valuation_id={} asset_name={} industry={}", + user_id, valuation_id, getattr(data, 'asset_name', None), getattr(data, 'industry', None)) return Success( data={ "task_status": "queued", "message": "估值计算任务已提交,正在后台处理中", "user_id": user_id, - "asset_name": getattr(data, 'asset_name', None) + "asset_name": getattr(data, 'asset_name', None), + "valuation_id": valuation_id, + "order_no": str(valuation_id) } ) @@ -525,8 +683,7 @@ async def _extract_calculation_params_b1( innovation_ratio = 0.0 # 流量因子B12相关参数 - # 近30天搜索指数S1 - 从社交媒体数据计算 TODO 需要使用第三方API - baidu_index = 1 + # 近30天搜索指数S1 - 使用微信指数除以10计算 # 获取微信指数并计算近30天平均值 try: @@ -535,10 +692,9 @@ async def _extract_calculation_params_b1( logger.info(f"资产 '{data.asset_name}' 的微信指数近30天平均值: {wechat_index}") except Exception as e: logger.error(f"获取微信指数失败: {e}") - wechat_index = 1 + wechat_index = 10 # 失败时默认值,使得 S1 = 1 - weibo_index = 1 - search_index_s1 = calculate_search_index_s1(baidu_index, wechat_index, weibo_index) # 默认值,实际应从API获取 + search_index_s1 = calculate_search_index_s1(wechat_index) # S1 = 微信指数 / 10 # 行业均值S2 - 从数据库查询行业数据计算 from app.utils.industry_calculator import calculate_industry_average_s2 @@ -585,6 +741,7 @@ async def _extract_calculation_params_b1( 'likes': safe_float(info["likes"]), 'comments': safe_float(info["comments"]), 'shares': safe_float(info["shares"]), + 'views': safe_float(info.get("views", 0)), # followers 非当前计算用键,先移除避免干扰 # click_count 与 view_count 目前未参与计算,先移除 @@ -629,10 +786,18 @@ async def _extract_calculation_params_b2(data: UserValuationCreate) -> Dict[str, kuaishou_views = safe_float(rs.get("kuaishou", None).get("likes", 0)) if rs.get("kuaishou", None) else 0 bilibili_views = safe_float(rs.get("bilibili", None).get("likes", 0)) if rs.get("bilibili", None) else 0 - # 跨界合作深度:将枚举映射为项目数;若为数值字符串则直接取数值 + # 跨界合作深度:将枚举映射为分值 + # 前端传入的是数字字符串 ("0", "1", "2", "3"),后端也支持中文标签 try: val = getattr(data, 'cooperation_depth', None) mapping = { + # 前端传入的数字字符串 + "0": 0.0, # 无 + "1": 3.0, # 品牌联名 + "2": 5.0, # 科技载体 + "3": 10.0, # 国家外交礼品 + # 兼容中文标签(以防其他入口传入) + "无": 0.0, "品牌联名": 3.0, "科技载体": 5.0, "国家外交礼品": 10.0, @@ -647,14 +812,28 @@ async def _extract_calculation_params_b2(data: UserValuationCreate) -> Dict[str, # 纹样基因值B22相关参数 # 以下三项需由后续模型/服务计算;此处提供默认可计算占位 - # # 历史传承度HI(用户填写) + # HI = 证据数量 × 对应权重后加总 + # 权重分配:出土实物(1.0) + 古代文献(0.8) + 传承人佐证(0.6) + 现代研究(0.4) + # 示例: (2*1 + 5*0.8 + 5*0.6 + 6*0.4) = 11.4 historical_inheritance = 0.0 try: + evidence_weights = { + "artifacts": 1.0, # 出土实物 + "ancient_literature": 0.8, # 古代文献 + "inheritor_testimony": 0.6, # 传承人佐证 + "modern_research": 0.4, # 现代研究 + } if isinstance(data.historical_evidence, dict): - historical_inheritance = sum([safe_float(v) for v in data.historical_evidence.values()]) + for key, weight in evidence_weights.items(): + count = safe_float(data.historical_evidence.get(key, 0)) + historical_inheritance += count * weight elif isinstance(data.historical_evidence, (list, tuple)): - historical_inheritance = sum([safe_float(i) for i in data.historical_evidence]) + # 列表顺序:[出土实物, 古代文献, 传承人佐证, 现代研究] + weights = [1.0, 0.8, 0.6, 0.4] + for i, weight in enumerate(weights): + if i < len(data.historical_evidence): + historical_inheritance += safe_float(data.historical_evidence[i]) * weight except Exception: historical_inheritance = 0.0 structure_complexity = 1.5 # 默认值 纹样基因熵值B22(系统计算) @@ -686,17 +865,36 @@ async def _extract_calculation_params_b2(data: UserValuationCreate) -> Dict[str, } -# 获取 文化价值B2 相关参数 +# 获取 风险调整系数B3 相关参数 async def _extract_calculation_params_b3(data: UserValuationCreate) -> Dict[str, Any]: - # 过去30天最高价格 过去30天最低价格 TODO 需要根据字样进行切分获取最高价和最低价 转换成 float 类型 + # 过去30天最高价格 过去30天最低价格 price_fluctuation = [float(i) for i in data.price_fluctuation] highest_price, lowest_price = max(price_fluctuation), min(price_fluctuation) - # lawsuit_status = "无诉讼" # 诉讼状态 TODO (API获取) - inheritor_ages = [float(i) for i in data.inheritor_age_count] # [45, 60, 75] # 传承人年龄列表 + + # 传承风险:根据各年龄段传承人数量计算 + # 前端传入: inheritor_age_count = [≤50岁人数, 50-70岁人数, ≥70岁人数] + # 评分规则: ≤50岁(10分), 50-70岁(5分), >70岁(0分),取有传承人的最高分 + inheritor_age_count = data.inheritor_age_count or [0, 0, 0] + + # 根据年龄段人数生成虚拟年龄列表(用于风险计算) + # 如果有≤50岁的传承人,添加一个45岁的代表 + # 如果有50-70岁的传承人,添加一个60岁的代表 + # 如果有>70岁的传承人,添加一个75岁的代表 + inheritor_ages = [] + if len(inheritor_age_count) > 0 and safe_float(inheritor_age_count[0]) > 0: + inheritor_ages.append(45) # ≤50岁代表 → 10分 + if len(inheritor_age_count) > 1 and safe_float(inheritor_age_count[1]) > 0: + inheritor_ages.append(60) # 50-70岁代表 → 5分 + if len(inheritor_age_count) > 2 and safe_float(inheritor_age_count[2]) > 0: + inheritor_ages.append(75) # >70岁代表 → 0分 + + # 如果没有任何传承人,默认给一个高风险年龄 + if not inheritor_ages: + inheritor_ages = [75] # 默认高风险 + return { "highest_price": highest_price, "lowest_price": lowest_price, - "inheritor_ages": inheritor_ages, } @@ -791,6 +989,7 @@ async def _extract_calculation_params_c(data: UserValuationCreate) -> Dict[str, "expert_valuations": expert_valuations, # 专家估值列表 (系统配置) # 计算热度系数C2 "daily_browse_volume": daily_browse_volume, # 近7日日均浏览量 (API获取) + "platform_views": daily_browse_volume, # 从 platform_accounts/views 或 link_views 获取的浏览量 "collection_count": collection_count, # 收藏数 "issuance_level": circulation, # 默认 限量发行 计算稀缺性乘数C3 "recent_market_activity": recent_market_activity, # 默认 '近一月' 计算市场估值C diff --git a/app/api/v1/calculation/calcuation.py b/app/api/v1/calculation/calcuation.py index 73c66ce..6c2fe2a 100644 --- a/app/api/v1/calculation/calcuation.py +++ b/app/api/v1/calculation/calcuation.py @@ -276,11 +276,9 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str, # 流量因子B12相关参数 - # 近30天搜索指数S1 - 从社交媒体数据计算 TODO 需要使用第三方API - baidu_index = 0.0 - wechat_index = wechat_index_calculator.process_wechat_index_response(universal_api.wx_index(data.asset_name)) # 通过资产信息获取微信指数 TODO 这里返回的没确认指数参数,有可能返回的图示是指数信息 - weibo_index = 0.0 - search_index_s1 = calculate_search_index_s1(baidu_index,wechat_index,weibo_index) # 默认值,实际应从API获取 + # 近30天搜索指数S1 - 使用微信指数除以10计算 + wechat_index = wechat_index_calculator.process_wechat_index_response(universal_api.wx_index(data.asset_name)) # 通过资产信息获取微信指数 + search_index_s1 = calculate_search_index_s1(wechat_index) # S1 = 微信指数 / 10 # 行业均值S2 TODO 系统内置 未找到相关内容 industry_average_s2 = 0.0 # 社交媒体传播度S3 - TODO 需要使用第三方API,click_count view_count 未找到对应参数 @@ -344,8 +342,22 @@ async def _extract_calculation_params_b2(data: UserValuationCreate) -> Dict[str, douyin_views = 0 kuaishou_views= 0 bilibili_views= 0 - # 跨界合作深度 品牌联名0.3,科技载体0.5,国家外交礼品1.0 - cross_border_depth = float(data.cooperation_depth) + # 跨界合作深度:将枚举映射为分值 + # 前端传入的是数字字符串 ("0", "1", "2", "3"),后端也支持中文标签 + depth_mapping = { + # 前端传入的数字字符串 + "0": 0.0, # 无 + "1": 3.0, # 品牌联名 + "2": 5.0, # 科技载体 + "3": 10.0, # 国家外交礼品 + # 兼容中文标签(以防其他入口传入) + "无": 0.0, + "品牌联名": 3.0, + "科技载体": 5.0, + "国家外交礼品": 10.0, + } + depth_val = str(data.cooperation_depth) if data.cooperation_depth else "0" + cross_border_depth = depth_mapping.get(depth_val, 0.0) # 纹样基因值B22相关参数 diff --git a/app/api/v1/sms/sms.py b/app/api/v1/sms/sms.py index 501964b..15ca57a 100644 --- a/app/api/v1/sms/sms.py +++ b/app/api/v1/sms/sms.py @@ -135,6 +135,7 @@ async def verify_code(payload: VerifyCodeRequest) -> BasicResponse[dict]: from app.settings import settings if settings.SMS_BYPASS_CODE and payload.code == settings.SMS_BYPASS_CODE: logger.info("sms.verify_code bypass phone={}", payload.phone) + store.mark_verified(payload.phone) return Success(data={"status": "OK", "message": "verified"}) ok, reason = store.can_verify(payload.phone) if not ok: @@ -154,6 +155,7 @@ async def verify_code(payload: VerifyCodeRequest) -> BasicResponse[dict]: store.clear_code(payload.phone) store.reset_failures(payload.phone) logger.info("sms.verify_code success phone={}", payload.phone) + store.mark_verified(payload.phone) return Success(data={"status": "OK", "message": "verified"}) diff --git a/app/controllers/app_user.py b/app/controllers/app_user.py index f1eb690..e9412fb 100644 --- a/app/controllers/app_user.py +++ b/app/controllers/app_user.py @@ -21,6 +21,15 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc # 检查手机号是否已存在 existing_user = await self.model.filter(phone=register_data.phone).first() if existing_user: + if getattr(existing_user, "is_deleted", False): + default_password = register_data.phone[-6:] + hashed_password = get_password_hash(default_password) + existing_user.is_deleted = False + existing_user.deleted_at = None + existing_user.is_active = True + existing_user.password = hashed_password + await existing_user.save() + return existing_user raise HTTPException(status_code=400, detail="手机号已存在") # 生成默认密码:手机号后六位 @@ -42,7 +51,7 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc 用户认证 """ user = await self.model.filter( - phone=login_data.phone, is_active=True + phone=login_data.phone, is_active=True, is_deleted=False ).first() if not user: @@ -57,13 +66,13 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc """ 根据ID获取用户 """ - return await self.model.filter(id=user_id, is_active=True).first() + return await self.model.filter(id=user_id, is_active=True, is_deleted=False).first() async def get_user_by_phone(self, phone: str) -> Optional[AppUser]: """ 根据手机号获取用户 """ - return await self.model.filter(phone=phone, is_active=True).first() + return await self.model.filter(phone=phone, is_active=True, is_deleted=False).first() async def update_last_login(self, user_id: int) -> bool: """ @@ -149,6 +158,27 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc return True return False + async def delete_user_account(self, user_id: int) -> bool: + user = await self.model.filter(id=user_id).first() + if not user: + return False + user.is_active = False + user.is_deleted = True + user.deleted_at = datetime.now() + masked = f"deleted_{user.id}" + user.username = None + user.alias = None + user.email = None + user.password = "" + user.company_name = None + user.company_address = None + user.company_contact = None + user.company_phone = None + user.company_email = None + user.phone = masked + await user.save() + return True + # 创建控制器实例 app_user_controller = AppUserController() diff --git a/app/controllers/valuation.py b/app/controllers/valuation.py index 023b9e3..3c3c03d 100644 --- a/app/controllers/valuation.py +++ b/app/controllers/valuation.py @@ -444,6 +444,30 @@ class ValuationController: lines.append("```") lines.append("") + # 计算过程部分(显示详细的计算步骤) + if output_result and isinstance(output_result, dict): + # 首先检查 calculation_detail 字段 + calculation_detail = output_result.get('calculation_detail') + if calculation_detail and isinstance(calculation_detail, dict): + lines.append("**计算过程:**") + lines.append("") + # 按步骤顺序显示 + steps = [] + for key in sorted(calculation_detail.keys()): + if key.startswith('step'): + steps.append(f"> {calculation_detail[key]}") + if steps: + lines.extend(steps) + lines.append("") + + # 然后检查旧的 calculation 字段 + calculation = output_result.get('calculation') + if calculation and not calculation_detail: + lines.append("**计算过程:**") + lines.append("") + lines.append(f"> {calculation}") + lines.append("") + # 结果部分 if output_result: # 提取主要结果值 @@ -521,6 +545,14 @@ class ValuationController: if not output_result or not isinstance(output_result, dict): return None + # 调试:打印B3的output_result + if 'risk_value_b3' in str(output_result) or 'legal_risk' in str(output_result): + print(f"=== _extract_main_result 调试 ===") + print(f"formula_name: {formula_name}") + print(f"output_result keys: {list(output_result.keys())}") + print(f"output_result: {output_result}") + print(f"================================") + # 移除 duration_ms 等元数据字段 filtered_result = {k: v for k, v in output_result.items() if k not in ['duration_ms', 'duration', 'timestamp', 'status']} @@ -538,6 +570,47 @@ class ValuationController: else: return json.dumps(value, ensure_ascii=False) + # 优先查找常见的结果字段(优先级从高到低) + # 这个列表的顺序很重要,确保正确的结果字段优先被选中 + common_result_keys = [ + # 计算引擎实际使用的结果字段名 + 'risk_value_b3', # 风险调整系数B3 + 'risk_adjustment_b3', # 风险调整系数B3(备选) + 'economic_value_b1', # 经济价值B1 + 'cultural_value_b2', # 文化价值B2 + 'model_value_b', # 模型估值B + 'market_value_c', # 市场估值C + 'final_value_a', # 最终估值A + 'final_value_ab', # 最终估值AB + 'basic_value_b11', # 基础价值B11 + 'traffic_factor_b12', # 流量因子B12 + 'policy_multiplier_b13', # 政策乘数B13 + 'living_heritage_b21', # 活态传承系数B21 + 'pattern_gene_b22', # 纹样基因值B22 + # 通用结果字段 + 'result', 'value', 'output', 'final_value', 'calculated_value', + # 子计算结果字段 + 'financial_value_f', 'legal_strength_l', 'development_potential_d', + 'social_media_spread_s3', 'interaction_index', 'coverage_index', 'conversion_efficiency', + 'market_bid_c1', 'heat_coefficient_c2', 'scarcity_multiplier_c3', 'timeliness_decay_c4', + 'teaching_frequency', 'inheritor_level_coefficient', + 'risk_score_sum', # 风险评分总和R + 'dynamic_pledge_rate', # 动态质押率 + ] + + # 首先检查常见结果字段(这个优先级最高,避免错误匹配子风险值) + for key in common_result_keys: + if key in filtered_result: + value = filtered_result[key] + if isinstance(value, (int, float)): + if 'risk' in formula_name.lower() or 'b3' in formula_name.lower(): + print(f"=== 返回值调试 (common_keys) ===") + print(f"formula_name: {formula_name}") + print(f"matched key: {key}") + print(f"返回值: {value}") + print(f"================================") + return str(value) + # 尝试根据公式名称匹配字段 # 例如:"财务价值 F" -> 查找 "financial_value_f", "财务价值F" 等 # 提取公式名称中的关键部分(通常是最后一个字母或单词) @@ -577,14 +650,6 @@ class ValuationController: if isinstance(value, (int, float)): return str(value) - # 查找常见的结果字段 - common_result_keys = ['result', 'value', 'output', 'final_value', 'calculated_value'] - for key in common_result_keys: - if key in filtered_result: - value = filtered_result[key] - if isinstance(value, (int, float)): - return str(value) - # 返回第一个数值类型的值 for key, value in filtered_result.items(): if isinstance(value, (int, float)): diff --git a/app/core/token_blacklist.py b/app/core/token_blacklist.py new file mode 100644 index 0000000..6faf147 --- /dev/null +++ b/app/core/token_blacklist.py @@ -0,0 +1,13 @@ +from datetime import datetime +from typing import Optional + +from app.models.token_blacklist import TokenBlacklist + + +async def add_to_blacklist(token: str, user_id: int, exp: Optional[datetime] = None, jti: Optional[str] = None) -> None: + await TokenBlacklist.create(token=token, user_id=user_id, exp=exp, jti=jti) + + +async def is_blacklisted(token: str) -> bool: + return await TokenBlacklist.filter(token=token).exists() + diff --git a/app/models/__init__.py b/app/models/__init__.py index 6a76223..3e05114 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -6,4 +6,5 @@ from .industry import * from .policy import * from .user import * from .valuation import * -from .invoice import * \ No newline at end of file +from .invoice import * +from .token_blacklist import * diff --git a/app/models/token_blacklist.py b/app/models/token_blacklist.py new file mode 100644 index 0000000..791aa4e --- /dev/null +++ b/app/models/token_blacklist.py @@ -0,0 +1,15 @@ +from tortoise import fields + +from .base import BaseModel, TimestampMixin + + +class TokenBlacklist(BaseModel, TimestampMixin): + token = fields.TextField(description="JWT令牌") + jti = fields.CharField(max_length=64, null=True, description="令牌唯一ID", index=True) + user_id = fields.IntField(description="用户ID", index=True) + exp = fields.DatetimeField(null=True, description="过期时间", index=True) + + class Meta: + table = "token_blacklist" + table_description = "JWT令牌黑名单" + diff --git a/app/models/user.py b/app/models/user.py index 0fd6202..fc89308 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -21,6 +21,8 @@ class AppUser(BaseModel, TimestampMixin): last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True) remaining_quota = fields.IntField(default=0, description="剩余估值次数", index=True) notes = fields.CharField(max_length=256, null=True, description="备注") + is_deleted = fields.BooleanField(default=False, description="是否已注销", index=True) + deleted_at = fields.DatetimeField(null=True, description="注销时间", index=True) class Meta: table = "app_user" @@ -38,4 +40,4 @@ class AppUserQuotaLog(BaseModel, TimestampMixin): class Meta: table = "app_user_quota_log" - table_description = "App用户估值次数操作日志" \ No newline at end of file + table_description = "App用户估值次数操作日志" diff --git a/app/services/sms_store.py b/app/services/sms_store.py index b015dc8..83f5293 100644 --- a/app/services/sms_store.py +++ b/app/services/sms_store.py @@ -28,6 +28,7 @@ class VerificationStore: self.codes: Dict[str, Tuple[str, float]] = {} self.sends: Dict[str, Dict[str, float]] = {} self.failures: Dict[str, Dict[str, float]] = {} + self.verified: Dict[str, float] = {} def generate_code(self) -> str: """生成数字验证码 @@ -144,5 +145,13 @@ class VerificationStore: """ self.failures.pop(phone, None) + def mark_verified(self, phone: str, ttl_seconds: int = 300) -> None: + until = time.time() + ttl_seconds + self.verified[phone] = until -store = VerificationStore() \ No newline at end of file + def is_recently_verified(self, phone: str) -> bool: + until = self.verified.get(phone, 0.0) + return until > time.time() + + +store = VerificationStore() diff --git a/app/utils/app_user_jwt.py b/app/utils/app_user_jwt.py index 4a8ea12..c127b48 100644 --- a/app/utils/app_user_jwt.py +++ b/app/utils/app_user_jwt.py @@ -3,6 +3,7 @@ from typing import Optional import jwt from fastapi import HTTPException, status, Depends, Header from app.controllers.app_user import app_user_controller +from app.core.token_blacklist import is_blacklisted from app.schemas.app_user import AppUserJWTPayload from app.settings import settings @@ -48,18 +49,24 @@ def verify_app_user_token(token: str) -> Optional[AppUserJWTPayload]: return None -def get_current_app_user_id(token: str = Header(None)) -> int: +async def get_current_app_user_id(token: str = Header(None)) -> int: """ 从令牌中获取当前AppUser ID """ credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="无效的认证凭据", + detail="未登录,请重新登录", headers={"WWW-Authenticate": "Bearer"}, ) if not token: raise credentials_exception + if token and token != "dev": + try: + if await is_blacklisted(token): + raise credentials_exception + except Exception: + pass payload = verify_app_user_token(token) if payload is None: @@ -80,4 +87,4 @@ async def get_current_app_user( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在或已被停用" ) - return user \ No newline at end of file + return user diff --git a/app/utils/calculation_engine/cultural_value_b2/sub_formulas/__init__.py b/app/utils/calculation_engine/cultural_value_b2/sub_formulas/__init__.py index ccdfd26..120ddbe 100644 --- a/app/utils/calculation_engine/cultural_value_b2/sub_formulas/__init__.py +++ b/app/utils/calculation_engine/cultural_value_b2/sub_formulas/__init__.py @@ -9,7 +9,7 @@ 2. pattern_gene_b22: 纹样基因值B22计算 - 结构复杂度SC = Σ(元素权重 × 复杂度系数) / 总元素数 - 归一化信息熵H = -Σ(p_i × log2(p_i)) / log2(n) - - 历史传承度HI = 传承年限权重 × 0.4 + 文化意义权重 × 0.3 + 保护状况权重 × 0.3 + - 历史传承度HI = 出土实物×1.0 + 古代文献×0.8 + 传承人佐证×0.6 + 现代研究×0.4 - 纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10 - 文化价值B2 = 活态传承系数B21 × 0.6 + (纹样基因值B22 / 10) × 0.4 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 add801b..af050b5 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 @@ -277,26 +277,19 @@ def calculate_heat_score(daily_views: float, favorites: int) -> float: return 0.0 # 30天搜索指数S1 -def calculate_search_index_s1(baidu_index: float, - wechat_index: float, - weibo_index: float) -> float: +def calculate_search_index_s1(wechat_index: float) -> float: """ 计算近30天搜索指数S1 - 近30天搜索指数S1 = 百度搜索指数 × 0.4 + 微信搜索指数 × 0.3 + 微博搜索指数 × 0.3 + 近30天搜索指数S1 = 微信指数 / 10 args: - baidu_index: 百度搜索指数 (API获取) wechat_index: 微信搜索指数 (API获取) - weibo_index: 微博搜索指数 (API获取) returns: float: 近30天搜索指数S1 """ - # - search_index = (baidu_index * 0.4 + - wechat_index * 0.3 + - weibo_index * 0.3) + search_index = wechat_index / 10.0 return search_index # 示例使用 @@ -306,10 +299,8 @@ if __name__ == "__main__": processor = PlatformDataProcessor() # 示例数据 - # 搜索指数数据 (API获取) - baidu_index = 6000.0 + # 微信指数数据 (API获取) wechat_index = 4500.0 - weibo_index = 3000.0 # 行业均值 (系统配置) industry_average = 5000.0 @@ -339,7 +330,7 @@ if __name__ == "__main__": view_count = 200 # 计算各项指标 - search_index_s1 = calculate_search_index_s1(baidu_index, wechat_index, weibo_index) + search_index_s1 = calculate_search_index_s1(wechat_index) # S1 = 微信指数 / 10 interaction_index, coverage_index = processor.calculate_multi_platform_interaction(platform_data) conversion_efficiency = calculator.calculate_conversion_efficiency(click_count, view_count) # 互动量指数 × 0.4 + 覆盖人群指数 × 0.3 + 转化效率 × 0.3 diff --git a/app/utils/calculation_engine/formula_registry.py b/app/utils/calculation_engine/formula_registry.py index e216ca2..16c3fca 100644 --- a/app/utils/calculation_engine/formula_registry.py +++ b/app/utils/calculation_engine/formula_registry.py @@ -231,6 +231,86 @@ FORMULA_TREE: List[FormulaTreeNode] = [ "40", group="DYNAMIC_PLEDGE", ), + # API查询结果记录 + _node( + "API_ESG_QUERY", + "ESG评分查询", + "根据行业名称查询ESG基准分", + "50.1", + group="API_QUERY", + ), + _node( + "API_INDUSTRY_QUERY", + "行业系数查询", + "根据行业名称查询行业修正系数I", + "50.2", + group="API_QUERY", + ), + _node( + "API_POLICY_QUERY", + "政策匹配度查询", + "根据行业名称查询政策匹配度评分", + "50.3", + group="API_QUERY", + ), + _node( + "API_JUDICIAL_QUERY", + "司法诉讼查询", + "根据机构名称查询诉讼状态,映射为法律风险评分(无诉讼:10分, 已解决:7分, 未解决:0分)", + "50.4", + group="API_QUERY", + ), + _node( + "API_PATENT_QUERY", + "专利信息查询", + "根据专利申请号查询专利数量和剩余年限,计算专利评分", + "50.5", + group="API_QUERY", + ), + _node( + "API_WECHAT_INDEX", + "微信指数查询", + "根据资产名称查询微信指数,计算搜索指数S1 = 微信指数 / 10", + "50.6", + group="API_QUERY", + ), + # 参数映射记录 + _node( + "MAPPING_CROSS_BORDER_DEPTH", + "跨界合作深度映射", + "用户选项映射为评分:无(0分), 品牌联名(3分), 科技载体(5分), 国家外交礼品(10分)", + "51.1", + group="PARAM_MAPPING", + ), + _node( + "MAPPING_INHERITOR_LEVEL", + "传承人等级映射", + "用户选项映射为系数:国家级(10分), 省级(7分), 市级及以下(4分)", + "51.2", + group="PARAM_MAPPING", + ), + # 权重计算记录 + _node( + "CALC_HISTORICAL_INHERITANCE", + "历史传承度计算", + "HI = 出土实物×1.0 + 古代文献×0.8 + 传承人佐证×0.6 + 现代研究×0.4", + "52.1", + group="PARAM_CALC", + ), + _node( + "CALC_INHERITANCE_RISK", + "传承风险年龄转换", + "根据各年龄段传承人数量计算传承风险评分:≤50岁(10分), 50-70岁(5分), >70岁(0分), 取最高分", + "52.2", + group="PARAM_CALC", + ), + _node( + "CALC_MARKET_RISK", + "市场风险价格波动", + "根据30天价格波动计算市场风险评分:波动率≤5%(10分), 5-15%(5分), >15%(0分)", + "52.3", + group="PARAM_CALC", + ), ], ) ] diff --git a/app/utils/calculation_engine/risk_adjustment_b3/sub_formulas/risk_adjustment_b3.py b/app/utils/calculation_engine/risk_adjustment_b3/sub_formulas/risk_adjustment_b3.py index eca281d..3a39ae6 100644 --- a/app/utils/calculation_engine/risk_adjustment_b3/sub_formulas/risk_adjustment_b3.py +++ b/app/utils/calculation_engine/risk_adjustment_b3/sub_formulas/risk_adjustment_b3.py @@ -222,7 +222,10 @@ class RiskAdjustmentB3Calculator: valuation_id, "MODEL_B_RISK_B3_INHERITANCE", status="completed", - input_params={"inheritor_ages": input_data.get("inheritor_ages")}, + input_params={ + "inheritor_ages": input_data.get("inheritor_ages"), + "score_rule": "≤50岁:10分, 50-70岁:5分, >70岁:0分, 取最高分" + }, output_result={'inheritance_risk': inheritance_risk}, ) @@ -232,12 +235,30 @@ class RiskAdjustmentB3Calculator: # 计算风险调整系数B3 risk_adjustment_b3 = self.calculate_risk_adjustment_b3(risk_score_sum) + # 调试输出:打印B3计算的关键值 + print(f"=== B3计算调试 ===") + print(f"市场风险: {market_risk}") + print(f"法律风险: {legal_risk}") + print(f"传承风险: {inheritance_risk}") + print(f"风险评分总和R: {risk_score_sum}") + print(f"风险调整系数B3: {risk_adjustment_b3}") + print(f"=== B3计算完成 ===") + result = { "risk_value_b3": risk_adjustment_b3, "risk_score_sum": risk_score_sum, "market_risk": market_risk, "legal_risk": legal_risk, "inheritance_risk": inheritance_risk, + # 详细计算过程 + "calculation_detail": { + "step1_market_risk": f"市场风险 = {market_risk}分 (波动率评分)", + "step2_legal_risk": f"法律风险 = {legal_risk}分 (诉讼状态评分)", + "step3_inheritance_risk": f"传承风险 = {inheritance_risk}分 (年龄评分)", + "step4_risk_score_sum": f"R = ({market_risk}×0.3 + {legal_risk}×0.4 + {inheritance_risk}×0.3) / 10 = {risk_score_sum}", + "step5_risk_adjustment_b3": f"B3 = 0.8 + {risk_score_sum}×0.4 = {risk_adjustment_b3}", + "formula": "风险调整系数B3 = 0.8 + R × 0.4, R = (市场风险×0.3 + 法律风险×0.4 + 传承风险×0.3) / 10" + } } await self.valuation_controller.log_formula_step( valuation_id, diff --git a/menu_init.sql b/menu_init.sql deleted file mode 100644 index f7582f1..0000000 --- a/menu_init.sql +++ /dev/null @@ -1,74 +0,0 @@ --- 完整菜单初始化SQL --- 创建时间: 2025-11-20 --- 说明: 包含所有新增的菜单项和权限分配 - --- ======================================== --- 1. 工作台菜单 --- ======================================== -INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at) -VALUES - (22, '工作台', 'menu', 'carbon:dashboard', '/workbench', 1, 0, 0, '/workbench', 1, NULL, datetime('now'), datetime('now')); - --- ======================================== --- 2. 交易管理菜单 --- ======================================== --- 插入一级目录:交易管理 -INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at) -VALUES - (16, '交易管理', 'catalog', 'carbon:receipt', '/transaction', 3, 0, 0, 'Layout', 0, '/transaction/invoice', datetime('now'), datetime('now')); - --- 插入二级菜单:交易管理 -INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at) -VALUES - (17, '交易管理', 'menu', 'carbon:document', 'invoice', 1, 16, 0, '/transaction/invoice', 0, NULL, datetime('now'), datetime('now')); - --- ======================================== --- 3. 估值管理菜单 --- ======================================== --- 插入一级目录:估值管理 -INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at) -VALUES - (18, '估值管理', 'catalog', 'carbon:calculator', '/valuation', 4, 0, 0, 'Layout', 0, '/valuation/audit', datetime('now'), datetime('now')); - --- 插入二级菜单:审核列表 -INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at) -VALUES - (19, '审核列表', 'menu', 'carbon:task-approved', 'audit', 1, 18, 0, '/valuation/audit', 0, NULL, datetime('now'), datetime('now')); - --- ======================================== --- 4. 用户管理菜单 --- ======================================== --- 插入一级目录:用户管理 -INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at) -VALUES - (20, '用户管理', 'catalog', 'carbon:user-multiple', '/user-management', 5, 0, 0, 'Layout', 0, '/user-management/user-list', datetime('now'), datetime('now')); - --- 插入二级菜单:用户列表 -INSERT INTO menu (id, name, menu_type, icon, path, "order", parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at) -VALUES - (21, '用户列表', 'menu', 'carbon:user', 'user-list', 1, 20, 0, '/user-management/user-list', 0, NULL, datetime('now'), datetime('now')); - --- ======================================== --- 角色权限分配 --- ======================================== - --- 为管理员角色(role_id=1)分配所有菜单权限 -INSERT INTO role_menu (role_id, menu_id) -VALUES - (1, 22), -- 工作台 - (1, 16), -- 交易管理 - (1, 17), -- 交易管理 - (1, 18), -- 估值管理 - (1, 19), -- 审核列表 - (1, 20), -- 用户管理 - (1, 21); -- 用户列表 - --- 为普通用户角色(role_id=2)分配基础菜单权限 -INSERT INTO role_menu (role_id, menu_id) -VALUES - (2, 22), -- 工作台 - (2, 16), -- 交易管理 - (2, 17), -- 交易管理 - (2, 18), -- 估值管理 - (2, 19); -- 审核列表 - -- 注意:普通用户不分配用户管理权限 \ No newline at end of file diff --git a/scripts/api_smoke_test.py b/scripts/api_smoke_test.py index cabc7fa..79effb4 100644 --- a/scripts/api_smoke_test.py +++ b/scripts/api_smoke_test.py @@ -189,7 +189,7 @@ def build_sample_payload() -> Dict[str, Any]: "market_activity_time": "近一周", "monthly_transaction_amount": "月交易额>100万<500万", "platform_accounts": { - "douyin": {"account": "成都文交所", "likes": "500000", "comments": "89222", "shares": "97412"} + "douyin": {"account": "成都文交所", "likes": "500000", "comments": "89222", "shares": "97412", "views": "100000"} } } # 若 application_coverage 为占位,则用 coverage_area 回填 diff --git a/scripts/user_flow_test.py b/scripts/user_flow_test.py index 70025cb..4d2b29d 100644 --- a/scripts/user_flow_test.py +++ b/scripts/user_flow_test.py @@ -346,6 +346,18 @@ async def main() -> None: d_ok = (d_code == 200 and isinstance(d_data, dict) and d_data.get("data", {}).get("deleted")) results.append({"name": "删除估值", "status": "PASS" if d_ok else "FAIL", "message": "删除成功" if d_ok else "删除失败", "detail": {"http": d_code, "body": d_data}}) + # 注销账号 + da_resp = await client.delete(make_url(base, "/app-user/account"), headers={"token": use_token}) + da_code = da_resp.status_code + da_data = _ensure_dict(da_resp.json() if da_resp.headers.get("content-type", "").startswith("application/json") else {"raw": da_resp.text}) + da_ok = (da_code == 200 and isinstance(da_data, dict)) + results.append({"name": "注销账号", "status": "PASS" if da_ok else "FAIL", "message": "注销成功" if da_ok else "注销失败", "detail": {"http": da_code, "body": da_data}}) + + # 注销后旧token访问应失败 + vt2_code, vt2_data = await api_get(client, make_url(base, "/app-user/validate-token"), headers={"token": use_token}) + vt2_ok = (vt2_code in (401, 403)) + results.append({"name": "注销后token访问", "status": "PASS" if vt2_ok else "FAIL", "message": "拒绝访问" if vt2_ok else "未拒绝", "detail": {"http": vt2_code, "body": vt2_data}}) + # 登出 lo_code, lo_data = await api_post_json(client, make_url(base, "/app-user/logout"), {}, headers={"token": use_token}) lo_ok = (lo_code == 200) @@ -378,4 +390,4 @@ async def api_put_json(client: httpx.AsyncClient, url: str, payload: Dict[str, A data = r.json() except Exception: data = {"raw": r.text} - return r.status_code, data \ No newline at end of file + return r.status_code, data diff --git a/smtp_test_output.txt b/smtp_test_output.txt deleted file mode 100644 index e69de29..0000000 diff --git a/web/src/utils/report.js b/web/src/utils/report.js index 471eb1a..ed5ee9d 100644 --- a/web/src/utils/report.js +++ b/web/src/utils/report.js @@ -237,7 +237,8 @@ export const generateReport = async (detailData) => { if (calcResult.model_value_b !== undefined) data.B = formatNumberValue(calcResult.model_value_b) if (calcResult.economic_value_b1 !== undefined) data.B1 = formatNumberValue(calcResult.economic_value_b1) if (calcResult.cultural_value_b2 !== undefined) data.B2 = formatNumberValue(calcResult.cultural_value_b2) - if (calcResult.risk_adjustment_b3 !== undefined) data.B3 = calcResult.risk_adjustment_b3 + if (calcResult.risk_value_b3 !== undefined) data.B3 = calcResult.risk_value_b3 + else if (calcResult.risk_adjustment_b3 !== undefined) data.B3 = calcResult.risk_adjustment_b3 if (calcResult.market_value_c !== undefined) data.C = formatNumberValue(calcResult.market_value_c) // ========== 6.7 添加动态质押率(DPR) ========== diff --git a/web1/dist 2.zip b/web1/dist 2.zip new file mode 100644 index 0000000..91ea42d Binary files /dev/null and b/web1/dist 2.zip differ diff --git a/估值字段.txt b/估值字段.txt index 8566973..07fef1a 100644 --- a/估值字段.txt +++ b/估值字段.txt @@ -37,8 +37,8 @@ export DOCKER_DEFAULT_PLATFORM=linux/amd64 -docker build -t zfc931912343/guzhi-fastapi-admin:v3.1 . -docker push zfc931912343/guzhi-fastapi-admin:v3.1 +docker build -t zfc931912343/guzhi-fastapi-admin:v3.8 . +docker push zfc931912343/guzhi-fastapi-admin:v3.8 # 运行容器 @@ -68,12 +68,12 @@ 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:v3.8 && 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:v3.8 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: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 + docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.8 && docker rm -f guzhi_pro && docker run -itd --name=guzhi_pro -p 8080:9999 -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.8 + docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.3 && 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.3 \ No newline at end of file diff --git a/部署文档.md b/部署文档.md new file mode 100644 index 0000000..dcbf81d --- /dev/null +++ b/部署文档.md @@ -0,0 +1,225 @@ +# 非遗资产估值系统 - 部署文档 + +## 项目概述 + +非遗资产估值系统是一个基于 Vue.js + FastAPI 的全栈应用,用于非物质文化遗产资产的价值评估。 + +- **前端**: Vue.js + Vite + pnpm +- **后端**: Python 3.11 + FastAPI + Tortoise ORM +- **数据库**: MySQL +- **容器化**: Docker + +--- + +## 目录结构 + +``` +youshu-guzhi/ +├── app/ # 后端 FastAPI 应用 +│ ├── api/ # API 路由 +│ ├── controllers/ # 业务控制器 +│ ├── models/ # 数据库模型 +│ ├── schemas/ # Pydantic 数据模型 +│ ├── settings/ # 配置文件 +│ └── utils/ # 工具函数和计算引擎 +├── web/ # 前端 Vue.js 应用 +├── deploy/ # 部署相关文件 +│ ├── entrypoint.sh # 容器启动脚本 +│ └── web.conf # Nginx 配置 +├── Dockerfile # Docker 构建文件 +├── requirements.txt # Python 依赖 +└── run.py # 应用启动入口 +``` + +--- + +## 环境配置 + +### 数据库配置 + +#### 使用 Docker 部署 MySQL + +```bash +# 创建数据目录 +mkdir -p ~/mysql-data + +# 启动 MySQL 容器 +docker run -d \ + --name mysql-valuation \ + -p 3306:3306 \ + -e MYSQL_ROOT_PASSWORD=your_password \ + -e MYSQL_DATABASE=valuation_service \ + -v ~/mysql-data:/var/lib/mysql \ + --restart=unless-stopped \ + mysql:8.0 +``` + +#### 应用配置 + +配置文件位置: `app/settings/config.py` + +```python +TORTOISE_ORM = { + "connections": { + "mysql": { + "engine": "tortoise.backends.mysql", + "credentials": { + "host": "your_mysql_host", # 数据库主机地址 + "port": 3306, # 数据库端口 + "user": "root", # 数据库用户名 + "password": "your_password", # 数据库密码 + "database": "valuation_service", # 数据库名称 + }, + }, + }, + ... +} +``` + +### 第三方服务配置 + +| 服务 | 配置项 | 说明 | +|-----|-------|------| +| 阿里云短信 | `ALIBABA_CLOUD_ACCESS_KEY_ID/SECRET` | 短信验证码发送 | +| 阿里云邮件 | `SMTP_*` | 邮件发送 | + +--- + +## 本地开发 + +### 1. 安装依赖 + +```bash +# 安装 Python 依赖 +pip install -r requirements.txt + +# 安装前端依赖 +cd web +pnpm install +``` + +### 2. 启动服务 + +```bash +# 启动后端 (端口 9999) +python run.py + +# 启动前端开发服务器 (另一个终端) +cd web +pnpm run dev +``` + +--- + +## Docker 部署 + +### 1. 构建镜像 + +```bash +# 设置平台 (M1/M2 Mac 需要) +export DOCKER_DEFAULT_PLATFORM=linux/amd64 + +# 构建镜像 +docker build -t zfc931912343/guzhi-fastapi-admin:v3.9 . + +# 推送到 Docker Hub +docker push zfc931912343/guzhi-fastapi-admin:v3.9 +``` + +### 2. 部署到服务器 + +#### 生产环境 + +```bash +# 创建数据目录 +mkdir -p ~/guzhi-data/static/images + +# 拉取并运行 +docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.9 \ + && docker rm -f guzhi_pro \ + && docker run -itd \ + --name=guzhi_pro \ + -p 8080:9999 \ + -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.9 +``` + +#### 开发/测试环境 + +```bash +docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v3.9 \ + && 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.9 +``` + +--- + +## 端口说明 + +| 环境 | 容器名 | 主机端口 | 容器端口 | +|-----|-------|---------|---------| +| 生产 | guzhi_pro | 8080 | 9999 | +| 开发 | guzhi_dev | 9990 | 9999 | + +--- + +## 数据持久化 + +容器挂载的数据目录: + +``` +~/guzhi-data/static/images -> /opt/vue-fastapi-admin/app/static/images +``` + +用于存储用户上传的图片文件(如非遗纹样图片、证书图片等)。 + +--- + +## 常用运维命令 + +```bash +# 查看容器日志 +docker logs -f guzhi_pro + +# 进入容器 +docker exec -it guzhi_pro bash + +# 重启容器 +docker restart guzhi_pro + +# 查看容器状态 +docker ps | grep guzhi +``` + +--- + +## API 接口说明 + +| 模块 | 路径前缀 | 说明 | +|-----|---------|------| +| 用户端估值 | `/api/v1/app-valuations/` | 用户提交估值请求 | +| 管理端估值 | `/api/v1/valuations/` | 管理后台查看/审核 | +| 计算报告 | `/api/v1/valuations/{id}/report` | 获取计算过程报告 | + +--- + +## 版本历史 + +| 版本 | 日期 | 说明 | +|-----|------|------| +| v3.9 | 2025-12-18 | 修复风险调整系数B3显示问题,添加计算过程详情 | +| v3.8 | 2025-12-18 | 修复历史传承度HI权重计算 | + +--- + +## 联系信息 + +如有问题,请联系项目负责人。 diff --git a/需求文档.md b/需求文档.md deleted file mode 100644 index ad36a6f..0000000 --- a/需求文档.md +++ /dev/null @@ -1,186 +0,0 @@ -# 需求文档 - -### 1.1 项目范围 - -包含范围: - -- 非遗IP价值评估计算引擎 -- 用户管理和权限控制系统 -- 评估申请和审核流程 -- 评估报告生成 -- 第三方支付集成 -- 第三方登录集成 -- 第三方数据集成 - -## 2. 用户角色定义 - -### 2.1 管理端用户 - -#### 2.1.1 系统管理员 - -角色描述:负责系统整体管理和维护 -主要职责: - -- 用户账号管理和权限分配 -- 系统配置和参数设置 -- 基础数据维护(行业、ESG、政策等) -- 系统监控和日志管理 -- 第三方API配置管理 - -权限范围: - -- 所有功能模块的完整访问权限 -- 用户创建、编辑、删除权限 -- 系统配置修改权限 -- 数据导入导出权限 - -#### 2.1.2 业务审核员 - -角色描述:负责评估申请的审核和质量控制 -主要职责: - -- 评估申请的初步审核 -- 数据完整性和合理性检查 -- 计算结果的人工复核 -- 评估报告的审批发布 -- 异常情况的处理 - -权限范围: - -- 评估申请查看和审核权限 -- 审核状态修改权限 -- 审核备注添加权限 -- 报告生成和发布权限 - -### 2.2 应用端用户 - -#### 2.2.1 个人用户 - -角色描述:非遗传承人、文化工作者等个人申请者 -主要需求: - -- 提交个人非遗资产评估申请 -- 查看评估进度和结果 -- 下载评估报告 -- 管理个人信息 - -## 3. 功能需求 - -### 3.1 用户管理系统 - -#### 3.1.1 用户注册登录 - -功能描述:提供用户注册、登录、密码管理功能 - -详细需求: - -FR-001 手机号注册 - -- 用户可使用手机号进行注册 -- 支持短信验证码验证 -- 注册时需填写基本信息(姓名、机构等) -- 系统自动分配默认权限 - -FR-002 手机号登录 - -- 支持手机号+密码登录 -- 支持手机号+验证码登录 - -#### 3.1.2 权限管理 - -功能描述:基于RBAC模型的权限控制系统 - -详细需求: - -FR-004 角色管理 - -- 支持创建、编辑、删除角色 -- 角色可分配菜单权限和API权限 -- 预设系统管理员、审核员、普通用户角色 - -### 3.2 估值评估系统 - -#### 3.2.1 评估申请提交 - -功能描述:用户提交非遗资产评估申请的完整流程 - -#### 3.2.2 评估结果管理 - -功能描述:评估结果的存储、展示和管理 - -详细需求: - -FR-015 结果存储 - -- 完整计算过程和中间结果保存 -- 输入参数和输出结果关联存储 -- 计算时间和版本信息记录 - -FR-016 结果展示 - -- 估值结果可视化展示 -- 计算过程分步骤展示 -- 各维度得分雷达图展示 -- 风险评估结果展示 - -FR-017 报告生成 - -- 自动生成详细评估报告 -- 支持PDF格式导出 -- 报告包含计算过程和结论 - -### 3.3 审核管理系统 - -### 3.4 数据管理系统 - -#### 3.4.1 基础数据管理 - -功能描述:系统基础数据的维护和管理 - -详细需求: - -FR-024 行业数据管理 - -- 行业分类标准维护 -- 行业ROE系数管理 - -FR-025 ESG数据管理 - -- ESG评级标准维护 -- 行业ESG基准分管理 - -FR-026 政策数据管理 - -- 政策匹配规则维护 -- 资助政策数据库管理 - -## 4. 移动端开发 - -### 4.1 微信小程序开发 - -#### 4.1.1 功能范围 - -功能描述:基于微信小程序平台的移动端应用开发 - -详细需求: - -FR-027 用户认证 - -- 支持微信授权登录 -- 支持手机号快速登录 -- 与PC端用户体系统一 -- 自动获取微信用户基本信息 - -FR-028 评估申请 - -- 移动端评估表单提交 -- 支持拍照上传证书材料 -- 表单数据与PC端保持一致 -- 支持草稿保存和续填 - -FR-029 进度查询 - -- 实时查看评估申请状态 -- 接收微信消息推送通知 -- 查看评估结果和报告 -- 支持报告分享功能 \ No newline at end of file