Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45aae516b2 | ||
|
|
4110dca428 | ||
| f17c1678c8 | |||
| 1157704d4b | |||
|
|
17b56a1c19 | ||
|
|
6718b51fb9 | ||
|
|
58f16be457 | ||
|
|
90c0f85972 | ||
|
|
7819c60ace | ||
|
|
1d73f6ed54 | ||
|
|
6b5967a4bb | ||
|
|
8926e047d4 | ||
| f1c1db580c | |||
| b10c357a56 |
@ -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权重计算 |
|
||||
|
||||
---
|
||||
|
||||
## 联系信息
|
||||
|
||||
如有问题,请联系项目负责人。
|
||||
10
Dockerfile
10
Dockerfile
@ -1,8 +1,14 @@
|
||||
FROM node:18.12.0-alpine3.16 AS web
|
||||
FROM node:18-alpine AS web
|
||||
|
||||
WORKDIR /opt/vue-fastapi-admin
|
||||
COPY /web ./web
|
||||
RUN npm install -g pnpm && cd /opt/vue-fastapi-admin/web && pnpm install --registry=https://registry.npmmirror.com && pnpm run build
|
||||
|
||||
# 安装pnpm并设置配置
|
||||
RUN npm install -g pnpm && \
|
||||
cd /opt/vue-fastapi-admin/web && \
|
||||
pnpm config set registry https://registry.npmmirror.com && \
|
||||
pnpm install && \
|
||||
pnpm run build
|
||||
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
|
||||
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):
|
||||
@ -117,13 +117,23 @@ async def create_with_receipt(payload: AppCreateInvoiceWithReceipt, current_user
|
||||
)
|
||||
inv = await invoice_controller.create(inv_data)
|
||||
if payload.receipt_urls:
|
||||
receipts = []
|
||||
for url in payload.receipt_urls:
|
||||
receipt = await invoice_controller.create_receipt(inv.id, PaymentReceiptCreate(url=url, note=payload.note))
|
||||
detail = await invoice_controller.get_receipt_by_id(receipt.id)
|
||||
if detail:
|
||||
receipts.append(detail)
|
||||
return Success(data={"invoice_id": inv.id, "receipts": receipts}, msg="创建并上传成功")
|
||||
urls = payload.receipt_urls
|
||||
main_url = urls[0] if isinstance(urls, list) and urls else None
|
||||
receipt = await invoice_controller.create_receipt(
|
||||
inv.id,
|
||||
PaymentReceiptCreate(url=main_url, note=payload.note, extra=urls)
|
||||
)
|
||||
detail = await invoice_controller.get_receipt_by_id(receipt.id)
|
||||
return Success(data={"invoice_id": inv.id, "receipts": [detail] if detail else []}, msg="创建并上传成功")
|
||||
if isinstance(payload.receipt_url, list) and payload.receipt_url:
|
||||
urls = payload.receipt_url
|
||||
main_url = urls[0]
|
||||
receipt = await invoice_controller.create_receipt(
|
||||
inv.id,
|
||||
PaymentReceiptCreate(url=main_url, note=payload.note, extra=urls)
|
||||
)
|
||||
detail = await invoice_controller.get_receipt_by_id(receipt.id)
|
||||
return Success(data={"invoice_id": inv.id, "receipts": [detail] if detail else []}, msg="创建并上传成功")
|
||||
if payload.receipt_url:
|
||||
receipt = await invoice_controller.create_receipt(inv.id, PaymentReceiptCreate(url=payload.receipt_url, note=payload.note))
|
||||
detail = await invoice_controller.get_receipt_by_id(receipt.id)
|
||||
|
||||
@ -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
|
||||
else:
|
||||
infringement_score = 10.0
|
||||
except:
|
||||
infringement_score = 0.0
|
||||
input_data_by_b1["infringement_score"] = infringement_score
|
||||
_data = judicial_data.get("data", {})
|
||||
judicial_api_response = _data # 保存原始返回
|
||||
target = _data.get("target", None) # 诉讼标的
|
||||
total = _data.get("total", 0) # 诉讼总数
|
||||
|
||||
# 获取专利信息 TODO 参数
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 获取专利信息
|
||||
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,21 +181,10 @@ 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(
|
||||
await valuation_controller.update_calc(
|
||||
valuation_id,
|
||||
ValuationAssessmentUpdate(
|
||||
calculation_input=input_data,
|
||||
@ -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)
|
||||
@ -212,7 +229,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
# api_calc_fields["flow_correction"] = None
|
||||
|
||||
if api_calc_fields:
|
||||
await valuation_controller.update(
|
||||
await valuation_controller.update_calc(
|
||||
valuation_id,
|
||||
ValuationAssessmentUpdate(**api_calc_fields)
|
||||
)
|
||||
@ -220,12 +237,136 @@ 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)
|
||||
|
||||
# 步骤2:更新计算结果字段(模型估值B、市场估值C、最终估值AB、完整计算结果)
|
||||
try:
|
||||
await valuation_controller.update(
|
||||
await valuation_controller.update_calc(
|
||||
valuation_id,
|
||||
ValuationAssessmentUpdate(
|
||||
model_value_b=calculation_result.get('model_value_b'),
|
||||
@ -280,7 +421,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
base_pledge_rate_value = "0.5" # 固定值:基础质押率 = 0.5
|
||||
flow_correction_value = "0.3" # 固定值:流量修正系数 = 0.3
|
||||
|
||||
await valuation_controller.update(
|
||||
await valuation_controller.update_calc(
|
||||
valuation_id,
|
||||
ValuationAssessmentUpdate(
|
||||
dynamic_pledge_rate=drp_result,
|
||||
@ -307,19 +448,13 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 步骤4:最后更新状态为成功
|
||||
# 步骤4:计算完成,保持状态为 pending,等待后台审核
|
||||
try:
|
||||
result = await valuation_controller.update(
|
||||
valuation_id,
|
||||
ValuationAssessmentUpdate(
|
||||
status='success'
|
||||
)
|
||||
)
|
||||
logger.info("valuation.status_updated valuation_id={} status=success", valuation_id)
|
||||
except Exception as e:
|
||||
logger.warning("valuation.failed_to_update_status valuation_id={} err={}", valuation_id, repr(e))
|
||||
# 即使状态更新失败,也尝试获取结果用于日志
|
||||
result = await valuation_controller.get_by_id(valuation_id)
|
||||
logger.info("valuation.calc_finished valuation_id={} status=pending", valuation_id)
|
||||
except Exception as e:
|
||||
logger.warning("valuation.failed_to_fetch_after_calc valuation_id={} err={}", valuation_id, repr(e))
|
||||
result = None
|
||||
|
||||
logger.info("valuation.background_calc_success user_id={} valuation_id={}", user_id, valuation_id)
|
||||
|
||||
@ -345,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():
|
||||
@ -362,7 +501,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
fail_update_fields.update(api_calc_fields)
|
||||
|
||||
try:
|
||||
await valuation_controller.update(
|
||||
await valuation_controller.update_calc(
|
||||
valuation_id,
|
||||
ValuationAssessmentUpdate(**fail_update_fields)
|
||||
)
|
||||
@ -372,7 +511,7 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
# 如果保存失败,至少更新状态
|
||||
try:
|
||||
fail_update = ValuationAssessmentUpdate(status='rejected')
|
||||
await valuation_controller.update(valuation_id, fail_update)
|
||||
await valuation_controller.update_calc(valuation_id, fail_update)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
@ -474,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)
|
||||
}
|
||||
)
|
||||
|
||||
@ -531,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:
|
||||
@ -541,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
|
||||
@ -591,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 目前未参与计算,先移除
|
||||
@ -635,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,
|
||||
@ -653,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(系统计算)
|
||||
@ -692,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,
|
||||
}
|
||||
|
||||
@ -797,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"})
|
||||
|
||||
|
||||
|
||||
@ -85,6 +85,11 @@ async def send_email(payload: SendEmailRequest = Body(...)):
|
||||
raise HTTPException(status_code=400, detail="收件方地址域名不可用或未正确解析")
|
||||
if payload.file_urls:
|
||||
urls.extend([u.strip().strip('`') for u in payload.file_urls if isinstance(u, str)])
|
||||
if payload.file_url:
|
||||
if isinstance(payload.file_url, str):
|
||||
urls.append(payload.file_url.strip().strip('`'))
|
||||
elif isinstance(payload.file_url, list):
|
||||
urls.extend([u.strip().strip('`') for u in payload.file_url if isinstance(u, str)])
|
||||
if urls:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
@ -122,20 +127,25 @@ async def send_email(payload: SendEmailRequest = Body(...)):
|
||||
else:
|
||||
logger.error("transactions.email_send_fail email={} err={}", payload.email, error)
|
||||
|
||||
if status == "OK" and payload.receipt_id:
|
||||
if payload.receipt_id:
|
||||
try:
|
||||
r = await PaymentReceipt.filter(id=payload.receipt_id).first()
|
||||
if r:
|
||||
r.extra = (r.extra or {}) | payload.model_dump()
|
||||
await r.save()
|
||||
try:
|
||||
inv = await r.invoice
|
||||
if inv:
|
||||
inv.status = "invoiced"
|
||||
s = str(payload.status or "").lower()
|
||||
if s in {"invoiced", "success"}:
|
||||
target = "invoiced"
|
||||
elif s in {"refunded", "rejected", "pending"}:
|
||||
target = s
|
||||
else:
|
||||
target = "invoiced"
|
||||
inv.status = target
|
||||
await inv.save()
|
||||
logger.info("transactions.invoice_mark_invoiced receipt_id={} invoice_id={}", payload.receipt_id, inv.id)
|
||||
logger.info("transactions.invoice_status_updated receipt_id={} invoice_id={} status={}", payload.receipt_id, inv.id, target)
|
||||
except Exception as e2:
|
||||
logger.warning("transactions.invoice_mark_invoiced_fail receipt_id={} err={}", payload.receipt_id, str(e2))
|
||||
logger.warning("transactions.invoice_status_update_fail receipt_id={} err={}", payload.receipt_id, str(e2))
|
||||
except Exception as e:
|
||||
logger.error("transactions.email_extra_save_fail id={} err={}", payload.receipt_id, str(e))
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -271,20 +271,23 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
items = []
|
||||
for r in rows:
|
||||
inv = await r.invoice
|
||||
urls = []
|
||||
if isinstance(r.extra, list):
|
||||
urls = [str(u) for u in r.extra if u]
|
||||
elif isinstance(r.extra, dict):
|
||||
v = r.extra.get("urls")
|
||||
if isinstance(v, list):
|
||||
urls = [str(u) for u in v if u]
|
||||
if not urls:
|
||||
urls = [r.url] if r.url else []
|
||||
receipts = [{"id": r.id, "url": u, "note": r.note, "verified": r.verified} for u in urls]
|
||||
items.append({
|
||||
"id": r.id,
|
||||
"invoice_id": getattr(inv, "id", None),
|
||||
"submitted_at": r.created_at.isoformat() if r.created_at else "",
|
||||
"receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
|
||||
"extra": r.extra,
|
||||
"receipts": [
|
||||
{
|
||||
"id": r.id,
|
||||
"url": r.url,
|
||||
"note": r.note,
|
||||
"verified": r.verified,
|
||||
}
|
||||
],
|
||||
"receipts": receipts,
|
||||
"phone": inv.phone,
|
||||
"wechat": inv.wechat,
|
||||
"company_name": inv.company_name,
|
||||
@ -313,20 +316,23 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
if not r:
|
||||
return None
|
||||
inv = await r.invoice
|
||||
urls = []
|
||||
if isinstance(r.extra, list):
|
||||
urls = [str(u) for u in r.extra if u]
|
||||
elif isinstance(r.extra, dict):
|
||||
v = r.extra.get("urls")
|
||||
if isinstance(v, list):
|
||||
urls = [str(u) for u in v if u]
|
||||
if not urls:
|
||||
urls = [r.url] if r.url else []
|
||||
receipts = [{"id": r.id, "url": u, "note": r.note, "verified": r.verified} for u in urls]
|
||||
return {
|
||||
"id": r.id,
|
||||
"invoice_id": getattr(inv, "id", None),
|
||||
"submitted_at": r.created_at.isoformat() if r.created_at else "",
|
||||
"receipt_uploaded_at": r.updated_at.isoformat() if getattr(r, "updated_at", None) else "",
|
||||
"extra": r.extra,
|
||||
"receipts": [
|
||||
{
|
||||
"id": r.id,
|
||||
"url": r.url,
|
||||
"note": r.note,
|
||||
"verified": r.verified,
|
||||
}
|
||||
],
|
||||
"receipts": receipts,
|
||||
"phone": inv.phone,
|
||||
"wechat": inv.wechat,
|
||||
"company_name": inv.company_name,
|
||||
|
||||
@ -127,7 +127,7 @@ class UserValuationController:
|
||||
inheritor_ages=valuation.inheritor_ages,
|
||||
inheritor_age_count=valuation.inheritor_age_count,
|
||||
inheritor_certificates=valuation.inheritor_certificates,
|
||||
heritage_level=valuation.heritage_level,
|
||||
heritage_level=getattr(valuation, "heritage_level", None),
|
||||
heritage_asset_level=valuation.heritage_asset_level,
|
||||
patent_application_no=valuation.patent_application_no,
|
||||
patent_remaining_years=valuation.patent_remaining_years,
|
||||
@ -197,7 +197,7 @@ class UserValuationController:
|
||||
inheritor_ages=valuation.inheritor_ages,
|
||||
inheritor_age_count=valuation.inheritor_age_count,
|
||||
inheritor_certificates=valuation.inheritor_certificates,
|
||||
heritage_level=valuation.heritage_level,
|
||||
heritage_level=getattr(valuation, "heritage_level", None),
|
||||
heritage_asset_level=valuation.heritage_asset_level,
|
||||
patent_application_no=valuation.patent_application_no,
|
||||
patent_remaining_years=valuation.patent_remaining_years,
|
||||
|
||||
@ -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)):
|
||||
@ -813,7 +878,7 @@ class ValuationController:
|
||||
return None
|
||||
|
||||
from datetime import datetime
|
||||
update_data = {"status": "pending", "audited_at": datetime.now(), "updated_at": datetime.now()}
|
||||
update_data = {"status": "success", "audited_at": datetime.now(), "updated_at": datetime.now()}
|
||||
if admin_notes:
|
||||
update_data["admin_notes"] = admin_notes
|
||||
|
||||
@ -847,6 +912,18 @@ class ValuationController:
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
|
||||
async def update_calc(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]:
|
||||
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
|
||||
if not valuation:
|
||||
return None
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
valuation.status ="pending"
|
||||
if update_data:
|
||||
await valuation.update_from_dict(update_data)
|
||||
await valuation.save()
|
||||
out = ValuationAssessmentOut.model_validate(valuation)
|
||||
return await self._attach_user_phone(out)
|
||||
|
||||
async def _attach_user_phone(self, out: ValuationAssessmentOut) -> ValuationAssessmentOut:
|
||||
user = await AppUser.filter(id=out.user_id).first()
|
||||
out.user_phone = getattr(user, "phone", None) if user else None
|
||||
|
||||
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()
|
||||
|
||||
@ -7,3 +7,4 @@ from .policy import *
|
||||
from .user import *
|
||||
from .valuation 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"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Union, Dict, Any
|
||||
from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator
|
||||
|
||||
|
||||
@ -115,7 +115,7 @@ class UpdateType(BaseModel):
|
||||
class PaymentReceiptCreate(BaseModel):
|
||||
url: str = Field(..., min_length=1, max_length=512)
|
||||
note: Optional[str] = Field(None, max_length=256)
|
||||
extra: Optional[dict] = None
|
||||
extra: Optional[Union[List[str], Dict[str, Any]]] = None
|
||||
|
||||
|
||||
class PaymentReceiptOut(BaseModel):
|
||||
@ -124,7 +124,7 @@ class PaymentReceiptOut(BaseModel):
|
||||
note: Optional[str]
|
||||
verified: bool
|
||||
created_at: str
|
||||
extra: Optional[dict] = None
|
||||
extra: Optional[Union[List[str], Dict[str, Any]]] = None
|
||||
|
||||
|
||||
class AppCreateInvoiceWithReceipt(BaseModel):
|
||||
@ -133,7 +133,7 @@ class AppCreateInvoiceWithReceipt(BaseModel):
|
||||
invoice_type: Optional[str] = Field(None, pattern=r"^(special|normal)$")
|
||||
# 兼容前端索引字段:"0"→normal,"1"→special
|
||||
invoiceTypeIndex: Optional[str] = None
|
||||
receipt_url: Optional[str] = Field(None, max_length=512)
|
||||
receipt_url: Optional[Union[str, List[str]]] = Field(None)
|
||||
receipt_urls: Optional[List[str]] = None
|
||||
note: Optional[str] = Field(None, max_length=256)
|
||||
|
||||
@ -145,14 +145,26 @@ class AppCreateInvoiceWithReceipt(BaseModel):
|
||||
@field_validator('receipt_url', mode='before')
|
||||
@classmethod
|
||||
def _clean_receipt_url(cls, v):
|
||||
if isinstance(v, list) and v:
|
||||
v = v[0]
|
||||
if isinstance(v, list):
|
||||
cleaned: List[str] = []
|
||||
for item in v:
|
||||
if isinstance(item, str):
|
||||
s = item.strip()
|
||||
if s.startswith('`') and s.endswith('`'):
|
||||
s = s[1:-1].strip()
|
||||
while s.endswith('\\'):
|
||||
s = s[:-1].strip()
|
||||
if s:
|
||||
cleaned.append(s)
|
||||
return cleaned or None
|
||||
if isinstance(v, str):
|
||||
s = v.strip()
|
||||
if s.startswith('`') and s.endswith('`'):
|
||||
s = s[1:-1].strip()
|
||||
while s.endswith('\\'):
|
||||
s = s[:-1].strip()
|
||||
return s or None
|
||||
return v
|
||||
return None
|
||||
|
||||
@field_validator('receipt_urls', mode='before')
|
||||
@classmethod
|
||||
@ -162,10 +174,18 @@ class AppCreateInvoiceWithReceipt(BaseModel):
|
||||
if isinstance(v, str):
|
||||
v = [v]
|
||||
if isinstance(v, list):
|
||||
seen = set()
|
||||
cleaned = []
|
||||
for item in v:
|
||||
if isinstance(item, str) and item.strip():
|
||||
cleaned.append(item.strip())
|
||||
if isinstance(item, str):
|
||||
s = item.strip()
|
||||
if s.startswith('`') and s.endswith('`'):
|
||||
s = s[1:-1].strip()
|
||||
while s.endswith('\\'):
|
||||
s = s[:-1].strip()
|
||||
if s and s not in seen:
|
||||
seen.add(s)
|
||||
cleaned.append(s)
|
||||
return cleaned or None
|
||||
return None
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ class SendEmailRequest(BaseModel):
|
||||
subject: Optional[str] = Field(None, description="邮件主题")
|
||||
body: str = Field(..., description="文案内容")
|
||||
file_urls: Optional[List[str]] = Field(None, description="附件URL列表")
|
||||
file_url: Optional[Union[str, List[str]]] = Field(None, description="附件URL或列表(兼容前端传参)")
|
||||
status: Optional[str] = Field(None, description="开票状态标记: success|invoiced|rejected|refunded")
|
||||
|
||||
|
||||
class SendEmailBody(BaseModel):
|
||||
|
||||
@ -193,7 +193,7 @@ class ValuationAssessmentOut(ValuationAssessmentBase):
|
||||
status: str = Field(..., description="评估状态")
|
||||
admin_notes: Optional[str] = Field(None, description="管理员备注")
|
||||
created_at: datetime = Field(..., description="创建时间")
|
||||
updated_at: datetime = Field(..., description="更新时间")
|
||||
updated_at: Optional[datetime] = Field(None, description="更新时间")
|
||||
audited_at: Optional[datetime] = Field(None, description="审核时间")
|
||||
is_active: bool = Field(..., description="是否激活")
|
||||
|
||||
@ -246,7 +246,7 @@ class UserValuationOut(ValuationAssessmentBase):
|
||||
status: str = Field(..., description="评估状态")
|
||||
admin_notes: Optional[str] = Field(None, description="管理员备注")
|
||||
created_at: datetime = Field(..., description="创建时间")
|
||||
updated_at: datetime = Field(..., description="更新时间")
|
||||
updated_at: Optional[datetime] = Field(None, description="更新时间")
|
||||
is_active: Optional[bool] = Field(None, description="是否激活")
|
||||
|
||||
class Config:
|
||||
@ -290,7 +290,7 @@ class UserValuationDetail(ValuationAssessmentBase):
|
||||
status: str = Field(..., description="评估状态")
|
||||
admin_notes: Optional[str] = Field(None, description="管理员备注")
|
||||
created_at: datetime = Field(..., description="创建时间")
|
||||
updated_at: datetime = Field(..., description="更新时间")
|
||||
updated_at: Optional[datetime] = Field(None, description="更新时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@ -422,7 +422,7 @@ class ValuationCalculationStepOut(ValuationCalculationStepBase):
|
||||
id: int = Field(..., description="主键ID")
|
||||
valuation_id: int = Field(..., description="关联的估值评估ID")
|
||||
created_at: datetime = Field(..., description="创建时间")
|
||||
updated_at: datetime = Field(..., description="更新时间")
|
||||
updated_at: Optional[datetime] = Field(None, description="更新时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@ -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
|
||||
|
||||
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:
|
||||
|
||||
@ -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,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
nginx
|
||||
# nginx
|
||||
|
||||
python run.py
|
||||
@ -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)
|
||||
|
||||
@ -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) ==========
|
||||
|
||||
@ -176,7 +176,7 @@ const columns = [
|
||||
NPopover,
|
||||
{
|
||||
trigger: 'hover',
|
||||
placement: 'top',
|
||||
placement: 'right',
|
||||
showArrow: false,
|
||||
displayDirective: 'show', // keep content mounted so image preview won't flash close
|
||||
},
|
||||
|
||||
@ -46,7 +46,8 @@ const activeDetailTab = ref('audit')
|
||||
const reportLoading = ref(false)
|
||||
const reportContent = ref('')
|
||||
|
||||
const pickFilledValue = (...values) => values.find((val) => val !== undefined && val !== null && val !== '')
|
||||
const pickFilledValue = (...values) =>
|
||||
values.find((val) => val !== undefined && val !== null && val !== '')
|
||||
const formatEnumField = (key, ...values) => formatEnumByKey(pickFilledValue(...values), key)
|
||||
|
||||
// 证书弹窗相关状态
|
||||
@ -97,7 +98,11 @@ const detailSections = computed(() => {
|
||||
fields: [
|
||||
{ label: '资产名称', type: 'text', value: detail.asset_name || '-' },
|
||||
{ label: '所属机构/权利人', type: 'text', value: detail.institution || '-' },
|
||||
{ label: '统一社会信用代码/身份证号', type: 'text', value: detail.credit_code_or_id || '-' },
|
||||
{
|
||||
label: '统一社会信用代码/身份证号',
|
||||
type: 'text',
|
||||
value: detail.credit_code_or_id || '-',
|
||||
},
|
||||
{ label: '所属行业', type: 'text', value: detail.industry || '-' },
|
||||
{ label: '业务/传承介绍', type: 'text', value: detail.biz_intro || '-' },
|
||||
],
|
||||
@ -106,17 +111,37 @@ const detailSections = computed(() => {
|
||||
key: 'finance',
|
||||
title: '财务状况',
|
||||
fields: [
|
||||
{ label: '近12个月机构营收/万元', type: 'text', value: formatNumberValue(detail.annual_revenue) },
|
||||
{ label: '近12个月机构研发投入/万元', type: 'text', value: formatNumberValue(detail.rd_investment) },
|
||||
{ label: '近三年机构收益/万元', type: 'list', value: formatThreeYearIncome(detail.three_year_income) },
|
||||
{ label: '资产受资助情况', type: 'text', value: formatEnumField('fundingStatus', detail.funding_status) },
|
||||
{
|
||||
label: '近12个月机构营收/万元',
|
||||
type: 'text',
|
||||
value: formatNumberValue(detail.annual_revenue),
|
||||
},
|
||||
{
|
||||
label: '近12个月机构研发投入/万元',
|
||||
type: 'text',
|
||||
value: formatNumberValue(detail.rd_investment),
|
||||
},
|
||||
{
|
||||
label: '近三年机构收益/万元',
|
||||
type: 'list',
|
||||
value: formatThreeYearIncome(detail.three_year_income),
|
||||
},
|
||||
{
|
||||
label: '资产受资助情况',
|
||||
type: 'text',
|
||||
value: formatEnumField('fundingStatus', detail.funding_status),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'tech',
|
||||
title: '非遗等级与技术',
|
||||
fields: [
|
||||
{ label: '非遗传承人等级', type: 'text', value: formatEnumField('inheritorLevel', detail.inheritor_level) },
|
||||
{
|
||||
label: '非遗传承人等级',
|
||||
type: 'text',
|
||||
value: formatEnumField('inheritorLevel', detail.inheritor_level),
|
||||
},
|
||||
{
|
||||
label: '非遗传承人年龄水平及数量',
|
||||
type: 'list',
|
||||
@ -126,10 +151,22 @@ const detailSections = computed(() => {
|
||||
{
|
||||
label: '非遗等级',
|
||||
type: 'text',
|
||||
value: formatEnumField('heritageLevel', detail.heritage_level, detail.heritage_asset_level),
|
||||
value: formatEnumField(
|
||||
'heritageLevel',
|
||||
detail.heritage_level,
|
||||
detail.heritage_asset_level
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '非遗资产所用专利的申请号',
|
||||
type: 'text',
|
||||
value: detail.patent_application_no || '-',
|
||||
},
|
||||
{
|
||||
label: '非遗资产历史证明证据及数量',
|
||||
type: 'list',
|
||||
value: formatHistoricalEvidence(detail.historical_evidence),
|
||||
},
|
||||
{ label: '非遗资产所用专利的申请号', type: 'text', value: detail.patent_application_no || '-' },
|
||||
{ label: '非遗资产历史证明证据及数量', type: 'list', value: formatHistoricalEvidence(detail.historical_evidence) },
|
||||
{
|
||||
label: '非遗资产所用专利/纹样图片',
|
||||
type: 'images',
|
||||
@ -144,50 +181,86 @@ const detailSections = computed(() => {
|
||||
{
|
||||
label: '非遗资产应用成熟度',
|
||||
type: 'text',
|
||||
value: formatEnumField('applicationMaturity', detail.application_maturity, detail.implementation_stage),
|
||||
value: formatEnumField(
|
||||
'applicationMaturity',
|
||||
detail.application_maturity,
|
||||
detail.implementation_stage
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '非遗资产应用覆盖范围',
|
||||
type: 'text',
|
||||
value: formatEnumField('applicationCoverage', detail.application_coverage, detail.coverage_area),
|
||||
value: formatEnumField(
|
||||
'applicationCoverage',
|
||||
detail.application_coverage,
|
||||
detail.coverage_area
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '非遗资产跨界合作深度',
|
||||
type: 'text',
|
||||
value: formatEnumField('cooperationDepth', detail.cooperation_depth, detail.collaboration_type),
|
||||
value: formatEnumField(
|
||||
'cooperationDepth',
|
||||
detail.cooperation_depth,
|
||||
detail.collaboration_type
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '近12个月线下相关宣讲活动次数',
|
||||
type: 'text',
|
||||
value: formatNumberValue(detail.offline_activities ?? detail.offline_teaching_count),
|
||||
},
|
||||
{ label: '线上相关宣传账号信息', type: 'list', value: formatPlatformAccounts(detail.platform_accounts) },
|
||||
{
|
||||
label: '线上相关宣传账号信息',
|
||||
type: 'list',
|
||||
value: formatPlatformAccounts(detail.platform_accounts),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'products',
|
||||
title: '非遗资产衍生商品信息',
|
||||
fields: [
|
||||
{ label: '代表产品近12个月销售数量', type: 'text', value: formatNumberValue(detail.sales_volume) },
|
||||
{
|
||||
label: '代表产品近12个月销售数量',
|
||||
type: 'text',
|
||||
value: formatNumberValue(detail.sales_volume),
|
||||
},
|
||||
{ label: '商品链接浏览量', type: 'text', value: formatNumberValue(detail.link_views) },
|
||||
{ label: '发行量', type: 'text', value: formatEnumField('circulation', detail.circulation, detail.scarcity_level) },
|
||||
{
|
||||
label: '发行量',
|
||||
type: 'text',
|
||||
value: formatEnumField('circulation', detail.circulation, detail.scarcity_level),
|
||||
},
|
||||
{
|
||||
label: '最近一次市场活动时间',
|
||||
type: 'text',
|
||||
value: formatEnumField('marketActivity', detail.last_market_activity, detail.market_activity_time),
|
||||
value: formatEnumField(
|
||||
'marketActivity',
|
||||
detail.last_market_activity,
|
||||
detail.market_activity_time
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '月交易额水平',
|
||||
type: 'text',
|
||||
value: formatEnumField('monthlyTransaction', detail.monthly_transaction, detail.monthly_transaction_amount),
|
||||
value: formatEnumField(
|
||||
'monthlyTransaction',
|
||||
detail.monthly_transaction,
|
||||
detail.monthly_transaction_amount
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '近30天价格区间',
|
||||
type: 'text',
|
||||
value: formatPriceRange(detail.price_fluctuation),
|
||||
},
|
||||
{ label: '近30天价格区间', type: 'text', value: formatPriceRange(detail.price_fluctuation) },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// 为每个 section 生成 NDataTable 需要的 columns 和 data
|
||||
return sections.map(section => {
|
||||
return sections.map((section) => {
|
||||
const columns = [
|
||||
{
|
||||
title: '字段名',
|
||||
@ -196,55 +269,65 @@ const detailSections = computed(() => {
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
},
|
||||
...section.fields.map(field => ({
|
||||
...section.fields.map((field) => ({
|
||||
title: field.label,
|
||||
key: field.label,
|
||||
width: 200,
|
||||
ellipsis: ['list', 'images'].includes(field.type) ? false : {
|
||||
tooltip: {
|
||||
style: { maxWidth: '600px', maxHeight: '400px', overflow: 'auto' }
|
||||
},
|
||||
},
|
||||
ellipsis: ['list', 'images'].includes(field.type)
|
||||
? false
|
||||
: {
|
||||
tooltip: {
|
||||
style: { maxWidth: '600px', maxHeight: '400px', overflow: 'auto' },
|
||||
},
|
||||
},
|
||||
render: (row) => {
|
||||
const fieldData = row[field.label]
|
||||
if (!fieldData) return '-'
|
||||
|
||||
if (fieldData.type === 'list') {
|
||||
if (fieldData.value && fieldData.value.length) {
|
||||
return h('div', { style: 'display: flex; flex-direction: column; gap: 4px;' },
|
||||
fieldData.value.map(item => h('span', item))
|
||||
return h(
|
||||
'div',
|
||||
{ style: 'display: flex; flex-direction: column; gap: 4px;' },
|
||||
fieldData.value.map((item) => h('span', item))
|
||||
)
|
||||
}
|
||||
return '-'
|
||||
} else if (fieldData.type === 'images') {
|
||||
if (fieldData.value && fieldData.value.length) {
|
||||
const createImages = () => h(NImageGroup, {}, () =>
|
||||
fieldData.value.map(img =>
|
||||
h(NImage, {
|
||||
src: img,
|
||||
width: 72,
|
||||
height: 48,
|
||||
objectFit: 'cover',
|
||||
style: 'margin-right: 8px;'
|
||||
})
|
||||
const createImages = () =>
|
||||
h(NImageGroup, {}, () =>
|
||||
fieldData.value.map((img) =>
|
||||
h(NImage, {
|
||||
src: img,
|
||||
width: 72,
|
||||
height: 48,
|
||||
objectFit: 'cover',
|
||||
style: 'margin-right: 8px;',
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return h(NPopover, {
|
||||
trigger: 'hover',
|
||||
displayDirective: 'show',
|
||||
keepAliveOnHover: true,
|
||||
style: { maxWidth: '600px', maxHeight: '400px', overflow: 'auto' }
|
||||
}, {
|
||||
trigger: () => h('div', { style: 'display: flex; overflow: hidden;' }, createImages()),
|
||||
default: () => createImages()
|
||||
})
|
||||
return h(
|
||||
NPopover,
|
||||
{
|
||||
trigger: 'hover',
|
||||
displayDirective: 'show',
|
||||
keepAliveOnHover: true,
|
||||
style: { maxWidth: '600px', maxHeight: '400px', overflow: 'auto' },
|
||||
},
|
||||
{
|
||||
trigger: () =>
|
||||
h('div', { style: 'display: flex; overflow: hidden;' }, createImages()),
|
||||
default: () => createImages(),
|
||||
}
|
||||
)
|
||||
}
|
||||
return '-'
|
||||
} else {
|
||||
return fieldData.value || '-'
|
||||
}
|
||||
}
|
||||
},
|
||||
})),
|
||||
]
|
||||
|
||||
@ -268,18 +351,15 @@ const detailSections = computed(() => {
|
||||
|
||||
const calcFlow = computed(() => props.detailData?.calculation_result?.flow || [])
|
||||
|
||||
|
||||
|
||||
const renderedFlowHtml = computed(() => {
|
||||
return marked.parse(reportContent.value || mockReportMarkdown)
|
||||
})
|
||||
|
||||
|
||||
// 证书相关功能
|
||||
const handleUploadCertificate = () => {
|
||||
certificateModalMode.value = 'upload'
|
||||
certificateData.value = {
|
||||
detailData: props.detailData
|
||||
detailData: props.detailData,
|
||||
}
|
||||
certificateModalVisible.value = true
|
||||
}
|
||||
@ -290,20 +370,23 @@ const handleViewCertificate = () => {
|
||||
const formatFiles = (urlData) => {
|
||||
if (!urlData) return []
|
||||
// Handle string (single or comma-separated)
|
||||
const urls = typeof urlData === 'string' ? urlData.split(',') : (Array.isArray(urlData) ? urlData : [])
|
||||
const urls =
|
||||
typeof urlData === 'string' ? urlData.split(',') : Array.isArray(urlData) ? urlData : []
|
||||
|
||||
return urls.filter(u => u).map((url, index) => ({
|
||||
id: String(index),
|
||||
name: url.substring(url.lastIndexOf('/') + 1) || 'unknown',
|
||||
status: 'finished',
|
||||
url: url
|
||||
}))
|
||||
return urls
|
||||
.filter((u) => u)
|
||||
.map((url, index) => ({
|
||||
id: String(index),
|
||||
name: url.substring(url.lastIndexOf('/') + 1) || 'unknown',
|
||||
status: 'finished',
|
||||
url: url,
|
||||
}))
|
||||
}
|
||||
|
||||
certificateData.value = {
|
||||
reportFiles: formatFiles(props.detailData?.report_url),
|
||||
certificateFiles: formatFiles(props.detailData?.certificate_url),
|
||||
detailData: props.detailData
|
||||
detailData: props.detailData,
|
||||
}
|
||||
certificateModalVisible.value = true
|
||||
}
|
||||
@ -312,21 +395,20 @@ const handleCertificateConfirm = async (data) => {
|
||||
console.log('证书数据:', data)
|
||||
|
||||
try {
|
||||
const certificateUrl = data.certificateFiles?.map(f => f.url).filter(Boolean) || []
|
||||
const reportUrl = data.reportFiles?.map(f => f.url).filter(Boolean) || []
|
||||
const certificateUrl = data.certificateFiles?.map((f) => f.url).filter(Boolean) || []
|
||||
const reportUrl = data.reportFiles?.map((f) => f.url).filter(Boolean) || []
|
||||
|
||||
// 现在改为只能上传 1 张
|
||||
const payload = {
|
||||
...props.detailData,
|
||||
certificate_url: certificateUrl?.[0],
|
||||
report_url: reportUrl?.[0],
|
||||
status: 'success'
|
||||
status: 'success',
|
||||
}
|
||||
console.log("🔥🔥🔥🔥🔥🔥🔥 ~ handleCertificateConfirm ~ payload:", payload);
|
||||
|
||||
await api.updateValuation(payload)
|
||||
|
||||
$message.success('上传并通知成功')
|
||||
$message.success('上传成功')
|
||||
certificateModalVisible.value = false
|
||||
emit('back') // 或者 emit('refresh') 取决于需求,这里假设返回列表或刷新
|
||||
} catch (error) {
|
||||
@ -359,12 +441,7 @@ const handleCertificateConfirm = async (data) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NTabs
|
||||
v-model:value="activeDetailTab"
|
||||
type="line"
|
||||
size="large"
|
||||
class="audit-tabs"
|
||||
>
|
||||
<NTabs v-model:value="activeDetailTab" type="line" size="large" class="audit-tabs">
|
||||
<NTabPane name="audit" tab="审核信息">
|
||||
<NSpin :show="loading">
|
||||
<div v-for="section in detailSections" :key="section.key" class="detail-section">
|
||||
@ -395,19 +472,11 @@ const handleCertificateConfirm = async (data) => {
|
||||
|
||||
<!-- 证书按钮 -->
|
||||
<div class="certificate-actions">
|
||||
<NButton
|
||||
v-if="mode === 'approve'"
|
||||
type="primary"
|
||||
@click="handleUploadCertificate"
|
||||
>
|
||||
<NButton v-if="mode === 'approve'" type="primary" @click="handleUploadCertificate">
|
||||
<TheIcon icon="mdi:upload" :size="16" class="mr-4" />
|
||||
上传证书
|
||||
</NButton>
|
||||
<NButton
|
||||
v-else
|
||||
type="info"
|
||||
@click="handleViewCertificate"
|
||||
>
|
||||
<NButton v-else type="info" @click="handleViewCertificate">
|
||||
<TheIcon icon="mdi:eye" :size="16" class="mr-4" />
|
||||
查看证书
|
||||
</NButton>
|
||||
@ -441,7 +510,7 @@ const handleCertificateConfirm = async (data) => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: -16px 0px 10px ;
|
||||
margin: -16px 0px 10px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@ -567,10 +636,23 @@ const handleCertificateConfirm = async (data) => {
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.markdown-body :deep(h1) { font-size: 2em; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
||||
.markdown-body :deep(h2) { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
||||
.markdown-body :deep(h3) { font-size: 1.25em; }
|
||||
.markdown-body :deep(p) { margin-top: 0; margin-bottom: 16px; }
|
||||
.markdown-body :deep(h1) {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
.markdown-body :deep(h2) {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
.markdown-body :deep(h3) {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.markdown-body :deep(p) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.markdown-body :deep(blockquote) {
|
||||
margin: 0 0 16px;
|
||||
padding: 0 1em;
|
||||
@ -592,7 +674,8 @@ const handleCertificateConfirm = async (data) => {
|
||||
.markdown-body :deep(table tr:nth-child(2n)) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
.markdown-body :deep(table th), .markdown-body :deep(table td) {
|
||||
.markdown-body :deep(table th),
|
||||
.markdown-body :deep(table td) {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
@ -603,7 +686,7 @@ const handleCertificateConfirm = async (data) => {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(27,31,35,0.05);
|
||||
background-color: rgba(27, 31, 35, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.markdown-body :deep(pre) {
|
||||
@ -767,6 +850,4 @@ const handleCertificateConfirm = async (data) => {
|
||||
color: #999;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,15 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import {
|
||||
NModal,
|
||||
NCard,
|
||||
NButton,
|
||||
NUpload,
|
||||
NText,
|
||||
NImage,
|
||||
NImageGroup,
|
||||
useMessage
|
||||
} from 'naive-ui'
|
||||
import { NModal, NCard, NButton, NUpload, NText, NImage, NImageGroup, useMessage } from 'naive-ui'
|
||||
// 临时移除图标导入以解决模块解析问题
|
||||
// import { DownloadIcon } from '@vicons/tabler'
|
||||
|
||||
@ -51,8 +42,14 @@ watch(
|
||||
if (val) {
|
||||
if (props.mode === 'view') {
|
||||
// 查看模式,加载已有数据
|
||||
reportFileList.value = (props.certificateData?.reportFiles || []).map(f => ({ ...f, status: 'finished' }))
|
||||
certificateFileList.value = (props.certificateData?.certificateFiles || []).map(f => ({ ...f, status: 'finished' }))
|
||||
reportFileList.value = (props.certificateData?.reportFiles || []).map((f) => ({
|
||||
...f,
|
||||
status: 'finished',
|
||||
}))
|
||||
certificateFileList.value = (props.certificateData?.certificateFiles || []).map((f) => ({
|
||||
...f,
|
||||
status: 'finished',
|
||||
}))
|
||||
} else {
|
||||
// 上传模式,清空数据
|
||||
reportFileList.value = []
|
||||
@ -70,16 +67,19 @@ const handleClose = () => {
|
||||
// 确认操作
|
||||
// 确认操作
|
||||
const handleConfirm = () => {
|
||||
const getFiles = (list) => list.map(f => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
url: f.url,
|
||||
type: f.type
|
||||
})).filter(f => f.url)
|
||||
const getFiles = (list) =>
|
||||
list
|
||||
.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
url: f.url,
|
||||
type: f.type,
|
||||
}))
|
||||
.filter((f) => f.url)
|
||||
|
||||
emit('confirm', {
|
||||
reportFiles: getFiles(reportFileList.value),
|
||||
certificateFiles: getFiles(certificateFileList.value)
|
||||
certificateFiles: getFiles(certificateFileList.value),
|
||||
})
|
||||
handleClose()
|
||||
}
|
||||
@ -154,15 +154,12 @@ const handleCertificateUploadFinish = ({ file, event }) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return props.mode === 'upload' ? '上传' : '查看'
|
||||
})
|
||||
|
||||
const isUploadMode = computed(() => props.mode === 'upload')
|
||||
|
||||
|
||||
// 下载报告
|
||||
const handleDownloadReport = async () => {
|
||||
try {
|
||||
@ -200,13 +197,14 @@ const handleSmsNotify = async () => {
|
||||
}
|
||||
message.loading('正在发送短信...')
|
||||
await api.sendSmsReport({
|
||||
phone: phone
|
||||
phone: phone,
|
||||
})
|
||||
message.success('短信发送成功')
|
||||
handleConfirm()
|
||||
handleClose()
|
||||
emit('confirm', {
|
||||
reportFiles: [],
|
||||
certificateFiles: []
|
||||
certificateFiles: [],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@ -231,9 +229,7 @@ const handleSmsNotify = async () => {
|
||||
<div class="upload-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title">报告:</div>
|
||||
<NButton text type="primary" @click="handleDownloadReport">
|
||||
点击下载原版报告
|
||||
</NButton>
|
||||
<NButton text type="primary" @click="handleDownloadReport"> 点击下载原版报告 </NButton>
|
||||
</div>
|
||||
<div class="upload-content">
|
||||
<NUpload
|
||||
@ -275,19 +271,12 @@ const handleSmsNotify = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<NButton @click="handleClose">取消</NButton>
|
||||
<NButton v-if="isUploadMode" type="primary" @click="handleConfirm">
|
||||
上传
|
||||
</NButton>
|
||||
<NButton v-if="isUploadMode" type="primary" @click="handleSmsNotify">
|
||||
短信通知
|
||||
</NButton>
|
||||
<NButton v-else type="primary" @click="handleConfirm">
|
||||
确定
|
||||
</NButton>
|
||||
<NButton v-if="isUploadMode" type="primary" @click="handleConfirm"> 上传 </NButton>
|
||||
<NButton v-if="isUploadMode" type="primary" @click="handleSmsNotify"> 短信通知 </NButton>
|
||||
<NButton v-else type="primary" @click="handleConfirm"> 确定 </NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
@ -353,7 +342,6 @@ const handleSmsNotify = async () => {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
@ -399,7 +387,7 @@ const handleSmsNotify = async () => {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
@ -429,7 +417,6 @@ const handleSmsNotify = async () => {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
/* 底部按钮 */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
@ -437,12 +424,10 @@ const handleSmsNotify = async () => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.certificate-modal {
|
||||
width: 95vw !important;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -148,7 +148,7 @@ export const formatPlatformAccounts = (accounts = {}) => {
|
||||
if (!info) return `${label}:-`
|
||||
return `${label}:${info.account || '-'}(赞${formatNumberValue(info.likes)} / 评${formatNumberValue(
|
||||
info.comments
|
||||
)} / 转${formatNumberValue(info.shares)})`
|
||||
)} / 转${formatNumberValue(info.shares)}/ 七日浏览量${formatNumberValue(info.views)})`
|
||||
})
|
||||
return list.length ? list : ['暂无账号信息']
|
||||
}
|
||||
|
||||
BIN
web1/dist.zip
Normal file
BIN
web1/dist.zip
Normal file
Binary file not shown.
@ -5,6 +5,11 @@ export default {
|
||||
getUserInfo: () => request.get('/base/userinfo'),
|
||||
getUserMenu: () => request.get('/base/usermenu'),
|
||||
getUserApi: () => request.get('/base/userapi'),
|
||||
deleteAccount: (data) => request.delete('/app-user/account', {
|
||||
data: {
|
||||
code: data.code // Body 中携带 code
|
||||
}
|
||||
}),
|
||||
// 手机号
|
||||
registerPhone: (data) => request.post('/app-user/register', data, { noNeedToken: true }),
|
||||
loginPhone: (data) => request.post('/app-user/login', data, { noNeedToken: true }),
|
||||
@ -18,6 +23,7 @@ export default {
|
||||
getHistoryList: (params) => request.get('/app-valuations/', { params }),
|
||||
valuations: (data = {}) => request.post('/app-valuations/', data),
|
||||
deleteValuations: (params = {}) => request.delete(`/app-valuations/${params.id}`),
|
||||
getValuation: (id) => request.get(`/app-valuations/${id}`),
|
||||
// profile
|
||||
updatePassword: (data = {}) => request.post('/base/update_password', data),
|
||||
// users
|
||||
|
||||
BIN
web1/src/assets/icon/iconLogout.png
Normal file
BIN
web1/src/assets/icon/iconLogout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
web1/src/assets/icon/评估完成.png
Normal file
BIN
web1/src/assets/icon/评估完成.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -52,6 +52,14 @@ export const basicRoutes = [
|
||||
title: '抬头管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Logout',
|
||||
path: 'logout',
|
||||
component: () => import('@/views/user-center/components/Logout.vue'),
|
||||
meta: {
|
||||
title: '账号注销',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,6 +1,21 @@
|
||||
<template>
|
||||
<div class="pages">
|
||||
<AppHeader class="page-header" />
|
||||
<!-- 页面初始化 loading -->
|
||||
<div
|
||||
v-if="pageLoading"
|
||||
class="right"
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<n-spin size="large" />
|
||||
<div style="font-size: 16px; color: #999999; margin-top: 20px">加载中...</div>
|
||||
</div>
|
||||
<!-- 左侧边栏 - 已隐藏 -->
|
||||
<!-- <div class="left">
|
||||
<img style="width: 198px; height: 32px; margin: 20px;" src="@/assets/images/logo.png" alt="" @click="navToLogin">
|
||||
@ -11,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div v-if="status == 'create'" class="right">
|
||||
<div v-if="!pageLoading && status == 'create'" class="right">
|
||||
<StepProgressBar
|
||||
style="width: 800px; margin: auto; margin-top: 40px"
|
||||
:steps="steps"
|
||||
@ -186,7 +201,7 @@
|
||||
<template #trigger>
|
||||
<img style="width: 14px; height: 14px" src="@/assets/images/ps.png" alt="" />
|
||||
</template>
|
||||
填写近3年资产相关的年收益,用以判断资产的商业价值
|
||||
填写近3年资产相关的年收益,用以判断资产的商业价值,若机构成立时间不足三年,其成立前年份的收益值,可按成立后最近一个完整年度的数据填报
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
@ -300,7 +315,9 @@
|
||||
:default-file-list="modalForm.inheritor_certificates"
|
||||
:max="uploadLimit"
|
||||
list-type="image-card"
|
||||
:on-before-upload="(options) => handleBeforeUpload(options, 'inheritor_certificates')"
|
||||
:on-before-upload="
|
||||
(options) => handleBeforeUpload(options, 'inheritor_certificates')
|
||||
"
|
||||
@finish="handleFinish3"
|
||||
@remove="delete3"
|
||||
>
|
||||
@ -564,35 +581,48 @@
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<n-select
|
||||
style="width: 330px"
|
||||
v-model:value="modalForm.online_accounts[0]"
|
||||
placeholder="请选择"
|
||||
:options="accountsOptions"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[1]"
|
||||
placeholder="请输入账号"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[2]"
|
||||
placeholder="请输入点赞数量"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
type="number"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[3]"
|
||||
placeholder="请输入评论数量"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
type="number"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[4]"
|
||||
placeholder="请输入分享数量"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
type="number"
|
||||
/>
|
||||
<div>
|
||||
<div style="display: flex">
|
||||
<n-select
|
||||
style="width: 220px"
|
||||
v-model:value="modalForm.online_accounts[0]"
|
||||
placeholder="请选择"
|
||||
:options="accountsOptions"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[1]"
|
||||
placeholder="请输入账号"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[2]"
|
||||
placeholder="请输入点赞数量"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; margin-top: 12px">
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[3]"
|
||||
placeholder="请输入评论数量"
|
||||
style="width: 220px"
|
||||
type="number"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[4]"
|
||||
placeholder="请输入分享数量"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
type="number"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="modalForm.online_accounts[5]"
|
||||
placeholder="请输入近七天点击量"
|
||||
style="width: 220px; margin-left: 10px"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
<n-grid v-if="currentStep == 4" :cols="24" :x-gap="0">
|
||||
@ -742,7 +772,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="status == 'success'" class="right">
|
||||
<div v-if="!pageLoading && status != 'fail' && status != 'create'" class="right">
|
||||
<div class="price-container" :style="{ backgroundImage: `url(${backgroundImg})` }">
|
||||
<div>¥{{ parseInt(selectedObj?.calculation_result?.final_value_ab) }}</div>
|
||||
<div style="font-size: 20px; color: #303133; line-height: 20px">最终评估结果</div>
|
||||
@ -866,20 +896,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="retry"
|
||||
@click="retry"
|
||||
style="background: #f8f8f8; color: #303133; line-height: 40px"
|
||||
>
|
||||
<img
|
||||
style="width: 16px; height: 16px; transform: translate(0, 2px); margin-right: 6px"
|
||||
src="@/assets/images/retry.png"
|
||||
alt=""
|
||||
/>
|
||||
重新评估
|
||||
|
||||
<div style="display: flex; justify-content: center">
|
||||
<div class="retry" @click="gotoHome">返回首页</div>
|
||||
<div
|
||||
class="retry"
|
||||
@click="gotoHistory"
|
||||
style="background: #f8f8f8; color: #303133; line-height: 40px"
|
||||
>
|
||||
查看估值记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="status == 'fail'" class="right" style="text-align: center">
|
||||
<div v-if="!pageLoading && status == 'fail'" class="right" style="text-align: center">
|
||||
<img
|
||||
style="width: 100px; height: 100px; margin-top: 30vh"
|
||||
src="@/assets/images/fail.png"
|
||||
@ -898,17 +927,19 @@
|
||||
重新评估
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="status == 'pending'" class="right" style="text-align: center">
|
||||
<img
|
||||
style="width: 100px; height: 100px; margin-top: 30vh"
|
||||
src="@/assets/images/loading.png"
|
||||
alt=""
|
||||
/>
|
||||
<div style="font-size: 20px">评估中…</div>
|
||||
<!-- <div v-if="!pageLoading && status == 'pending'" class="right" style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
">
|
||||
<img class="loading-icon" style="width: 100px; height: 100px" src="@/assets/images/loading.png" alt="" />
|
||||
<div style="font-size: 20px; margin-top: 20px">评估中</div>
|
||||
<div style="font-size: 14px; color: #999999; margin-top: 10px">
|
||||
您的知识产权和非物质文化遗产的价值正在评估中,请耐心等候
|
||||
您的知识产权和非物质文化遗产的价值正在评估中,预计30秒到1分钟,请耐心等候
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -927,6 +958,7 @@ import backgroundImg4 from '@/assets/images/circleg.png'
|
||||
import backgroundImg5 from '@/assets/images/circley.png'
|
||||
import backgroundImg6 from '@/assets/images/circlec.png'
|
||||
const status = ref('create')
|
||||
const pageLoading = ref(false) // 页面初始化 loading 状态
|
||||
const message = useMessage()
|
||||
const modalFormRef = ref<FormInst | null>(null)
|
||||
const isSelected = ref<Number | null>(null)
|
||||
@ -1161,7 +1193,7 @@ const modalRules = {
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur'],
|
||||
len: 5,
|
||||
len: 6,
|
||||
fields: [
|
||||
{
|
||||
required: true,
|
||||
@ -1188,6 +1220,11 @@ const modalRules = {
|
||||
message: '请输入账号分享量',
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
message: '请输入近七天点击量',
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -1490,7 +1527,7 @@ const nextStep = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
const submit = () => {
|
||||
const submit = async () => {
|
||||
const data = {
|
||||
...modalForm,
|
||||
industry: industryOptions.value.find((item) => item.value === modalForm.industry).label,
|
||||
@ -1537,6 +1574,7 @@ const submit = () => {
|
||||
likes: modalForm.online_accounts[2],
|
||||
comments: modalForm.online_accounts[3],
|
||||
shares: modalForm.online_accounts[4],
|
||||
views: modalForm.online_accounts[5],
|
||||
},
|
||||
}
|
||||
} else if (modalForm.online_accounts[0] == '1') {
|
||||
@ -1546,6 +1584,7 @@ const submit = () => {
|
||||
likes: modalForm.online_accounts[2],
|
||||
comments: modalForm.online_accounts[3],
|
||||
shares: modalForm.online_accounts[4],
|
||||
views: modalForm.online_accounts[5],
|
||||
},
|
||||
}
|
||||
} else if (modalForm.online_accounts[0] == '2') {
|
||||
@ -1555,6 +1594,7 @@ const submit = () => {
|
||||
likes: modalForm.online_accounts[2],
|
||||
comments: modalForm.online_accounts[3],
|
||||
shares: modalForm.online_accounts[4],
|
||||
views: modalForm.online_accounts[5],
|
||||
},
|
||||
}
|
||||
} else if (modalForm.online_accounts[0] == '3') {
|
||||
@ -1564,21 +1604,65 @@ const submit = () => {
|
||||
likes: modalForm.online_accounts[2],
|
||||
comments: modalForm.online_accounts[3],
|
||||
shares: modalForm.online_accounts[4],
|
||||
views: modalForm.online_accounts[5],
|
||||
},
|
||||
}
|
||||
}
|
||||
data.inheritor_age_count = data.inheritor_ages
|
||||
message.success('正在评估中,请前往历史记录查看')
|
||||
loading.value = true
|
||||
// message.success('正在评估中,请前往历史记录查看')
|
||||
pageLoading.value = true
|
||||
api.valuations(data).then((res) => {
|
||||
loading.value = false
|
||||
getHistoryList()
|
||||
message.success('评估完成 将于7个工作日内生成报告与证书 以短信形式通知')
|
||||
router.push('/user-center')
|
||||
// loading.value = false
|
||||
// getHistoryList()
|
||||
// message.success('评估完成 将于7个工作日内生成报告与证书 以短信形式通知')
|
||||
// router.push('/user-center')
|
||||
// status.value = 'success'
|
||||
// setTimeout(() => {
|
||||
// window.location.reload()
|
||||
// }, 1000)
|
||||
// 增加最大重试次数限制,避免无限轮询(可选)
|
||||
|
||||
const pollWithLimit = async (maxRetries = 60, valuation_id) => {
|
||||
let retries = 0
|
||||
const poll = async () => {
|
||||
retries++
|
||||
if (retries > maxRetries) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const res = await api.getValuation(valuation_id)
|
||||
if (res.data && res.data.calculation_result) {
|
||||
pageLoading.value = false
|
||||
selectTimeBox(res.data)
|
||||
dialog.success({
|
||||
title: '评估完成',
|
||||
content: '将于7个工作日内生成报告与证书以短信形式通知',
|
||||
positiveText: '我已知晓',
|
||||
negativeText: '取消',
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await poll())
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await poll())
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await poll()
|
||||
}
|
||||
console.log(res)
|
||||
if (res.code == 200) {
|
||||
pollWithLimit(60, res.data.valuation_id)
|
||||
}
|
||||
})
|
||||
}
|
||||
const getHistoryList = () => {
|
||||
@ -1622,6 +1706,14 @@ async function retry() {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
function gotoHome() {
|
||||
router.push('/home')
|
||||
}
|
||||
function gotoHistory() {
|
||||
router.push('/user-center/history')
|
||||
}
|
||||
|
||||
const dialog = useDialog()
|
||||
const openSubmitConfirm = () => {
|
||||
dialog.warning({
|
||||
@ -1632,22 +1724,7 @@ const openSubmitConfirm = () => {
|
||||
onPositiveClick: () => submit(),
|
||||
})
|
||||
}
|
||||
const deleteValuations = (item) => {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: '是否确认删除记录?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
draggable: true,
|
||||
onPositiveClick: () => {
|
||||
api.deleteValuations(item).then(() => {
|
||||
message.success('删除成功')
|
||||
getHistoryList()
|
||||
status.value = 'create'
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts, EChartsOption } from 'echarts'
|
||||
import { useUserStore } from '@/store'
|
||||
@ -1739,6 +1816,12 @@ onMounted(async () => {
|
||||
page: 1,
|
||||
page_size: 99,
|
||||
}
|
||||
|
||||
// 如果有 id 参数,说明是从估值记录进入,需要显示 loading
|
||||
if (route.query.id) {
|
||||
pageLoading.value = true
|
||||
}
|
||||
|
||||
await getHistoryList()
|
||||
|
||||
if (route.query.id) {
|
||||
@ -1747,6 +1830,8 @@ onMounted(async () => {
|
||||
if (targetItem) {
|
||||
selectTimeBox(targetItem)
|
||||
}
|
||||
// 数据加载完成,关闭 loading
|
||||
pageLoading.value = false
|
||||
}
|
||||
// 使用await提高可读性
|
||||
const res = await api.getIndustryList(params)
|
||||
@ -1783,6 +1868,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f8f8f8;
|
||||
|
||||
/* 左侧边栏样式 - 已隐藏 */
|
||||
/* .left {
|
||||
width: 270px;
|
||||
@ -1802,17 +1888,20 @@ onMounted(async () => {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
float: right;
|
||||
transform: translate(0, 2px);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.timeBox {
|
||||
margin-top: 10px;
|
||||
width: 200px;
|
||||
@ -1826,6 +1915,7 @@ onMounted(async () => {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeBox2 {
|
||||
margin-top: 10px;
|
||||
width: 200px;
|
||||
@ -1849,25 +1939,30 @@ onMounted(async () => {
|
||||
background: #eeeeee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin: auto;
|
||||
margin-top: 30px;
|
||||
width: 900px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: inline-block;
|
||||
width: 330px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-title-box {
|
||||
margin: auto;
|
||||
margin-top: 40px;
|
||||
width: 900px;
|
||||
}
|
||||
|
||||
.title-left {
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
@ -1876,6 +1971,7 @@ onMounted(async () => {
|
||||
display: inline-block;
|
||||
transform: translate(0, 3px);
|
||||
}
|
||||
|
||||
.title-form {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
@ -1886,11 +1982,13 @@ onMounted(async () => {
|
||||
font-style: normal;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.submit-box {
|
||||
width: 900px;
|
||||
margin: auto;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
@ -1920,9 +2018,10 @@ onMounted(async () => {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbbbbb;
|
||||
}
|
||||
|
||||
.retry {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
margin: 12px;
|
||||
margin-top: 40px;
|
||||
width: 180px;
|
||||
height: 40px;
|
||||
@ -1931,6 +2030,7 @@ onMounted(async () => {
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.price-container {
|
||||
@ -1945,11 +2045,13 @@ onMounted(async () => {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/* 文本样式 */
|
||||
color: #b5906b; /* 假设白色文本在背景上更清晰 */
|
||||
color: #b5906b;
|
||||
/* 假设白色文本在背景上更清晰 */
|
||||
font-size: 52px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.credibility-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -1964,9 +2066,11 @@ onMounted(async () => {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.credibility-label {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.credibility-stars {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@ -1974,15 +2078,18 @@ onMounted(async () => {
|
||||
height: 18px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stars-base,
|
||||
.stars-fill {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.stars-base {
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
.stars-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -1991,11 +2098,13 @@ onMounted(async () => {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.credibility-percent {
|
||||
color: #595959;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.secondary-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -2011,6 +2120,7 @@ onMounted(async () => {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.score-box {
|
||||
padding: 30px;
|
||||
width: 320px;
|
||||
@ -2019,11 +2129,13 @@ onMounted(async () => {
|
||||
margin-left: 25px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-top: 0;
|
||||
font-size: 12px;
|
||||
color: #9c9c9c;
|
||||
}
|
||||
|
||||
.upload-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -2039,3 +2151,19 @@ input[type='number']::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -185,11 +185,7 @@
|
||||
</n-form-item>
|
||||
|
||||
<div class="btn-container">
|
||||
<button
|
||||
class="primary-btn"
|
||||
:disabled="!isFormValid"
|
||||
@click="handleUploadSubmit"
|
||||
>
|
||||
<button class="primary-btn" :disabled="!isFormValid" @click="handleUploadSubmit">
|
||||
确认上传
|
||||
</button>
|
||||
</div>
|
||||
@ -295,11 +291,7 @@ const message = useMessage()
|
||||
|
||||
// 计算表单是否有效
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
uploadedFiles.value.length > 0 &&
|
||||
!!formModel.invoiceHeader &&
|
||||
!!formModel.invoiceType
|
||||
)
|
||||
return uploadedFiles.value.length > 0 && !!formModel.invoiceHeader && !!formModel.invoiceType
|
||||
})
|
||||
|
||||
const handleUploadFinish = ({ file, event }) => {
|
||||
@ -384,10 +376,10 @@ defineExpose({
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background: #a30113;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
|
||||
@ -315,10 +315,10 @@ const rowKey = (row) => row.id ?? row.name ?? row.company_name ?? row.taxId ?? r
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background: #a30113;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
|
||||
368
web1/src/views/user-center/components/Logout.vue
Normal file
368
web1/src/views/user-center/components/Logout.vue
Normal file
@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="header-left">
|
||||
<div class="title-bar"></div>
|
||||
<div class="title-text">账号注销</div>
|
||||
</div>
|
||||
<div v-if="deleteAble" class="logout-container">
|
||||
<!-- 手机号信息 -->
|
||||
<div class="phone-info">当前绑定的手机号码为:{{ maskedPhone }}</div>
|
||||
|
||||
<!-- 验证码输入区域 -->
|
||||
<div class="code-form">
|
||||
<n-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<n-form-item label="" prop="code" class="verify-code-item">
|
||||
<!-- 验证码输入框 + 按钮 容器 -->
|
||||
<div class="code-input-wrapper">
|
||||
<n-input
|
||||
v-model:value="form.code"
|
||||
placeholder="手机验证码"
|
||||
maxlength="6"
|
||||
@input="handleCodeInput"
|
||||
class="custom-code-input"
|
||||
:bordered="false"
|
||||
/>
|
||||
<n-button
|
||||
class="send-code-btn"
|
||||
text
|
||||
@click="handleSendCode"
|
||||
:disabled="countDown > 0"
|
||||
>
|
||||
{{ countDown > 0 ? `${countDown}s后重新获取` : '获取验证码' }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<!-- 注销按钮 -->
|
||||
<n-button class="logout-btn" @click="handleLogout" :disabled="!isFormValid"> 确定 </n-button>
|
||||
</div>
|
||||
<div v-else class="logout-container" style="text-align: center">
|
||||
<img src="@/assets/icon/评估完成.png" mode="scaleToFill" class="warn-icon" />
|
||||
<p class="warn-title">无法注销</p>
|
||||
<p class="warn-content">当前有未完成的估值记录/剩余估值次数,无法注销账号</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
import { useMessage, useDialog } from 'naive-ui' // 引入 Naive UI 消息/弹窗
|
||||
|
||||
// 初始化 Naive UI 消息和弹窗
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 接收父组件传递的手机号
|
||||
const props = defineProps({
|
||||
userPhone: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
deleteAble: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emits = defineEmits(['send-code', 'confirm-logout'])
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
code: '',
|
||||
})
|
||||
// 倒计时
|
||||
const countDown = ref(0)
|
||||
let timer = null
|
||||
|
||||
// 手机号脱敏处理
|
||||
const maskedPhone = computed(() => {
|
||||
if (!props.userPhone) return ''
|
||||
return `${props.userPhone.slice(0, 3)}****${props.userPhone.slice(7, 11)}`
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = ref({
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ len: 6, message: '请输入6位验证码', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
|
||||
// 表单是否有效
|
||||
const isFormValid = computed(() => {
|
||||
return props.userPhone && form.value.code.length === 6
|
||||
})
|
||||
|
||||
// 验证码输入限制
|
||||
const handleCodeInput = () => {
|
||||
if (form.value.code.length > 6) {
|
||||
form.value.code = form.value.code.slice(0, 6)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const handleSendCode = async () => {
|
||||
if (!props.userPhone) {
|
||||
message.warning('手机号不存在,无法发送验证码')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用父组件的发送验证码方法
|
||||
const res = await emits('send-code', props.userPhone)
|
||||
if (res) {
|
||||
// 启动倒计时
|
||||
countDown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countDown.value--
|
||||
if (countDown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
message.success('验证码发送成功')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交注销
|
||||
const handleLogout = () => {
|
||||
// 表单验证
|
||||
formRef.value.validate((errors) => {
|
||||
if (!errors) {
|
||||
emits('confirm-logout', {
|
||||
code: form.value.code,
|
||||
})
|
||||
} else {
|
||||
message.warning('请填写正确的6位验证码')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面卸载时清除定时器
|
||||
watch(
|
||||
() => countDown.value,
|
||||
(val) => {
|
||||
if (val <= 0 && timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 根容器样式 */
|
||||
.logout-container {
|
||||
width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 30px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.logout-title {
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
line-height: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 手机号信息样式 */
|
||||
.phone-info {
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
/* 表单容器 */
|
||||
.code-form {
|
||||
margin-bottom: 40px;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
/* 验证码表单项 */
|
||||
.verify-code-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* 验证码输入框 + 按钮 容器(核心布局) */
|
||||
.code-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
gap: 10px; /* 输入框和按钮间距 */
|
||||
}
|
||||
|
||||
/* 自定义验证码输入框样式 */
|
||||
.custom-code-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e5e5e5;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.warn-icon {
|
||||
margin-top: 80px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.warn-title {
|
||||
margin-top: 20px;
|
||||
font-size: 20px;
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.warn-content {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
}
|
||||
/* 穿透修改 Naive UI 输入框样式 */
|
||||
:deep(.custom-code-input .n-input-wrapper) {
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
:deep(.custom-code-input .n-input__input-el) {
|
||||
height: 100%;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 输入框 hover 状态 */
|
||||
.custom-code-input:hover {
|
||||
border-color: #a3011380; /* 半透明红 */
|
||||
}
|
||||
|
||||
/* 输入框 聚焦状态 */
|
||||
:deep(.custom-code-input .n-input-wrapper:focus-within) {
|
||||
border-color: #a30113;
|
||||
box-shadow: 0 0 0 2px rgba(163, 1, 19, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 输入框 错误状态 */
|
||||
:deep(.n-form-item--error .custom-code-input) {
|
||||
border-color: #d03050;
|
||||
}
|
||||
:deep(.n-form-item--error .custom-code-input .n-input-wrapper:focus-within) {
|
||||
border-color: #d03050;
|
||||
box-shadow: 0 0 0 2px rgba(208, 48, 80, 0.1);
|
||||
}
|
||||
|
||||
/* 验证码按钮样式 */
|
||||
.send-code-btn {
|
||||
height: 100%;
|
||||
min-width: 120px;
|
||||
padding: 0 15px;
|
||||
border-radius: 4px;
|
||||
color: #a30113;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
background: #a3011332;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 按钮 hover(非禁用) */
|
||||
.send-code-btn:not(:disabled):hover {
|
||||
background: #a3011332;
|
||||
color: #a30113;
|
||||
}
|
||||
|
||||
/* 按钮 点击态 */
|
||||
.send-code-btn:not(:disabled):active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 按钮 禁用态 */
|
||||
.send-code-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 注销按钮样式 */
|
||||
.logout-btn {
|
||||
margin: auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
width: 180px;
|
||||
height: 40px;
|
||||
background: #a30113;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 注销按钮 hover(非禁用) */
|
||||
.logout-btn:not(:disabled):hover {
|
||||
background: #a30113;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 注销按钮 禁用态 */
|
||||
.logout-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.title-bar {
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background: #a30113;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 表单错误提示位置调整 */
|
||||
:deep(.n-form-item-feedback-wrapper) {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
padding-top: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:deep(.custom-code-input) {
|
||||
/* Chrome/Safari/Edge */
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
/* Firefox */
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
:deep(.n-button__content) {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@ -6,6 +6,8 @@
|
||||
</div>
|
||||
<div class="user-phone">{{ userPhone || '18988880000' }}</div>
|
||||
<div class="valuation-count">剩余评估次数:{{ valuationCount }}</div>
|
||||
|
||||
<div class="logout-btn" @click="logout">退出登录</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-list">
|
||||
@ -29,24 +31,48 @@
|
||||
import iconHistory from '@/assets/icon/估值记录.png'
|
||||
import iconTransfer from '@/assets/icon/对公转账.png'
|
||||
import iconInvoice from '@/assets/icon/抬头管理.png'
|
||||
import iconLogout from '@/assets/icon/iconLogout.png'
|
||||
import { useDialog } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const dialog = useDialog()
|
||||
defineProps({
|
||||
userPhone: String,
|
||||
valuationCount: Number,
|
||||
currentMenu: String,
|
||||
menuList: Array
|
||||
menuList: Array,
|
||||
})
|
||||
|
||||
defineEmits(['menu-click'])
|
||||
|
||||
function getMenuIcon(id) {
|
||||
const iconMap = {
|
||||
'history': iconHistory,
|
||||
'transfer': iconTransfer,
|
||||
'invoice': iconInvoice
|
||||
history: iconHistory,
|
||||
transfer: iconTransfer,
|
||||
invoice: iconInvoice,
|
||||
logout: iconLogout,
|
||||
}
|
||||
return iconMap[id]
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await new Promise((resolve, reject) => {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确认退出登录',
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => resolve(),
|
||||
onNegativeClick: () => reject('cancel'),
|
||||
onClose: () => reject('cancel'),
|
||||
})
|
||||
})
|
||||
// 清除本地缓存
|
||||
localStorage.removeItem('ACCESS_TOKEN')
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -117,12 +143,12 @@ function getMenuIcon(id) {
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #F5F7FA;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: #FFF0F0;
|
||||
color: #A30113;
|
||||
background: #fff0f0;
|
||||
color: #a30113;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -153,6 +179,19 @@ function getMenuIcon(id) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 168px;
|
||||
height: 32px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-card {
|
||||
width: 100%;
|
||||
|
||||
@ -141,34 +141,44 @@ const columns = [
|
||||
NSpace,
|
||||
{ size: 8, justify: 'center' },
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
handleDownloadReport(row)
|
||||
},
|
||||
},
|
||||
{ default: () => '下载报告' }
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
handleDownloadCertificate(row)
|
||||
},
|
||||
},
|
||||
{ default: () => '下载证书' }
|
||||
),
|
||||
],
|
||||
default: () => {
|
||||
let arr = []
|
||||
if (row.report_download_urls.length) {
|
||||
arr.push(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
handleDownloadReport(row)
|
||||
},
|
||||
},
|
||||
{ default: () => '下载报告' }
|
||||
)
|
||||
)
|
||||
}
|
||||
if (row.certificate_download_urls.length)
|
||||
arr.push(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
handleDownloadCertificate(row)
|
||||
},
|
||||
},
|
||||
{ default: () => '下载证书' }
|
||||
)
|
||||
)
|
||||
|
||||
return arr
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
@ -217,10 +227,10 @@ onMounted(fetchHistory)
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background: #a30113;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
|
||||
@ -16,23 +16,19 @@
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="content-area">
|
||||
<!-- 返回按钮 (暂时注释) -->
|
||||
<!-- <div class="back-button" @click="handleBackToHome">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>返回首页</span>
|
||||
</div> -->
|
||||
|
||||
<!-- 子路由视图 -->
|
||||
<router-view
|
||||
:asset-list="assetList"
|
||||
:invoice-list="invoiceList"
|
||||
:user-phone="userPhone"
|
||||
:deleteAble="deleteAble"
|
||||
@return-home="handleBackToHome"
|
||||
@add-invoice="addInvoice"
|
||||
@update-invoice="updateInvoice"
|
||||
@delete-invoice="deleteInvoice"
|
||||
@upload-submit="uploadSubmit"
|
||||
@send-code="sendVerificationCode"
|
||||
@confirm-logout="confirmAccountLogout"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,12 +36,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, h, watch, computed } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useMessage, useDialog } from 'naive-ui' // 引入 Naive UI 消息/弹窗
|
||||
import api from '@/api'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import UserSidebar from './components/UserSidebar.vue'
|
||||
|
||||
// 初始化 Naive UI 消息和弹窗实例
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@ -55,12 +56,14 @@ const currentMenu = computed(() => {
|
||||
if (path.includes('/history')) return 'history'
|
||||
if (path.includes('/transfer')) return 'transfer'
|
||||
if (path.includes('/invoice')) return 'invoice'
|
||||
if (path.includes('/logout')) return 'logout' // 新增:注销菜单匹配
|
||||
return 'history'
|
||||
})
|
||||
|
||||
// 用户信息
|
||||
const userPhone = ref('')
|
||||
const valuationCount = ref(0)
|
||||
const deleteAble = ref(true)
|
||||
|
||||
// 资产列表
|
||||
const assetList = ref([])
|
||||
@ -68,7 +71,7 @@ const assetList = ref([])
|
||||
// 开票列表
|
||||
const invoiceList = ref([])
|
||||
|
||||
// 菜单列表
|
||||
// 菜单列表(新增注销菜单)
|
||||
const menuList = ref([
|
||||
{
|
||||
id: 'history',
|
||||
@ -82,6 +85,10 @@ const menuList = ref([
|
||||
id: 'invoice',
|
||||
label: '抬头管理',
|
||||
},
|
||||
{
|
||||
id: 'logout', // 新增:注销账号菜单
|
||||
label: '账号注销',
|
||||
},
|
||||
])
|
||||
|
||||
// 返回首页
|
||||
@ -91,7 +98,6 @@ function handleBackToHome() {
|
||||
|
||||
// 菜单点击
|
||||
function handleMenuClick(menu) {
|
||||
// 使用路由导航
|
||||
router.push(`/user-center/${menu.id}`)
|
||||
}
|
||||
|
||||
@ -114,10 +120,12 @@ async function loadData() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载估值记录失败:', error)
|
||||
$message.error('加载估值记录失败,请稍后重试')
|
||||
// 替换:ElMessage → message
|
||||
message.error('加载估值记录失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
//加载发票抬头列表
|
||||
|
||||
// 加载发票抬头列表
|
||||
async function loadInvoiceList() {
|
||||
try {
|
||||
const res = await api.getInvoiceHeaders()
|
||||
@ -127,37 +135,43 @@ async function loadInvoiceList() {
|
||||
}
|
||||
}
|
||||
|
||||
//添加发票抬头
|
||||
// 添加发票抬头
|
||||
async function addInvoice(data) {
|
||||
try {
|
||||
await api.addInvoiceHeaders(data)
|
||||
$message.success('添加成功')
|
||||
// 替换:ElMessage → message
|
||||
message.success('添加成功')
|
||||
loadInvoiceList()
|
||||
} catch (error) {
|
||||
console.error('新增发票抬头失败:', error)
|
||||
}
|
||||
}
|
||||
//更新发票抬头
|
||||
|
||||
// 更新发票抬头
|
||||
async function updateInvoice(data) {
|
||||
try {
|
||||
await api.updateInvoiceHeaders(data)
|
||||
$message.success('编辑成功')
|
||||
// 替换:ElMessage → message
|
||||
message.success('编辑成功')
|
||||
loadInvoiceList()
|
||||
} catch (error) {
|
||||
console.error('更新发票抬头失败:', error)
|
||||
}
|
||||
}
|
||||
//删除发票抬头
|
||||
|
||||
// 删除发票抬头
|
||||
async function deleteInvoice(data) {
|
||||
try {
|
||||
await api.deleteInvoiceHeaders(data.id)
|
||||
$message.success('删除成功')
|
||||
// 替换:ElMessage → message
|
||||
message.success('删除成功')
|
||||
loadInvoiceList()
|
||||
} catch (error) {
|
||||
console.error('删除发票抬头失败:', error)
|
||||
}
|
||||
}
|
||||
//上传对公转账凭证
|
||||
|
||||
// 上传对公转账凭证
|
||||
async function uploadSubmit(data) {
|
||||
try {
|
||||
console.log('上传对公转账凭证===', data)
|
||||
@ -167,6 +181,53 @@ async function uploadSubmit(data) {
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:发送注销验证码
|
||||
async function sendVerificationCode(phone) {
|
||||
try {
|
||||
await api.sendVerifyCode({ phone })
|
||||
message.success('验证码已发送,请注意查收')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:确认注销账号
|
||||
async function confirmAccountLogout(formData) {
|
||||
// 替换:ElMessageBox.confirm → dialog.warning
|
||||
await new Promise((resolve, reject) => {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '提交后账号将注销,不可撤回,确认注销账户?',
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => resolve(),
|
||||
onNegativeClick: () => reject('cancel'),
|
||||
onClose: () => reject('cancel'),
|
||||
})
|
||||
})
|
||||
|
||||
// 调用注销接口(传入手机号和验证码)
|
||||
api
|
||||
.deleteAccount({
|
||||
code: formData.code,
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res, '111111111111111')
|
||||
// 替换:ElMessage → message
|
||||
message.success('账号注销成功')
|
||||
// 清除本地缓存
|
||||
localStorage.removeItem('phone')
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, '111111111111111')
|
||||
deleteAble.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadInvoiceList()
|
||||
@ -200,26 +261,7 @@ onMounted(() => {
|
||||
min-height: calc(100vh - 100px); /* Adjust based on header + margin */
|
||||
position: relative;
|
||||
overflow: hidden; /* Ensure rounded corners */
|
||||
}
|
||||
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
color: #a30113;
|
||||
padding: 20px; /* 新增:添加内边距 */
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@ -232,9 +274,5 @@ onMounted(() => {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
11
估值字段.txt
11
估值字段.txt
@ -37,8 +37,8 @@
|
||||
|
||||
|
||||
export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
docker build -t zfc931912343/guzhi-fastapi-admin:v2.6 .
|
||||
docker push zfc931912343/guzhi-fastapi-admin:v2.6
|
||||
docker build -t zfc931912343/guzhi-fastapi-admin:v3.8 .
|
||||
docker push zfc931912343/guzhi-fastapi-admin:v3.8
|
||||
|
||||
|
||||
# 运行容器
|
||||
@ -68,11 +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:v2.5 && docker rm -f guzhi_pro && docker run -itd --name=guzhi_pro -p 8080:80 -v ~/guzhi-data/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v2.5
|
||||
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