feat: Implement user account soft deletion with token blacklisting, admin management, and SMS verification tracking.
This commit is contained in:
parent
90c0f85972
commit
1157704d4b
@ -1,131 +0,0 @@
|
||||
## 目标概述
|
||||
|
||||
* 加强“交易管理-发票附件与发送邮箱”能力,完善数据记录与事务保障。
|
||||
|
||||
* 改进“开票弹窗附件上传”支持多文件上传(后端存储结构)。
|
||||
|
||||
* 优化“用户管理备注与操作记录”,区分备注维度并完善日志。
|
||||
|
||||
* 覆盖单元/集成测试、数据库迁移、API文档与审计日志。
|
||||
|
||||
## 数据库与模型变更
|
||||
|
||||
* EmailSendLog
|
||||
|
||||
* 新增:`extra: JSONField(null=True)` 完整记录发送邮箱的请求数据(收件人、主题、正文、附件列表、重试信息等)。
|
||||
|
||||
* Invoice(或与开票弹窗相关的业务模型)
|
||||
|
||||
* 新增:`attachments: JSONField(null=True)` 支持多个附件URL(与弹窗上传对应)。
|
||||
|
||||
* <br />
|
||||
|
||||
* 迁移脚本:创建/修改上述字段;保留历史数据不丢失。
|
||||
|
||||
## 事务与原子性
|
||||
|
||||
* 发送邮箱流程(交易管理)
|
||||
|
||||
* 封装在 `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. 更新接口文档与部署回归验证。
|
||||
|
||||
@ -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 校验用例覆盖必填与格式。
|
||||
|
||||
## 交付内容。
|
||||
|
||||
* 完成基本交互与接口验证,确保行为符合预期。
|
||||
|
||||
225
DEPLOYMENT.md
Normal file
225
DEPLOYMENT.md
Normal file
@ -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权重计算 |
|
||||
|
||||
---
|
||||
|
||||
## 联系信息
|
||||
|
||||
如有问题,请联系项目负责人。
|
||||
29
aaa.json
29
aaa.json
@ -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):
|
||||
@ -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="账号已注销")
|
||||
|
||||
@ -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)):
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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相关参数
|
||||
|
||||
|
||||
@ -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"})
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)):
|
||||
|
||||
13
app/core/token_blacklist.py
Normal file
13
app/core/token_blacklist.py
Normal file
@ -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()
|
||||
|
||||
@ -6,4 +6,5 @@ from .industry import *
|
||||
from .policy import *
|
||||
from .user import *
|
||||
from .valuation import *
|
||||
from .invoice import *
|
||||
from .invoice import *
|
||||
from .token_blacklist import *
|
||||
|
||||
15
app/models/token_blacklist.py
Normal file
15
app/models/token_blacklist.py
Normal file
@ -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令牌黑名单"
|
||||
|
||||
@ -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用户估值次数操作日志"
|
||||
table_description = "App用户估值次数操作日志"
|
||||
|
||||
@ -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()
|
||||
def is_recently_verified(self, phone: str) -> bool:
|
||||
until = self.verified.get(phone, 0.0)
|
||||
return until > time.time()
|
||||
|
||||
|
||||
store = VerificationStore()
|
||||
|
||||
@ -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
|
||||
return user
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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); -- 审核列表
|
||||
-- 注意:普通用户不分配用户管理权限
|
||||
@ -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 回填
|
||||
|
||||
@ -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
|
||||
return r.status_code, data
|
||||
|
||||
@ -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) ==========
|
||||
|
||||
BIN
web1/dist 2.zip
Normal file
BIN
web1/dist 2.zip
Normal file
Binary file not shown.
10
估值字段.txt
10
估值字段.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
|
||||
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
|
||||
225
部署文档.md
Normal file
225
部署文档.md
Normal file
@ -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权重计算 |
|
||||
|
||||
---
|
||||
|
||||
## 联系信息
|
||||
|
||||
如有问题,请联系项目负责人。
|
||||
186
需求文档.md
186
需求文档.md
@ -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 进度查询
|
||||
|
||||
- 实时查看评估申请状态
|
||||
- 接收微信消息推送通知
|
||||
- 查看评估结果和报告
|
||||
- 支持报告分享功能
|
||||
Loading…
x
Reference in New Issue
Block a user