Compare commits

...

157 Commits
master ... main

Author SHA1 Message Date
若拙_233
45aae516b2 fix: 上传短信并通知按钮逻辑修改 2025-12-19 13:57:30 +08:00
若拙_233
4110dca428 fix: 提交审核页面逻辑修改 2025-12-18 22:38:13 +08:00
f17c1678c8 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi 2025-12-18 19:14:06 +08:00
1157704d4b feat: Implement user account soft deletion with token blacklisting, admin management, and SMS verification tracking. 2025-12-18 19:14:03 +08:00
Wei_佳
17b56a1c19 docs: 更新“近三年机构收益/万元”字段的提示文本并优化部分组件属性排版 2025-12-18 14:30:07 +08:00
若拙_233
6718b51fb9 fix: 新增7天浏览量 2025-12-10 16:23:24 +08:00
若拙_233
58f16be457 fix: 新增账号注销功能 2025-12-09 17:51:12 +08:00
Wei_佳
90c0f85972 feat: 为加载图标添加旋转动画并优化评估中提示文案 2025-12-05 15:40:30 +08:00
Wei_佳
7819c60ace fix: 调整发票页面气泡提示框的显示位置。 2025-12-04 17:47:40 +08:00
Wei_佳
1d73f6ed54 feat: 页面初始化时添加加载状态及UI展示,并在加载期间隐藏其他内容 2025-12-04 17:36:44 +08:00
Wei_佳
6b5967a4bb Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat(valuation): 添加update_calc方法并更新相关字段类型
2025-12-04 17:23:31 +08:00
Wei_佳
8926e047d4 style: 调整评估中状态页面的布局和间距。 2025-12-04 15:34:29 +08:00
f1c1db580c refactor(user-center): 将企业转账组件中的n-button替换为普通button
简化按钮样式实现,移除不必要的属性配置
2025-12-04 14:44:23 +08:00
b10c357a56 feat(valuation): 添加update_calc方法并更新相关字段类型
为ValuationController添加update_calc方法用于计算更新
将updated_at等字段改为Optional类型
修复heritage_level字段获取方式
更新docker镜像版本至v2.7
2025-12-02 18:50:46 +08:00
Wei_佳
cd8170ac02 feat: 按钮文案居中 2025-12-02 17:48:26 +08:00
01ed8fb25b feat: 添加短信验证码绕过功能并更新API权限同步
- 添加SMS_BYPASS_CODE配置允许特定验证码绕过验证
- 实现角色与API权限的自动同步功能
- 更新评估模型的时间字段为可空
- 移除前端PC路由配置
- 更新Docker镜像版本至v2.6
- 切换开发环境API基础地址
2025-12-02 16:41:03 +08:00
Wei_佳
253ed14c87 feat: 为上传组件增加了文件数量和大小限制及上传前校验 2025-12-02 16:39:23 +08:00
Wei_佳
17d275e18c feat: 优化图片展示方式并新增上传提示 2025-12-02 16:31:32 +08:00
Wei_佳
20d8f155c6 feat: 支持多张支付凭证上传,更新了后端API、数据模型和前端上传组件。 2025-12-02 15:55:26 +08:00
Wei_佳
5093cf8146 feat: 新增行业、政策、ESG和数据首页管理页面,并更新API接口。 2025-12-01 17:25:01 +08:00
Wei_佳
e7e31213da Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  up repo
2025-12-01 17:00:55 +08:00
Wei_佳
4e2296300b feat: 更新页面中市级传承人标签文本 2025-12-01 17:00:47 +08:00
9c219cda83 up repo 2025-11-29 11:18:32 +08:00
Wei_佳
0b2824c3b0 feat: 新增环境变量并更新中文翻译 2025-11-28 16:47:14 +08:00
Wei_佳
81653a257d feat: 为图片类型字段添加 NPopover 优化图片展示体验并调整省略号逻辑 2025-11-28 16:01:01 +08:00
Wei_佳
1c493965d0 fix: 统一日期时间显示为秒级格式并修正文件上传响应解析逻辑 2025-11-27 22:38:11 +08:00
Wei_佳
7ded549269 fix: 更新上传接口地址为通用文件上传 2025-11-27 22:23:28 +08:00
Wei_佳
f2068c7b16 feat: 优化发票和用户列表按钮渲染逻辑,并调整用户限额设置弹窗的初始化字段。 2025-11-27 18:21:08 +08:00
Wei_佳
d40e8c2e0d Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  fix(valuations): 修正估值评估更新方法中的状态设置问题
2025-11-27 18:08:02 +08:00
Wei_佳
a33a80063d chore: 更新项目配置并移除GitHub链接组件
- 更新应用标题为中文"管理后台"
- 简化package.json中的项目名称为"admin-web"
- 删除GithubSite.vue组件及其相关导入
- 移除header中的GitHub链接图标
- 清理发票列表中的权限指令装饰器
- 清理用户列表中的权限指令装饰器
- 优化项目配置和组件结构
2025-11-27 18:07:52 +08:00
0021c94024 fix(valuations): 修正估值评估更新方法中的状态设置问题
修复valuation_controller.update方法中状态被硬编码为"pending"的问题,并添加update1方法用于不同的状态更新场景
同时更新dockerignore文件,添加web1/node_modules和migrations目录的忽略
优化发票抬头相关逻辑,包括空字符串处理和发票信息同步
调整发票抬头列表接口,支持分页和用户过滤
2025-11-27 18:01:12 +08:00
Wei_佳
803106ecf2 修复: 更新获取发票头部的API路径。 2025-11-27 18:00:56 +08:00
Wei_佳
9d08b3d2cb Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  调整估值与交易逻辑
  feat: 更新邮件客户端和评估状态处理逻辑
2025-11-27 17:48:35 +08:00
Wei_佳
6f99611a0c fix(api): 更新发票列表接口端点并优化证书审核短信通知流程
- 将发票列表接口端点从 `/invoice/list` 更新为 `/app-invoices/headers`
- 在短信通知成功后自动关闭证书审核弹窗
- 短信发送成功后触发 confirm 事件并返回空的报告和证书文件列表
- 改进用户体验,使短信通知流程更加完整和流畅
2025-11-27 17:48:22 +08:00
7df2bde70b 调整估值与交易逻辑 2025-11-27 16:35:36 +08:00
97b872aa9b feat(报告): 实现评估报告生成与展示功能
- 新增报告模板文件及生成工具模块
- 在拦截器中添加 isRaw 配置支持原始响应
- 实现报告内容获取接口及前端展示逻辑
- 完善发票模块的附件处理功能
- 优化用户管理界面的默认值设置
- 移除用户列表中的备注列显示
2025-11-27 16:34:37 +08:00
Wei_佳
df35a1a5bf feat: LimitSettingModal 初始化 targetCount 和 quotaType 为固定默认值 2025-11-27 16:31:31 +08:00
Wei_佳
cd4d00b013 feat: 增加 file_urls 字段映射 formData.attachments 2025-11-27 16:15:38 +08:00
Wei_佳
633f733f1c refactor: 优化上报工具函数逻辑。 2025-11-27 16:09:21 +08:00
Wei_佳
64334ba4e3 feat: 新增估值报告获取与展示功能,并在审计详情页添加计算流程tab。 2025-11-27 15:49:18 +08:00
hhm
99f411b31a feat: 评估完成后跳转个人中心 2025-11-27 15:21:15 +08:00
Wei_佳
1b64f7c1fd feat: 在查看模式下回显发票详情及附件。 2025-11-27 15:20:03 +08:00
Wei_佳
cf19b56b6d Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  更新数据库字段未更新
2025-11-27 15:12:10 +08:00
Wei_佳
05f9c02517 feat(user-management): 更新报告模板并隐藏用户列表备注列
- 更新报告模板文件 (report_template.docx)
- 隐藏用户列表中的备注列,注释相关配置代码
- 优化用户管理界面显示字段
2025-11-27 15:11:58 +08:00
5ca0152c55 feat: 更新邮件客户端和评估状态处理逻辑
修复邮件发送时的收件方地址验证问题,添加域名解析检查
更新评估状态字段值从"approved"为"pending"以保持一致性
修改发票创建接口以支持无凭证上传的情况
添加用户管理接口的时间范围查询功能
更新SMTP和短信服务的默认配置
2025-11-27 15:04:37 +08:00
c7e191f096 更新数据库字段未更新 2025-11-27 15:02:02 +08:00
Wei_佳
cdc4253a10 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  fix: PC-对公转账信息不对
2025-11-27 12:26:48 +08:00
Wei_佳
dc218e3d0e feat: 在证书审核弹窗中增加短信通知功能并调整发票发送状态字段 2025-11-27 12:26:42 +08:00
hhm
4cac09cf57 fix: PC-对公转账信息不对 2025-11-27 12:11:52 +08:00
Wei_佳
fd07c81d4b feat: 优化发票发送成功提示及审计详情表格提示框样式,并修正发票发送接口请求体格式 2025-11-27 11:52:00 +08:00
Wei_佳
7612792e08 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat: 重新评估增加剩余次数判断
2025-11-27 11:06:53 +08:00
Wei_佳
823230cb2d feat: 添加发票退款功能并优化发票上传组件样式和功能 2025-11-27 11:06:47 +08:00
hhm
a187d0e8fa feat: 重新评估增加剩余次数判断 2025-11-27 10:46:44 +08:00
Wei_佳
d98330d5ce Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat: 凭证上传的图片加了大小限制,10mb
  feat: 上传凭证图片地址参数更改
  更新 app/api/v1/app_valuations/app_valuations.py
  更新 app/api/v1/app_valuations/app_valuations.py
  更新 app/utils/calculation_engine/formula_registry.py
2025-11-27 10:35:49 +08:00
hhm
e99febec4e feat: 凭证上传的图片加了大小限制,10mb 2025-11-27 10:30:15 +08:00
Wei_佳
782b433821 feat: 移除状态筛选中的“全部”选项 2025-11-27 10:21:55 +08:00
hhm
3c03eaf353 feat: 上传凭证图片地址参数更改 2025-11-27 09:45:54 +08:00
831560592f 更新 app/api/v1/app_valuations/app_valuations.py 2025-11-26 21:04:28 +08:00
b0c80c500f 更新 app/api/v1/app_valuations/app_valuations.py 2025-11-26 20:49:49 +08:00
3328439241 更新 app/utils/calculation_engine/formula_registry.py 2025-11-26 20:46:47 +08:00
Wei_佳
f6a84442d1 feat: 审计详情页引入枚举格式化工具,统一处理枚举字段显示并兼容多源数据。 2025-11-26 18:35:53 +08:00
c15f3e9925 fix(invoice): 移除发票创建模型中不必要的必填限制
refactor(valuations): 添加估值计算前的用户配额检查逻辑

docs: 添加发票抬头必填字段的修改方案文档
2025-11-26 18:17:47 +08:00
27b6276cdc feat(交易管理): 新增多文件上传及邮件发送事务支持
- 新增多文件上传接口及发票附件存储
- 邮件发送流程增加事务保障及完整日志记录
- 用户管理备注支持分维度更新并记录操作日志
- 覆盖单元测试与集成测试用例
2025-11-26 18:17:07 +08:00
Wei_佳
5059e57f19 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  up repo md
  refactor(valuation): 重构评估状态逻辑并添加创建时间筛选
2025-11-26 17:47:30 +08:00
Wei_佳
f4714f6ca6 feat: 新增计算流程文档,移除工作台统计与项目卡片,并更新登录页提示文本 2025-11-26 17:47:22 +08:00
d347f1b4c9 up repo md 2025-11-26 17:28:51 +08:00
df7ff7af23 refactor(valuation): 重构评估状态逻辑并添加创建时间筛选
统一评估状态为 pending/completed 并兼容旧状态
添加创建时间范围筛选条件
移除发票状态中的 rejected 选项
更新前端路由配置
2025-11-26 17:05:22 +08:00
hhm
9359666805 fix: PC-新用户首次登录没有评估次数,点击开始评估按钮,应该继续跳转到对公汇款页面 2025-11-26 16:30:31 +08:00
Wei_佳
e990b0eb68 refactor: 移除发票和估值审核中的“已拒绝”和“已通过”状态。 2025-11-26 16:10:38 +08:00
Wei_佳
7c59d3385a fix: 增强报告生成的数据校验,并优化报告模板的数据处理与格式化。 2025-11-26 16:04:25 +08:00
Wei_佳
5332324b10 feat: 添加评估报告下载功能,包括下载页面、生成工具和模板,并更新了路由和权限守卫。 2025-11-26 12:25:49 +08:00
Wei_佳
4b945339d0 feat: CrudTable 新增重置事件,并优化查询栏日期范围的重置处理 2025-11-26 12:01:03 +08:00
Wei_佳
8972ead5b9 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  up bug
  feat(发票): 支持多附件上传和邮件发送功能
2025-11-26 10:55:49 +08:00
Wei_佳
2b917878ee style:审核详情表格用 text展示。优化 UI样式 2025-11-26 10:55:44 +08:00
b1f02e6b7a up bug 2025-11-25 21:46:59 +08:00
d4b2c801f4 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi 2025-11-25 20:09:51 +08:00
552c02516a feat(发票): 支持多附件上传和邮件发送功能
refactor(用户管理): 优化用户列表查询和备注字段处理

feat(估值): 评估报告和证书URL改为数组类型并添加下载地址

docs: 添加交易管理与用户备注功能增强实施计划

fix(邮件): 修复邮件发送接口的多附件支持问题

style: 清理注释代码和格式化文件
2025-11-25 20:09:50 +08:00
Wei_佳
0694ec28fb fix: 修复上传问题,报告和凭证限制 1 条,开票附件限制 2 条,需要后端修改接收数组 2025-11-25 18:10:11 +08:00
Wei_佳
9657901339 fix: 修改新增字段名统一社会信用代码/身份证号、业务/传承介绍 2025-11-25 17:33:29 +08:00
hhm
01cdcec0b4 feat: 开始评估前判断剩余评估次数 2025-11-25 14:00:12 +08:00
hhm
de8c4e9cab feat: 对公转账上传凭证 2025-11-25 12:14:10 +08:00
Wei_佳
f6243a66a0 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat: 报告、证书下载功能
2025-11-25 11:04:17 +08:00
Wei_佳
10ed15d983 feat: 改进证书查看功能,支持格式化显示报告和证书文件。 2025-11-25 11:04:10 +08:00
hhm
19ec7ca25a feat: 报告、证书下载功能 2025-11-25 10:49:24 +08:00
Wei_佳
695769076a refactor: 优化证书上传模态框文件处理逻辑并移除强制上传校验 2025-11-25 10:26:28 +08:00
Wei_佳
db31e25c61 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat: 抬头管理
2025-11-24 19:34:33 +08:00
hhm
c19b0167e4 feat: 抬头管理 2025-11-24 18:01:39 +08:00
Wei_佳
28a5607dad Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat(发票模型): 添加app_user_id字段并启用属性转换
2025-11-24 17:20:00 +08:00
Wei_佳
3db6b38e2e feat: 实现证书和报告文件的实际上传功能,并在审核详情页中集成上传后的数据更新逻辑 2025-11-24 17:19:52 +08:00
62d9fb8516 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi 2025-11-24 17:19:28 +08:00
efe2ec6416 feat(发票模型): 添加app_user_id字段并启用属性转换
refactor(估值模型): 修改historical_evidence类型并添加URL验证器

在发票模型中添加可选用户ID字段并配置ORM属性转换
在估值模型中调整历史证据字段类型为字典并添加URL字段验证逻辑
2025-11-24 17:19:26 +08:00
Wei_佳
6432325387 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat: 新增发票管理模块和用户端接口
  feat: 个人中心评估次数
2025-11-24 16:40:27 +08:00
Wei_佳
9e821e1bd2 feat: 用户详情模态框发票抬头列表新增分页功能。 2025-11-24 16:40:11 +08:00
e803102263 Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi 2025-11-24 16:39:56 +08:00
c690a95cab feat: 新增发票管理模块和用户端接口
refactor: 优化响应格式和错误处理

fix: 修复文件上传类型校验和删除无用PDF文件

perf: 添加估值评估审核时间字段和查询条件

docs: 更新Docker镜像版本至v1.8

test: 添加响应格式检查脚本

style: 统一API响应数据结构

chore: 清理无用静态文件和更新构建脚本
2025-11-24 16:39:53 +08:00
Wei_佳
728ff095cb feat: 优化用户列表详情弹窗数据传递并移除微信号字段,同时更新审核详情计算流程为动态展示。 2025-11-24 16:21:57 +08:00
17a7da123d feat: 个人中心评估次数 2025-11-24 14:44:29 +08:00
Wei_佳
d9c6150ae1 feat: 审计页面新增提交与审核日期范围筛选,并优化发票列表凭证展示及上传逻辑。 2025-11-24 14:10:00 +08:00
Wei_佳
def0d75840 feat: 优化凭证和开票记录的日期范围查询,使用 dayjs 精确处理起止时间 2025-11-24 11:16:43 +08:00
Wei_佳
948ac409df feat: 用户服务协议 2025-11-24 10:54:56 +08:00
Wei_佳
1d71f5b8bf Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat: 登录页接口联调
2025-11-24 10:42:34 +08:00
Wei_佳
1a4880ef4a feat: 格式化用户详情模态框操作日志中的时间显示。 2025-11-24 10:42:26 +08:00
5ad541cf9e Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi 2025-11-21 18:08:39 +08:00
Wei_佳
e2c8744d14 feat: 更新了头部图标的背景和边框样式,并调整了表单项的布局 2025-11-21 18:08:29 +08:00
ea0f03d0d7 feat: 登录页接口联调 2025-11-21 18:08:21 +08:00
Wei_佳
a148b94dff feat: 优化估值记录分页及跳转功能,重构企业转让凭证上传与开票信息管理,并调整开票管理为抬头管理 2025-11-21 17:55:03 +08:00
Wei_佳
1b56856995 feat: 引入 AppHeader 组件,将表格重构为 Naive UI 的 n-data-table,并优化页面布局和交互。 2025-11-21 16:44:27 +08:00
Wei_佳
3ccbaef170 feat: 优化用户中心页面布局和样式,并更新图标资源。 2025-11-21 15:24:19 +08:00
Wei_佳
77af212fc2 feat: 新增发票弹窗组件,并优化登录页、首页样式及用户中心相关功能 2025-11-21 14:48:15 +08:00
Wei_佳
ba2bc6c53e feat: 将登录页面的表单输入、验证和复选框替换为 Naive UI 组件并进行样式调整 2025-11-21 12:51:46 +08:00
Wei_佳
8b17d74359 feat: 重新设计登录页面UI,新增相关图片资源并更新登录逻辑。 2025-11-21 12:08:52 +08:00
Wei_佳
97ce7c5ccc Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi
* 'main' of https://git.1024tool.vip/zfc/guzhi:
  feat: 新增交易记录管理功能与统一上传接口

# Conflicts:
#	web/src/api/index.js
2025-11-21 11:00:22 +08:00
Wei_佳
85f8fbe694 feat: 优化评估审核页估值结果和联系方式显示逻辑,并移除API模拟数据 2025-11-21 10:55:49 +08:00
Wei_佳
e479bcfa97 feat: 优化评估审核页面数据展示逻辑,调整发票邮件发送附件处理并移除mock数据 2025-11-21 10:55:31 +08:00
1dd9a313e6 feat: 新增用户中心模块及短信登录功能
- 新增用户中心模块,包含估值记录、对公转账和开票管理功能
- 实现短信验证码登录功能,优化登录流程
- 新增首页和个人中心页面设计
- 更新API接口以支持新功能
- 调整环境变量配置,更新API基础路径
- 优化用户管理界面,增加ID查询和操作记录展示
- 重构开票记录页面,简化操作流程
- 添加菜单初始化SQL脚本
- 修复若干已知问题,优化用户体验
2025-11-20 20:54:51 +08:00
f536178428 feat: 新增交易记录管理功能与统一上传接口
feat(交易记录): 新增交易记录管理页面与API接口
feat(上传): 添加统一上传接口支持自动识别文件类型
feat(用户管理): 为用户模型添加备注字段并更新相关接口
feat(邮件): 实现SMTP邮件发送功能并添加测试脚本
feat(短信): 增强短信服务配置灵活性与日志记录

fix(发票): 修复发票列表时间筛选功能
fix(nginx): 调整上传大小限制与超时配置

docs: 添加多个功能模块的说明文档
docs(估值): 补充估值计算流程与API提交数据说明

chore: 更新依赖与Docker镜像版本
2025-11-20 20:53:09 +08:00
Wei_佳
a0e857b115 feat: 优化发票列表显示付款凭证、调整列宽并新增权限控制,同时为用户管理模块的增删改操作添加权限验证 2025-11-20 17:03:56 +08:00
Wei_佳
2ff5421c27 feat: 替换用户管理模块的模拟数据为真实接口,并优化额度设置功能 2025-11-20 14:52:15 +08:00
Wei_佳
7c64f0c76a feat: 启用短信验证码发送与登录功能,更新API路径并移除模拟逻辑。 2025-11-20 11:20:16 +08:00
Wei_佳
b050b1c875 Merge branch 'dev-feature-client-weijia'
* dev-feature-client-weijia:
  调整用户中心页面内容区域的顶部边距,并优化返回按钮的样式和定位。
  feat: 新增用户中心估值记录、对公转账、发票管理和侧边栏组件,并重构用户中心页面。
  refactor: 使用自定义useAppDark替代vueuse的useDark
  feat: 添加返回导航功能
  feat: 完成客户端个人中心和抬头管理功能
  feat: 添加独立首页路由,优化登录后跳转逻辑
  feat: 添加短信验证码登录功能,优化登录页面交互体验
2025-11-20 10:34:52 +08:00
c905d2492b Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi 2025-11-19 19:36:12 +08:00
cc352d3184 feat: 重构后端服务并添加新功能
refactor: 优化API路由和响应模型
feat(admin): 添加App用户管理接口
feat(sms): 实现阿里云短信服务集成
feat(email): 添加SMTP邮件发送功能
feat(upload): 支持文件上传接口
feat(rate-limiter): 实现手机号限流器
fix: 修复计算步骤入库问题
docs: 更新API文档和测试计划
chore: 更新依赖和配置
2025-11-19 19:36:03 +08:00
Wei_佳
24bf93f0b3 调整用户中心页面内容区域的顶部边距,并优化返回按钮的样式和定位。 2025-11-19 16:51:08 +08:00
Wei_佳
671504bb33 Merge branch 'release'
* release:
  refactor: 使用自定义useAppDark替代vueuse的useDark
2025-11-19 16:36:33 +08:00
Wei_佳
62354c9e23 feat: 新增用户中心估值记录、对公转账、发票管理和侧边栏组件,并重构用户中心页面。 2025-11-19 14:39:07 +08:00
Wei_佳
26701c25cf refactor: 使用自定义useAppDark替代vueuse的useDark
将ThemeMode组件和app store中的useDark替换为自定义的useAppDark composable,统一暗色模式管理逻辑

(cherry picked from commit 2ec91a6085a7df2432bbe2d242daa2d018c1c64c)
2025-11-18 15:39:34 +08:00
Wei_佳
0eff495f75 feat: 添加返回导航功能
- 个人中心页面添加返回首页按钮
- 抬头管理添加页面添加返回按钮
- 优化页面导航流程,提升用户体验
2025-11-17 18:46:13 +08:00
Wei_佳
ee580ff22c feat: 完成客户端个人中心和抬头管理功能
- 新增首页,登录后跳转到首页
- 新增个人中心页面,包含估值记录、对公转账、抬头管理三个模块
- 新增抬头管理添加页面,支持添加发票抬头信息
- 优化登录流程,添加短信验证码输入框
- 统一使用系统配色方案和设计风格
- 对公转账定价设置为6000元/次
2025-11-17 18:38:16 +08:00
Wei_佳
bde761864b feat: 添加独立首页路由,优化登录后跳转逻辑
- 新增/home路由指向独立首页组件
- 将根路径重定向从/pages改为/home
- 更新登录成功后默认跳转路径为/home
- 设置首页路由为隐藏状态,不在菜单中显示
2025-11-17 18:15:02 +08:00
Wei_佳
2ce82f3401 feat: 添加短信验证码登录功能,优化登录页面交互体验
- 新增验证码输入框和发送验证码按钮,支持60秒倒计时
- 添加手机号格式验证(11位,1开头)
- 新增sendVerifyCode和loginWithVerifyCode API接口(待后端实现)
- 优化登录流程,添加完整的表单验证和错误提示
- 调整登录容器高度以适应新增的验证码输入区域
- 添加组件卸载时的定时器清理逻辑
- 保留原有登录逻辑作为临时方案,
2025-11-17 18:13:33 +08:00
Wei_佳
ebf41d74c8 feat: 添加完整菜单初始化SQL文件
- 新增complete_menu_setup.sql包含所有菜单项
- 包含工作台、交易管理、估值管理、用户管理菜单
- 完整的角色权限分配(管理员和普通用户)
- 删除重复的单独菜单SQL文件,统一管理
- 提供验证SQL便于检查菜单配置
2025-11-17 15:04:17 +08:00
Wei_佳
52beb9b264 feat: 重构估值审核详情计算流程页面,优化公式展示和层级结构
- 将计算流程改为左右分栏布局,左侧展示详细计算公式,右侧展示流程大纲
- 新增完整的估值计算公式体系,包含模型估值和市场估值的详细计算逻辑
- 优化公式层级结构,使用缩进和编号清晰展示计算步骤
- 移除原有的卡片式汇总展示和步骤列表,统一为公式展示
- 完善样式设计,提升公式可读性和页面布局美观度
2025-11-17 15:00:38 +08:00
Wei_佳
2d9a83d68e refactor: 简化证书弹窗组件,统一上传和查看模式的UI实现
- 移除查看模式的独立模板代码,统一使用上传模式的UI结构
- 优化报告区域布局,将下载按钮移至标题旁边
- 在查看模式下禁用上传组件,保持界面一致性
- 删除冗余的查看模式样式代码(certificate-display、certificate-item等)
- 简化组件结构,减少代码重复,提升可维护性
2025-11-17 14:53:48 +08:00
Wei_佳
5cf79a3f7e feat: 优化发票和证书弹窗组件,完善查看模式和文件预览功能
- 发票弹窗新增查看模式禁用状态,邮箱、文案和上传组件在查看模式下不可编辑
- 优化发票弹窗底部按钮,查看模式下仅显示"关闭"按钮
- 移除发票弹窗上传提示文案和上传区域样式冗余代码
- 证书弹窗新增图片组预览功能,支持图片文件点击放大查看
- 证书弹窗新增文件下载功能,支持所有文件类型下载
- 优化证书文
2025-11-17 14:48:34 +08:00
Wei_佳
67ac563ddb feat(web): 添加用户列表注册时间筛选功能 2025-11-17 14:01:44 +08:00
Wei_佳
11ae08dc96 feat: 优化估值申请表单字段,完善基础信息填写项
- 新增"统一社会信用代码/身份证号"字段,支持机构和个人信息录入
- 将"所属机构"字段更名为"所属机构/权利人",扩展适用范围
- 新增"业务/传承介绍"多行文本输入框,替代原有企业简介和业务简介
- 优化表单布局,调整字段宽度和排列顺序
- 完善表单验证规则,确保新增字段必填
- 同步更新审核详情页面字段显示
- 修改web1端
2025-11-17 11:02:31 +08:00
Wei_佳
09469888b4 feat: 完善用户管理功能,新增用户详情弹窗和审核页面优化
- 新增UserDetailModal组件,支持用户基础信息、发票抬头和操作记录查看
- 完善用户管理API数据结构,添加用户类型字段和模拟数据优化
- 实现用户详情查看功能,支持完整信息展示
- 优化估值审核详情页样式,改进标签页和区块视觉效果
- 修复审核详情页面布局问题,提升用户体验
2025-11-14 18:05:05 +08:00
Wei_佳
850a63b37c feat: 新增证书上传弹窗组件,支持报告和证书文件管理
- 实现CertificateModal.vue组件,包含上传和查看两种模式
- 支持缩略图文件列表上传,最多支持5个文件
- 报告文件支持下载原版报告功能
- 证书文件支持图片预览和删除功能
- 文件类型支持:图片、PDF、Word、视频,最大50MB
- 使用Naive UI组件库,界面美观且交互友好
- 修复组件导入和类型检查问题,确保代码质量
2025-11-14 16:20:38 +08:00
Wei_佳
60b2a2777d refactor: 优化估值审核详情页表格显示,改用NDataTable组件解决表头显示问题 2025-11-14 11:25:29 +08:00
Wei_佳
b63306890d refactor: 重构审核列表页面,优化详情页面和弹窗交互 2025-11-13 18:47:53 +08:00
Wei_佳
ad4bd8145c feat: 优化审核列表短信文案配置弹窗按钮布局和文案 2025-11-13 18:03:30 +08:00
Wei_佳
be4a7802f7 优化表格组件和主题配置 2025-11-13 17:53:07 +08:00
Wei_佳
bd76792ec9 优化估值审核页面功能 2025-11-13 17:53:01 +08:00
Wei_佳
106472944b 新增开票发送接口和估值审核模拟数据 2025-11-13 17:52:53 +08:00
Wei_佳
bf88fb502b 完善开票记录页面功能 2025-11-13 17:52:45 +08:00
Wei_佳
92aed53158 修正开票弹窗按钮文案 2025-11-13 17:51:49 +08:00
Wei_佳
cde36df597 feat(用户管理): 优化用户列表页面结构和功能
- 调整表格列显示微信号替代邮箱字段
- 添加备注和剩余体验次数列
- 移除状态切换功能改为次数设置按钮
- 移动LimitSettingModal组件到页面目录
- 隐藏新建用户按钮简化界面
- 优化查询条件支持微信号搜索
2025-11-13 16:53:11 +08:00
Wei_佳
d9ee381801 feat(交易管理): 优化开票记录操作按钮和mock数据
- 调整操作列宽度为180px适应按钮显示
- 未开票状态显示开票和退款按钮
- 已开票状态显示查看按钮
- 已退款状态不显示操作按钮
- 添加开票记录mock数据包含10条测试记录
- 支持按状态、手机号、公司信息搜索和分页
- 新增退款处理逻辑和查看详情功能
2025-11-13 16:52:54 +08:00
Wei_佳
8b47f76965 refactor: 重构用户列表字段结构和组件化弹窗
- 调整用户列表字段,包含ID、手机号、微信号、注册时间、备注、剩余体验次数
- 更新mock数据,匹配新的字段结构
- 将次数设置弹窗拆分为独立组件LimitSettingModal
- 优化搜索功能,支持手机号和微信号搜索
- 简化主页面代码,提高可维护性
2025-11-13 16:33:35 +08:00
Wei_佳
85ced0bf36 feat: 完善用户列表功能和次数设置弹窗
- 为用户列表添加mock数据,包含8个用户的完整信息
- 支持手机号和邮箱搜索功能
- 修改操作列,只显示详情和次数设置按钮
- 新增次数设置弹窗,包含剩余次数、类型选择、体验次数调整和备注功能
- 完善弹窗交互逻辑和样式设计
2025-11-13 16:28:03 +08:00
Wei_佳
c66d0dffde style: 优化开票记录搜索表单对齐样式
- 统一所有搜索条件标签宽度为80px
- 设置各输入框固定宽度,提升视觉一致性
2025-11-13 16:00:20 +08:00
Wei_佳
a797d29c68 style: 优化审核列表搜索表单对齐样式
- 统一所有搜索条件标签宽度为80px
- 设置各输入框固定宽度,提升视觉一致性
2025-11-13 15:56:38 +08:00
Wei_佳
e062fc6607 feat: 新增估值管理模块和审核列表功能
- 新增估值管理一级菜单目录
- 新增审核列表二级菜单
- 实现审核列表页面,支持查看详情和审核操作
- 添加估值评估相关API接口定义
- 支持审核通过和拒绝操作
2025-11-13 15:53:44 +08:00
Wei_佳
082b9b7902 docs: 添加交易管理菜单SQL脚本和项目规范文档
- 新增交易管理菜单数据库变更SQL脚本
- 添加项目开发规范文档AGENTS.md
2025-11-13 15:48:48 +08:00
Wei_佳
f468a87fdd feat: 新增交易管理模块和开票记录功能
- 新增交易管理一级菜单目录
- 新增开票记录二级菜单
- 实现开票记录列表页面,包含搜索、状态管理等功能
- 添加开票记录相关API接口定义
2025-11-13 15:46:45 +08:00
Wei_佳
9337f06f6e 简化侧边栏菜单逻辑
- 移除单个子路由的特殊处理
- 统一使用children方式渲染菜单项
- 优化菜单结构,提升代码可维护性
2025-11-13 15:40:17 +08:00
Wei_佳
aa512e6154 新增用户管理功能
- 新增用户列表页面,支持用户查询和筛选
- 完善侧边栏菜单导航功能
- 添加用户管理相关API接口
- 更新权限系统支持用户管理路由
- 新增用户管理国际化文案
2025-11-13 15:35:16 +08:00
169 changed files with 21251 additions and 7736 deletions

View File

@ -1 +1,3 @@
web/node_modules
web1/node_modules
migrations

View File

@ -1,54 +0,0 @@
## 项目(快速)指导 — 供 AI 编码代理使用
下面的要点帮助你快速理解并在本代码库中高效工作。保持简短、具体并以可执行示例为主。
- 项目类型FastAPI 后端 (Python 3.11) + Vue3/Vite 前端(目录 `web/`)。后端使用 Tortoise ORM配置在 `app/settings/config.py`),前端用 pnpm/vite。
- 快速启动(后端):在项目根目录
- 建议 Python venv然后安装依赖`pip install -r requirements.txt`(或使用项目 README 中的 uv/uvenv 过程)。
- 启动:`python run.py`。这会通过 `uvicorn` 运行 `app:app`(见 `run.py`),开启 `reload=True`OpenAPI 在 `/docs`
- 快速启动(前端):进入 `web/`,使用 pnpm或 npm安装并运行`pnpm i``pnpm dev`
- 后端关键入口
- `run.py`:应用启动脚本,设置 uvicorn 日志格式并运行 `app:app`
- `app/__init__.py`:创建 FastAPI app调用 `core/init_app.py` 中的注册函数init 数据、注册中间件、异常处理与路由(路由前缀为 `/api`)。
- `app/core/init_app.py`(注意:此文件包含启动时的路由/中间件/异常注册逻辑,请优先阅读它来理解请求生命周期)。
- 重要配置点
- `app/settings/config.py`:使用 Pydantic Settings包含 `TORTOISE_ORM`(默认 SQLitedb 文件在项目根 `db.sqlite3`、JWT、SECRET_KEY、CORS 等。修改环境变量即可覆盖设置。
- `app/utils/api_config.py`:提供 `api_config` 全局实例,用来存放第三方 API示例`chinaz``xiaohongshu`)。常用方法:`api_config.get_api_key(provider)``get_endpoint_config(provider, endpoint)``add_endpoint(...)``save_config()`
- 路由与模块约定
- API 版本化:`app/api/v1/` 下放置 v1 接口。路由统一由 `core/init_app.py` 通过 `register_routers(..., prefix='/api')` 注册。
- 控制器HTTP handlers位于 `app/controllers/`,数据模型在 `app/models/`Pydantic schemas 在 `app/schemas/`
- 数据库与迁移
- 使用 Tortoise ORM`TORTOISE_ORM``app/settings/config.py`。项目把 `aerich.models` 列入 models见配置repository 中存在 `migrations/` 文件夹。若需变更模型,按项目现有工具链(如 aerich执行迁移在不确定时先检查 `pyproject.toml`/`requirements.txt` 是否包含 aerich 并复核 README。
- 日志与持久化
- 日志目录:`app/logs`(可在 `settings.LOGS_ROOT` 找到)。运行时可根据 `run.py` 中的 LOGGING_CONFIG 调整格式。
- 第三方 API 集成(示例)
- `api_config` 示例用法Python:
```py
from app.utils.api_config import api_config
cfg = api_config.get_endpoint_config('xiaohongshu', 'xiaohongshu_note_detail')
base = api_config.get_base_url('xiaohongshu')
key = api_config.get_api_key('xiaohongshu')
```
- 环境变量覆盖CHINAZ_API_KEY、XIAOHONGSHU_TOKEN、EXAMPLE_API_KEY 等会被 `api_config` 或 settings 读取。
- 编辑/贡献约定(可自动推断的现有模式)
- 新增 API`app/api/v1/...` 添加路由模块,控制器放 `app/controllers/`schema 放 `app/schemas/`,并在 `core/init_app.py` 中确保路由被注册。
- 新增模型:更新 `app/models/` 并生成迁移(项目使用 Tortoise + aerich 风格)。先检查 `migrations/models` 是否有对应变更。
- 调试提示
- 本地运行时使用 `python run.py`reload=True然后访问 `http://localhost:9999/docs` 查看 OpenAPI确认路由/依赖注入是否按预期工作。
- 常见故障点:环境变量未设置(导致 API keys 丢失、Tortoise 连接配置错误(检查 `TORTOISE_ORM.connections`)、以及中间件注册顺序会影响异常处理。
- 其它注意事项(小而具体)
- 前端以 `/api` 为后端前缀,修改后端接口时请同步前端 `web/src/api` 的调用。
- `app/utils/api_config.py` 会在模块导入时创建 `api_config` 单例;修改该文件时注意导入时机(不要在模块顶层做阻塞网络调用)。
如果需要我把 README 中的启动说明转成更精确的 shell 命令(或添加 aerich 的迁移示例命令),我可以继续补充。请告诉我你希望强调的额外部分或需要澄清的地方。

225
DEPLOYMENT.md Normal file
View 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权重计算 |
---
## 联系信息
如有问题,请联系项目负责人。

View File

@ -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

View File

@ -1,83 +0,0 @@
{
"asset_name": "资产名称",
"institution": "所属机构",
"industry": "农业",
"annual_revenue": "22",
"rd_investment": "33",
"three_year_income": [
"11",
"22",
"33"
],
"funding_status": "国家级资助",
"sales_volume": "22",
"link_views": "22",
"circulation": "0",
"last_market_activity": "0",
"monthly_transaction": "0",
"price_fluctuation": [
"2",
"3"
],
"application_maturity": "0",
"application_coverage": "0",
"cooperation_depth": "1",
"offline_activities": "3",
"online_accounts": [
"0",
"333"
],
"inheritor_level": "国家级传承人",
"inheritor_age_count": [
"55",
"66",
"77"
],
"inheritor_certificates": [
"http://example.com/国家级非遗传承人证书.jpg"
],
"heritage_level": "0",
"historical_evidence": {
"artifacts": "22",
"ancient_literature": "33",
"inheritor_testimony": "66"
},
"patent_certificates": [
"http://example.com/专利证书1.jpg",
"http://example.com/专利证书2.jpg"
],
"pattern_images": [
"pattern1.jpg"
],
"patent_application_no": "22",
"heritage_asset_level": "国家级非遗",
"inheritor_ages": [
"55",
"66",
"77"
],
"implementation_stage": "成熟应用",
"coverage_area": "全球覆盖",
"collaboration_type": "品牌联名",
"platform_accounts": {
"bilibili": {
"followers_count": 8000,
"likes": 1000,
"comments": 500,
"shares": 500
},
"douyin": {
"followers_count": 8000,
"likes": 1000,
"comments": 500,
"shares": 500
}
},
"scarcity_level": "孤品:全球唯一,不可复制(如特定版权、唯一实物)",
"market_activity_time": "近一周",
"price_range": {
"highest": "2",
"lowest": "3"
},
"monthly_transaction_amount": "月交易额<100万元"
}

View File

@ -26,11 +26,33 @@ async def lifespan(app: FastAPI):
def create_app() -> FastAPI:
openapi_tags = [
{"name": "app-用户认证与账户", "description": "用户端账户与认证相关接口(公开/需认证以端点说明为准)"},
{"name": "app-估值评估", "description": "用户端估值评估相关接口(需用户端认证)"},
{"name": "app-短信服务", "description": "用户端短信验证码与登录相关接口(公开)"},
{"name": "app-上传", "description": "用户端文件上传接口(公开)"},
{"name": "admin-基础", "description": "后台登录与个人信息接口(部分公开,其他需认证)"},
{"name": "admin-用户管理", "description": "后台用户管理接口(需认证与权限)"},
{"name": "admin-角色管理", "description": "后台角色管理接口(需认证与权限)"},
{"name": "admin-菜单管理", "description": "后台菜单管理接口(需认证与权限)"},
{"name": "admin-API权限管理", "description": "后台 API 权限管理接口(需认证与权限)"},
{"name": "admin-部门管理", "description": "后台部门管理接口(需认证与权限)"},
{"name": "admin-审计日志", "description": "后台审计日志查询接口(需认证与权限)"},
{"name": "admin-估值评估", "description": "后台估值评估接口(需认证与权限)"},
{"name": "admin-发票管理", "description": "后台发票与抬头管理接口(需认证与权限)"},
{"name": "admin-交易管理", "description": "后台交易/对公转账记录接口(需认证与权限)"},
{"name": "admin-内置接口", "description": "后台第三方内置接口调用(需认证与权限)"},
{"name": "admin-行业管理", "description": "后台行业数据管理(当前公开)"},
{"name": "admin-指数管理", "description": "后台指数数据管理(当前公开)"},
{"name": "admin-政策管理", "description": "后台政策数据管理(当前公开)"},
{"name": "admin-ESG管理", "description": "后台 ESG 数据管理(当前公开)"},
]
app = FastAPI(
title=settings.APP_TITLE,
description=settings.APP_DESCRIPTION,
version=settings.VERSION,
openapi_url="/openapi.json",
openapi_tags=openapi_tags,
middleware=make_middlewares(),
lifespan=lifespan,
redirect_slashes=False, # 禁用尾部斜杠重定向

View File

@ -5,6 +5,7 @@ from app.utils.app_user_jwt import get_current_app_user
from .apis import apis_router
from .app_users import app_users_router
from .app_users.admin_manage import admin_app_users_router
from .app_valuations import app_valuations_router
from .auditlog import auditlog_router
from .base import base_router
@ -19,28 +20,38 @@ from .third_party_api import third_party_api_router
from .upload import router as upload_router
from .users import users_router
from .valuations import router as valuations_router
from .invoice.invoice import invoice_router
from .transactions.transactions import transactions_router
from .app_invoices.app_invoices import app_invoices_router
from .sms.sms import router as sms_router
v1_router = APIRouter()
v1_router.include_router(base_router, prefix="/base")
v1_router.include_router(app_users_router, prefix="/app-user") # AppUser路由无需权限依赖
v1_router.include_router(base_router, prefix="/base", tags=["admin-基础"])
v1_router.include_router(app_users_router, prefix="/app-user", tags=["app-用户认证与账户"]) # AppUser路由无需权限依赖
v1_router.include_router(admin_app_users_router, prefix="/app-user-admin", tags=["admin-App用户管理"])
# 注意app-valuations 路由在各自的端点内部使用 get_current_app_user 进行认证
# 这样可以保持App用户认证系统的独立性不与后台管理权限系统混合
v1_router.include_router(app_valuations_router, prefix="/app-valuations") # 用户端估值评估路由
v1_router.include_router(users_router, prefix="/user", dependencies=[DependAuth, DependPermission])
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependAuth, DependPermission])
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependAuth, DependPermission])
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependAuth, DependPermission])
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependAuth, DependPermission])
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependAuth, DependPermission])
v1_router.include_router(esg_router, prefix="/esg")
v1_router.include_router(index_router, prefix="/index")
v1_router.include_router(industry_router, prefix="/industry")
v1_router.include_router(policy_router, prefix="/policy")
v1_router.include_router(upload_router, prefix="/upload") # 文件上传路由
v1_router.include_router(app_valuations_router, prefix="/app-valuations", tags=["app-估值评估"]) # 用户端估值评估路由
v1_router.include_router(users_router, prefix="/user", dependencies=[DependAuth, DependPermission], tags=["admin-用户管理"])
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependAuth, DependPermission], tags=["admin-角色管理"])
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependAuth, DependPermission], tags=["admin-菜单管理"])
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependAuth, DependPermission], tags=["admin-API权限管理"])
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependAuth, DependPermission], tags=["admin-部门管理"])
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependAuth, DependPermission], tags=["admin-审计日志"])
v1_router.include_router(esg_router, prefix="/esg", tags=["admin-ESG管理"])
v1_router.include_router(index_router, prefix="/index", tags=["admin-指数管理"])
v1_router.include_router(industry_router, prefix="/industry", tags=["admin-行业管理"])
v1_router.include_router(policy_router, prefix="/policy", tags=["admin-政策管理"])
v1_router.include_router(upload_router, prefix="/upload", tags=["app-上传"]) # 文件上传路由
v1_router.include_router(
third_party_api_router,
prefix="/third_party_api",
dependencies=[DependAuth, DependPermission],
tags=["admin-内置接口"],
)
v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission])
v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission], tags=["admin-估值评估"])
v1_router.include_router(invoice_router, prefix="/invoice", tags=["admin-发票管理"])
v1_router.include_router(transactions_router, prefix="/transactions", dependencies=[DependAuth, DependPermission], tags=["admin-交易管理"])
v1_router.include_router(sms_router, prefix="/sms", tags=["app-短信服务"])
v1_router.include_router(app_invoices_router, prefix="/app-invoices", tags=["app-发票管理"])

View File

@ -3,12 +3,14 @@ from tortoise.expressions import Q
from app.controllers.api import api_controller
from app.schemas import Success, SuccessExtra
from app.schemas.base import BasicResponse, PageResponse, MessageOut
from app.schemas.apis import BaseApi
from app.schemas.apis import *
router = APIRouter()
@router.get("/list", summary="查看API列表")
@router.get("/list", summary="查看API列表", response_model=PageResponse[BaseApi])
async def list_api(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -28,7 +30,7 @@ async def list_api(
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看Api")
@router.get("/get", summary="查看Api", response_model=BasicResponse[BaseApi])
async def get_api(
id: int = Query(..., description="Api"),
):
@ -37,7 +39,7 @@ async def get_api(
return Success(data=data)
@router.post("/create", summary="创建Api")
@router.post("/create", summary="创建Api", response_model=BasicResponse[MessageOut])
async def create_api(
api_in: ApiCreate,
):
@ -45,7 +47,7 @@ async def create_api(
return Success(msg="Created Successfully")
@router.post("/update", summary="更新Api")
@router.post("/update", summary="更新Api", response_model=BasicResponse[MessageOut])
async def update_api(
api_in: ApiUpdate,
):
@ -53,7 +55,7 @@ async def update_api(
return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除Api")
@router.delete("/delete", summary="删除Api", response_model=BasicResponse[MessageOut])
async def delete_api(
api_id: int = Query(..., description="ApiID"),
):
@ -61,7 +63,7 @@ async def delete_api(
return Success(msg="Deleted Success")
@router.post("/refresh", summary="刷新API列表")
@router.post("/refresh", summary="刷新API列表", response_model=BasicResponse[MessageOut])
async def refresh_api():
await api_controller.refresh_api()
return Success(msg="OK")

View File

@ -0,0 +1,153 @@
from fastapi import APIRouter, Query, Depends
from typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
from app.schemas.invoice import InvoiceOut, InvoiceHeaderOut, InvoiceHeaderCreate, InvoiceHeaderUpdate, PaymentReceiptCreate, AppCreateInvoiceWithReceipt, InvoiceCreate
from app.controllers.invoice import invoice_controller
from app.utils.app_user_jwt import get_current_app_user
from app.models.user import AppUser
from app.models.invoice import InvoiceHeader
app_invoices_router = APIRouter(tags=["app-发票管理"])
@app_invoices_router.get("/list", summary="我的发票列表", response_model=PageResponse[InvoiceOut])
async def get_my_invoices(
status: Optional[str] = Query(None),
ticket_type: Optional[str] = Query(None),
invoice_type: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
current_user: AppUser = Depends(get_current_app_user),
):
result = await invoice_controller.list(
page=page,
page_size=page_size,
status=status,
ticket_type=ticket_type,
invoice_type=invoice_type,
app_user_id=current_user.id,
)
return SuccessExtra(
data=result.items,
total=result.total,
page=result.page,
page_size=result.page_size,
msg="获取成功",
)
@app_invoices_router.get("/headers", summary="我的发票抬头", response_model=BasicResponse[list[InvoiceHeaderOut]])
async def get_my_headers(current_user: AppUser = Depends(get_current_app_user)):
headers = await invoice_controller.get_headers(user_id=current_user.id)
return Success(data=headers, msg="获取成功")
@app_invoices_router.get("/headers/{id}", summary="我的发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut])
async def get_my_header_by_id(id: int, current_user: AppUser = Depends(get_current_app_user)):
header = await invoice_controller.get_header_by_id(id)
if not header or getattr(header, "id", None) is None:
return Success(data={}, msg="未找到")
# 仅允许访问属于自己的抬头
if getattr(header, "app_user_id", None) not in (current_user.id, None):
return Success(data={}, msg="未找到")
return Success(data=header, msg="获取成功")
@app_invoices_router.post("/headers", summary="新增我的发票抬头", response_model=BasicResponse[InvoiceHeaderOut])
async def create_my_header(data: InvoiceHeaderCreate, current_user: AppUser = Depends(get_current_app_user)):
header = await invoice_controller.create_header(user_id=current_user.id, data=data)
return Success(data=header, msg="创建成功")
@app_invoices_router.put("/headers/{id}", summary="更新我的发票抬头", response_model=BasicResponse[InvoiceHeaderOut])
async def update_my_header(id: int, data: InvoiceHeaderUpdate, current_user: AppUser = Depends(get_current_app_user)):
existing = await invoice_controller.get_header_by_id(id)
if not existing or getattr(existing, "id", None) is None:
return Success(data={}, msg="未找到")
if getattr(existing, "app_user_id", None) != current_user.id:
return Success(data={}, msg="未找到")
header = await invoice_controller.update_header(id, data)
return Success(data=header or {}, msg="更新成功" if header else "未找到")
@app_invoices_router.delete("/headers/{id}", summary="删除我的发票抬头", response_model=BasicResponse[dict])
async def delete_my_header(id: int, current_user: AppUser = Depends(get_current_app_user)):
existing = await invoice_controller.get_header_by_id(id)
if not existing or getattr(existing, "id", None) is None:
return Success(data={"deleted": False}, msg="未找到")
if getattr(existing, "app_user_id", None) != current_user.id:
return Success(data={"deleted": False}, msg="未找到")
ok = await invoice_controller.delete_header(id)
return Success(data={"deleted": ok}, msg="删除成功" if ok else "未找到")
@app_invoices_router.post("/receipts/{id}", summary="上传我的付款凭证", response_model=BasicResponse[dict])
async def upload_my_receipt(id: int, data: PaymentReceiptCreate, current_user: AppUser = Depends(get_current_app_user)):
inv = await invoice_controller.model.filter(id=id, app_user_id=current_user.id).first()
if not inv:
return Success(data={}, msg="未找到")
receipt = await invoice_controller.create_receipt(id, data)
detail = await invoice_controller.get_receipt_by_id(receipt.id)
return Success(data=detail, msg="上传成功")
@app_invoices_router.post("/create-with-receipt", summary="创建我的发票并上传付款凭证", response_model=BasicResponse[dict])
async def create_with_receipt(payload: AppCreateInvoiceWithReceipt, current_user: AppUser = Depends(get_current_app_user)):
header = await InvoiceHeader.filter(id=payload.header_id, app_user_id=current_user.id).first()
if not header:
return Success(data={}, msg="抬头未找到")
ticket_type = payload.ticket_type or "electronic"
invoice_type = payload.invoice_type
if not invoice_type:
mapping = {"0": "normal", "1": "special"}
invoice_type = mapping.get(str(payload.invoiceTypeIndex)) if payload.invoiceTypeIndex is not None else None
if not invoice_type:
invoice_type = "normal"
inv_data = InvoiceCreate(
ticket_type=ticket_type,
invoice_type=invoice_type,
phone=current_user.phone,
email=header.email,
company_name=header.company_name,
tax_number=header.tax_number,
register_address=header.register_address,
register_phone=header.register_phone,
bank_name=header.bank_name,
bank_account=header.bank_account,
app_user_id=current_user.id,
header_id=header.id,
wechat=getattr(current_user, "alias", None),
)
inv = await invoice_controller.create(inv_data)
if payload.receipt_urls:
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)
return Success(data=detail, msg="创建并上传成功")
else:
out = await invoice_controller.get_out(inv.id)
return Success(data=out.model_dump() if out else {}, msg="创建成功,未上传凭证")
@app_invoices_router.get("/headers/list", summary="我的抬头列表(分页)", response_model=PageResponse[InvoiceHeaderOut])
async def get_my_headers_paged(page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), current_user: AppUser = Depends(get_current_app_user)):
qs = invoice_controller.model_header.filter(app_user_id=current_user.id) if hasattr(invoice_controller, "model_header") else None
# Fallback when controller没有暴露model_header
from app.models.invoice import InvoiceHeader
qs = InvoiceHeader.filter(app_user_id=current_user.id)
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
items = [InvoiceHeaderOut.model_validate(r) for r in rows]
return SuccessExtra(data=[i.model_dump() for i in items], total=total, page=page, page_size=page_size, msg="获取成功")

View File

@ -0,0 +1,146 @@
from fastapi import APIRouter, Query, Depends, HTTPException
from typing import Optional, List
from datetime import datetime
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
from app.schemas.app_user import AppUserQuotaUpdateSchema, AppUserQuotaLogOut, AppUserUpdateSchema
from app.controllers.app_user import app_user_controller
from app.models.user import AppUser, AppUserQuotaLog
from app.core.dependency import DependAuth, DependPermission, AuthControl
admin_app_users_router = APIRouter(dependencies=[DependAuth, DependPermission], tags=["admin-App用户管理"])
@admin_app_users_router.get("/list", summary="App用户列表", response_model=PageResponse[dict])
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),
created_at: Optional[List[int]] = Query(None),
page: int = Query(1, ge=1),
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:
qs = qs.filter(phone__icontains=phone)
if wechat:
qs = qs.filter(alias__icontains=wechat)
if created_start or created_end:
def _parse_dt(s: Optional[str]):
if not s:
return None
s = s.replace('+', ' ').strip()
try:
return datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
except Exception:
try:
ms = float(s)
return datetime.fromtimestamp(ms / 1000)
except Exception:
return None
start_dt = _parse_dt(created_start)
end_dt = _parse_dt(created_end)
if start_dt and end_dt:
qs = qs.filter(created_at__gte=start_dt, created_at__lte=end_dt)
elif start_dt:
qs = qs.filter(created_at__gte=start_dt)
elif end_dt:
qs = qs.filter(created_at__lte=end_dt)
elif created_at and len(created_at) == 2:
start_dt = datetime.fromtimestamp(created_at[0] / 1000)
end_dt = datetime.fromtimestamp(created_at[1] / 1000)
qs = qs.filter(created_at__gte=start_dt, created_at__lte=end_dt)
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
items = []
for u in rows:
last_log = await AppUserQuotaLog.filter(app_user_id=u.id).order_by("-created_at").first()
items.append({
"id": u.id,
"phone": u.phone,
"wechat": u.alias,
"created_at": u.created_at.isoformat() if u.created_at else "",
"notes": getattr(u, "notes", "") or "",
"remaining_count": int(getattr(u, "remaining_quota", 0) or 0),
"user_type": getattr(last_log, "op_type", None),
})
return SuccessExtra(data=items, total=total, page=page, page_size=page_size, msg="获取成功")
@admin_app_users_router.post("/quota", summary="调整用户剩余估值次数", response_model=BasicResponse[dict])
async def update_quota(payload: AppUserQuotaUpdateSchema, operator=Depends(AuthControl.is_authed)):
user = await app_user_controller.update_quota(
operator_id=getattr(operator, "id", 0),
operator_name=getattr(operator, "username", "admin"),
user_id=payload.user_id,
target_count=payload.target_count,
delta=payload.delta,
op_type=payload.op_type,
remark=payload.remark,
)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
# if payload.remark is not None:
# user.notes = payload.remark
# await user.save()
return Success(data={"user_id": user.id, "remaining_quota": user.remaining_quota}, msg="调整成功")
@admin_app_users_router.get("/{user_id}/quota-logs", summary="用户估值次数操作日志", response_model=PageResponse[AppUserQuotaLogOut])
async def quota_logs(user_id: int, page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100)):
qs = AppUserQuotaLog.filter(app_user_id=user_id)
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
models = [
AppUserQuotaLogOut(
id=r.id,
app_user_id=r.app_user_id,
operator_id=r.operator_id,
operator_name=r.operator_name,
before_count=r.before_count,
after_count=r.after_count,
op_type=r.op_type,
remark=r.remark,
created_at=r.created_at.isoformat() if r.created_at else "",
) for r in rows
]
data_items = [m.model_dump() for m in models]
return SuccessExtra(data=data_items, total=total, page=page, page_size=page_size, msg="获取成功")
@admin_app_users_router.put("/{user_id}", summary="更新App用户信息", response_model=BasicResponse[dict])
async def update_app_user(user_id: int, data: AppUserUpdateSchema):
user = await app_user_controller.update_user_info(user_id, data)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return Success(data={
"id": user.id,
"phone": user.phone,
"wechat": getattr(user, "alias", None),
"company_name": getattr(user, "company_name", None),
"company_address": getattr(user, "company_address", None),
"company_contact": getattr(user, "company_contact", None),
"company_phone": getattr(user, "company_phone", None),
"company_email": getattr(user, "company_email", None),
"notes": getattr(user, "notes", None),
"is_active": user.is_active,
"created_at": user.created_at.isoformat() if user.created_at else "",
"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="账号已注销")

View File

@ -6,19 +6,34 @@ from app.schemas.app_user import (
AppUserJWTOut,
AppUserInfoOut,
AppUserUpdateSchema,
AppUserChangePasswordSchema
AppUserChangePasswordSchema,
AppUserDashboardOut,
AppUserQuotaOut,
)
from app.schemas.app_user import AppUserRegisterOut, TokenValidateOut
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()
@router.post("/register", response_model=dict, summary="用户注册")
@router.post("/register", response_model=BasicResponse[dict], summary="用户注册")
async def register(
register_data: AppUserRegisterSchema
):
@ -28,20 +43,16 @@ async def register(
"""
try:
user = await app_user_controller.register(register_data)
return {
"code": 200,
"message": "注册成功",
"data": {
"user_id": user.id,
"phone": user.phone,
"default_password": register_data.phone[-6:] # 返回默认密码供用户知晓
}
}
return Success(data={
"user_id": user.id,
"phone": user.phone,
"default_password": register_data.phone[-6:]
})
except Exception as e:
raise HTTPException(status_code=200, detail=str(e))
@router.post("/login", response_model=AppUserJWTOut, summary="用户登录")
@router.post("/login", response_model=BasicResponse[dict], summary="用户登录")
async def login(
login_data: AppUserLoginSchema
):
@ -61,30 +72,132 @@ async def login(
# 生成访问令牌
access_token = create_app_user_access_token(user.id, user.phone)
return AppUserJWTOut(
access_token=access_token,
token_type="bearer",
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
return Success(data={
"access_token": access_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
})
@router.post("/logout", summary="用户登出")
@router.post("/logout", summary="用户登出", response_model=BasicResponse[dict])
async def logout(current_user: AppUser = Depends(get_current_app_user)):
"""
用户登出客户端需要删除本地token
"""
return {"code": 200, "message": "登出成功"}
return Success(data={"message": "登出成功"})
@router.get("/profile", response_model=AppUserInfoOut, summary="获取用户信息")
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)):
"""
获取当前用户信息
"""
return current_user
user_info = AppUserInfoOut(
id=current_user.id,
phone=current_user.phone,
nickname=getattr(current_user, "alias", None),
avatar=None,
company_name=current_user.company_name,
company_address=current_user.company_address,
company_contact=current_user.company_contact,
company_phone=current_user.company_phone,
company_email=current_user.company_email,
is_active=current_user.is_active,
last_login=current_user.last_login,
created_at=current_user.created_at,
updated_at=current_user.updated_at,
remaining_quota=current_user.remaining_quota,
)
return Success(data=user_info.model_dump())
@router.put("/profile", response_model=AppUserInfoOut, summary="更新用户信息")
@router.get("/dashboard", response_model=BasicResponse[dict], summary="用户首页摘要")
async def get_dashboard(current_user: AppUser = Depends(get_current_app_user)):
"""
用户首页摘要
功能:
- 返回剩余估值次数暂以 0 占位后续可接入配额系统
- 返回最近一条估值评估记录若有
- 返回待处理发票数量
"""
# 最近估值记录
latest = await user_valuation_controller.model.filter(user_id=current_user.id).order_by("-created_at").first()
latest_out = None
if latest:
latest_out = {
"id": latest.id,
"asset_name": latest.asset_name,
"valuation_result": latest.final_value_ab,
"status": latest.status,
"created_at": latest.created_at.isoformat() if latest.created_at else "",
}
# 待处理发票数量
try:
pending_invoices = await invoice_controller.count_pending_for_user(current_user.id)
except Exception:
pending_invoices = 0
# 剩余估值次数
remaining_quota = current_user.remaining_quota
return Success(data={"remaining_quota": remaining_quota, "latest_valuation": latest_out, "pending_invoices": pending_invoices})
@router.get("/quota", response_model=BasicResponse[dict], summary="剩余估值次数")
async def get_quota(current_user: AppUser = Depends(get_current_app_user)):
"""
剩余估值次数查询
说明:
- 当前实现返回默认 0 次与用户类型占位
- 若后续接入配额系统可从数据库中读取真实值
"""
remaining_count = current_user.remaining_quota
return Success(data={"remaining_count": remaining_count})
@router.put("/profile", response_model=BasicResponse[dict], summary="更新用户信息")
async def update_profile(
update_data: AppUserUpdateSchema,
current_user: AppUser = Depends(get_current_app_user)
@ -96,10 +209,26 @@ async def update_profile(
if not updated_user:
raise HTTPException(status_code=404, detail="用户不存在")
return updated_user
user_info = AppUserInfoOut(
id=updated_user.id,
phone=updated_user.phone,
nickname=getattr(updated_user, "alias", None),
avatar=None,
company_name=updated_user.company_name,
company_address=updated_user.company_address,
company_contact=updated_user.company_contact,
company_phone=updated_user.company_phone,
company_email=updated_user.company_email,
is_active=updated_user.is_active,
last_login=updated_user.last_login,
created_at=updated_user.created_at,
updated_at=updated_user.updated_at,
remaining_quota=updated_user.remaining_quota,
)
return Success(data=user_info.model_dump())
@router.post("/change-password", summary="修改密码")
@router.post("/change-password", summary="修改密码", response_model=BasicResponse[dict])
async def change_password(
password_data: AppUserChangePasswordSchema,
current_user: AppUser = Depends(get_current_app_user)
@ -116,19 +245,12 @@ async def change_password(
if not success:
raise HTTPException(status_code=400, detail="原密码错误")
return {"code": 200, "message": "密码修改成功"}
return Success(data={"message": "密码修改成功"})
@router.get("/validate-token", summary="验证token")
@router.get("/validate-token", summary="验证token", response_model=BasicResponse[dict])
async def validate_token(current_user: AppUser = Depends(get_current_app_user)):
"""
验证token是否有效
"""
return {
"code": 200,
"message": "token有效",
"data": {
"user_id": current_user.id,
"phone": current_user.phone
}
}
return Success(data={"user_id": current_user.id, "phone": current_user.phone})

View File

@ -9,6 +9,8 @@ import asyncio
import time
from app.controllers.user_valuation import user_valuation_controller
from app.controllers.valuation import valuation_controller
from app.schemas.valuation import ValuationAssessmentUpdate
from app.schemas.valuation import (
UserValuationCreate,
UserValuationQuery,
@ -16,13 +18,13 @@ from app.schemas.valuation import (
UserValuationOut,
UserValuationDetail
)
from app.schemas.base import Success, SuccessExtra
from app.schemas.base import Success, BasicResponse
from app.utils.app_user_jwt import get_current_app_user_id, get_current_app_user
from app.utils.calculation_engine import FinalValueACalculator
from app.utils.calculation_engine.cultural_value_b2.sub_formulas.living_heritage_b21 import cross_border_depth_dict
# from app.utils.calculation_engine.cultural_value_b2.sub_formulas.living_heritage_b21 import cross_border_depth_dict
from app.utils.calculation_engine.drp import DynamicPledgeRateCalculator
from app.utils.calculation_engine.economic_value_b1.sub_formulas.basic_value_b11 import calculate_popularity_score, \
calculate_infringement_score, calculate_patent_usage_score, calculate_patent_score
# from app.utils.calculation_engine.economic_value_b1.sub_formulas.basic_value_b11 import calculate_popularity_score
from app.utils.calculation_engine.economic_value_b1.sub_formulas.traffic_factor_b12 import calculate_search_index_s1
from app.log.log import logger
from app.models.esg import ESG
@ -35,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
@ -68,9 +70,13 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
except Exception as e:
logger.warning("valuation.policy_fetch_timeout industry={} err={}", data.industry, repr(e))
policy_match_score = getattr(policy_obj, 'score', 0.0) or 0.0
# 提取 经济价值B1 计算参数
input_data_by_b1 = await _extract_calculation_params_b1(data)
input_data_by_b1 = await _extract_calculation_params_b1(
data, esg_score=esg_score, industry_coefficient=fix_num_score, policy_match_score=policy_match_score
)
# ESG关联价值 ESG分 (0-10分)
input_data_by_b1["esg_score"] = esg_score
# 行业修正系数I
@ -78,25 +84,53 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
# 政策匹配度
input_data_by_b1["policy_match_score"] = policy_match_score
# 侵权分 默认 6
# 法律风险/侵权记录通过司法API查询诉讼状态
# 评分规则:无诉讼(10分), 已解决诉讼(7分), 未解决诉讼(0分)
lawsuit_status_text = "无诉讼" # 默认无诉讼
judicial_api_response = {} # 保存API原始返回用于日志
try:
judicial_data = universal_api.query_judicial_data(data.institution)
_data = judicial_data["data"].get("target", None) # 诉讼标的
if _data:
infringement_score = 0.0
_data = judicial_data.get("data", {})
judicial_api_response = _data # 保存原始返回
target = _data.get("target", None) # 诉讼标的
total = _data.get("total", 0) # 诉讼总数
if target or total > 0:
# 有诉讼记录,检查是否已解决
settled = _data.get("settled", False)
if settled:
lawsuit_status_text = "已解决诉讼"
infringement_score = 7.0
else:
lawsuit_status_text = "未解决诉讼"
infringement_score = 0.0
else:
lawsuit_status_text = "无诉讼"
infringement_score = 10.0
except:
logger.info(f"法律风险查询结果: 机构={data.institution} 诉讼状态={lawsuit_status_text} 评分={infringement_score}")
except Exception as e:
logger.warning(f"法律风险查询失败: {e}")
lawsuit_status_text = "查询失败"
infringement_score = 0.0
judicial_api_response = {"error": str(e)}
input_data_by_b1["infringement_score"] = infringement_score
# 保存诉讼状态文本,用于前端展示
lawsuit_status_for_display = lawsuit_status_text
# 获取专利信息 TODO 参数
# 获取专利信息
patent_api_response = {} # 保存API原始返回用于日志
patent_matched_count = 0
patent_years_total = 0
try:
patent_data = universal_api.query_patent_info(data.industry)
patent_api_response = patent_data # 保存原始返回
except Exception as e:
logger.warning("valuation.patent_api_error err={}", repr(e))
input_data_by_b1["patent_count"] = 0.0
input_data_by_b1["patent_score"] = 0.0
patent_api_response = {"error": str(e)}
patent_dict = patent_data if isinstance(patent_data, dict) else {}
inner_data = patent_dict.get("data", {}) if isinstance(patent_dict.get("data", {}), dict) else {}
@ -107,13 +141,20 @@ 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 = calculate_patent_usage_score(len(matched))
input_data_by_b1["patent_count"] = float(patent_count)
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
patent_score = calculate_patent_score(calculate_total_years(data_list))
patent_years_total = calculate_total_years(data_list)
if patent_years_total > 10:
patent_score = 10.0
elif patent_years_total >= 5:
patent_score = 7.0
else:
patent_score = 3.0
input_data_by_b1["patent_score"] = patent_score
# 提取 文化价值B2 计算参数
@ -139,10 +180,211 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
"market_data": input_data_by_c,
}
calculator = FinalValueACalculator()
# 计算最终估值A统一计算
calculation_result = await calculator.calculate_complete_final_value_a(input_data)
# 步骤1立即更新计算输入参数不管后续是否成功
try:
await valuation_controller.update_calc(
valuation_id,
ValuationAssessmentUpdate(
calculation_input=input_data,
)
)
logger.info("valuation.input_updated valuation_id={}", valuation_id)
except Exception as e:
logger.warning("valuation.failed_to_update_input valuation_id={} err={}", valuation_id, repr(e))
# 步骤1.5更新内置API计算字段
try:
# 准备内置API计算字段的值
api_calc_fields = {}
# ESG关联价值
api_calc_fields["esg_value"] = str(esg_score) if esg_score is not None else None
# 政策匹配度
api_calc_fields["policy_matching"] = str(policy_match_score) if policy_match_score is not None else None
# 侵权记录/法律风险 - 使用实际查询到的诉讼状态
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)
api_calc_fields["patent_count"] = str(patent_count_value) if patent_count_value is not None else None
# 结构复杂度纹样基因熵值B22
structure_complexity_value = input_data_by_b2.get("structure_complexity", 1.5)
api_calc_fields["pattern_complexity"] = str(structure_complexity_value) if structure_complexity_value is not None else None
# 归一化信息熵H
normalized_entropy_value = input_data_by_b2.get("normalized_entropy", 9)
api_calc_fields["normalized_entropy"] = str(normalized_entropy_value) if normalized_entropy_value is not None else None
# 线上课程点击量暂时没有计算逻辑设为None或默认值
# api_calc_fields["online_course_views"] = None
# 基础质押率和流量修正系数暂时没有计算逻辑设为None或默认值
# api_calc_fields["base_pledge_rate"] = None
# api_calc_fields["flow_correction"] = None
if api_calc_fields:
await valuation_controller.update_calc(
valuation_id,
ValuationAssessmentUpdate(**api_calc_fields)
)
logger.info("valuation.api_calc_fields_updated valuation_id={} fields={}", valuation_id, list(api_calc_fields.keys()))
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_calc(
valuation_id,
ValuationAssessmentUpdate(
model_value_b=calculation_result.get('model_value_b'),
market_value_c=calculation_result.get('market_value_c'),
final_value_ab=calculation_result.get('final_value_ab'),
calculation_result=calculation_result,
status='pending',
)
)
logger.info(
"valuation.result_updated valuation_id={} model_b={} market_c={} final_ab={}",
valuation_id,
calculation_result.get('model_value_b'),
calculation_result.get('market_value_c'),
calculation_result.get('final_value_ab'),
)
except Exception as e:
logger.warning("valuation.failed_to_update_result valuation_id={} err={}", valuation_id, repr(e))
# 计算动态质押
drp_c = DynamicPledgeRateCalculator()
@ -152,7 +394,45 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
'''
# 解析月交易额字符串为数值
monthly_amount = drp_c.parse_monthly_transaction_amount(data.monthly_transaction_amount or "")
drp_start_ts = time.monotonic()
drp_result = drp_c.calculate_dynamic_pledge_rate(monthly_amount, data.heritage_asset_level)
drp_duration_ms = int((time.monotonic() - drp_start_ts) * 1000)
# 记录动态质押率计算步骤
await valuation_controller.log_formula_step(
valuation_id,
"DYNAMIC_PLEDGE_RATE",
status="completed",
input_params={
"monthly_transaction_amount": data.monthly_transaction_amount,
"monthly_amount": monthly_amount,
"heritage_asset_level": data.heritage_asset_level,
},
output_result={
"dynamic_pledge_rate": drp_result,
"duration_ms": drp_duration_ms,
},
)
logger.info("valuation.drp_calculated valuation_id={} drp={} duration_ms={}", valuation_id, drp_result, drp_duration_ms)
# 步骤3更新动态质押率及相关字段
try:
# 从动态质押率计算器中获取基础质押率和流量修正系数
base_pledge_rate_value = "0.5" # 固定值:基础质押率 = 0.5
flow_correction_value = "0.3" # 固定值:流量修正系数 = 0.3
await valuation_controller.update_calc(
valuation_id,
ValuationAssessmentUpdate(
dynamic_pledge_rate=drp_result,
base_pledge_rate=base_pledge_rate_value,
flow_correction=flow_correction_value,
)
)
logger.info("valuation.drp_updated valuation_id={} drp={} base_rate={} flow_correction={}",
valuation_id, drp_result, base_pledge_rate_value, flow_correction_value)
except Exception as e:
logger.warning("valuation.failed_to_update_drp valuation_id={} err={}", valuation_id, repr(e))
# 结构化日志:关键分值
try:
@ -168,46 +448,80 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
except Exception:
pass
# 创建估值评估记录
result = await user_valuation_controller.create_valuation(
user_id=user_id,
data=data,
calculation_result=calculation_result,
calculation_input={
'model_data': {
'economic_data': list(input_data.get('model_data', {}).get('economic_data', {}).keys()),
'cultural_data': list(input_data.get('model_data', {}).get('cultural_data', {}).keys()),
'risky_data': list(input_data.get('model_data', {}).get('risky_data', {}).keys()),
},
'market_data': list(input_data.get('market_data', {}).keys()),
},
drp_result=drp_result,
status='success' # 计算成功设置为approved状态
)
# 步骤4计算完成保持状态为 pending等待后台审核
try:
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, result.id)
logger.info("valuation.background_calc_success user_id={} valuation_id={}", user_id, valuation_id)
except Exception as e:
import traceback
print(traceback.format_exc())
logger.error("valuation.background_calc_failed user_id={} err={}", user_id, repr(e))
# 计算失败时也创建记录状态设置为failed
# 计算失败时更新记录为失败状态
try:
result = await user_valuation_controller.create_valuation(
user_id=user_id,
data=data,
calculation_result=None,
calculation_input=None,
drp_result=None,
status='rejected' # 计算失败设置为rejected状态
)
logger.info("valuation.failed_record_created user_id={} valuation_id={}", user_id, result.id)
if 'valuation_id' in locals():
# 准备失败时需要更新的字段
fail_update_fields = {"status": "rejected"}
# 如果 input_data 已经准备好,确保 calculation_input 被更新(即使计算失败)
if 'input_data' in locals():
fail_update_fields["calculation_input"] = input_data
# 如果内置API计算字段已经准备好也尝试更新即使计算失败
# 这些字段在步骤1.5中计算如果步骤1.5执行了,这些变量应该已经存在
api_calc_fields = {}
if 'esg_score' in locals():
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 '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():
patent_count_value = input_data_by_b1.get("patent_count", 0.0)
api_calc_fields["patent_count"] = str(patent_count_value) if patent_count_value is not None else None
if 'input_data_by_b2' in locals():
structure_complexity_value = input_data_by_b2.get("structure_complexity", 1.5)
api_calc_fields["pattern_complexity"] = str(structure_complexity_value) if structure_complexity_value is not None else None
normalized_entropy_value = input_data_by_b2.get("normalized_entropy", 9)
api_calc_fields["normalized_entropy"] = str(normalized_entropy_value) if normalized_entropy_value is not None else None
# 合并所有需要更新的字段
fail_update_fields.update(api_calc_fields)
try:
await valuation_controller.update_calc(
valuation_id,
ValuationAssessmentUpdate(**fail_update_fields)
)
logger.info("valuation.failed_but_fields_saved valuation_id={} fields={}", valuation_id, list(fail_update_fields.keys()))
except Exception as input_err:
logger.warning("valuation.failed_to_save_fields_on_error valuation_id={} err={}", valuation_id, repr(input_err))
# 如果保存失败,至少更新状态
try:
fail_update = ValuationAssessmentUpdate(status='rejected')
await valuation_controller.update_calc(valuation_id, fail_update)
except Exception:
pass
else:
# 如果 valuation_id 都不存在,说明在创建记录时就失败了,无法更新
logger.warning("valuation.failed_before_creation user_id={}", user_id)
except Exception as create_error:
logger.error("valuation.failed_to_create_record user_id={} err={}", user_id, repr(create_error))
logger.error("valuation.failed_to_update_record user_id={} err={}", user_id, repr(create_error))
@app_valuations_router.post("/", summary="创建估值评估")
@app_valuations_router.post("/", summary="创建估值评估", response_model=BasicResponse[dict])
async def calculate_valuation(
background_tasks: BackgroundTasks,
data: UserValuationCreate,
@ -278,20 +592,52 @@ async def calculate_valuation(
"""
try:
# 添加后台任务
background_tasks.add_task(_perform_valuation_calculation, user_id, data)
from app.models.user import AppUser, AppUserQuotaLog
user = await AppUser.filter(id=user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if (user.remaining_quota or 0) < 1:
raise HTTPException(status_code=400, detail="估值次数不足")
before = user.remaining_quota or 0
user.remaining_quota = before - 1
await user.save()
try:
await AppUserQuotaLog.create(
app_user_id=user_id,
operator_id=user_id,
operator_name=user.alias or user.username or user.phone or "",
before_count=before,
after_count=before - 1,
remark="发起估值"
)
except Exception:
pass
logger.info("valuation.task_queued user_id={} asset_name={} industry={}",
user_id, getattr(data, 'asset_name', None), getattr(data, 'industry', None))
# 先创建估值记录以获取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
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)
},
msg="估值计算任务已启动"
"asset_name": getattr(data, 'asset_name', None),
"valuation_id": valuation_id,
"order_no": str(valuation_id)
}
)
except Exception as e:
@ -299,7 +645,12 @@ async def calculate_valuation(
raise HTTPException(status_code=500, detail=f"任务提交失败: {str(e)}")
async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str, Any]:
async def _extract_calculation_params_b1(
data: UserValuationCreate,
esg_score: float = 0.0,
industry_coefficient: float = 0.0,
policy_match_score: float = 0.0,
) -> Dict[str, Any]:
"""
从用户提交的数据中提取计算所需的参数
@ -315,7 +666,13 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
# 法律强度L相关参数
# 普及地域分值 默认 7分
popularity_score = calculate_popularity_score(data.application_coverage)
# 普及地域分:全球覆盖(10)、全国覆盖(7)、区域覆盖(4),默认全国覆盖(7)
try:
coverage = data.application_coverage or "全国覆盖"
mapping = {"全球覆盖": 10.0, "全国覆盖": 7.0, "区域覆盖": 4.0}
popularity_score = mapping.get(coverage, 7.0)
except Exception:
popularity_score = 7.0
# 创新投入比 = (研发费用/营收) * 100
try:
@ -326,8 +683,7 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
innovation_ratio = 0.0
# 流量因子B12相关参数
# 近30天搜索指数S1 - 从社交媒体数据计算 TODO 需要使用第三方API
baidu_index = 1
# 近30天搜索指数S1 - 使用微信指数除以10计算
# 获取微信指数并计算近30天平均值
try:
@ -336,10 +692,9 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
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
@ -386,6 +741,7 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
'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 目前未参与计算,先移除
@ -393,7 +749,10 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
'link_views': safe_float(data.link_views),
# 政策乘数B13相关参数
'implementation_stage': implementation_stage,
'funding_support': funding_support
'funding_support': funding_support,
'esg_score': safe_float(esg_score),
'industry_coefficient': safe_float(industry_coefficient),
'policy_match_score': safe_float(policy_match_score),
}
@ -427,18 +786,72 @@ 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.3科技载体0.5国家外交礼品1.0
cross_border_depth = cross_border_depth_dict(data.cooperation_depth)
# 跨界合作深度:将枚举映射为分值
# 前端传入的是数字字符串 ("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,
}
if isinstance(val, str):
cross_border_depth = mapping.get(val, safe_float(val))
else:
cross_border_depth = safe_float(val)
except Exception:
cross_border_depth = 0.0
# 纹样基因值B22相关参数
# 以下三项需由后续模型/服务计算;此处提供默认可计算占位
#
# 历史传承度HI(用户填写)
historical_inheritance = sum([safe_float(i) for i in data.historical_evidence])
# 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):
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)):
# 列表顺序:[出土实物, 古代文献, 传承人佐证, 现代研究]
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(系统计算)
normalized_entropy = 9 # 默认值 归一化信息熵H(系统计算)
logger.info(
"b2.params inheritor_level_coefficient={} offline_sessions={} douyin_views={} kuaishou_views={} bilibili_views={} cross_border_depth={} historical_inheritance={} structure_complexity={} normalized_entropy={}",
inheritor_level_coefficient,
offline_sessions,
douyin_views,
kuaishou_views,
bilibili_views,
cross_border_depth,
historical_inheritance,
structure_complexity,
normalized_entropy,
)
return {
"inheritor_level_coefficient": inheritor_level_coefficient,
"offline_sessions": offline_sessions,
@ -452,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 = 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,
}
@ -557,13 +989,14 @@ 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
}
@app_valuations_router.get("/", summary="获取我的估值评估列表")
@app_valuations_router.get("/", summary="获取我的估值评估列表", response_model=BasicResponse[dict])
async def get_my_valuations(
query: UserValuationQuery = Depends(),
current_user: AppUser = Depends(get_current_app_user)
@ -580,13 +1013,14 @@ async def get_my_valuations(
# 使用model_dump_json()来正确序列化datetime然后解析为dict列表
import json
serialized_items = [json.loads(item.model_dump_json()) for item in result.items]
return SuccessExtra(
data=serialized_items,
total=result.total,
page=result.page,
page_size=result.size,
pages=result.pages,
msg="获取估值评估列表成功"
return Success(
data={
"items": serialized_items,
"total": result.total,
"page": result.page,
"page_size": result.size,
"pages": result.pages,
}
)
except Exception as e:
raise HTTPException(
@ -595,7 +1029,7 @@ async def get_my_valuations(
)
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情")
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情", response_model=BasicResponse[dict])
async def get_valuation_detail(
valuation_id: int,
current_user: AppUser = Depends(get_current_app_user)
@ -618,7 +1052,7 @@ async def get_valuation_detail(
# 使用model_dump_json()来正确序列化datetime然后解析为dict
import json
result_dict = json.loads(result.model_dump_json())
return Success(data=result_dict, msg="获取估值评估详情成功")
return Success(data=result_dict)
except HTTPException:
raise
except Exception as e:
@ -628,7 +1062,7 @@ async def get_valuation_detail(
)
@app_valuations_router.get("/statistics/overview", summary="获取我的估值评估统计")
@app_valuations_router.get("/statistics/overview", summary="获取我的估值评估统计", response_model=BasicResponse[dict])
async def get_my_valuation_statistics(
current_user: AppUser = Depends(get_current_app_user)
):
@ -639,7 +1073,7 @@ async def get_my_valuation_statistics(
result = await user_valuation_controller.get_user_valuation_statistics(
user_id=current_user.id
)
return Success(data=result, msg="获取统计信息成功")
return Success(data=result)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -647,7 +1081,7 @@ async def get_my_valuation_statistics(
)
@app_valuations_router.delete("/{valuation_id}", summary="删除估值评估")
@app_valuations_router.delete("/{valuation_id}", summary="删除估值评估", response_model=BasicResponse[dict])
async def delete_valuation(
valuation_id: int,
current_user: AppUser = Depends(get_current_app_user)
@ -667,7 +1101,7 @@ async def delete_valuation(
detail="估值评估记录不存在或已被删除"
)
return Success(data={"deleted": True}, msg="删除估值评估成功")
return Success(data={"deleted": True})
except HTTPException:
raise
except Exception as e:
@ -705,3 +1139,4 @@ def safe_float(v):
return float(v)
except (ValueError, TypeError):
return 0.0
from app.log.log import logger

View File

@ -6,7 +6,7 @@ from app.controllers.user import user_controller
from app.core.ctx import CTX_USER_ID
from app.core.dependency import DependAuth
from app.models.admin import Api, Menu, Role, User
from app.schemas.base import Fail, Success
from app.schemas.base import Fail, Success, BasicResponse
from app.schemas.login import *
from app.schemas.users import UpdatePassword
from app.settings import settings
@ -16,7 +16,7 @@ from app.utils.password import get_password_hash, verify_password
router = APIRouter()
@router.post("/access_token", summary="获取token")
@router.post("/access_token", summary="获取token", response_model=BasicResponse[JWTOut])
async def login_access_token(credentials: CredentialsSchema):
user: User = await user_controller.authenticate(credentials)
await user_controller.update_last_login(user.id)
@ -37,7 +37,7 @@ async def login_access_token(credentials: CredentialsSchema):
return Success(data=data.model_dump())
@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth])
@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth], response_model=BasicResponse[dict])
async def get_userinfo():
user_id = CTX_USER_ID.get()
user_obj = await user_controller.get(id=user_id)
@ -46,7 +46,7 @@ async def get_userinfo():
return Success(data=data)
@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth])
@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth], response_model=BasicResponse[list])
async def get_user_menu():
user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first()
@ -74,7 +74,7 @@ async def get_user_menu():
return Success(data=res)
@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth])
@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth], response_model=BasicResponse[list])
async def get_user_api():
user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first()
@ -91,7 +91,7 @@ async def get_user_api():
return Success(data=apis)
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth])
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth], response_model=BasicResponse[dict])
async def update_user_password(req_in: UpdatePassword):
user_id = CTX_USER_ID.get()
user = await user_controller.get(user_id)

View File

@ -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相关参数

View File

@ -2,12 +2,14 @@ from fastapi import APIRouter, Query
from app.controllers.dept import dept_controller
from app.schemas import Success
from app.schemas.base import BasicResponse, MessageOut
from app.schemas.depts import BaseDept
from app.schemas.depts import *
router = APIRouter()
@router.get("/list", summary="查看部门列表")
@router.get("/list", summary="查看部门列表", response_model=BasicResponse[list[BaseDept]])
async def list_dept(
name: str = Query(None, description="部门名称"),
):
@ -15,7 +17,7 @@ async def list_dept(
return Success(data=dept_tree)
@router.get("/get", summary="查看部门")
@router.get("/get", summary="查看部门", response_model=BasicResponse[BaseDept])
async def get_dept(
id: int = Query(..., description="部门ID"),
):
@ -24,7 +26,7 @@ async def get_dept(
return Success(data=data)
@router.post("/create", summary="创建部门")
@router.post("/create", summary="创建部门", response_model=BasicResponse[MessageOut])
async def create_dept(
dept_in: DeptCreate,
):
@ -32,7 +34,7 @@ async def create_dept(
return Success(msg="Created Successfully")
@router.post("/update", summary="更新部门")
@router.post("/update", summary="更新部门", response_model=BasicResponse[MessageOut])
async def update_dept(
dept_in: DeptUpdate,
):
@ -40,7 +42,7 @@ async def update_dept(
return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除部门")
@router.delete("/delete", summary="删除部门", response_model=BasicResponse[MessageOut])
async def delete_dept(
dept_id: int = Query(..., description="部门ID"),
):

View File

@ -3,12 +3,14 @@ from tortoise.expressions import Q
from app.controllers.esg import esg_controller
from app.schemas import Success, SuccessExtra
from app.schemas.base import BasicResponse, PageResponse, MessageOut
from app.schemas.esg import ESGResponse
from app.schemas.esg import ESGCreate, ESGUpdate, ESGResponse
router = APIRouter(tags=["ESG管理"])
@router.get("/list", summary="查看ESG列表")
@router.get("/list", summary="查看ESG列表", response_model=PageResponse[ESGResponse])
async def list_esg(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -28,7 +30,7 @@ async def list_esg(
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看ESG详情")
@router.get("/get", summary="查看ESG详情", response_model=BasicResponse[ESGResponse])
async def get_esg(
id: int = Query(..., description="ESG ID"),
):
@ -37,7 +39,7 @@ async def get_esg(
return Success(data=data)
@router.post("/create", summary="创建ESG")
@router.post("/create", summary="创建ESG", response_model=BasicResponse[MessageOut])
async def create_esg(
esg_in: ESGCreate,
):
@ -49,7 +51,7 @@ async def create_esg(
return Success(msg="创建成功")
@router.post("/update", summary="更新ESG")
@router.post("/update", summary="更新ESG", response_model=BasicResponse[MessageOut])
async def update_esg(
esg_in: ESGUpdate,
):
@ -63,7 +65,7 @@ async def update_esg(
return Success(msg="更新成功")
@router.delete("/delete", summary="删除ESG")
@router.delete("/delete", summary="删除ESG", response_model=BasicResponse[MessageOut])
async def delete_esg(
esg_id: int = Query(..., description="ESG ID"),
):

View File

@ -3,12 +3,14 @@ from tortoise.expressions import Q
from app.controllers.index import index_controller
from app.schemas import Success, SuccessExtra
from app.schemas.base import BasicResponse, PageResponse, MessageOut
from app.schemas.index import IndexResponse
from app.schemas.index import IndexCreate, IndexUpdate, IndexResponse
router = APIRouter(tags=["指数管理"])
@router.get("/list", summary="查看指数列表")
@router.get("/list", summary="查看指数列表", response_model=PageResponse[IndexResponse])
async def list_index(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -25,7 +27,7 @@ async def list_index(
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看指数详情")
@router.get("/get", summary="查看指数详情", response_model=BasicResponse[IndexResponse])
async def get_index(
id: int = Query(..., description="指数 ID"),
):
@ -34,7 +36,7 @@ async def get_index(
return Success(data=data)
@router.post("/create", summary="创建指数")
@router.post("/create", summary="创建指数", response_model=BasicResponse[MessageOut])
async def create_index(
index_in: IndexCreate,
):
@ -46,7 +48,7 @@ async def create_index(
return Success(msg="创建成功")
@router.post("/update", summary="更新指数")
@router.post("/update", summary="更新指数", response_model=BasicResponse[MessageOut])
async def update_index(
index_in: IndexUpdate,
):
@ -60,7 +62,7 @@ async def update_index(
return Success(msg="更新成功")
@router.delete("/delete", summary="删除指数")
@router.delete("/delete", summary="删除指数", response_model=BasicResponse[MessageOut])
async def delete_index(
index_id: int = Query(..., description="指数 ID"),
):

View File

@ -3,12 +3,13 @@ from tortoise.expressions import Q
from app.controllers.industry import industry_controller
from app.schemas import Success, SuccessExtra
from app.schemas.base import BasicResponse, PageResponse, MessageOut
from app.schemas.industry import IndustryCreate, IndustryUpdate, IndustryResponse
router = APIRouter(tags=["行业管理"])
@router.get("/list", summary="查看行业列表")
@router.get("/list", summary="查看行业列表", response_model=PageResponse[IndustryResponse])
async def list_industry(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -25,7 +26,7 @@ async def list_industry(
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看行业详情")
@router.get("/get", summary="查看行业详情", response_model=BasicResponse[IndustryResponse])
async def get_industry(
id: int = Query(..., description="行业 ID"),
):
@ -34,7 +35,7 @@ async def get_industry(
return Success(data=data)
@router.post("/create", summary="创建行业")
@router.post("/create", summary="创建行业", response_model=BasicResponse[MessageOut])
async def create_industry(
industry_in: IndustryCreate,
):
@ -46,7 +47,7 @@ async def create_industry(
return Success(msg="创建成功")
@router.post("/update", summary="更新行业")
@router.post("/update", summary="更新行业", response_model=BasicResponse[MessageOut])
async def update_industry(
industry_in: IndustryUpdate,
):
@ -60,7 +61,7 @@ async def update_industry(
return Success(msg="更新成功")
@router.delete("/delete", summary="删除行业")
@router.delete("/delete", summary="删除行业", response_model=BasicResponse[MessageOut])
async def delete_industry(
industry_id: int = Query(..., description="行业 ID"),
):

View File

@ -0,0 +1,3 @@
from .invoice import invoice_router
__all__ = ["invoice_router"]

View File

@ -0,0 +1,183 @@
from fastapi import APIRouter, Query, Depends, Header, HTTPException
from typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
from app.schemas.invoice import (
InvoiceCreate,
InvoiceUpdate,
UpdateStatus,
UpdateType,
InvoiceHeaderCreate,
InvoiceHeaderUpdate,
PaymentReceiptCreate,
AppCreateInvoiceWithReceipt,
InvoiceOut,
InvoiceList,
InvoiceHeaderOut,
PaymentReceiptOut,
)
from app.controllers.invoice import invoice_controller
from app.utils.app_user_jwt import get_current_app_user
from app.core.dependency import DependAuth, DependPermission
from app.models.user import AppUser
from app.models.invoice import InvoiceHeader
invoice_router = APIRouter(tags=["发票管理"])
@invoice_router.get("/list", summary="获取发票列表", response_model=PageResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def list_invoices(
phone: Optional[str] = Query(None),
company_name: Optional[str] = Query(None),
tax_number: Optional[str] = Query(None),
status: Optional[str] = Query(None),
ticket_type: Optional[str] = Query(None),
invoice_type: Optional[str] = Query(None),
user_id: Optional[int] = Query(None, description="按App用户ID过滤"),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
):
"""
发票列表查询
参数支持按手机号公司名称税号状态发票类型进行筛选
返回分页结构
"""
result = await invoice_controller.list(
page=page,
page_size=page_size,
phone=phone,
company_name=company_name,
tax_number=tax_number,
status=status,
ticket_type=ticket_type,
invoice_type=invoice_type,
app_user_id=user_id,
)
return SuccessExtra(
data=result.items, total=result.total, page=result.page, page_size=result.page_size, msg="获取成功"
)
@invoice_router.get("/detail", summary="发票详情", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def invoice_detail(id: int = Query(...)):
"""
根据ID获取发票详情
"""
out = await invoice_controller.get_out(id)
if not out:
return Success(data={}, msg="未找到")
return Success(data=out, msg="获取成功")
@invoice_router.post("/create", summary="创建发票", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def create_invoice(data: InvoiceCreate):
"""
创建发票记录
"""
inv = await invoice_controller.create(data)
out = await invoice_controller.get_out(inv.id)
return Success(data=out, msg="创建成功")
@invoice_router.post("/update", summary="更新发票", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def update_invoice(data: InvoiceUpdate, id: int = Query(...)):
"""
更新发票记录
"""
updated = await invoice_controller.update(id, data)
out = await invoice_controller.get_out(id) if updated else None
return Success(data=out or {}, msg="更新成功" if updated else "未找到")
@invoice_router.delete("/delete", summary="删除发票", response_model=BasicResponse[MessageOut], dependencies=[DependAuth, DependPermission])
async def delete_invoice(id: int = Query(...)):
"""
删除发票记录
"""
try:
await invoice_controller.remove(id)
ok = True
except Exception:
ok = False
return Success(data={"deleted": ok}, msg="删除成功" if ok else "未找到")
@invoice_router.post("/update-status", summary="更新发票状态", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def update_invoice_status(data: UpdateStatus):
"""
更新发票状态pending|invoiced|rejected|refunded
"""
out = await invoice_controller.update_status(data)
return Success(data=out or {}, msg="更新成功" if out else "未找到")
@invoice_router.post("/{id}/receipt", summary="上传付款凭证", response_model=BasicResponse[dict], dependencies=[DependAuth, DependPermission])
async def upload_payment_receipt(id: int, data: PaymentReceiptCreate):
"""
上传对公转账付款凭证
"""
receipt = await invoice_controller.create_receipt(id, data)
detail = await invoice_controller.get_receipt_by_id(receipt.id)
return Success(data=detail, msg="上传成功")
@invoice_router.get("/headers", summary="发票抬头列表", response_model=PageResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
async def get_invoice_headers(
app_user_id: Optional[int] = Query(None, description="按App用户ID过滤"),
user_id: Optional[int] = Query(None, description="按App用户ID过滤(兼容参数)"),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
):
"""
管理端抬头列表管理员token支持按 App 用户过滤与分页
"""
uid = app_user_id if app_user_id is not None else user_id
qs = InvoiceHeader.all()
if uid is not None:
qs = qs.filter(app_user_id=uid)
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
items = [InvoiceHeaderOut.model_validate(r) for r in rows]
return SuccessExtra(data=[i.model_dump() for i in items], total=total, page=page, page_size=page_size, msg="获取成功")
@invoice_router.get("/headers/{id}", summary="发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
async def get_invoice_header_by_id(id: int):
"""
获取发票抬头详情
"""
header = await invoice_controller.get_header_by_id(id)
return Success(data=header or {}, msg="获取成功" if header else "未找到")
@invoice_router.post("/headers", summary="新增发票抬头", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
async def create_invoice_header(data: InvoiceHeaderCreate, app_user_id: Optional[int] = Query(None)):
"""
新增发票抬头
"""
header = await invoice_controller.create_header(user_id=app_user_id, data=data)
return Success(data=header, msg="创建成功")
@invoice_router.put("/{id}/type", summary="更新发票类型", response_model=BasicResponse[InvoiceOut], dependencies=[DependAuth, DependPermission])
async def update_invoice_type(id: int, data: UpdateType):
"""
更新发票的电子/纸质与专票/普票类型
"""
out = await invoice_controller.update_type(id, data)
return Success(data=out or {}, msg="更新成功" if out else "未找到")
@invoice_router.delete("/headers/{id}", summary="删除发票抬头", response_model=BasicResponse[MessageOut], dependencies=[DependAuth, DependPermission])
async def delete_invoice_header(id: int):
ok = await invoice_controller.delete_header(id)
return Success(msg="删除成功" if ok else "未找到")
@invoice_router.put("/headers/{id}", summary="更新发票抬头", response_model=BasicResponse[InvoiceHeaderOut], dependencies=[DependAuth, DependPermission])
async def update_invoice_header(id: int, data: InvoiceHeaderUpdate):
header = await invoice_controller.update_header(id, data)
return Success(data=header or {}, msg="更新成功" if header else "未找到")

View File

@ -3,7 +3,8 @@ import logging
from fastapi import APIRouter, Query
from app.controllers.menu import menu_controller
from app.schemas.base import Fail, Success, SuccessExtra
from app.schemas.base import Fail, Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
from app.schemas.menus import BaseMenu
from app.schemas.menus import *
logger = logging.getLogger(__name__)
@ -11,7 +12,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看菜单列表")
@router.get("/list", summary="查看菜单列表", response_model=PageResponse[BaseMenu])
async def list_menu(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -28,7 +29,7 @@ async def list_menu(
return SuccessExtra(data=res_menu, total=len(res_menu), page=page, page_size=page_size)
@router.get("/get", summary="查看菜单")
@router.get("/get", summary="查看菜单", response_model=BasicResponse[BaseMenu])
async def get_menu(
menu_id: int = Query(..., description="菜单id"),
):
@ -36,7 +37,7 @@ async def get_menu(
return Success(data=result)
@router.post("/create", summary="创建菜单")
@router.post("/create", summary="创建菜单", response_model=BasicResponse[MessageOut])
async def create_menu(
menu_in: MenuCreate,
):
@ -44,7 +45,7 @@ async def create_menu(
return Success(msg="Created Success")
@router.post("/update", summary="更新菜单")
@router.post("/update", summary="更新菜单", response_model=BasicResponse[MessageOut])
async def update_menu(
menu_in: MenuUpdate,
):
@ -52,7 +53,7 @@ async def update_menu(
return Success(msg="Updated Success")
@router.delete("/delete", summary="删除菜单")
@router.delete("/delete", summary="删除菜单", response_model=BasicResponse[MessageOut])
async def delete_menu(
id: int = Query(..., description="菜单id"),
):

View File

@ -3,12 +3,14 @@ from tortoise.expressions import Q
from app.controllers.policy import policy_controller
from app.schemas import Success, SuccessExtra
from app.schemas.base import BasicResponse, PageResponse, MessageOut
from app.schemas.policy import PolicyResponse
from app.schemas.policy import PolicyCreate, PolicyUpdate, PolicyResponse
router = APIRouter(tags=["政策管理"])
@router.get("/list", summary="查看政策列表")
@router.get("/list", summary="查看政策列表", response_model=PageResponse[PolicyResponse])
async def list_policy(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -28,7 +30,7 @@ async def list_policy(
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看政策详情")
@router.get("/get", summary="查看政策详情", response_model=BasicResponse[PolicyResponse])
async def get_policy(
id: int = Query(..., description="政策 ID"),
):
@ -37,7 +39,7 @@ async def get_policy(
return Success(data=data)
@router.post("/create", summary="创建政策")
@router.post("/create", summary="创建政策", response_model=BasicResponse[MessageOut])
async def create_policy(
policy_in: PolicyCreate,
):
@ -49,7 +51,7 @@ async def create_policy(
return Success(msg="创建成功")
@router.post("/update", summary="更新政策")
@router.post("/update", summary="更新政策", response_model=BasicResponse[MessageOut])
async def update_policy(
policy_in: PolicyUpdate,
):
@ -63,7 +65,7 @@ async def update_policy(
return Success(msg="更新成功")
@router.delete("/delete", summary="删除政策")
@router.delete("/delete", summary="删除政策", response_model=BasicResponse[MessageOut])
async def delete_policy(
policy_id: int = Query(..., description="政策 ID"),
):

View File

@ -5,14 +5,15 @@ from fastapi.exceptions import HTTPException
from tortoise.expressions import Q
from app.controllers import role_controller
from app.schemas.base import Success, SuccessExtra
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
from app.schemas.roles import BaseRole
from app.schemas.roles import *
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看角色列表")
@router.get("/list", summary="查看角色列表", response_model=PageResponse[BaseRole])
async def list_role(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -26,7 +27,7 @@ async def list_role(
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看角色")
@router.get("/get", summary="查看角色", response_model=BasicResponse[BaseRole])
async def get_role(
role_id: int = Query(..., description="角色ID"),
):
@ -34,7 +35,7 @@ async def get_role(
return Success(data=await role_obj.to_dict())
@router.post("/create", summary="创建角色")
@router.post("/create", summary="创建角色", response_model=BasicResponse[MessageOut])
async def create_role(role_in: RoleCreate):
if await role_controller.is_exist(name=role_in.name):
raise HTTPException(
@ -45,13 +46,13 @@ async def create_role(role_in: RoleCreate):
return Success(msg="Created Successfully")
@router.post("/update", summary="更新角色")
@router.post("/update", summary="更新角色", response_model=BasicResponse[MessageOut])
async def update_role(role_in: RoleUpdate):
await role_controller.update(id=role_in.id, obj_in=role_in)
return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除角色")
@router.delete("/delete", summary="删除角色", response_model=BasicResponse[MessageOut])
async def delete_role(
role_id: int = Query(..., description="角色ID"),
):
@ -59,14 +60,14 @@ async def delete_role(
return Success(msg="Deleted Success")
@router.get("/authorized", summary="查看角色权限")
@router.get("/authorized", summary="查看角色权限", response_model=BasicResponse[BaseRole])
async def get_role_authorized(id: int = Query(..., description="角色ID")):
role_obj = await role_controller.get(id=id)
data = await role_obj.to_dict(m2m=True)
return Success(data=data)
@router.post("/authorized", summary="更新角色权限")
@router.post("/authorized", summary="更新角色权限", response_model=BasicResponse[MessageOut])
async def update_role_authorized(role_in: RoleUpdateMenusApis):
role_obj = await role_controller.get(id=role_in.id)
await role_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos)

224
app/api/v1/sms/sms.py Normal file
View File

@ -0,0 +1,224 @@
from fastapi import APIRouter, HTTPException, status, Depends
from pydantic import BaseModel, Field
from typing import Optional
import time
from app.services.sms_client import sms_client
from app.services.rate_limiter import PhoneRateLimiter
from app.services.sms_store import store
from app.core.dependency import DependAuth
from app.log import logger
from app.schemas.app_user import AppUserInfoOut, AppUserJWTOut
from app.schemas.base import BasicResponse, Success
class SendCodeRequest(BaseModel):
phone: str = Field(...)
class SendReportRequest(BaseModel):
phone: str = Field(...)
class VerifyCodeRequest(BaseModel):
phone: str = Field(...)
code: str = Field(...)
class SendResponse(BaseModel):
status: str = Field(..., description="发送状态")
message: str = Field(..., description="说明")
request_id: Optional[str] = Field(None, description="请求ID")
class VerifyResponse(BaseModel):
status: str = Field(..., description="验证状态")
message: str = Field(..., description="说明")
class SMSLoginResponse(BaseModel):
user: AppUserInfoOut
token: AppUserJWTOut
rate_limiter = PhoneRateLimiter(60)
router = APIRouter(tags=["短信服务"])
@router.post("/send-code", response_model=BasicResponse[dict], summary="验证码发送")
async def send_code(payload: SendCodeRequest) -> BasicResponse[dict]:
"""发送验证码短信
Args:
payload: 请求体含手机号与验证码
Returns:
发送结果响应
"""
ok, reason = store.allow_send(payload.phone)
if not ok:
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=str(reason))
try:
otp = store.generate_code()
store.set_code(payload.phone, otp)
from app.settings import settings
if settings.SMS_DEBUG_LOG_CODE:
logger.info("sms.code generated phone={} code={}", payload.phone, otp)
res = sms_client.send_code(payload.phone, otp)
code = res.get("Code") or res.get("ResponseCode")
rid = res.get("RequestId") or res.get("MessageId")
if code == "OK":
logger.info("sms.send_code success phone={} request_id={}", payload.phone, rid)
return Success(
data={
"status": "OK",
"message": "sent",
"request_id": str(rid) if rid else None,
}
)
msg = res.get("Message") or res.get("ResponseDescription") or "error"
logger.warning("sms.send_code fail phone={} code={} msg={}", payload.phone, code, msg)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(msg))
except HTTPException:
raise
except Exception as e:
logger.error("sms.send_code exception err={}", repr(e))
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="短信服务异常")
@router.post("/send-report", response_model=BasicResponse[dict], summary="报告通知发送", dependencies=[DependAuth])
async def send_report(payload: SendReportRequest) -> BasicResponse[dict]:
"""发送报告通知短信
Args:
payload: 请求体含手机号
Returns:
发送结果响应
"""
ok, reason = store.allow_send(payload.phone)
if not ok:
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=str(reason))
try:
res = sms_client.send_report(payload.phone)
code = res.get("Code") or res.get("ResponseCode")
rid = res.get("RequestId") or res.get("MessageId")
if code == "OK":
logger.info("sms.send_report success phone={} request_id={}", payload.phone, rid)
return Success(
data={
"status": "OK",
"message": "sent",
"request_id": str(rid) if rid else None,
}
)
msg = res.get("Message") or res.get("ResponseDescription") or "error"
logger.warning("sms.send_report fail phone={} code={} msg={}", payload.phone, code, msg)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(msg))
except HTTPException:
raise
except Exception as e:
logger.error("sms.send_report exception err={}", repr(e))
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="短信服务异常")
@router.post("/verify-code", summary="验证码验证", response_model=BasicResponse[dict])
async def verify_code(payload: VerifyCodeRequest) -> BasicResponse[dict]:
"""验证验证码
Args:
payload: 请求体含手机号与验证码
Returns:
验证结果字典
"""
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:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=str(reason))
record = store.get_code(payload.phone)
if not record:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码已过期")
code, expires_at = record
if time.time() > expires_at:
store.clear_code(payload.phone)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码已过期")
if payload.code != code:
count, locked = store.record_verify_failure(payload.phone)
if locked:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail="尝试次数过多,已锁定")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码错误")
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"})
class SMSLoginRequest(BaseModel):
phone_number: str = Field(...)
verification_code: str = Field(...)
device_id: Optional[str] = Field(None)
@router.post("/login", summary="短信验证码登录", response_model=BasicResponse[dict])
async def sms_login(payload: SMSLoginRequest) -> BasicResponse[dict]:
from app.settings import settings
bypass = settings.SMS_BYPASS_CODE and payload.verification_code == settings.SMS_BYPASS_CODE
if not bypass:
ok, reason = store.can_verify(payload.phone_number)
if not ok:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=str(reason))
record = store.get_code(payload.phone_number)
if not record:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="验证码过期")
code, expires_at = record
if time.time() > expires_at:
store.clear_code(payload.phone_number)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="验证码过期")
if payload.verification_code != code:
count, locked = store.record_verify_failure(payload.phone_number)
if locked:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail="尝试次数过多,已锁定")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="验证码错误")
from app.controllers.app_user import app_user_controller
from app.schemas.app_user import AppUserRegisterSchema, AppUserInfoOut, AppUserJWTOut
from app.utils.app_user_jwt import create_app_user_access_token, ACCESS_TOKEN_EXPIRE_MINUTES
user = await app_user_controller.get_user_by_phone(payload.phone_number)
if user is None:
user = await app_user_controller.register(AppUserRegisterSchema(phone=payload.phone_number))
await app_user_controller.update_last_login(user.id)
access_token = create_app_user_access_token(user_id=user.id, phone=user.phone)
if not bypass:
store.clear_code(payload.phone_number)
store.reset_failures(payload.phone_number)
logger.info("sms.login success phone={}", payload.phone_number)
user_info = AppUserInfoOut(
id=user.id,
phone=user.phone,
nickname=getattr(user, "alias", None),
avatar=None,
company_name=user.company_name,
company_address=user.company_address,
company_contact=user.company_contact,
company_phone=user.company_phone,
company_email=user.company_email,
is_active=user.is_active,
last_login=user.last_login,
created_at=user.created_at,
updated_at=user.updated_at,
remaining_quota=user.remaining_quota,
)
token_out = AppUserJWTOut(access_token=access_token, expires_in=ACCESS_TOKEN_EXPIRE_MINUTES)
return Success(data={"user": user_info.model_dump(), "token": token_out.model_dump()})
class VerifyCodeRequest(BaseModel):
phone: str = Field(...)
code: str = Field(...)

View File

@ -0,0 +1,3 @@
from .transactions import transactions_router
__all__ = ["transactions_router"]

View File

@ -0,0 +1,173 @@
from fastapi import APIRouter, Query, UploadFile, File, HTTPException
from typing import Optional
from app.schemas.base import Success, SuccessExtra, PageResponse, BasicResponse
from app.schemas.invoice import PaymentReceiptOut
from app.controllers.invoice import invoice_controller
from app.models.invoice import PaymentReceipt
from fastapi import Body
from app.schemas.transactions import SendEmailRequest, SendEmailResponse
from app.services.email_client import email_client
from app.models.invoice import EmailSendLog
from app.settings.config import settings
from app.log.log import logger
import httpx
transactions_router = APIRouter(tags=["交易管理"])
@transactions_router.get("/receipts", summary="对公转账记录列表", response_model=PageResponse[PaymentReceiptOut])
async def list_receipts(
phone: Optional[str] = Query(None),
wechat: Optional[str] = Query(None),
company_name: Optional[str] = Query(None),
tax_number: Optional[str] = Query(None),
status: Optional[str] = Query(None),
ticket_type: Optional[str] = Query(None),
invoice_type: Optional[str] = Query(None),
created_at: Optional[list[int]] = Query(None),
submitted_start: Optional[str] = Query(None),
submitted_end: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
):
"""
对公转账记录列表含提交时间凭证与关联企业信息
"""
result = await invoice_controller.list_receipts(
page=page,
page_size=page_size,
phone=phone,
wechat=wechat,
company_name=company_name,
tax_number=tax_number,
status=status,
ticket_type=ticket_type,
invoice_type=invoice_type,
created_at=created_at,
submitted_start=submitted_start,
submitted_end=submitted_end,
)
return SuccessExtra(
data=result["items"],
total=result["total"],
page=result["page"],
page_size=result["page_size"],
msg="获取成功",
)
@transactions_router.get("/receipts/{id}", summary="对公转账记录详情", response_model=BasicResponse[PaymentReceiptOut])
async def get_receipt_detail(id: int):
"""
对公转账记录详情
"""
data = await invoice_controller.get_receipt_by_id(id)
return Success(data=data or {}, msg="获取成功" if data else "未找到")
@transactions_router.post("/send-email", summary="发送邮件", response_model=BasicResponse[SendEmailResponse])
async def send_email(payload: SendEmailRequest = Body(...)):
attachments = []
urls = []
try:
domain = payload.email.split("@")[-1]
import dns.resolver
try:
dns.resolver.resolve(domain, "MX")
except Exception:
dns.resolver.resolve(domain, "A")
except Exception:
raise HTTPException(status_code=400, detail="收件方地址域名不可用或未正确解析")
if payload.file_urls:
urls.extend([u.strip().strip('`') for u in payload.file_urls if isinstance(u, str)])
if 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:
for u in urls:
r = await client.get(u)
r.raise_for_status()
attachments.append((r.content, u.split("/")[-1]))
except Exception as e:
raise HTTPException(status_code=400, detail=f"附件下载失败: {e}")
logger.info("transactions.email_send_start email={} subject={}", payload.email, payload.subject or "")
try:
result = email_client.send_many(payload.email, payload.subject, payload.body, attachments)
except RuntimeError as e:
result = {"status": "FAIL", "error": str(e)}
except Exception as e:
result = {"status": "FAIL", "error": str(e)}
body_summary = payload.body
status = result.get("status")
error = result.get("error")
first_name = attachments[0][1] if attachments else None
first_url = urls[0] if urls else None
log = await EmailSendLog.create(
email=payload.email,
subject=payload.subject,
body_summary=body_summary,
file_name=first_name,
file_url=first_url,
status=status,
error=error,
)
if status == "OK":
logger.info("transactions.email_send_ok email={}", payload.email)
else:
logger.error("transactions.email_send_fail email={} err={}", payload.email, error)
if payload.receipt_id:
try:
r = await PaymentReceipt.filter(id=payload.receipt_id).first()
if r:
try:
inv = await r.invoice
if inv:
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_status_updated receipt_id={} invoice_id={} status={}", payload.receipt_id, inv.id, target)
except Exception as 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))
return Success(data={"status": status, "log_id": log.id, "error": error}, msg="发送成功" if status == "OK" else "发送失败")
@transactions_router.get("/smtp-config", summary="SMTP配置状态", response_model=BasicResponse[dict])
async def smtp_config_status():
configured = all([
settings.SMTP_HOST,
settings.SMTP_PORT,
settings.SMTP_FROM,
settings.SMTP_USERNAME,
settings.SMTP_PASSWORD,
])
data = {
"host": bool(settings.SMTP_HOST),
"port": bool(settings.SMTP_PORT),
"from": bool(settings.SMTP_FROM),
"username": bool(settings.SMTP_USERNAME),
"password": bool(settings.SMTP_PASSWORD),
"tls": settings.SMTP_TLS,
"configured": configured,
}
return Success(data=data, msg="OK")

View File

@ -1,14 +1,11 @@
from fastapi import APIRouter, UploadFile, File
from app.controllers.upload import UploadController
from app.schemas.upload import ImageUploadResponse
from app.schemas.upload import ImageUploadResponse, FileUploadResponse
from app.schemas.base import BasicResponse, Success
router = APIRouter()
@router.post("/image", response_model=ImageUploadResponse, summary="上传图片")
async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
"""
上传图片接口
:param file: 图片文件
:return: 图片URL和文件名
"""
return await UploadController.upload_image(file)
@router.post("/file", response_model=BasicResponse[dict], summary="统一上传接口")
async def upload(file: UploadFile = File(...)) -> BasicResponse[dict]:
res = await UploadController.upload_any(file)
return Success(data={"url": res.url, "filename": res.filename, "content_type": res.content_type})

View File

@ -5,7 +5,8 @@ from tortoise.expressions import Q
from app.controllers.dept import dept_controller
from app.controllers.user import user_controller
from app.schemas.base import Fail, Success, SuccessExtra
from app.schemas.base import Fail, Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
from app.schemas.users import BaseUser
from app.schemas.users import *
logger = logging.getLogger(__name__)
@ -13,7 +14,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/list", summary="查看用户列表")
@router.get("/list", summary="查看用户列表", response_model=PageResponse[BaseUser])
async def list_user(
page: int = Query(1, description="页码"),
page_size: int = Query(10, description="每页数量"),
@ -37,7 +38,7 @@ async def list_user(
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
@router.get("/get", summary="查看用户")
@router.get("/get", summary="查看用户", response_model=BasicResponse[BaseUser])
async def get_user(
user_id: int = Query(..., description="用户ID"),
):
@ -46,7 +47,7 @@ async def get_user(
return Success(data=user_dict)
@router.post("/create", summary="创建用户")
@router.post("/create", summary="创建用户", response_model=BasicResponse[MessageOut])
async def create_user(
user_in: UserCreate,
):
@ -58,7 +59,7 @@ async def create_user(
return Success(msg="Created Successfully")
@router.post("/update", summary="更新用户")
@router.post("/update", summary="更新用户", response_model=BasicResponse[MessageOut])
async def update_user(
user_in: UserUpdate,
):
@ -67,7 +68,7 @@ async def update_user(
return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除用户")
@router.delete("/delete", summary="删除用户", response_model=BasicResponse[MessageOut])
async def delete_user(
user_id: int = Query(..., description="用户ID"),
):
@ -75,7 +76,7 @@ async def delete_user(
return Success(msg="Deleted Successfully")
@router.post("/reset_password", summary="重置密码")
@router.post("/reset_password", summary="重置密码", response_model=BasicResponse[MessageOut])
async def reset_password(user_id: int = Body(..., description="用户ID", embed=True)):
await user_controller.reset_password(user_id)
return Success(msg="密码已重置为123456")

View File

@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Query, Depends
from typing import Optional
from typing import Optional, List
from app.controllers.valuation import valuation_controller
from app.schemas.valuation import (
@ -9,15 +9,16 @@ from app.schemas.valuation import (
ValuationAssessmentList,
ValuationAssessmentQuery,
ValuationApprovalRequest,
ValuationAdminNotesUpdate
ValuationAdminNotesUpdate,
ValuationCalculationStepOut
)
from app.schemas.base import Success, SuccessExtra
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
from app.core.ctx import CTX_USER_ID
valuations_router = APIRouter(tags=["估值评估"])
@valuations_router.post("/", summary="创建估值评估")
@valuations_router.post("/", summary="创建估值评估", response_model=BasicResponse[ValuationAssessmentOut])
async def create_valuation(data: ValuationAssessmentCreate):
"""创建新的估值评估记录"""
try:
@ -25,37 +26,78 @@ async def create_valuation(data: ValuationAssessmentCreate):
user_id = CTX_USER_ID.get()
print(user_id)
result = await valuation_controller.create(data, user_id)
return Success(data=result, msg="创建成功")
import json
return Success(data=json.loads(result.model_dump_json()), msg="创建成功")
except Exception as e:
raise HTTPException(status_code=400, detail=f"创建失败: {str(e)}")
@valuations_router.get("/statistics/overview", summary="获取统计信息")
@valuations_router.get("/statistics/overview", summary="获取统计信息", response_model=BasicResponse[dict])
async def get_statistics():
"""获取估值评估统计信息"""
result = await valuation_controller.get_statistics()
return Success(data=result, msg="获取统计信息成功")
@valuations_router.get("/{valuation_id}", summary="获取估值评估详情")
@valuations_router.get("/{valuation_id}", summary="获取估值评估详情", response_model=BasicResponse[ValuationAssessmentOut])
async def get_valuation(valuation_id: int):
"""根据ID获取估值评估详情"""
result = await valuation_controller.get_by_id(valuation_id)
if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在")
return Success(data=result, msg="获取成功")
import json
return Success(data=json.loads(result.model_dump_json()), msg="获取成功")
@valuations_router.put("/{valuation_id}", summary="更新估值评估")
@valuations_router.get("/{valuation_id}/steps", summary="获取估值计算步骤", response_model=BasicResponse[List[ValuationCalculationStepOut]])
async def get_valuation_steps(valuation_id: int):
"""根据估值ID获取所有计算步骤"""
steps = await valuation_controller.get_calculation_steps(valuation_id)
if not steps:
raise HTTPException(status_code=404, detail="未找到该估值的计算步骤")
import json
steps_out = [json.loads(step.model_dump_json()) for step in steps]
return Success(data=steps_out, msg="获取计算步骤成功")
@valuations_router.get("/{valuation_id}/report", summary="获取估值计算报告Markdown格式")
async def get_valuation_report(valuation_id: int):
"""
根据估值ID生成计算过程的 Markdown 报告
返回格式化的 Markdown 文档包含
- 估值基本信息
- 计算结果摘要
- 详细计算过程按公式层级组织
- 每个公式的输入参数输出结果状态等信息
"""
try:
markdown = await valuation_controller.get_calculation_report_markdown(valuation_id)
from fastapi import Response
return Response(
content=markdown,
media_type="text/markdown; charset=utf-8",
headers={
"Content-Disposition": f'attachment; filename="valuation_report_{valuation_id}.md"'
}
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"生成报告失败: {str(e)}")
@valuations_router.put("/{valuation_id}", summary="更新估值评估", response_model=BasicResponse[ValuationAssessmentOut])
async def update_valuation(valuation_id: int, data: ValuationAssessmentUpdate):
"""更新估值评估记录"""
result = await valuation_controller.update(valuation_id, data)
result = await valuation_controller.update1(valuation_id, data)
if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在")
return Success(data=result, msg="更新成功")
import json
return Success(data=json.loads(result.model_dump_json()), msg="更新成功")
@valuations_router.delete("/{valuation_id}", summary="删除估值评估")
@valuations_router.delete("/{valuation_id}", summary="删除估值评估", response_model=BasicResponse[dict])
async def delete_valuation(valuation_id: int):
"""软删除估值评估记录"""
result = await valuation_controller.delete(valuation_id)
@ -64,7 +106,7 @@ async def delete_valuation(valuation_id: int):
return Success(data={"deleted": True}, msg="删除成功")
@valuations_router.get("/", summary="获取估值评估列表")
@valuations_router.get("/", summary="获取估值评估列表", response_model=PageResponse[ValuationAssessmentOut])
async def get_valuations(
asset_name: Optional[str] = Query(None, description="资产名称"),
institution: Optional[str] = Query(None, description="所属机构"),
@ -72,8 +114,14 @@ async def get_valuations(
heritage_level: Optional[str] = Query(None, description="非遗等级"),
status: Optional[str] = Query(None, description="评估状态"),
is_active: Optional[bool] = Query(None, description="是否激活"),
phone: Optional[str] = Query(None, description="手机号模糊查询"),
submitted_start: Optional[str] = Query(None, description="提交时间开始毫秒或ISO"),
submitted_end: Optional[str] = Query(None, description="提交时间结束毫秒或ISO"),
audited_start: Optional[str] = Query(None, description="审核时间开始证书修改时间毫秒或ISO"),
audited_end: Optional[str] = Query(None, description="审核时间结束证书修改时间毫秒或ISO"),
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量")
size: int = Query(10, ge=1, le=100, description="每页数量"),
page_size: Optional[int] = Query(None, alias="page_size", ge=1, le=100, description="每页数量")
):
"""获取估值评估列表,支持筛选和分页"""
query = ValuationAssessmentQuery(
@ -83,12 +131,19 @@ async def get_valuations(
heritage_level=heritage_level,
status=status,
is_active=is_active,
phone=phone,
submitted_start=submitted_start,
submitted_end=submitted_end,
audited_start=audited_start,
audited_end=audited_end,
page=page,
size=size
size=page_size if page_size is not None else size
)
result = await valuation_controller.get_list(query)
import json
items = [json.loads(item.model_dump_json()) for item in result.items]
return SuccessExtra(
data=result.items,
data=items,
total=result.total,
page=result.page,
page_size=result.size,
@ -97,16 +152,19 @@ async def get_valuations(
)
@valuations_router.get("/search/keyword", summary="搜索估值评估")
@valuations_router.get("/search/keyword", summary="搜索估值评估", response_model=PageResponse[ValuationAssessmentOut])
async def search_valuations(
keyword: str = Query(..., description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量")
size: int = Query(10, ge=1, le=100, description="每页数量"),
page_size: Optional[int] = Query(None, alias="page_size", ge=1, le=100, description="每页数量")
):
"""根据关键词搜索估值评估记录"""
result = await valuation_controller.search(keyword, page, size)
result = await valuation_controller.search(keyword, page, page_size if page_size is not None else size)
import json
items = [json.loads(item.model_dump_json()) for item in result.items]
return SuccessExtra(
data=result.items,
data=items,
total=result.total,
page=result.page,
page_size=result.size,
@ -116,7 +174,7 @@ async def search_valuations(
# 批量操作接口
@valuations_router.post("/batch/delete", summary="批量删除估值评估")
@valuations_router.post("/batch/delete", summary="批量删除估值评估", response_model=BasicResponse[dict])
async def batch_delete_valuations(valuation_ids: list[int]):
"""批量软删除估值评估记录"""
success_count = 0
@ -140,7 +198,7 @@ async def batch_delete_valuations(valuation_ids: list[int]):
# 导出接口
@valuations_router.get("/export/excel", summary="导出估值评估数据")
@valuations_router.get("/export/excel", summary="导出估值评估数据", response_model=BasicResponse[dict])
async def export_valuations(
asset_name: Optional[str] = Query(None, description="资产名称"),
institution: Optional[str] = Query(None, description="所属机构"),
@ -154,28 +212,31 @@ async def export_valuations(
# 审核管理接口
@valuations_router.post("/{valuation_id}/approve", summary="审核通过估值评估")
@valuations_router.post("/{valuation_id}/approve", summary="审核通过估值评估", response_model=BasicResponse[ValuationAssessmentOut])
async def approve_valuation(valuation_id: int, data: ValuationApprovalRequest):
"""审核通过估值评估"""
result = await valuation_controller.approve_valuation(valuation_id, data.admin_notes)
if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在")
return Success(data=result, msg="审核通过成功")
import json
return Success(data=json.loads(result.model_dump_json()), msg="审核通过成功")
@valuations_router.post("/{valuation_id}/reject", summary="审核拒绝估值评估")
@valuations_router.post("/{valuation_id}/reject", summary="审核拒绝估值评估", response_model=BasicResponse[ValuationAssessmentOut])
async def reject_valuation(valuation_id: int, data: ValuationApprovalRequest):
"""审核拒绝估值评估"""
result = await valuation_controller.reject_valuation(valuation_id, data.admin_notes)
if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在")
return Success(data=result, msg="审核拒绝成功")
import json
return Success(data=json.loads(result.model_dump_json()), msg="审核拒绝成功")
@valuations_router.put("/{valuation_id}/admin-notes", summary="更新管理员备注")
@valuations_router.put("/{valuation_id}/admin-notes", summary="更新管理员备注", response_model=BasicResponse[ValuationAssessmentOut])
async def update_admin_notes(valuation_id: int, data: ValuationAdminNotesUpdate):
"""更新管理员备注"""
result = await valuation_controller.update_admin_notes(valuation_id, data.admin_notes)
if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在")
return Success(data=result, msg="管理员备注更新成功")
import json
return Success(data=json.loads(result.model_dump_json()), msg="管理员备注更新成功")

View File

@ -1,4 +1,5 @@
from app.models.user import AppUser
from app.models.user import AppUserQuotaLog
from app.schemas.app_user import AppUserRegisterSchema, AppUserLoginSchema, AppUserUpdateSchema
from app.utils.password import get_password_hash, verify_password
from app.core.crud import CRUDBase
@ -20,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="手机号已存在")
# 生成默认密码:手机号后六位
@ -41,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:
@ -56,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:
"""
@ -85,11 +95,40 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
# 更新字段
update_dict = update_data.model_dump(exclude_unset=True)
if "nickname" in update_dict:
update_dict["alias"] = update_dict.pop("nickname")
update_dict.pop("avatar", None)
for field, value in update_dict.items():
setattr(user, field, value)
await user.save()
return user
async def update_quota(self, operator_id: int, operator_name: str, user_id: int, target_count: Optional[int] = None, delta: Optional[int] = None, op_type: str = "调整", remark: Optional[str] = None) -> Optional[AppUser]:
user = await self.model.filter(id=user_id).first()
if not user:
return None
before = int(getattr(user, "remaining_quota", 0) or 0)
after = before
if target_count is not None:
after = max(0, int(target_count))
elif delta is not None:
after = max(0, before + int(delta))
user.remaining_quota = after
await user.save()
await AppUserQuotaLog.create(
app_user_id=user_id,
operator_id=operator_id,
operator_name=operator_name,
before_count=before,
after_count=after,
op_type=op_type,
remark=remark,
)
# if remark is not None:
# user.notes = remark
# await user.save()
return user
async def change_password(self, user_id: int, old_password: str, new_password: str) -> bool:
"""
@ -119,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()
app_user_controller = AppUserController()

381
app/controllers/invoice.py Normal file
View File

@ -0,0 +1,381 @@
from typing import Optional, List
from tortoise.queryset import QuerySet
from app.core.crud import CRUDBase
from app.models.invoice import Invoice, InvoiceHeader, PaymentReceipt
from app.schemas.invoice import (
InvoiceCreate,
InvoiceUpdate,
InvoiceOut,
InvoiceList,
InvoiceHeaderCreate,
InvoiceHeaderUpdate,
InvoiceHeaderOut,
UpdateStatus,
UpdateType,
PaymentReceiptCreate,
PaymentReceiptOut,
)
class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
"""发票控制器"""
def __init__(self):
super().__init__(model=Invoice)
async def create_header(self, user_id: Optional[int], data: InvoiceHeaderCreate) -> InvoiceHeaderOut:
"""
创建发票抬头
参数:
user_id: 关联的 AppUser ID可选
data: 发票抬头创建数据
返回:
InvoiceHeaderOut: 抬头输出对象
"""
payload = data.model_dump()
for k in ["register_address", "register_phone", "bank_name", "bank_account"]:
if payload.get(k) is None:
payload[k] = ""
if payload.get("is_default"):
if user_id is not None:
await InvoiceHeader.filter(app_user_id=user_id).update(is_default=False)
header = await InvoiceHeader.create(app_user_id=user_id, **payload)
return InvoiceHeaderOut.model_validate(header)
async def get_headers(self, user_id: Optional[int] = None) -> List[InvoiceHeaderOut]:
"""
获取发票抬头列表
参数:
user_id: 可筛选 AppUser 的抬头
返回:
List[InvoiceHeaderOut]: 抬头列表
"""
qs = InvoiceHeader.all()
if user_id is not None:
qs = qs.filter(app_user_id=user_id)
headers = await qs.order_by("-created_at")
return [InvoiceHeaderOut.model_validate(h) for h in headers]
async def get_header_by_id(self, id_: int) -> Optional[InvoiceHeaderOut]:
"""
根据ID获取抬头
参数:
id_: 抬头ID
返回:
InvoiceHeaderOut None
"""
header = await InvoiceHeader.filter(id=id_).first()
return InvoiceHeaderOut.model_validate(header) if header else None
async def delete_header(self, id_: int) -> bool:
header = await InvoiceHeader.filter(id=id_).first()
if not header:
return False
await header.delete()
return True
async def update_header(self, id_: int, data: InvoiceHeaderUpdate) -> Optional[InvoiceHeaderOut]:
header = await InvoiceHeader.filter(id=id_).first()
if not header:
return None
update_data = data.model_dump(exclude_unset=True)
if update_data:
if update_data.get("is_default"):
if header.app_user_id is not None:
await InvoiceHeader.filter(app_user_id=header.app_user_id).exclude(id=header.id).update(is_default=False)
await header.update_from_dict(update_data).save()
# 同步引用该抬头的发票基本信息
sync_fields = {
"company_name": header.company_name,
"tax_number": header.tax_number,
"register_address": header.register_address,
"register_phone": header.register_phone,
"bank_name": header.bank_name,
"bank_account": header.bank_account,
"email": header.email,
}
await Invoice.filter(header_id=header.id).update(**sync_fields)
return InvoiceHeaderOut.model_validate(header)
async def list(self, page: int = 1, page_size: int = 10, **filters) -> InvoiceList:
"""
获取发票列表支持筛选与分页
参数:
page: 页码
page_size: 每页数量
**filters: phonecompany_nametax_numberstatusticket_typeinvoice_type时间范围等
返回:
InvoiceList: 分页结果
"""
qs: QuerySet = self.model.all()
if filters.get("phone"):
qs = qs.filter(phone__icontains=filters["phone"])
if filters.get("company_name"):
qs = qs.filter(company_name__icontains=filters["company_name"])
if filters.get("tax_number"):
qs = qs.filter(tax_number__icontains=filters["tax_number"])
if filters.get("status"):
qs = qs.filter(status=filters["status"])
if filters.get("ticket_type"):
qs = qs.filter(ticket_type=filters["ticket_type"])
if filters.get("invoice_type"):
qs = qs.filter(invoice_type=filters["invoice_type"])
if filters.get("app_user_id"):
qs = qs.filter(app_user_id=filters["app_user_id"])
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
items = [
InvoiceOut(
id=row.id,
created_at=row.created_at.isoformat() if row.created_at else "",
ticket_type=row.ticket_type,
invoice_type=row.invoice_type,
phone=row.phone,
email=row.email,
company_name=row.company_name,
tax_number=row.tax_number,
register_address=row.register_address,
register_phone=row.register_phone,
bank_name=row.bank_name,
bank_account=row.bank_account,
status=row.status,
app_user_id=row.app_user_id,
header_id=row.header_id,
wechat=row.wechat,
)
for row in rows
]
return InvoiceList(items=items, total=total, page=page, page_size=page_size)
async def update_status(self, data: UpdateStatus) -> Optional[InvoiceOut]:
"""
更新发票状态
参数:
data: 包含 id 与目标状态
返回:
更新后的发票输出或 None
"""
inv = await self.model.filter(id=data.id).first()
if not inv:
return None
inv.status = data.status
await inv.save()
return await self.get_out(inv.id)
async def update_type(self, id_: int, data: UpdateType) -> Optional[InvoiceOut]:
"""
更新发票类型电子/纸质专票/普票
参数:
id_: 发票ID
data: 类型更新数据
返回:
更新后的发票输出或 None
"""
inv = await self.model.filter(id=id_).first()
if not inv:
return None
inv.ticket_type = data.ticket_type
inv.invoice_type = data.invoice_type
await inv.save()
return await self.get_out(inv.id)
async def create_receipt(self, invoice_id: int, data: PaymentReceiptCreate) -> PaymentReceiptOut:
"""
上传付款凭证
参数:
invoice_id: 发票ID
data: 凭证创建数据
返回:
PaymentReceiptOut
"""
receipt = await PaymentReceipt.create(invoice_id=invoice_id, **data.model_dump())
return PaymentReceiptOut(
id=receipt.id,
url=receipt.url,
note=receipt.note,
verified=receipt.verified,
created_at=receipt.created_at.isoformat() if receipt.created_at else "",
extra=receipt.extra,
)
async def list_receipts(self, page: int = 1, page_size: int = 10, **filters) -> dict:
"""
对公转账记录列表
参数:
page: 页码
page_size: 每页数量
**filters: 提交时间范围手机号微信号公司名称/税号状态开票类型等
返回:
dict: { items, total, page, page_size }
"""
qs = PaymentReceipt.all().prefetch_related("invoice")
# 通过关联发票进行筛选
if filters.get("phone"):
qs = qs.filter(invoice__phone__icontains=filters["phone"])
if filters.get("wechat"):
qs = qs.filter(invoice__wechat__icontains=filters["wechat"])
if filters.get("company_name"):
qs = qs.filter(invoice__company_name__icontains=filters["company_name"])
if filters.get("tax_number"):
qs = qs.filter(invoice__tax_number__icontains=filters["tax_number"])
if filters.get("status"):
qs = qs.filter(invoice__status=filters["status"])
if filters.get("ticket_type"):
qs = qs.filter(invoice__ticket_type=filters["ticket_type"])
if filters.get("invoice_type"):
qs = qs.filter(invoice__invoice_type=filters["invoice_type"])
# 时间区间筛选(凭证提交时间)
created_range = filters.get("created_at")
submitted_start = filters.get("submitted_start")
submitted_end = filters.get("submitted_end")
if created_range and isinstance(created_range, (list, tuple)) and len(created_range) == 2:
try:
# 前端可能传毫秒时间戳
start_ms = int(created_range[0])
end_ms = int(created_range[1])
from datetime import datetime
start_dt = datetime.fromtimestamp(start_ms / 1000)
end_dt = datetime.fromtimestamp(end_ms / 1000)
qs = qs.filter(created_at__gte=start_dt, created_at__lte=end_dt)
except Exception:
pass
else:
from datetime import datetime
def parse_time(v):
try:
iv = int(v)
return datetime.fromtimestamp(iv / 1000)
except Exception:
try:
# ISO 字符串
return datetime.fromisoformat(v)
except Exception:
return None
if submitted_start:
s_dt = parse_time(submitted_start)
if s_dt:
qs = qs.filter(created_at__gte=s_dt)
if submitted_end:
e_dt = parse_time(submitted_end)
if e_dt:
qs = qs.filter(created_at__lte=e_dt)
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
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": receipts,
"phone": inv.phone,
"wechat": inv.wechat,
"company_name": inv.company_name,
"tax_number": inv.tax_number,
"register_address": inv.register_address,
"register_phone": inv.register_phone,
"bank_name": inv.bank_name,
"bank_account": inv.bank_account,
"email": inv.email,
"ticket_type": inv.ticket_type,
"invoice_type": inv.invoice_type,
"status": inv.status,
})
return {"items": items, "total": total, "page": page, "page_size": page_size}
async def get_receipt_by_id(self, id_: int) -> Optional[dict]:
"""
对公转账记录详情
参数:
id_: 付款凭证ID
返回:
dict None
"""
r = await PaymentReceipt.filter(id=id_).first()
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": receipts,
"phone": inv.phone,
"wechat": inv.wechat,
"company_name": inv.company_name,
"tax_number": inv.tax_number,
"register_address": inv.register_address,
"register_phone": inv.register_phone,
"bank_name": inv.bank_name,
"bank_account": inv.bank_account,
"email": inv.email,
"ticket_type": inv.ticket_type,
"invoice_type": inv.invoice_type,
"status": inv.status,
}
async def get_out(self, id_: int) -> Optional[InvoiceOut]:
"""
根据ID返回发票输出对象
参数:
id_: 发票ID
返回:
InvoiceOut None
"""
inv = await self.model.filter(id=id_).first()
if not inv:
return None
return InvoiceOut(
id=inv.id,
created_at=inv.created_at.isoformat() if inv.created_at else "",
ticket_type=inv.ticket_type,
invoice_type=inv.invoice_type,
phone=inv.phone,
email=inv.email,
company_name=inv.company_name,
tax_number=inv.tax_number,
register_address=inv.register_address,
register_phone=inv.register_phone,
bank_name=inv.bank_name,
bank_account=inv.bank_account,
status=inv.status,
app_user_id=inv.app_user_id,
header_id=inv.header_id,
wechat=inv.wechat,
)
invoice_controller = InvoiceController()

View File

@ -2,7 +2,7 @@ import os
from pathlib import Path
from typing import List
from fastapi import UploadFile
from app.schemas.upload import ImageUploadResponse
from app.schemas.upload import ImageUploadResponse, FileUploadResponse
from app.settings.config import settings
class UploadController:
@ -15,8 +15,9 @@ class UploadController:
:param file: 上传的图片文件
:return: 图片URL和文件名
"""
# 检查文件类型
if not file.content_type.startswith('image/'):
ext = os.path.splitext(file.filename or "")[1].lower()
image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
if not (file.content_type.startswith('image/') or ext in image_exts):
raise ValueError("只支持上传图片文件")
# 获取项目根目录
@ -49,4 +50,80 @@ class UploadController:
return ImageUploadResponse(
url=f"{settings.BASE_URL}/static/images/{filename}",
filename=filename
)
)
@staticmethod
async def upload_file(file: UploadFile) -> FileUploadResponse:
allowed = {
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel",
"application/zip",
"application/x-zip-compressed",
"application/octet-stream",
"text/plain",
"text/csv",
"application/json",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/x-rar-compressed",
"application/x-7z-compressed",
}
allowed_exts = {
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".zip",
".rar",
".7z",
".txt",
".csv",
".ppt",
".pptx",
".json",
}
ext = os.path.splitext(file.filename or "")[1].lower()
if (file.content_type not in allowed) and (ext not in allowed_exts):
raise ValueError("不支持的文件类型")
base_dir = Path(__file__).resolve().parent.parent
upload_dir = base_dir / "static" / "files"
if not upload_dir.exists():
upload_dir.mkdir(parents=True, exist_ok=True)
filename = file.filename
file_path = upload_dir / filename
counter = 1
while file_path.exists():
name, ext = os.path.splitext(filename)
filename = f"{name}_{counter}{ext}"
file_path = upload_dir / filename
counter += 1
content = await file.read()
with open(file_path, "wb") as f:
f.write(content)
return FileUploadResponse(
url=f"{settings.BASE_URL}/static/files/{filename}",
filename=filename,
content_type=file.content_type,
)
@staticmethod
async def upload_any(file: UploadFile) -> FileUploadResponse:
"""
统一上传入口自动识别图片与非图片类型
返回统一结构url, filename, content_type
"""
image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
ext = os.path.splitext(file.filename or "")[1].lower()
if (file.content_type and file.content_type.startswith("image/")) or (ext in image_exts):
img = await UploadController.upload_image(file)
return FileUploadResponse(url=img.url, filename=img.filename, content_type=file.content_type or "image")
# 非图片类型复用原文件上传校验
return await UploadController.upload_file(file)

View File

@ -114,11 +114,7 @@ class UserValuationController:
async def _to_user_out(self, valuation: ValuationAssessment) -> UserValuationOut:
"""转换为用户端输出模型"""
return UserValuationOut.model_validate(valuation)
async def _to_user_detail(self, valuation: ValuationAssessment) -> UserValuationDetail:
"""转换为用户端详细模型"""
return UserValuationDetail(
return UserValuationOut(
id=valuation.id,
asset_name=valuation.asset_name,
institution=valuation.institution,
@ -131,13 +127,15 @@ 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,
historical_evidence=valuation.historical_evidence,
patent_certificates=valuation.patent_certificates,
pattern_images=valuation.pattern_images,
report_url=valuation.report_url,
certificate_url=valuation.certificate_url,
application_maturity=valuation.application_maturity,
implementation_stage=valuation.implementation_stage,
application_coverage=valuation.application_coverage,
@ -159,6 +157,78 @@ class UserValuationController:
price_fluctuation=valuation.price_fluctuation,
price_range=valuation.price_range,
market_price=valuation.market_price,
credit_code_or_id=valuation.credit_code_or_id,
biz_intro=valuation.biz_intro,
infringement_record=valuation.infringement_record,
patent_count=valuation.patent_count,
esg_value=valuation.esg_value,
policy_matching=valuation.policy_matching,
online_course_views=valuation.online_course_views,
pattern_complexity=valuation.pattern_complexity,
normalized_entropy=valuation.normalized_entropy,
legal_risk=valuation.legal_risk,
base_pledge_rate=valuation.base_pledge_rate,
flow_correction=valuation.flow_correction,
model_value_b=valuation.model_value_b,
market_value_c=valuation.market_value_c,
final_value_ab=valuation.final_value_ab,
dynamic_pledge_rate=valuation.dynamic_pledge_rate,
calculation_result=valuation.calculation_result,
calculation_input=valuation.calculation_input,
status=valuation.status,
admin_notes=valuation.admin_notes,
created_at=valuation.created_at,
updated_at=valuation.updated_at,
is_active=valuation.is_active,
)
async def _to_user_detail(self, valuation: ValuationAssessment) -> UserValuationDetail:
"""转换为用户端详细模型"""
return UserValuationDetail(
id=valuation.id,
asset_name=valuation.asset_name,
institution=valuation.institution,
industry=valuation.industry,
annual_revenue=valuation.annual_revenue,
rd_investment=valuation.rd_investment,
three_year_income=valuation.three_year_income,
funding_status=valuation.funding_status,
inheritor_level=valuation.inheritor_level,
inheritor_ages=valuation.inheritor_ages,
inheritor_age_count=valuation.inheritor_age_count,
inheritor_certificates=valuation.inheritor_certificates,
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,
historical_evidence=valuation.historical_evidence,
patent_certificates=valuation.patent_certificates,
pattern_images=valuation.pattern_images,
report_url=valuation.report_url,
certificate_url=valuation.certificate_url,
application_maturity=valuation.application_maturity,
implementation_stage=valuation.implementation_stage,
application_coverage=valuation.application_coverage,
coverage_area=valuation.coverage_area,
cooperation_depth=valuation.cooperation_depth,
collaboration_type=valuation.collaboration_type,
offline_activities=valuation.offline_activities,
offline_teaching_count=valuation.offline_teaching_count,
online_accounts=valuation.online_accounts,
platform_accounts=valuation.platform_accounts,
sales_volume=valuation.sales_volume,
link_views=valuation.link_views,
circulation=valuation.circulation,
scarcity_level=valuation.scarcity_level,
last_market_activity=valuation.last_market_activity,
market_activity_time=valuation.market_activity_time,
monthly_transaction=valuation.monthly_transaction,
monthly_transaction_amount=valuation.monthly_transaction_amount,
price_fluctuation=valuation.price_fluctuation,
price_range=valuation.price_range,
market_price=valuation.market_price,
credit_code_or_id=valuation.credit_code_or_id,
biz_intro=valuation.biz_intro,
infringement_record=valuation.infringement_record,
patent_count=valuation.patent_count,
esg_value=valuation.esg_value,
@ -184,4 +254,4 @@ class UserValuationController:
# 创建控制器实例
user_valuation_controller = UserValuationController()
user_valuation_controller = UserValuationController()

View File

@ -1,22 +1,662 @@
from typing import List, Optional
import json
from typing import Any, Dict, List, Optional
from tortoise.expressions import Q
from tortoise.queryset import QuerySet
from tortoise.functions import Count
from app.models.valuation import ValuationAssessment
from app.models.valuation import ValuationAssessment, ValuationCalculationStep
from app.schemas.valuation import (
ValuationAssessmentCreate,
ValuationAssessmentUpdate,
ValuationAssessmentQuery,
ValuationAssessmentOut,
ValuationAssessmentList
ValuationAssessmentList,
ValuationCalculationStepCreate,
ValuationCalculationStepOut
)
from app.models.user import AppUser
from app.utils.calculation_engine.formula_registry import get_formula_meta
class ValuationController:
"""估值评估控制器"""
model = ValuationAssessment
step_model = ValuationCalculationStep
# 参数说明映射表:将参数名(英文)映射到中文说明
PARAM_DESCRIPTIONS = {
# 财务价值相关
"three_year_income": "近三年收益(万元)",
"annual_revenue_3_years": "近三年收益(万元)",
"financial_value_f": "财务价值F",
# 法律强度相关
"patent_score": "专利分",
"popularity_score": "普及分",
"infringement_score": "侵权分",
"legal_strength_l": "法律强度L",
# 发展潜力相关
"patent_count": "专利数量",
"esg_score": "ESG分",
"innovation_ratio": "创新投入比",
"development_potential_d": "发展潜力D",
# 行业系数
"industry_coefficient": "行业系数I",
"target_industry_roe": "目标行业ROE",
"benchmark_industry_roe": "基准行业ROE",
# 流量因子相关
"search_index_s1": "搜索指数S1",
"industry_average_s2": "行业均值S2",
"social_media_spread_s3": "社交媒体传播度S3",
"likes": "点赞数",
"comments": "评论数",
"shares": "转发数",
"sales_volume": "销售量",
"link_views": "链接浏览量",
# 政策乘数相关
"implementation_stage": "实施阶段评分",
"funding_support": "资金支持度",
"policy_match_score": "政策匹配度",
# 文化价值相关
"inheritor_level_coefficient": "传承人等级系数",
"offline_sessions": "线下传习次数",
"douyin_views": "抖音浏览量",
"bilibili_views": "B站浏览量",
"kuaishou_views": "快手浏览量",
"cross_border_depth": "跨界合作深度",
"historical_inheritance": "历史传承度HI",
"structure_complexity": "结构复杂度SC",
"normalized_entropy": "归一化信息熵H",
# 风险调整相关
"highest_price": "最高价格",
"lowest_price": "最低价格",
"inheritor_ages": "传承人年龄列表",
"lawsuit_status": "诉讼状态",
# 市场估值相关
"manual_bids": "手动竞价列表",
"expert_valuations": "专家估值列表",
"weighted_average_price": "加权平均价格",
"daily_browse_volume": "日均浏览量",
"collection_count": "收藏数",
"issuance_level": "发行量",
"recent_market_activity": "最近市场活动时间",
# 动态质押率相关
"monthly_transaction_amount": "月交易额(万元)",
"monthly_amount": "月交易额(万元)",
"heritage_asset_level": "非遗等级",
"dynamic_pledge_rate": "动态质押率",
"base_pledge_rate": "基础质押率",
"flow_correction": "流量修正系数",
}
async def create_calculation_step(self, data: ValuationCalculationStepCreate) -> ValuationCalculationStepOut:
"""
创建估值计算步骤
Args:
data (ValuationCalculationStepCreate): 估值计算步骤数据
Returns:
ValuationCalculationStepOut: 创建的估值计算步骤
"""
step = await self.step_model.create(**data.model_dump())
logger.info(
"calcstep.create valuation_id={} order={} name={}",
data.valuation_id,
data.step_order,
data.step_name,
)
return ValuationCalculationStepOut.model_validate(step)
async def log_formula_step(
self,
valuation_id: int,
formula_code: str,
*,
status: str = "processing",
input_params: Optional[Dict[str, Any]] = None,
output_result: Optional[Dict[str, Any]] = None,
error_message: Optional[str] = None,
step_description: Optional[str] = None,
duration_ms: Optional[int] = None,
) -> ValuationCalculationStepOut:
"""
幂等记录或更新某个公式节点的计算过程
"""
meta = get_formula_meta(formula_code)
description = step_description or meta.formula
create_payload: Dict[str, Any] = {
"valuation_id": valuation_id,
"formula_code": meta.code,
"formula_name": meta.name,
"formula_text": meta.formula,
"parent_formula_code": meta.parent_code,
"group_code": meta.group_code,
"step_order": meta.order,
"step_name": meta.name,
"step_description": description,
"status": status,
}
if input_params is not None:
create_payload["input_params"] = input_params
if output_result is not None:
create_payload["output_result"] = output_result
if error_message is not None:
create_payload["error_message"] = error_message
# 准备更新字段
update_fields: Dict[str, Any] = {
"status": status,
"step_description": description,
"formula_name": meta.name,
"formula_text": meta.formula,
"parent_formula_code": meta.parent_code,
"group_code": meta.group_code,
"step_order": meta.order,
"step_name": meta.name,
}
if input_params is not None:
update_fields["input_params"] = input_params
if output_result is not None:
update_fields["output_result"] = output_result
if error_message is not None:
update_fields["error_message"] = error_message
if duration_ms is not None:
result = update_fields.get("output_result") or {}
if not isinstance(result, dict):
result = {}
result["duration_ms"] = duration_ms
update_fields["output_result"] = result
# 先尝试查询是否存在(明确排除 formula_code 为 NULL 的情况)
step = await self.step_model.filter(
valuation_id=valuation_id,
formula_code=meta.code
).first()
# 如果没找到,再检查是否有 formula_code 为 NULL 的旧记录(不应该有,但为了安全)
if not step and meta.code:
# 检查是否有重复的旧记录formula_code 为 NULL
old_steps = await self.step_model.filter(
valuation_id=valuation_id,
formula_code__isnull=True
).all()
if old_steps:
logger.warning(
"calcstep.log_formula found old records with NULL formula_code: valuation_id={} count={}",
valuation_id,
len(old_steps),
)
logger.info(
"calcstep.log_formula query: valuation_id={} formula_code={} found={}",
valuation_id,
meta.code,
step is not None,
)
if step:
# 更新现有记录
await step.update_from_dict(update_fields).save()
logger.info(
"calcstep.log_formula updated valuation_id={} formula_code={}",
valuation_id,
meta.code,
)
else:
# 尝试创建新记录
if duration_ms is not None:
result = create_payload.setdefault("output_result", {}) or {}
if not isinstance(result, dict):
result = {}
result["duration_ms"] = duration_ms
create_payload["output_result"] = result
try:
step = await self.step_model.create(**create_payload)
logger.info(
"calcstep.log_formula created valuation_id={} formula_code={}",
valuation_id,
meta.code,
)
except Exception as e:
# 如果因为唯一约束冲突而失败(可能是并发插入),重新查询并更新
error_str = str(e).lower()
if "duplicate" in error_str or "unique" in error_str or "1062" in error_str:
logger.warning(
"calcstep.log_formula duplicate key detected, retrying query: {}",
str(e),
)
# 重新查询(可能已被其他请求插入)
step = await self.step_model.filter(
valuation_id=valuation_id,
formula_code=meta.code
).first()
if step:
# 更新刚插入的记录
await step.update_from_dict(update_fields).save()
logger.info(
"calcstep.log_formula updated after duplicate key: valuation_id={} formula_code={}",
valuation_id,
meta.code,
)
else:
# 如果还是找不到,记录错误但继续
logger.error(
"calcstep.log_formula failed to find record after duplicate key error: valuation_id={} formula_code={}",
valuation_id,
meta.code,
)
raise
else:
# 其他错误直接抛出
raise
return ValuationCalculationStepOut.model_validate(step)
async def update_calculation_step(self, step_id: int, update: dict) -> ValuationCalculationStepOut:
step = await self.step_model.filter(id=step_id).first()
if not step:
raise ValueError(f"calculation_step not found: {step_id}")
await step.update_from_dict(update).save()
logger.info(
"calcstep.update id={} fields={}",
step_id,
list(update.keys()),
)
return ValuationCalculationStepOut.model_validate(step)
async def get_calculation_steps(self, valuation_id: int) -> List[ValuationCalculationStepOut]:
"""
根据估值ID获取所有相关的计算步骤
此方法从数据库中检索与特定估值ID关联的所有计算步骤记录
并按创建时间升序排序确保步骤的顺序正确
Args:
valuation_id (int): 估值的唯一标识符
Returns:
List[ValuationCalculationStepOut]: 一个包含所有相关计算步骤的列表
如果找不到任何步骤则返回空列表
"""
steps = await self.step_model.filter(valuation_id=valuation_id).order_by('created_at')
logger.info("calcstep.list valuation_id={} count={}", valuation_id, len(steps))
return [ValuationCalculationStepOut.model_validate(step) for step in steps]
async def get_calculation_report_markdown(self, valuation_id: int) -> str:
"""
根据估值ID生成计算过程的 Markdown 报告
此方法会查询所有相关的计算步骤按照公式顺序组织
并生成格式化的 Markdown 文档包含
- 公式名称
- 输入参数
- 公式文本
- 输出结果
Args:
valuation_id (int): 估值的唯一标识符
Returns:
str: Markdown 格式的计算报告
Raises:
ValueError: 如果找不到对应的估值记录
"""
# 验证估值记录是否存在
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
if not valuation:
raise ValueError(f"估值记录不存在: {valuation_id}")
# 获取所有计算步骤,按 step_order 排序
steps = await self.step_model.filter(valuation_id=valuation_id).order_by('step_order')
if not steps:
return f"# 计算摘要\n\n**估值ID**: {valuation_id}\n\n**资产名称**: {valuation.asset_name}\n\n> 暂无计算步骤记录。\n"
# 转换为字典列表,便于处理
steps_data = []
for step in steps:
step_dict = ValuationCalculationStepOut.model_validate(step).model_dump()
steps_data.append(step_dict)
# 生成 Markdown
markdown = self._generate_markdown(valuation, steps_data)
logger.info("calcstep.report_markdown generated valuation_id={} steps_count={}", valuation_id, len(steps_data))
return markdown
def _build_formula_tree(self, steps: List[Dict]) -> Dict:
"""
构建公式的树形结构
Args:
steps: 计算步骤列表
Returns:
Dict: 树形结构的字典key formula_codevalue 为步骤数据和子节点
"""
# 按 formula_code 索引
step_map = {}
for step in steps:
code = step.get('formula_code')
if code:
step_map[code] = step
# 构建树形结构
tree = {}
processed = set()
# 第一遍:创建所有节点
for step in steps:
code = step.get('formula_code')
if not code or code in processed:
continue
node = {
'step': step,
'children': []
}
tree[code] = node
processed.add(code)
# 第二遍:建立父子关系
root_nodes = []
for step in steps:
code = step.get('formula_code')
if not code:
continue
parent_code = step.get('parent_formula_code')
node = tree[code]
if parent_code and parent_code in tree:
# 有父节点,添加到父节点的 children
tree[parent_code]['children'].append(node)
else:
# 根节点
root_nodes.append(node)
# 按 step_order 排序
def sort_nodes(nodes):
nodes.sort(key=lambda n: float(n['step'].get('step_order', 0)))
for node in nodes:
if node['children']:
sort_nodes(node['children'])
sort_nodes(root_nodes)
return {'roots': root_nodes, 'all': tree}
def _generate_markdown(self, valuation, steps_data: List[Dict]) -> str:
"""
生成 Markdown 格式的报告
Args:
valuation: 估值评估对象
steps_data: 计算步骤列表已按 step_order 排序
Returns:
str: Markdown 格式的字符串
"""
lines = []
# 标题
lines.append("# 计算摘要")
lines.append("")
lines.append("")
# 遍历所有步骤,按顺序生成
for step in steps_data:
name = step.get('formula_name', step.get('step_name', '未知'))
formula_text = step.get('formula_text', step.get('step_description', ''))
input_params = step.get('input_params')
output_result = step.get('output_result')
# 公式标题(二级标题)
lines.append(f"## {name}")
lines.append("")
# 参数部分
if input_params:
lines.append("**参数:**")
lines.append("")
# 格式化参数显示
param_lines = self._format_params(input_params)
lines.extend(param_lines)
lines.append("")
# 公式部分
if formula_text:
lines.append("**公式:**")
lines.append("")
lines.append("```")
lines.append(formula_text)
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:
# 提取主要结果值
result_value = self._extract_main_result(output_result, name)
if result_value is not None:
lines.append("**结果:**")
lines.append("")
lines.append(f"`{result_value}`")
lines.append("")
lines.append("")
return "\n".join(lines)
def _format_params(self, params: Dict[str, Any]) -> List[str]:
"""
格式化参数显示优先使用列表格式如果是数组否则显示为列表项
参数名会附带中文说明如果存在
Args:
params: 参数字典
Returns:
List[str]: 格式化后的参数行列表
"""
lines = []
def _get_param_label(key: str) -> str:
"""获取参数标签,包含中文说明"""
description = self.PARAM_DESCRIPTIONS.get(key)
if description:
return f"{key}{description}"
return key
# 如果参数只有一个键,且值是数组,直接显示数组(不带参数名,符合示例格式)
if len(params) == 1:
key, value = next(iter(params.items()))
if isinstance(value, (list, tuple)):
# 格式化为列表:- [12.2, 13.2, 14.2]
value_str = json.dumps(list(value), ensure_ascii=False)
lines.append(f"- {value_str}")
return lines
# 多个参数或非数组,显示为列表项(带说明)
for key, value in params.items():
param_label = _get_param_label(key)
if isinstance(value, (list, tuple)):
value_str = json.dumps(list(value), ensure_ascii=False)
lines.append(f"- **{param_label}**: {value_str}")
elif isinstance(value, dict):
value_str = json.dumps(value, ensure_ascii=False)
lines.append(f"- **{param_label}**: {value_str}")
else:
lines.append(f"- **{param_label}**: {value}")
return lines
def _extract_main_result(self, output_result: Dict[str, Any], formula_name: str) -> Optional[str]:
"""
从输出结果中提取主要结果值
优先顺序
1. 如果结果中只有一个数值类型的值返回该值
2. 如果结果中包含与公式名称相关的字段 "财务价值 F" -> "financial_value_f"返回该值
3. 如果结果中包含常见的计算结果字段 "result", "value", "output"返回该值
4. 返回第一个数值类型的值
Args:
output_result: 输出结果字典
formula_name: 公式名称
Returns:
Optional[str]: 主要结果值的字符串表示如果找不到则返回 None
"""
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']}
if not filtered_result:
return None
# 如果只有一个值,直接返回
if len(filtered_result) == 1:
value = next(iter(filtered_result.values()))
if isinstance(value, (int, float)):
return str(value)
elif isinstance(value, (list, tuple)) and len(value) == 1:
return str(value[0])
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" 等
# 提取公式名称中的关键部分(通常是最后一个字母或单词)
name_parts = formula_name.split()
if name_parts:
# 获取最后一个部分(通常是字母,如 "F", "L", "D"
last_part = name_parts[-1].lower()
# 构建可能的字段名:如 "financial_value_f", "legal_strength_l" 等
# 将中文名称转换为可能的英文字段名模式
possible_keys = []
# 1. 直接匹配包含最后部分的字段(如包含 "f", "l", "d"
for key in filtered_result.keys():
if last_part in key.lower() or key.lower().endswith(f"_{last_part}"):
possible_keys.append(key)
# 2. 尝试匹配常见的命名模式
# 例如:"财务价值 F" -> "financial_value_f"
# 这里我们尝试匹配以最后部分结尾的字段
suffix_patterns = [
f"_{last_part}",
f"_{last_part}_",
last_part,
]
for key in filtered_result.keys():
key_lower = key.lower()
for pattern in suffix_patterns:
if key_lower.endswith(pattern) or pattern in key_lower:
if key not in possible_keys:
possible_keys.append(key)
# 按优先级匹配
for key in possible_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)):
return str(value)
# 如果都没有,返回整个结果的 JSON但简化显示
return json.dumps(filtered_result, ensure_ascii=False)
async def create(self, data: ValuationAssessmentCreate, user_id: int) -> ValuationAssessmentOut:
"""创建估值评估"""
@ -24,13 +664,15 @@ class ValuationController:
create_data = data.model_dump()
create_data['user_id'] = user_id
valuation = await self.model.create(**create_data)
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def get_by_id(self, valuation_id: int) -> Optional[ValuationAssessmentOut]:
"""根据ID获取估值评估"""
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
if valuation:
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
return None
async def update(self, valuation_id: int, data: ValuationAssessmentUpdate) -> Optional[ValuationAssessmentOut]:
@ -41,11 +683,52 @@ class ValuationController:
update_data = data.model_dump(exclude_unset=True)
if update_data:
if 'certificate_url' in update_data and update_data.get('certificate_url'):
from datetime import datetime
update_data['audited_at'] = datetime.now()
update_data['updated_at'] = datetime.now()
else:
from datetime import datetime
update_data['updated_at'] = datetime.now()
await valuation.update_from_dict(update_data)
await valuation.save()
from datetime import datetime
valuation.status ="pending"
if not getattr(valuation, "audited_at", None):
valuation.audited_at = datetime.now()
valuation.updated_at = datetime.now()
await valuation.save()
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def update1(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)
if update_data:
if 'certificate_url' in update_data and update_data.get('certificate_url'):
from datetime import datetime
update_data['audited_at'] = datetime.now()
update_data['updated_at'] = datetime.now()
else:
from datetime import datetime
update_data['updated_at'] = datetime.now()
await valuation.update_from_dict(update_data)
await valuation.save()
from datetime import datetime
valuation.status ="success"
if not getattr(valuation, "audited_at", None):
valuation.audited_at = datetime.now()
valuation.updated_at = datetime.now()
await valuation.save()
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def delete(self, valuation_id: int) -> bool:
"""软删除估值评估"""
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
@ -69,6 +752,7 @@ class ValuationController:
# 转换为输出模型
items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
items = await self._attach_user_phone_bulk(items)
# 计算总页数
pages = (total + query.size - 1) // query.size
@ -102,7 +786,38 @@ class ValuationController:
# 添加状态筛选
if hasattr(query, 'status') and query.status:
queryset = queryset.filter(status=query.status)
if getattr(query, 'phone', None):
queryset = queryset.filter(user__phone__icontains=query.phone)
def _parse_time(v: Optional[str]):
if not v:
return None
try:
iv = int(v)
from datetime import datetime
return datetime.fromtimestamp(iv / 1000)
except Exception:
try:
from datetime import datetime
return datetime.fromisoformat(v)
except Exception:
return None
s_dt = _parse_time(getattr(query, 'submitted_start', None))
e_dt = _parse_time(getattr(query, 'submitted_end', None))
if s_dt:
queryset = queryset.filter(created_at__gte=s_dt)
if e_dt:
queryset = queryset.filter(created_at__lte=e_dt)
a_s_dt = _parse_time(getattr(query, 'audited_start', None))
a_e_dt = _parse_time(getattr(query, 'audited_end', None))
if a_s_dt:
queryset = queryset.filter(updated_at__gte=a_s_dt)
if a_e_dt:
queryset = queryset.filter(updated_at__lte=a_e_dt)
return queryset
async def get_statistics(self) -> dict:
@ -143,6 +858,7 @@ class ValuationController:
# 转换为输出模型
items = [ValuationAssessmentOut.model_validate(v) for v in valuations]
items = await self._attach_user_phone_bulk(items)
# 计算总页数
pages = (total + size - 1) // size
@ -161,12 +877,14 @@ class ValuationController:
if not valuation:
return None
update_data = {"status": "approved"}
from datetime import datetime
update_data = {"status": "success", "audited_at": datetime.now(), "updated_at": datetime.now()}
if admin_notes:
update_data["admin_notes"] = admin_notes
await valuation.update_from_dict(update_data).save()
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def reject_valuation(self, valuation_id: int, admin_notes: Optional[str] = None) -> Optional[ValuationAssessmentOut]:
"""审核拒绝估值评估"""
@ -174,22 +892,54 @@ class ValuationController:
if not valuation:
return None
update_data = {"status": "rejected"}
from datetime import datetime
update_data = {"status": "rejected", "audited_at": datetime.now(), "updated_at": datetime.now()}
if admin_notes:
update_data["admin_notes"] = admin_notes
await valuation.update_from_dict(update_data).save()
return ValuationAssessmentOut.model_validate(valuation)
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)
async def update_admin_notes(self, valuation_id: int, admin_notes: str) -> Optional[ValuationAssessmentOut]:
"""更新管理员备注"""
valuation = await self.model.filter(id=valuation_id, is_active=True).first()
if not valuation:
return None
await valuation.update_from_dict({"admin_notes": admin_notes}).save()
return ValuationAssessmentOut.model_validate(valuation)
from datetime import datetime
await valuation.update_from_dict({"admin_notes": admin_notes, "updated_at": datetime.now()}).save()
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
return out
async def _attach_user_phone_bulk(self, items: List[ValuationAssessmentOut]) -> List[ValuationAssessmentOut]:
ids = list({item.user_id for item in items if item.user_id})
if not ids:
return items
users = await AppUser.filter(id__in=ids).values("id", "phone")
phone_map = {u["id"]: u["phone"] for u in users}
for item in items:
item.user_phone = phone_map.get(item.user_id)
return items
# 创建控制器实例
valuation_controller = ValuationController()
valuation_controller = ValuationController()
from app.log import logger

View File

@ -16,6 +16,7 @@ async def DoesNotExistHandle(req: Request, exc: DoesNotExist) -> JSONResponse:
content = dict(
code=404,
msg=f"Object has not found, exc: {exc}, query_params: {req.query_params}",
data={},
)
return JSONResponse(content=content, status_code=404)
@ -24,20 +25,21 @@ async def IntegrityHandle(_: Request, exc: IntegrityError) -> JSONResponse:
content = dict(
code=500,
msg=f"IntegrityError{exc}",
data={},
)
return JSONResponse(content=content, status_code=500)
async def HttpExcHandle(_: Request, exc: HTTPException) -> JSONResponse:
content = dict(code=exc.status_code, msg=exc.detail, data=None)
content = dict(code=exc.status_code, msg=exc.detail, data={})
return JSONResponse(content=content, status_code=exc.status_code)
async def RequestValidationHandle(_: Request, exc: RequestValidationError) -> JSONResponse:
content = dict(code=422, msg=f"RequestValidationError, {exc}")
content = dict(code=422, msg=f"RequestValidationError, {exc}", data={})
return JSONResponse(content=content, status_code=422)
async def ResponseValidationHandle(_: Request, exc: ResponseValidationError) -> JSONResponse:
content = dict(code=500, msg=f"ResponseValidationError, {exc}")
content = dict(code=500, msg=f"ResponseValidationError, {exc}", data={})
return JSONResponse(content=content, status_code=500)

View File

@ -23,6 +23,7 @@ from app.core.exceptions import (
)
from app.log import logger
from app.models.admin import Api, Menu, Role
from app.models.invoice import Invoice, PaymentReceipt
from app.schemas.menus import MenuType
from app.settings.config import settings
@ -237,14 +238,158 @@ async def init_menus():
redirect="",
)
# 创建交易管理菜单
transaction_menu = await Menu.create(
menu_type=MenuType.CATALOG,
name="交易管理",
path="/transaction",
order=3,
parent_id=0,
icon="carbon:wallet",
is_hidden=False,
component="Layout",
keepalive=False,
redirect="/transaction/invoice",
)
transaction_children = [
Menu(
menu_type=MenuType.MENU,
name="发票管理",
path="invoice",
order=1,
parent_id=transaction_menu.id,
icon="mdi:file-document-outline",
is_hidden=False,
component="/transaction/invoice",
keepalive=False,
),
Menu(
menu_type=MenuType.MENU,
name="交易记录",
path="receipts",
order=2,
parent_id=transaction_menu.id,
icon="mdi:receipt-text-outline",
is_hidden=False,
component="/transaction/receipts",
keepalive=False,
),
]
await Menu.bulk_create(transaction_children)
async def init_apis():
apis = await api_controller.model.exists()
if not apis:
await api_controller.refresh_api()
await api_controller.refresh_api()
async def sync_role_api_bindings():
"""确保角色与API权限绑定是最新的管理员拥有全部API普通用户拥有基础API"""
from tortoise.expressions import Q
try:
admin_role = await Role.filter(name="管理员").first()
if admin_role:
all_apis = await Api.all()
current = await admin_role.apis.all()
current_keys = {(a.method, a.path) for a in current}
missing = [a for a in all_apis if (a.method, a.path) not in current_keys]
if missing:
await admin_role.apis.add(*missing)
user_role = await Role.filter(name="普通用户").first()
if user_role:
basic_apis = await Api.filter(Q(method__in=["GET"]) | Q(tags="基础模块"))
current_u = await user_role.apis.all()
current_u_keys = {(a.method, a.path) for a in current_u}
missing_u = [a for a in basic_apis if (a.method, a.path) not in current_u_keys]
if missing_u:
await user_role.apis.add(*missing_u)
except Exception:
pass
async def _ensure_unique_index():
"""确保 valuation_calculation_steps 表的唯一索引存在"""
try:
conn_alias = settings.TORTOISE_ORM["apps"]["models"]["default_connection"]
from tortoise import connections
conn = connections.get(conn_alias)
# 检查表是否存在
result = await conn.execute_query(
"SHOW TABLES LIKE 'valuation_calculation_steps'"
)
if not result or len(result[1]) == 0:
logger.info("Table valuation_calculation_steps does not exist, skipping index check")
return
# 检查唯一索引是否存在
# 查找包含 valuation_id 和 formula_code 的唯一索引
index_result = await conn.execute_query(
"SHOW INDEX FROM `valuation_calculation_steps` WHERE Non_unique = 0 AND Column_name IN ('valuation_id', 'formula_code')"
)
# 查找是否存在 (valuation_id, formula_code) 的唯一索引
# 对于复合索引SHOW INDEX 会返回多行,每行对应一个列
# 需要检查是否有同一个 Key_name 包含两个列
has_unique_index = False
if index_result and len(index_result) > 1:
# 按 Key_name 分组
index_groups = {}
for row in index_result[1]:
if len(row) >= 5:
key_name = row[2] if len(row) > 2 else ""
non_unique = row[1] if len(row) > 1 else 1
column_name = row[4] if len(row) > 4 else ""
seq_in_index = row[3] if len(row) > 3 else 0
if non_unique == 0 and column_name in ('valuation_id', 'formula_code'):
if key_name not in index_groups:
index_groups[key_name] = []
index_groups[key_name].append(column_name)
# 检查是否有索引包含两个列
for key_name, columns in index_groups.items():
if 'valuation_id' in columns and 'formula_code' in columns:
has_unique_index = True
logger.debug(f"Found unique index: {key_name} on (valuation_id, formula_code)")
break
if not has_unique_index:
logger.warning("Unique index on (valuation_id, formula_code) not found, attempting to create...")
try:
# 先删除可能存在的重复记录
await conn.execute_query("""
DELETE t1 FROM `valuation_calculation_steps` t1
INNER JOIN `valuation_calculation_steps` t2
WHERE t1.id > t2.id
AND t1.valuation_id = t2.valuation_id
AND t1.formula_code = t2.formula_code
AND t1.formula_code IS NOT NULL
""")
logger.info("Cleaned up duplicate records")
# 创建唯一索引
await conn.execute_query("""
CREATE UNIQUE INDEX `uidx_valuation_formula`
ON `valuation_calculation_steps` (`valuation_id`, `formula_code`)
""")
logger.info("Created unique index on (valuation_id, formula_code)")
except Exception as idx_err:
error_str = str(idx_err).lower()
if "duplicate key name" in error_str or "already exists" in error_str:
logger.info("Unique index already exists (different name)")
else:
logger.warning(f"Failed to create unique index: {idx_err}")
else:
logger.debug("Unique index on (valuation_id, formula_code) already exists")
except Exception as e:
logger.warning(f"Failed to ensure unique index: {e}")
async def init_db():
import os
from pathlib import Path
from tortoise import Tortoise
from tortoise.exceptions import OperationalError
command = Command(tortoise_config=settings.TORTOISE_ORM)
try:
await command.init_db(safe=True)
@ -252,14 +397,84 @@ async def init_db():
pass
await command.init()
# 检查并清理可能冲突的迁移文件(避免交互式提示)
# Aerich 在检测到迁移文件已存在时会交互式提示,我们提前删除冲突文件
migrations_dir = Path("migrations/models")
if migrations_dir.exists():
# 查找包含 "update" 的迁移文件(通常是自动生成的冲突文件)
for migration_file in migrations_dir.glob("*update*.py"):
if migration_file.name != "__init__.py":
logger.info(f"Removing conflicting migration file: {migration_file.name}")
migration_file.unlink()
# 尝试执行 migrate
try:
await command.migrate()
except AttributeError:
logger.warning("unable to retrieve model history from database, model history will be created from scratch")
shutil.rmtree("migrations")
await command.init_db(safe=True)
except Exception as e:
# 如果 migrate 失败,记录警告但继续执行 upgrade
logger.warning(f"Migrate failed: {e}, continuing with upgrade...")
await command.upgrade(run_in_transaction=True)
# 在 upgrade 之前,先检查表是否存在,如果不存在则先创建表
try:
await command.upgrade(run_in_transaction=True)
# upgrade 成功后,验证并修复唯一索引
await _ensure_unique_index()
except (OperationalError, Exception) as e:
error_msg = str(e)
# 如果是因为表不存在而失败,先让 Tortoise 生成表结构
if "doesn't exist" in error_msg.lower() or ("table" in error_msg.lower() and "valuation_calculation_steps" in error_msg):
logger.warning(f"Table not found during upgrade: {error_msg}, generating schemas first...")
# 确保 Tortoise 已初始化Aerich 的 init 应该已经初始化了,但为了安全再检查)
try:
# 生成表结构safe=True 表示如果表已存在则跳过)
await Tortoise.generate_schemas(safe=True)
logger.info("Tables generated successfully, retrying upgrade...")
# 重新尝试 upgrade这次应该会成功因为表已经存在
try:
await command.upgrade(run_in_transaction=True)
except Exception as upgrade_err:
# 如果 upgrade 仍然失败,可能是迁移文件的问题,记录警告但继续
logger.warning(f"Upgrade still failed after generating schemas: {upgrade_err}, continuing anyway...")
except Exception as gen_err:
logger.error(f"Failed to generate schemas: {gen_err}")
raise
# 如果是重复字段错误,说明迁移已经执行过,直接跳过并确保索引
elif "duplicate column name" in error_msg.lower():
logger.warning(f"Duplicate column detected during upgrade: {error_msg}, skipping migration step and ensuring schema integrity...")
await _ensure_unique_index()
# 如果是重复索引错误,删除表并重新创建(最简单可靠的方法)
elif "duplicate key" in error_msg.lower() or "duplicate key name" in error_msg.lower():
logger.warning(f"Duplicate index detected: {error_msg}, dropping and recreating table...")
try:
# Aerich 的 command.init() 已经初始化了 Tortoise直接使用连接
# 连接别名是 "mysql"(从配置中读取)
conn_alias = settings.TORTOISE_ORM["apps"]["models"]["default_connection"]
from tortoise import connections
# 尝试获取连接,如果失败则重新初始化
try:
conn = connections.get(conn_alias)
except Exception:
# 如果连接不存在,重新初始化 Tortoise
await Tortoise.init(config=settings.TORTOISE_ORM)
conn = connections.get(conn_alias)
# 删除表
await conn.execute_query("DROP TABLE IF EXISTS `valuation_calculation_steps`")
logger.info("Dropped valuation_calculation_steps table")
# 重新生成表结构(包含正确的唯一索引)
# 使用 safe=True 避免尝试创建已存在的其他表(如 user_role只创建不存在的表
await Tortoise.generate_schemas(safe=True)
logger.info("Table regenerated successfully with correct unique index")
except Exception as recreate_err:
logger.error(f"Failed to recreate table: {recreate_err}")
raise
else:
raise
async def init_roles():
@ -287,9 +502,124 @@ async def init_roles():
await user_role.apis.add(*basic_apis)
async def init_demo_transactions():
"""
创建开发环境演示用的发票与交易记录付款凭证数据
功能:
- 在无现有付款凭证数据时批量生成若干 `Invoice` 与关联的 `PaymentReceipt`
- 仅在调试模式下执行避免污染生产环境
参数:
返回: `None`异步执行插入操作
"""
if not settings.DEBUG:
return
has_receipt = await PaymentReceipt.exists()
if has_receipt:
return
demo_invoices = []
demo_payloads = [
{
"ticket_type": "electronic",
"invoice_type": "normal",
"phone": "13800000001",
"email": "demo1@example.com",
"company_name": "演示科技有限公司",
"tax_number": "91310000MA1DEMO01",
"register_address": "上海市浦东新区演示路 100 号",
"register_phone": "021-88880001",
"bank_name": "招商银行上海分行",
"bank_account": "6214830000000001",
"status": "pending",
"wechat": "demo_wechat_01",
},
{
"ticket_type": "paper",
"invoice_type": "special",
"phone": "13800000002",
"email": "demo2@example.com",
"company_name": "示例信息技术股份有限公司",
"tax_number": "91310000MA1DEMO02",
"register_address": "北京市海淀区知春路 66 号",
"register_phone": "010-66660002",
"bank_name": "中国银行北京分行",
"bank_account": "6216610000000002",
"status": "invoiced",
"wechat": "demo_wechat_02",
},
{
"ticket_type": "electronic",
"invoice_type": "special",
"phone": "13800000003",
"email": "demo3@example.com",
"company_name": "华夏制造有限公司",
"tax_number": "91310000MA1DEMO03",
"register_address": "广州市天河区高新大道 8 号",
"register_phone": "020-77770003",
"bank_name": "建设银行广州分行",
"bank_account": "6227000000000003",
"status": "rejected",
"wechat": "demo_wechat_03",
},
{
"ticket_type": "paper",
"invoice_type": "normal",
"phone": "13800000004",
"email": "demo4@example.com",
"company_name": "泰岳网络科技有限公司",
"tax_number": "91310000MA1DEMO04",
"register_address": "杭州市滨江区科技大道 1 号",
"register_phone": "0571-55550004",
"bank_name": "农业银行杭州分行",
"bank_account": "6228480000000004",
"status": "refunded",
"wechat": "demo_wechat_04",
},
{
"ticket_type": "electronic",
"invoice_type": "normal",
"phone": "13800000005",
"email": "demo5@example.com",
"company_name": "星云数据有限公司",
"tax_number": "91310000MA1DEMO05",
"register_address": "成都市高新区软件园 9 号楼",
"register_phone": "028-33330005",
"bank_name": "工商银行成都分行",
"bank_account": "6222020000000005",
"status": "pending",
"wechat": "demo_wechat_05",
},
]
for payload in demo_payloads:
inv = await Invoice.create(**payload)
demo_invoices.append(inv)
for idx, inv in enumerate(demo_invoices, start=1):
await PaymentReceipt.create(
invoice=inv,
url=f"https://example.com/demo-receipt-{idx}-a.png",
note="DEMO 凭证 A",
verified=(inv.status == "invoiced"),
)
if idx % 2 == 0:
await PaymentReceipt.create(
invoice=inv,
url=f"https://example.com/demo-receipt-{idx}-b.png",
note="DEMO 凭证 B",
verified=False,
)
async def init_data():
await init_db()
await init_superuser()
await init_menus()
await init_apis()
await init_roles()
await sync_role_api_bindings()
await init_demo_transactions()

View File

@ -149,7 +149,8 @@ class HttpAuditLogMiddleware(BaseHTTPMiddleware):
try:
return json.loads(stripped)
except (ValueError, TypeError):
return stripped
# 将非 JSON 字符串包装为字典,以便 JSONField 能够正确存储
return {"text": stripped}
if isinstance(value, (dict, list, int, float, bool)):
return value

View 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()

View File

@ -5,4 +5,6 @@ from .index import *
from .industry import *
from .policy import *
from .user import *
from .valuation import *
from .valuation import *
from .invoice import *
from .token_blacklist import *

66
app/models/invoice.py Normal file
View File

@ -0,0 +1,66 @@
from tortoise import fields
from .base import BaseModel, TimestampMixin
class InvoiceHeader(BaseModel, TimestampMixin):
app_user_id = fields.IntField(null=True, description="App用户ID", index=True)
company_name = fields.CharField(max_length=128, description="公司名称", index=True)
tax_number = fields.CharField(max_length=32, description="公司税号", index=True)
register_address = fields.CharField(max_length=256, description="注册地址")
register_phone = fields.CharField(max_length=32, description="注册电话")
bank_name = fields.CharField(max_length=128, description="开户银行")
bank_account = fields.CharField(max_length=64, description="银行账号")
email = fields.CharField(max_length=128, description="接收邮箱")
is_default = fields.BooleanField(default=False, description="是否默认抬头", index=True)
class Meta:
table = "invoice_header"
table_description = "发票抬头"
class Invoice(BaseModel, TimestampMixin):
ticket_type = fields.CharField(max_length=16, description="票据类型: electronic|paper", index=True)
invoice_type = fields.CharField(max_length=16, description="发票类型: special|normal", index=True)
phone = fields.CharField(max_length=20, description="手机号", index=True)
email = fields.CharField(max_length=128, description="接收邮箱")
company_name = fields.CharField(max_length=128, description="公司名称", index=True)
tax_number = fields.CharField(max_length=32, description="公司税号", index=True)
register_address = fields.CharField(max_length=256, description="注册地址")
register_phone = fields.CharField(max_length=32, description="注册电话")
bank_name = fields.CharField(max_length=128, description="开户银行")
bank_account = fields.CharField(max_length=64, description="银行账号")
status = fields.CharField(max_length=16, description="状态: pending|invoiced|rejected|refunded", index=True, default="pending")
app_user_id = fields.IntField(null=True, description="App用户ID", index=True)
header = fields.ForeignKeyField("models.InvoiceHeader", related_name="invoices", null=True, description="抬头关联")
wechat = fields.CharField(max_length=64, null=True, description="微信号", index=True)
class Meta:
table = "invoice"
table_description = "发票记录"
class PaymentReceipt(BaseModel, TimestampMixin):
invoice = fields.ForeignKeyField("models.Invoice", related_name="receipts", description="关联发票")
url = fields.CharField(max_length=512, description="付款凭证图片地址")
note = fields.CharField(max_length=256, null=True, description="备注")
verified = fields.BooleanField(default=False, description="是否已核验")
extra = fields.JSONField(null=True, description="额外信息:邮件发送相关")
class Meta:
table = "payment_receipt"
table_description = "对公转账付款凭证"
class EmailSendLog(BaseModel, TimestampMixin):
email = fields.CharField(max_length=255, description="目标邮箱", index=True)
subject = fields.CharField(max_length=255, null=True, description="主题")
body_summary = fields.CharField(max_length=512, null=True, description="正文摘要")
file_name = fields.CharField(max_length=255, null=True, description="附件文件名")
file_url = fields.CharField(max_length=512, null=True, description="附件URL")
status = fields.CharField(max_length=16, description="状态: OK|FAIL", index=True)
error = fields.TextField(null=True, description="错误信息")
class Meta:
table = "email_send_log"
table_description = "邮件发送日志"

View 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令牌黑名单"

View File

@ -19,7 +19,25 @@ class AppUser(BaseModel, TimestampMixin):
company_email = fields.CharField(max_length=100, null=True, description="公司邮箱")
is_active = fields.BooleanField(default=True, description="是否激活", index=True)
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"
table_description = "用户表"
table_description = "用户表"
class AppUserQuotaLog(BaseModel, TimestampMixin):
app_user_id = fields.IntField(description="App用户ID", index=True)
operator_id = fields.IntField(description="操作人ID", index=True)
operator_name = fields.CharField(max_length=64, description="操作人")
before_count = fields.IntField(description="变更前次数")
after_count = fields.IntField(description="变更后次数")
op_type = fields.CharField(max_length=32, description="操作类型")
remark = fields.CharField(max_length=256, null=True, description="备注")
class Meta:
table = "app_user_quota_log"
table_description = "App用户估值次数操作日志"

View File

@ -23,18 +23,26 @@ class ValuationAssessment(Model):
inheritor_ages = fields.JSONField(null=True, description="传承人年龄列表")
inheritor_age_count = fields.JSONField(null=True, description="非遗传承人年龄水平及数量")
inheritor_certificates = fields.JSONField(null=True, description="非遗传承人等级证书")
heritage_level = fields.CharField(max_length=50, null=True, description="非遗等级")
heritage_asset_level = fields.CharField(max_length=50, null=True, description="非遗资产等级")
patent_application_no = fields.CharField(max_length=100, null=True, description="非遗资产所用专利的申请号")
patent_remaining_years = fields.CharField(max_length=50, null=True, description="专利剩余年限")
historical_evidence = fields.JSONField(null=True, description="非遗资产历史证明证据及数量")
patent_certificates = fields.JSONField(null=True, description="非遗资产所用专利的证书")
pattern_images = fields.JSONField(null=True, description="非遗纹样图片")
report_url = fields.CharField(max_length=512, null=True, description="管理员上传的评估报告URL")
certificate_url = fields.CharField(max_length=512, null=True, description="管理员上传的证书URL")
# 非遗应用与推广
implementation_stage = fields.CharField(max_length=100, null=True, description="非遗资产应用成熟度")
application_maturity = fields.CharField(max_length=100, null=True, description="非遗资产应用成熟度")
application_coverage = fields.CharField(max_length=100, null=True, description="非遗资产应用覆盖范围")
coverage_area = fields.CharField(max_length=100, null=True, description="应用覆盖范围")
cooperation_depth = fields.CharField(max_length=100, null=True, description="非遗资产跨界合作深度")
collaboration_type = fields.CharField(max_length=100, null=True, description="跨界合作类型")
offline_activities = fields.CharField(max_length=50, null=True, description="近12个月线下相关宣讲活动次数")
offline_teaching_count = fields.IntField(null=True, description="近12个月线下相关演讲活动次数")
online_accounts = fields.JSONField(null=True, description="线上相关宣传账号信息")
platform_accounts = fields.JSONField(null=True, description="线上相关宣传账号信息")
# 非遗资产衍生商品信息
@ -44,10 +52,13 @@ class ValuationAssessment(Model):
scarcity_level = fields.CharField(max_length=50, null=True, description="稀缺等级")
last_market_activity = fields.CharField(max_length=100, null=True, description="该商品最近一次市场活动时间")
market_activity_time = fields.CharField(max_length=100, null=True, description="市场活动的时间")
monthly_transaction = fields.CharField(max_length=50, null=True, description="月交易额")
monthly_transaction_amount = fields.CharField(max_length=50, null=True, description="月交易额")
price_fluctuation = fields.JSONField(null=True, description="该商品近30天价格波动区间")
price_range = fields.JSONField(null=True, description="资产商品的价格波动率")
market_price = fields.FloatField(null=True, description="市场价格(单位:万元)")
credit_code_or_id = fields.CharField(max_length=64, null=True, description="统一社会信用代码或身份证号")
biz_intro = fields.TextField(null=True, description="业务/传承介绍")
# 内置API计算字段
infringement_record = fields.CharField(max_length=100, null=True, description="侵权记录")
@ -71,10 +82,11 @@ class ValuationAssessment(Model):
# 系统字段
user = fields.ForeignKeyField("models.AppUser", related_name="valuations", description="提交用户")
status = fields.CharField(max_length=20, default="success", description="评估状态: pending(待审核), success(已通过), fail(已拒绝)")
status = fields.CharField(max_length=20, default="pending", description="评估状态: pending(待审核), success(已通过), rejected(已拒绝)")
admin_notes = fields.TextField(null=True, description="管理员备注")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
updated_at = fields.DatetimeField(null=True, description="更新时间")
audited_at = fields.DatetimeField(null=True, description="审核时间")
is_active = fields.BooleanField(default=True, description="是否激活")
class Meta:
@ -82,4 +94,35 @@ class ValuationAssessment(Model):
table_description = "估值评估表"
def __str__(self):
return f"估值评估-{self.asset_name}"
return f"估值评估-{self.asset_name}"
class ValuationCalculationStep(Model):
"""估值计算步骤模型"""
id = fields.IntField(pk=True, description="主键ID")
valuation = fields.ForeignKeyField("models.ValuationAssessment", related_name="calculation_steps", description="关联的估值评估")
formula_code = fields.CharField(max_length=64, null=True, description="公式编码")
formula_name = fields.CharField(max_length=255, null=True, description="公式名称")
formula_text = fields.TextField(null=True, description="公式说明")
parent_formula_code = fields.CharField(max_length=64, null=True, description="父级公式编码")
group_code = fields.CharField(max_length=64, null=True, description="分组编码")
step_order = fields.DecimalField(max_digits=8, decimal_places=3, description="步骤顺序")
step_name = fields.CharField(max_length=255, description="步骤名称")
step_description = fields.TextField(null=True, description="步骤描述")
input_params = fields.JSONField(null=True, description="输入参数")
output_result = fields.JSONField(null=True, description="输出结果")
status = fields.CharField(max_length=20, default="processing", description="步骤状态: processing, completed, failed")
error_message = fields.TextField(null=True, description="错误信息")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(null=True, description="更新时间")
class Meta:
table = "valuation_calculation_steps"
table_description = "估值计算步骤表"
ordering = ["step_order"]
# 唯一索引同一估值ID下同一公式编码只能有一条记录
# 注意formula_code 允许为 NULL但新逻辑中 formula_code 总是有值
unique_together = [("valuation", "formula_code")]
def __str__(self):
return f"估值ID {self.valuation_id} - 步骤 {self.step_order}: {self.step_name}"

View File

@ -50,6 +50,7 @@ class AppUserInfoOut(BaseModel):
last_login: Optional[datetime] = None
created_at: datetime
updated_at: datetime
remaining_quota: int
class AppUserUpdateSchema(BaseModel):
@ -61,9 +62,56 @@ class AppUserUpdateSchema(BaseModel):
company_contact: Optional[str] = Field(None, description="公司联系人")
company_phone: Optional[str] = Field(None, description="公司电话")
company_email: Optional[str] = Field(None, description="公司邮箱")
notes: Optional[str] = Field(None, description="备注")
class AppUserChangePasswordSchema(BaseModel):
"""AppUser修改密码Schema"""
old_password: str = Field(..., description="原密码")
new_password: str = Field(..., description="新密码")
new_password: str = Field(..., description="新密码")
class AppUserDashboardOut(BaseModel):
"""AppUser首页摘要输出"""
remaining_quota: int
latest_valuation: Optional[dict] = None
pending_invoices: int
class AppUserQuotaOut(BaseModel):
"""AppUser剩余估值次数输出"""
remaining_count: int
user_type: Optional[str] = None
class AppUserQuotaUpdateSchema(BaseModel):
user_id: int = Field(..., description="用户ID")
target_count: Optional[int] = Field(None, description="目标次数")
delta: Optional[int] = Field(None, description="增减次数")
op_type: str = Field(..., description="操作类型")
remark: Optional[str] = Field(None, description="备注")
class AppUserQuotaLogOut(BaseModel):
id: int
app_user_id: int
operator_id: int
operator_name: str
before_count: int
after_count: int
op_type: str
remark: Optional[str] = None
created_at: str
class AppUserRegisterOut(BaseModel):
"""App 用户注册结果"""
user_id: int = Field(..., description="用户ID")
phone: str = Field(..., description="手机号")
default_password: str = Field(..., description="默认密码(手机号后六位)")
class TokenValidateOut(BaseModel):
"""Token 校验结果"""
user_id: int = Field(..., description="用户ID")
phone: str = Field(..., description="手机号")

View File

@ -1,6 +1,9 @@
from typing import Any, Optional
from typing import Any, Optional, Generic, TypeVar, List
from pydantic import BaseModel, Field
from pydantic.generics import GenericModel
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
class Success(JSONResponse):
@ -11,9 +14,9 @@ class Success(JSONResponse):
data: Optional[Any] = None,
**kwargs,
):
content = {"code": code, "msg": msg, "data": data}
content = {"code": code, "msg": msg, "data": ({} if data is None else data)}
content.update(kwargs)
super().__init__(content=content, status_code=code)
super().__init__(content=jsonable_encoder(content), status_code=code)
class Fail(JSONResponse):
@ -24,9 +27,9 @@ class Fail(JSONResponse):
data: Optional[Any] = None,
**kwargs,
):
content = {"code": code, "msg": msg, "data": data}
content = {"code": code, "msg": msg, "data": ({} if data is None else data)}
content.update(kwargs)
super().__init__(content=content, status_code=code)
super().__init__(content=jsonable_encoder(content), status_code=code)
class SuccessExtra(JSONResponse):
@ -49,4 +52,27 @@ class SuccessExtra(JSONResponse):
"page_size": page_size,
}
content.update(kwargs)
super().__init__(content=content, status_code=code)
super().__init__(content=jsonable_encoder(content), status_code=code)
T = TypeVar("T")
class BasicResponse(GenericModel, Generic[T]):
code: int = Field(200, description="状态码")
msg: Optional[str] = Field("OK", description="信息")
data: Optional[T] = Field(None, description="数据载荷")
class PageResponse(GenericModel, Generic[T]):
code: int = Field(200, description="状态码")
msg: Optional[str] = Field(None, description="信息")
data: List[T] = Field(default_factory=list, description="数据列表")
total: int = Field(0, description="总数量")
page: int = Field(1, description="当前页码")
page_size: int = Field(20, description="每页数量")
pages: Optional[int] = Field(None, description="总页数")
class MessageOut(BaseModel):
message: str = Field(..., description="提示信息")

200
app/schemas/invoice.py Normal file
View File

@ -0,0 +1,200 @@
from typing import Optional, List, Union, Dict, Any
from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator
class InvoiceHeaderCreate(BaseModel):
company_name: str = Field(..., min_length=1, max_length=128)
tax_number: str = Field(..., min_length=1, max_length=32)
register_address: Optional[str] = Field(None, min_length=1, max_length=256)
register_phone: Optional[str] = Field(None, min_length=1, max_length=32)
bank_name: Optional[str] = Field(None, min_length=1, max_length=128)
bank_account: Optional[str] = Field(None, min_length=1, max_length=64)
email: EmailStr
is_default: Optional[bool] = False
@field_validator('register_address', 'register_phone', 'bank_name', 'bank_account', mode='before')
@classmethod
def _empty_to_none(cls, v):
if isinstance(v, str) and v.strip() == "":
return None
return v
class InvoiceHeaderOut(BaseModel):
id: int
app_user_id: Optional[int] = None
company_name: str
tax_number: str
register_address: str
register_phone: str
bank_name: str
bank_account: str
email: Optional[str] = None
class Config:
from_attributes = True
is_default: Optional[bool] = False
class InvoiceHeaderUpdate(BaseModel):
company_name: Optional[str] = Field(None, min_length=1, max_length=128)
tax_number: Optional[str] = Field(None, min_length=1, max_length=32)
register_address: Optional[str] = Field(None, max_length=256)
register_phone: Optional[str] = Field(None, max_length=32)
bank_name: Optional[str] = Field(None, max_length=128)
bank_account: Optional[str] = Field(None, max_length=64)
email: Optional[EmailStr] = None
is_default: Optional[bool] = None
class InvoiceCreate(BaseModel):
ticket_type: str = Field(..., pattern=r"^(electronic|paper)$")
invoice_type: str = Field(..., pattern=r"^(special|normal)$")
phone: str = Field(..., min_length=5, max_length=20)
email: EmailStr
company_name: str = Field(..., min_length=1, max_length=128)
tax_number: str = Field(..., min_length=1, max_length=32)
register_address: str = Field(..., max_length=256)
register_phone: str = Field(..., max_length=32)
bank_name: str = Field(..., max_length=128)
bank_account: str = Field(..., max_length=64)
app_user_id: Optional[int] = None
header_id: Optional[int] = None
wechat: Optional[str] = None
class InvoiceUpdate(BaseModel):
ticket_type: Optional[str] = Field(None, pattern=r"^(electronic|paper)$")
invoice_type: Optional[str] = Field(None, pattern=r"^(special|normal)$")
phone: Optional[str] = Field(None, min_length=5, max_length=20)
email: Optional[EmailStr] = None
company_name: Optional[str] = Field(None, min_length=1, max_length=128)
tax_number: Optional[str] = Field(None, min_length=1, max_length=32)
register_address: Optional[str] = Field(None, min_length=1, max_length=256)
register_phone: Optional[str] = Field(None, min_length=1, max_length=32)
bank_name: Optional[str] = Field(None, min_length=1, max_length=128)
bank_account: Optional[str] = Field(None, min_length=1, max_length=64)
wechat: Optional[str] = None
class InvoiceOut(BaseModel):
id: int
created_at: str
ticket_type: str
invoice_type: str
phone: str
email: EmailStr
company_name: str
tax_number: str
register_address: str
register_phone: str
bank_name: str
bank_account: str
status: str
app_user_id: Optional[int]
header_id: Optional[int]
wechat: Optional[str]
class InvoiceList(BaseModel):
items: List[InvoiceOut]
total: int
page: int
page_size: int
class UpdateStatus(BaseModel):
id: int
status: str = Field(..., pattern=r"^(pending|invoiced|rejected|refunded)$")
class UpdateType(BaseModel):
ticket_type: str = Field(..., pattern=r"^(electronic|paper)$")
invoice_type: str = Field(..., pattern=r"^(special|normal)$")
class PaymentReceiptCreate(BaseModel):
url: str = Field(..., min_length=1, max_length=512)
note: Optional[str] = Field(None, max_length=256)
extra: Optional[Union[List[str], Dict[str, Any]]] = None
class PaymentReceiptOut(BaseModel):
id: int
url: str
note: Optional[str]
verified: bool
created_at: str
extra: Optional[Union[List[str], Dict[str, Any]]] = None
class AppCreateInvoiceWithReceipt(BaseModel):
header_id: int
ticket_type: Optional[str] = Field(None, pattern=r"^(electronic|paper)$")
invoice_type: Optional[str] = Field(None, pattern=r"^(special|normal)$")
# 兼容前端索引字段:"0"→normal"1"→special
invoiceTypeIndex: Optional[str] = None
receipt_url: Optional[Union[str, List[str]]] = Field(None)
receipt_urls: Optional[List[str]] = None
note: Optional[str] = Field(None, max_length=256)
@field_validator('ticket_type', mode='before')
@classmethod
def _default_ticket_type(cls, v):
return v or 'electronic'
@field_validator('receipt_url', mode='before')
@classmethod
def _clean_receipt_url(cls, v):
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 None
@field_validator('receipt_urls', mode='before')
@classmethod
def _clean_receipt_urls(cls, v):
if v is None:
return v
if isinstance(v, str):
v = [v]
if isinstance(v, list):
seen = set()
cleaned = []
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 and s not in seen:
seen.add(s)
cleaned.append(s)
return cleaned or None
return None
@model_validator(mode='after')
def _coerce_invoice_type(self):
if not self.invoice_type and self.invoiceTypeIndex is not None:
mapping = {'0': 'normal', '1': 'special'}
self.invoice_type = mapping.get(str(self.invoiceTypeIndex))
# 若仍为空,默认 normal
if not self.invoice_type:
self.invoice_type = 'normal'
return self

View File

@ -0,0 +1,36 @@
from pydantic import BaseModel, Field, EmailStr
from typing import Optional, List, Union
class SendEmailRequest(BaseModel):
receipt_id: Optional[int] = Field(None, description="付款凭证ID")
email: EmailStr = Field(..., description="邮箱地址")
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):
data: SendEmailRequest
class SendEmailResponse(BaseModel):
status: str
log_id: Optional[int] = None
error: Optional[str] = None
class EmailSendLogOut(BaseModel):
id: int
email: str
subject: Optional[str]
body_summary: Optional[str]
file_name: Optional[str]
file_url: Optional[str]
status: str
class SendEmailBody(BaseModel):
data: SendEmailRequest

View File

@ -3,4 +3,9 @@ from pydantic import BaseModel
class ImageUploadResponse(BaseModel):
"""图片上传响应模型"""
url: str
filename: str
filename: str
class FileUploadResponse(BaseModel):
url: str
filename: str
content_type: str

View File

@ -1,6 +1,7 @@
from datetime import datetime
from typing import List, Optional, Any, Dict, Union
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator, model_validator
from decimal import Decimal
class ValuationAssessmentBase(BaseModel):
@ -28,6 +29,8 @@ class ValuationAssessmentBase(BaseModel):
historical_evidence: Optional[Dict[str, int]] = Field(None, description="非遗资产历史证明证据及数量")
patent_certificates: Optional[List[str]] = Field(None, description="非遗资产所用专利的证书")
pattern_images: Optional[List[str]] = Field(None, description="非遗纹样图片")
report_url: Optional[str] = Field(None, description="评估报告URL")
certificate_url: Optional[str] = Field(None, description="证书URL")
# 非遗应用与推广
application_maturity: Optional[str] = Field(None, description="非遗资产应用成熟度")
@ -53,6 +56,8 @@ class ValuationAssessmentBase(BaseModel):
price_fluctuation: Optional[List[Union[str, int, float]]] = Field(None, description="该商品近30天价格波动区间")
price_range: Optional[Dict[str, Union[int, float]]] = Field(None, description="资产商品的价格波动率") # 未使用
market_price: Optional[Union[int, float]] = Field(None, description="市场价格(单位:万元)") # 未使用
credit_code_or_id: Optional[str] = Field(None, description="统一社会信用代码或身份证号")
biz_intro: Optional[str] = Field(None, description="业务/传承介绍")
# 内置API计算字段
infringement_record: Optional[str] = Field(None, description="侵权记录")
@ -95,40 +100,101 @@ class ValuationAssessmentUpdate(BaseModel):
# 非遗等级与技术
inheritor_level: Optional[str] = Field(None, description="非遗传承人等级")
inheritor_ages: Optional[List[int]] = Field(None, description="传承人年龄列表")
inheritor_age_count: Optional[List[Any]] = Field(None, description="非遗传承人年龄水平及数量")
inheritor_certificates: Optional[List[Any]] = Field(None, description="非遗传承人等级证书")
heritage_level: Optional[str] = Field(None, description="非遗等级")
heritage_asset_level: Optional[str] = Field(None, description="非遗资产等级")
patent_application_no: Optional[str] = Field(None, description="非遗资产所用专利的申请号")
historical_evidence: Optional[List[Any]] = Field(None, description="非遗资产历史证明证据及数量")
patent_remaining_years: Optional[str] = Field(None, description="专利剩余年限")
historical_evidence: Optional[Dict[str, int]] = Field(None, description="非遗资产历史证明证据及数量")
patent_certificates: Optional[List[Any]] = Field(None, description="非遗资产所用专利的证书")
pattern_images: Optional[List[Any]] = Field(None, description="非遗纹样图片")
report_url: Optional[str] = Field(None, description="评估报告URL")
certificate_url: Optional[str] = Field(None, description="证书URL")
# 非遗应用与推广
implementation_stage: Optional[str] = Field(None, description="非遗资产应用成熟度")
application_maturity: Optional[str] = Field(None, description="非遗资产应用成熟度")
application_coverage: Optional[str] = Field(None, description="非遗资产应用覆盖范围")
coverage_area: Optional[str] = Field(None, description="应用覆盖范围")
cooperation_depth: Optional[str] = Field(None, description="非遗资产跨界合作深度")
collaboration_type: Optional[str] = Field(None, description="跨界合作类型")
offline_activities: Optional[str] = Field(None, description="近12个月线下相关宣讲活动次数")
offline_teaching_count: Optional[int] = Field(None, description="近12个月线下相关演讲活动次数")
online_accounts: Optional[List[Any]] = Field(None, description="线上相关宣传账号信息")
platform_accounts: Optional[Dict[str, Dict[str, Union[str, int]]]] = Field(None, description="线上相关宣传账号信息")
# 非遗资产衍生商品信息
sales_volume: Optional[str] = Field(None, description="该商品近12个月销售量")
link_views: Optional[str] = Field(None, description="该商品近12个月的链接浏览量")
circulation: Optional[str] = Field(None, description="该商品的发行量")
scarcity_level: Optional[str] = Field(None, description="稀缺等级")
last_market_activity: Optional[str] = Field(None, description="该商品最近一次市场活动时间")
market_activity_time: Optional[str] = Field(None, description="市场活动的时间")
monthly_transaction: Optional[str] = Field(None, description="月交易额")
monthly_transaction_amount: Optional[str] = Field(None, description="月交易额")
price_fluctuation: Optional[List[Union[str, int, float]]] = Field(None, description="该商品近30天价格波动区间")
price_range: Optional[Dict[str, Union[int, float]]] = Field(None, description="资产商品的价格波动率")
market_price: Optional[Union[int, float]] = Field(None, description="市场价格(单位:万元)")
credit_code_or_id: Optional[str] = Field(None, description="统一社会信用代码或身份证号")
biz_intro: Optional[str] = Field(None, description="业务/传承介绍")
# 内置API计算字段
infringement_record: Optional[str] = Field(None, description="侵权记录")
patent_count: Optional[str] = Field(None, description="专利使用量")
esg_value: Optional[str] = Field(None, description="ESG关联价值")
policy_matching: Optional[str] = Field(None, description="政策匹配度")
online_course_views: Optional[int] = Field(None, description="线上课程点击量")
pattern_complexity: Optional[str] = Field(None, description="结构复杂度")
normalized_entropy: Optional[str] = Field(None, description="归一化信息熵")
legal_risk: Optional[str] = Field(None, description="法律风险-侵权诉讼历史")
base_pledge_rate: Optional[str] = Field(None, description="基础质押率")
flow_correction: Optional[str] = Field(None, description="流量修正系数")
# 计算结果字段
model_value_b: Optional[float] = Field(None, description="模型估值B万元")
market_value_c: Optional[float] = Field(None, description="市场估值C万元")
final_value_ab: Optional[float] = Field(None, description="最终估值AB万元")
dynamic_pledge_rate: Optional[float] = Field(None, description="动态质押率")
calculation_result: Optional[Dict[str, Any]] = Field(None, description="完整计算结果JSON")
calculation_input: Optional[Dict[str, Any]] = Field(None, description="计算输入参数JSON")
# 系统字段
status: Optional[str] = Field(None, description="评估状态: pending(待审核), success(已通过), fail(已拒绝)")
admin_notes: Optional[str] = Field(None, description="管理员备注")
is_active: Optional[bool] = Field(None, description="是否激活")
@field_validator('report_url', 'certificate_url', mode='before')
@classmethod
def _coerce_url(cls, v):
if v is None:
return None
if isinstance(v, list) and v:
v = v[0]
if isinstance(v, str):
s = v.strip()
if s.startswith('`') and s.endswith('`'):
s = s[1:-1].strip()
return s
return v
class ValuationAssessmentOut(ValuationAssessmentBase):
"""估值评估输出模型"""
id: int = Field(..., description="主键ID")
user_id: int = Field(..., description="用户ID")
user_phone: Optional[str] = Field(None, description="用户手机号")
report_url: List[str] = Field(default_factory=list, description="评估报告URL列表")
certificate_url: List[str] = Field(default_factory=list, description="证书URL列表")
report_download_urls: List[str] = Field(default_factory=list, description="评估报告下载地址列表")
certificate_download_urls: List[str] = Field(default_factory=list, description="证书下载地址列表")
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="是否激活")
class Config:
@ -139,6 +205,29 @@ class ValuationAssessmentOut(ValuationAssessmentBase):
# 确保所有字段都被序列化包括None值
exclude_none = False
@field_validator('report_url', 'certificate_url', mode='before')
@classmethod
def _to_list(cls, v):
def clean(s: str) -> str:
s = s.strip()
if s.startswith('`') and s.endswith('`'):
s = s[1:-1].strip()
return s
if v is None:
return []
if isinstance(v, list):
return [clean(str(i)) for i in v if i is not None and str(i).strip() != ""]
if isinstance(v, str):
s = clean(v)
return [s] if s else []
return []
@model_validator(mode='after')
def _fill_downloads(self):
self.report_download_urls = list(self.report_url or [])
self.certificate_download_urls = list(self.certificate_url or [])
return self
# 用户端专用模式
class UserValuationCreate(ValuationAssessmentBase):
@ -150,10 +239,14 @@ class UserValuationOut(ValuationAssessmentBase):
"""用户端估值评估输出模型"""
id: int = Field(..., description="主键ID")
user_id: Optional[int] = Field(None, description="用户ID")
report_url: List[str] = Field(default_factory=list, description="评估报告URL列表")
certificate_url: List[str] = Field(default_factory=list, description="证书URL列表")
report_download_urls: List[str] = Field(default_factory=list, description="评估报告下载地址列表")
certificate_download_urls: List[str] = Field(default_factory=list, description="证书下载地址列表")
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:
@ -163,14 +256,41 @@ class UserValuationOut(ValuationAssessmentBase):
}
exclude_none = False
@field_validator('report_url', 'certificate_url', mode='before')
@classmethod
def _to_list(cls, v):
def clean(s: str) -> str:
s = s.strip()
if s.startswith('`') and s.endswith('`'):
s = s[1:-1].strip()
return s
if v is None:
return []
if isinstance(v, list):
return [clean(str(i)) for i in v if i is not None and str(i).strip() != ""]
if isinstance(v, str):
s = clean(v)
return [s] if s else []
return []
@model_validator(mode='after')
def _fill_downloads(self):
self.report_download_urls = list(self.report_url or [])
self.certificate_download_urls = list(self.certificate_url or [])
return self
class UserValuationDetail(ValuationAssessmentBase):
"""用户端详细估值评估模型"""
id: int = Field(..., description="主键ID")
report_url: List[str] = Field(default_factory=list, description="评估报告URL列表")
certificate_url: List[str] = Field(default_factory=list, description="证书URL列表")
report_download_urls: List[str] = Field(default_factory=list, description="评估报告下载地址列表")
certificate_download_urls: List[str] = Field(default_factory=list, description="证书下载地址列表")
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
@ -178,6 +298,29 @@ class UserValuationDetail(ValuationAssessmentBase):
datetime: lambda v: v.isoformat()
}
@field_validator('report_url', 'certificate_url', mode='before')
@classmethod
def _to_list(cls, v):
def clean(s: str) -> str:
s = s.strip()
if s.startswith('`') and s.endswith('`'):
s = s[1:-1].strip()
return s
if v is None:
return []
if isinstance(v, list):
return [clean(str(i)) for i in v if i is not None and str(i).strip() != ""]
if isinstance(v, str):
s = clean(v)
return [s] if s else []
return []
@model_validator(mode='after')
def _fill_downloads(self):
self.report_download_urls = list(self.report_url or [])
self.certificate_download_urls = list(self.certificate_url or [])
return self
class UserValuationList(BaseModel):
"""用户端估值评估列表模型"""
@ -219,8 +362,13 @@ class ValuationAssessmentQuery(BaseModel):
institution: Optional[str] = Field(None, description="所属机构")
industry: Optional[str] = Field(None, description="所属行业")
heritage_level: Optional[str] = Field(None, description="非遗等级")
status: Optional[str] = Field(None, description="评估状态: pending(待审核), approved(已通过), rejected(已拒绝)")
status: Optional[str] = Field(None, description="评估状态: pending(待审核), success(已通过), rejected(已拒绝)")
is_active: Optional[bool] = Field(None, description="是否激活")
phone: Optional[str] = Field(None, description="手机号模糊查询")
submitted_start: Optional[str] = Field(None, description="提交时间开始毫秒时间戳或ISO字符串")
submitted_end: Optional[str] = Field(None, description="提交时间结束毫秒时间戳或ISO字符串")
audited_start: Optional[str] = Field(None, description="审核时间开始证书修改时间毫秒时间戳或ISO字符串")
audited_end: Optional[str] = Field(None, description="审核时间结束证书修改时间毫秒时间戳或ISO字符串")
page: int = Field(1, ge=1, description="页码")
size: int = Field(10, ge=1, le=100, description="每页数量")
@ -233,4 +381,52 @@ class ValuationApprovalRequest(BaseModel):
class ValuationAdminNotesUpdate(BaseModel):
"""管理员备注更新模型"""
admin_notes: str = Field(..., description="管理员备注")
admin_notes: str = Field(..., description="管理员备注")
class ValuationCalculationStepBase(BaseModel):
"""估值计算步骤基础模型"""
step_order: Decimal = Field(..., description="步骤顺序")
step_name: str = Field(..., description="步骤名称")
step_description: Optional[str] = Field(None, description="步骤描述")
input_params: Optional[Dict[str, Any]] = Field(None, description="输入参数")
output_result: Optional[Dict[str, Any]] = Field(None, description="输出结果")
status: str = Field(..., description="步骤状态: processing/completed/failed")
error_message: Optional[str] = Field(None, description="错误信息")
formula_code: Optional[str] = Field(None, description="公式编码")
formula_name: Optional[str] = Field(None, description="公式名称")
formula_text: Optional[str] = Field(None, description="公式说明")
parent_formula_code: Optional[str] = Field(None, description="父级公式编码")
group_code: Optional[str] = Field(None, description="分组编码")
@field_validator('step_order', mode='before')
@classmethod
def _coerce_step_order(cls, v):
if isinstance(v, Decimal):
return v
if isinstance(v, (int, float, str)):
try:
return Decimal(str(v))
except Exception:
raise ValueError('Invalid step_order')
raise ValueError('Invalid step_order type')
class ValuationCalculationStepCreate(ValuationCalculationStepBase):
"""创建估值计算步骤模型"""
valuation_id: int = Field(..., description="关联的估值评估ID")
class ValuationCalculationStepOut(ValuationCalculationStepBase):
"""估值计算步骤输出模型"""
id: int = Field(..., description="主键ID")
valuation_id: int = Field(..., description="关联的估值评估ID")
created_at: datetime = Field(..., description="创建时间")
updated_at: Optional[datetime] = Field(None, description="更新时间")
class Config:
from_attributes = True
json_encoders = {
datetime: lambda v: v.isoformat(),
Decimal: lambda v: float(v)
}

View File

@ -0,0 +1,66 @@
import smtplib
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email import encoders
from typing import Optional, List, Tuple
import httpx
from app.settings.config import settings
class EmailClient:
def send(self, to_email: str, subject: Optional[str], body: str, file_bytes: Optional[bytes], file_name: Optional[str], content_type: Optional[str]) -> dict:
if not settings.SMTP_HOST or not settings.SMTP_PORT or not settings.SMTP_FROM:
raise RuntimeError("SMTP 未配置")
msg = MIMEMultipart()
msg["From"] = settings.SMTP_FROM
msg["To"] = to_email
msg["Subject"] = subject or "估值服务通知"
msg.attach(MIMEText(body, "plain", "utf-8"))
if file_bytes and file_name:
part = MIMEBase("application", "octet-stream")
part.set_payload(file_bytes)
encoders.encode_base64(part)
part.add_header("Content-Disposition", f"attachment; filename=\"{file_name}\"")
msg.attach(part)
if hasattr(self, "_extra_attachments") and isinstance(self._extra_attachments, list):
for fb, fn in self._extra_attachments:
part = MIMEBase("application", "octet-stream")
part.set_payload(fb)
encoders.encode_base64(part)
part.add_header("Content-Disposition", f"attachment; filename=\"{fn}\"")
msg.attach(part)
if settings.SMTP_TLS:
server = smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT, timeout=30)
server.starttls()
else:
server = smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT, timeout=30)
try:
if settings.SMTP_USERNAME and settings.SMTP_PASSWORD:
server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD)
server.sendmail(settings.SMTP_FROM, [to_email], msg.as_string())
server.quit()
return {"status": "OK"}
except Exception as e:
try:
server.quit()
except Exception:
pass
if isinstance(e, smtplib.SMTPRecipientsRefused):
return {"status": "FAIL", "error": "收件方地址不存在或暂时不可用"}
return {"status": "FAIL", "error": str(e)}
def send_many(self, to_email: str, subject: Optional[str], body: str, attachments: Optional[List[Tuple[bytes, str]]] = None) -> dict:
self._extra_attachments = attachments or []
try:
return self.send(to_email, subject, body, None, None, None)
finally:
self._extra_attachments = []
email_client = EmailClient()

View File

@ -0,0 +1,52 @@
import time
from typing import Dict
class PhoneRateLimiter:
def __init__(self, window_seconds: int = 60) -> None:
"""手机号限流器
Args:
window_seconds: 限流窗口秒数
Returns:
None
"""
self.window = window_seconds
self.last_sent: Dict[str, float] = {}
def allow(self, phone: str) -> bool:
"""校验是否允许发送
Args:
phone: 手机号
Returns:
True 表示允许发送False 表示命中限流
"""
now = time.time()
ts = self.last_sent.get(phone, 0)
if now - ts < self.window:
return False
self.last_sent[phone] = now
return True
def next_allowed_at(self, phone: str) -> float:
"""返回下一次允许发送的时间戳
Args:
phone: 手机号
Returns:
时间戳
"""
ts = self.last_sent.get(phone, 0)
return ts + self.window
def reset(self) -> None:
"""重置限流状态
Returns:
None
"""
self.last_sent.clear()

107
app/services/sms_client.py Normal file
View File

@ -0,0 +1,107 @@
import json
from typing import Optional, Dict, Any
from app.settings import settings
from app.log import logger
class SMSClient:
def __init__(self) -> None:
"""初始化短信客户端
Returns:
None
"""
self.client = None
def _ensure_client(self) -> None:
"""确保客户端初始化
Returns:
None
"""
if self.client is not None:
return
from alibabacloud_tea_openapi import models as open_api_models # type: ignore
from alibabacloud_dysmsapi20170525.client import Client as DysmsClient # type: ignore
from alibabacloud_credentials.client import Client as CredentialClient # type: ignore
use_chain = bool(settings.ALIYUN_USE_DEFAULT_CREDENTIALS) or (not settings.ALIBABA_CLOUD_ACCESS_KEY_ID or not settings.ALIBABA_CLOUD_ACCESS_KEY_SECRET)
if not use_chain and (not settings.ALIBABA_CLOUD_ACCESS_KEY_ID or not settings.ALIBABA_CLOUD_ACCESS_KEY_SECRET):
raise RuntimeError("短信凭证未配置:请设置 ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET 或启用默认凭据链")
if not settings.ALIYUN_SMS_SIGN_NAME:
raise RuntimeError("短信签名未配置:请设置 ALIYUN_SMS_SIGN_NAME")
if str(settings.ALIYUN_SMS_SIGN_NAME).upper().startswith("SMS_"):
raise RuntimeError("短信签名配置错误:签名不应为模板码")
if settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY and settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT and settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY == settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT:
raise RuntimeError("短信模板配置错误:验证码模板与报告模板重复")
if use_chain:
credential = CredentialClient()
config = open_api_models.Config(credential=credential)
else:
config = open_api_models.Config(
access_key_id=settings.ALIBABA_CLOUD_ACCESS_KEY_ID,
access_key_secret=settings.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
)
config.endpoint = settings.ALIYUN_SMS_ENDPOINT
self.client = DysmsClient(config)
def send_by_template(self, phone: str, template_code: str, template_param: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""按模板发送短信
Args:
phone: 接收短信手机号
template_code: 模板 Code
template_param: 模板变量字典
Returns:
返回体映射字典
"""
from alibabacloud_dysmsapi20170525 import models as sms_models # type: ignore
self._ensure_client()
req = sms_models.SendSmsRequest(
phone_numbers=phone,
sign_name=settings.ALIYUN_SMS_SIGN_NAME,
template_code=template_code,
template_param=json.dumps(template_param or {}),
)
logger.info("sms.send start phone={} sign={} template={}", phone, settings.ALIYUN_SMS_SIGN_NAME, template_code)
try:
resp = self.client.send_sms(req)
body = resp.body.to_map() if hasattr(resp, "body") else {}
logger.info("sms.send response code={} request_id={} phone={}", body.get("Code"), body.get("RequestId"), phone)
return body
except Exception as e:
logger.error("sms.provider_error err={}", repr(e))
return {"Code": "ERROR", "Message": str(e)}
def send_code(self, phone: str, code: str) -> Dict[str, Any]:
"""发送验证码短信
Args:
phone: 接收短信手机号
code: 验证码
Returns:
返回体映射字典
"""
key = settings.ALIYUN_SMS_TEMPLATE_PARAM_CODE_KEY or "code"
template = settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY
logger.info("sms.send_code using key={} template={} phone={}", key, template, phone)
return self.send_by_template(phone, template, {key: code})
def send_report(self, phone: str) -> Dict[str, Any]:
"""发送报告通知短信
Args:
phone: 接收短信手机号
Returns:
返回体映射字典
"""
template = settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT
logger.info("sms.send_report using template={} phone={}", template, phone)
return self.send_by_template(phone, template, {})
sms_client = SMSClient()

157
app/services/sms_store.py Normal file
View File

@ -0,0 +1,157 @@
import random
import time
from datetime import date
from typing import Dict, Optional, Tuple
from app.settings import settings
class VerificationStore:
def __init__(self, code_ttl_seconds: int = 300, minute_window: int = 60, daily_limit: int = 10, max_failures: int = 5, lock_seconds: int = 3600) -> None:
"""验证码与限流存储
Args:
code_ttl_seconds: 验证码有效期秒数
minute_window: 同号分钟级限流窗口
daily_limit: 每日发送上限次数
max_failures: 最大失败次数后锁定
lock_seconds: 锁定时长秒数
Returns:
None
"""
self.code_ttl = code_ttl_seconds
self.minute_window = minute_window
self.daily_limit = daily_limit
self.max_failures = max_failures
self.lock_seconds = lock_seconds
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:
"""生成数字验证码
Returns:
指定位数的数字字符串
"""
digits = int(getattr(settings, "SMS_CODE_DIGITS", 6) or 6)
max_val = (10 ** digits) - 1
return f"{random.randint(0, max_val):0{digits}d}"
def set_code(self, phone: str, code: str) -> None:
"""设置验证码与过期时间
Args:
phone: 手机号
code: 验证码
Returns:
None
"""
expires_at = time.time() + self.code_ttl
self.codes[phone] = (code, expires_at)
def get_code(self, phone: str) -> Optional[Tuple[str, float]]:
"""获取存储的验证码与过期时间
Args:
phone: 手机号
Returns:
元组(code, expires_at)或None
"""
return self.codes.get(phone)
def clear_code(self, phone: str) -> None:
"""清除验证码记录
Args:
phone: 手机号
Returns:
None
"""
self.codes.pop(phone, None)
def allow_send(self, phone: str) -> Tuple[bool, Optional[str]]:
"""校验是否允许发送验证码
Args:
phone: 手机号
Returns:
(允许, 拒绝原因)
"""
now = time.time()
dkey = date.today().isoformat()
info = self.sends.get(phone) or {"day": dkey, "count": 0.0, "last_ts": 0.0}
if info["day"] != dkey:
info = {"day": dkey, "count": 0.0, "last_ts": 0.0}
if now - info["last_ts"] < self.minute_window:
self.sends[phone] = info
return False, "发送频率过高"
if info["count"] >= float(self.daily_limit):
self.sends[phone] = info
return False, "今日发送次数已达上限"
info["last_ts"] = now
info["count"] = info["count"] + 1.0
self.sends[phone] = info
return True, None
def can_verify(self, phone: str) -> Tuple[bool, Optional[str]]:
"""校验是否允许验证
Args:
phone: 手机号
Returns:
(允许, 拒绝原因)
"""
now = time.time()
stat = self.failures.get(phone)
if stat and stat.get("lock_until", 0.0) > now:
return False, "尝试次数过多,已锁定"
return True, None
def record_verify_failure(self, phone: str) -> Tuple[int, bool]:
"""记录一次验证失败并判断是否触发锁定
Args:
phone: 手机号
Returns:
(失败次数, 是否锁定)
"""
now = time.time()
stat = self.failures.get(phone) or {"count": 0.0, "lock_until": 0.0}
if stat.get("lock_until", 0.0) > now:
return int(stat["count"]), True
stat["count"] = stat.get("count", 0.0) + 1.0
if int(stat["count"]) >= self.max_failures:
stat["lock_until"] = now + self.lock_seconds
self.failures[phone] = stat
return int(stat["count"]), stat["lock_until"] > now
def reset_failures(self, phone: str) -> None:
"""重置失败计数
Args:
phone: 手机号
Returns:
None
"""
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()

View File

@ -31,22 +31,22 @@ class Settings(BaseSettings):
TORTOISE_ORM: dict = {
"connections": {
# SQLite configuration
"sqlite": {
"engine": "tortoise.backends.sqlite",
"credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file
},
# "sqlite": {
# "engine": "tortoise.backends.sqlite",
# "credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file
# },
# MySQL/MariaDB configuration
# Install with: tortoise-orm[asyncmy]
# "mysql": {
# "engine": "tortoise.backends.mysql",
# "credentials": {
# "host": "localhost", # Database host address
# "port": 3306, # Database port
# "user": "yourusername", # Database username
# "password": "yourpassword", # Database password
# "database": "yourdatabase", # Database name
# },
# },
"mysql": {
"engine": "tortoise.backends.mysql",
"credentials": {
"host": "sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com", # Database host address
"port": 28555, # Database port
"user": "root", # Database username
"password": "api2api..", # Database password
"database": "valuation_service", # Database name
},
},
# PostgreSQL configuration
# Install with: tortoise-orm[asyncpg]
# "postgres": {
@ -87,7 +87,7 @@ class Settings(BaseSettings):
"apps": {
"models": {
"models": ["app.models", "aerich.models"],
"default_connection": "sqlite",
"default_connection": "mysql",
},
},
"use_tz": False, # Whether to use timezone-aware datetimes
@ -95,5 +95,24 @@ class Settings(BaseSettings):
}
DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
ALIBABA_CLOUD_ACCESS_KEY_ID: typing.Optional[str] = "LTAI5tA8gcgM8Qc7K9qCtmXg"
ALIBABA_CLOUD_ACCESS_KEY_SECRET: typing.Optional[str] = "eWZIWi6xILGtmPSGyJEAhILX5fQx0h"
ALIYUN_SMS_SIGN_NAME: typing.Optional[str] = "成都文化产权交易所"
ALIYUN_SMS_ENDPOINT: str = "dysmsapi.aliyuncs.com"
ALIYUN_SMS_TEMPLATE_CODE_VERIFY: typing.Optional[str] = "SMS_498140213"
ALIYUN_SMS_TEMPLATE_CODE_REPORT: typing.Optional[str] = "SMS_498190229"
SMS_CODE_DIGITS: int = 6
SMS_DEBUG_LOG_CODE: bool = True
ALIYUN_USE_DEFAULT_CREDENTIALS: bool = False
ALIYUN_SMS_TEMPLATE_PARAM_CODE_KEY: typing.Optional[str] = "code"
SMS_BYPASS_CODE: typing.Optional[str] = "202511"
SMTP_HOST: typing.Optional[str] = "smtp.qiye.aliyun.com"
SMTP_PORT: typing.Optional[int] = 465
SMTP_USERNAME: typing.Optional[str] = "value@cdcee.net"
SMTP_PASSWORD: typing.Optional[str] = "PPXbILdGlRCn2VOx"
SMTP_TLS: bool = False
SMTP_FROM: typing.Optional[str] = "value@cdcee.net"
settings = Settings()

View File

@ -0,0 +1,2 @@
"id" "asset_name" "institution" "industry" "annual_revenue" "rd_investment" "three_year_income" "funding_status" "inheritor_level" "inheritor_ages" "inheritor_age_count" "inheritor_certificates" "heritage_asset_level" "patent_application_no" "patent_remaining_years" "historical_evidence" "patent_certificates" "pattern_images" "implementation_stage" "application_coverage" "cooperation_depth" "offline_activities" "platform_accounts" "sales_volume" "link_views" "circulation" "scarcity_level" "last_market_activity" "market_activity_time" "monthly_transaction_amount" "price_fluctuation" "price_range" "market_price" "infringement_record" "patent_count" "esg_value" "policy_matching" "online_course_views" "pattern_complexity" "normalized_entropy" "legal_risk" "base_pledge_rate" "flow_correction" "model_value_b" "market_value_c" "final_value_ab" "dynamic_pledge_rate" "calculation_result" "calculation_input" "status" "admin_notes" "created_at" "updated_at" "is_active" "user_id"
"19" "蜀锦" "成都古蜀蜀锦研究所" "纺织业" "169" "32" "[169,169,169]" "无资助" "省级传承人" "[0,0,2]" "[0,0,2]" "[]" "国家级非遗" "" "{""artifacts"":2,""ancient_literature"":5,""inheritor_testimony"":5,""modern_research"":6}" "[]" "[]" "成熟应用" "1" "1" "50" "{""douyin"":{""account"":""huguangjing3691"",""likes"":""67000"",""comments"":""800"",""shares"":""500""}}" "5000" "296000" "限量:总发行份数 ≤100份" "限量:总发行份数 ≤100份" "0" "近一周" "月交易额100万500万" "[1580,3980]" "success" "2025-11-17 18:13:17.435287+08:00" "2025-11-17 18:13:17.435322+08:00" "1" "30"

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

View File

@ -3,6 +3,7 @@ from typing import Optional
import jwt
from fastapi import HTTPException, status, Depends, Header
from app.controllers.app_user import app_user_controller
from app.core.token_blacklist import is_blacklisted
from app.schemas.app_user import AppUserJWTPayload
from app.settings import settings
@ -48,18 +49,24 @@ def verify_app_user_token(token: str) -> Optional[AppUserJWTPayload]:
return None
def get_current_app_user_id(token: str = Header(None)) -> int:
async def get_current_app_user_id(token: str = Header(None)) -> int:
"""
从令牌中获取当前AppUser ID
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭据",
detail="未登录,请重新登录",
headers={"WWW-Authenticate": "Bearer"},
)
if not token:
raise credentials_exception
if token and token != "dev":
try:
if await is_blacklisted(token):
raise credentials_exception
except Exception:
pass
payload = verify_app_user_token(token)
if payload is None:
@ -80,4 +87,4 @@ async def get_current_app_user(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在或已被停用"
)
return user
return user

View File

@ -1,53 +1,53 @@
'''
这是非物质文化遗产IP知识产权评估系统的核心计算引擎包
'''
from app.utils.calculation_engine.economic_value_b1 import EconomicValueB1Calculator
from app.utils.calculation_engine.economic_value_b1.sub_formulas import (
BasicValueB11Calculator,
TrafficFactorB12Calculator,
PolicyMultiplierB13Calculator
)
from app.utils.calculation_engine.cultural_value_b2 import CulturalValueB2Calculator
from app.utils.calculation_engine.cultural_value_b2.sub_formulas import (
LivingHeritageB21Calculator,
PatternGeneB22Calculator
)
from app.utils.calculation_engine.risk_adjustment_b3 import RiskAdjustmentB3Calculator
from app.utils.calculation_engine.market_value_c import MarketValueCCalculator
from app.utils.calculation_engine.market_value_c.sub_formulas import (
MarketBiddingC1Calculator,
HeatCoefficientC2Calculator,
ScarcityMultiplierC3Calculator,
TemporalDecayC4Calculator
)
from app.utils.calculation_engine.final_value_ab import FinalValueACalculator
"""
非遗资产估值计算引擎包
提供各类计算器并通过懒加载避免循环依赖
"""
from importlib import import_module
from typing import Any
__version__ = "1.0.0"
__author__ = "Assessment Team"
__all__ = [
# 经济价值B1模块
"EconomicValueB1Calculator",
"BasicValueB11Calculator",
"TrafficFactorB12Calculator",
"PolicyMultiplierB13Calculator",
# 文化价值B2模块
"CulturalValueB2Calculator",
"LivingHeritageB21Calculator",
"PatternGeneB22Calculator",
# 风险调整系数B3模块
"RiskAdjustmentB3Calculator",
# 市场估值C模块
"MarketValueCCalculator",
"MarketBiddingC1Calculator",
"HeatCoefficientC2Calculator",
"ScarcityMultiplierC3Calculator",
"TemporalDecayC4Calculator",
# 最终估值A模块
"FinalValueACalculator"
"FinalValueACalculator",
]
_EXPORT_MAP = {
"EconomicValueB1Calculator": "app.utils.calculation_engine.economic_value_b1",
"BasicValueB11Calculator": "app.utils.calculation_engine.economic_value_b1.sub_formulas.basic_value_b11",
"TrafficFactorB12Calculator": "app.utils.calculation_engine.economic_value_b1.sub_formulas.traffic_factor_b12",
"PolicyMultiplierB13Calculator": "app.utils.calculation_engine.economic_value_b1.sub_formulas.policy_multiplier_b13",
"CulturalValueB2Calculator": "app.utils.calculation_engine.cultural_value_b2.cultural_value_b2",
"LivingHeritageB21Calculator": "app.utils.calculation_engine.cultural_value_b2.sub_formulas.living_heritage_b21",
"PatternGeneB22Calculator": "app.utils.calculation_engine.cultural_value_b2.sub_formulas.pattern_gene_b22",
"RiskAdjustmentB3Calculator": "app.utils.calculation_engine.risk_adjustment_b3.sub_formulas.risk_adjustment_b3",
"MarketValueCCalculator": "app.utils.calculation_engine.market_value_c.market_value_c",
"MarketBiddingC1Calculator": "app.utils.calculation_engine.market_value_c.sub_formulas.market_bidding_c1",
"HeatCoefficientC2Calculator": "app.utils.calculation_engine.market_value_c.sub_formulas.heat_coefficient_c2",
"ScarcityMultiplierC3Calculator": "app.utils.calculation_engine.market_value_c.sub_formulas.scarcity_multiplier_c3",
"TemporalDecayC4Calculator": "app.utils.calculation_engine.market_value_c.sub_formulas.temporal_decay_c4",
"FinalValueACalculator": "app.utils.calculation_engine.final_value_ab.final_value_a",
}
def __getattr__(name: str) -> Any:
module_path = _EXPORT_MAP.get(name)
if not module_path:
raise AttributeError(f"module {__name__} has no attribute {name}")
module = import_module(module_path)
attr = getattr(module, name)
globals()[name] = attr
return attr

View File

@ -6,14 +6,24 @@
"""
from typing import Dict, List, Optional
import sys
import os
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, '..', '..', '..'))
try:
# 相对导入(当作为包使用时)
from .sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator
from .sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator
from app.controllers.valuation import ValuationController
except ImportError:
# 绝对导入(当直接运行时)
from sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator
from sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator
from app.controllers.valuation import ValuationController
from app.controllers.valuation import ValuationController
class CulturalValueB2Calculator:
@ -23,6 +33,7 @@ class CulturalValueB2Calculator:
"""初始化计算器"""
self.living_heritage_calculator = LivingHeritageB21Calculator()
self.pattern_gene_calculator = PatternGeneB22Calculator()
self.valuation_controller = ValuationController()
def calculate_cultural_value_b2(self,
living_heritage_b21: float,
@ -42,48 +53,124 @@ class CulturalValueB2Calculator:
return cultural_value
def calculate_complete_cultural_value_b2(self, input_data: Dict) -> Dict:
async def calculate_complete_cultural_value_b2(self, valuation_id: int, input_data: Dict) -> Dict[str, float]:
"""
计算完整的文化价值B2包含所有子公式
args:
input_data: 输入数据字典包含所有必要的参数
return:
Dict: 包含所有中间计算结果和最终结果的字典
"""
# 计算活态传承系数B21
teaching_frequency = self.living_heritage_calculator.calculate_teaching_frequency(
input_data["offline_sessions"],
input_data["douyin_views"],
input_data["kuaishou_views"],
input_data["bilibili_views"]
)
living_heritage_b21 = self.living_heritage_calculator.calculate_living_heritage_b21(
input_data['inheritor_level_coefficient'],
teaching_frequency,
input_data['cross_border_depth']
)
# 计算纹样基因值B22
pattern_gene_b22 = self.pattern_gene_calculator.calculate_pattern_gene_b22(
input_data['structure_complexity'],
input_data['normalized_entropy'],
input_data['historical_inheritance']
)
# 计算文化价值B2
cultural_value_b2 = self.calculate_cultural_value_b2(
living_heritage_b21,
pattern_gene_b22
)
return {
'living_heritage_b21': living_heritage_b21,
'pattern_gene_b22': pattern_gene_b22,
'cultural_value_b2': cultural_value_b2
}
计算完整的文化价值B2并记录所有计算步骤
该函数通过整合活态传承系数B21和纹样基因值B22的计算
最终得出文化价值B2每一步的计算过程都会被记录下来
以确保计算的透明度和可追溯性
Args:
valuation_id (int): 估值的唯一标识符用于关联所有计算步骤
input_data (Dict): 包含所有计算所需参数的字典例如
{
'inheritor_level_coefficient': 10.0, # B21
'offline_sessions': 1, # B21
'structure_complexity': 0.75, # B22
...
}
Returns:
Dict[str, float]: 包含文化价值B2及子公式结果的字典
Raises:
Exception: 在计算过程中遇到的任何异常都会被捕获记录并重新抛出
"""
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_CULTURAL_B2",
status="processing",
input_params=input_data,
)
try:
# 计算活态传承系数B21
teaching_frequency = self.living_heritage_calculator.calculate_teaching_frequency(
input_data["offline_sessions"],
input_data["douyin_views"],
input_data["kuaishou_views"],
input_data["bilibili_views"]
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_CULTURAL_B21_TEACHING_FREQ",
status="completed",
input_params={
"offline_sessions": input_data.get("offline_sessions"),
"douyin_views": input_data.get("douyin_views"),
"kuaishou_views": input_data.get("kuaishou_views"),
"bilibili_views": input_data.get("bilibili_views"),
},
output_result={"teaching_frequency": teaching_frequency},
)
living_heritage_b21 = self.living_heritage_calculator.calculate_living_heritage_b21(
input_data['inheritor_level_coefficient'],
teaching_frequency,
input_data['cross_border_depth']
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_CULTURAL_B21",
status="completed",
input_params={
"inheritor_level_coefficient": input_data.get("inheritor_level_coefficient"),
"offline_sessions": input_data.get("offline_sessions"),
"douyin_views": input_data.get("douyin_views"),
"kuaishou_views": input_data.get("kuaishou_views"),
"bilibili_views": input_data.get("bilibili_views"),
"cross_border_depth": input_data.get("cross_border_depth"),
},
output_result={
"living_heritage_b21": living_heritage_b21,
"teaching_frequency": teaching_frequency,
},
)
# 计算纹样基因值B22
pattern_gene_b22 = self.pattern_gene_calculator.calculate_pattern_gene_b22(
input_data['structure_complexity'],
input_data['normalized_entropy'],
input_data['historical_inheritance']
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_CULTURAL_B22",
status="completed",
input_params={
"structure_complexity": input_data.get("structure_complexity"),
"normalized_entropy": input_data.get("normalized_entropy"),
"historical_inheritance": input_data.get("historical_inheritance"),
},
output_result={"pattern_gene_b22": pattern_gene_b22},
)
# 计算文化价值B2
cultural_value_b2 = self.calculate_cultural_value_b2(
living_heritage_b21,
pattern_gene_b22
)
result = {
"cultural_value_b2": cultural_value_b2,
"living_heritage_b21": living_heritage_b21,
"pattern_gene_b22": pattern_gene_b22,
}
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_CULTURAL_B2",
status="completed",
output_result=result,
)
return result
except Exception as e:
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_CULTURAL_B2",
status="failed",
error_message=str(e),
)
raise
# 示例使用
if __name__ == "__main__":

View File

@ -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

View File

@ -8,12 +8,26 @@
import sys
import os
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
try:
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError:
# 处理可能的导入错误
pass
class LivingHeritageB21Calculator:
"""活态传承系数B21计算器"""
def __init__(self):
"""初始化计算器"""
pass
self.valuation_controller = ValuationController()
def calculate_living_heritage_b21(self,
inheritor_level_coefficient: float,
@ -22,7 +36,6 @@ class LivingHeritageB21Calculator:
"""
计算活态传承系数B21
活态传承系数B21 = 传承人等级系数 × 0.4 + 教学传播频次 × 0.3 + 跨界合作深度 × 0.3
args:
@ -30,14 +43,12 @@ class LivingHeritageB21Calculator:
teaching_frequency: 教学传播频次 (用户填写)
cross_border_depth: 跨界合作深度 (用户填写)
return:
return:
float: 活态传承系数B21
"""
#
living_heritage = (inheritor_level_coefficient * 0.4 +
teaching_frequency * 0.3 +
cross_border_depth * 0.3)
return living_heritage
def calculate_inheritor_level_coefficient(self, inheritor_level: str) -> float:
@ -47,13 +58,12 @@ class LivingHeritageB21Calculator:
传承人等级评分标准
- 国家级传承人: 1
- 省级传承人: 0.7
- 市级传承人: .44
- 市级传承人: 0.4
args:
inheritor_level: 传承人等级 (用户填写)
return:
return:
float: 传承人等级系数
"""
level_scores = {
@ -61,7 +71,6 @@ class LivingHeritageB21Calculator:
"省级传承人": 0.7,
"市级传承人": 0.4,
}
return level_scores.get(inheritor_level, 0.4)
def calculate_teaching_frequency(self,
@ -74,16 +83,8 @@ class LivingHeritageB21Calculator:
教学传播频次 = 线下传习次数 × 0.6 + 线上课程点击量 × 0.4
线下传习次数统计规范
1) 单次活动标准传承人主导时长2小时参与人数5
2) 频次计算按自然年度累计同一内容重复培训不计入
线上课程折算
- 抖音/快手播放量按100:1折算为学习人次
- B站课程按50:1折算
args:
offline_sessions: 线下传习次数符合标准的活动次数
offline_sessions: 线下传习次数
douyin_views: 抖音播放量
kuaishou_views: 快手播放量
bilibili_views: B站播放量
@ -91,71 +92,55 @@ class LivingHeritageB21Calculator:
returns:
float: 教学传播频次评分
"""
# 线下传习次数权重计算
offline_score = offline_sessions * 0.6
# 线上课程点击量折算
# 抖音/快手按100:1折算
douyin_kuaishou_learning_sessions = (douyin_views + kuaishou_views) / 100
# B站按50:1折算
bilibili_learning_sessions = bilibili_views / 50
online_views_in_ten_thousands = (douyin_kuaishou_learning_sessions + bilibili_learning_sessions) / 10000
online_score = online_views_in_ten_thousands * 0.4
teaching_frequency_score = offline_score + online_score
return teaching_frequency_score
def calculate_cross_border_depth(self, cross_border_projects: int) -> float:
"""
计算跨界合作深度
# 线上总学习人次(万)
online_learning_sessions_10k = (douyin_kuaishou_learning_sessions + bilibili_learning_sessions) / 10000
每参与1个跨界合作项目+1最高10分
# 线上课程权重计算
online_score = online_learning_sessions_10k * 0.4
# 总教学传播频次
teaching_frequency = offline_score + online_score
return teaching_frequency
def cross_border_depth_dict(border_depth: str) -> float:
cross_border_depth_scores = {
"品牌联名": 0.3,
"科技载体": 0.5,
"国家外交礼品": 1,
}
return cross_border_depth_scores.get(border_depth, 0.3)
args:
cross_border_projects: 跨界合作项目数
returns:
float: 跨界合作深度评分
"""
return min(cross_border_projects, 10.0)
# 示例使用
if __name__ == "__main__":
calculator = LivingHeritageB21Calculator()
# 示例数据
inheritor_level = "国家级传承人" # 传承人等级 (用户填写)
cross_border_depth = 50.0
# 教学传播频次数据
offline_sessions = 20 # 线下传习次数符合标准传承人主导、时长≥2小时、参与人数≥5人
douyin_views = 10000000 # 抖音播放量
kuaishou_views = 0 # 快手播放量
bilibili_views = 0 # B站播放量
async def calculate_complete_living_heritage_b21(self, valuation_id: int, input_data: dict) -> float:
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.21,
step_name="活态传承系数B21计算",
step_description="开始计算活态传承系数B21",
input_params=input_data,
status="in_progress"
)
)
try:
inheritor_level_coefficient = self.calculate_inheritor_level_coefficient(input_data['inheritor_level'])
teaching_frequency = self.calculate_teaching_frequency(input_data['offline_sessions'], input_data.get('douyin_views', 0), input_data.get('kuaishou_views', 0), input_data.get('bilibili_views', 0))
cross_border_depth = self.calculate_cross_border_depth(input_data['cross_border_projects'])
# 计算各项指标
inheritor_level_coefficient = calculator.calculate_inheritor_level_coefficient(inheritor_level)
teaching_frequency = calculator.calculate_teaching_frequency(
offline_sessions=offline_sessions,
douyin_views=douyin_views,
kuaishou_views=kuaishou_views,
bilibili_views=bilibili_views
)
print(teaching_frequency)
living_heritage_b21 = self.calculate_living_heritage_b21(inheritor_level_coefficient, teaching_frequency, cross_border_depth)
# 计算活态传承系数B21
living_heritage_b21 = calculator.calculate_living_heritage_b21(
1, teaching_frequency, 0.3
)
print(f"传承人等级系数: {inheritor_level_coefficient:.2f}")
print(f"教学传播频次: {teaching_frequency:.2f}")
print(f" - 线下传习次数: {offline_sessions}")
print(f" - 抖音播放量: {douyin_views:,}")
print(f" - 快手播放量: {kuaishou_views:,}")
print(f" - B站播放量: {bilibili_views:,}")
print(f"跨界合作深度: {cross_border_depth:.2f}")
print(f"活态传承系数B21: {living_heritage_b21:.4f}")
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"living_heritage_b21": living_heritage_b21}}
)
return living_heritage_b21
except Exception as e:
error_message = f"活态传承系数B21计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
raise

View File

@ -1,21 +1,26 @@
"""
纹样基因值B22计算模块
纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10
"""
import sys
import os
import math
from typing import Dict, List
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
try:
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError:
# 处理可能的导入错误
pass
class PatternGeneB22Calculator:
"""纹样基因值B22计算器"""
def __init__(self):
"""初始化计算器"""
pass
self.valuation_controller = ValuationController()
def calculate_pattern_gene_b22(self,
structure_complexity: float,
@ -24,7 +29,6 @@ class PatternGeneB22Calculator:
"""
计算纹样基因值B22
纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10
args:
@ -35,11 +39,9 @@ class PatternGeneB22Calculator:
return:
float: 纹样基因值B22
"""
pattern_gene = ((structure_complexity * 0.6 +
normalized_entropy * 0.4) *
historical_inheritance * 10)
return pattern_gene
def calculate_structure_complexity(self, pattern_elements: List[Dict]) -> float:
@ -87,57 +89,49 @@ class PatternGeneB22Calculator:
if not pattern_data or len(pattern_data) <= 1:
return 0.0
# 计算概率分布
total = sum(pattern_data)
if total == 0:
return 0.0
probabilities = [x / total for x in pattern_data if x > 0]
# 计算信息熵
entropy = 0.0
for p in probabilities:
if p > 0:
entropy -= p * math.log2(p)
# 归一化
n = len(probabilities)
if n <= 1:
return 0.0
normalized_entropy = entropy / math.log2(n)
return normalized_entropy
async def calculate_complete_pattern_gene_b22(self, valuation_id: int, input_data: dict) -> float:
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.22,
step_name="纹样基因值B22计算",
step_description="开始计算纹样基因值B22",
input_params=input_data,
status="in_progress"
)
)
try:
structure_complexity = self.calculate_structure_complexity(input_data['pattern_elements'])
normalized_entropy = self.calculate_normalized_entropy(input_data['entropy_data'])
historical_inheritance = input_data['historical_inheritance']
# 示例使用
if __name__ == "__main__":
pattern_gene_b22 = self.calculate_pattern_gene_b22(structure_complexity, normalized_entropy, historical_inheritance)
calculator = PatternGeneB22Calculator()
# 示例数据
pattern_elements = [
{'type': '几何图形', 'weight': 0.3, 'complexity': 0.7},
{'type': '植物纹样', 'weight': 0.4, 'complexity': 0.8},
{'type': '动物纹样', 'weight': 0.3, 'complexity': 0.6}
]
entropy_data = [0.3, 0.4, 0.3]
inheritance_years = 500 # 传承年数 (用户填写)
cultural_significance = "国家级" # 文化意义等级 (用户填写)
preservation_status = "良好" # 保护状况 (用户填写)
historical_inheritance = 100.0
# 计算各项指标
structure_complexity = calculator.calculate_structure_complexity(pattern_elements)
normalized_entropy = calculator.calculate_normalized_entropy(entropy_data)
# 计算纹样基因值B22
pattern_gene_b22 = calculator.calculate_pattern_gene_b22(
1.5, 9, historical_inheritance
)
print(f"结构复杂度SC: {structure_complexity:.4f}")
print(f"归一化信息熵H: {normalized_entropy:.4f}")
print(f"历史传承度HI: {historical_inheritance:.4f}")
print(f"纹样基因值B22: {pattern_gene_b22:.4f}")
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"pattern_gene_b22": pattern_gene_b22}}
)
return pattern_gene_b22
except Exception as e:
error_message = f"纹样基因值B22计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
raise

View File

@ -6,17 +6,11 @@
"""
from typing import Dict
from app.controllers.valuation import ValuationController
try:
# 相对导入(当作为包使用时)
from .sub_formulas.basic_value_b11 import BasicValueB11Calculator, calculate_popularity_score
from .sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator
from .sub_formulas.policy_multiplier_b13 import PolicyMultiplierB13Calculator
except ImportError:
# 绝对导入(当直接运行时)
from sub_formulas.basic_value_b11 import BasicValueB11Calculator
from sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator
from sub_formulas.policy_multiplier_b13 import PolicyMultiplierB13Calculator
from .sub_formulas.basic_value_b11 import BasicValueB11Calculator
from .sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator
from .sub_formulas.policy_multiplier_b13 import PolicyMultiplierB13Calculator
class EconomicValueB1Calculator:
@ -27,6 +21,7 @@ class EconomicValueB1Calculator:
self.basic_value_calculator = BasicValueB11Calculator()
self.traffic_factor_calculator = TrafficFactorB12Calculator()
self.policy_multiplier_calculator = PolicyMultiplierB13Calculator()
self.valuation_controller = ValuationController()
def calculate_economic_value_b1(self,
basic_value_b11: float,
@ -50,95 +45,251 @@ class EconomicValueB1Calculator:
return economic_value
def calculate_complete_economic_value_b1(self, input_data: Dict) -> Dict:
async def calculate_complete_economic_value_b1(self, valuation_id: int, input_data: Dict) -> Dict[str, float]:
"""
计算完整的经济价值B1包含所有子公式
args:
input_data: 输入数据字典包含所有必要的参数
returns:
Dict: 包含所有中间计算结果和最终结果的字典
计算完整的经济价值B1并记录所有计算步骤
此函数集成了基础价值B11流量因子B12和政策乘数B13的计算
通过调用相应的子计算器来完成每一步的计算结果都会被记录下来
以支持后续的审计和分析
Args:
valuation_id (int): 估值的唯一标识符用于关联所有计算步骤
input_data (Dict): 包含所有计算所需参数的字典例如
{
'three_year_income': [2000, 2400, 2600], # B11
'patent_score': 1, # B11
'search_index_s1': 4500.0, # B12
'policy_match_score': 10.0, # B13
...
}
Returns:
Dict[str, float]: 包含经济价值B1及各子公式结果的字典
Raises:
Exception: 在计算过程中发生的任何异常都会被捕获记录并重新抛出
"""
# 财务价值F 近三年年均收益列表 [1,2,3]
financial_value = self.basic_value_calculator.calculate_financial_value_f(input_data["three_year_income"])
# 计算法律强度L patent_score: 专利分 (0-10分) (用户填写)
# popularity_score: 普及地域分 (0-10分) (用户填写)
# infringement_score: 侵权分 (0-10分) (用户填写)
legal_strength = self.basic_value_calculator.calculate_legal_strength_l(
input_data["patent_score"],
input_data["popularity_score"],
input_data["infringement_score"],
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B1",
status="processing",
input_params=input_data,
)
try:
financial_value = self.basic_value_calculator.calculate_financial_value_f(input_data["three_year_income"])
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B11_FINANCIAL_VALUE",
status="completed",
input_params={"three_year_income": input_data.get("three_year_income")},
output_result={"financial_value_f": financial_value},
)
# 发展潜力 patent_count: 专利分 (0-10分) (用户填写)
# esg_score: ESG分 (0-10分) (用户填写)
# innovation_ratio: 创新投入比 (研发费用/营收) * 100 (用户填写)
development_potential = self.basic_value_calculator.calculate_development_potential_d(
legal_strength = self.basic_value_calculator.calculate_legal_strength_l(
input_data["patent_score"],
input_data["popularity_score"],
input_data["infringement_score"],
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B11_LEGAL_STRENGTH",
status="completed",
input_params={
"patent_score": input_data.get("patent_score"),
"popularity_score": input_data.get("popularity_score"),
"infringement_score": input_data.get("infringement_score"),
},
output_result={"legal_strength_l": legal_strength},
)
input_data["patent_count"],
input_data["esg_score"],
input_data["innovation_ratio"],
)
# 计算行业系数I target_industry_roe: 目标行业平均ROE (系统配置)
# benchmark_industry_roe: 基准行业ROE (系统配置)
# industry_coefficient = self.basic_value_calculator.calculate_industry_coefficient_i(
#
# )
# 计算基础价值B11
basic_value_b11 = self.basic_value_calculator.calculate_basic_value_b11(
financial_value, # 财务价值F
legal_strength, # 法律强度L
development_potential,
input_data["industry_coefficient"]
)
development_potential = self.basic_value_calculator.calculate_development_potential_d(
input_data["patent_count"],
input_data["esg_score"],
input_data["innovation_ratio"],
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B11_DEVELOPMENT_POTENTIAL",
status="completed",
input_params={
"patent_count": input_data.get("patent_count"),
"esg_score": input_data.get("esg_score"),
"innovation_ratio": input_data.get("innovation_ratio"),
},
output_result={"development_potential_d": development_potential},
)
# 计算互动量指数
interaction_index = self.traffic_factor_calculator.calculate_interaction_index(
input_data["likes"],
input_data["comments"],
input_data["shares"],
)
# 计算覆盖人群指数
coverage_index = self.traffic_factor_calculator.calculate_coverage_index(0)
# 计算转化率
conversion_efficiency = self.traffic_factor_calculator.calculate_conversion_efficiency(
input_data["sales_volume"], input_data["link_views"])
industry_coefficient = input_data["industry_coefficient"]
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B11_INDUSTRY_COEFFICIENT",
status="completed",
input_params={"industry_coefficient": industry_coefficient},
output_result={"industry_coefficient": industry_coefficient},
)
social_media_spread_s3 = self.traffic_factor_calculator.calculate_social_media_spread_s3(interaction_index,
coverage_index,
conversion_efficiency)
basic_value_b11 = self.basic_value_calculator.calculate_basic_value_b11(
financial_value,
legal_strength,
development_potential,
industry_coefficient,
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B11",
status="completed",
input_params={
"financial_value_f": financial_value,
"legal_strength_l": legal_strength,
"development_potential_d": development_potential,
"industry_coefficient": industry_coefficient,
},
output_result={
"basic_value_b11": basic_value_b11,
"financial_value_f": financial_value,
"legal_strength_l": legal_strength,
"development_potential_d": development_potential,
"industry_coefficient": industry_coefficient,
},
)
traffic_factor_b12 = self.traffic_factor_calculator.calculate_traffic_factor_b12(
input_data['search_index_s1'],
input_data['industry_average_s2'],
social_media_spread_s3
)
interaction_index = self.traffic_factor_calculator.calculate_interaction_index(
input_data["likes"],
input_data["comments"],
input_data["shares"],
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_FLOW_B12_INTERACTION_INDEX",
status="completed",
input_params={
"likes": input_data.get("likes"),
"comments": input_data.get("comments"),
"shares": input_data.get("shares"),
},
output_result={"interaction_index": interaction_index},
)
# 计算政策乘数B13
policy_compatibility_score = self.policy_multiplier_calculator.calculate_policy_compatibility_score(
input_data["policy_match_score"],
input_data["implementation_stage"],
input_data["funding_support"])
policy_multiplier_b13 = self.policy_multiplier_calculator.calculate_policy_multiplier_b13(
policy_compatibility_score
)
coverage_index = self.traffic_factor_calculator.calculate_coverage_index(input_data.get("followers", 0))
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_FLOW_B12_COVERAGE_INDEX",
status="completed",
input_params={"followers": input_data.get("followers", 0)},
output_result={"coverage_index": coverage_index},
)
# 计算经济价值B1
economic_value_b1 = self.calculate_economic_value_b1(
basic_value_b11,
traffic_factor_b12,
policy_multiplier_b13
)
conversion_efficiency = self.traffic_factor_calculator.calculate_conversion_efficiency(
input_data["sales_volume"],
input_data["link_views"],
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_FLOW_B12_CONVERSION_EFFICIENCY",
status="completed",
input_params={
"sales_volume": input_data.get("sales_volume"),
"link_views": input_data.get("link_views"),
},
output_result={"conversion_efficiency": conversion_efficiency},
)
return {
'basic_value_b11': basic_value_b11,
'traffic_factor_b12': traffic_factor_b12,
'policy_multiplier_b13': policy_multiplier_b13,
'economic_value_b1': economic_value_b1
}
social_media_spread_s3 = self.traffic_factor_calculator.calculate_social_media_spread_s3(
interaction_index,
coverage_index,
conversion_efficiency,
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_FLOW_B12_SOCIAL_SPREAD",
status="completed",
input_params={
"interaction_index": interaction_index,
"coverage_index": coverage_index,
"conversion_efficiency": conversion_efficiency,
},
output_result={"social_media_spread_s3": social_media_spread_s3},
)
traffic_factor_b12 = self.traffic_factor_calculator.calculate_traffic_factor_b12(
input_data["search_index_s1"],
input_data["industry_average_s2"],
social_media_spread_s3,
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_FLOW_B12",
status="completed",
input_params={
"search_index_s1": input_data.get("search_index_s1"),
"industry_average_s2": input_data.get("industry_average_s2"),
},
output_result={
"traffic_factor_b12": traffic_factor_b12,
"social_media_spread_s3": social_media_spread_s3,
},
)
policy_compatibility = self.policy_multiplier_calculator.calculate_policy_compatibility_score(
input_data["policy_match_score"],
input_data["implementation_stage"],
input_data["funding_support"],
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_POLICY_B13",
status="processing",
input_params={
"policy_match_score": input_data.get("policy_match_score"),
"implementation_stage": input_data.get("implementation_stage"),
"funding_support": input_data.get("funding_support"),
},
output_result={"policy_compatibility_score": policy_compatibility},
)
policy_multiplier_b13 = self.policy_multiplier_calculator.calculate_policy_multiplier_b13(
policy_compatibility,
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_POLICY_B13",
status="completed",
output_result={
"policy_multiplier_b13": policy_multiplier_b13,
"policy_compatibility_score": policy_compatibility,
},
)
economic_value_b1 = self.calculate_economic_value_b1(
basic_value_b11,
traffic_factor_b12,
policy_multiplier_b13,
)
result = {
"economic_value_b1": economic_value_b1,
"basic_value_b11": basic_value_b11,
"traffic_factor_b12": traffic_factor_b12,
"policy_multiplier_b13": policy_multiplier_b13,
"financial_value_f": financial_value,
"legal_strength_l": legal_strength,
"development_potential_d": development_potential,
}
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B1",
status="completed",
output_result=result,
)
return result
except Exception as e:
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_ECON_B1",
status="failed",
error_message=str(e),
)
raise
# 示例使用
if __name__ == "__main__":

View File

@ -1,13 +1,25 @@
import math
from typing import List, Optional
import sys
import os
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
try:
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError:
# 处理可能的导入错误
pass
class BasicValueB11Calculator:
"""基础价值B11计算器"""
def __init__(self):
"""初始化计算器"""
pass
self.valuation_controller = ValuationController()
def calculate_basic_value_b11(self,
financial_value: float,
@ -94,7 +106,7 @@ class BasicValueB11Calculator:
# 使用两个增长率的平均值
avg_growth_rate = (growth_rate_1 + growth_rate_2) / 2
return avg_growth_rate
return max(avg_growth_rate, 0.0)
def calculate_legal_strength_l(self,
patent_score: float,
@ -168,94 +180,146 @@ class BasicValueB11Calculator:
return industry_coefficient
def _calculate_patent_score(self, patent_remaining_years: int) -> float:
"""
计算专利分
# 专利相关计算函数
def calculate_patent_score(patent_remaining_years: int) -> float:
"""
计算专利
专利剩余保护期评分标准
- >10: 10
- 5-10: 7
- <5: 3
专利剩余保护期评分标准
- >10: 10
- 5-10: 7
- <5: 3
args:
patent_remaining_years: 专利剩余保护期 (用户填写)
args:
patent_remaining_years: 专利剩余保护期 (用户填写)
returns:
float: 专利分
"""
if patent_remaining_years > 10:
return 10.0
elif patent_remaining_years >= 5:
return 7.0
else:
return 3.0
returns:
float: 专利分
"""
if patent_remaining_years > 10:
return 10.0
elif patent_remaining_years >= 5:
return 7.0
else:
return 3.0
def _calculate_patent_usage_score(self, patent_count: int) -> float:
"""
计算专利使用量分
专利使用量评分标准
- 未引用: 0
- 每引用一项: +2.5
- 10分封顶
# 识别用户所上传的图像中的专利号通过API验证专利是否存在按所用专利数量赋分未引用0分每引用一项+2.5分10分封顶0-10分
def calculate_patent_usage_score(patent_count: int) -> float:
"""
计算专利使用量分
args:
patent_count: 专利数量 (用户填写)
专利使用量评分标准
- 未引用: 0
- 每引用一项: +2.5
- 10分封顶
returns:
float: 专利使用量分
"""
score = min(patent_count * 2.5, 10.0)
return score
args:
patent_count: 专利数量 (用户填写)
def _calculate_popularity_score(self, region_coverage: str) -> float:
"""
计算普及地域分
returns:
float: 专利使用量分
"""
score = min(patent_count * 2.5, 10.0)
return score
全球覆盖(10)全国覆盖(7)区域覆盖(4)
args:
region_coverage: 普及地域类型 (用户填写)
# 普及地域评分
def calculate_popularity_score(region_coverage: str) -> float:
"""
计算普及地域分
returns:
float: 普及地域分
"""
coverage_scores = {
"全球覆盖": 10.0,
"全国覆盖": 7.0,
"区域覆盖": 4.0
}
全球覆盖(10)全国覆盖(7)区域覆盖(4)
return coverage_scores.get(region_coverage, 7.0)
args:
region_coverage: 普及地域类型 (用户填写)
def _calculate_infringement_score(self, infringement_status: str) -> float:
"""
计算侵权记录分
returns:
float: 普及地域分
"""
coverage_scores = {
"全球覆盖": 10.0,
"全国覆盖": 7.0,
"区域覆盖": 4.0
}
无侵权记录(10)历史侵权已解决(6)现存纠纷(2)
return coverage_scores.get(region_coverage, 7.0)
args:
infringement_status: 侵权记录状态 (用户填写)
returns:
float: 侵权记录分
"""
infringement_scores = {
"无侵权记录": 10.0,
"历史侵权已解决": 6.0,
"现存纠纷": 2.0
}
# 侵权记录评分
def calculate_infringement_score(infringement_status: str) -> float:
"""
计算侵权记录分
return infringement_scores.get(infringement_status, 6.0)
无侵权记录(10)历史侵权已解决(6)现存纠纷(2)
async def calculate_complete_basic_value_b11(self, valuation_id: int, input_data: dict) -> float:
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.11,
step_name="基础价值B11计算",
step_description="开始计算基础价值B11",
input_params=input_data,
status="in_progress"
)
)
try:
financial_value = self.calculate_financial_value_f(input_data['annual_revenue_3_years'])
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.111, step_name="财务价值F",
output_result={'financial_value': financial_value}, status="completed"
)
)
args:
infringement_status: 侵权记录状态 (用户填写)
patent_score = self._calculate_patent_score(input_data['patent_remaining_years'])
popularity_score = self._calculate_popularity_score(input_data['region_coverage'])
infringement_score = self._calculate_infringement_score(input_data['infringement_status'])
legal_strength = self.calculate_legal_strength_l(patent_score, popularity_score, infringement_score)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.112, step_name="法律强度L",
output_result={'legal_strength': legal_strength}, status="completed"
)
)
returns:
float: 侵权记录分
"""
infringement_scores = {
"无侵权记录": 10.0,
"历史侵权已解决": 6.0,
"现存纠纷": 2.0
}
patent_usage_score = self._calculate_patent_usage_score(input_data['patent_count'])
development_potential = self.calculate_development_potential_d(patent_usage_score, input_data['esg_score'], input_data['innovation_ratio'])
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.113, step_name="发展潜力D",
output_result={'development_potential': development_potential}, status="completed"
)
)
return infringement_scores.get(infringement_status, 6.0)
industry_coefficient = self.calculate_industry_coefficient_i(input_data['target_industry_roe'], input_data['benchmark_industry_roe'])
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.114, step_name="行业系数I",
output_result={'industry_coefficient': industry_coefficient}, status="completed"
)
)
basic_value_b11 = self.calculate_basic_value_b11(financial_value, legal_strength, development_potential, industry_coefficient)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"basic_value_b11": basic_value_b11}}
)
return basic_value_b11
except Exception as e:
error_message = f"基础价值B11计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
raise
# 示例使用
if __name__ == "__main__":
@ -315,4 +379,4 @@ if __name__ == "__main__":
# print(f"增长率: {growth_rate*100}%")
# print(f"(1+14%)^5 = {growth_factor:.4f}")
# print(f"2333 × {growth_factor:.4f} = {initial_value * growth_factor:.2f}")
# print(f"再除以5: {initial_value * growth_factor:.2f} ÷ 5 = {result:.2f}")
# print(f"再除以5: {initial_value * growth_factor:.2f} ÷ 5 = {result:.2f}")

View File

@ -1,15 +1,28 @@
import sys
import os
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
try:
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError:
# 处理可能的导入错误
pass
class PolicyMultiplierB13Calculator:
"""政策乘数B13计算器"""
def __init__(self):
"""初始化计算器"""
pass
self.valuation_controller = ValuationController()
def calculate_policy_multiplier_b13(self, policy_compatibility_score: float) -> float:
"""
计算政策乘数B13
政策乘数B13 = 1 + (政策契合度评分P × 0.15)
Args:
@ -18,9 +31,7 @@ class PolicyMultiplierB13Calculator:
returns:
float: 政策乘数B13
"""
#
policy_multiplier = 1 + (policy_compatibility_score * 0.15)
return policy_multiplier
def calculate_policy_compatibility_score(self,
@ -30,7 +41,6 @@ class PolicyMultiplierB13Calculator:
"""
计算政策契合度评分P
政策契合度P = 政策匹配度 × 0.4 + 实施阶段评分 × 0.3 + 资金支持度 × 0.3
Args:
@ -41,11 +51,9 @@ class PolicyMultiplierB13Calculator:
returns:
float: 政策契合度评分P
"""
#
policy_compatibility = (policy_match_score * 0.4 +
implementation_stage_score * 0.3 +
funding_support_score * 0.3)
return policy_compatibility
def calculate_policy_match_score(self, industry: str) -> float:
@ -60,8 +68,8 @@ class PolicyMultiplierB13Calculator:
returns:
float: 政策匹配度
"""
return 5
# 此处应有更复杂的逻辑根据行业匹配政策,暂时返回固定值
return 5.0
def calculate_implementation_stage_score(self, implementation_stage: str) -> float:
"""
@ -80,8 +88,7 @@ class PolicyMultiplierB13Calculator:
"推广阶段": 7.0,
"试点阶段": 4.0
}
return stage_scores.get(implementation_stage, 10.0)
return stage_scores.get(implementation_stage, 7.0)
def calculate_funding_support_score(self, funding_support: str) -> float:
"""
@ -100,8 +107,44 @@ class PolicyMultiplierB13Calculator:
"省级资助": 7.0,
"无资助": 0.0
}
return funding_scores.get(funding_support, 7.0)
return funding_scores.get(funding_support, 0.0)
async def calculate_complete_policy_multiplier_b13(self, valuation_id: int, input_data: dict) -> float:
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.13,
step_name="政策乘数B13计算",
step_description="开始计算政策乘数B13",
input_params=input_data,
status="in_progress"
)
)
try:
policy_match_score = self.calculate_policy_match_score(input_data['industry'])
implementation_stage_score = self.calculate_implementation_stage_score(input_data['implementation_stage'])
funding_support_score = self.calculate_funding_support_score(input_data['funding_support'])
policy_compatibility_score = self.calculate_policy_compatibility_score(policy_match_score, implementation_stage_score, funding_support_score)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.131, step_name="政策契合度评分P",
output_result={'policy_compatibility_score': policy_compatibility_score}, status="completed"
)
)
policy_multiplier_b13 = self.calculate_policy_multiplier_b13(policy_compatibility_score)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"policy_multiplier_b13": policy_multiplier_b13}}
)
return policy_multiplier_b13
except Exception as e:
error_message = f"政策乘数B13计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
raise
# 示例使用

View File

@ -1,14 +1,25 @@
import math
from typing import Dict, Tuple
import sys
import os
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
try:
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError:
# 处理可能的导入错误
pass
class TrafficFactorB12Calculator:
"""流量因子B12计算器"""
def __init__(self):
"""初始化计算器"""
pass
self.valuation_controller = ValuationController()
def calculate_traffic_factor_b12(self,
search_index_s1: float,
@ -31,18 +42,29 @@ class TrafficFactorB12Calculator:
if industry_average_s2 == 0:
raise ValueError("行业均值S2必须大于0")
if search_index_s1 == 0:
if search_index_s1 <= 0:
# 如果搜索指数为0或负数使用最小值避免对数计算错误
search_index_s1 = 1.0
# ,不进行任何拆分
traffic_factor = (math.log(search_index_s1 / industry_average_s2) * 0.3 +
social_media_spread_s3 * 0.7)
"""
为什么需要
- 经济价值 B1 的公式是 B1 = B11 × (1 + B12) × B13 app/utils/calculation_engine/economic_value_b1/economic_value_b1.py:34-45
- 如果 B12 < -1 (1 + B12) 会变成负数导致 B1 翻成负值并把模型估值 Bfinal_value_ab/model_value_b.py:48-50拉到巨负
- 通过设置 B12 -0.9 确保 (1 + B12) 0.1 即乘数始终为正且不至于过小
直观示例
- 原始计算得到 B12 = -1.8 例如 ln(S1/S2) 很大负社交传播度 S3 又很低 (1 + B12) = -0.8 会让 B1 变负
- 裁剪后 B12 = -0.9 (1 + B12) = 0.1 B1 保持为正避免最终估值出现大幅负值
"""
if traffic_factor < -0.9:
traffic_factor = -0.9
return traffic_factor
def calculate_social_media_spread_s3(self,
interaction_index: float,
coverage_index: float,
@ -60,7 +82,6 @@ class TrafficFactorB12Calculator:
returns:
float: 社交媒体传播度S3
"""
#
social_media_spread = (interaction_index * 0.4 +
coverage_index * 0.3 +
conversion_efficiency * 0.3)
@ -84,7 +105,6 @@ class TrafficFactorB12Calculator:
returns:
float: 互动量指数
"""
#
interaction_index = (likes + comments + shares) / 1000.0
return interaction_index
@ -101,11 +121,45 @@ class TrafficFactorB12Calculator:
returns:
float: 覆盖人群指数
"""
#
if followers == 0:
return 0
return 0.0
coverage_index = followers / 10000.0
return coverage_index
async def calculate_complete_traffic_factor_b12(self, valuation_id: int, input_data: dict) -> float:
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.12,
step_name="流量因子B12计算",
step_description="开始计算流量因子B12",
input_params=input_data,
status="in_progress"
)
)
try:
interaction_index = self.calculate_interaction_index(input_data['likes'], input_data['comments'], input_data['shares'])
coverage_index = self.calculate_coverage_index(input_data['followers'])
social_media_spread_s3 = self.calculate_social_media_spread_s3(interaction_index, coverage_index, input_data['conversion_efficiency'])
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.121, step_name="社交媒体传播度S3",
output_result={'social_media_spread_s3': social_media_spread_s3}, status="completed"
)
)
traffic_factor_b12 = self.calculate_traffic_factor_b12(input_data['search_index_s1'], input_data['industry_average_s2'], social_media_spread_s3)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"traffic_factor_b12": traffic_factor_b12}}
)
return traffic_factor_b12
except Exception as e:
error_message = f"流量因子B12计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
raise
def calculate_conversion_efficiency(self,
click_count: int,
@ -223,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
# 示例使用
@ -252,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
@ -285,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
@ -306,4 +351,4 @@ if __name__ == "__main__":
print(f"覆盖人群指数: {coverage_index:.4f}")
print(f"转化效率: {conversion_efficiency:.4f}")
print(f"社交媒体传播度S3: {social_media_spread_s3:.4f}")
print(f"流量因子B12: {traffic_factor:.4f}")
print(f"流量因子B12: {traffic_factor:.4f}")

View File

@ -19,10 +19,12 @@ try:
# 包内相对导入
from .model_value_b import ModelValueBCalculator
from ..market_value_c import MarketValueCCalculator
from app.controllers.valuation import ValuationController
except ImportError:
# 直接运行时的绝对导入
from app.utils.calculation_engine.final_value_ab.model_value_b import ModelValueBCalculator
from app.utils.calculation_engine.market_value_c import MarketValueCCalculator
from app.controllers.valuation import ValuationController
class FinalValueACalculator:
@ -32,6 +34,7 @@ class FinalValueACalculator:
"""初始化计算器"""
self.model_value_calculator = ModelValueBCalculator()
self.market_value_calculator = MarketValueCCalculator()
self.valuation_controller = ValuationController()
def calculate_final_value_a(self,
model_value_b: float,
@ -64,79 +67,100 @@ class FinalValueACalculator:
return final_value
async def calculate_complete_final_value_a(self, input_data: Dict) -> Dict:
async def calculate_complete_final_value_a(self, valuation_id: int, input_data: Dict) -> float:
"""
计算完整的最终估值A包含所有子模块
计算完整的最终估值A并记录每一步的计算过程
input_data: 输入数据字典包含所有必要的参数
该函数作为最终估值计算的入口协调调用模型估值B和市场估值C的计算
并将计算过程中的关键步骤如子模块的调用输入输出持久化
以便于后续的审计和追溯
包含所有中间计算结果和最终结果的字典
Args:
valuation_id (int): 本次估值的唯一标识符用于关联所有计算步骤
input_data (Dict): 包含所有计算所需参数的字典结构如下
{
'model_data': { ... }, # 模型估值B所需数据
'market_data': { ... } # 市场估值C所需数据
}
Returns:
float: 计算得出的最终估值A
Raises:
Exception: 在计算过程中遇到的任何异常都会被重新抛出
并在记录最后一步为计算失败后终止
"""
import time
start_time = time.time()
# 记录输入参数
logger.info("final_value_a.calculation_start input_data_keys={} model_data_keys={} market_data_keys={}",
list(input_data.keys()),
list(input_data.get('model_data', {}).keys()),
list(input_data.get('market_data', {}).keys()))
# 详细记录模型数据参数
model_data = input_data.get('model_data', {})
if 'economic_data' in model_data:
economic_data = model_data['economic_data']
logger.info("final_value_a.economic_data 经济价值B1参数: 近三年机构收益={} 专利分={} 普及地域分={} 侵权分={} 创新投入比={} ESG分={} 专利使用量={} 行业修正系数={}",
economic_data.get('three_year_income'),
economic_data.get('patent_score'),
economic_data.get('popularity_score'),
economic_data.get('infringement_score'),
economic_data.get('innovation_ratio'),
economic_data.get('esg_score'),
economic_data.get('patent_count'),
economic_data.get('industry_coefficient'))
if 'cultural_data' in model_data:
cultural_data = model_data['cultural_data']
logger.info("final_value_a.cultural_data 文化价值B2参数: 传承人等级系数={} 跨境深度={} 线下教学次数={} 抖音浏览量={} 快手浏览量={} 哔哩哔哩浏览量={} 结构复杂度={} 归一化信息熵={} 历史传承度={}",
cultural_data.get('inheritor_level_coefficient'),
cultural_data.get('cross_border_depth'),
cultural_data.get('offline_sessions'),
cultural_data.get('douyin_views'),
cultural_data.get('kuaishou_views'),
cultural_data.get('bilibili_views'),
cultural_data.get('structure_complexity'),
cultural_data.get('normalized_entropy'),
cultural_data.get('historical_inheritance'))
if 'risky_data' in model_data:
risky_data = model_data['risky_data']
logger.info("final_value_a.risky_data 风险调整B3参数: 最高价={} 最低价={} 诉讼状态={} 传承人年龄={}",
risky_data.get('highest_price'),
risky_data.get('lowest_price'),
risky_data.get('lawsuit_status'),
risky_data.get('inheritor_ages'))
# 详细记录市场数据参数
market_data = input_data.get('market_data', {})
logger.info("final_value_a.market_data 市场估值C参数: 平均交易价={} 手动出价={} 专家估值={} 日浏览量={} 收藏数量={} 发行等级={} 最近市场活动={}",
market_data.get('average_transaction_price'),
market_data.get('manual_bids'),
market_data.get('expert_valuations'),
market_data.get('daily_browse_volume'),
market_data.get('collection_count'),
market_data.get('issuance_level'),
market_data.get('recent_market_activity'))
# 计算模型估值B
logger.info("final_value_a.calculating_model_value_b 开始计算模型估值B")
model_start_time = time.time()
await self.valuation_controller.log_formula_step(
valuation_id,
"FINAL_A",
status="processing",
input_params=input_data,
)
try:
model_result = self.model_value_calculator.calculate_complete_model_value_b(
# 详细记录模型数据参数
model_data = input_data.get('model_data', {})
if 'economic_data' in model_data:
economic_data = model_data['economic_data']
logger.info("final_value_a.economic_data 经济价值B1参数: 近三年机构收益={} 专利分={} 普及地域分={} 侵权分={} 创新投入比={} ESG分={} 专利使用量={} 行业修正系数={}",
economic_data.get('three_year_income'),
economic_data.get('patent_score'),
economic_data.get('popularity_score'),
economic_data.get('infringement_score'),
economic_data.get('innovation_ratio'),
economic_data.get('esg_score'),
economic_data.get('patent_count'),
economic_data.get('industry_coefficient'))
if 'cultural_data' in model_data:
cultural_data = model_data['cultural_data']
logger.info("final_value_a.cultural_data 文化价值B2参数: 传承人等级系数={} 跨境深度={} 线下教学次数={} 抖音浏览量={} 快手浏览量={} 哔哩哔哩浏览量={} 结构复杂度={} 归一化信息熵={} 历史传承度={}",
cultural_data.get('inheritor_level_coefficient'),
cultural_data.get('cross_border_depth'),
cultural_data.get('offline_sessions'),
cultural_data.get('douyin_views'),
cultural_data.get('kuaishou_views'),
cultural_data.get('bilibili_views'),
cultural_data.get('structure_complexity'),
cultural_data.get('normalized_entropy'),
cultural_data.get('historical_inheritance'))
if 'risky_data' in model_data:
risky_data = model_data['risky_data']
logger.info("final_value_a.risky_data 风险调整B3参数: 最高价={} 最低价={} 诉讼状态={} 传承人年龄={}",
risky_data.get('highest_price'),
risky_data.get('lowest_price'),
risky_data.get('lawsuit_status'),
risky_data.get('inheritor_ages'))
# 详细记录市场数据参数
market_data = input_data.get('market_data', {})
logger.info("final_value_a.market_data 市场估值C参数: 平均交易价={} 手动出价={} 专家估值={} 日浏览量={} 收藏数量={} 发行等级={} 最近市场活动={}",
market_data.get('average_transaction_price'),
market_data.get('manual_bids'),
market_data.get('expert_valuations'),
market_data.get('daily_browse_volume'),
market_data.get('collection_count'),
market_data.get('issuance_level'),
market_data.get('recent_market_activity'))
# 计算模型估值B
logger.info("final_value_a.calculating_model_value_b 开始计算模型估值B")
model_start_time = time.time()
model_result_raw = await self.model_value_calculator.calculate_complete_model_value_b(
valuation_id,
input_data['model_data']
)
model_value_b = model_result['model_value_b']
model_result = model_result_raw if isinstance(model_result_raw, dict) else {"model_value_b": model_result_raw}
model_value_b = model_result.get('model_value_b')
model_duration = time.time() - model_start_time
logger.info("final_value_a.model_value_b_calculated 模型估值B计算完成: 模型估值B={}万元 耗时={}ms 返回字段={}",
@ -144,35 +168,27 @@ class FinalValueACalculator:
int(model_duration * 1000),
list(model_result.keys()))
except Exception as e:
logger.error("final_value_a.model_value_b_calculation_failed 模型估值B计算失败: 错误={} 输入数据={}", str(e), input_data.get('model_data', {}))
raise
# 计算市场估值C
logger.info("final_value_a.calculating_market_value_c 开始计算市场估值C")
market_start_time = time.time()
try:
market_result = await self.market_value_calculator.calculate_complete_market_value_c(
# 计算市场估值C
logger.info("final_value_a.calculating_market_value_c 开始计算市场估值C")
market_start_time = time.time()
market_result_raw = await self.market_value_calculator.calculate_complete_market_value_c(
valuation_id,
input_data['market_data']
)
market_value_c = market_result['market_value_c']
market_result = market_result_raw if isinstance(market_result_raw, dict) else {"market_value_c": market_result_raw}
market_value_c = market_result.get('market_value_c')
market_duration = time.time() - market_start_time
logger.info("final_value_a.market_value_c_calculated 市场估值C计算完成: 市场估值C={}万元 耗时={}ms 返回字段={}",
logger.info("final_value_a.market_value_c_calculated 市场估值C计算完成: 市场估值C={}万元 耗时={}ms 请求字段={}",
market_value_c,
int(market_duration * 1000),
list(market_result.keys()))
input_data['market_data'])
# 计算最终估值A
logger.info("final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B={}万元 市场估值C={}万元",
model_value_b, market_value_c)
except Exception as e:
logger.error("final_value_a.market_value_c_calculation_failed 市场估值C计算失败: 错误={} 输入数据={}", str(e), input_data.get('market_data', {}))
raise
# 计算最终估值A
logger.info("final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B={}万元 市场估值C={}万元",
model_value_b, market_value_c)
try:
final_value_a = self.calculate_final_value_a(
model_value_b,
market_value_c
@ -188,16 +204,35 @@ class FinalValueACalculator:
int(model_duration * 1000),
int(market_duration * 1000))
except Exception as e:
logger.error("final_value_a.final_value_calculation_failed 最终估值A计算失败: 错误={} 模型估值B={}万元 市场估值C={}万元",
str(e), model_value_b, market_value_c)
raise
await self.valuation_controller.log_formula_step(
valuation_id,
"FINAL_A",
status="completed",
output_result={
"model_value_b": model_value_b,
"market_value_c": market_value_c,
"final_value_ab": final_value_a,
"model_duration_ms": int(model_duration * 1000),
"market_duration_ms": int(market_duration * 1000),
"total_duration_ms": int(total_duration * 1000),
},
)
return {
"model_value_b": model_value_b,
"market_value_c": market_value_c,
"final_value_ab": final_value_a,
}
return {
'model_value_b': model_value_b,
'market_value_c': market_value_c,
'final_value_ab': final_value_a,
}
except Exception as e:
logger.error("final_value_a.calculation_failed 计算失败: 错误={}", str(e))
await self.valuation_controller.log_formula_step(
valuation_id,
"FINAL_A",
status="failed",
error_message=str(e),
)
raise

View File

@ -12,10 +12,12 @@ try:
# 相对导入(当作为包使用时)
from ..economic_value_b1.economic_value_b1 import EconomicValueB1Calculator
from ..cultural_value_b2.cultural_value_b2 import CulturalValueB2Calculator
from app.controllers.valuation import ValuationController
except ImportError:
# 绝对导入(当直接运行时)
from app.utils.calculation_engine.economic_value_b1.economic_value_b1 import EconomicValueB1Calculator
from app.utils.calculation_engine.cultural_value_b2.cultural_value_b2 import CulturalValueB2Calculator
from app.controllers.valuation import ValuationController
class ModelValueBCalculator:
@ -26,6 +28,7 @@ class ModelValueBCalculator:
self.economic_value_calculator = EconomicValueB1Calculator()
self.cultural_value_calculator = CulturalValueB2Calculator()
self.risk_adjustment_calculator = RiskAdjustmentB3Calculator()
self.valuation_controller = ValuationController()
def calculate_model_value_b(self,
economic_value_b1: float,
@ -46,45 +49,104 @@ class ModelValueBCalculator:
return model_value
def calculate_complete_model_value_b(self, input_data: Dict) -> Dict:
async def calculate_complete_model_value_b(self, valuation_id: int, input_data: Dict) -> Dict[str, float]:
"""
计算完整的模型估值B包含所有子公式
计算完整的模型估值B并记录详细的计算步骤
此函数通过依次调用经济价值B1文化价值B2和风险调整系数B3的计算器
完成模型估值B的全面计算每一步的计算包括子模块的调用输入输出
都会被记录下来用于后续的分析和审计
Args:
input_data: 输入数据字典包含所有必要的参数
valuation_id (int): 估值的唯一标识符用于关联所有计算步骤
input_data (Dict): 包含所有计算所需参数的字典结构应包含
{
'economic_data': { ... }, # 经济价值B1所需数据
'cultural_data': { ... }, # 文化价值B2所需数据
'risky_data': { ... } # 风险调整系数B3所需数据
}
Returns:
Dict: 包含所有中间计算结果和最终结果的字典
Dict[str, float]: 包含中间结果和最终模型估值B的字典
Raises:
Exception: 在计算过程中遇到的任何异常都会被捕获记录然后重新抛出
"""
# 计算经济价值B1
economic_result = self.economic_value_calculator.calculate_complete_economic_value_b1(
input_data['economic_data']
)
economic_value_b1 = economic_result['economic_value_b1']
# 计算文化价值B2
cultural_result = self.cultural_value_calculator.calculate_complete_cultural_value_b2(
input_data['cultural_data']
)
cultural_value_b2 = cultural_result['cultural_value_b2']
risk_value_result = self.risk_adjustment_calculator.calculate_complete_risky_value_b3(
input_data['risky_data']
)
risk_value_b3 = risk_value_result['risk_adjustment_b3']
# 计算模型估值B
model_value_b = self.calculate_model_value_b(
economic_value_b1,
cultural_value_b2,
risk_value_b3
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B",
status="processing",
input_params=input_data,
)
return {
'economic_value_b1': economic_value_b1,
'cultural_value_b2': cultural_value_b2,
'risk_value_b3': risk_value_b3,
'model_value_b': model_value_b,
}
current_stage = "初始化模型估值B参数"
try:
if not isinstance(input_data, dict):
raise TypeError(f"model_data必须为字典当前类型为{type(input_data).__name__}")
required_sections = ("economic_data", "cultural_data", "risky_data")
missing_sections = [
section for section in required_sections
if not isinstance(input_data.get(section), dict)
]
if missing_sections:
raise ValueError(f"model_data缺少必要字段: {', '.join(missing_sections)}")
# 计算经济价值B1传入估值ID并等待异步完成
current_stage = "经济价值B1计算"
economic_result = await self.economic_value_calculator.calculate_complete_economic_value_b1(
valuation_id,
input_data['economic_data']
)
economic_value_b1 = economic_result["economic_value_b1"]
# 计算文化价值B2传入估值ID并等待异步完成
current_stage = "文化价值B2计算"
cultural_result = await self.cultural_value_calculator.calculate_complete_cultural_value_b2(
valuation_id,
input_data['cultural_data']
)
cultural_value_b2 = cultural_result["cultural_value_b2"]
# 计算风险调整系数B3传入估值ID并等待异步完成
current_stage = "风险调整系数B3计算"
risk_result = await self.risk_adjustment_calculator.calculate_complete_risky_value_b3(
valuation_id,
input_data['risky_data']
)
risk_value_b3 = risk_result["risk_value_b3"]
# 计算模型估值B
current_stage = "模型估值B汇总"
model_value_b = self.calculate_model_value_b(
economic_value_b1,
cultural_value_b2,
risk_value_b3
)
result = {
"economic_value_b1": economic_value_b1,
"cultural_value_b2": cultural_value_b2,
"risk_value_b3": risk_value_b3,
"model_value_b": model_value_b,
"economic_details": economic_result,
"cultural_details": cultural_result,
"risk_details": risk_result,
}
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B",
status="completed",
output_result=result,
)
return result
except Exception as e:
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B",
status="failed",
error_message=f"{current_stage}失败: {e}",
)
raise
# 示例使用

View File

@ -0,0 +1,358 @@
"""
公式元数据注册表
用于将计算引擎中的每个公式节点含子公式映射到唯一的 code名称公式说明以及排序
以便在 valuation_calculation_steps 表中进行结构化记录并最终生成可读的计算报告
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import Dict, List, Optional
@dataclass(frozen=True)
class FormulaMeta:
code: str
name: str
formula: str
order: Decimal
parent_code: Optional[str]
group_code: str
FormulaTreeNode = Dict[str, object]
def _node(
code: str,
name: str,
formula: str,
order: str,
children: Optional[List[FormulaTreeNode]] = None,
group: Optional[str] = None,
) -> FormulaTreeNode:
return {
"code": code,
"name": name,
"formula": formula,
"order": order,
"group_code": group,
"children": children or [],
}
FORMULA_TREE: List[FormulaTreeNode] = [
_node(
"FINAL_A",
"最终估值A",
"最终估值A = 模型估值B × 0.7 + 市场估值C × 0.3",
"10",
children=[
_node(
"MODEL_B",
"模型估值B",
"模型估值B = 经济价值B1 × 0.7 + 文化价值B2 × 0.3 × 风险调整系数B3",
"20",
group="MODEL_B",
children=[
_node(
"MODEL_B_ECON_B1",
"经济价值B1",
"经济价值B1 = 基础价值B11 × (1 + 流量因子B12) × 政策乘数B13",
"21",
children=[
_node(
"MODEL_B_ECON_B11",
"基础价值B11",
"基础价值B11 = 财务价值F × (0.45 + 0.05 × 行业系数I) + 法律强度L × (0.35 + 0.05 × 行业系数I) + 发展潜力D × 0.2",
"21.1",
children=[
_node(
"MODEL_B_ECON_B11_FINANCIAL_VALUE",
"财务价值F",
"财务价值F = [3年内年均收益 × (1 + 增长率)^5] ÷ 5",
"21.11",
),
_node(
"MODEL_B_ECON_B11_LEGAL_STRENGTH",
"法律强度L",
"法律强度L = 专利分 × 0.4 + 普及分 × 0.3 + 侵权分 × 0.3",
"21.12",
),
_node(
"MODEL_B_ECON_B11_DEVELOPMENT_POTENTIAL",
"发展潜力D",
"发展潜力D = 专利分 × 0.5 + ESG分 × 0.2 + 创新投入比 × 0.3",
"21.13",
),
_node(
"MODEL_B_ECON_B11_INDUSTRY_COEFFICIENT",
"行业系数I",
"行业系数I = (目标行业平均ROE - 基准行业ROE) ÷ 基准行业ROE",
"21.14",
),
],
),
_node(
"MODEL_B_FLOW_B12",
"流量因子B12",
"流量因子B12 = ln(S1 ÷ S2) × 0.3 + 社交媒体传播度S3 × 0.7",
"21.2",
children=[
_node(
"MODEL_B_FLOW_B12_INTERACTION_INDEX",
"互动量指数",
"互动量指数 = (点赞 + 评论 + 分享) ÷ 1000",
"21.21",
),
_node(
"MODEL_B_FLOW_B12_COVERAGE_INDEX",
"覆盖人群指数",
"覆盖人群指数 = 粉丝数 ÷ 10000",
"21.22",
),
_node(
"MODEL_B_FLOW_B12_CONVERSION_EFFICIENCY",
"转化效率",
"转化效率 = 商品链接点击量 ÷ 内容浏览量",
"21.23",
),
_node(
"MODEL_B_FLOW_B12_SOCIAL_SPREAD",
"社交媒体传播度S3",
"社交媒体传播度S3 = 互动量指数 × 0.4 + 覆盖人群指数 × 0.3 + 转化效率 × 0.3",
"21.24",
),
],
),
_node(
"MODEL_B_POLICY_B13",
"政策乘数B13",
"政策乘数B13 = 1 + 政策契合度评分P × 0.15,其中 P = 政策匹配度 × 0.4 + 实施阶段评分 × 0.3 + 资金支持度 × 0.3",
"21.3",
),
],
),
_node(
"MODEL_B_CULTURAL_B2",
"文化价值B2",
"文化价值B2 = 活态传承系数B21 × 0.6 + (纹样基因值B22 ÷ 10) × 0.4",
"22",
children=[
_node(
"MODEL_B_CULTURAL_B21",
"活态传承系数B21",
"活态传承系数B21 = 传承人等级系数 × 0.4 + 教学传播频次 × 0.3 + 跨界合作深度 × 0.3",
"22.1",
children=[
_node(
"MODEL_B_CULTURAL_B21_TEACHING_FREQ",
"教学传播频次",
"教学传播频次 = 线下传习次数 × 0.6 + 线上课程点击量(万) × 0.4",
"22.11",
),
],
),
_node(
"MODEL_B_CULTURAL_B22",
"纹样基因值B22",
"纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10",
"22.2",
),
],
),
_node(
"MODEL_B_RISK_B3",
"风险调整系数B3",
"风险调整系数B3 = 0.8 + 风险评分总和R × 0.4,其中 R = 市场风险 × 0.3 + 法律风险 × 0.4 + 传承风险 × 0.3",
"23",
children=[
_node(
"MODEL_B_RISK_B3_MARKET",
"市场风险",
"市场风险依据价格波动率:波动率 ≤5% 计10分5-15%计5分>15%计0分",
"23.1",
),
_node(
"MODEL_B_RISK_B3_LEGAL",
"法律风险",
"法律风险根据诉讼状态评分(无诉讼/已解决/未解决)",
"23.2",
),
_node(
"MODEL_B_RISK_B3_INHERITANCE",
"传承风险",
"传承风险依据传承人年龄≤50岁10分50-70岁5分>70岁0分取最高分",
"23.3",
),
],
),
],
),
_node(
"MARKET_C",
"市场估值C",
"市场估值C = 市场竞价C1 × 热度系数C2 × 稀缺性乘数C3 × 时效性衰减C4",
"30",
group="MARKET_C",
children=[
_node(
"MARKET_C_C1",
"市场竞价C1",
"市场竞价C1 结合历史交易价格、人工竞价与专家估值的加权结果",
"30.1",
),
_node(
"MARKET_C_C2",
"热度系数C2",
"热度系数C2 = 1 + 浏览热度分(依据日均浏览量与收藏数量)",
"30.2",
),
_node(
"MARKET_C_C3",
"稀缺性乘数C3",
"稀缺性乘数C3 = 1 + 稀缺等级分",
"30.3",
),
_node(
"MARKET_C_C4",
"时效性衰减C4",
"时效性衰减C4 依据距最近市场活动天数的衰减系数",
"30.4",
),
],
),
_node(
"DYNAMIC_PLEDGE_RATE",
"动态质押率DPR",
"动态质押率DPR = 基础质押率 × (1 + 流量修正系数) + 政策加成系数 - 流动性调节因子",
"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",
),
],
)
]
def _build_index() -> Dict[str, FormulaMeta]:
index: Dict[str, FormulaMeta] = {}
def dfs(nodes: List[FormulaTreeNode], parent_code: Optional[str], group_code: Optional[str]):
for node in nodes:
code = node["code"]
name = node["name"]
formula = node["formula"]
order = Decimal(str(node["order"]))
explicit_group = node.get("group_code")
if explicit_group:
current_group = explicit_group
elif parent_code is None:
current_group = code
else:
current_group = group_code or parent_code
meta = FormulaMeta(
code=code,
name=name,
formula=formula,
order=order,
parent_code=parent_code,
group_code=current_group,
)
index[code] = meta
dfs(node.get("children", []), code, current_group)
dfs(FORMULA_TREE, None, None)
return index
FORMULA_INDEX: Dict[str, FormulaMeta] = _build_index()
def get_formula_meta(code: str) -> FormulaMeta:
meta = FORMULA_INDEX.get(code)
if not meta:
raise KeyError(f"公式编码未注册: {code}")
return meta

View File

@ -8,6 +8,9 @@ import logging
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.append(current_dir)
# 添加项目根目录
sys.path.append(os.path.join(current_dir, '..', '..', '..'))
try:
# 相对导入(当作为包使用时)
@ -16,6 +19,7 @@ try:
from .sub_formulas.scarcity_multiplier_c3 import ScarcityMultiplierC3Calculator
from .sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator
from .market_data_analyzer import market_data_analyzer
from app.controllers.valuation import ValuationController
except ImportError:
# 绝对导入(当直接运行时)
from sub_formulas.market_bidding_c1 import MarketBiddingC1Calculator
@ -23,6 +27,7 @@ except ImportError:
from sub_formulas.scarcity_multiplier_c3 import ScarcityMultiplierC3Calculator
from sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator
from market_data_analyzer import market_data_analyzer
from app.controllers.valuation import ValuationController
logger = logging.getLogger(__name__)
@ -36,6 +41,7 @@ class MarketValueCCalculator:
self.heat_coefficient_calculator = HeatCoefficientC2Calculator()
self.scarcity_multiplier_calculator = ScarcityMultiplierC3Calculator()
self.temporal_decay_calculator = TemporalDecayC4Calculator()
self.valuation_controller = ValuationController()
async def _get_dynamic_default_price(self, input_data: Dict) -> float:
"""
@ -93,64 +99,131 @@ class MarketValueCCalculator:
market_value = (market_bidding_c1 * heat_coefficient_c2 *
scarcity_multiplier_c3 * temporal_decay_c4)
return market_value
return market_value / 10000.0
async def calculate_complete_market_value_c(self, input_data: Dict) -> Dict:
async def calculate_complete_market_value_c(self, valuation_id: int, input_data: Dict) -> Dict[str, float]:
"""
计算完整的市场估值C包含所有子公式
args:
input_data: 输入数据字典包含所有必要的参数
参数来源标记用户填写/系统配置/API获取/系统计算
- average_transaction_price: 系统计算(基于用户填写/API获取)
- market_activity_coefficient: 系统计算(基于用户填写)
- daily_browse_volume: API获取/系统估算
- collection_count: API获取/系统估算
- issuance_level: 用户填写
- recent_market_activity: 用户填写
- issuance_scarcity/circulation_scarcity/uniqueness_scarcity: 系统配置/系统计算保留向后兼容
return:
Dict: 包含所有中间计算结果和最终结果的字典
"""
# 计算市场竞价C1
market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1(
transaction_data={'weighted_average_price': input_data.get('weighted_average_price', 0)},
manual_bids=input_data.get('manual_bids', []),
expert_valuations=input_data.get('expert_valuations', [])
)
计算完整的市场估值C并记录每一步的计算过程
# 计算热度系数C2
heat_coefficient_c2 = self.heat_coefficient_calculator.calculate_heat_coefficient_c2(
input_data.get('daily_browse_volume', 500.0),
input_data.get('collection_count', 50)
该函数通过顺序调用市场竞价C1热度系数C2稀缺性乘数C3和时效性衰减C4的计算器
最终得出市场估值C计算过程中的每个子步骤都会被详细记录以便于审计和跟踪
Args:
valuation_id (int): 估值的唯一标识符用于关联所有计算步骤
input_data (Dict): 包含所有计算所需参数的字典例如
{
'weighted_average_price': 50000.0, # C1
'manual_bids': [48000.0, 52000.0], # C1
'expert_valuations': [49000.0, 51000.0], # C1
'daily_browse_volume': 500.0, # C2
'collection_count': 50, # C2
'issuance_level': '限量', # C3
'recent_market_activity': '2024-01-15' # C4
}
Returns:
Dict[str, float]: 包含市场估值C及子公式结果的字典
Raises:
Exception: 如果在计算过程中发生任何错误将记录失败状态并重新抛出异常
"""
await self.valuation_controller.log_formula_step(
valuation_id,
"MARKET_C",
status="processing",
input_params=input_data,
)
# 计算稀缺性乘数C3
scarcity_multiplier_c3 = self.scarcity_multiplier_calculator.calculate_scarcity_multiplier_c3(
input_data.get('issuance_level', '限量')
)
# 计算时效性衰减C4
temporal_decay_c4 = self.temporal_decay_calculator.calculate_temporal_decay_c4(
input_data.get('recent_market_activity', '2024-01-15')
)
# 计算市场估值C
market_value_c = self.calculate_market_value_c(
market_bidding_c1,
heat_coefficient_c2,
scarcity_multiplier_c3,
temporal_decay_c4
)
return {
'market_bidding_c1': market_bidding_c1,
'heat_coefficient_c2': heat_coefficient_c2,
'scarcity_multiplier_c3': scarcity_multiplier_c3,
'temporal_decay_c4': temporal_decay_c4,
'market_value_c': market_value_c
}
try:
# 计算市场竞价C1
market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1(
transaction_data={'weighted_average_price': input_data.get('weighted_average_price', 0)},
manual_bids=input_data.get('manual_bids', []),
expert_valuations=input_data.get('expert_valuations', [])
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MARKET_C_C1",
status="completed",
input_params={
"weighted_average_price": input_data.get('weighted_average_price'),
"manual_bids": input_data.get('manual_bids'),
"expert_valuations": input_data.get('expert_valuations'),
},
output_result={'market_bidding_c1': market_bidding_c1},
)
# 计算热度系数C2
heat_coefficient_c2 = self.heat_coefficient_calculator.calculate_heat_coefficient_c2(
input_data.get('daily_browse_volume', 500.0),
input_data.get('collection_count', 50)
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MARKET_C_C2",
status="completed",
input_params={
"daily_browse_volume": input_data.get('daily_browse_volume'),
"collection_count": input_data.get('collection_count'),
},
output_result={'heat_coefficient_c2': heat_coefficient_c2},
)
# 计算稀缺性乘数C3
scarcity_multiplier_c3 = self.scarcity_multiplier_calculator.calculate_scarcity_multiplier_c3(
input_data.get('issuance_level', '限量')
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MARKET_C_C3",
status="completed",
input_params={'issuance_level': input_data.get('issuance_level')},
output_result={'scarcity_multiplier_c3': scarcity_multiplier_c3},
)
# 计算时效性衰减C4
temporal_decay_c4 = self.temporal_decay_calculator.calculate_temporal_decay_c4(
input_data.get('recent_market_activity', '2024-01-15')
)
await self.valuation_controller.log_formula_step(
valuation_id,
"MARKET_C_C4",
status="completed",
input_params={'recent_market_activity': input_data.get('recent_market_activity')},
output_result={'temporal_decay_c4': temporal_decay_c4},
)
# 计算市场估值C
market_value_c = self.calculate_market_value_c(
market_bidding_c1,
heat_coefficient_c2,
scarcity_multiplier_c3,
temporal_decay_c4
)
result = {
"market_value_c": market_value_c,
"market_bidding_c1": market_bidding_c1,
"heat_coefficient_c2": heat_coefficient_c2,
"scarcity_multiplier_c3": scarcity_multiplier_c3,
"temporal_decay_c4": temporal_decay_c4,
}
await self.valuation_controller.log_formula_step(
valuation_id,
"MARKET_C",
status="completed",
output_result=result,
)
return result
except Exception as e:
error_message = f"市场估值C计算失败: {e}"
logger.error(error_message, exc_info=True)
await self.valuation_controller.log_formula_step(
valuation_id,
"MARKET_C",
status="failed",
error_message=str(e),
)
raise
# 示例使用

View File

@ -6,13 +6,23 @@
"""
from typing import Dict, List
import sys
import os
# 添加项目根目录到Python路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
try:
from app.controllers.valuation import ValuationController
except ImportError:
pass
class RiskAdjustmentB3Calculator:
"""风险调整系数B3计算器"""
def __init__(self):
pass
self.valuation_controller = ValuationController()
def calculate_risk_adjustment_b3(self, risk_score_sum: float) -> float:
"""
@ -155,22 +165,116 @@ class RiskAdjustmentB3Calculator:
return max_score
def calculate_complete_risky_value_b3(self, input_data: Dict) -> Dict:
# 计算各项风险评分
market_risk = self.calculate_market_risk(input_data["highest_price"], input_data["lowest_price"])
legal_risk = self.calculate_legal_risk(input_data["lawsuit_status"])
inheritance_risk = self.calculate_inheritance_risk(input_data["inheritor_ages"])
async def calculate_complete_risky_value_b3(self, valuation_id: int, input_data: Dict) -> Dict[str, float]:
"""
计算完整的风险调整系数B3并记录所有计算步骤
# 计算风险评分总和R
risk_score_sum = self.calculate_risk_score_sum(market_risk, legal_risk, inheritance_risk)
该函数通过整合市场风险法律风险和传承风险的评估
计算出风险评分总和R并最终得出风险调整系数B3
每一步的计算过程都会被记录下来以确保计算的透明度和可追溯性
# 计算风险调整系数B3
risk_adjustment_b3 = self.calculate_risk_adjustment_b3(risk_score_sum)
return {
'risk_score_sum': risk_score_sum,
'risk_adjustment_b3': risk_adjustment_b3
}
Args:
valuation_id (int): 估值的唯一标识符用于关联所有计算步骤
input_data (Dict): 包含所有计算所需参数的字典例如
{
'highest_price': 340.0, # 市场风险
'lowest_price': 300.0, # 市场风险
'lawsuit_status': 10.0, # 法律风险
'inheritor_ages': [100, 20, 5], # 传承风险
...
}
Returns:
Dict[str, float]: 包含各项风险评分和风险调整系数的字典
Raises:
Exception: 在计算过程中遇到的任何异常都会被捕获记录并重新抛出
"""
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_RISK_B3",
status="processing",
input_params=input_data,
)
try:
# 计算各项风险评分
market_risk = self.calculate_market_risk(input_data["highest_price"], input_data["lowest_price"])
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_RISK_B3_MARKET",
status="completed",
input_params={
"highest_price": input_data.get("highest_price"),
"lowest_price": input_data.get("lowest_price"),
},
output_result={'market_risk': market_risk},
)
legal_risk = self.calculate_legal_risk(input_data["lawsuit_status"])
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_RISK_B3_LEGAL",
status="completed",
input_params={"lawsuit_status": input_data.get("lawsuit_status")},
output_result={'legal_risk': legal_risk},
)
inheritance_risk = self.calculate_inheritance_risk(input_data["inheritor_ages"])
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_RISK_B3_INHERITANCE",
status="completed",
input_params={
"inheritor_ages": input_data.get("inheritor_ages"),
"score_rule": "≤50岁:10分, 50-70岁:5分, >70岁:0分, 取最高分"
},
output_result={'inheritance_risk': inheritance_risk},
)
# 计算风险评分总和R
risk_score_sum = self.calculate_risk_score_sum(market_risk, legal_risk, inheritance_risk)
# 计算风险调整系数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,
"MODEL_B_RISK_B3",
status="completed",
output_result=result,
)
return result
except Exception as e:
await self.valuation_controller.log_formula_step(
valuation_id,
"MODEL_B_RISK_B3",
status="failed",
error_message=str(e),
)
raise
# 示例使用
if __name__ == "__main__":

File diff suppressed because it is too large Load Diff

View File

@ -1,384 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import json
import random
import time
# API基础URL
BASE_URL = "http://127.0.0.1:9999/api/v1"
# 测试数据
test_phone = f"1380000{random.randint(1000, 9999)}"
test_password = test_phone[-6:] # 默认密码是手机号后6位
access_token = None
user_id = None
valuation_id = None
def test_register():
"""测试用户注册功能"""
print("\n===== 测试用户注册 =====")
url = f"{BASE_URL}/app-user/register"
data = {
"phone": test_phone
}
response = requests.post(url, json=data)
print(f"请求URL: {url}")
print(f"请求数据: {data}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "注册请求失败"
result = response.json()
assert result["code"] == 200, "注册失败"
assert result["data"]["phone"] == test_phone, "返回的手机号不匹配"
assert result["data"]["default_password"] == test_phone[-6:], "默认密码不正确"
print("✅ 用户注册测试通过")
return result
def test_login():
"""测试用户登录功能"""
global access_token
print("\n===== 测试用户登录 =====")
url = f"{BASE_URL}/app-user/login"
data = {
"phone": test_phone,
"password": test_password
}
response = requests.post(url, json=data)
print(f"请求URL: {url}")
print(f"请求数据: {data}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "登录请求失败"
result = response.json()
assert "access_token" in result, "登录失败未返回token"
# 保存token供后续请求使用
access_token = result["access_token"]
print("✅ 用户登录测试通过")
return result
def test_create_valuation():
"""测试创建估值评估申请"""
global access_token
print("\n===== 测试创建估值评估申请 =====")
url = f"{BASE_URL}/app-valuations/"
# 准备请求头包含授权token
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
# 估值评估申请数据 - 根据估值字段.txt更新
data = {
# 02 - 基础信息 - 非遗IP资产的基本信息
"asset_name": f"蜀绣-{random.randint(1000, 9999)}", # 资产名称:必须是企业全名称
"institution": "有数", # 所属机构拥有或管理该非遗IP资产的机构名称
"industry": "文化艺术", # 所属行业非遗IP资产所属的行业分类
# 03 - 财务状况
"rd_investment": "5", # 近12个月的研发费用单位万元
"annual_revenue": "100", # 近12个月的营收额单位万元用于计算创新投入比
"three_year_income": [100, 120, 150], # 近三年的每年收益资产近3年的年收益数据单位万元用于计算年均收益和增长率
"funding_status": "国家级资助", # 资金支持: 国家级资助(10分)、省级资助(7分)、无资助(0分)
# 04 - 非遗等级与技术
"inheritor_level": "国家级", # 传承人等级
"inheritor_ages": [60, 42, 35], # 传承人年龄
"inheritor_certificates": ["http://example.com/国家级非遗传承人证书.jpg"], # 传承人证书:传承人资质证明材料
"heritage_asset_level": "国家级非遗", # 非遗资产等级
"patent_remaining_years": "8", # [实际上就是专利号 通过API查询到的 ]专利剩余年限资产相关专利的剩余保护期8年对应7分
"historical_evidence": { # 资产历史证据类型+数量:历史传承的证据材料
"artifacts": 1, # 出土实物数量
"ancient_literature": 2, # 古代文献数量
"inheritor_testimony": 3, # 传承人佐证数量
"modern_research": 1 # 现代研究数量
},
# 专利证书:
"patent_certificates": ["http://example.com/专利证书1.jpg", "http://example.com/专利证书2.jpg"],
"pattern_images": ["pattern1.jpg"], # 纹样图片:资产相关的纹样图片文件
# 04 - 非遗应用与推广
"implementation_stage": "成熟应用", # 非遗资产应用成熟度
"coverage_area": "区域覆盖", # 非遗资产应用覆盖范围
"collaboration_type": "品牌联名", # 非遗资产跨界合作深度
"offline_teaching_count": 12, # 近12个月线下相关演讲活动次数
"platform_accounts": { # 线上相关宣传账号信息
"bilibili": {
"followers_count": 8000, # 粉丝数量
"likes": 1000, # 点赞数
"comments": 500, # 评论数
"shares": 500 # 转发数
}, # B站账号
"douyin": {
"followers_count": 8000, # 粉丝数量
"likes": 1000, # 点赞数
"comments": 500, # 评论数
"shares": 500 # 转发数
} # 抖音账号
},
# 06 - 非遗资产衍生商品信息
#该商品近12个月销售量
"sales_volume": "1000", # 近12个月销售量资产衍生商品的近12个月销售量单位 链接购买量
# 该商品近12个月的链接浏览量
"link_views": "10000", # 近12个月链接浏览量资产衍生商品相关链接的近12个月浏览量单位 浏览量
"scarcity_level": "流通", # 稀缺等级:资产的稀缺程度,流通(发行量>1000份对应0.1分
"market_activity_time": "近一月", # 市场活动的时间
"monthly_transaction_amount": "<100万元", # 月交易额:资产衍生商品的月交易额水平,<100万元对应-0.1
"price_range": { # 资产商品的价格波动率近30天商品价格的波动情况
"highest": 239, # 最高价(单位:元)
"lowest": 189 # 最低价(单位:元)
},
"market_price": 0, # 直接提供的市场价格(单位:万元) 用户输入: 专家审核 或者 系统默认 专家审核
# 内置API 计算字段
"infringement_record": "无侵权记录", # 侵权记录资产的侵权历史情况无侵权记录对应10分
"patent_count": "1", # 专利使用量:资产相关的专利数量,每引用一项专利+2.5分
"esg_value": "10", # ESG关联价值根据行业匹配的ESG环境、社会、治理关联价值
"policy_matching": "10", # 政策匹配度:根据行业自动匹配的政策匹配度分值
"online_course_views": 2000, # 线上课程点击量:抖音/快手播放量按100:1折算为学习人次B站课程按50:1折算
"pattern_complexity": "1.459", # 结构复杂度:纹样的结构复杂度值 搞一个默认值: 0.0
"normalized_entropy": "9.01", # 归一化信息熵:纹样的归一化信息熵值 搞一个默认值: 0.0
"legal_risk": "无诉讼", # 法律风险-侵权诉讼历史资产所属机构的诉讼历史无诉讼对应10分
# 动态质押率DPR计算公式=基础质押率*(1+流量修正系数)+政策加成系数-流动性调节因子
"base_pledge_rate": "50%", # 基础质押率基础质押率固定值50%
"flow_correction": "0.3", # 流量修正系数固定值0.3
}
response = requests.post(url, headers=headers, json=data)
print(f"请求URL: {url}")
print(f"请求头: {headers}")
print(f"请求数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "创建估值评估申请请求失败"
result = response.json()
assert result["code"] == 200, "创建估值评估申请失败"
print("✅ 创建估值评估申请测试通过")
return result
def test_get_profile():
"""测试获取用户个人信息"""
global access_token, user_id
print("\n===== 测试获取用户个人信息 =====")
url = f"{BASE_URL}/app-user/profile"
headers = {
"Authorization": f"Bearer {access_token}"
}
response = requests.get(url, headers=headers)
print(f"请求URL: {url}")
print(f"请求头: {headers}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "获取用户信息请求失败"
result = response.json()
user_id = result["id"] # 保存用户ID供后续使用
print("✅ 获取用户个人信息测试通过")
return result
def test_change_password():
"""测试修改密码"""
global access_token
print("\n===== 测试修改密码 =====")
url = f"{BASE_URL}/app-user/change-password"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
new_password = "new" + test_password
data = {
"old_password": test_password,
"new_password": new_password
}
response = requests.post(url, headers=headers, json=data)
print(f"请求URL: {url}")
print(f"请求头: {headers}")
print(f"请求数据: {data}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "修改密码请求失败"
result = response.json()
assert result["code"] == 200, "修改密码失败"
print("✅ 修改密码测试通过")
return result
def test_update_profile():
"""测试更新用户信息"""
global access_token
print("\n===== 测试更新用户信息 =====")
url = f"{BASE_URL}/app-user/profile"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = {
"nickname": f"测试用户{random.randint(100, 999)}",
"avatar": "https://example.com/avatar.jpg",
"gender": "male",
"email": f"test{random.randint(100, 999)}@example.com"
}
response = requests.put(url, headers=headers, json=data)
print(f"请求URL: {url}")
print(f"请求头: {headers}")
print(f"请求数据: {data}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "更新用户信息请求失败"
result = response.json()
# 更新用户信息接口直接返回用户对象不包含code字段
assert "id" in result, "更新用户信息失败"
print("✅ 更新用户信息测试通过")
return result
def test_logout():
"""测试用户登出"""
global access_token
print("\n===== 测试用户登出 =====")
url = f"{BASE_URL}/app-user/logout"
headers = {
"Authorization": f"Bearer {access_token}"
}
response = requests.post(url, headers=headers)
print(f"请求URL: {url}")
print(f"请求头: {headers}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "登出请求失败"
result = response.json()
assert result["code"] == 200, "登出失败"
print("✅ 用户登出测试通过")
return result
def test_get_valuation_list():
"""测试获取用户估值列表"""
global access_token
print("\n===== 测试获取用户估值列表 =====")
url = f"{BASE_URL}/app-valuations/"
headers = {
"Authorization": f"Bearer {access_token}"
}
response = requests.get(url, headers=headers)
print(f"请求URL: {url}")
print(f"请求头: {headers}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "获取估值列表请求失败"
result = response.json()
assert result["code"] == 200, "获取估值列表失败"
print("✅ 获取用户估值列表测试通过")
return result
def test_get_valuation_detail():
"""测试获取估值详情"""
global access_token, valuation_id
# 先获取估值列表获取第一个估值ID
if not valuation_id:
list_result = test_get_valuation_list()
if list_result["data"] and len(list_result["data"]) > 0:
valuation_id = list_result["data"][0]["id"]
else:
print("⚠️ 没有可用的估值记录,跳过估值详情测试")
return None
print("\n===== 测试获取估值详情 =====")
url = f"{BASE_URL}/app-valuations/{valuation_id}"
headers = {
"Authorization": f"Bearer {access_token}"
}
response = requests.get(url, headers=headers)
print(f"请求URL: {url}")
print(f"请求头: {headers}")
print(f"响应状态码: {response.status_code}")
print(f"响应内容: {response.text}")
assert response.status_code == 200, "获取估值详情请求失败"
result = response.json()
assert result["code"] == 200, "获取估值详情失败"
print("✅ 获取估值详情测试通过")
return result
def run_tests():
"""运行所有测试"""
try:
# 测试注册
test_register()
# 等待一秒,确保数据已保存
time.sleep(1)
# 测试登录
test_login()
# 测试获取用户个人信息
test_get_profile()
# 测试更新用户信息
test_update_profile()
# 测试创建估值评估申请
test_create_valuation()
# 测试获取估值列表
test_get_valuation_list()
# 测试获取估值详情
test_get_valuation_detail()
# 测试修改密码
test_change_password()
# 测试登出
# test_logout()
print("\n===== 所有测试通过 =====")
except AssertionError as e:
print(f"\n❌ 测试失败: {e}")
except Exception as e:
print(f"\n❌ 发生错误: {e}")
if __name__ == "__main__":
run_tests()

View File

@ -1,5 +1,6 @@
#!/bin/sh
set -e
nginx
# nginx
python run.py

View File

@ -1,13 +1,31 @@
server {
listen 80;
server_name localhost;
location = /docs {
proxy_pass http://127.0.0.1:9999/docs;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location = /redoc {
proxy_pass http://127.0.0.1:9999/redoc;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location = /openapi.json {
proxy_pass http://127.0.0.1:9999/openapi.json;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /opt/vue-fastapi-admin/web/dist;
index index.html index.htm;
try_files $uri /index.html;
}
location ^~ /api/ {
proxy_pass http://127.0.0.1:9999;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

View File

@ -1,3 +0,0 @@
{
"type": "module"
}

34
package-lock.json generated
View File

@ -1,34 +0,0 @@
{
"name": "guzhi",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"echarts": "^6.0.0"
}
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"dependencies": {
"tslib": "2.3.0"
}
}
}
}

View File

@ -1,5 +0,0 @@
{
"dependencies": {
"echarts": "^6.0.0"
}
}

View File

@ -69,6 +69,11 @@ dependencies = [
"websockets==14.1",
"pyproject-toml>=0.1.0",
"uvloop==0.21.0 ; sys_platform != 'win32'",
"alibabacloud_dysmsapi20170525==4.1.2",
"alibabacloud_tea_openapi==0.4.1",
"alibabacloud_tea_util==0.3.14",
"pytest==8.3.3",
"pytest-html==4.1.1",
]
[tool.black]

Binary file not shown.

213
scripts/admin_flow_test.py Normal file
View File

@ -0,0 +1,213 @@
import os
import json
import time
import uuid
import random
from typing import Dict, Any, List, Optional, Tuple
import httpx
def make_url(base_url: str, path: str) -> str:
if base_url.endswith("/"):
base_url = base_url[:-1]
return f"{base_url}{path}"
def now_ms() -> int:
return int(time.time() * 1000)
def ensure_dict(obj: Any) -> Dict[str, Any]:
if isinstance(obj, dict):
return obj
return {"raw": str(obj)}
async def api_get(client: httpx.AsyncClient, url: str, headers: Optional[Dict[str, str]] = None, params: Optional[Dict[str, Any]] = None) -> Tuple[int, Dict[str, Any]]:
r = await client.get(url, headers=headers or {}, params=params or {})
try:
parsed = r.json()
except Exception:
parsed = {"raw": r.text}
return r.status_code, ensure_dict(parsed)
async def api_post_json(client: httpx.AsyncClient, url: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> Tuple[int, Dict[str, Any]]:
r = await client.post(url, json=payload, headers=headers or {})
try:
parsed = r.json()
except Exception:
parsed = {"raw": r.text}
return r.status_code, ensure_dict(parsed)
async def api_put_json(client: httpx.AsyncClient, url: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> Tuple[int, Dict[str, Any]]:
r = await client.put(url, json=payload, headers=headers or {})
try:
parsed = r.json()
except Exception:
parsed = {"raw": r.text}
return r.status_code, ensure_dict(parsed)
async def api_delete(client: httpx.AsyncClient, url: str, headers: Optional[Dict[str, str]] = None, params: Optional[Dict[str, Any]] = None) -> Tuple[int, Dict[str, Any]]:
r = await client.delete(url, headers=headers or {}, params=params or {})
try:
parsed = r.json()
except Exception:
parsed = {"raw": r.text}
return r.status_code, ensure_dict(parsed)
def write_html_report(filepath: str, title: str, results: List[Dict[str, Any]]) -> None:
rows = []
for r in results:
color = {"PASS": "#4caf50", "FAIL": "#f44336"}.get(r.get("status"), "#9e9e9e")
rows.append(
f"<tr><td>{r.get('name')}</td><td style='color:{color};font-weight:600'>{r.get('status')}</td><td>{r.get('message','')}</td><td><pre>{json.dumps(r.get('detail', {}), ensure_ascii=False, indent=2)}</pre></td></tr>"
)
html = f"""
<!doctype html>
<html><head><meta charset='utf-8'><title>{title}</title>
<style>body{{font-family:Arial;padding:12px}} table{{border-collapse:collapse;width:100%}} td,th{{border:1px solid #ddd;padding:8px}}</style>
</head><body>
<h2>{title}</h2>
<p>生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}</p>
<table><thead><tr><th>用例</th><th>结果</th><th>说明</th><th>详情</th></tr></thead><tbody>
{''.join(rows)}
</tbody></table>
</body></html>
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
f.write(html)
async def test_base(client: httpx.AsyncClient, base: str, token: str, results: List[Dict[str, Any]]):
for path, name in [("/base/userinfo", "admin用户信息"), ("/base/userapi", "admin接口权限"), ("/base/usermenu", "admin菜单")]:
code, data = await api_get(client, make_url(base, path), headers={"token": token})
ok = (code == 200)
results.append({"name": name, "status": "PASS" if ok else "FAIL", "message": "获取成功" if ok else "获取失败", "detail": {"http": code, "body": data}})
async def test_users_crud(client: httpx.AsyncClient, base: str, token: str, results: List[Dict[str, Any]]):
email = f"admin_{uuid.uuid4().hex[:6]}@test.com"
username = "adm_" + uuid.uuid4().hex[:6]
code, data = await api_post_json(client, make_url(base, "/user/create"), {"email": email, "username": username, "password": "123456", "is_active": True, "is_superuser": False, "role_ids": [], "dept_id": 0}, headers={"token": token})
results.append({"name": "创建用户", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
code, data = await api_get(client, make_url(base, "/user/list"), headers={"token": token}, params={"page": 1, "page_size": 10, "email": email})
ok = (code == 200 and isinstance(data.get("data"), list))
uid = None
if ok and data["data"]:
uid = data["data"][0].get("id")
results.append({"name": "查询用户", "status": "PASS" if ok else "FAIL", "message": "获取成功" if ok else "获取失败", "detail": {"http": code, "body": data}})
if uid:
code, data = await api_post_json(client, make_url(base, "/user/update"), {"id": uid, "email": email, "username": username + "_u", "is_active": True, "is_superuser": False, "role_ids": [], "dept_id": 0}, headers={"token": token})
results.append({"name": "更新用户", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
code, data = await api_delete(client, make_url(base, "/user/delete"), headers={"token": token}, params={"user_id": uid})
results.append({"name": "删除用户", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
async def test_roles_menus_apis(client: httpx.AsyncClient, base: str, token: str, results: List[Dict[str, Any]]):
rname = "role_" + uuid.uuid4().hex[:6]
code, data = await api_post_json(client, make_url(base, "/role/create"), {"name": rname, "desc": "测试角色"}, headers={"token": token})
results.append({"name": "创建角色", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
code, data = await api_get(client, make_url(base, "/role/list"), headers={"token": token}, params={"page": 1, "page_size": 10, "role_name": rname})
ok = (code == 200 and isinstance(data.get("data"), list))
rid = None
if ok and data["data"]:
rid = data["data"][0].get("id")
results.append({"name": "查询角色", "status": "PASS" if ok else "FAIL", "message": "获取成功" if ok else "获取失败", "detail": {"http": code, "body": data}})
code, data = await api_post_json(client, make_url(base, "/api/refresh"), {}, headers={"token": token})
results.append({"name": "刷新API权限表", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
code, data = await api_get(client, make_url(base, "/api/list"), headers={"token": token}, params={"page": 1, "page_size": 10})
ok_apis = (code == 200 and isinstance(data.get("data"), list))
results.append({"name": "API列表", "status": "PASS" if ok_apis else "FAIL", "message": "获取成功" if ok_apis else "获取失败", "detail": {"http": code, "body": data}})
if rid and ok_apis:
api_infos = []
if data["data"]:
first = data["data"][0]
api_infos = [{"path": first.get("path"), "method": first.get("method")}] if first.get("path") and first.get("method") else []
code, data = await api_post_json(client, make_url(base, "/role/authorized"), {"id": rid, "menu_ids": [], "api_infos": api_infos}, headers={"token": token})
results.append({"name": "角色授权", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
code, data = await api_delete(client, make_url(base, "/role/delete"), headers={"token": token}, params={"role_id": rid})
results.append({"name": "删除角色", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
async def test_dept_crud(client: httpx.AsyncClient, base: str, token: str, results: List[Dict[str, Any]]):
dname = "dept_" + uuid.uuid4().hex[:6]
code, data = await api_post_json(client, make_url(base, "/dept/create"), {"name": dname, "desc": "测试部门"}, headers={"token": token})
results.append({"name": "创建部门", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
code, data = await api_get(client, make_url(base, "/dept/list"), headers={"token": token}, params={"page": 1, "page_size": 10})
ok = (code == 200 and isinstance(data.get("data"), list))
results.append({"name": "查询部门", "status": "PASS" if ok else "FAIL", "message": "获取成功" if ok else "获取失败", "detail": {"http": code, "body": data}})
async def test_valuations_admin(client: httpx.AsyncClient, base: str, token: str, results: List[Dict[str, Any]]):
payload = {"asset_name": "Admin资产", "institution": "Admin机构", "industry": "行业", "three_year_income": [10, 20, 30]}
code, data = await api_post_json(client, make_url(base, "/valuations/"), payload, headers={"token": token})
results.append({"name": "创建估值(管理员)", "status": "PASS" if code == 200 and data.get("code") == 200 else "FAIL", "message": data.get("msg"), "detail": {"http": code, "body": data}})
code, data = await api_get(client, make_url(base, "/valuations/"), headers={"token": token}, params={"page": 1, "size": 5})
ok = (code == 200 and isinstance(data.get("data"), list))
results.append({"name": "估值列表(管理员)", "status": "PASS" if ok else "FAIL", "message": "获取成功" if ok else "获取失败", "detail": {"http": code, "body": data}})
async def test_invoice_transactions(client: httpx.AsyncClient, base: str, token: str, results: List[Dict[str, Any]]):
code, data = await api_get(client, make_url(base, "/invoice/list"), headers={"token": token}, params={"page": 1, "page_size": 10})
ok = (code == 200 and isinstance(data.get("data"), list))
results.append({"name": "发票列表", "status": "PASS" if ok else "FAIL", "message": "获取成功" if ok else "获取失败", "detail": {"http": code, "body": data}})
code, data = await api_get(client, make_url(base, "/transactions/receipts"), headers={"token": token}, params={"page": 1, "page_size": 10})
ok = (code == 200 and isinstance(data.get("data"), list))
results.append({"name": "对公转账列表", "status": "PASS" if ok else "FAIL", "message": "获取成功" if ok else "获取失败", "detail": {"http": code, "body": data}})
async def perf_benchmark(client: httpx.AsyncClient, base: str, token: str, results: List[Dict[str, Any]]):
endpoints = ["/user/list", "/valuations/", "/invoice/list"]
conc = 20
metrics = []
for ep in endpoints:
start = now_ms()
tasks = [api_get(client, make_url(base, ep), headers={"token": token}, params={"page": 1, "page_size": 10}) for _ in range(conc)]
rets = await httpx.AsyncClient.gather(*tasks) if hasattr(httpx.AsyncClient, "gather") else None
# 兼容:无 gather 则顺序执行
if rets is None:
rets = []
for _ in range(conc):
rets.append(await api_get(client, make_url(base, ep), headers={"token": token}, params={"page": 1, "page_size": 10}))
dur = now_ms() - start
ok = sum(1 for (code, _) in rets if code == 200)
metrics.append({"endpoint": ep, "concurrency": conc, "duration_ms": dur, "success": ok, "total": conc})
results.append({"name": "性能基准", "status": "PASS", "message": "并发测试完成", "detail": {"metrics": metrics}})
async def main() -> None:
base = os.getenv("ADMIN_BASE_URL", "http://localhost:9999/api/v1")
token = os.getenv("ADMIN_TOKEN", "dev")
results: List[Dict[str, Any]] = []
endpoint_list = [
{"path": "/base/userinfo", "desc": "管理员信息"},
{"path": "/user/*", "desc": "用户管理"},
{"path": "/role/*", "desc": "角色管理与授权"},
{"path": "/api/*", "desc": "API权限管理与刷新"},
{"path": "/dept/*", "desc": "部门管理"},
{"path": "/valuations/*", "desc": "估值评估管理"},
{"path": "/invoice/*", "desc": "发票与抬头"},
{"path": "/transactions/*", "desc": "对公转账记录"},
]
async with httpx.AsyncClient(timeout=10) as client:
await test_base(client, base, token, results)
await test_users_crud(client, base, token, results)
await test_roles_menus_apis(client, base, token, results)
await test_dept_crud(client, base, token, results)
await test_valuations_admin(client, base, token, results)
await test_invoice_transactions(client, base, token, results)
await perf_benchmark(client, base, token, results)
passes = sum(1 for r in results if r.get("status") == "PASS")
print(json.dumps({"total": len(results), "passes": passes, "results": results, "endpoints": endpoint_list}, ensure_ascii=False, indent=2))
write_html_report("reports/admin_flow_script_report.html", "后台管理员维度接口全流程测试报告", results)
if __name__ == "__main__":
import asyncio
asyncio.run(main())

270
scripts/api_smoke_test.py Normal file
View File

@ -0,0 +1,270 @@
import argparse
import json
import time
from typing import Dict, Any, Optional
import requests
def _print(title: str, payload: Any) -> None:
print(f"\n[{title}]\n{json.dumps(payload, ensure_ascii=False, indent=2)}")
def _url(base: str, path: str) -> str:
return f"{base}{path}"
class AppClient:
"""
用户端客户端会话维持与常用接口封装
参数:
base: API 基础地址 http://127.0.0.1:9991/api/v1
属性:
session: requests.Session 会话对象携带 token
"""
def __init__(self, base: str) -> None:
self.base = base.rstrip("/")
self.session = requests.Session()
def set_token(self, token: str) -> None:
"""
设置用户端 token 到请求头
参数:
token: 登录接口返回的 access_token
返回:
None
"""
self.session.headers.update({"token": token})
def register(self, phone: str) -> Dict[str, Any]:
"""
用户注册
参数:
phone: 手机号
返回:
注册响应 dict
"""
resp = self.session.post(_url(self.base, "/app-user/register"), json={"phone": phone})
return _safe_json(resp)
def login(self, phone: str, password: str) -> Optional[str]:
"""
用户登录
参数:
phone: 手机号
password: 密码
返回:
access_token None
"""
resp = self.session.post(_url(self.base, "/app-user/login"), json={"phone": phone, "password": password})
data = _safe_json(resp)
token = data.get("access_token") if isinstance(data, dict) else None
if token:
self.set_token(token)
return token
def profile(self) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, "/app-user/profile"))
return _safe_json(resp)
def dashboard(self) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, "/app-user/dashboard"))
return _safe_json(resp)
def quota(self) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, "/app-user/quota"))
return _safe_json(resp)
def submit_valuation(self, payload: Dict[str, Any]) -> Dict[str, Any]:
"""
提交估值评估
参数:
payload: 估值评估输入数据
返回:
提交响应 dict
"""
resp = self.session.post(_url(self.base, "/app-valuations/"), json=payload)
return _safe_json(resp)
def list_valuations(self) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, "/app-valuations/"))
return _safe_json(resp)
def valuation_detail(self, valuation_id: int) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, f"/app-valuations/{valuation_id}"))
return _safe_json(resp)
class AdminClient:
"""
后台客户端会话维持与接口封装
参数:
base: API 基础地址
"""
def __init__(self, base: str) -> None:
self.base = base.rstrip("/")
self.session = requests.Session()
def set_token(self, token: str) -> None:
self.session.headers.update({"token": token})
def login(self, username: str, password: str) -> Optional[str]:
resp = self.session.post(_url(self.base, "/base/access_token"), json={"username": username, "password": password})
data = _safe_json(resp)
token = data.get("data", {}).get("access_token") if isinstance(data, dict) else None
if token:
self.set_token(token)
return token
def list_valuations(self) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, "/valuations/"))
return _safe_json(resp)
def valuation_detail(self, valuation_id: int) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, f"/valuations/{valuation_id}"))
return _safe_json(resp)
def valuation_steps(self, valuation_id: int) -> Dict[str, Any]:
resp = self.session.get(_url(self.base, f"/valuations/{valuation_id}/steps"))
return _safe_json(resp)
def _safe_json(resp: requests.Response) -> Dict[str, Any]:
try:
return resp.json()
except Exception:
return {"status_code": resp.status_code, "text": resp.text}
def build_sample_payload() -> Dict[str, Any]:
"""
构建估值评估示例输入精简版
返回:
dict: 估值评估输入
"""
# 使用你提供的参数,保持后端计算逻辑不变
payload = {
"asset_name": "马王堆",
"institution": "成都文化产权交易所",
"industry": "文化艺术业",
"annual_revenue": "10000",
"rd_investment": "6000",
"three_year_income": ["8000", "9000", "9500"],
"funding_status": "省级资助",
"sales_volume": "60000",
"link_views": "350000",
"circulation": "3",
"last_market_activity": "0",
"monthly_transaction": "1",
"price_fluctuation": [402, 445],
"application_maturity": "0",
"application_coverage": "0",
"cooperation_depth": "0",
"offline_activities": "20",
"online_accounts": ["1", "成都文交所", "500000", "89222", "97412"],
"inheritor_level": "省级传承人",
"inheritor_age_count": [200, 68, 20],
"inheritor_certificates": [],
"heritage_level": "2",
"historical_evidence": {"artifacts": "58", "ancient_literature": "789", "inheritor_testimony": "100"},
"patent_certificates": [],
"pattern_images": [],
"patent_application_no": "",
"heritage_asset_level": "纳入《国家文化数字化战略清单》",
"inheritor_ages": [200, 68, 20],
"implementation_stage": "成熟应用",
"coverage_area": "全球覆盖",
"collaboration_type": "",
"scarcity_level": "流通:总发行份数 >1000份或二级市场流通率 ≥ 5%",
"market_activity_time": "近一周",
"monthly_transaction_amount": "月交易额100万500万",
"platform_accounts": {
"douyin": {"account": "成都文交所", "likes": "500000", "comments": "89222", "shares": "97412", "views": "100000"}
}
}
# 若 application_coverage 为占位,则用 coverage_area 回填
if payload.get("application_coverage") in (None, "0", "") and payload.get("coverage_area"):
payload["application_coverage"] = payload["coverage_area"]
return payload
def main() -> None:
parser = argparse.ArgumentParser(description="估值二期 API 冒烟测试")
parser.add_argument("--base", default="http://127.0.0.1:9991/api/v1", help="API基础地址")
parser.add_argument("--phone", default="13800138001", help="测试手机号")
args = parser.parse_args()
base = args.base.rstrip("/")
phone = args.phone
default_pwd = phone[-6:]
app = AppClient(base)
admin = AdminClient(base)
# 用户注册
reg = app.register(phone)
_print("用户注册", reg)
# 用户登录
token = app.login(phone, default_pwd)
_print("用户登录token", {"access_token": token})
if not token:
print("登录失败,终止测试")
return
# 用户相关接口
_print("用户信息", app.profile())
_print("首页摘要", app.dashboard())
_print("剩余估值次数", app.quota())
# 提交估值
payload = build_sample_payload()
submit = app.submit_valuation(payload)
_print("提交估值", submit)
# 轮询估值列表抓取最新记录
valuation_id = None
for _ in range(10):
lst = app.list_valuations()
_print("我的估值列表", lst)
try:
items = lst.get("data", []) if isinstance(lst, dict) else []
if items:
valuation_id = items[0].get("id") or items[-1].get("id")
if valuation_id:
break
except Exception:
pass
time.sleep(0.8)
if valuation_id:
detail = app.valuation_detail(valuation_id)
_print("估值详情", detail)
else:
print("未获得估值ID跳过详情")
# 后台登录
admin_token = admin.login("admin", "123456")
_print("后台登录token", {"access_token": admin_token})
if admin_token:
vlist = admin.list_valuations()
_print("后台估值列表", vlist)
if valuation_id:
vdetail = admin.valuation_detail(valuation_id)
_print("后台估值详情", vdetail)
vsteps = admin.valuation_steps(valuation_id)
_print("后台估值计算步骤", vsteps)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,104 @@
import json
from typing import Dict, Any, List, Tuple
from fastapi import FastAPI
from app import create_app
def load_openapi(app: FastAPI) -> Dict[str, Any]:
return app.openapi()
def is_object_schema(schema: Dict[str, Any]) -> bool:
return schema.get("type") == "object"
def get_schema_props(schema: Dict[str, Any]) -> Dict[str, Any]:
return schema.get("properties", {}) if schema else {}
def check_success_schema(props: Dict[str, Any]) -> Tuple[bool, List[str]]:
issues: List[str] = []
code_prop = props.get("code")
msg_prop = props.get("msg")
data_prop = props.get("data")
if code_prop is None:
issues.append("缺少字段: code")
elif code_prop.get("type") != "integer":
issues.append(f"code类型错误: {code_prop.get('type')}")
if msg_prop is None:
issues.append("缺少字段: msg")
elif msg_prop.get("type") != "string":
issues.append(f"msg类型错误: {msg_prop.get('type')}")
if data_prop is None:
issues.append("缺少字段: data")
else:
tp = data_prop.get("type")
if tp != "object":
issues.append(f"data类型错误: {tp}")
return (len(issues) == 0, issues)
def check_paths(openapi: Dict[str, Any]) -> Dict[str, Any]:
paths = openapi.get("paths", {})
compliant: List[Dict[str, Any]] = []
non_compliant: List[Dict[str, Any]] = []
for path, ops in paths.items():
for method, meta in ops.items():
op_id = meta.get("operationId")
tags = meta.get("tags", [])
responses = meta.get("responses", {})
success = responses.get("200") or responses.get("201")
if not success:
non_compliant.append({
"path": path,
"method": method.upper(),
"operationId": op_id,
"tags": tags,
"issues": ["无成功响应模型(200/201)"],
})
continue
content = success.get("content", {}).get("application/json", {})
schema = content.get("schema")
if not schema:
non_compliant.append({
"path": path,
"method": method.upper(),
"operationId": op_id,
"tags": tags,
"issues": ["成功响应未声明JSON Schema"],
})
continue
props = get_schema_props(schema)
ok, issues = check_success_schema(props)
rec = {
"path": path,
"method": method.upper(),
"operationId": op_id,
"tags": tags,
}
if ok:
compliant.append(rec)
else:
non_compliant.append({**rec, "issues": issues})
total = len(compliant) + len(non_compliant)
rate = 0 if total == 0 else round(len(compliant) / total * 100, 2)
return {
"compliant": compliant,
"non_compliant": non_compliant,
"stats": {"total": total, "compliant": len(compliant), "non_compliant": len(non_compliant), "rate": rate},
}
def main() -> None:
app = create_app()
openapi = load_openapi(app)
result = check_paths(openapi)
print(json.dumps(result, ensure_ascii=False, indent=2))
with open("scripts/response_format_report.json", "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()

103
scripts/send_email_test.py Normal file
View File

@ -0,0 +1,103 @@
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Dict, Optional
def _parse_bool(value: Optional[str]) -> bool:
"""
功能: 将环境变量中的布尔字符串解析为布尔值
参数: value (Optional[str]) - 环境变量字符串值, "true", "1", "yes"
返回: bool - 解析后的布尔值
"""
if value is None:
return False
return str(value).strip().lower() in {"1", "true", "yes", "y"}
def get_smtp_config() -> Dict[str, Optional[str]]:
"""
功能: 从环境变量读取SMTP配置并返回配置字典
参数:
返回: Dict[str, Optional[str]] - 包含hostportfromusernamepasswordtls等键的配置
"""
host = os.environ.get("SMTP_HOST", "smtp.qiye.aliyun.com")
port_str = os.environ.get("SMTP_PORT", "465")
from_addr = os.environ.get("SMTP_FROM","value@cdcee.net")
username = os.environ.get("SMTP_USERNAME","value@cdcee.net")
password = os.environ.get("SMTP_PASSWORD","PPXbILdGlRCn2VOx")
tls = _parse_bool(os.environ.get("SMTP_TLS"))
port = None
if port_str:
try:
port = int(port_str)
except Exception:
port = None
return {
"host": host,
"port": port,
"from": from_addr,
"username": username,
"password": password,
"tls": tls,
}
def send_test_email(to_email: str, subject: Optional[str], body: str) -> Dict[str, str]:
"""
功能: 使用SMTP配置发送测试邮件到指定邮箱
参数: to_email (str) - 收件人邮箱; subject (Optional[str]) - 邮件主题; body (str) - 邮件正文内容
返回: Dict[str, str] - 发送结果字典, 包含status("OK"/"FAIL")与error(失败信息)
"""
cfg = get_smtp_config()
if not cfg["host"] or not cfg["port"] or not cfg["from"]:
return {"status": "FAIL", "error": "SMTP 未配置: 需设置 SMTP_HOST/SMTP_PORT/SMTP_FROM"}
msg = MIMEMultipart()
msg["From"] = cfg["from"]
msg["To"] = to_email
msg["Subject"] = subject or "估值服务通知"
msg.attach(MIMEText(body, "plain", "utf-8"))
server = None
try:
if cfg["tls"]:
server = smtplib.SMTP(cfg["host"], cfg["port"], timeout=30)
server.starttls()
else:
server = smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=30)
if cfg["username"] and cfg["password"]:
server.login(cfg["username"], cfg["password"])
server.sendmail(cfg["from"], [to_email], msg.as_string())
server.quit()
return {"status": "OK"}
except Exception as e:
try:
if server:
server.quit()
except Exception:
pass
return {"status": "FAIL", "error": str(e)}
if __name__ == "__main__":
to = "zfc9393@163.com"
subject = "测试邮件"
body = "这是一封测试邮件用于验证SMTP配置。"
cfg = get_smtp_config()
print({
"host": cfg["host"],
"port": cfg["port"],
"from": cfg["from"],
"username_set": bool(cfg["username"]),
"password_set": bool(cfg["password"]),
"tls": cfg["tls"],
})
result = send_test_email(to, subject, body)
print(result)

View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
TOKEN=""
EMAIL=""
SUBJECT=""
BODY=""
FILE_PATH=""
FILE_URL=""
BASE_API="http://127.0.0.1:9999/api/v1"
while getopts ":t:e:s:b:f:u:a:" opt; do
case "$opt" in
t) TOKEN="$OPTARG" ;;
e) EMAIL="$OPTARG" ;;
s) SUBJECT="$OPTARG" ;;
b) BODY="$OPTARG" ;;
f) FILE_PATH="$OPTARG" ;;
u) FILE_URL="$OPTARG" ;;
a) BASE_API="$OPTARG" ;;
*) echo "Invalid option: -$OPTARG"; exit 1 ;;
esac
done
if [[ -z "$TOKEN" || -z "$EMAIL" || -z "$BODY" ]]; then
echo "Usage: $0 -t <token> -e <email> -b <body> [-s <subject>] [-f <file_path> | -u <file_url>] [-a <base_api>]"
exit 1
fi
URL="$BASE_API/transactions/send-email"
if [[ -n "$FILE_PATH" ]]; then
if [[ ! -f "$FILE_PATH" ]]; then
echo "File not found: $FILE_PATH"
exit 1
fi
curl -s -X POST "$URL" \
-H "accept: application/json" \
-H "token: $TOKEN" \
-F "email=$EMAIL" \
-F "subject=$SUBJECT" \
-F "body=$BODY" \
-F "file=@$FILE_PATH" | jq -r '.' 2>/dev/null || true
else
PAYLOAD="{\"email\":\"$EMAIL\",\"subject\":\"$SUBJECT\",\"body\":\"$BODY\"}"
if [[ -n "$FILE_URL" ]]; then
PAYLOAD="{\"email\":\"$EMAIL\",\"subject\":\"$SUBJECT\",\"body\":\"$BODY\",\"file_url\":\"$FILE_URL\"}"
fi
curl -s -X POST "$URL" \
-H "accept: application/json" \
-H "token: $TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" | jq -r '.' 2>/dev/null || true
fi

393
scripts/user_flow_test.py Normal file
View File

@ -0,0 +1,393 @@
import os
import sys
import json
import time
import uuid
import random
from typing import Dict, Any, List, Tuple, Optional
import httpx
def now_ms() -> int:
return int(time.time() * 1000)
def make_url(base_url: str, path: str) -> str:
if base_url.endswith("/"):
base_url = base_url[:-1]
return f"{base_url}{path}"
def write_html_report(filepath: str, title: str, results: List[Dict[str, Any]]) -> None:
"""
生成HTML测试报告
参数:
filepath: 报告输出文件路径
title: 报告标题
results: 测试结果列表包含 name/status/message/detail
返回:
None
"""
rows = []
for r in results:
color = {"PASS": "#4caf50", "FAIL": "#f44336"}.get(r.get("status"), "#9e9e9e")
rows.append(
f"<tr><td>{r.get('name')}</td><td style='color:{color};font-weight:600'>{r.get('status')}</td><td>{r.get('message','')}</td><td><pre>{json.dumps(r.get('detail', {}), ensure_ascii=False, indent=2)}</pre></td></tr>"
)
html = f"""
<!doctype html>
<html><head><meta charset='utf-8'><title>{title}</title>
<style>body{{font-family:Arial;padding:12px}} table{{border-collapse:collapse;width:100%}} td,th{{border:1px solid #ddd;padding:8px}}</style>
</head><body>
<h2>{title}</h2>
<p>生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}</p>
<table><thead><tr><th>用例</th><th>结果</th><th>说明</th><th>详情</th></tr></thead><tbody>
{''.join(rows)}
</tbody></table>
</body></html>
"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
f.write(html)
def _ensure_dict(obj: Any) -> Dict[str, Any]:
if isinstance(obj, dict):
return obj
return {"raw": str(obj)}
async def api_post_json(client: httpx.AsyncClient, url: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> Tuple[int, Dict[str, Any]]:
"""
发送POST JSON请求
参数:
client: httpx异步客户端
url: 完整URL
payload: 请求体JSON
headers: 请求头
返回:
(状态码, 响应JSON)
"""
r = await client.post(url, json=payload, headers=headers or {})
try:
parsed = r.json()
except Exception:
parsed = {"raw": r.text}
if parsed is None:
parsed = {"raw": r.text}
return r.status_code, _ensure_dict(parsed)
async def api_get(client: httpx.AsyncClient, url: str, headers: Optional[Dict[str, str]] = None, params: Optional[Dict[str, Any]] = None) -> Tuple[int, Dict[str, Any]]:
"""
发送GET请求
参数:
client: httpx异步客户端
url: 完整URL
headers: 请求头
params: 查询参数
返回:
(状态码, 响应JSON)
"""
r = await client.get(url, headers=headers or {}, params=params or {})
try:
parsed = r.json()
except Exception:
parsed = {"raw": r.text}
if parsed is None:
parsed = {"raw": r.text}
return r.status_code, _ensure_dict(parsed)
async def api_put_json(client: httpx.AsyncClient, url: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> Tuple[int, Dict[str, Any]]:
"""
发送PUT JSON请求
参数:
client: httpx异步客户端
url: 完整URL
payload: 请求体JSON
headers: 请求头
返回:
(状态码, 响应JSON)
"""
r = await client.put(url, json=payload, headers=headers or {})
try:
parsed = r.json()
except Exception:
parsed = {"raw": r.text}
if parsed is None:
parsed = {"raw": r.text}
return r.status_code, _ensure_dict(parsed)
async def user_register_flow(base_url: str, client: httpx.AsyncClient, phone: str, expect_success: bool = True) -> Dict[str, Any]:
"""
用户注册流程
参数:
base_url: 基础URL( /api/v1)
client: httpx客户端
phone: 手机号
返回:
测试结果字典
"""
url = make_url(base_url, "/app-user/register")
code, data = await api_post_json(client, url, {"phone": phone})
rs = {"name": f"注册-{phone}", "status": "FAIL", "message": "", "detail": {"http": code, "body": _ensure_dict(data)}}
body = _ensure_dict(data)
payload = _ensure_dict(body.get("data"))
ok = (body.get("code") == 200 and payload.get("phone") == phone)
# 期望失败场景:重复注册或无效格式
if not expect_success:
ok = (body.get("code") in (400, 422) or (isinstance(body.get("msg"), str) and "已存在" in body.get("msg")))
rs["message"] = "注册失败(符合预期)" if ok else "注册失败(不符合预期)"
else:
rs["message"] = "注册成功" if ok else "注册失败"
rs["status"] = "PASS" if ok else "FAIL"
return rs
async def user_login_flow(base_url: str, client: httpx.AsyncClient, phone: str, password: str, expect_success: bool = True) -> Tuple[Dict[str, Any], str]:
"""
用户登录流程
参数:
base_url: 基础URL( /api/v1)
client: httpx客户端
phone: 手机号
password: 密码
返回:
(测试结果字典, access_token字符串或空)
"""
url = make_url(base_url, "/app-user/login")
code, data = await api_post_json(client, url, {"phone": phone, "password": password})
token = ""
is_ok = (code == 200 and isinstance(data, dict) and data.get("access_token"))
if is_ok:
token = data.get("access_token", "")
if not expect_success:
ok = (code in (401, 403))
rs = {"name": f"登录-{phone}", "status": "PASS" if ok else "FAIL", "message": "登录失败(符合预期)" if ok else "登录失败(不符合预期)", "detail": {"http": code, "body": data}}
else:
rs = {"name": f"登录-{phone}", "status": "PASS" if is_ok else "FAIL", "message": "登录成功" if is_ok else "登录失败", "detail": {"http": code, "body": data}}
return rs, token
async def user_profile_flow(base_url: str, client: httpx.AsyncClient, token: str) -> Dict[str, Any]:
"""
用户资料查看与编辑
参数:
base_url: 基础URL( /api/v1)
client: httpx客户端
token: 用户JWT
返回:
测试结果字典
"""
headers = {"token": token}
view_url = make_url(base_url, "/app-user/profile")
v_code, v_data = await api_get(client, view_url, headers=headers)
ok_view = (v_code == 200 and isinstance(v_data, dict) and v_data.get("id"))
upd_url = make_url(base_url, "/app-user/profile")
nickname = "tester-" + uuid.uuid4().hex[:6]
u_code, u_data = await api_put_json(client, upd_url, {"nickname": nickname}, headers=headers)
ok_upd = (u_code == 200 and isinstance(u_data, dict) and u_data.get("nickname") == nickname)
is_ok = ok_view and ok_upd
return {"name": "资料查看与编辑", "status": "PASS" if is_ok else "FAIL", "message": "个人资料操作成功" if is_ok else "个人资料操作失败", "detail": {"view": {"http": v_code, "body": v_data}, "update": {"http": u_code, "body": u_data}}}
async def permission_flow(base_url: str, client: httpx.AsyncClient, admin_token: str) -> Dict[str, Any]:
"""
权限控制验证
参数:
base_url: 基础URL( /api/v1)
client: httpx客户端
admin_token: 管理端token头值
返回:
测试结果字典
"""
protected_url = make_url(base_url, "/user/list")
c1, d1 = await api_get(client, protected_url)
c2, d2 = await api_get(client, protected_url, headers={"token": admin_token})
ok1 = (c1 in (401, 403, 422))
ok2 = (c2 in (200, 403))
is_ok = ok1 and ok2
return {"name": "权限控制", "status": "PASS" if is_ok else "FAIL", "message": "权限校验完成", "detail": {"no_token": {"http": c1, "body": d1}, "with_token": {"http": c2, "body": d2}}}
async def main() -> None:
"""
主流程
参数:
返回:
None
"""
base = os.getenv("TEST_BASE_URL", "http://localhost:9991/api/v1")
admin_token = os.getenv("ADMIN_TOKEN", "dev")
results: List[Dict[str, Any]] = []
endpoint_list = [
{"path": "/app-user/register", "desc": "用户注册"},
{"path": "/app-user/login", "desc": "用户登录"},
{"path": "/app-user/profile", "desc": "获取用户信息(需token)"},
{"path": "/app-user/profile", "desc": "更新用户信息(需token) PUT"},
{"path": "/app-user/dashboard", "desc": "用户首页摘要(需token)"},
{"path": "/app-user/quota", "desc": "剩余估值次数(需token)"},
{"path": "/app-user/change-password", "desc": "修改密码(需token)"},
{"path": "/app-user/validate-token", "desc": "验证token(需token)"},
{"path": "/app-user/logout", "desc": "登出(需token)"},
{"path": "/upload/file", "desc": "上传文件"},
{"path": "/app-valuations/", "desc": "创建估值评估(需token)"},
{"path": "/app-valuations/", "desc": "获取我的估值评估列表(需token)"},
{"path": "/app-valuations/{id}", "desc": "获取估值评估详情(需token)"},
{"path": "/app-valuations/statistics/overview", "desc": "获取我的估值统计(需token)"},
{"path": "/app-valuations/{id}", "desc": "删除估值评估(需token) DELETE"},
]
async with httpx.AsyncClient(timeout=10) as client:
def gen_cn_phone() -> str:
second = str(random.choice([3,4,5,6,7,8,9]))
rest = "".join(random.choice("0123456789") for _ in range(9))
return "1" + second + rest
phone_ok = gen_cn_phone()
r1 = await user_register_flow(base, client, phone_ok, expect_success=True)
results.append(r1)
r2 = await user_register_flow(base, client, phone_ok, expect_success=False)
results.append(r2)
r3 = await user_register_flow(base, client, "abc", expect_success=False)
results.append(r3)
lr_ok, token = await user_login_flow(base, client, phone_ok, phone_ok[-6:], expect_success=True)
results.append(lr_ok)
lr_bad, _ = await user_login_flow(base, client, phone_ok, "wrong", expect_success=False)
results.append(lr_bad)
# token 场景:验证、资料、首页、配额
if token:
# 验证token
vt_code, vt_data = await api_get(client, make_url(base, "/app-user/validate-token"), headers={"token": token})
vt_ok = (vt_code == 200 and isinstance(vt_data, dict) and vt_data.get("data", {}).get("user_id"))
results.append({"name": "验证token", "status": "PASS" if vt_ok else "FAIL", "message": "token有效" if vt_ok else "token无效", "detail": {"http": vt_code, "body": vt_data}})
# 资料查看与编辑
pr = await user_profile_flow(base, client, token)
results.append(pr)
# 首页摘要
db_code, db_data = await api_get(client, make_url(base, "/app-user/dashboard"), headers={"token": token})
db_ok = (db_code == 200 and isinstance(db_data, dict))
results.append({"name": "用户首页摘要", "status": "PASS" if db_ok else "FAIL", "message": "获取成功" if db_ok else "获取失败", "detail": {"http": db_code, "body": db_data}})
# 剩余估值次数
qt_code, qt_data = await api_get(client, make_url(base, "/app-user/quota"), headers={"token": token})
qt_ok = (qt_code == 200 and isinstance(qt_data, dict))
results.append({"name": "剩余估值次数", "status": "PASS" if qt_ok else "FAIL", "message": "获取成功" if qt_ok else "获取失败", "detail": {"http": qt_code, "body": qt_data}})
# 修改密码并验证新旧密码
cp_code, cp_data = await api_post_json(client, make_url(base, "/app-user/change-password"), {"old_password": phone_ok[-6:], "new_password": "Npw" + phone_ok[-6:]}, headers={"token": token})
cp_ok = (cp_code == 200 and isinstance(cp_data, dict) and cp_data.get("code") == 200)
results.append({"name": "修改密码", "status": "PASS" if cp_ok else "FAIL", "message": "修改成功" if cp_ok else "修改失败", "detail": {"http": cp_code, "body": cp_data}})
# 旧密码登录应失败
lr_old, _ = await user_login_flow(base, client, phone_ok, phone_ok[-6:], expect_success=False)
results.append(lr_old)
# 新密码登录成功
lr_new, token2 = await user_login_flow(base, client, phone_ok, "Npw" + phone_ok[-6:], expect_success=True)
results.append(lr_new)
use_token = token2 or token
# 上传文件pdf
file_url = ""
try:
up_resp = await client.post(make_url(base, "/upload/file"), files={"file": ("demo.pdf", b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n", "application/pdf")})
u_code = up_resp.status_code
u_data = _ensure_dict(up_resp.json() if up_resp.headers.get("content-type", "").startswith("application/json") else {"raw": up_resp.text})
file_url = u_data.get("url", "")
u_ok = (u_code == 200 and file_url)
results.append({"name": "上传文件", "status": "PASS" if u_ok else "FAIL", "message": "上传成功" if u_ok else "上传失败", "detail": {"http": u_code, "body": u_data}})
except Exception as e:
results.append({"name": "上传文件", "status": "FAIL", "message": "上传异常", "detail": {"error": repr(e)}})
# 创建估值评估
create_payload = {
"asset_name": "测试资产",
"institution": "测试机构",
"industry": "测试行业",
"three_year_income": [100, 120, 140],
"application_coverage": "全国覆盖",
"rd_investment": "10",
"annual_revenue": "100",
"price_fluctuation": [10, 20],
"platform_accounts": {"douyin": {"likes": 1, "comments": 1, "shares": 1}},
"pattern_images": [],
"report_url": file_url or None,
"certificate_url": file_url or None,
}
cv_code, cv_data = await api_post_json(client, make_url(base, "/app-valuations/"), create_payload, headers={"token": use_token})
cv_ok = (cv_code == 200 and isinstance(cv_data, dict) and cv_data.get("data", {}).get("task_status") == "queued")
results.append({"name": "创建估值评估", "status": "PASS" if cv_ok else "FAIL", "message": "任务已提交" if cv_ok else "提交失败", "detail": {"http": cv_code, "body": cv_data}})
# 等待片刻后获取列表与详情
import asyncio
await asyncio.sleep(0.3)
gl_code, gl_data = await api_get(client, make_url(base, "/app-valuations/"), headers={"token": use_token}, params={"page": 1, "size": 10})
gl_ok = (gl_code == 200 and isinstance(gl_data, dict) and isinstance(gl_data.get("data"), list))
results.append({"name": "估值列表", "status": "PASS" if gl_ok else "FAIL", "message": "获取成功" if gl_ok else "获取失败", "detail": {"http": gl_code, "body": gl_data}})
vid = None
if gl_ok and gl_data.get("data"):
vid = gl_data["data"][0].get("id")
if vid:
gd_code, gd_data = await api_get(client, make_url(base, f"/app-valuations/{vid}"), headers={"token": use_token})
gd_ok = (gd_code == 200 and isinstance(gd_data, dict) and gd_data.get("data", {}).get("id") == vid)
results.append({"name": "估值详情", "status": "PASS" if gd_ok else "FAIL", "message": "获取成功" if gd_ok else "获取失败", "detail": {"http": gd_code, "body": gd_data}})
# 统计
st_code, st_data = await api_get(client, make_url(base, "/app-valuations/statistics/overview"), headers={"token": use_token})
st_ok = (st_code == 200 and isinstance(st_data, dict))
results.append({"name": "估值统计", "status": "PASS" if st_ok else "FAIL", "message": "获取成功" if st_ok else "获取失败", "detail": {"http": st_code, "body": st_data}})
# 删除
del_resp = await client.delete(make_url(base, f"/app-valuations/{vid}"), headers={"token": use_token})
d_code = del_resp.status_code
d_data = _ensure_dict(del_resp.json() if del_resp.headers.get("content-type", "").startswith("application/json") else {"raw": del_resp.text})
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)
results.append({"name": "登出", "status": "PASS" if lo_ok else "FAIL", "message": "登出成功" if lo_ok else "登出失败", "detail": {"http": lo_code, "body": lo_data}})
perm = await permission_flow(base, client, admin_token)
results.append(perm)
passes = sum(1 for r in results if r.get("status") == "PASS")
total = len(results)
print(json.dumps({"total": total, "passes": passes, "results": results, "endpoints": endpoint_list}, ensure_ascii=False, indent=2))
write_html_report("reports/user_flow_script_report.html", "用户维度功能测试报告(脚本)", results)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
async def api_put_json(client: httpx.AsyncClient, url: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> Tuple[int, Dict[str, Any]]:
"""
发送PUT JSON请求
参数:
client: httpx异步客户端
url: 完整URL
payload: 请求体JSON
headers: 请求头
返回:
(状态码, 响应JSON)
"""
r = await client.put(url, json=payload, headers=headers or {})
data = {}
try:
data = r.json()
except Exception:
data = {"raw": r.text}
return r.status_code, data

View File

@ -1,86 +0,0 @@
#!/usr/bin/env python3
"""
测试动态默认值计算逻辑
"""
import sys
import os
import asyncio
# 添加项目根目录到 Python 路径
project_root = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, project_root)
from app.utils.calculation_engine.market_value_c.market_data_analyzer import MarketDataAnalyzer
async def test_market_data_analyzer():
"""测试市场数据分析器"""
print("=== 测试市场数据分析器 ===")
analyzer = MarketDataAnalyzer()
# 测试获取近期交易中位数
print("\n1. 测试获取近期交易中位数:")
try:
median_price = await analyzer.get_recent_transaction_median(days=30)
print(f" 近30天交易中位数: {median_price}万元")
except Exception as e:
print(f" 获取交易中位数失败: {e}")
# 测试自适应默认值计算
print("\n2. 测试自适应默认值计算:")
test_cases = [
("限量", "文化艺术"),
("普通", "科技创新"),
("稀有", "传统工艺"),
("", "") # 空值测试
]
for issuance_level, asset_type in test_cases:
try:
adaptive_price = analyzer.calculate_adaptive_default_value(asset_type, issuance_level)
print(f" 发行级别: {issuance_level or '未知'}, 资产类型: {asset_type or '未知'} -> {adaptive_price}万元")
except Exception as e:
print(f" 计算自适应默认值失败 ({issuance_level}, {asset_type}): {e}")
async def test_market_value_c_integration():
"""测试市场估值C的集成"""
print("\n=== 测试市场估值C集成 ===")
from app.utils.calculation_engine.market_value_c.market_value_c import MarketValueCCalculator
calculator = MarketValueCCalculator()
# 测试数据:没有提供 average_transaction_price
test_data = {
'daily_browse_volume': 500.0,
'collection_count': 50,
'issuance_level': '限量',
'recent_market_activity': '近一周'
}
print(f"\n测试输入数据: {test_data}")
try:
result = await calculator.calculate_complete_market_value_c(test_data)
print(f"\n计算结果:")
print(f" 市场估值C: {result['market_value_c']}万元")
print(f" 市场竞价C1: {result['market_bidding_c1']}万元")
print(f" 热度系数C2: {result['heat_coefficient_c2']}")
print(f" 稀缺性乘数C3: {result['scarcity_multiplier_c3']}")
print(f" 时间衰减C4: {result['temporal_decay_c4']}")
except Exception as e:
print(f"计算失败: {e}")
import traceback
traceback.print_exc()
async def main():
"""主测试函数"""
print("开始测试动态默认值计算逻辑...")
await test_market_data_analyzer()
await test_market_value_c_integration()
print("\n测试完成!")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,3 +1,3 @@
VITE_TITLE = 'Vue FastAPI Admin'
VITE_TITLE = '估值后台管理系统'
VITE_PORT = 3100

View File

@ -5,4 +5,5 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_PROXY = true
# base api
VITE_BASE_API = '/api/v1'
VITE_BASE_API = 'http://139.224.70.152:9990/api/v1'
# VITE_BASE_API = 'http://127.0.0.1:9999/api/v1'

View File

@ -1,6 +1,6 @@
{
"lang": "中文",
"app_name": "Vue FastAPI Admin",
"app_name": "估值后台管理系统",
"header": {
"label_profile": "个人信息",
"label_logout": "退出登录",
@ -49,6 +49,30 @@
"errors": {
"label_error": "错误页",
"text_back_to_home": "返回首页"
},
"user_management": {
"label_user_management": "用户管理",
"label_user_list": "用户列表",
"label_phone": "手机号",
"label_wechat": "微信号",
"label_register_time": "注册时间",
"label_remark": "备注",
"label_data_count": "创建数据数量",
"label_status": "状态",
"label_actions": "操作",
"button_detail": "详情",
"button_freeze": "冻结",
"button_unfreeze": "解冻",
"button_add_user": "新建用户",
"placeholder_phone": "请输入手机号",
"placeholder_wechat": "请输入微信号",
"placeholder_remark": "请输入备注",
"message_phone_required": "请输入手机号",
"message_phone_format_error": "请输入正确的手机号格式",
"message_freeze_success": "已冻结该用户",
"message_unfreeze_success": "已解冻该用户",
"text_status_normal": "正常",
"text_status_frozen": "冻结"
}
},
"common": {
@ -59,4 +83,4 @@
"update": "修改"
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More