feat: 重构后端服务并添加新功能

refactor: 优化API路由和响应模型
feat(admin): 添加App用户管理接口
feat(sms): 实现阿里云短信服务集成
feat(email): 添加SMTP邮件发送功能
feat(upload): 支持文件上传接口
feat(rate-limiter): 实现手机号限流器
fix: 修复计算步骤入库问题
docs: 更新API文档和测试计划
chore: 更新依赖和配置
This commit is contained in:
邹方成 2025-11-19 19:36:03 +08:00
parent ebf41d74c8
commit cc352d3184
94 changed files with 4791 additions and 6054 deletions

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 的迁移示例命令),我可以继续补充。请告诉我你希望强调的额外部分或需要澄清的地方。

View File

@ -0,0 +1,59 @@
## 输出目标
- 以 `admin`(后台)与 `app`(用户端)两大类重组全部现有 `v1` API。
- 统一每个接口的文档格式:路径、方法、版本、功能说明、公开/认证、admin权限要求、请求参数与格式、响应结构、错误代码。
- 版本标注统一为 `v1`(前缀 `"/api/v1"`)。
## 分类规则
- `admin`:在 `app/api/v1/__init__.py:33-38,45-51` 通过 `dependencies=[DependAuth, DependPermission]` 绑定的模块及其接口:`user/role/menu/api/dept/auditlog/valuations/invoice/transactions/third_party_api`,以及 `base`(后台登录与个人信息)。
- `app`:面向终端用户的模块:`app-user``app-valuations``sms`(登录与验证码相关)、`upload`
- 管理功能但当前公开(未绑定后台依赖):`industry/index/policy/esg`,归入 `admin公开`,在文档中明确“公开接口”。
## 文档结构
- 顶层两章:
- `app用户端`:模块分组(用户认证与账户、用户资料与仪表盘、用户端估值、短信验证码、上传)
- `admin后台`模块分组用户管理、角色管理、菜单管理、API 权限管理、部门管理、审计日志、估值评估、发票管理、交易管理、第三方内置接口、内容管理:行业/指数/政策/ESG、基础登录/个人信息)
- 接口条目统一字段:
- 路径:`/api/v1/<module>/<subpath>`
- 方法:`GET/POST/PUT/DELETE`
- 版本:`v1`
- 功能说明:一句话摘要
- 公开/认证:`公开``需认证``admin` 另标注“需权限校验”)
- 权限要求admin是否受 `DependPermission` 控制(匹配 `(method, path)`
- 请求参数Query/Path/BodyBody 引用 `pydantic` 模型名)
- 响应结构:统一 `{code,msg,data}` 或分页 `{code,msg,data,total,page,page_size}`(引用响应模型)
- 错误代码200/400/401/403/404/422/500依据全局异常与业务抛出
## 信息来源与标注依据
- 路由与版本:`app/api/v1/__init__.py:28-52`(前缀与模块挂载)。
- 认证与权限:
- `admin` 统一:`DependAuth``DependPermission``app/api/v1/__init__.py:33-38,45-51`;依赖定义于 `app/core/dependency.py`)。
- `app` 认证:`Depends(get_current_app_user)`/`Depends(get_current_app_user_id)``app/utils/app_user_jwt.py:51,71-72`)。
- 单接口特例:`sms``/send-report` 需后台认证(`app/api/v1/sms/sms.py:68`)。
- 请求/响应模型:端点签名与 `response_model`(来源 `app/schemas/*`)。
- 错误码与统一响应:`app/core/init_app.py:56-59`(注册),`app/core/exceptions.py:15,23,31`(处理器)。
## 示例格式(两条)
- `appPOST /api/v1/sms/send-code`v1
- 功能:发送登录验证码到手机号
- 公开/认证:公开
- 请求参数Body`SendCodeRequest`
- 响应结构:`SendResponse``{code,msg,data}`
- 错误码:`400/422/500`
- 代码参照:`app/api/v1/sms/sms.py:68`
- `adminGET /api/v1/user/list`v1
- 功能:分页查询后台用户
- 公开/认证:需认证;需权限校验
- 请求参数Query分页与过滤
- 响应结构:`SuccessExtra``{code,msg,data,total,page,page_size}`
- 错误码:`401/403/422/500`
- 代码参照:`app/api/v1/__init__.py:33`
## 交付内容
- 生成统一的 Markdown 文档(建议 `docs/api-v1.md`),按 `app``admin` 两章、功能模块分组列出全部接口,逐条填充统一字段。
- 附“错误码说明”与“认证/权限机制”章节,提供关键代码路径引用,便于后续维护与审计。
## 验证与维护
- 通过聚合路由与端点扫描确认无遗漏;如有新接口,按相同格式追加。
- 校验每条接口的认证与权限标注是否与代码一致;抽样比对响应结构、错误码与异常处理一致性。
请确认上述按 `admin``app` 分类的计划;确认后我将开始生成完整文档并交付。

View File

@ -0,0 +1,184 @@
## 现状速览
* 后端框架FastAPIORMTortoise权限`DependAuth` + `DependPermission` 挂载在 admin 路由(`app/api/v1/__init__.py:33-37`)。
* 用户端估值评估路由:`/api/v1/app-valuations``app/api/v1/app_valuations/app_valuations.py`),控制器分层清晰(`app/controllers/user_valuation.py``app/controllers/valuation.py`)。
* 上传目前仅支持图片(`app/controllers/upload.py:12-52``app/api/v1/upload/upload.py:7-14`)。
* 发票与抬头能力完善(`app/api/v1/invoice/invoice.py``app/controllers/invoice.py`)。
* Web 管理端用户列表当前使用 Mock 数据展示“剩余体验次数”(`web/src/views/user-management/user-list/index.vue:115-122``web/src/api/index.js:279-352`)。
## 目标改造概览
* 交易管理新增“邮件发送”接口,支持正文与附件,完备校验与日志。
* 用户管理新增“剩余估值次数”字段与管理员调额能力,提供操作日志(前/后值、类型、备注)。
* 估值表新增“报告/证书”URL 字段与多格式上传能力,生成可下载链接。
* 估值表新增“统一社会信用代码/身份证号”与“业务/传承介绍”,前后端同步与校验。
* 全量权限控制、API 文档补充、数据库变更记录与单元测试覆盖。
## 数据库与模型变更
* ValuationAssessment`app/models/valuation.py`)新增:
* `report_url: CharField(512)``certificate_url: CharField(512)` 用于管理员上传的报告/证书下载地址。
* `credit_code_or_id: CharField(64)` 用于统一社会信用代码或身份证号。
* `biz_intro: TextField` 业务/传承介绍。
* 用户配额与日志:
* AppUser`app/models/user.py`)新增 `remaining_quota: IntField(default=0)`
* 新增操作日志模型(建议放入 `app/models/invoice.py` 同模块管理,或新建 `app/models/transaction.py``AppUserQuotaLog` 字段:`app_user_id``operator_id``operator_name``before_count``after_count``op_type`(如:付费估值)、`remark``created_at`
* 邮件发送日志模型:`EmailSendLog` 字段:`email``subject``body` 摘要(前 N 字符)、`file_name`/`file_url``status`OK/FAIL`error`(可空)、`sent_at`
* 迁移:使用 Aerich 生成并升级(保持兼容,所有新增字段均可空或有安全默认值)。
## 接口设计与权限控制
* 交易管理新增:`POST /api/v1/transactions/send-email`
* Bodymultipart 或 JSON
* `email`(必填,邮箱校验)
* `subject`(可选,默认“估值服务通知”)
* `body`(必填,文案内容,长度与危险字符校验)
* `file`(可选,`UploadFile`,或 `file_url` 字符串二选一)
* 行为:使用标准库 `smtplib` + `email` 组合发送;支持 TLS/SSL发送后落库 `EmailSendLog`;返回发送状态与日志 ID。
* 权限:挂载于 `transactions_router`(已带 `DependAuth``DependPermission``app/api/v1/__init__.py:52`)。
* 用户管理新增:
* `GET /api/v1/user/list` 返回结构新增 `remaining_quota` 字段。
* `POST /api/v1/user/quota`(管理员)调整用户剩余估值次数:
* Body`user_id``target_count``delta``op_type``remark`
* 行为:读取当前值,计算前后值,更新 `AppUser.remaining_quota`,记录 `AppUserQuotaLog`
* `GET /api/v1/user/{id}/quota-logs` 返回日志列表(分页、类型筛选)。
* 发票抬头查看:
* 复用现有接口 `GET /api/v1/invoice/headers?app_user_id=...``app/api/v1/invoice/invoice.py:118-124`)。
* 估值评估新增字段对外:
* 管理端与用户端输出 Schema 同步包含 `report_url``certificate_url``credit_code_or_id``biz_intro`
## 上传能力扩展
* 控制器:新增 `upload_file(file: UploadFile)` 支持 `pdf/docx/xlsx/zip` 等白名单;存储到 `app/static/files`;生成可下载链接 `settings.BASE_URL + /static/files/{name}`
* 路由:`POST /api/v1/upload/file`(保留图片接口不变)。
* 校验:
* MIME 白名单与大小限制;文件名清洗与去重;异常返回 4xx/5xx。
## Schema 与后端校验
* 估值 Schema`app/schemas/valuation.py`)新增并校验:
* `credit_code_or_id`:正则校验(统一社会信用代码/18位身份证格式二选一
* `report_url``certificate_url`URL 格式校验。
* 用户配额:新增 `AppUserQuotaUpdateSchema``AppUserQuotaLogOutSchema`
* 邮件发送:`SendEmailRequest` 支持两种附件输入;返回 `SendEmailResponse`
<br />
## 发送逻辑实现要点
* 设置:在 `app/settings/config.py` 增加 SMTP 相关配置:`SMTP_HOST``SMTP_PORT``SMTP_USERNAME``SMTP_PASSWORD``SMTP_TLS``SMTP_FROM`(默认 None走环境变量注入
* 发送器:`app/services/email_client.py`(或 `app/controllers/transactions.py` 内部封装),使用 `smtplib.SMTP_SSL`/`SMTP.starttls()`,构造 `MIMEText``MIMEBase`,附件从 `UploadFile` 或远程 `file_url` 下载后附加。
* 错误处理:
* 参数校验失败返回 422SMTP 异常记录 `EmailSendLog.error` 并返回 500长正文截断日志摘要防止超长存储。
* 日志:统一 `loguru` 记录关键事件(如 `transactions.email_send_start/ok/fail`)。
## 权限控制与 API 权限表
* 新增接口在 `Api` 表登记(路径+方法+标签),使用现有刷新接口 `POST /api/v1/api/refresh` 扫描路由自动入库。
* 路由均落于已挂载依赖的 admin 模块App 端路由继续独立(`app/api/v1/__init__.py:28-33`)。
## API 文档与变更记录
* 为所有新增接口与控制器方法补充函数级注释(功能、参数、返回值),满足用户规范。
* 通过 FastAPI 自动生成的 OpenAPI 展示补充接口示例与错误码说明Docstring
* 数据库变更记录:在迁移文件中含新增字段/表说明;在说明文档中列出字段语义与默认值(本次提交提供变更概要)。
## 单元测试计划
* 邮件发送:
* 伪造 `FakeEmailClient`,覆盖成功/失败/附件两种输入;比照 `tests/api/v1/test_sms.py` 的 monkeypatch 风格。
* 用户配额:
* 调额接口:前后值正确、日志记录正确、权限检查(需登录管理员)。
* 估值字段:
* 创建/更新时包含新字段URL 与正则校验失败用例;上传文件生成链接断言。
* 上传:
* 非法 MIME 与超限大小拒绝;合法文件成功返回 URL。
## 兼容性与回滚策略
* 所有新增字段均为可空或安全默认,旧数据不受影响。
* 新增接口均为新增路由,不改动原有行为;前端逐步切换数据源,保留 Mock 作为回退。
* 迁移脚本按标准生成;如需回滚 aerich 支持 `downgrade`
## 实施步骤
1. 模型与 Schema 更新;生成 Aerich 迁移;本地升级并验证。
2. 上传控制器扩展与新路由;估值控制器/输出字段同步。
3. 交易管理发送接口(含 SMTP 封装、日志落库、异常处理)。
4. 用户配额接口与日志模型/路由admin 列表与详情改造。
5. 权限入库与刷新;为接口添加函数级注释。
6. 单元测试编写与通过OpenAPI 检视;交付 API/DB 变更说明。
## 关键代码定位参考
* 路由注册:`app/api/v1/__init__.py:28-52`
* 用户端估值入口:`app/api/v1/app_valuations/app_valuations.py:233-318`
* 估值控制器:`app/controllers/valuation.py:73-100``app/controllers/user_valuation.py:21-42`
* 上传控制器:`app/controllers/upload.py:12-52`
* 发票抬头接口:`app/api/v1/invoice/invoice.py:118-144`
* 管理端用户列表(前端):`web/src/views/user-management/user-list/index.vue:69-166`
* Mock 数据与 API`web/src/api/index.js:279-352, 433-479`
——请确认方案后,我将按上述步骤开始落地实现、编写迁移与测试。

View File

@ -0,0 +1,71 @@
## 目标与范围
- 针对“估值二期”需求(用户端、管理端)设计完整 API去除 Webhook 回调。
- 对齐现有约定:认证 `POST /api/v1/base/access_token`app/api/v1/base/base.py:19-38`token` 请求头web/src/utils/http/interceptors.js:11-14响应 `Success/SuccessExtra/Fail`app/schemas/base.py成功码 `code===200`web/src/utils/http/interceptors.js:23-33估值域已有 `/api/v1/valuations`app/api/v1/valuations/valuations.py:21-191
## 核心流程
- 用户端:登录→评估提交→个人中心(汇款凭证、抬头选择、类型选择、发票列表/详情)→估值记录(下载证书/报告、分享、历史结果、剩余次数)。
- 管理端:交易管理(查看/核验/邮件/开票/状态)→用户管理(信息/操作/修改/审核/投诉/短信文案/证书与报告)→审核列表(上传证书/下载/重传)。
## 实体与关系
- AppUser ⇄ Valuation(1..n)、Invoice(1..n)、InvoiceHeader(n)
- Valuation ⇄ ValuationCalculationStep(1..n) ⇄ Certificate/Report
- Invoice ⇄ InvoiceHeader/PaymentReceipt/Transaction
- Complaint、SMSMessage 与用户/估值/发票按需关联
## 端点设计(与前端映射保持一致)
- 认证与用户
- POST `/api/v1/base/access_token` 登录app/api/v1/base/base.py:19-38
- GET `/api/v1/base/userinfo` 用户信息app/api/v1/base/base.py:40-46
- GET `/api/v1/app-user/profile` 当前用户画像与剩余估值次数
- GET `/api/v1/app-user/list`、GET `/api/v1/app-user/get`、POST `/api/v1/app-user/register|update`、DELETE `/api/v1/app-user/delete`(对齐 web/src/api/index.js:433-503
- 估值评估(沿用并扩展)
- POST `/api/v1/valuations/`、GET `/api/v1/valuations/`、GET `/api/v1/valuations/{id}`(已存在)
- GET `/api/v1/valuations/{id}/steps`(已存在,用于过程展示)
- GET `/api/v1/valuations/{id}/certificate`(新增:证书下载)
- GET `/api/v1/valuations/{id}/report`(新增:报告下载)
- POST `/api/v1/valuations/{id}/share`(新增:生成分享链接/小程序码,异步)
- POST `/api/v1/valuations/batch/delete`(已存在)
- 发票与交易(保留现有路径)
- GET `/api/v1/invoice/list`、GET `/api/v1/invoice/detail`、POST `/api/v1/invoice/create|update|send|remind|refund`、DELETE `/api/v1/invoice/delete`、POST `/api/v1/invoice/update-status`(对齐 web/src/api/index.js:504-725
- POST `/api/v1/invoice/{id}/receipt`(新增:上传付款凭证)
- GET `/api/v1/invoice/headers`、GET `/api/v1/invoice/headers/{id}`、POST `/api/v1/invoice/headers`(新增:抬头管理)
- POST `/api/v1/invoice/{id}/issue`(新增:开票,异步 Job
- 审核与证书
- GET `/api/v1/review/valuations` 审核列表(新增)
- POST `/api/v1/review/valuations/{id}/approve|reject`复用估值审核app/api/v1/valuations/valuations.py:167-183
- POST `/api/v1/review/valuations/{id}/certificate` 上传证书(新增)
- PUT `/api/v1/review/valuations/{id}/report` 重传报告(新增)
- 投诉与短信
- GET `/api/v1/complaints`、GET `/api/v1/complaints/{id}`、PUT `/api/v1/complaints/{id}`(新增)
- GET `/api/v1/sms/templates`、POST `/api/v1/sms/templates`、POST `/api/v1/sms/send`(新增)
## 请求/响应格式与认证
- 统一 JSON请求头 `token` 必填(除登录与公共资源)。
- 成功:`{code:200,data:...,msg:"success"}`;失败:`{code:4xx/5xx,msg:"错误"}`
## 字段与校验(示例)
- ValuationCreate`asset_name(1-64)`, `institution(1-128)`, `industry(1-64)`, `heritage_level?`, `inputs(object)`, `attachments?[url[]]`
- InvoiceCreate`ticket_type(electronic|paper)`, `invoice_type(special|normal)`, `phone`, `email`, `company_name`, `tax_number`, `register_address`, `register_phone`, `bank_name`, `bank_account`
- PaymentReceipt`url`, `uploaded_at`, `verified`
- ShareRequest`channel(miniprogram|link)`, `expire(<=604800)`
- 规则:邮箱/手机号/税号格式;枚举校验;附件数量与大小限制。
## 错误码
- 200 成功400 参数错误401 未认证前端自动登出web/src/utils/http/interceptors.js:45-53403 无权限404 不存在409 冲突422 校验失败429 频率限制500 内部错误。
## 批量与异步(无 Webhook
- 批量:发票批量开具、邮件批量发送(`POST /api/v1/invoice/batch/issue|send`)。
- 异步 Job开票/报告/分享生成返回 `job_id`;查询 `GET /api/v1/jobs/{id}``status: pending|running|success|failed`)。客户端采用轮询或前端提示重试。
## 性能目标
- 登录/用户信息 P95 ≤ 100ms列表分页 P95 ≤ 200ms单页 ≤ 100 条)。
- 异步任务完成 ≤ 5sQPS单实例读 200+/s、写 50+/s。
## 实施建议
1. 在 `app/api/v1/` 新增 invoices/reviews/complaints/sms 路由文件。
2. 在 `controllers/` 实现控制器,复用 `ValuationController` 的计算步骤记录app/controllers/valuation.py:24-53
3. 在 `schemas/` 新增/扩展 Pydantic 模型,严格校验。
4. 增加 Job 状态查询端点,统一返回结构;无 Webhook 的情况下采用客户端轮询。
5. 前端按 `web/src/api/index.js` 对齐接入,复用错误处理与 401 登出。
——请确认该无 Webhook 版本的 API 方案,确认后我将开始后端路由/控制器/模型实现并提供前端对接示例。

View File

@ -0,0 +1,184 @@
## 目标
* 完整设计并落实“估值计算步骤”API与落库机制保证
1. 用户提交估值后,所有中间计算步骤按规范写入数据库;
2. 管理端在详情中查看完整步骤链条与中间结果;
3. 统一数学公式、变量来源、步骤编号与展示结构。
## 现有能力与锚点
* 步骤模型:`ValuationCalculationStep`app/models/valuation.py:88-107
* 步骤写入:控制器提供创建/查询app/controllers/valuation.py:24-53, 37-53
* 管理端步骤查询:`GET /api/v1/valuations/{id}/steps`app/api/v1/valuations/valuations.py:50-56
* 已有示例记录风险调整B3模块内已演示步骤写入app/utils/calculation\_engine/risk\_adjustment\_b3/sub\_formulas/risk\_adjustment\_b3.py:195-237
* 用户端计算入口后台任务执行统一计算app/api/v1/app\_valuations/app\_valuations.py:210-299
## 公式总览与数学表达
1. 经济价值 B1economic\_value\_b1
* 基础价值 B11依据财务与法律/创新、普及度
* 示例表达:`B11 = w_f * f(three_year_income) + w_i * innovation_ratio + w_p * popularity_score + w_l * infringement_factor + w_pat * patent_score`
* 流量因子 B12`S = α * S1 + β * S2 + γ * S3`;其中 S1 搜索指数(百度/微信/微博S2 行业均值S3 社交传播(点赞/评论/分享)
* 政策乘数 B13`P = p_impl * implementation_stage_score + p_fund * funding_support_score`
* 汇总:`B1 = B11 * (1 + θ * S) * (1 + λ * P)`
1. 文化价值 B2cultural\_value\_b2
* 活态传承 B21`B21 = κ1 * inheritor_level_coefficient + κ2 * offline_sessions + κ3 * social_views`
* 纹样基因 B22`B22 = μ1 * historical_inheritance + μ2 * structure_complexity + μ3 * normalized_entropy`
* 汇总:`B2 = B21 + B22`
1. 风险调整 B3risk\_adjustment\_b3
* 风险评分总和:`R = 0.3 * market_risk + 0.4 * legal_risk + 0.3 * inheritance_risk`
* 风险调整系数:`B3 = 0.8 + 0.4 * R`app/utils/.../risk\_adjustment\_b3.py:33-45, 47-66
1. 市场价值 Cmarket\_value\_c
* 竞价 C1`C1 = weighted_average_price(transaction_data, manual_bids, expert_valuations)`
* 热度系数 C2`C2 = ψ1 * daily_browse_volume + ψ2 * collection_count`
* 稀缺性乘数 C3`C3 = φ(circulation)`(限量>稀缺性高)
* 时效性衰减 C4`C4 = decay(recent_market_activity)`
* 汇总:`C = C1 * (1 + C2) * C3 * C4`
1. 最终估值 ABfinal\_value\_ab
* 模型估值 B`B = B1 + B2`;再叠加风险调整:`B_adj = B * B3`
* 市场估值:`C`
* 最终:`Final = f(B_adj, C)`(例如加权平均或规则合成)
## 变量定义与来源映射
* 用户输入UserValuationCreateapp/schemas/valuation.py:144-147
* `three_year_income``annual_revenue``rd_investment``application_coverage``offline_activities``platform_accounts``sales_volume``link_views``circulation``last_market_activity``price_fluctuation``funding_status``implementation_stage``patent_application_no``historical_evidence``pattern_images``inheritor_level``inheritor_age_count`
* 系统/API来源
* 搜索指数S1、行业均值S2、社交传播S3app/api/v1/app\_valuations/app\_valuations.py:328-347, 333-343
* ESG分、行业系数、政策匹配度app/api/v1/app\_valuations/app\_valuations.py:47-80
* 侵权/专利校验app/api/v1/app\_valuations/app\_valuations.py:81-118
## 计算步骤落库设计
* 统一步骤结构app/schemas/valuation.py:239-259
* `step_order`:序号(含小数层级,如 1.11, 2.31
* `step_name`中文名称如“基础价值B11计算”
* `step_description`:公式与解释
* `input_params`:输入参数 JSON含变量与其来源
* `output_result`:中间结果(如每项得分,最终值)
* `status``in_progress|completed|failed`
* `error_message`:失败描述
* 步骤编号建议:
* 经济价值 B12.1xB11=2.11B12=2.12B13=2.13汇总B1=2.19
* 文化价值 B22.2xB21=2.21B22=2.22汇总B2=2.29
* 风险调整 B32.3x总评R=2.30B3=2.31
* 市场价值 C3.1xC1=3.11C2=3.12C3=3.13C4=3.14汇总C=3.19
* 最终估值 AB4.1xB组合=4.11B×B3=4.12Final=4.19
* 落库时机统一在后台任务中分模块记录app/api/v1/app\_valuations/app\_valuations.py:38-41, 142-171
* 写入方式:通过控制器 `create_calculation_step`app/controllers/valuation.py:24-36
* 已有范例风险调整B3模块先 `in_progress``completed`app/utils/.../risk\_adjustment\_b3.py:195-237
## 完整流程说明
1. 原始数据输入:`POST /api/v1/app-valuations/`app/api/v1/app\_valuations/app\_valuations.py:210-299
2. 后台任务提取参数B1/B2/B3/Capp/api/v1/app\_valuations/app\_valuations.py:302-567
3. 模块计算与步骤记录:按编号分别执行,逐步写入 `ValuationCalculationStep`
4. 汇总合成:计算 `model_value_b``market_value_c``final_value_ab``dynamic_pledge_rate` 并存入 `ValuationAssessment`
5. 管理端查看:
* 详情:`GET /api/v1/valuations/{id}`(返回序列化后的详情)
* 步骤:`GET /api/v1/valuations/{id}/steps`(返回序列化后的步骤数组)
## 示例计算过程(模拟数据)
* 输入(节选):
* `three_year_income=[400,450,500]``annual_revenue=500``rd_investment=50`(创新投入比=10%
* `application_coverage=全国覆盖`popularity\_score→由B11计算器给分`offline_activities=12`
* `platform_accounts.douyin.likes=1200`S3参数`price_fluctuation=[95,105]`(波动率)
* `inheritor_level=市级传承人`(转换为系数)、`inheritor_age_count=[45,60,75]`
* `historical_evidence={历史文献:3, 考古发现:2, 传承谱系:5}`
* 步骤样例:
* 2.11 基础价值B11`input_params={three_year_income, innovation_ratio, popularity_score, infringement_score, patent_score}``output_result={B11: 123.45}`
* 2.12 流量因子B12`input_params={S1,S2,S3}``output_result={S: 0.32}`
* 2.13 政策乘数B13`input_params={implementation_stage,funding_support}``output_result={P: 0.15}`
* 2.19 B1汇总`output_result={B1: 156.78}`
* 2.21 活态传承B21`input_params={inheritor_level_coefficient,offline_sessions, social_views}``output_result={B21: 10.2}`
* 2.22 纹样基因B22`input_params={historical_inheritance,structure_complexity,normalized_entropy}``output_result={B22: 8.9}`
* 2.30 风险总评R`input_params={market_risk,legal_risk,inheritance_risk}``output_result={R: 0.42}`
* 2.31 风险调整B3`output_result={B3: 0.97}`
* 3.11~~3.14 市场价值子项:分别写入 C1~~C4
* 3.19 市场价值C`output_result={C: 118.0}`
* 4.11/4.12/4.19 最终汇总:`output_result={B: 175.88, B_adj: 170.6, Final: 122.0}`
## 后台展示规范
* 列表返回序列化后的 Pydantic 对象,避免 JSONResponse 序列化错误(已在管理端端点处理)
* 步骤展示:按照 `step_order` 升序,逐条显示 `step_name``step_description``input_params``output_result``status`;失败步骤显示 `error_message`
## 实施项
1. 将 B1、B2、C 模块对齐 B3 的“步骤写入”模式:每个子公式在计算前记录 `in_progress`,完成后记录 `completed` 并带结果;异常时标记 `failed`
2. 在 `FinalValueACalculator` 合成阶段补充步骤记录B组合、B×B3、Final
3. 确保管理端详情与步骤返回统一进行 JSON 序列化(管理端端点已按 `model_dump_json()` 修复)。
## 交付
* 我将按上述规范逐步在计算引擎各子模块与统一计算入口中补充“步骤写入”,并确保管理端端点返回可序列化的数据结构;完成后会提供一份面向管理员的“估值步骤查看”前后端对接说明(端点与字段)。

View File

@ -0,0 +1,23 @@
## 问题
- 后台任务在提取B1参数时引用未定义函数calculate_popularity_score、calculate_patent_score导致计算中止步骤未入库。
## 修复方案
1) 移除未定义函数引用,在 `_extract_calculation_params_b1` 内实现本地计算:
- 普及地域分mapping {全球覆盖:10全国覆盖:7区域覆盖:4}默认7
- 专利分:按剩余年限近似 {>10年:105-10年:7<5年:3}用已有 `calculate_total_years(data_list)` 近似转换
- 保留创新投入比与搜索指数、行业均值等现有逻辑
- 增加logger输出popularity_score、innovation_ratio、patent_score
2) 确保步骤写入链路:
- 计算入口:先创建估值记录取 `valuation_id`;将 `valuation_id` 传入统一计算器;计算后用 `ValuationAssessmentUpdate` 更新记录
- 模型B汇总对经济B1、文化B2、风险B3的计算改为 `await` 并传 `valuation_id`
- 市场C与风险B3保持原有按 `valuation_id` 写入
3) 日志增强:
- 在步骤创建与更新时写入日志calcstep.create/update/list
- 在估值初始化与更新时写入日志valuation.init_created/updated
## 验证
- 重启服务、运行脚本查看服务日志应出现calcstep.create/update/list与valuation.updatedAdmin脚本打印“后台估值计算步骤”。
——确认后我将按以上方案进行代码调整并回填日志。

View File

@ -0,0 +1,29 @@
## 目标
- 汇总所有前后端需求与 API 到一个文档,其他分散文档删除,仅保留总览,便于统一查看与维护。
## 操作与产物
- 新增单一文档:`docs/估值二期-需求与API总览.md`
- 内容包含:
1) 概述与约定(认证、响应包装、错误码、前缀)
2) 数据实体InvoiceHeader、PaymentReceipt、TransactionRecord、Valuation、AppUser、Complaint、SMSMessage与字段定义
3) 用户端流程与 API登录/首页摘要、评估提交/校验/状态、估值记录下载与分享、个人中心-对公转账、发票抬头/类型、发票列表与详情、剩余次数、投诉与短信、批量与异步Job
4) 管理端流程与 API交易管理记录列表/详情/状态/邮件/开票/批量;审核列表;用户管理)
5) 前端对接映射(`web/src/api/index.js` 现有与新增占位)
6) 请求/响应示例与字段校验(重点:抬头管理、对公转账、交易管理记录)
7) 性能与限制(分页、异步、频率)
- 删除分散文档:
- `web/docs/项目结构.md``web/docs/技术架构.md``web/docs/关键模块说明.md``web/docs/接口约定与预留.md`
- `app/docs/项目结构.md``app/docs/技术架构.md``app/docs/关键模块说明.md``app/docs/接口约定与预留.md`
## 你提出的新增明确项(将详列在文档中)
- 抬头管理字段(必填项标注):公司名称、公司税号、注册地址、注册电话、开户银行、银行账号、邮箱
- 对公转账(用户端):上传图片(付款凭证)+ 选择发票抬头 + 开票类型(电子/纸质、专票/普票)
- 交易管理记录(管理端):提交时间、付款凭证、手机号、微信号、公司名称、公司税号、注册地址、注册电话、开户银行、银行账号、接收邮箱、开票类型、状态
- 状态枚举pending/verified/issued/rejected 等
## 执行步骤
1. 创建总览文档并写入上述结构内容与端点清单
2. 迁移现有 app/web 文档中的有效信息到总览文档
3. 删除分散文档,仅保留总览
——请确认上述方案,确认后我将生成唯一的总览文档并删除分散文档。

View File

@ -0,0 +1,26 @@
## 问题
- 计算步骤创建时报 Pydantic 校验错误:`step_order` 期望整型,但代码使用层级小数(如 2.1、2.11)。这不是计算公式问题,而是“类型不匹配”导致步骤未入库。
## 修复原则
- 不改变任何计算公式或数值流程,仅调整“步骤顺序”的存储与校验类型,使其能接受层级小数。
## 具体改动
1) 模型字段修改(不涉公式):
- `app/models/valuation.py``ValuationCalculationStep.step_order: IntField → DecimalField(max_digits=8, decimal_places=3)`;保留 `ordering=["step_order"]`,确保排序正确。
2) Schema 修改(不涉公式):
- `ValuationCalculationStepCreate.step_order: int → Decimal`,添加前置校验,支持 int/float/str 自动转换为 Decimal`ValuationCalculationStepOut` 同步为 Decimal。
- 列表与详情端点已使用 `model_dump_json()``json.loads()`Decimal 会被正确序列化为 JSON 数字,无需改动。
3) 代码调用无需改(不涉公式):
- 由于 Schema 接受 float 并转换为 Decimal现有调用处传入 `2.1/2.11/...` 不需改。
4) 迁移与验证
- 启动时执行 Aerich 迁移更新列类型(项目已有初始化流程)。
- 跑脚本观测:`calcstep.create` 不再报错;`calcstep.list` 数量 > 0后台“估值计算步骤”返回完整数组。
## 影响范围与安全性
- 仅变更“步骤顺序”的字段类型与 Schema 校验,不触及任何计算逻辑或公式。
- 排序按照 Decimal 正常工作层级表达2.11 < 2.2保留
——确认后,我将按以上方案修改模型与 Schema并执行验证保证不改变计算逻辑仅解决类型不匹配问题。

View File

@ -0,0 +1,17 @@
## 目标
- 让每次用户估值的所有中间步骤写入 `valuation_calculation_steps` 并可关联该估值ID
- 测试脚本打印详细步骤链,包括 step_order、step_name、step_description、input_params、output_result、status
## 代码改动
1) 计算入口 `_perform_valuation_calculation`
- 先创建估值记录以拿到 `valuation_id`
- 传 `valuation_id``FinalValueACalculator.calculate_complete_final_value_a`
- 计算完成后用 `ValuationAssessmentUpdate` 将结果更新到该记录
2) 测试脚本:
- 在 AdminClient 增加 `valuation_steps(id)` 方法
- 打印步骤数组,包含名称、描述、输入与输出
## 验证
- 运行 `python run.py`
- 运行脚本并查看详细步骤输出

View File

@ -0,0 +1,99 @@
## 目标与范围
- 接入阿里云短信服务,封装发送客户端
- 提供两类发送接口:验证码通知、报告生成通知,供 App 调用
- 支持模板动态调用与验证码变量 `${code}` 的正确替换
- 记录发送日志并融入现有审计体系
- 实现同一手机号每分钟不超过 1 条的频率限制
- 安全存储 AccessKey 等敏感信息(环境变量/配置)
## 技术选型
- 后端框架FastAPI现有工程
- 短信 SDKAlibaba Cloud SMS Python SDKTea/OpenAPI V2`alibabacloud_dysmsapi20170525`
- 端点(中国站):`dysmsapi.aliyuncs.com`
- 关键请求字段:`PhoneNumbers``SignName``TemplateCode``TemplateParam`
- 日志:沿用 `app/log` 的 Loguru 与审计中间件
- 频率限制:服务内共享的内存限流(后续可升级为 Redis
- 安全通过环境变量注入凭证Pydantic Settings 读取
- 参考文档:
- Alibaba Cloud SDK V2Python示例SendSmshttps://www.alibabacloud.com/help/en/sdk/developer-reference/v2-python-integrated-sdk
- 短信服务 SendSms 接口2017-05-25https://help.aliyun.com/zh/sms/developer-reference/api-dysmsapi-2017-05-25-sendsms
## 代码改动
- 新增:`app/services/sms_client.py`
- 初始化 Dysms 客户端(读取 `ALIBABA_CLOUD_ACCESS_KEY_ID``ALIBABA_CLOUD_ACCESS_KEY_SECRET``ALIYUN_SMS_SIGN_NAME``ALIYUN_SMS_ENDPOINT`
- 方法:`send_by_template(phone, template_code, template_param_json)`
- 方法:`send_code(phone, code)`(模板:`SMS_498190229`
- 方法:`send_report(phone)`(模板:`SMS_498140213`
- 新增:`app/services/rate_limiter.py`
- 类:`PhoneRateLimiter`,键为手机号,值为最近一次发送时间戳;判定 60s 内拒绝
- 新增路由:`app/api/v1/sms/sms.py`
- `POST /api/v1/sms/send-code`(无鉴权,用于登录场景)
- `POST /api/v1/sms/send-report`(需要鉴权,防滥用)
- 统一返回结构:`{status, message, request_id}`
- 路由聚合:在 `app/api/v1/__init__.py` 注册 `sms_router(prefix="/sms", tags=["短信服务"])`
- 配置:扩展 `app/settings/config.py`Pydantic Settings增加短信相关字段并从环境读入
## 接口设计
- `POST /api/v1/sms/send-code`
- 请求体:`{ "phone": "1390000****", "code": "123456" }`
- 处理:限流校验 → 构造 `TemplateParam``{"code": "123456"}` → 调用 `SMS_498190229`
- 成功:`{ "status": "OK", "message": "sent", "request_id": "..." }`
- 失败:`{ "status": "ERROR", "message": "..." }`
- `POST /api/v1/sms/send-report`
- 请求体:`{ "phone": "1390000****" }`
- 处理:鉴权(`DependAuth`)→ 限流校验 → 调用 `SMS_498140213`
- 返回同上
- 校验:手机号格式(支持无前缀或 `+86``code` 为 48 位数字(可按需约束)
## 模板与变量替换
- 验证码模板:`SMS_498190229`
- `TemplateParam``{"code": "<动态验证码>"}``${code}` 正确对应
- 报告通知模板:`SMS_498140213`
- 不含变量,可传空对象 `{}` 或不传 `TemplateParam`
- 签名:`ALIYUN_SMS_SIGN_NAME` 读取为“成都文化产权交易所”且不在代码中硬编码
## 日志与审计
- 路由层:审计中间件自动记录请求/响应(`module=短信服务``summary=验证码发送/报告通知发送`
- 服务层:`from app.log import logger`
- 发送开始、Provider 请求入参(不含敏感信息)、返回码、`RequestId`、耗时、失败异常
- 敏感信息不入日志AccessKey、完整模板内容不打印
## 频率限制
- 策略:同一手机号在 60 秒内全模板合并限 1 次(共享窗口)
- 实现:进程内 `dict[phone]=last_ts`;进入路由先校验再发送;返回 429或业务错误码
- 进阶:如需多实例一致性,后续接入 Redis`sms:limit:{phone}` TTL=60s
## 安全与配置
- 环境变量:
- `ALIBABA_CLOUD_ACCESS_KEY_ID`
- `ALIBABA_CLOUD_ACCESS_KEY_SECRET`
- `ALIYUN_SMS_SIGN_NAME`
- `ALIYUN_SMS_ENDPOINT`(默认 `dysmsapi.aliyuncs.com`
- Pydantic Settings 统一读取,避免硬编码,并在 `/docs` 与审计中隐藏敏感字段
## 依赖与安装
- 在 `pyproject.toml` 添加:
- `alibabacloud_dysmsapi20170525`
- `alibabacloud_tea_openapi`
- `alibabacloud_tea_util`
- 与 `requirements.txt` 保持一致版本钉死策略Python 3.11 兼容性验证
## 测试与验证
- 单元测试:
- Mock SDK 客户端,校验 `TemplateCode``TemplateParam` 的正确构造
- 限流:同号 60s 内第二次返回限制错误
- 集成测试:
- 使用 `httpx.AsyncClient` 调用两个接口并断言响应结构
- 在预设测试手机号上进行真实发送,观察到达与模板内容正确
- 观测:
- 查看应用日志与审计表(`AuditLog`)记录
## 风险与回滚
- 进程内限流仅在单实例有效,多实例需 Redis后续迭代
- SDK 版本冲突,采用独立最小版本并逐项验证;必要时锁版本
- 若出现发送失败,保留错误码与 `RequestId`,按官方错误码表排查(见 SendSms 文档)
## 交付物
- 新增短信客户端与路由模块
- 两个可调用接口(验证码发送、报告通知发送)
- 限流与日志落地,配置基于环境变量

View File

@ -0,0 +1,41 @@
## 目标
- 编写一个一次性可运行的接口测试脚本,按照总览文档顺序执行:
1) App 用户注册 → 登录 → 用户相关接口
2) 提交估值(用户端)并轮询结果(列表/详情)
3) 管理端登录admin→ 查看估值数据(列表/详情)
- 输出结构化结果与关键字段校验,便于快速人工检查。
## 脚本位置与运行
- 路径:`scripts/api_smoke_test.py`
- 运行:`python scripts/api_smoke_test.py --base http://127.0.0.1:9991/api/v1`
- 基础:默认使用 `http://127.0.0.1:9991/api/v1`(根据 `run.py`),可通过参数覆盖。
## 步骤与端点
1. App 用户注册与登录
- POST `/app-user/register`(若手机号已存在则跳过)
- POST `/app-user/login` → 获取 `access_token`
- GET `/app-user/profile`、GET `/app-user/dashboard`、GET `/app-user/quota`
2. 用户端估值
- POST `/app-valuations/`(使用总览文档示例数据,最小必要字段)
- 轮询 GET `/app-valuations/`(分页)查看是否新增记录,捕获 `id`
- GET `/app-valuations/{id}` 详情
3. 管理端数据查看
- POST `/base/access_token`admin/123456获取后端 `token`
- GET `/valuations` 列表(后台视角)
- GET `/valuations/{id}` 详情(与用户端一致性对比)
## 输出与校验
- 每步打印:请求路径、状态码、关键字段(如 `access_token``user_id``latest_valuation``final_value_ab`
- 断言:登录成功、列表包含新记录、详情字段存在。
- 错误处理:捕获非 200 情况并打印 `code/msg`
## 技术细节
- 使用 `requests``Session` 维护 `token`;用户端与后台端各独立 `Session`
- 函数化:为每个步骤提供函数与函数级注释(描述、参数、返回值)。
- 兼容:对注册接口“手机号已存在”返回情况做兼容(脚本继续执行)。
## 交付
- 创建 `scripts/api_smoke_test.py` 并填充完整逻辑;默认数据内置,必要处留参数。
- 如你需要,我可在脚本创建后直接运行,输出结果供你检查。

View File

@ -0,0 +1,20 @@
## 目标
- 将 `scripts/api_smoke_test.py` 的示例负载替换为你提供的完整参数,保持后端现有计算逻辑不变。
## 具体改动
- 替换 `build_sample_payload()` 返回值为你提供的 JSON字段逐项对齐
- `asset_name``institution``industry``annual_revenue``rd_investment``three_year_income``funding_status``sales_volume``link_views``circulation``last_market_activity``monthly_transaction``price_fluctuation``application_maturity``application_coverage``cooperation_depth``offline_activities``inheritor_level``inheritor_age_count``inheritor_certificates``heritage_level``historical_evidence``patent_certificates``pattern_images``patent_application_no``heritage_asset_level``inheritor_ages``implementation_stage``coverage_area``collaboration_type``scarcity_level``market_activity_time``monthly_transaction_amount``platform_accounts`
- 保留 `platform_accounts`douyin作为后端期望的数据源`online_accounts`(数组)不参与当前计算,保留或忽略均可;默认保留以便后续扩展。
- `application_coverage`:后端当前使用该字段计算普及分;你的参数中同时有 `coverage_area`,将按优先 `application_coverage` 使用;若 `application_coverage` 为占位如“0”建议同步设置为“全球覆盖”或我在脚本中用 `coverage_area` 回填)。
- 其余数值以字符串提供,后端已通过 `safe_float` 做转换,无需脚本侧强制转数值。
## 兼容与注意
- 不改计算逻辑;仅更新脚本负载以贴合后端字段期望。
- 保持 `AdminClient` 输出“后台估值详情”和“后台估值计算步骤”打印,便于你核验。
## 验证
- 启动后端并确保迁移已执行(`step_order` 已支持 Decimal
- 运行脚本:`python scripts/api_smoke_test.py --base http://127.0.0.1:9991/api/v1 --phone 13800138001`
- 观察输出:用户侧成功提交,后台列表/详情显示完整数据,步骤列表非空。
——确认后我将直接更新脚本并提交。

View File

@ -0,0 +1,48 @@
## 目标
* 将“用户端”API补齐内容更新到现有文档便于你审阅。
## 更新范围
* 修改 `app/docs/接口约定与预留.md`:新增“用户端 API”章节覆盖登录/首页/评估/个人中心/估值记录/通知/批量与异步,与错误码和校验对齐。
* 修改 `web/docs/接口约定与预留.md`:新增“前端对接(用户端)”章节,列出与 `web/src/api/index.js` 的映射与新增端点占位,确保路径与请求方式一致。
## 文档结构变更
* `app/docs/接口约定与预留.md`
* 新增:
* “用户端 API 概览”
* “认证与首页”
* “评估提交与引导提示”
* “估值记录与分享”
* “个人中心:汇款凭证/发票抬头与类型/发票列表与详情”
* “剩余估值次数”
<br />
* `web/docs/接口约定与预留.md`
* 新增:
* “用户端对接路径”与现有 `invoice/*`、`valuation/*`、`app-user/*` 的映射
* 新增端点建议(如 `app-valuation/*`、`invoice/headers`、`invoice/{id}/receipt`、`jobs/{id}`)的前端占位说明
## 风格与格式
* 统一中文、RESTful端点风格示例以JSON格式。
* 保持与现有文档用语一致Success/Fail、token、code===200
## 交付
* 完成上述两处文档更新不新增新文档文件更新内容可直接在IDE中查看。

View File

@ -0,0 +1,103 @@
## 工作范围与目标
- 范围:梳理 `web` 目录(排除 `web1`),形成结构与架构产物
- 目标:
- 生成完整项目结构文档(目录树、职责说明、关键路径)
- 制作技术架构示意图(前端分层、运行时链路、对后端的调用关系)
- 编写关键模块说明文档路由、状态、鉴权、HTTP、页面域
- 预留接口说明,支撑后续开发接入
## 技术栈识别
- 框架Vue 3`web/package.json:37`构建Vite`web/package.json:43``web/vite.config.js:1`
- 路由vue-router@4`web/package.json:39`);守卫统一注册(`web/src/router/guard/index.js:5-9`
- 状态Pinia`web/package.json:26``web/src/store/index.js:1-5`
- UI 与样式Naive UI`web/package.json:25`UnoCSS`web/package.json:30``web/src/main.js:3`),全局样式(`web/src/styles/global.scss`
- 国际化vue-i18n`web/package.json:38``web/src/main.js:22``web/i18n/index.js`
- 网络Axios`web/package.json:20`),自建请求封装(`web/src/utils/http/index.js:4-18`)与拦截器(`web/src/utils/http/interceptors.js:23-33,35-59`
- 运行环境:`.env.*` 配置,开发代理到后端 `127.0.0.1:9999``/api/v1``web/.env.development:8``web/build/constant.js:19-22``web/vite.config.js:31-35`
## 项目结构综述
- 顶层关键目录:
- `build/`Vite 定制化(定义、插件、脚本、代理)(`web/build/*`
- `i18n/`:国际化资源与实例(`web/i18n/index.js``web/i18n/messages/*`
- `settings/`:主题与全局设置(`web/settings/theme.json``web/settings/index.js`
- `public/`:静态资源与加载占位(`web/public/resource/*`
- `src/`:业务主目录(见下)
- `src/` 结构与职责:
- 入口与应用:`main.js`(应用装配,挂载插件)(`web/src/main.js:14-23``App.vue`
- 路由:`router/`(基本路由、动态路由、守卫、滚动)(`web/src/router/index.js:7-18,30-55`
- 状态:`store/`Pinia 注册与模块聚合)(`web/src/store/index.js:1-5``web/src/store/modules/index.js:1-4`
- 组件:`components/`(通用、表格、查询栏、图标、页面容器)
- 视图:`views/`(系统管理、估值评估、交易开票、登录、工作台等域)
- 工具:`utils/`鉴权、存储、HTTP、通用工具`web/src/utils/*`
- 指令:`directives/`(权限等自定义指令)
- 可复用逻辑:`composables/`(如 `useCRUD`
- 样式:`styles/`Reset、全局样式UnoCSS 原子类)
## 核心模块与功能点
- 鉴权与导航:
- 登录白名单与重定向(`web/src/router/guard/auth-guard.js:3-16`
- 动态路由注入、用户与权限联动(`web/src/router/index.js:30-55`
- 状态管理:
- 用户信息获取、登出流程(`web/src/store/modules/user/index.js:37-60`
- 标签、权限、应用模块聚合(`web/src/store/modules/index.js:1-4`
- 网络与错误处理:
- `request` 实例与 `baseURL` 环境绑定(`web/src/utils/http/index.js:17-19`
- 成功码约定 `code === 200`、统一错误提示(`web/src/utils/http/interceptors.js:23-33`
- 401 处理与自动登出(`web/src/utils/http/interceptors.js:45-53`
- 业务域:
- 系统管理(用户、角色、菜单、部门、审计日志)(`web/src/views/system/*`API 汇总:`web/src/api/index.js:393-431`
- 客户端用户管理、开票记录、估值评估(`web/src/views/user-management/*``web/src/views/transaction/invoice/*``web/src/views/valuation/*`;对应 API`web/src/api/index.js`
- UI 框架与布局:
- 布局与头部/侧栏/标签页组件(`web/src/layout/*`
- Naive UI 组件与 UnoCSS 原子化样式协同
## 技术架构示意图
```mermaid
graph TD
U[用户] --> A[Vue 应用]
A --> R[Router]
A --> S[Pinia]
A --> V[视图与组件]
V --> UI[Naive UI / UnoCSS]
A --> I18N[vue-i18n]
S --> H[HTTP 封装]
R --> G[路由守卫]
H --> X[Axios 拦截器]
X --> B[(后端 API /api/v1)]
subgraph 构建与运行
Vite[Vite Dev/Build]
end
A --> Vite
```
## 文档产出方案
- 目录与位置:在 `web/docs/` 下生成 4 个文档,统一中文、层级清晰、可落地
- `项目结构.md`:目录树 + 角色说明 + 入口与关键路径
- `技术架构.md`:架构分层 + 运行链路 + Mermaid 图
- `关键模块说明.md`路由、状态、HTTP、业务域的职责与协作
- `接口约定与预留.md`:环境、鉴权、响应约定、扩展接入指引
- 文档格式约定:
- 标题层级H1 总览H2 模块H3 文件与职责;统一术语与中文阐述
- 代码引用统一用内联反引号与文件定位(如 `web/src/router/index.js:30-55`
## 接口预留说明(用于后续开发)
- 基础约定:
- `baseURL``VITE_BASE_API`(默认 `/api/v1``web/.env.development:8`
- 认证头:`token`(由拦截器自动注入,`web/src/utils/http/interceptors.js:11-14`
- 成功响应:`{ code: 200, data, msg }``web/src/utils/http/interceptors.js:23-33`
- 接入方式:在 `web/src/api/index.js` 中以函数方式声明对应业务端点,统一走 `request`
- 错误处理:全局弹窗与 401 自动登出链路已就绪(`web/src/utils/http/interceptors.js:45-53`
## 执行步骤
1. 固化目录树与职责说明,输出《项目结构.md》
2. 绘制 Mermaid 架构图并输出《技术架构.md》
3. 编写《关键模块说明.md》覆盖路由、状态、HTTP、页面域
4. 编写《接口约定与预留.md》包含新增接口接入模板与约束
5. 交付文档后,等待新需求文档,启动开发
## 输出验收与规范
- 文档格式:统一中文,标题层级一致,引用路径与行号定位
- 风格一致:术语与代码片段与现有实现保持一致(如 `request``useUserStore`
- 可演进:接口文档预留扩展章节,支持后续模块按同规范接入
——请确认以上方案,确认后我将按该方案生成 4 个文档并提交供评审。

View File

@ -1,23 +0,0 @@
# Repository Guidelines
## Project Structure & Module Organization
FastAPI backend code lives in `app/`: routers under `app/api/v1`, orchestration in `app/controllers`, schemas/models in `app/schemas` and `app/models`, and shared helpers in `app/utils`. Config defaults stay in `app/settings/config.py`, migrations in `migrations/`, and the service boots through `run.py`. Frontend assets reside in `web/` with source code in `web/src`, static files in `web/public`, and build toggles in `web/settings`; deployment collateral sits in `deploy/`.
## Build, Test, and Development Commands
- `make install` (uv) or `pip install -r requirements.txt` prepares backend deps; `pnpm i` handles `web/`.
- `make start` / `python run.py` launches the API against `db.sqlite3`; `cd web && pnpm dev` starts the SPA; `pnpm build` prepares production assets.
- `make check` runs Black+isort in check mode plus Ruff; `make format` applies fixes; `make lint` is Ruff-only.
- `make test` loads `.env` variables into the shell and executes `pytest -vv -s`; target files with `pytest tests/api/test_x.py -k keyword`.
- Database maintenance: `make migrate` (generate Aerich migrations), `make upgrade` (apply), `make clean-db` (reset SQLite + migrations).
## Coding Style & Naming Conventions
Python follows Black (120 columns), isorts Black profile, and Ruff; keep modules snake_case and Pydantic models PascalCase. Vue code respects the repo ESLint + UnoCSS presets, uses TypeScript script blocks, and keeps component directories kebab-case; run `pnpm lint` or `pnpm lint:fix` as needed.
## Testing Guidelines
Back-end features need pytest coverage mirroring the `app` layout—e.g., `tests/api/v1/test_users.py` for router logic and async tests following the patterns in `test_dynamic_default.py`. Seed deterministic data via fixtures instead of the shared `db.sqlite3`, and document any `.env` flags a test requires. Frontend changes should gain vitest or Playwright checks under `web/tests` before UI regressions reach `main`.
## Commit & Pull Request Guidelines
Stick to Conventional Commit prefixes already present (`feat:`, `refactor:`, `debug:`) and keep subject lines imperative with optional scopes (`feat(api):`). Each PR must summarize changes, list verification commands, reference related issues, and attach UI screenshots/GIFs when touching `web/`. Run `make check` and relevant tests locally, avoid committing `web/dist` or SQLite WAL files, and prefer small, reviewable diffs.
## Security & Configuration Tips
Secrets belong in `.env`, which `app/settings/config.py` loads automatically; rotate `SECRET_KEY`, JWT parameters, and database credentials before deployment. Swap the Tortoise connection from SQLite to MySQL/PostgreSQL by editing the provided templates and running `make migrate && make upgrade`. Lock down CORS (`CORS_ORIGINS`) before exposing the API publicly.

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,36 @@ 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 .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", dependencies=[DependAuth, DependPermission], 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-短信服务"])

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,76 @@
from fastapi import APIRouter, Query, Depends, HTTPException
from typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
from app.schemas.app_user import AppUserQuotaUpdateSchema, AppUserQuotaLogOut
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),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
):
qs = AppUser.filter()
if phone:
qs = qs.filter(phone__icontains=phone)
if wechat:
qs = qs.filter(alias__icontains=wechat)
total = await qs.count()
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
items = []
for u in rows:
items.append({
"id": u.id,
"phone": u.phone,
"wechat": u.alias,
"created_at": u.created_at.isoformat() if u.created_at else "",
"notes": "",
"remaining_count": int(getattr(u, "remaining_quota", 0) or 0),
"user_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="用户不存在")
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,
) 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="获取成功")

View File

@ -6,19 +6,25 @@ 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
from app.utils.app_user_jwt import (
create_app_user_access_token,
get_current_app_user,
ACCESS_TOKEN_EXPIRE_MINUTES
)
from app.models.user import AppUser
from app.controllers.user_valuation import user_valuation_controller
from app.controllers.invoice import invoice_controller
router = APIRouter()
@router.post("/register", response_model=dict, summary="用户注册")
@router.post("/register", response_model=BasicResponse[AppUserRegisterOut], summary="用户注册")
async def register(
register_data: AppUserRegisterSchema
):
@ -30,11 +36,11 @@ async def register(
user = await app_user_controller.register(register_data)
return {
"code": 200,
"message": "注册成功",
"msg": "注册成功",
"data": {
"user_id": user.id,
"phone": user.phone,
"default_password": register_data.phone[-6:] # 返回默认密码供用户知晓
"default_password": register_data.phone[-6:]
}
}
except Exception as e:
@ -68,12 +74,12 @@ async def login(
)
@router.post("/logout", summary="用户登出")
@router.post("/logout", summary="用户登出", response_model=BasicResponse[MessageOut])
async def logout(current_user: AppUser = Depends(get_current_app_user)):
"""
用户登出客户端需要删除本地token
"""
return {"code": 200, "message": "登出成功"}
return {"code": 200, "msg": "OK", "data": {"message": "登出成功"}}
@router.get("/profile", response_model=AppUserInfoOut, summary="获取用户信息")
@ -84,6 +90,49 @@ async def get_profile(current_user: AppUser = Depends(get_current_app_user)):
return current_user
@router.get("/dashboard", response_model=AppUserDashboardOut, 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 = 0
return AppUserDashboardOut(remaining_quota=remaining_quota, latest_valuation=latest_out, pending_invoices=pending_invoices)
@router.get("/quota", response_model=AppUserQuotaOut, summary="剩余估值次数")
async def get_quota(current_user: AppUser = Depends(get_current_app_user)):
"""
剩余估值次数查询
说明:
- 当前实现返回默认 0 次与用户类型占位
- 若后续接入配额系统可从数据库中读取真实值
"""
remaining_count = 0
user_type = "体验用户"
return AppUserQuotaOut(remaining_count=remaining_count, user_type=user_type)
@router.put("/profile", response_model=AppUserInfoOut, summary="更新用户信息")
async def update_profile(
update_data: AppUserUpdateSchema,
@ -99,7 +148,7 @@ async def update_profile(
return updated_user
@router.post("/change-password", summary="修改密码")
@router.post("/change-password", summary="修改密码", response_model=BasicResponse[MessageOut])
async def change_password(
password_data: AppUserChangePasswordSchema,
current_user: AppUser = Depends(get_current_app_user)
@ -116,17 +165,17 @@ async def change_password(
if not success:
raise HTTPException(status_code=400, detail="原密码错误")
return {"code": 200, "message": "密码修改成功"}
return {"code": 200, "msg": "OK", "data": {"message": "密码修改成功"}}
@router.get("/validate-token", summary="验证token")
@router.get("/validate-token", summary="验证token", response_model=BasicResponse[TokenValidateOut])
async def validate_token(current_user: AppUser = Depends(get_current_app_user)):
"""
验证token是否有效
"""
return {
"code": 200,
"message": "token有效",
"msg": "token有效",
"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, SuccessExtra, BasicResponse, PageResponse
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
@ -108,12 +110,18 @@ 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)]
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))
years_total = calculate_total_years(data_list)
if years_total > 10:
patent_score = 10.0
elif years_total >= 5:
patent_score = 7.0
else:
patent_score = 3.0
input_data_by_b1["patent_score"] = patent_score
# 提取 文化价值B2 计算参数
@ -141,8 +149,20 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
calculator = FinalValueACalculator()
# 计算最终估值A统一计算
calculation_result = await calculator.calculate_complete_final_value_a(input_data)
# 先创建估值记录以获取ID方便步骤落库关联
initial_detail = await user_valuation_controller.create_valuation(
user_id=user_id,
data=data,
calculation_result=None,
calculation_input=None,
drp_result=None,
status='pending'
)
valuation_id = initial_detail.id
logger.info("valuation.init_created user_id={} valuation_id={}", user_id, valuation_id)
# 计算最终估值A统一计算传入估值ID以关联步骤落库
calculation_result = await calculator.calculate_complete_final_value_a(valuation_id, input_data)
# 计算动态质押
drp_c = DynamicPledgeRateCalculator()
@ -168,10 +188,12 @@ 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,
# 更新估值评估记录(写入计算结果与输入摘要)
update_data = 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'),
dynamic_pledge_rate=drp_result,
calculation_result=calculation_result,
calculation_input={
'model_data': {
@ -181,8 +203,15 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
},
'market_data': list(input_data.get('market_data', {}).keys()),
},
drp_result=drp_result,
status='success' # 计算成功设置为approved状态
status='success'
)
result = await valuation_controller.update(valuation_id, update_data)
logger.info(
"valuation.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'),
)
logger.info("valuation.background_calc_success user_id={} valuation_id={}", user_id, result.id)
@ -192,22 +221,16 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
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 = ValuationAssessmentUpdate(status='rejected')
await valuation_controller.update(valuation_id, fail_update)
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,
@ -315,7 +338,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:
@ -427,18 +456,50 @@ 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)
# 跨界合作深度:将枚举映射为项目数;若为数值字符串则直接取数值
try:
val = getattr(data, 'cooperation_depth', None)
mapping = {
"品牌联名": 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])
historical_inheritance = 0.0
try:
if isinstance(data.historical_evidence, dict):
historical_inheritance = sum([safe_float(v) for v in data.historical_evidence.values()])
elif isinstance(data.historical_evidence, (list, tuple)):
historical_inheritance = sum([safe_float(i) for i in data.historical_evidence])
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,
@ -563,7 +624,7 @@ async def _extract_calculation_params_c(data: UserValuationCreate) -> Dict[str,
}
@app_valuations_router.get("/", summary="获取我的估值评估列表")
@app_valuations_router.get("/", summary="获取我的估值评估列表", response_model=PageResponse[UserValuationOut])
async def get_my_valuations(
query: UserValuationQuery = Depends(),
current_user: AppUser = Depends(get_current_app_user)
@ -595,7 +656,7 @@ async def get_my_valuations(
)
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情")
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情", response_model=BasicResponse[UserValuationDetail])
async def get_valuation_detail(
valuation_id: int,
current_user: AppUser = Depends(get_current_app_user)
@ -628,7 +689,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)
):
@ -647,7 +708,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)
@ -705,3 +766,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

@ -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,154 @@
from fastapi import APIRouter, Query
from typing import Optional
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
from app.schemas.invoice import (
InvoiceCreate,
InvoiceUpdate,
UpdateStatus,
UpdateType,
InvoiceHeaderCreate,
PaymentReceiptCreate,
InvoiceOut,
InvoiceList,
InvoiceHeaderOut,
PaymentReceiptOut,
)
from app.controllers.invoice import invoice_controller
invoice_router = APIRouter(tags=["发票管理"])
@invoice_router.get("/list", summary="获取发票列表", response_model=PageResponse[InvoiceOut])
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),
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,
)
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])
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])
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])
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])
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])
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[PaymentReceiptOut])
async def upload_payment_receipt(id: int, data: PaymentReceiptCreate):
"""
上传对公转账付款凭证
"""
receipt = await invoice_controller.create_receipt(id, data)
return Success(data=receipt, msg="上传成功")
@invoice_router.get("/headers", summary="发票抬头列表", response_model=BasicResponse[list[InvoiceHeaderOut]])
async def get_invoice_headers(app_user_id: Optional[int] = Query(None)):
"""
获取发票抬头列表可按 AppUser 过滤
"""
headers = await invoice_controller.get_headers(user_id=app_user_id)
return Success(data=headers, msg="获取成功")
@invoice_router.get("/headers/{id}", summary="发票抬头详情", response_model=BasicResponse[InvoiceHeaderOut])
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])
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])
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 "未找到")
# 对公转账记录接口在 transactions 路由中统一暴露

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)

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

@ -0,0 +1,197 @@
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
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=SendResponse, summary="验证码发送")
async def send_code(payload: SendCodeRequest) -> SendResponse:
"""发送验证码短信
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:
code = store.generate_code()
store.set_code(payload.phone, code)
res = sms_client.send_code(payload.phone, code)
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 SendResponse(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=SendResponse, summary="报告通知发送", dependencies=[DependAuth])
async def send_report(payload: SendReportRequest) -> SendResponse:
"""发送报告通知短信
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 SendResponse(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=VerifyResponse)
async def verify_code(payload: VerifyCodeRequest) -> VerifyResponse:
"""验证验证码
Args:
payload: 请求体含手机号与验证码
Returns:
验证结果字典
"""
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)
return VerifyResponse(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=SMSLoginResponse)
async def sms_login(payload: SMSLoginRequest) -> SMSLoginResponse:
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)
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=user.nickname,
avatar=user.avatar,
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,
)
token_out = AppUserJWTOut(access_token=access_token, expires_in=ACCESS_TOKEN_EXPIRE_MINUTES)
return SMSLoginResponse(user=user_info, token=token_out)
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,104 @@
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.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),
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,
)
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(data: SendEmailRequest, file: Optional[UploadFile] = File(None)):
if not data.email or "@" not in data.email:
raise HTTPException(status_code=422, detail="邮箱格式不正确")
if not data.body:
raise HTTPException(status_code=422, detail="文案内容不能为空")
file_bytes = None
file_name = None
if file is not None:
file_bytes = await file.read()
file_name = file.filename
elif data.file_url:
try:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(data.file_url)
r.raise_for_status()
file_bytes = r.content
file_name = data.file_url.split("/")[-1]
except Exception as e:
raise HTTPException(status_code=400, detail=f"附件下载失败: {e}")
logger.info("transactions.email_send_start email={} subject={}", data.email, data.subject or "")
result = email_client.send(data.email, data.subject, data.body, file_bytes, file_name, getattr(file, "content_type", None))
body_summary = data.body[:500]
status = result.get("status")
error = result.get("error")
log = await EmailSendLog.create(
email=data.email,
subject=data.subject,
body_summary=body_summary,
file_name=file_name,
file_url=data.file_url,
status=status,
error=error,
)
if status == "OK":
logger.info("transactions.email_send_ok email={}", data.email)
else:
logger.error("transactions.email_send_fail email={} err={}", data.email, error)
return Success(data={"status": status, "log_id": log.id, "error": error}, msg="发送成功" if status == "OK" else "发送失败")

View File

@ -1,6 +1,6 @@
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
router = APIRouter()
@ -11,4 +11,8 @@ async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
:param file: 图片文件
:return: 图片URL和文件名
"""
return await UploadController.upload_image(file)
return await UploadController.upload_image(file)
@router.post("/file", response_model=FileUploadResponse, summary="上传文件")
async def upload_file(file: UploadFile = File(...)) -> FileUploadResponse:
return await UploadController.upload_file(file)

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,51 @@ 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.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)
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 +79,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="所属机构"),
@ -87,8 +102,10 @@ async def get_valuations(
size=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,7 +114,7 @@ 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="页码"),
@ -105,8 +122,10 @@ async def search_valuations(
):
"""根据关键词搜索估值评估记录"""
result = await valuation_controller.search(keyword, page, 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 +135,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 +159,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 +173,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
@ -90,6 +91,29 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
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,
)
return user
async def change_password(self, user_id: int, old_password: str, new_password: str) -> bool:
"""

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

@ -0,0 +1,286 @@
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,
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: 抬头输出对象
"""
header = await InvoiceHeader.create(app_user_id=user_id, **data.model_dump())
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 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"])
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 "",
)
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"])
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
items.append({
"submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": {
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
},
"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
return {
"submitted_at": r.created_at.isoformat() if r.created_at else "",
"receipt": {
"id": r.id,
"url": r.url,
"note": r.note,
"verified": r.verified,
},
"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:
@ -49,4 +49,42 @@ 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",
}
if file.content_type not in allowed:
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,
)

View File

@ -138,6 +138,8 @@ class UserValuationController:
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 +161,8 @@ 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,

View File

@ -3,13 +3,15 @@ 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
)
@ -17,6 +19,56 @@ class ValuationController:
"""估值评估控制器"""
model = ValuationAssessment
step_model = ValuationCalculationStep
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 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 create(self, data: ValuationAssessmentCreate, user_id: int) -> ValuationAssessmentOut:
"""创建估值评估"""
@ -192,4 +244,5 @@ class ValuationController:
# 创建控制器实例
valuation_controller = ValuationController()
valuation_controller = ValuationController()
from app.log import logger

View File

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

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

@ -0,0 +1,64 @@
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="接收邮箱")
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)
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="是否已核验")
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

@ -19,7 +19,22 @@ 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)
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="侵权记录")
@ -82,4 +93,26 @@ 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="关联的估值评估")
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="SUCCESS", description="步骤状态: SUCCESS, FAILED")
error_message = fields.TextField(null=True, description="错误信息")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
class Meta:
table = "valuation_calculation_steps"
table_description = "估值计算步骤表"
ordering = ["step_order"]
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):
@ -66,4 +67,49 @@ class AppUserUpdateSchema(BaseModel):
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
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,4 +1,6 @@
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
@ -50,3 +52,26 @@ class SuccessExtra(JSONResponse):
}
content.update(kwargs)
super().__init__(content=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="提示信息")

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

@ -0,0 +1,102 @@
from typing import Optional, List
from pydantic import BaseModel, Field, EmailStr
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: str = Field(..., min_length=1, max_length=256)
register_phone: str = Field(..., min_length=1, max_length=32)
bank_name: str = Field(..., min_length=1, max_length=128)
bank_account: str = Field(..., min_length=1, max_length=64)
email: EmailStr
class InvoiceHeaderOut(BaseModel):
id: int
company_name: str
tax_number: str
register_address: str
register_phone: str
bank_name: str
bank_account: str
email: EmailStr
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(..., min_length=1, max_length=256)
register_phone: str = Field(..., min_length=1, max_length=32)
bank_name: str = Field(..., min_length=1, max_length=128)
bank_account: str = Field(..., min_length=1, 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)
class PaymentReceiptOut(BaseModel):
id: int
url: str
note: Optional[str]
verified: bool
created_at: str

View File

@ -0,0 +1,25 @@
from pydantic import BaseModel, Field
from typing import Optional
class SendEmailRequest(BaseModel):
email: str = Field(..., description="邮箱地址")
subject: Optional[str] = Field(None, description="邮件主题")
body: str = Field(..., description="文案内容")
file_url: Optional[str] = Field(None, description="附件URL")
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

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
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="侵权记录")
@ -102,6 +107,8 @@ class ValuationAssessmentUpdate(BaseModel):
historical_evidence: Optional[List[Any]] = 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")
# 非遗应用与推广
application_maturity: Optional[str] = Field(None, description="非遗资产应用成熟度")
@ -117,6 +124,8 @@ class ValuationAssessmentUpdate(BaseModel):
last_market_activity: Optional[str] = Field(None, description="该商品最近一次市场活动时间")
monthly_transaction: Optional[str] = Field(None, description="月交易额")
price_fluctuation: Optional[List[Union[str, int, float]]] = Field(None, description="该商品近30天价格波动区间")
credit_code_or_id: Optional[str] = Field(None, description="统一社会信用代码或身份证号")
biz_intro: Optional[str] = Field(None, description="业务/传承介绍")
is_active: Optional[bool] = Field(None, description="是否激活")
@ -233,4 +242,46 @@ 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="步骤状态")
error_message: 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="创建时间")
class Config:
from_attributes = True
json_encoders = {
datetime: lambda v: v.isoformat(),
Decimal: lambda v: float(v)
}

View File

@ -0,0 +1,49 @@
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
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 settings.SMTP_TLS:
server = smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT)
server.starttls()
else:
server = smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT)
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
return {"status": "FAIL", "error": str(e)}
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()

View File

@ -0,0 +1,91 @@
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
if 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")
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={} template={}", phone, 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:
返回体映射字典
"""
return self.send_by_template(phone, "SMS_498190229", {"code": code})
def send_report(self, phone: str) -> Dict[str, Any]:
"""发送报告通知短信
Args:
phone: 接收短信手机号
Returns:
返回体映射字典
"""
return self.send_by_template(phone, "SMS_498140213", {})
sms_client = SMSClient()

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

@ -0,0 +1,144 @@
import random
import time
from datetime import date
from typing import Dict, Optional, Tuple
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]] = {}
def generate_code(self) -> str:
"""生成6位数字验证码
Returns:
六位数字字符串
"""
return f"{random.randint(0, 999999):06d}"
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)
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,17 @@ 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] = "SMS_498140213"
ALIYUN_SMS_ENDPOINT: str = "dysmsapi.aliyuncs.com"
SMTP_HOST: typing.Optional[str] = None
SMTP_PORT: typing.Optional[int] = None
SMTP_USERNAME: typing.Optional[str] = None
SMTP_PASSWORD: typing.Optional[str] = None
SMTP_TLS: bool = True
SMTP_FROM: typing.Optional[str] = None
settings = Settings()

View File

@ -0,0 +1,2 @@
%PDF-1.4
%粤マモ

View File

@ -0,0 +1,2 @@
%PDF-1.4
%粤マモ

View File

@ -0,0 +1,2 @@
%PDF-1.4
%粤マモ

View File

@ -0,0 +1,2 @@
%PDF-1.4
%粤マモ

View File

@ -0,0 +1 @@
%PDF-1.4

View File

@ -0,0 +1 @@
%PDF-1.4

View File

@ -0,0 +1 @@
%PDF-1.4

View File

@ -0,0 +1 @@
%PDF-1.4

View File

View File

@ -6,14 +6,25 @@
"""
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
from app.schemas.valuation import ValuationCalculationStepCreate
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.schemas.valuation import ValuationCalculationStepCreate
class CulturalValueB2Calculator:
@ -23,6 +34,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 +54,88 @@ 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) -> 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:
float: 计算得出的文化价值B2
Raises:
Exception: 在计算过程中遇到的任何异常都会被捕获记录并重新抛出
"""
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.2,
step_name="文化价值B2计算",
step_description="开始计算文化价值B2公式为活态传承系数B21 × 0.6 + (纹样基因值B22 / 10) × 0.4",
input_params=input_data,
status="in_progress"
)
)
try:
# 计算活态传承系数B21
living_heritage_b21 = self.living_heritage_calculator.calculate_living_heritage_b21(
input_data['inheritor_level_coefficient'],
self.living_heritage_calculator.calculate_teaching_frequency(
input_data["offline_sessions"],
input_data["douyin_views"],
input_data["kuaishou_views"],
input_data["bilibili_views"]
),
input_data['cross_border_depth']
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.21, step_name="活态传承系数B21",
output_result={'living_heritage_b21': living_heritage_b21}, status="completed"
)
)
# 计算纹样基因值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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.22, step_name="纹样基因值B22",
output_result={'pattern_gene_b22': pattern_gene_b22}, status="completed"
)
)
# 计算文化价值B2
cultural_value_b2 = self.calculate_cultural_value_b2(
living_heritage_b21,
pattern_gene_b22
)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"cultural_value_b2": cultural_value_b2}}
)
return cultural_value_b2
except Exception as e:
error_message = f"文化价值B2计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
raise
# 示例使用
if __name__ == "__main__":

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,12 @@
"""
from typing import Dict
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
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 +22,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 +46,103 @@ 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) -> 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:
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"],
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.1,
step_name="经济价值B1计算",
step_description="开始计算经济价值B1公式为基础价值B11 × (1 + 流量因子B12) × 政策乘数B13",
input_params=input_data,
status="in_progress"
)
)
try:
# 计算基础价值B11
basic_value_b11 = self.basic_value_calculator.calculate_basic_value_b11(
self.basic_value_calculator.calculate_financial_value_f(input_data["three_year_income"]),
self.basic_value_calculator.calculate_legal_strength_l(input_data["patent_score"], input_data["popularity_score"], input_data["infringement_score"]),
self.basic_value_calculator.calculate_development_potential_d(input_data["patent_count"], input_data["esg_score"], input_data["innovation_ratio"]),
input_data["industry_coefficient"]
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.11, step_name="基础价值B11",
output_result={'basic_value_b11': basic_value_b11}, status="completed"
)
)
# 发展潜力 patent_count: 专利分 (0-10分) (用户填写)
# esg_score: ESG分 (0-10分) (用户填写)
# innovation_ratio: 创新投入比 (研发费用/营收) * 100 (用户填写)
development_potential = self.basic_value_calculator.calculate_development_potential_d(
# 计算流量因子B12
traffic_factor_b12 = self.traffic_factor_calculator.calculate_traffic_factor_b12(
input_data['search_index_s1'],
input_data['industry_average_s2'],
self.traffic_factor_calculator.calculate_social_media_spread_s3(
self.traffic_factor_calculator.calculate_interaction_index(input_data["likes"], input_data["comments"], input_data["shares"]),
self.traffic_factor_calculator.calculate_coverage_index(0),
self.traffic_factor_calculator.calculate_conversion_efficiency(input_data["sales_volume"], input_data["link_views"])
)
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.12, step_name="流量因子B12",
output_result={'traffic_factor_b12': traffic_factor_b12}, status="completed"
)
)
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"]
)
# 计算政策乘数B13
policy_multiplier_b13 = self.policy_multiplier_calculator.calculate_policy_multiplier_b13(
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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.13, step_name="政策乘数B13",
output_result={'policy_multiplier_b13': policy_multiplier_b13}, status="completed"
)
)
# 计算互动量指数
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"])
social_media_spread_s3 = self.traffic_factor_calculator.calculate_social_media_spread_s3(interaction_index,
coverage_index,
conversion_efficiency)
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
)
# 计算政策乘数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
)
# 计算经济价值B1
economic_value_b1 = self.calculate_economic_value_b1(
basic_value_b11,
traffic_factor_b12,
policy_multiplier_b13
)
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
}
# 计算经济价值B1
economic_value_b1 = self.calculate_economic_value_b1(
basic_value_b11,
traffic_factor_b12,
policy_multiplier_b13
)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"economic_value_b1": economic_value_b1}}
)
return economic_value_b1
except Exception as e:
error_message = f"经济价值B1计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
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,
@ -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__":

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,15 @@ 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)
return traffic_factor
def calculate_social_media_spread_s3(self,
interaction_index: float,
coverage_index: float,
@ -60,7 +68,6 @@ class TrafficFactorB12Calculator:
returns:
float: 社交媒体传播度S3
"""
#
social_media_spread = (interaction_index * 0.4 +
coverage_index * 0.3 +
conversion_efficiency * 0.3)
@ -84,7 +91,6 @@ class TrafficFactorB12Calculator:
returns:
float: 互动量指数
"""
#
interaction_index = (likes + comments + shares) / 1000.0
return interaction_index
@ -101,11 +107,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,

View File

@ -19,10 +19,14 @@ try:
# 包内相对导入
from .model_value_b import ModelValueBCalculator
from ..market_value_c import MarketValueCCalculator
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
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
from app.schemas.valuation import ValuationCalculationStepCreate
class FinalValueACalculator:
@ -32,6 +36,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,17 +69,32 @@ 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()
step_order = 1
# 记录输入参数
logger.info("final_value_a.calculation_start input_data_keys={} model_data_keys={} market_data_keys={}",
@ -82,61 +102,74 @@ class FinalValueACalculator:
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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="开始计算最终估值A",
step_description="接收输入参数,准备开始计算。",
input_params=input_data,
status="processing"
)
)
step_order += 1
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 = 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_value_b = model_result if isinstance(model_result, (int, float)) else 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,19 +177,28 @@ 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:
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算模型估值B",
step_description="调用ModelValueBCalculator计算模型估值B。",
input_params=input_data.get('model_data', {}),
output_result=model_result,
status="completed"
)
)
step_order += 1
# 计算市场估值C
logger.info("final_value_a.calculating_market_value_c 开始计算市场估值C")
market_start_time = time.time()
market_result = 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_value_c = market_result if isinstance(market_result, (int, float)) else 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 返回字段={}",
@ -164,15 +206,23 @@ class FinalValueACalculator:
int(market_duration * 1000),
list(market_result.keys()))
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:
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算市场估值C",
step_description="调用MarketValueCCalculator计算市场估值C。",
input_params=input_data.get('market_data', {}),
output_result=market_result,
status="completed"
)
)
step_order += 1
# 计算最终估值A
logger.info("final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B={}万元 市场估值C={}万元",
model_value_b, market_value_c)
final_value_a = self.calculate_final_value_a(
model_value_b,
market_value_c
@ -188,16 +238,37 @@ 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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算最终估值A",
step_description="最终估值A = 模型估值B × 0.7 + 市场估值C × 0.3",
input_params={"model_value_b": model_value_b, "market_value_c": market_value_c},
output_result={"final_value_a": final_value_a},
status="completed"
)
)
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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算失败",
step_description="计算过程中发生错误。",
status="failed",
error_message=str(e)
)
)
raise

View File

@ -12,10 +12,14 @@ 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
from app.schemas.valuation import ValuationCalculationStepCreate
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
from app.schemas.valuation import ValuationCalculationStepCreate
class ModelValueBCalculator:
@ -26,6 +30,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 +51,127 @@ 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) -> 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: 包含所有中间计算结果和最终结果的字典
float: 计算得出的模型估值B
Raises:
Exception: 在计算过程中遇到的任何异常都会被捕获记录然后重新抛出
"""
# 计算经济价值B1
economic_result = self.economic_value_calculator.calculate_complete_economic_value_b1(
input_data['economic_data']
step_order = 1
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="开始计算模型估值B",
step_description="接收输入参数,准备开始计算。",
input_params=input_data,
status="processing"
)
)
economic_value_b1 = economic_result['economic_value_b1']
step_order += 1
# 计算文化价值B2
cultural_result = self.cultural_value_calculator.calculate_complete_cultural_value_b2(
input_data['cultural_data']
)
cultural_value_b2 = cultural_result['cultural_value_b2']
try:
# 计算经济价值B1传入估值ID并等待异步完成
economic_value_b1 = await self.economic_value_calculator.calculate_complete_economic_value_b1(
valuation_id,
input_data['economic_data']
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算经济价值B1",
step_description="调用EconomicValueB1Calculator计算经济价值B1。",
input_params=input_data.get('economic_data', {}),
output_result={"economic_value_b1": economic_value_b1},
status="completed"
)
)
step_order += 1
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
)
# 计算文化价值B2传入估值ID并等待异步完成
cultural_value_b2 = await self.cultural_value_calculator.calculate_complete_cultural_value_b2(
valuation_id,
input_data['cultural_data']
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算文化价值B2",
step_description="调用CulturalValueB2Calculator计算文化价值B2。",
input_params=input_data.get('cultural_data', {}),
output_result={"cultural_value_b2": cultural_value_b2},
status="completed"
)
)
step_order += 1
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,
}
# 计算风险调整系数B3传入估值ID并等待异步完成
risk_value_b3 = await self.risk_adjustment_calculator.calculate_complete_risky_value_b3(
valuation_id,
input_data['risky_data']
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算风险调整系数B3",
step_description="调用RiskAdjustmentB3Calculator计算风险调整系数B3。",
input_params=input_data.get('risky_data', {}),
output_result={"risk_adjustment_b3": risk_value_b3},
status="completed"
)
)
step_order += 1
# 计算模型估值B
model_value_b = self.calculate_model_value_b(
economic_value_b1,
cultural_value_b2,
risk_value_b3
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算模型估值B",
step_description="模型估值B = 经济价值B1*0.7+文化价值B2*0.3*风险调整系数B3",
input_params={"economic_value_b1": economic_value_b1, "cultural_value_b2": cultural_value_b2, "risk_value_b3": risk_value_b3},
output_result={"model_value_b": model_value_b},
status="completed"
)
)
return model_value_b
except Exception as e:
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=step_order,
step_name="计算失败",
step_description="计算过程中发生错误。",
status="failed",
error_message=str(e)
)
)
raise
# 示例使用

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,8 @@ 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
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError:
# 绝对导入(当直接运行时)
from sub_formulas.market_bidding_c1 import MarketBiddingC1Calculator
@ -23,6 +28,8 @@ 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
from app.schemas.valuation import ValuationCalculationStepCreate
logger = logging.getLogger(__name__)
@ -36,6 +43,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:
"""
@ -95,62 +103,109 @@ class MarketValueCCalculator:
return market_value
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) -> 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:
float: 计算得出的市场估值C
Raises:
Exception: 如果在计算过程中发生任何错误将记录失败状态并重新抛出异常
"""
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=3,
step_name="市场估值C计算",
step_description="开始计算市场估值C公式为市场竞价C1 × 热度系数C2 × 稀缺性乘数C3 × 时效性衰减C4",
input_params=input_data,
status="in_progress"
)
)
# 计算稀缺性乘数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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=3.1, step_name="市场竞价C1",
output_result={'market_bidding_c1': market_bidding_c1}, status="completed"
)
)
# 计算热度系数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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=3.2, step_name="热度系数C2",
output_result={'heat_coefficient_c2': heat_coefficient_c2}, status="completed"
)
)
# 计算稀缺性乘数C3
scarcity_multiplier_c3 = self.scarcity_multiplier_calculator.calculate_scarcity_multiplier_c3(
input_data.get('issuance_level', '限量')
)
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=3.3, step_name="稀缺性乘数C3",
output_result={'scarcity_multiplier_c3': scarcity_multiplier_c3}, status="completed"
)
)
# 计算时效性衰减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.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=3.4, step_name="时效性衰减C4",
output_result={'temporal_decay_c4': temporal_decay_c4}, status="completed"
)
)
# 计算市场估值C
market_value_c = self.calculate_market_value_c(
market_bidding_c1,
heat_coefficient_c2,
scarcity_multiplier_c3,
temporal_decay_c4
)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {"market_value_c": market_value_c}}
)
return market_value_c
except Exception as e:
error_message = f"市场估值C计算失败: {e}"
logger.error(error_message, exc_info=True)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
raise
# 示例使用

View File

@ -6,13 +6,25 @@
"""
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
from app.schemas.valuation import ValuationCalculationStepCreate
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 +167,81 @@ 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) -> 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:
float: 计算得出的风险调整系数B3
Raises:
Exception: 在计算过程中遇到的任何异常都会被捕获记录并重新抛出
"""
step = await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id,
step_order=2.3,
step_name="风险调整系数B3计算",
step_description="开始计算风险调整系数B3公式为0.8 + 风险评分总和R × 0.4",
input_params=input_data,
status="in_progress"
)
)
try:
# 计算各项风险评分
market_risk = self.calculate_market_risk(input_data["highest_price"], input_data["lowest_price"])
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.31, step_name="市场风险评分",
output_result={'market_risk': market_risk}, status="completed"
)
)
legal_risk = self.calculate_legal_risk(input_data["lawsuit_status"])
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.32, step_name="法律风险评分",
output_result={'legal_risk': legal_risk}, status="completed"
)
)
inheritance_risk = self.calculate_inheritance_risk(input_data["inheritor_ages"])
await self.valuation_controller.create_calculation_step(
ValuationCalculationStepCreate(
valuation_id=valuation_id, step_order=2.33, step_name="传承风险评分",
output_result={'inheritance_risk': inheritance_risk}, status="completed"
)
)
# 计算风险评分总和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)
await self.valuation_controller.update_calculation_step(
step.id, {"status": "completed", "output_result": {'risk_adjustment_b3': risk_adjustment_b3}}
)
return risk_adjustment_b3
except Exception as e:
error_message = f"风险调整系数B3计算失败: {e}"
await self.valuation_controller.update_calculation_step(
step.id, {"status": "failed", "error_message": error_message}
)
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,24 +0,0 @@
-- 新增交易管理菜单
-- 创建时间: 2025-11-13
-- 插入一级目录:交易管理
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(16, '交易管理', 'catalog', 'carbon:receipt', '/transaction', 3, 0, 0, 'Layout', 0, '/transaction/invoice', datetime('now'), datetime('now'));
-- 插入二级菜单:开票记录
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(17, '开票记录', 'menu', 'carbon:document', 'invoice', 1, 16, 0, '/transaction/invoice', 0, NULL, datetime('now'), datetime('now'));
-- 为管理员角色分配菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(1, 16),
(1, 17);
-- 为普通用户角色分配菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(2, 16),
(2, 17);

View File

@ -1,24 +0,0 @@
-- 新增估值管理菜单
-- 创建时间: 2025-11-13
-- 插入一级目录:估值管理
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(18, '估值管理', 'catalog', 'carbon:calculator', '/valuation', 4, 0, 0, 'Layout', 0, '/valuation/audit', datetime('now'), datetime('now'));
-- 插入二级菜单:审核列表
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(19, '审核列表', 'menu', 'carbon:task-approved', 'audit', 1, 18, 0, '/valuation/audit', 0, NULL, datetime('now'), datetime('now'));
-- 为管理员角色分配菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(1, 18),
(1, 19);
-- 为普通用户角色分配菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(2, 18),
(2, 19);

View File

@ -1,86 +0,0 @@
-- 完整菜单初始化SQL
-- 创建时间: 2025-11-17
-- 说明: 包含所有新增的菜单项和权限分配
-- ========================================
-- 1. 工作台菜单
-- ========================================
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(22, '工作台', 'menu', 'carbon:dashboard', '/workbench', 1, 0, 0, '/workbench', 1, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 2. 交易管理菜单
-- ========================================
-- 插入一级目录:交易管理
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(16, '交易管理', 'catalog', 'carbon:receipt', '/transaction', 3, 0, 0, 'Layout', 0, '/transaction/invoice', datetime('now'), datetime('now'));
-- 插入二级菜单:开票记录
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(17, '开票记录', 'menu', 'carbon:document', 'invoice', 1, 16, 0, '/transaction/invoice', 0, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 3. 估值管理菜单
-- ========================================
-- 插入一级目录:估值管理
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(18, '估值管理', 'catalog', 'carbon:calculator', '/valuation', 4, 0, 0, 'Layout', 0, '/valuation/audit', datetime('now'), datetime('now'));
-- 插入二级菜单:审核列表
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(19, '审核列表', 'menu', 'carbon:task-approved', 'audit', 1, 18, 0, '/valuation/audit', 0, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 4. 用户管理菜单
-- ========================================
-- 插入一级目录:用户管理
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(20, '用户管理', 'catalog', 'carbon:user-multiple', '/user-management', 5, 0, 0, 'Layout', 0, '/user-management/user-list', datetime('now'), datetime('now'));
-- 插入二级菜单:用户列表
INSERT INTO menu (id, name, menu_type, icon, path, 'order', parent_id, is_hidden, component, keepalive, redirect, created_at, updated_at)
VALUES
(21, '用户列表', 'menu', 'carbon:user', 'user-list', 1, 20, 0, '/user-management/user-list', 0, NULL, datetime('now'), datetime('now'));
-- ========================================
-- 角色权限分配
-- ========================================
-- 为管理员角色(role_id=1)分配所有菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(1, 22), -- 工作台
(1, 16), -- 交易管理
(1, 17), -- 开票记录
(1, 18), -- 估值管理
(1, 19), -- 审核列表
(1, 20), -- 用户管理
(1, 21); -- 用户列表
-- 为普通用户角色(role_id=2)分配基础菜单权限
INSERT INTO role_menu (role_id, menu_id)
VALUES
(2, 22), -- 工作台
(2, 16), -- 交易管理
(2, 17), -- 开票记录
(2, 18), -- 估值管理
(2, 19); -- 审核列表
-- 注意:普通用户不分配用户管理权限
-- ========================================
-- 验证SQL
-- ========================================
-- 查询所有新增的菜单
-- SELECT * FROM menu WHERE id >= 16 ORDER BY parent_id, 'order';
-- 查询管理员角色的菜单权限
-- SELECT m.name, m.path, m.menu_type FROM menu m
-- JOIN role_menu rm ON m.id = rm.menu_id
-- WHERE rm.role_id = 1 AND m.id >= 16
-- ORDER BY m.parent_id, m.'order';

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]

View File

@ -64,3 +64,7 @@ uvicorn==0.34.0
uvloop==0.21.0
watchfiles==1.0.4
websockets==14.1
alibabacloud_dysmsapi20170525==4.1.2
alibabacloud_tea_openapi==0.4.1
alibabacloud_tea_util==0.3.14
pytest==8.3.3

2
run.py
View File

@ -10,5 +10,5 @@ if __name__ == "__main__":
] = '%(asctime)s - %(levelname)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True, log_config=LOGGING_CONFIG)
uvicorn.run("app:app", host="0.0.0.0", port=9991, reload=True, log_config=LOGGING_CONFIG)

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"}
}
}
# 若 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()

381
scripts/user_flow_test.py Normal file
View File

@ -0,0 +1,381 @@
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}})
# 登出
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

@ -37,8 +37,8 @@
export DOCKER_DEFAULT_PLATFORM=linux/amd64
docker build -t zfc931912343/guzhi-fastapi-admin:v1.4 .
docker push zfc931912343/guzhi-fastapi-admin:v1.4
docker build -t zfc931912343/bindbox-game:v1.0 .
docker push zfc931912343/bindbox-game:v1.0
# 运行容器