Compare commits

...

2 Commits

Author SHA1 Message Date
c905d2492b Merge branch 'main' of https://git.1024tool.vip/zfc/guzhi 2025-11-19 19:36:12 +08:00
cc352d3184 feat: 重构后端服务并添加新功能
refactor: 优化API路由和响应模型
feat(admin): 添加App用户管理接口
feat(sms): 实现阿里云短信服务集成
feat(email): 添加SMTP邮件发送功能
feat(upload): 支持文件上传接口
feat(rate-limiter): 实现手机号限流器
fix: 修复计算步骤入库问题
docs: 更新API文档和测试计划
chore: 更新依赖和配置
2025-11-19 19:36:03 +08:00
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: 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( app = FastAPI(
title=settings.APP_TITLE, title=settings.APP_TITLE,
description=settings.APP_DESCRIPTION, description=settings.APP_DESCRIPTION,
version=settings.VERSION, version=settings.VERSION,
openapi_url="/openapi.json", openapi_url="/openapi.json",
openapi_tags=openapi_tags,
middleware=make_middlewares(), middleware=make_middlewares(),
lifespan=lifespan, lifespan=lifespan,
redirect_slashes=False, # 禁用尾部斜杠重定向 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 .apis import apis_router
from .app_users import app_users_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 .app_valuations import app_valuations_router
from .auditlog import auditlog_router from .auditlog import auditlog_router
from .base import base_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 .upload import router as upload_router
from .users import users_router from .users import users_router
from .valuations import router as valuations_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 = APIRouter()
v1_router.include_router(base_router, prefix="/base") v1_router.include_router(base_router, prefix="/base", tags=["admin-基础"])
v1_router.include_router(app_users_router, prefix="/app-user") # AppUser路由无需权限依赖 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-valuations 路由在各自的端点内部使用 get_current_app_user 进行认证
# 这样可以保持App用户认证系统的独立性不与后台管理权限系统混合 # 这样可以保持App用户认证系统的独立性不与后台管理权限系统混合
v1_router.include_router(app_valuations_router, prefix="/app-valuations") # 用户端估值评估路由 v1_router.include_router(app_valuations_router, prefix="/app-valuations", tags=["app-估值评估"]) # 用户端估值评估路由
v1_router.include_router(users_router, prefix="/user", dependencies=[DependAuth, DependPermission]) v1_router.include_router(users_router, prefix="/user", dependencies=[DependAuth, DependPermission], tags=["admin-用户管理"])
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependAuth, DependPermission]) v1_router.include_router(roles_router, prefix="/role", dependencies=[DependAuth, DependPermission], tags=["admin-角色管理"])
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependAuth, DependPermission]) v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependAuth, DependPermission], tags=["admin-菜单管理"])
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependAuth, DependPermission]) 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]) v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependAuth, DependPermission], tags=["admin-部门管理"])
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependAuth, DependPermission]) v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependAuth, DependPermission], tags=["admin-审计日志"])
v1_router.include_router(esg_router, prefix="/esg") v1_router.include_router(esg_router, prefix="/esg", tags=["admin-ESG管理"])
v1_router.include_router(index_router, prefix="/index") v1_router.include_router(index_router, prefix="/index", tags=["admin-指数管理"])
v1_router.include_router(industry_router, prefix="/industry") v1_router.include_router(industry_router, prefix="/industry", tags=["admin-行业管理"])
v1_router.include_router(policy_router, prefix="/policy") v1_router.include_router(policy_router, prefix="/policy", tags=["admin-政策管理"])
v1_router.include_router(upload_router, prefix="/upload") # 文件上传路由 v1_router.include_router(upload_router, prefix="/upload", tags=["app-上传"]) # 文件上传路由
v1_router.include_router( v1_router.include_router(
third_party_api_router, third_party_api_router,
prefix="/third_party_api", prefix="/third_party_api",
dependencies=[DependAuth, DependPermission], 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.controllers.api import api_controller
from app.schemas import Success, SuccessExtra 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 * from app.schemas.apis import *
router = APIRouter() router = APIRouter()
@router.get("/list", summary="查看API列表") @router.get("/list", summary="查看API列表", response_model=PageResponse[BaseApi])
async def list_api( async def list_api(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_api(
id: int = Query(..., description="Api"), id: int = Query(..., description="Api"),
): ):
@ -37,7 +39,7 @@ async def get_api(
return Success(data=data) return Success(data=data)
@router.post("/create", summary="创建Api") @router.post("/create", summary="创建Api", response_model=BasicResponse[MessageOut])
async def create_api( async def create_api(
api_in: ApiCreate, api_in: ApiCreate,
): ):
@ -45,7 +47,7 @@ async def create_api(
return Success(msg="Created Successfully") return Success(msg="Created Successfully")
@router.post("/update", summary="更新Api") @router.post("/update", summary="更新Api", response_model=BasicResponse[MessageOut])
async def update_api( async def update_api(
api_in: ApiUpdate, api_in: ApiUpdate,
): ):
@ -53,7 +55,7 @@ async def update_api(
return Success(msg="Update Successfully") return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除Api") @router.delete("/delete", summary="删除Api", response_model=BasicResponse[MessageOut])
async def delete_api( async def delete_api(
api_id: int = Query(..., description="ApiID"), api_id: int = Query(..., description="ApiID"),
): ):
@ -61,7 +63,7 @@ async def delete_api(
return Success(msg="Deleted Success") return Success(msg="Deleted Success")
@router.post("/refresh", summary="刷新API列表") @router.post("/refresh", summary="刷新API列表", response_model=BasicResponse[MessageOut])
async def refresh_api(): async def refresh_api():
await api_controller.refresh_api() await api_controller.refresh_api()
return Success(msg="OK") 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, AppUserJWTOut,
AppUserInfoOut, AppUserInfoOut,
AppUserUpdateSchema, 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 ( from app.utils.app_user_jwt import (
create_app_user_access_token, create_app_user_access_token,
get_current_app_user, get_current_app_user,
ACCESS_TOKEN_EXPIRE_MINUTES ACCESS_TOKEN_EXPIRE_MINUTES
) )
from app.models.user import AppUser 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 = APIRouter()
@router.post("/register", response_model=dict, summary="用户注册") @router.post("/register", response_model=BasicResponse[AppUserRegisterOut], summary="用户注册")
async def register( async def register(
register_data: AppUserRegisterSchema register_data: AppUserRegisterSchema
): ):
@ -30,11 +36,11 @@ async def register(
user = await app_user_controller.register(register_data) user = await app_user_controller.register(register_data)
return { return {
"code": 200, "code": 200,
"message": "注册成功", "msg": "注册成功",
"data": { "data": {
"user_id": user.id, "user_id": user.id,
"phone": user.phone, "phone": user.phone,
"default_password": register_data.phone[-6:] # 返回默认密码供用户知晓 "default_password": register_data.phone[-6:]
} }
} }
except Exception as e: 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)): async def logout(current_user: AppUser = Depends(get_current_app_user)):
""" """
用户登出客户端需要删除本地token 用户登出客户端需要删除本地token
""" """
return {"code": 200, "message": "登出成功"} return {"code": 200, "msg": "OK", "data": {"message": "登出成功"}}
@router.get("/profile", response_model=AppUserInfoOut, summary="获取用户信息") @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 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="更新用户信息") @router.put("/profile", response_model=AppUserInfoOut, summary="更新用户信息")
async def update_profile( async def update_profile(
update_data: AppUserUpdateSchema, update_data: AppUserUpdateSchema,
@ -99,7 +148,7 @@ async def update_profile(
return updated_user return updated_user
@router.post("/change-password", summary="修改密码") @router.post("/change-password", summary="修改密码", response_model=BasicResponse[MessageOut])
async def change_password( async def change_password(
password_data: AppUserChangePasswordSchema, password_data: AppUserChangePasswordSchema,
current_user: AppUser = Depends(get_current_app_user) current_user: AppUser = Depends(get_current_app_user)
@ -116,17 +165,17 @@ async def change_password(
if not success: if not success:
raise HTTPException(status_code=400, detail="原密码错误") 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)): async def validate_token(current_user: AppUser = Depends(get_current_app_user)):
""" """
验证token是否有效 验证token是否有效
""" """
return { return {
"code": 200, "code": 200,
"message": "token有效", "msg": "token有效",
"data": { "data": {
"user_id": current_user.id, "user_id": current_user.id,
"phone": current_user.phone "phone": current_user.phone

View File

@ -9,6 +9,8 @@ import asyncio
import time import time
from app.controllers.user_valuation import user_valuation_controller 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 ( from app.schemas.valuation import (
UserValuationCreate, UserValuationCreate,
UserValuationQuery, UserValuationQuery,
@ -16,13 +18,13 @@ from app.schemas.valuation import (
UserValuationOut, UserValuationOut,
UserValuationDetail 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.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 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.drp import DynamicPledgeRateCalculator
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.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.traffic_factor_b12 import calculate_search_index_s1 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.log.log import logger
from app.models.esg import ESG 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 matched = [item for item in data_list if
isinstance(item, dict) and item.get("SQH") == getattr(data, 'patent_application_no', None)] isinstance(item, dict) and item.get("SQH") == getattr(data, 'patent_application_no', None)]
if matched: if matched:
patent_count = calculate_patent_usage_score(len(matched)) patent_count_score = min(len(matched) * 2.5, 10.0)
input_data_by_b1["patent_count"] = float(patent_count) input_data_by_b1["patent_count"] = float(patent_count_score)
else: else:
input_data_by_b1["patent_count"] = 0.0 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 input_data_by_b1["patent_score"] = patent_score
# 提取 文化价值B2 计算参数 # 提取 文化价值B2 计算参数
@ -141,8 +149,20 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
calculator = FinalValueACalculator() calculator = FinalValueACalculator()
# 计算最终估值A统一计算 # 先创建估值记录以获取ID方便步骤落库关联
calculation_result = await calculator.calculate_complete_final_value_a(input_data) 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() drp_c = DynamicPledgeRateCalculator()
@ -168,10 +188,12 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
except Exception: except Exception:
pass pass
# 创建估值评估记录 # 更新估值评估记录(写入计算结果与输入摘要)
result = await user_valuation_controller.create_valuation( update_data = ValuationAssessmentUpdate(
user_id=user_id, model_value_b=calculation_result.get('model_value_b'),
data=data, 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_result=calculation_result,
calculation_input={ calculation_input={
'model_data': { '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()), 'market_data': list(input_data.get('market_data', {}).keys()),
}, },
drp_result=drp_result, status='success'
status='success' # 计算成功设置为approved状态 )
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) 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()) print(traceback.format_exc())
logger.error("valuation.background_calc_failed user_id={} err={}", user_id, repr(e)) logger.error("valuation.background_calc_failed user_id={} err={}", user_id, repr(e))
# 计算失败时也创建记录状态设置为failed # 计算失败时更新记录为失败状态
try: try:
result = await user_valuation_controller.create_valuation( if 'valuation_id' in locals():
user_id=user_id, fail_update = ValuationAssessmentUpdate(status='rejected')
data=data, await valuation_controller.update(valuation_id, fail_update)
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)
except Exception as create_error: 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( async def calculate_valuation(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
data: UserValuationCreate, data: UserValuationCreate,
@ -315,7 +338,13 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
# 法律强度L相关参数 # 法律强度L相关参数
# 普及地域分值 默认 7分 # 普及地域分值 默认 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 # 创新投入比 = (研发费用/营收) * 100
try: 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 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 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相关参数 # 纹样基因值B22相关参数
# 以下三项需由后续模型/服务计算;此处提供默认可计算占位 # 以下三项需由后续模型/服务计算;此处提供默认可计算占位
# #
# 历史传承度HI(用户填写) # 历史传承度HI(用户填写)
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]) historical_inheritance = sum([safe_float(i) for i in data.historical_evidence])
except Exception:
historical_inheritance = 0.0
structure_complexity = 1.5 # 默认值 纹样基因熵值B22(系统计算) structure_complexity = 1.5 # 默认值 纹样基因熵值B22(系统计算)
normalized_entropy = 9 # 默认值 归一化信息熵H(系统计算) 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 { return {
"inheritor_level_coefficient": inheritor_level_coefficient, "inheritor_level_coefficient": inheritor_level_coefficient,
"offline_sessions": offline_sessions, "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( async def get_my_valuations(
query: UserValuationQuery = Depends(), query: UserValuationQuery = Depends(),
current_user: AppUser = Depends(get_current_app_user) 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( async def get_valuation_detail(
valuation_id: int, valuation_id: int,
current_user: AppUser = Depends(get_current_app_user) 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( async def get_my_valuation_statistics(
current_user: AppUser = Depends(get_current_app_user) 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( async def delete_valuation(
valuation_id: int, valuation_id: int,
current_user: AppUser = Depends(get_current_app_user) current_user: AppUser = Depends(get_current_app_user)
@ -705,3 +766,4 @@ def safe_float(v):
return float(v) return float(v)
except (ValueError, TypeError): except (ValueError, TypeError):
return 0.0 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.ctx import CTX_USER_ID
from app.core.dependency import DependAuth from app.core.dependency import DependAuth
from app.models.admin import Api, Menu, Role, User 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.login import *
from app.schemas.users import UpdatePassword from app.schemas.users import UpdatePassword
from app.settings import settings from app.settings import settings
@ -16,7 +16,7 @@ from app.utils.password import get_password_hash, verify_password
router = APIRouter() 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): async def login_access_token(credentials: CredentialsSchema):
user: User = await user_controller.authenticate(credentials) user: User = await user_controller.authenticate(credentials)
await user_controller.update_last_login(user.id) 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()) 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(): async def get_userinfo():
user_id = CTX_USER_ID.get() user_id = CTX_USER_ID.get()
user_obj = await user_controller.get(id=user_id) user_obj = await user_controller.get(id=user_id)
@ -46,7 +46,7 @@ async def get_userinfo():
return Success(data=data) 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(): async def get_user_menu():
user_id = CTX_USER_ID.get() user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first() user_obj = await User.filter(id=user_id).first()
@ -74,7 +74,7 @@ async def get_user_menu():
return Success(data=res) 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(): async def get_user_api():
user_id = CTX_USER_ID.get() user_id = CTX_USER_ID.get()
user_obj = await User.filter(id=user_id).first() user_obj = await User.filter(id=user_id).first()
@ -91,7 +91,7 @@ async def get_user_api():
return Success(data=apis) 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): async def update_user_password(req_in: UpdatePassword):
user_id = CTX_USER_ID.get() user_id = CTX_USER_ID.get()
user = await user_controller.get(user_id) 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.controllers.dept import dept_controller
from app.schemas import Success from app.schemas import Success
from app.schemas.base import BasicResponse, MessageOut
from app.schemas.depts import BaseDept
from app.schemas.depts import * from app.schemas.depts import *
router = APIRouter() router = APIRouter()
@router.get("/list", summary="查看部门列表") @router.get("/list", summary="查看部门列表", response_model=BasicResponse[list[BaseDept]])
async def list_dept( async def list_dept(
name: str = Query(None, description="部门名称"), name: str = Query(None, description="部门名称"),
): ):
@ -15,7 +17,7 @@ async def list_dept(
return Success(data=dept_tree) return Success(data=dept_tree)
@router.get("/get", summary="查看部门") @router.get("/get", summary="查看部门", response_model=BasicResponse[BaseDept])
async def get_dept( async def get_dept(
id: int = Query(..., description="部门ID"), id: int = Query(..., description="部门ID"),
): ):
@ -24,7 +26,7 @@ async def get_dept(
return Success(data=data) return Success(data=data)
@router.post("/create", summary="创建部门") @router.post("/create", summary="创建部门", response_model=BasicResponse[MessageOut])
async def create_dept( async def create_dept(
dept_in: DeptCreate, dept_in: DeptCreate,
): ):
@ -32,7 +34,7 @@ async def create_dept(
return Success(msg="Created Successfully") return Success(msg="Created Successfully")
@router.post("/update", summary="更新部门") @router.post("/update", summary="更新部门", response_model=BasicResponse[MessageOut])
async def update_dept( async def update_dept(
dept_in: DeptUpdate, dept_in: DeptUpdate,
): ):
@ -40,7 +42,7 @@ async def update_dept(
return Success(msg="Update Successfully") return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除部门") @router.delete("/delete", summary="删除部门", response_model=BasicResponse[MessageOut])
async def delete_dept( async def delete_dept(
dept_id: int = Query(..., description="部门ID"), 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.controllers.esg import esg_controller
from app.schemas import Success, SuccessExtra 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 from app.schemas.esg import ESGCreate, ESGUpdate, ESGResponse
router = APIRouter(tags=["ESG管理"]) router = APIRouter(tags=["ESG管理"])
@router.get("/list", summary="查看ESG列表") @router.get("/list", summary="查看ESG列表", response_model=PageResponse[ESGResponse])
async def list_esg( async def list_esg(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_esg(
id: int = Query(..., description="ESG ID"), id: int = Query(..., description="ESG ID"),
): ):
@ -37,7 +39,7 @@ async def get_esg(
return Success(data=data) return Success(data=data)
@router.post("/create", summary="创建ESG") @router.post("/create", summary="创建ESG", response_model=BasicResponse[MessageOut])
async def create_esg( async def create_esg(
esg_in: ESGCreate, esg_in: ESGCreate,
): ):
@ -49,7 +51,7 @@ async def create_esg(
return Success(msg="创建成功") return Success(msg="创建成功")
@router.post("/update", summary="更新ESG") @router.post("/update", summary="更新ESG", response_model=BasicResponse[MessageOut])
async def update_esg( async def update_esg(
esg_in: ESGUpdate, esg_in: ESGUpdate,
): ):
@ -63,7 +65,7 @@ async def update_esg(
return Success(msg="更新成功") return Success(msg="更新成功")
@router.delete("/delete", summary="删除ESG") @router.delete("/delete", summary="删除ESG", response_model=BasicResponse[MessageOut])
async def delete_esg( async def delete_esg(
esg_id: int = Query(..., description="ESG ID"), 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.controllers.index import index_controller
from app.schemas import Success, SuccessExtra 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 from app.schemas.index import IndexCreate, IndexUpdate, IndexResponse
router = APIRouter(tags=["指数管理"]) router = APIRouter(tags=["指数管理"])
@router.get("/list", summary="查看指数列表") @router.get("/list", summary="查看指数列表", response_model=PageResponse[IndexResponse])
async def list_index( async def list_index(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_index(
id: int = Query(..., description="指数 ID"), id: int = Query(..., description="指数 ID"),
): ):
@ -34,7 +36,7 @@ async def get_index(
return Success(data=data) return Success(data=data)
@router.post("/create", summary="创建指数") @router.post("/create", summary="创建指数", response_model=BasicResponse[MessageOut])
async def create_index( async def create_index(
index_in: IndexCreate, index_in: IndexCreate,
): ):
@ -46,7 +48,7 @@ async def create_index(
return Success(msg="创建成功") return Success(msg="创建成功")
@router.post("/update", summary="更新指数") @router.post("/update", summary="更新指数", response_model=BasicResponse[MessageOut])
async def update_index( async def update_index(
index_in: IndexUpdate, index_in: IndexUpdate,
): ):
@ -60,7 +62,7 @@ async def update_index(
return Success(msg="更新成功") return Success(msg="更新成功")
@router.delete("/delete", summary="删除指数") @router.delete("/delete", summary="删除指数", response_model=BasicResponse[MessageOut])
async def delete_index( async def delete_index(
index_id: int = Query(..., description="指数 ID"), 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.controllers.industry import industry_controller
from app.schemas import Success, SuccessExtra from app.schemas import Success, SuccessExtra
from app.schemas.base import BasicResponse, PageResponse, MessageOut
from app.schemas.industry import IndustryCreate, IndustryUpdate, IndustryResponse from app.schemas.industry import IndustryCreate, IndustryUpdate, IndustryResponse
router = APIRouter(tags=["行业管理"]) router = APIRouter(tags=["行业管理"])
@router.get("/list", summary="查看行业列表") @router.get("/list", summary="查看行业列表", response_model=PageResponse[IndustryResponse])
async def list_industry( async def list_industry(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_industry(
id: int = Query(..., description="行业 ID"), id: int = Query(..., description="行业 ID"),
): ):
@ -34,7 +35,7 @@ async def get_industry(
return Success(data=data) return Success(data=data)
@router.post("/create", summary="创建行业") @router.post("/create", summary="创建行业", response_model=BasicResponse[MessageOut])
async def create_industry( async def create_industry(
industry_in: IndustryCreate, industry_in: IndustryCreate,
): ):
@ -46,7 +47,7 @@ async def create_industry(
return Success(msg="创建成功") return Success(msg="创建成功")
@router.post("/update", summary="更新行业") @router.post("/update", summary="更新行业", response_model=BasicResponse[MessageOut])
async def update_industry( async def update_industry(
industry_in: IndustryUpdate, industry_in: IndustryUpdate,
): ):
@ -60,7 +61,7 @@ async def update_industry(
return Success(msg="更新成功") return Success(msg="更新成功")
@router.delete("/delete", summary="删除行业") @router.delete("/delete", summary="删除行业", response_model=BasicResponse[MessageOut])
async def delete_industry( async def delete_industry(
industry_id: int = Query(..., description="行业 ID"), 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 fastapi import APIRouter, Query
from app.controllers.menu import menu_controller 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 * from app.schemas.menus import *
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -11,7 +12,7 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("/list", summary="查看菜单列表") @router.get("/list", summary="查看菜单列表", response_model=PageResponse[BaseMenu])
async def list_menu( async def list_menu(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_menu(
menu_id: int = Query(..., description="菜单id"), menu_id: int = Query(..., description="菜单id"),
): ):
@ -36,7 +37,7 @@ async def get_menu(
return Success(data=result) return Success(data=result)
@router.post("/create", summary="创建菜单") @router.post("/create", summary="创建菜单", response_model=BasicResponse[MessageOut])
async def create_menu( async def create_menu(
menu_in: MenuCreate, menu_in: MenuCreate,
): ):
@ -44,7 +45,7 @@ async def create_menu(
return Success(msg="Created Success") return Success(msg="Created Success")
@router.post("/update", summary="更新菜单") @router.post("/update", summary="更新菜单", response_model=BasicResponse[MessageOut])
async def update_menu( async def update_menu(
menu_in: MenuUpdate, menu_in: MenuUpdate,
): ):
@ -52,7 +53,7 @@ async def update_menu(
return Success(msg="Updated Success") return Success(msg="Updated Success")
@router.delete("/delete", summary="删除菜单") @router.delete("/delete", summary="删除菜单", response_model=BasicResponse[MessageOut])
async def delete_menu( async def delete_menu(
id: int = Query(..., description="菜单id"), 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.controllers.policy import policy_controller
from app.schemas import Success, SuccessExtra 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 from app.schemas.policy import PolicyCreate, PolicyUpdate, PolicyResponse
router = APIRouter(tags=["政策管理"]) router = APIRouter(tags=["政策管理"])
@router.get("/list", summary="查看政策列表") @router.get("/list", summary="查看政策列表", response_model=PageResponse[PolicyResponse])
async def list_policy( async def list_policy(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_policy(
id: int = Query(..., description="政策 ID"), id: int = Query(..., description="政策 ID"),
): ):
@ -37,7 +39,7 @@ async def get_policy(
return Success(data=data) return Success(data=data)
@router.post("/create", summary="创建政策") @router.post("/create", summary="创建政策", response_model=BasicResponse[MessageOut])
async def create_policy( async def create_policy(
policy_in: PolicyCreate, policy_in: PolicyCreate,
): ):
@ -49,7 +51,7 @@ async def create_policy(
return Success(msg="创建成功") return Success(msg="创建成功")
@router.post("/update", summary="更新政策") @router.post("/update", summary="更新政策", response_model=BasicResponse[MessageOut])
async def update_policy( async def update_policy(
policy_in: PolicyUpdate, policy_in: PolicyUpdate,
): ):
@ -63,7 +65,7 @@ async def update_policy(
return Success(msg="更新成功") return Success(msg="更新成功")
@router.delete("/delete", summary="删除政策") @router.delete("/delete", summary="删除政策", response_model=BasicResponse[MessageOut])
async def delete_policy( async def delete_policy(
policy_id: int = Query(..., description="政策 ID"), policy_id: int = Query(..., description="政策 ID"),
): ):

View File

@ -5,14 +5,15 @@ from fastapi.exceptions import HTTPException
from tortoise.expressions import Q from tortoise.expressions import Q
from app.controllers import role_controller 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 * from app.schemas.roles import *
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("/list", summary="查看角色列表") @router.get("/list", summary="查看角色列表", response_model=PageResponse[BaseRole])
async def list_role( async def list_role(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_role(
role_id: int = Query(..., description="角色ID"), role_id: int = Query(..., description="角色ID"),
): ):
@ -34,7 +35,7 @@ async def get_role(
return Success(data=await role_obj.to_dict()) 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): async def create_role(role_in: RoleCreate):
if await role_controller.is_exist(name=role_in.name): if await role_controller.is_exist(name=role_in.name):
raise HTTPException( raise HTTPException(
@ -45,13 +46,13 @@ async def create_role(role_in: RoleCreate):
return Success(msg="Created Successfully") return Success(msg="Created Successfully")
@router.post("/update", summary="更新角色") @router.post("/update", summary="更新角色", response_model=BasicResponse[MessageOut])
async def update_role(role_in: RoleUpdate): async def update_role(role_in: RoleUpdate):
await role_controller.update(id=role_in.id, obj_in=role_in) await role_controller.update(id=role_in.id, obj_in=role_in)
return Success(msg="Updated Successfully") return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除角色") @router.delete("/delete", summary="删除角色", response_model=BasicResponse[MessageOut])
async def delete_role( async def delete_role(
role_id: int = Query(..., description="角色ID"), role_id: int = Query(..., description="角色ID"),
): ):
@ -59,14 +60,14 @@ async def delete_role(
return Success(msg="Deleted Success") 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")): async def get_role_authorized(id: int = Query(..., description="角色ID")):
role_obj = await role_controller.get(id=id) role_obj = await role_controller.get(id=id)
data = await role_obj.to_dict(m2m=True) data = await role_obj.to_dict(m2m=True)
return Success(data=data) return Success(data=data)
@router.post("/authorized", summary="更新角色权限") @router.post("/authorized", summary="更新角色权限", response_model=BasicResponse[MessageOut])
async def update_role_authorized(role_in: RoleUpdateMenusApis): async def update_role_authorized(role_in: RoleUpdateMenusApis):
role_obj = await role_controller.get(id=role_in.id) 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) 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 fastapi import APIRouter, UploadFile, File
from app.controllers.upload import UploadController from app.controllers.upload import UploadController
from app.schemas.upload import ImageUploadResponse from app.schemas.upload import ImageUploadResponse, FileUploadResponse
router = APIRouter() router = APIRouter()
@ -12,3 +12,7 @@ async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
:return: 图片URL和文件名 :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.dept import dept_controller
from app.controllers.user import user_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 * from app.schemas.users import *
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -13,7 +14,7 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("/list", summary="查看用户列表") @router.get("/list", summary="查看用户列表", response_model=PageResponse[BaseUser])
async def list_user( async def list_user(
page: int = Query(1, description="页码"), page: int = Query(1, description="页码"),
page_size: int = Query(10, 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) 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( async def get_user(
user_id: int = Query(..., description="用户ID"), user_id: int = Query(..., description="用户ID"),
): ):
@ -46,7 +47,7 @@ async def get_user(
return Success(data=user_dict) return Success(data=user_dict)
@router.post("/create", summary="创建用户") @router.post("/create", summary="创建用户", response_model=BasicResponse[MessageOut])
async def create_user( async def create_user(
user_in: UserCreate, user_in: UserCreate,
): ):
@ -58,7 +59,7 @@ async def create_user(
return Success(msg="Created Successfully") return Success(msg="Created Successfully")
@router.post("/update", summary="更新用户") @router.post("/update", summary="更新用户", response_model=BasicResponse[MessageOut])
async def update_user( async def update_user(
user_in: UserUpdate, user_in: UserUpdate,
): ):
@ -67,7 +68,7 @@ async def update_user(
return Success(msg="Updated Successfully") return Success(msg="Updated Successfully")
@router.delete("/delete", summary="删除用户") @router.delete("/delete", summary="删除用户", response_model=BasicResponse[MessageOut])
async def delete_user( async def delete_user(
user_id: int = Query(..., description="用户ID"), user_id: int = Query(..., description="用户ID"),
): ):
@ -75,7 +76,7 @@ async def delete_user(
return Success(msg="Deleted Successfully") 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)): async def reset_password(user_id: int = Body(..., description="用户ID", embed=True)):
await user_controller.reset_password(user_id) await user_controller.reset_password(user_id)
return Success(msg="密码已重置为123456") return Success(msg="密码已重置为123456")

View File

@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Query, Depends 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.controllers.valuation import valuation_controller
from app.schemas.valuation import ( from app.schemas.valuation import (
@ -9,15 +9,16 @@ from app.schemas.valuation import (
ValuationAssessmentList, ValuationAssessmentList,
ValuationAssessmentQuery, ValuationAssessmentQuery,
ValuationApprovalRequest, 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 from app.core.ctx import CTX_USER_ID
valuations_router = APIRouter(tags=["估值评估"]) valuations_router = APIRouter(tags=["估值评估"])
@valuations_router.post("/", summary="创建估值评估") @valuations_router.post("/", summary="创建估值评估", response_model=BasicResponse[ValuationAssessmentOut])
async def create_valuation(data: ValuationAssessmentCreate): async def create_valuation(data: ValuationAssessmentCreate):
"""创建新的估值评估记录""" """创建新的估值评估记录"""
try: try:
@ -25,37 +26,51 @@ async def create_valuation(data: ValuationAssessmentCreate):
user_id = CTX_USER_ID.get() user_id = CTX_USER_ID.get()
print(user_id) print(user_id)
result = await valuation_controller.create(data, 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: except Exception as e:
raise HTTPException(status_code=400, detail=f"创建失败: {str(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(): async def get_statistics():
"""获取估值评估统计信息""" """获取估值评估统计信息"""
result = await valuation_controller.get_statistics() result = await valuation_controller.get_statistics()
return Success(data=result, msg="获取统计信息成功") 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): async def get_valuation(valuation_id: int):
"""根据ID获取估值评估详情""" """根据ID获取估值评估详情"""
result = await valuation_controller.get_by_id(valuation_id) result = await valuation_controller.get_by_id(valuation_id)
if not result: if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在") 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): async def update_valuation(valuation_id: int, data: ValuationAssessmentUpdate):
"""更新估值评估记录""" """更新估值评估记录"""
result = await valuation_controller.update(valuation_id, data) result = await valuation_controller.update(valuation_id, data)
if not result: if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在") 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): async def delete_valuation(valuation_id: int):
"""软删除估值评估记录""" """软删除估值评估记录"""
result = await valuation_controller.delete(valuation_id) result = await valuation_controller.delete(valuation_id)
@ -64,7 +79,7 @@ async def delete_valuation(valuation_id: int):
return Success(data={"deleted": True}, msg="删除成功") return Success(data={"deleted": True}, msg="删除成功")
@valuations_router.get("/", summary="获取估值评估列表") @valuations_router.get("/", summary="获取估值评估列表", response_model=PageResponse[ValuationAssessmentOut])
async def get_valuations( async def get_valuations(
asset_name: Optional[str] = Query(None, description="资产名称"), asset_name: Optional[str] = Query(None, description="资产名称"),
institution: Optional[str] = Query(None, description="所属机构"), institution: Optional[str] = Query(None, description="所属机构"),
@ -87,8 +102,10 @@ async def get_valuations(
size=size size=size
) )
result = await valuation_controller.get_list(query) result = await valuation_controller.get_list(query)
import json
items = [json.loads(item.model_dump_json()) for item in result.items]
return SuccessExtra( return SuccessExtra(
data=result.items, data=items,
total=result.total, total=result.total,
page=result.page, page=result.page,
page_size=result.size, 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( async def search_valuations(
keyword: str = Query(..., description="搜索关键词"), keyword: str = Query(..., description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"), page: int = Query(1, ge=1, description="页码"),
@ -105,8 +122,10 @@ async def search_valuations(
): ):
"""根据关键词搜索估值评估记录""" """根据关键词搜索估值评估记录"""
result = await valuation_controller.search(keyword, page, size) result = await valuation_controller.search(keyword, page, size)
import json
items = [json.loads(item.model_dump_json()) for item in result.items]
return SuccessExtra( return SuccessExtra(
data=result.items, data=items,
total=result.total, total=result.total,
page=result.page, page=result.page,
page_size=result.size, 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]): async def batch_delete_valuations(valuation_ids: list[int]):
"""批量软删除估值评估记录""" """批量软删除估值评估记录"""
success_count = 0 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( async def export_valuations(
asset_name: Optional[str] = Query(None, description="资产名称"), asset_name: Optional[str] = Query(None, description="资产名称"),
institution: 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): async def approve_valuation(valuation_id: int, data: ValuationApprovalRequest):
"""审核通过估值评估""" """审核通过估值评估"""
result = await valuation_controller.approve_valuation(valuation_id, data.admin_notes) result = await valuation_controller.approve_valuation(valuation_id, data.admin_notes)
if not result: if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在") 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): async def reject_valuation(valuation_id: int, data: ValuationApprovalRequest):
"""审核拒绝估值评估""" """审核拒绝估值评估"""
result = await valuation_controller.reject_valuation(valuation_id, data.admin_notes) result = await valuation_controller.reject_valuation(valuation_id, data.admin_notes)
if not result: if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在") 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): async def update_admin_notes(valuation_id: int, data: ValuationAdminNotesUpdate):
"""更新管理员备注""" """更新管理员备注"""
result = await valuation_controller.update_admin_notes(valuation_id, data.admin_notes) result = await valuation_controller.update_admin_notes(valuation_id, data.admin_notes)
if not result: if not result:
raise HTTPException(status_code=404, detail="估值评估记录不存在") 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 AppUser
from app.models.user import AppUserQuotaLog
from app.schemas.app_user import AppUserRegisterSchema, AppUserLoginSchema, AppUserUpdateSchema from app.schemas.app_user import AppUserRegisterSchema, AppUserLoginSchema, AppUserUpdateSchema
from app.utils.password import get_password_hash, verify_password from app.utils.password import get_password_hash, verify_password
from app.core.crud import CRUDBase from app.core.crud import CRUDBase
@ -91,6 +92,29 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
await user.save() await user.save()
return user 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: 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 pathlib import Path
from typing import List from typing import List
from fastapi import UploadFile from fastapi import UploadFile
from app.schemas.upload import ImageUploadResponse from app.schemas.upload import ImageUploadResponse, FileUploadResponse
from app.settings.config import settings from app.settings.config import settings
class UploadController: class UploadController:
@ -50,3 +50,41 @@ class UploadController:
url=f"{settings.BASE_URL}/static/images/{filename}", url=f"{settings.BASE_URL}/static/images/{filename}",
filename=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, historical_evidence=valuation.historical_evidence,
patent_certificates=valuation.patent_certificates, patent_certificates=valuation.patent_certificates,
pattern_images=valuation.pattern_images, pattern_images=valuation.pattern_images,
report_url=valuation.report_url,
certificate_url=valuation.certificate_url,
application_maturity=valuation.application_maturity, application_maturity=valuation.application_maturity,
implementation_stage=valuation.implementation_stage, implementation_stage=valuation.implementation_stage,
application_coverage=valuation.application_coverage, application_coverage=valuation.application_coverage,
@ -159,6 +161,8 @@ class UserValuationController:
price_fluctuation=valuation.price_fluctuation, price_fluctuation=valuation.price_fluctuation,
price_range=valuation.price_range, price_range=valuation.price_range,
market_price=valuation.market_price, market_price=valuation.market_price,
credit_code_or_id=valuation.credit_code_or_id,
biz_intro=valuation.biz_intro,
infringement_record=valuation.infringement_record, infringement_record=valuation.infringement_record,
patent_count=valuation.patent_count, patent_count=valuation.patent_count,
esg_value=valuation.esg_value, esg_value=valuation.esg_value,

View File

@ -3,13 +3,15 @@ from tortoise.expressions import Q
from tortoise.queryset import QuerySet from tortoise.queryset import QuerySet
from tortoise.functions import Count from tortoise.functions import Count
from app.models.valuation import ValuationAssessment from app.models.valuation import ValuationAssessment, ValuationCalculationStep
from app.schemas.valuation import ( from app.schemas.valuation import (
ValuationAssessmentCreate, ValuationAssessmentCreate,
ValuationAssessmentUpdate, ValuationAssessmentUpdate,
ValuationAssessmentQuery, ValuationAssessmentQuery,
ValuationAssessmentOut, ValuationAssessmentOut,
ValuationAssessmentList ValuationAssessmentList,
ValuationCalculationStepCreate,
ValuationCalculationStepOut
) )
@ -17,6 +19,56 @@ class ValuationController:
"""估值评估控制器""" """估值评估控制器"""
model = ValuationAssessment 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: async def create(self, data: ValuationAssessmentCreate, user_id: int) -> ValuationAssessmentOut:
"""创建估值评估""" """创建估值评估"""
@ -193,3 +245,4 @@ class ValuationController:
# 创建控制器实例 # 创建控制器实例
valuation_controller = ValuationController() valuation_controller = ValuationController()
from app.log import logger

View File

@ -6,3 +6,4 @@ from .industry import *
from .policy import * from .policy import *
from .user 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="公司邮箱") company_email = fields.CharField(max_length=100, null=True, description="公司邮箱")
is_active = fields.BooleanField(default=True, description="是否激活", index=True) is_active = fields.BooleanField(default=True, description="是否激活", index=True)
last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True) last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True)
remaining_quota = fields.IntField(default=0, description="剩余估值次数", index=True)
class Meta: class Meta:
table = "app_user" 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_ages = fields.JSONField(null=True, description="传承人年龄列表")
inheritor_age_count = fields.JSONField(null=True, description="非遗传承人年龄水平及数量") inheritor_age_count = fields.JSONField(null=True, description="非遗传承人年龄水平及数量")
inheritor_certificates = 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="非遗资产等级") heritage_asset_level = fields.CharField(max_length=50, null=True, description="非遗资产等级")
patent_application_no = fields.CharField(max_length=100, 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="专利剩余年限") patent_remaining_years = fields.CharField(max_length=50, null=True, description="专利剩余年限")
historical_evidence = fields.JSONField(null=True, description="非遗资产历史证明证据及数量") historical_evidence = fields.JSONField(null=True, description="非遗资产历史证明证据及数量")
patent_certificates = fields.JSONField(null=True, description="非遗资产所用专利的证书") patent_certificates = fields.JSONField(null=True, description="非遗资产所用专利的证书")
pattern_images = 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="非遗资产应用成熟度") 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="非遗资产应用覆盖范围") 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="非遗资产跨界合作深度") 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_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="线上相关宣传账号信息") platform_accounts = fields.JSONField(null=True, description="线上相关宣传账号信息")
# 非遗资产衍生商品信息 # 非遗资产衍生商品信息
@ -44,10 +52,13 @@ class ValuationAssessment(Model):
scarcity_level = fields.CharField(max_length=50, null=True, description="稀缺等级") scarcity_level = fields.CharField(max_length=50, null=True, description="稀缺等级")
last_market_activity = fields.CharField(max_length=100, 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="市场活动的时间") 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="月交易额") monthly_transaction_amount = fields.CharField(max_length=50, null=True, description="月交易额")
price_fluctuation = fields.JSONField(null=True, description="该商品近30天价格波动区间") price_fluctuation = fields.JSONField(null=True, description="该商品近30天价格波动区间")
price_range = fields.JSONField(null=True, description="资产商品的价格波动率") price_range = fields.JSONField(null=True, description="资产商品的价格波动率")
market_price = fields.FloatField(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计算字段 # 内置API计算字段
infringement_record = fields.CharField(max_length=100, null=True, description="侵权记录") infringement_record = fields.CharField(max_length=100, null=True, description="侵权记录")
@ -83,3 +94,25 @@ class ValuationAssessment(Model):
def __str__(self): 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 last_login: Optional[datetime] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
remaining_quota: int
class AppUserUpdateSchema(BaseModel): class AppUserUpdateSchema(BaseModel):
@ -67,3 +68,48 @@ class AppUserChangePasswordSchema(BaseModel):
"""AppUser修改密码Schema""" """AppUser修改密码Schema"""
old_password: str = Field(..., description="原密码") 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 from fastapi.responses import JSONResponse
@ -50,3 +52,26 @@ class SuccessExtra(JSONResponse):
} }
content.update(kwargs) content.update(kwargs)
super().__init__(content=content, status_code=code) 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

@ -4,3 +4,8 @@ class ImageUploadResponse(BaseModel):
"""图片上传响应模型""" """图片上传响应模型"""
url: str 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 datetime import datetime
from typing import List, Optional, Any, Dict, Union 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): class ValuationAssessmentBase(BaseModel):
@ -28,6 +29,8 @@ class ValuationAssessmentBase(BaseModel):
historical_evidence: Optional[Dict[str, int]] = Field(None, description="非遗资产历史证明证据及数量") historical_evidence: Optional[Dict[str, int]] = Field(None, description="非遗资产历史证明证据及数量")
patent_certificates: Optional[List[str]] = Field(None, description="非遗资产所用专利的证书") patent_certificates: Optional[List[str]] = Field(None, description="非遗资产所用专利的证书")
pattern_images: 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="非遗资产应用成熟度") 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_fluctuation: Optional[List[Union[str, int, float]]] = Field(None, description="该商品近30天价格波动区间")
price_range: Optional[Dict[str, Union[int, float]]] = Field(None, description="资产商品的价格波动率") # 未使用 price_range: Optional[Dict[str, Union[int, float]]] = Field(None, description="资产商品的价格波动率") # 未使用
market_price: Optional[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计算字段 # 内置API计算字段
infringement_record: Optional[str] = Field(None, description="侵权记录") infringement_record: Optional[str] = Field(None, description="侵权记录")
@ -102,6 +107,8 @@ class ValuationAssessmentUpdate(BaseModel):
historical_evidence: Optional[List[Any]] = Field(None, description="非遗资产历史证明证据及数量") historical_evidence: Optional[List[Any]] = Field(None, description="非遗资产历史证明证据及数量")
patent_certificates: Optional[List[Any]] = Field(None, description="非遗资产所用专利的证书") patent_certificates: Optional[List[Any]] = Field(None, description="非遗资产所用专利的证书")
pattern_images: 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="非遗资产应用成熟度") application_maturity: Optional[str] = Field(None, description="非遗资产应用成熟度")
@ -117,6 +124,8 @@ class ValuationAssessmentUpdate(BaseModel):
last_market_activity: Optional[str] = Field(None, description="该商品最近一次市场活动时间") last_market_activity: Optional[str] = Field(None, description="该商品最近一次市场活动时间")
monthly_transaction: Optional[str] = Field(None, description="月交易额") monthly_transaction: Optional[str] = Field(None, description="月交易额")
price_fluctuation: Optional[List[Union[str, int, float]]] = Field(None, description="该商品近30天价格波动区间") 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="是否激活") is_active: Optional[bool] = Field(None, description="是否激活")
@ -234,3 +243,45 @@ class ValuationApprovalRequest(BaseModel):
class ValuationAdminNotesUpdate(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 = { TORTOISE_ORM: dict = {
"connections": { "connections": {
# SQLite configuration # SQLite configuration
"sqlite": { # "sqlite": {
"engine": "tortoise.backends.sqlite", # "engine": "tortoise.backends.sqlite",
"credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file # "credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file
}, # },
# MySQL/MariaDB configuration # MySQL/MariaDB configuration
# Install with: tortoise-orm[asyncmy] # Install with: tortoise-orm[asyncmy]
# "mysql": { "mysql": {
# "engine": "tortoise.backends.mysql", "engine": "tortoise.backends.mysql",
# "credentials": { "credentials": {
# "host": "localhost", # Database host address "host": "sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com", # Database host address
# "port": 3306, # Database port "port": 28555, # Database port
# "user": "yourusername", # Database username "user": "root", # Database username
# "password": "yourpassword", # Database password "password": "api2api..", # Database password
# "database": "yourdatabase", # Database name "database": "valuation_service", # Database name
# }, },
# }, },
# PostgreSQL configuration # PostgreSQL configuration
# Install with: tortoise-orm[asyncpg] # Install with: tortoise-orm[asyncpg]
# "postgres": { # "postgres": {
@ -87,7 +87,7 @@ class Settings(BaseSettings):
"apps": { "apps": {
"models": { "models": {
"models": ["app.models", "aerich.models"], "models": ["app.models", "aerich.models"],
"default_connection": "sqlite", "default_connection": "mysql",
}, },
}, },
"use_tz": False, # Whether to use timezone-aware datetimes "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" 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() 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 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: try:
# 相对导入(当作为包使用时) # 相对导入(当作为包使用时)
from .sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator from .sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator
from .sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator from .sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError: except ImportError:
# 绝对导入(当直接运行时) # 绝对导入(当直接运行时)
from sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator from sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator
from sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator from sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
class CulturalValueB2Calculator: class CulturalValueB2Calculator:
@ -23,6 +34,7 @@ class CulturalValueB2Calculator:
"""初始化计算器""" """初始化计算器"""
self.living_heritage_calculator = LivingHeritageB21Calculator() self.living_heritage_calculator = LivingHeritageB21Calculator()
self.pattern_gene_calculator = PatternGeneB22Calculator() self.pattern_gene_calculator = PatternGeneB22Calculator()
self.valuation_controller = ValuationController()
def calculate_cultural_value_b2(self, def calculate_cultural_value_b2(self,
living_heritage_b21: float, living_heritage_b21: float,
@ -42,28 +54,58 @@ class CulturalValueB2Calculator:
return cultural_value 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包含所有子公式 计算完整的文化价值B2并记录所有计算步骤
args: 该函数通过整合活态传承系数B21和纹样基因值B22的计算
input_data: 输入数据字典包含所有必要的参数 最终得出文化价值B2每一步的计算过程都会被记录下来
以确保计算的透明度和可追溯性
return: Args:
Dict: 包含所有中间计算结果和最终结果的字典 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 # 计算活态传承系数B21
teaching_frequency = self.living_heritage_calculator.calculate_teaching_frequency( 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["offline_sessions"],
input_data["douyin_views"], input_data["douyin_views"],
input_data["kuaishou_views"], input_data["kuaishou_views"],
input_data["bilibili_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'] 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 # 计算纹样基因值B22
pattern_gene_b22 = self.pattern_gene_calculator.calculate_pattern_gene_b22( pattern_gene_b22 = self.pattern_gene_calculator.calculate_pattern_gene_b22(
@ -71,6 +113,12 @@ class CulturalValueB2Calculator:
input_data['normalized_entropy'], input_data['normalized_entropy'],
input_data['historical_inheritance'] 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 # 计算文化价值B2
cultural_value_b2 = self.calculate_cultural_value_b2( cultural_value_b2 = self.calculate_cultural_value_b2(
@ -78,12 +126,16 @@ class CulturalValueB2Calculator:
pattern_gene_b22 pattern_gene_b22
) )
return { await self.valuation_controller.update_calculation_step(
'living_heritage_b21': living_heritage_b21, step.id, {"status": "completed", "output_result": {"cultural_value_b2": cultural_value_b2}}
'pattern_gene_b22': pattern_gene_b22, )
'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__": 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: class LivingHeritageB21Calculator:
"""活态传承系数B21计算器""" """活态传承系数B21计算器"""
def __init__(self): def __init__(self):
"""初始化计算器""" """初始化计算器"""
pass self.valuation_controller = ValuationController()
def calculate_living_heritage_b21(self, def calculate_living_heritage_b21(self,
inheritor_level_coefficient: float, inheritor_level_coefficient: float,
@ -22,7 +36,6 @@ class LivingHeritageB21Calculator:
""" """
计算活态传承系数B21 计算活态传承系数B21
活态传承系数B21 = 传承人等级系数 × 0.4 + 教学传播频次 × 0.3 + 跨界合作深度 × 0.3 活态传承系数B21 = 传承人等级系数 × 0.4 + 教学传播频次 × 0.3 + 跨界合作深度 × 0.3
args: args:
@ -33,11 +46,9 @@ class LivingHeritageB21Calculator:
return: return:
float: 活态传承系数B21 float: 活态传承系数B21
""" """
#
living_heritage = (inheritor_level_coefficient * 0.4 + living_heritage = (inheritor_level_coefficient * 0.4 +
teaching_frequency * 0.3 + teaching_frequency * 0.3 +
cross_border_depth * 0.3) cross_border_depth * 0.3)
return living_heritage return living_heritage
def calculate_inheritor_level_coefficient(self, inheritor_level: str) -> float: def calculate_inheritor_level_coefficient(self, inheritor_level: str) -> float:
@ -47,8 +58,7 @@ class LivingHeritageB21Calculator:
传承人等级评分标准 传承人等级评分标准
- 国家级传承人: 1 - 国家级传承人: 1
- 省级传承人: 0.7 - 省级传承人: 0.7
- 市级传承人: .44 - 市级传承人: 0.4
args: args:
inheritor_level: 传承人等级 (用户填写) inheritor_level: 传承人等级 (用户填写)
@ -61,7 +71,6 @@ class LivingHeritageB21Calculator:
"省级传承人": 0.7, "省级传承人": 0.7,
"市级传承人": 0.4, "市级传承人": 0.4,
} }
return level_scores.get(inheritor_level, 0.4) return level_scores.get(inheritor_level, 0.4)
def calculate_teaching_frequency(self, def calculate_teaching_frequency(self,
@ -74,16 +83,8 @@ class LivingHeritageB21Calculator:
教学传播频次 = 线下传习次数 × 0.6 + 线上课程点击量 × 0.4 教学传播频次 = 线下传习次数 × 0.6 + 线上课程点击量 × 0.4
线下传习次数统计规范
1) 单次活动标准传承人主导时长2小时参与人数5
2) 频次计算按自然年度累计同一内容重复培训不计入
线上课程折算
- 抖音/快手播放量按100:1折算为学习人次
- B站课程按50:1折算
args: args:
offline_sessions: 线下传习次数符合标准的活动次数 offline_sessions: 线下传习次数
douyin_views: 抖音播放量 douyin_views: 抖音播放量
kuaishou_views: 快手播放量 kuaishou_views: 快手播放量
bilibili_views: B站播放量 bilibili_views: B站播放量
@ -91,71 +92,55 @@ class LivingHeritageB21Calculator:
returns: returns:
float: 教学传播频次评分 float: 教学传播频次评分
""" """
# 线下传习次数权重计算
offline_score = offline_sessions * 0.6 offline_score = offline_sessions * 0.6
# 线上课程点击量折算
# 抖音/快手按100:1折算
douyin_kuaishou_learning_sessions = (douyin_views + kuaishou_views) / 100 douyin_kuaishou_learning_sessions = (douyin_views + kuaishou_views) / 100
# B站按50:1折算
bilibili_learning_sessions = bilibili_views / 50 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
# 总教学传播频次 args:
teaching_frequency = offline_score + online_score cross_border_projects: 跨界合作项目数
return teaching_frequency returns:
def cross_border_depth_dict(border_depth: str) -> float: float: 跨界合作深度评分
cross_border_depth_scores = { """
"品牌联名": 0.3, return min(cross_border_projects, 10.0)
"科技载体": 0.5,
"国家外交礼品": 1,
}
return cross_border_depth_scores.get(border_depth, 0.3)
async def calculate_complete_living_heritage_b21(self, valuation_id: int, input_data: dict) -> float:
# 示例使用 step = await self.valuation_controller.create_calculation_step(
if __name__ == "__main__": ValuationCalculationStepCreate(
calculator = LivingHeritageB21Calculator() valuation_id=valuation_id,
step_order=2.21,
# 示例数据 step_name="活态传承系数B21计算",
inheritor_level = "国家级传承人" # 传承人等级 (用户填写) step_description="开始计算活态传承系数B21",
cross_border_depth = 50.0 input_params=input_data,
# 教学传播频次数据 status="in_progress"
offline_sessions = 20 # 线下传习次数符合标准传承人主导、时长≥2小时、参与人数≥5人
douyin_views = 10000000 # 抖音播放量
kuaishou_views = 0 # 快手播放量
bilibili_views = 0 # B站播放量
# 计算各项指标
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)
# 计算活态传承系数B21
living_heritage_b21 = calculator.calculate_living_heritage_b21(
1, teaching_frequency, 0.3
) )
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'])
print(f"传承人等级系数: {inheritor_level_coefficient:.2f}") living_heritage_b21 = self.calculate_living_heritage_b21(inheritor_level_coefficient, teaching_frequency, cross_border_depth)
print(f"教学传播频次: {teaching_frequency:.2f}")
print(f" - 线下传习次数: {offline_sessions}") await self.valuation_controller.update_calculation_step(
print(f" - 抖音播放量: {douyin_views:,}") step.id, {"status": "completed", "output_result": {"living_heritage_b21": living_heritage_b21}}
print(f" - 快手播放量: {kuaishou_views:,}") )
print(f" - B站播放量: {bilibili_views:,}") return living_heritage_b21
print(f"跨界合作深度: {cross_border_depth:.2f}") except Exception as e:
print(f"活态传承系数B21: {living_heritage_b21:.4f}") 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 @@
""" import sys
纹样基因值B22计算模块 import os
纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10
"""
import math import math
from typing import Dict, List 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: class PatternGeneB22Calculator:
"""纹样基因值B22计算器""" """纹样基因值B22计算器"""
def __init__(self): def __init__(self):
"""初始化计算器""" """初始化计算器"""
pass self.valuation_controller = ValuationController()
def calculate_pattern_gene_b22(self, def calculate_pattern_gene_b22(self,
structure_complexity: float, structure_complexity: float,
@ -24,7 +29,6 @@ class PatternGeneB22Calculator:
""" """
计算纹样基因值B22 计算纹样基因值B22
纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10 纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10
args: args:
@ -35,11 +39,9 @@ class PatternGeneB22Calculator:
return: return:
float: 纹样基因值B22 float: 纹样基因值B22
""" """
pattern_gene = ((structure_complexity * 0.6 + pattern_gene = ((structure_complexity * 0.6 +
normalized_entropy * 0.4) * normalized_entropy * 0.4) *
historical_inheritance * 10) historical_inheritance * 10)
return pattern_gene return pattern_gene
def calculate_structure_complexity(self, pattern_elements: List[Dict]) -> float: def calculate_structure_complexity(self, pattern_elements: List[Dict]) -> float:
@ -87,20 +89,17 @@ class PatternGeneB22Calculator:
if not pattern_data or len(pattern_data) <= 1: if not pattern_data or len(pattern_data) <= 1:
return 0.0 return 0.0
# 计算概率分布
total = sum(pattern_data) total = sum(pattern_data)
if total == 0: if total == 0:
return 0.0 return 0.0
probabilities = [x / total for x in pattern_data if x > 0] probabilities = [x / total for x in pattern_data if x > 0]
# 计算信息熵
entropy = 0.0 entropy = 0.0
for p in probabilities: for p in probabilities:
if p > 0: if p > 0:
entropy -= p * math.log2(p) entropy -= p * math.log2(p)
# 归一化
n = len(probabilities) n = len(probabilities)
if n <= 1: if n <= 1:
return 0.0 return 0.0
@ -108,36 +107,31 @@ class PatternGeneB22Calculator:
normalized_entropy = entropy / math.log2(n) normalized_entropy = entropy / math.log2(n)
return normalized_entropy 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(
if __name__ == "__main__": valuation_id=valuation_id,
step_order=2.22,
calculator = PatternGeneB22Calculator() step_name="纹样基因值B22计算",
step_description="开始计算纹样基因值B22",
# 示例数据 input_params=input_data,
pattern_elements = [ status="in_progress"
{'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
) )
)
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']
print(f"结构复杂度SC: {structure_complexity:.4f}") pattern_gene_b22 = self.calculate_pattern_gene_b22(structure_complexity, normalized_entropy, historical_inheritance)
print(f"归一化信息熵H: {normalized_entropy:.4f}")
print(f"历史传承度HI: {historical_inheritance:.4f}") await self.valuation_controller.update_calculation_step(
print(f"纹样基因值B22: {pattern_gene_b22:.4f}") 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 typing import Dict
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
try: from .sub_formulas.basic_value_b11 import BasicValueB11Calculator
# 相对导入(当作为包使用时)
from .sub_formulas.basic_value_b11 import BasicValueB11Calculator, calculate_popularity_score
from .sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator from .sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator
from .sub_formulas.policy_multiplier_b13 import PolicyMultiplierB13Calculator 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
class EconomicValueB1Calculator: class EconomicValueB1Calculator:
@ -27,6 +22,7 @@ class EconomicValueB1Calculator:
self.basic_value_calculator = BasicValueB11Calculator() self.basic_value_calculator = BasicValueB11Calculator()
self.traffic_factor_calculator = TrafficFactorB12Calculator() self.traffic_factor_calculator = TrafficFactorB12Calculator()
self.policy_multiplier_calculator = PolicyMultiplierB13Calculator() self.policy_multiplier_calculator = PolicyMultiplierB13Calculator()
self.valuation_controller = ValuationController()
def calculate_economic_value_b1(self, def calculate_economic_value_b1(self,
basic_value_b11: float, basic_value_b11: float,
@ -50,79 +46,84 @@ class EconomicValueB1Calculator:
return economic_value 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包含所有子公式 计算完整的经济价值B1并记录所有计算步骤
args: 此函数集成了基础价值B11流量因子B12和政策乘数B13的计算
input_data: 输入数据字典包含所有必要的参数 通过调用相应的子计算器来完成每一步的计算结果都会被记录下来
以支持后续的审计和分析
returns: Args:
Dict: 包含所有中间计算结果和最终结果的字典 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] step = await self.valuation_controller.create_calculation_step(
financial_value = self.basic_value_calculator.calculate_financial_value_f(input_data["three_year_income"]) ValuationCalculationStepCreate(
# 计算法律强度L patent_score: 专利分 (0-10分) (用户填写) valuation_id=valuation_id,
# popularity_score: 普及地域分 (0-10分) (用户填写) step_order=2.1,
# infringement_score: 侵权分 (0-10分) (用户填写) step_name="经济价值B1计算",
step_description="开始计算经济价值B1公式为基础价值B11 × (1 + 流量因子B12) × 政策乘数B13",
legal_strength = self.basic_value_calculator.calculate_legal_strength_l( input_params=input_data,
input_data["patent_score"], status="in_progress"
input_data["popularity_score"],
input_data["infringement_score"],
) )
# 发展潜力 patent_count: 专利分 (0-10分) (用户填写)
# esg_score: ESG分 (0-10分) (用户填写)
# innovation_ratio: 创新投入比 (研发费用/营收) * 100 (用户填写)
development_potential = self.basic_value_calculator.calculate_development_potential_d(
input_data["patent_count"],
input_data["esg_score"],
input_data["innovation_ratio"],
) )
# 计算行业系数I target_industry_roe: 目标行业平均ROE (系统配置) try:
# benchmark_industry_roe: 基准行业ROE (系统配置)
# industry_coefficient = self.basic_value_calculator.calculate_industry_coefficient_i(
#
# )
# 计算基础价值B11 # 计算基础价值B11
basic_value_b11 = self.basic_value_calculator.calculate_basic_value_b11( basic_value_b11 = self.basic_value_calculator.calculate_basic_value_b11(
financial_value, # 财务价值F self.basic_value_calculator.calculate_financial_value_f(input_data["three_year_income"]),
legal_strength, # 法律强度L self.basic_value_calculator.calculate_legal_strength_l(input_data["patent_score"], input_data["popularity_score"], input_data["infringement_score"]),
development_potential, self.basic_value_calculator.calculate_development_potential_d(input_data["patent_count"], input_data["esg_score"], input_data["innovation_ratio"]),
input_data["industry_coefficient"] input_data["industry_coefficient"]
) )
await self.valuation_controller.create_calculation_step(
# 计算互动量指数 ValuationCalculationStepCreate(
interaction_index = self.traffic_factor_calculator.calculate_interaction_index( valuation_id=valuation_id, step_order=2.11, step_name="基础价值B11",
input_data["likes"], output_result={'basic_value_b11': basic_value_b11}, status="completed"
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)
# 计算流量因子B12
traffic_factor_b12 = self.traffic_factor_calculator.calculate_traffic_factor_b12( traffic_factor_b12 = self.traffic_factor_calculator.calculate_traffic_factor_b12(
input_data['search_index_s1'], input_data['search_index_s1'],
input_data['industry_average_s2'], input_data['industry_average_s2'],
social_media_spread_s3 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"
)
) )
# 计算政策乘数B13 # 计算政策乘数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_multiplier_b13 = self.policy_multiplier_calculator.calculate_policy_multiplier_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"]
)
)
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"
)
) )
# 计算经济价值B1 # 计算经济价值B1
@ -132,13 +133,16 @@ class EconomicValueB1Calculator:
policy_multiplier_b13 policy_multiplier_b13
) )
return { await self.valuation_controller.update_calculation_step(
'basic_value_b11': basic_value_b11, step.id, {"status": "completed", "output_result": {"economic_value_b1": economic_value_b1}}
'traffic_factor_b12': traffic_factor_b12, )
'policy_multiplier_b13': policy_multiplier_b13, return economic_value_b1
'economic_value_b1': 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__": if __name__ == "__main__":

View File

@ -1,13 +1,25 @@
import math import math
from typing import List, Optional 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: class BasicValueB11Calculator:
"""基础价值B11计算器""" """基础价值B11计算器"""
def __init__(self): def __init__(self):
"""初始化计算器""" """初始化计算器"""
pass self.valuation_controller = ValuationController()
def calculate_basic_value_b11(self, def calculate_basic_value_b11(self,
financial_value: float, financial_value: float,
@ -168,9 +180,7 @@ class BasicValueB11Calculator:
return industry_coefficient return industry_coefficient
def _calculate_patent_score(self, patent_remaining_years: int) -> float:
# 专利相关计算函数
def calculate_patent_score(patent_remaining_years: int) -> float:
""" """
计算专利分 计算专利分
@ -192,9 +202,7 @@ def calculate_patent_score(patent_remaining_years: int) -> float:
else: else:
return 3.0 return 3.0
def _calculate_patent_usage_score(self, patent_count: int) -> float:
# 识别用户所上传的图像中的专利号通过API验证专利是否存在按所用专利数量赋分未引用0分每引用一项+2.5分10分封顶0-10分
def calculate_patent_usage_score(patent_count: int) -> float:
""" """
计算专利使用量分 计算专利使用量分
@ -212,9 +220,7 @@ def calculate_patent_usage_score(patent_count: int) -> float:
score = min(patent_count * 2.5, 10.0) score = min(patent_count * 2.5, 10.0)
return score return score
def _calculate_popularity_score(self, region_coverage: str) -> float:
# 普及地域评分
def calculate_popularity_score(region_coverage: str) -> float:
""" """
计算普及地域分 计算普及地域分
@ -234,9 +240,7 @@ def calculate_popularity_score(region_coverage: str) -> float:
return coverage_scores.get(region_coverage, 7.0) return coverage_scores.get(region_coverage, 7.0)
def _calculate_infringement_score(self, infringement_status: str) -> float:
# 侵权记录评分
def calculate_infringement_score(infringement_status: str) -> float:
""" """
计算侵权记录分 计算侵权记录分
@ -256,6 +260,66 @@ def calculate_infringement_score(infringement_status: str) -> float:
return infringement_scores.get(infringement_status, 6.0) return infringement_scores.get(infringement_status, 6.0)
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"
)
)
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"
)
)
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"
)
)
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__": 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: class PolicyMultiplierB13Calculator:
"""政策乘数B13计算器""" """政策乘数B13计算器"""
def __init__(self): def __init__(self):
"""初始化计算器""" """初始化计算器"""
pass self.valuation_controller = ValuationController()
def calculate_policy_multiplier_b13(self, policy_compatibility_score: float) -> float: def calculate_policy_multiplier_b13(self, policy_compatibility_score: float) -> float:
""" """
计算政策乘数B13 计算政策乘数B13
政策乘数B13 = 1 + (政策契合度评分P × 0.15) 政策乘数B13 = 1 + (政策契合度评分P × 0.15)
Args: Args:
@ -18,9 +31,7 @@ class PolicyMultiplierB13Calculator:
returns: returns:
float: 政策乘数B13 float: 政策乘数B13
""" """
#
policy_multiplier = 1 + (policy_compatibility_score * 0.15) policy_multiplier = 1 + (policy_compatibility_score * 0.15)
return policy_multiplier return policy_multiplier
def calculate_policy_compatibility_score(self, def calculate_policy_compatibility_score(self,
@ -30,7 +41,6 @@ class PolicyMultiplierB13Calculator:
""" """
计算政策契合度评分P 计算政策契合度评分P
政策契合度P = 政策匹配度 × 0.4 + 实施阶段评分 × 0.3 + 资金支持度 × 0.3 政策契合度P = 政策匹配度 × 0.4 + 实施阶段评分 × 0.3 + 资金支持度 × 0.3
Args: Args:
@ -41,11 +51,9 @@ class PolicyMultiplierB13Calculator:
returns: returns:
float: 政策契合度评分P float: 政策契合度评分P
""" """
#
policy_compatibility = (policy_match_score * 0.4 + policy_compatibility = (policy_match_score * 0.4 +
implementation_stage_score * 0.3 + implementation_stage_score * 0.3 +
funding_support_score * 0.3) funding_support_score * 0.3)
return policy_compatibility return policy_compatibility
def calculate_policy_match_score(self, industry: str) -> float: def calculate_policy_match_score(self, industry: str) -> float:
@ -60,8 +68,8 @@ class PolicyMultiplierB13Calculator:
returns: returns:
float: 政策匹配度 float: 政策匹配度
""" """
# 此处应有更复杂的逻辑根据行业匹配政策,暂时返回固定值
return 5 return 5.0
def calculate_implementation_stage_score(self, implementation_stage: str) -> float: def calculate_implementation_stage_score(self, implementation_stage: str) -> float:
""" """
@ -80,8 +88,7 @@ class PolicyMultiplierB13Calculator:
"推广阶段": 7.0, "推广阶段": 7.0,
"试点阶段": 4.0 "试点阶段": 4.0
} }
return stage_scores.get(implementation_stage, 7.0)
return stage_scores.get(implementation_stage, 10.0)
def calculate_funding_support_score(self, funding_support: str) -> float: def calculate_funding_support_score(self, funding_support: str) -> float:
""" """
@ -100,8 +107,44 @@ class PolicyMultiplierB13Calculator:
"省级资助": 7.0, "省级资助": 7.0,
"无资助": 0.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 import math
from typing import Dict, Tuple 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: class TrafficFactorB12Calculator:
"""流量因子B12计算器""" """流量因子B12计算器"""
def __init__(self): def __init__(self):
"""初始化计算器""" """初始化计算器"""
pass self.valuation_controller = ValuationController()
def calculate_traffic_factor_b12(self, def calculate_traffic_factor_b12(self,
search_index_s1: float, search_index_s1: float,
@ -31,18 +42,15 @@ class TrafficFactorB12Calculator:
if industry_average_s2 == 0: if industry_average_s2 == 0:
raise ValueError("行业均值S2必须大于0") raise ValueError("行业均值S2必须大于0")
if search_index_s1 == 0: if search_index_s1 <= 0:
# 如果搜索指数为0或负数使用最小值避免对数计算错误 # 如果搜索指数为0或负数使用最小值避免对数计算错误
search_index_s1 = 1.0 search_index_s1 = 1.0
# ,不进行任何拆分
traffic_factor = (math.log(search_index_s1 / industry_average_s2) * 0.3 + traffic_factor = (math.log(search_index_s1 / industry_average_s2) * 0.3 +
social_media_spread_s3 * 0.7) social_media_spread_s3 * 0.7)
return traffic_factor return traffic_factor
def calculate_social_media_spread_s3(self, def calculate_social_media_spread_s3(self,
interaction_index: float, interaction_index: float,
coverage_index: float, coverage_index: float,
@ -60,7 +68,6 @@ class TrafficFactorB12Calculator:
returns: returns:
float: 社交媒体传播度S3 float: 社交媒体传播度S3
""" """
#
social_media_spread = (interaction_index * 0.4 + social_media_spread = (interaction_index * 0.4 +
coverage_index * 0.3 + coverage_index * 0.3 +
conversion_efficiency * 0.3) conversion_efficiency * 0.3)
@ -84,7 +91,6 @@ class TrafficFactorB12Calculator:
returns: returns:
float: 互动量指数 float: 互动量指数
""" """
#
interaction_index = (likes + comments + shares) / 1000.0 interaction_index = (likes + comments + shares) / 1000.0
return interaction_index return interaction_index
@ -101,12 +107,46 @@ class TrafficFactorB12Calculator:
returns: returns:
float: 覆盖人群指数 float: 覆盖人群指数
""" """
#
if followers == 0: if followers == 0:
return 0 return 0.0
coverage_index = followers / 10000.0 coverage_index = followers / 10000.0
return coverage_index 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, def calculate_conversion_efficiency(self,
click_count: int, click_count: int,
view_count: int) -> float: view_count: int) -> float:

View File

@ -19,10 +19,14 @@ try:
# 包内相对导入 # 包内相对导入
from .model_value_b import ModelValueBCalculator from .model_value_b import ModelValueBCalculator
from ..market_value_c import MarketValueCCalculator from ..market_value_c import MarketValueCCalculator
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError: except ImportError:
# 直接运行时的绝对导入 # 直接运行时的绝对导入
from app.utils.calculation_engine.final_value_ab.model_value_b import ModelValueBCalculator 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.utils.calculation_engine.market_value_c import MarketValueCCalculator
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
class FinalValueACalculator: class FinalValueACalculator:
@ -32,6 +36,7 @@ class FinalValueACalculator:
"""初始化计算器""" """初始化计算器"""
self.model_value_calculator = ModelValueBCalculator() self.model_value_calculator = ModelValueBCalculator()
self.market_value_calculator = MarketValueCCalculator() self.market_value_calculator = MarketValueCCalculator()
self.valuation_controller = ValuationController()
def calculate_final_value_a(self, def calculate_final_value_a(self,
model_value_b: float, model_value_b: float,
@ -64,17 +69,32 @@ class FinalValueACalculator:
return final_value 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 import time
start_time = time.time() start_time = time.time()
step_order = 1
# 记录输入参数 # 记录输入参数
logger.info("final_value_a.calculation_start input_data_keys={} model_data_keys={} market_data_keys={}", logger.info("final_value_a.calculation_start input_data_keys={} model_data_keys={} market_data_keys={}",
@ -82,6 +102,19 @@ class FinalValueACalculator:
list(input_data.get('model_data', {}).keys()), list(input_data.get('model_data', {}).keys()),
list(input_data.get('market_data', {}).keys())) list(input_data.get('market_data', {}).keys()))
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_data = input_data.get('model_data', {}) model_data = input_data.get('model_data', {})
if 'economic_data' in model_data: if 'economic_data' in model_data:
@ -132,11 +165,11 @@ class FinalValueACalculator:
logger.info("final_value_a.calculating_model_value_b 开始计算模型估值B") logger.info("final_value_a.calculating_model_value_b 开始计算模型估值B")
model_start_time = time.time() model_start_time = time.time()
try: model_result = await self.model_value_calculator.calculate_complete_model_value_b(
model_result = self.model_value_calculator.calculate_complete_model_value_b( valuation_id,
input_data['model_data'] 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 model_duration = time.time() - model_start_time
logger.info("final_value_a.model_value_b_calculated 模型估值B计算完成: 模型估值B={}万元 耗时={}ms 返回字段={}", logger.info("final_value_a.model_value_b_calculated 模型估值B计算完成: 模型估值B={}万元 耗时={}ms 返回字段={}",
@ -144,19 +177,28 @@ class FinalValueACalculator:
int(model_duration * 1000), int(model_duration * 1000),
list(model_result.keys())) list(model_result.keys()))
except Exception as e: await self.valuation_controller.create_calculation_step(
logger.error("final_value_a.model_value_b_calculation_failed 模型估值B计算失败: 错误={} 输入数据={}", str(e), input_data.get('model_data', {})) ValuationCalculationStepCreate(
raise 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 # 计算市场估值C
logger.info("final_value_a.calculating_market_value_c 开始计算市场估值C") logger.info("final_value_a.calculating_market_value_c 开始计算市场估值C")
market_start_time = time.time() market_start_time = time.time()
try:
market_result = await self.market_value_calculator.calculate_complete_market_value_c( market_result = await self.market_value_calculator.calculate_complete_market_value_c(
valuation_id,
input_data['market_data'] 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 market_duration = time.time() - market_start_time
logger.info("final_value_a.market_value_c_calculated 市场估值C计算完成: 市场估值C={}万元 耗时={}ms 返回字段={}", logger.info("final_value_a.market_value_c_calculated 市场估值C计算完成: 市场估值C={}万元 耗时={}ms 返回字段={}",
@ -164,15 +206,23 @@ class FinalValueACalculator:
int(market_duration * 1000), int(market_duration * 1000),
list(market_result.keys())) list(market_result.keys()))
except Exception as e: await self.valuation_controller.create_calculation_step(
logger.error("final_value_a.market_value_c_calculation_failed 市场估值C计算失败: 错误={} 输入数据={}", str(e), input_data.get('market_data', {})) ValuationCalculationStepCreate(
raise 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 # 计算最终估值A
logger.info("final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B={}万元 市场估值C={}万元", logger.info("final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B={}万元 市场估值C={}万元",
model_value_b, market_value_c) model_value_b, market_value_c)
try:
final_value_a = self.calculate_final_value_a( final_value_a = self.calculate_final_value_a(
model_value_b, model_value_b,
market_value_c market_value_c
@ -188,16 +238,37 @@ class FinalValueACalculator:
int(model_duration * 1000), int(model_duration * 1000),
int(market_duration * 1000)) int(market_duration * 1000))
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,
}
except Exception as e: except Exception as e:
logger.error("final_value_a.final_value_calculation_failed 最终估值A计算失败: 错误={} 模型估值B={}万元 市场估值C={}万元", logger.error("final_value_a.calculation_failed 计算失败: 错误={}", str(e))
str(e), model_value_b, market_value_c) 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 raise
return {
'model_value_b': model_value_b,
'market_value_c': market_value_c,
'final_value_ab': final_value_a,
}

View File

@ -12,10 +12,14 @@ try:
# 相对导入(当作为包使用时) # 相对导入(当作为包使用时)
from ..economic_value_b1.economic_value_b1 import EconomicValueB1Calculator from ..economic_value_b1.economic_value_b1 import EconomicValueB1Calculator
from ..cultural_value_b2.cultural_value_b2 import CulturalValueB2Calculator from ..cultural_value_b2.cultural_value_b2 import CulturalValueB2Calculator
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError: except ImportError:
# 绝对导入(当直接运行时) # 绝对导入(当直接运行时)
from app.utils.calculation_engine.economic_value_b1.economic_value_b1 import EconomicValueB1Calculator 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.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: class ModelValueBCalculator:
@ -26,6 +30,7 @@ class ModelValueBCalculator:
self.economic_value_calculator = EconomicValueB1Calculator() self.economic_value_calculator = EconomicValueB1Calculator()
self.cultural_value_calculator = CulturalValueB2Calculator() self.cultural_value_calculator = CulturalValueB2Calculator()
self.risk_adjustment_calculator = RiskAdjustmentB3Calculator() self.risk_adjustment_calculator = RiskAdjustmentB3Calculator()
self.valuation_controller = ValuationController()
def calculate_model_value_b(self, def calculate_model_value_b(self,
economic_value_b1: float, economic_value_b1: float,
@ -46,45 +51,127 @@ class ModelValueBCalculator:
return model_value 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: Args:
input_data: 输入数据字典包含所有必要的参数 valuation_id (int): 估值的唯一标识符用于关联所有计算步骤
input_data (Dict): 包含所有计算所需参数的字典结构应包含
{
'economic_data': { ... }, # 经济价值B1所需数据
'cultural_data': { ... }, # 文化价值B2所需数据
'risky_data': { ... } # 风险调整系数B3所需数据
}
Returns: Returns:
Dict: 包含所有中间计算结果和最终结果的字典 float: 计算得出的模型估值B
Raises:
Exception: 在计算过程中遇到的任何异常都会被捕获记录然后重新抛出
""" """
# 计算经济价值B1 step_order = 1
economic_result = self.economic_value_calculator.calculate_complete_economic_value_b1( 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"
)
)
step_order += 1
try:
# 计算经济价值B1传入估值ID并等待异步完成
economic_value_b1 = await self.economic_value_calculator.calculate_complete_economic_value_b1(
valuation_id,
input_data['economic_data'] input_data['economic_data']
) )
economic_value_b1 = economic_result['economic_value_b1'] 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
# 计算文化价值B2 # 计算文化价值B2传入估值ID并等待异步完成
cultural_result = self.cultural_value_calculator.calculate_complete_cultural_value_b2( cultural_value_b2 = await self.cultural_value_calculator.calculate_complete_cultural_value_b2(
valuation_id,
input_data['cultural_data'] input_data['cultural_data']
) )
cultural_value_b2 = cultural_result['cultural_value_b2'] 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
risk_value_result = self.risk_adjustment_calculator.calculate_complete_risky_value_b3( # 计算风险调整系数B3传入估值ID并等待异步完成
risk_value_b3 = await self.risk_adjustment_calculator.calculate_complete_risky_value_b3(
valuation_id,
input_data['risky_data'] input_data['risky_data']
) )
risk_value_b3 = risk_value_result['risk_adjustment_b3'] 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 # 计算模型估值B
model_value_b = self.calculate_model_value_b( model_value_b = self.calculate_model_value_b(
economic_value_b1, economic_value_b1,
cultural_value_b2, cultural_value_b2,
risk_value_b3 risk_value_b3
) )
await self.valuation_controller.create_calculation_step(
return { ValuationCalculationStepCreate(
'economic_value_b1': economic_value_b1, valuation_id=valuation_id,
'cultural_value_b2': cultural_value_b2, step_order=step_order,
'risk_value_b3': risk_value_b3, step_name="计算模型估值B",
'model_value_b': model_value_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__)) current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path: if current_dir not in sys.path:
sys.path.append(current_dir) sys.path.append(current_dir)
# 添加项目根目录
sys.path.append(os.path.join(current_dir, '..', '..', '..'))
try: try:
# 相对导入(当作为包使用时) # 相对导入(当作为包使用时)
@ -16,6 +19,8 @@ try:
from .sub_formulas.scarcity_multiplier_c3 import ScarcityMultiplierC3Calculator from .sub_formulas.scarcity_multiplier_c3 import ScarcityMultiplierC3Calculator
from .sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator from .sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator
from .market_data_analyzer import market_data_analyzer from .market_data_analyzer import market_data_analyzer
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
except ImportError: except ImportError:
# 绝对导入(当直接运行时) # 绝对导入(当直接运行时)
from sub_formulas.market_bidding_c1 import MarketBiddingC1Calculator 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.scarcity_multiplier_c3 import ScarcityMultiplierC3Calculator
from sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator from sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator
from market_data_analyzer import market_data_analyzer from market_data_analyzer import market_data_analyzer
from app.controllers.valuation import ValuationController
from app.schemas.valuation import ValuationCalculationStepCreate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -36,6 +43,7 @@ class MarketValueCCalculator:
self.heat_coefficient_calculator = HeatCoefficientC2Calculator() self.heat_coefficient_calculator = HeatCoefficientC2Calculator()
self.scarcity_multiplier_calculator = ScarcityMultiplierC3Calculator() self.scarcity_multiplier_calculator = ScarcityMultiplierC3Calculator()
self.temporal_decay_calculator = TemporalDecayC4Calculator() self.temporal_decay_calculator = TemporalDecayC4Calculator()
self.valuation_controller = ValuationController()
async def _get_dynamic_default_price(self, input_data: Dict) -> float: async def _get_dynamic_default_price(self, input_data: Dict) -> float:
""" """
@ -95,46 +103,89 @@ class MarketValueCCalculator:
return market_value 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包含所有子公式 计算完整的市场估值C并记录每一步的计算过程
args: 该函数通过顺序调用市场竞价C1热度系数C2稀缺性乘数C3和时效性衰减C4的计算器
input_data: 输入数据字典包含所有必要的参数 最终得出市场估值C计算过程中的每个子步骤都会被详细记录以便于审计和跟踪
参数来源标记用户填写/系统配置/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: Args:
Dict: 包含所有中间计算结果和最终结果的字典 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"
)
)
try:
# 计算市场竞价C1 # 计算市场竞价C1
market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1( market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1(
transaction_data={'weighted_average_price': input_data.get('weighted_average_price', 0)}, transaction_data={'weighted_average_price': input_data.get('weighted_average_price', 0)},
manual_bids=input_data.get('manual_bids', []), manual_bids=input_data.get('manual_bids', []),
expert_valuations=input_data.get('expert_valuations', []) 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 # 计算热度系数C2
heat_coefficient_c2 = self.heat_coefficient_calculator.calculate_heat_coefficient_c2( heat_coefficient_c2 = self.heat_coefficient_calculator.calculate_heat_coefficient_c2(
input_data.get('daily_browse_volume', 500.0), input_data.get('daily_browse_volume', 500.0),
input_data.get('collection_count', 50) 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 # 计算稀缺性乘数C3
scarcity_multiplier_c3 = self.scarcity_multiplier_calculator.calculate_scarcity_multiplier_c3( scarcity_multiplier_c3 = self.scarcity_multiplier_calculator.calculate_scarcity_multiplier_c3(
input_data.get('issuance_level', '限量') 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 # 计算时效性衰减C4
temporal_decay_c4 = self.temporal_decay_calculator.calculate_temporal_decay_c4( temporal_decay_c4 = self.temporal_decay_calculator.calculate_temporal_decay_c4(
input_data.get('recent_market_activity', '2024-01-15') 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 # 计算市场估值C
market_value_c = self.calculate_market_value_c( market_value_c = self.calculate_market_value_c(
@ -144,13 +195,17 @@ class MarketValueCCalculator:
temporal_decay_c4 temporal_decay_c4
) )
return { await self.valuation_controller.update_calculation_step(
'market_bidding_c1': market_bidding_c1, step.id, {"status": "completed", "output_result": {"market_value_c": market_value_c}}
'heat_coefficient_c2': heat_coefficient_c2, )
'scarcity_multiplier_c3': scarcity_multiplier_c3, return market_value_c
'temporal_decay_c4': temporal_decay_c4, except Exception as e:
'market_value_c': market_value_c 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 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: class RiskAdjustmentB3Calculator:
"""风险调整系数B3计算器""" """风险调整系数B3计算器"""
def __init__(self): def __init__(self):
pass self.valuation_controller = ValuationController()
def calculate_risk_adjustment_b3(self, risk_score_sum: float) -> float: def calculate_risk_adjustment_b3(self, risk_score_sum: float) -> float:
""" """
@ -155,22 +167,81 @@ class RiskAdjustmentB3Calculator:
return max_score return max_score
def calculate_complete_risky_value_b3(self, input_data: Dict) -> Dict: async def calculate_complete_risky_value_b3(self, valuation_id: int, input_data: Dict) -> float:
"""
计算完整的风险调整系数B3并记录所有计算步骤
该函数通过整合市场风险法律风险和传承风险的评估
计算出风险评分总和R并最终得出风险调整系数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"]) 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"]) 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"]) 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 # 计算风险评分总和R
risk_score_sum = self.calculate_risk_score_sum(market_risk, legal_risk, inheritance_risk) risk_score_sum = self.calculate_risk_score_sum(market_risk, legal_risk, inheritance_risk)
# 计算风险调整系数B3 # 计算风险调整系数B3
risk_adjustment_b3 = self.calculate_risk_adjustment_b3(risk_score_sum) risk_adjustment_b3 = self.calculate_risk_adjustment_b3(risk_score_sum)
return {
'risk_score_sum': risk_score_sum,
'risk_adjustment_b3': risk_adjustment_b3
}
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__": 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", "websockets==14.1",
"pyproject-toml>=0.1.0", "pyproject-toml>=0.1.0",
"uvloop==0.21.0 ; sys_platform != 'win32'", "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] [tool.black]

View File

@ -64,3 +64,7 @@ uvicorn==0.34.0
uvloop==0.21.0 uvloop==0.21.0
watchfiles==1.0.4 watchfiles==1.0.4
websockets==14.1 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' ] = '%(asctime)s - %(levelname)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%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 export DOCKER_DEFAULT_PLATFORM=linux/amd64
docker build -t zfc931912343/guzhi-fastapi-admin:v1.4 . docker build -t zfc931912343/bindbox-game:v1.0 .
docker push zfc931912343/guzhi-fastapi-admin:v1.4 docker push zfc931912343/bindbox-game:v1.0
# 运行容器 # 运行容器