feat: 重构后端服务并添加新功能
refactor: 优化API路由和响应模型 feat(admin): 添加App用户管理接口 feat(sms): 实现阿里云短信服务集成 feat(email): 添加SMTP邮件发送功能 feat(upload): 支持文件上传接口 feat(rate-limiter): 实现手机号限流器 fix: 修复计算步骤入库问题 docs: 更新API文档和测试计划 chore: 更新依赖和配置
This commit is contained in:
parent
ebf41d74c8
commit
cc352d3184
54
.github/copilot-instructions.md
vendored
54
.github/copilot-instructions.md
vendored
@ -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`(默认 SQLite,db 文件在项目根 `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 的迁移示例命令),我可以继续补充。请告诉我你希望强调的额外部分或需要澄清的地方。
|
||||
59
.trae/documents/API 文档重组与分类(v1)实施计划.md
Normal file
59
.trae/documents/API 文档重组与分类(v1)实施计划.md
Normal 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/Body(Body 引用 `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`(处理器)。
|
||||
|
||||
## 示例格式(两条)
|
||||
- `app|POST /api/v1/sms/send-code`(v1)
|
||||
- 功能:发送登录验证码到手机号
|
||||
- 公开/认证:公开
|
||||
- 请求参数(Body):`SendCodeRequest`
|
||||
- 响应结构:`SendResponse`(`{code,msg,data}`)
|
||||
- 错误码:`400/422/500`
|
||||
- 代码参照:`app/api/v1/sms/sms.py:68`
|
||||
- `admin|GET /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` 分类的计划;确认后我将开始生成完整文档并交付。
|
||||
184
.trae/documents/交易管理_用户管理_估值模块增量改造方案.md
Normal file
184
.trae/documents/交易管理_用户管理_估值模块增量改造方案.md
Normal file
@ -0,0 +1,184 @@
|
||||
## 现状速览
|
||||
|
||||
* 后端框架:FastAPI;ORM:Tortoise;权限:`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`
|
||||
|
||||
* Body(multipart 或 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` 下载后附加。
|
||||
|
||||
* 错误处理:
|
||||
|
||||
* 参数校验失败返回 422;SMTP 异常记录 `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`
|
||||
|
||||
——请确认方案后,我将按上述步骤开始落地实现、编写迁移与测试。
|
||||
71
.trae/documents/估值二期 API 设计方案.md
Normal file
71
.trae/documents/估值二期 API 设计方案.md
Normal 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-53);403 无权限;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 条)。
|
||||
- 异步任务完成 ≤ 5s;QPS(单实例):读 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 方案,确认后我将开始后端路由/控制器/模型实现并提供前端对接示例。
|
||||
184
.trae/documents/估值计算步骤 API 设计与落实方案.md
Normal file
184
.trae/documents/估值计算步骤 API 设计与落实方案.md
Normal 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. 经济价值 B1(economic\_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. 文化价值 B2(cultural\_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. 风险调整 B3(risk\_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. 市场价值 C(market\_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. 最终估值 AB(final\_value\_ab)
|
||||
|
||||
* 模型估值 B:`B = B1 + B2`;再叠加风险调整:`B_adj = B * B3`
|
||||
|
||||
* 市场估值:`C`
|
||||
|
||||
* 最终:`Final = f(B_adj, C)`(例如加权平均或规则合成)
|
||||
|
||||
## 变量定义与来源映射
|
||||
|
||||
* 用户输入(UserValuationCreate,app/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、社交传播S3(app/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`:失败描述
|
||||
|
||||
* 步骤编号建议:
|
||||
|
||||
* 经济价值 B1:2.1x(B11=2.11,B12=2.12,B13=2.13,汇总B1=2.19)
|
||||
|
||||
* 文化价值 B2:2.2x(B21=2.21,B22=2.22,汇总B2=2.29)
|
||||
|
||||
* 风险调整 B3:2.3x(总评R=2.30,B3=2.31)
|
||||
|
||||
* 市场价值 C:3.1x(C1=3.11,C2=3.12,C3=3.13,C4=3.14,汇总C=3.19)
|
||||
|
||||
* 最终估值 AB:4.1x(B组合=4.11,B×B3=4.12,Final=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/C(app/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()` 修复)。
|
||||
|
||||
## 交付
|
||||
|
||||
* 我将按上述规范逐步在计算引擎各子模块与统一计算入口中补充“步骤写入”,并确保管理端端点返回可序列化的数据结构;完成后会提供一份面向管理员的“估值步骤查看”前后端对接说明(端点与字段)。
|
||||
|
||||
23
.trae/documents/修复计算任务的NameError并确保步骤入库与日志可见.md
Normal file
23
.trae/documents/修复计算任务的NameError并确保步骤入库与日志可见.md
Normal file
@ -0,0 +1,23 @@
|
||||
## 问题
|
||||
- 后台任务在提取B1参数时引用未定义函数(calculate_popularity_score、calculate_patent_score),导致计算中止,步骤未入库。
|
||||
|
||||
## 修复方案
|
||||
1) 移除未定义函数引用,在 `_extract_calculation_params_b1` 内实现本地计算:
|
||||
- 普及地域分:mapping {全球覆盖:10,全国覆盖:7,区域覆盖:4},默认7
|
||||
- 专利分:按剩余年限近似 {>10年:10,5-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.updated;Admin脚本打印“后台估值计算步骤”。
|
||||
|
||||
——确认后我将按以上方案进行代码调整并回填日志。
|
||||
29
.trae/documents/合并输出:估值二期前后端 API 需求总览文档.md
Normal file
29
.trae/documents/合并输出:估值二期前后端 API 需求总览文档.md
Normal 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. 删除分散文档,仅保留总览
|
||||
|
||||
——请确认上述方案,确认后我将生成唯一的总览文档并删除分散文档。
|
||||
26
.trae/documents/在不改计算逻辑前提下修复步骤入库(step_order类型调整).md
Normal file
26
.trae/documents/在不改计算逻辑前提下修复步骤入库(step_order类型调整).md
Normal 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,并执行验证,保证不改变计算逻辑,仅解决类型不匹配问题。
|
||||
17
.trae/documents/完善计算步骤落库与测试输出详细化.md
Normal file
17
.trae/documents/完善计算步骤落库与测试输出详细化.md
Normal 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`
|
||||
- 运行脚本并查看详细步骤输出
|
||||
99
.trae/documents/接入阿里云短信并提供两个接口.md
Normal file
99
.trae/documents/接入阿里云短信并提供两个接口.md
Normal file
@ -0,0 +1,99 @@
|
||||
## 目标与范围
|
||||
- 接入阿里云短信服务,封装发送客户端
|
||||
- 提供两类发送接口:验证码通知、报告生成通知,供 App 调用
|
||||
- 支持模板动态调用与验证码变量 `${code}` 的正确替换
|
||||
- 记录发送日志并融入现有审计体系
|
||||
- 实现同一手机号每分钟不超过 1 条的频率限制
|
||||
- 安全存储 AccessKey 等敏感信息(环境变量/配置)
|
||||
|
||||
## 技术选型
|
||||
- 后端框架:FastAPI(现有工程)
|
||||
- 短信 SDK:Alibaba Cloud SMS Python SDK(Tea/OpenAPI V2,`alibabacloud_dysmsapi20170525`)
|
||||
- 端点(中国站):`dysmsapi.aliyuncs.com`
|
||||
- 关键请求字段:`PhoneNumbers`、`SignName`、`TemplateCode`、`TemplateParam`
|
||||
- 日志:沿用 `app/log` 的 Loguru 与审计中间件
|
||||
- 频率限制:服务内共享的内存限流(后续可升级为 Redis)
|
||||
- 安全:通过环境变量注入凭证,Pydantic Settings 读取
|
||||
- 参考文档:
|
||||
- Alibaba Cloud SDK V2(Python)示例(SendSms):https://www.alibabacloud.com/help/en/sdk/developer-reference/v2-python-integrated-sdk
|
||||
- 短信服务 SendSms 接口(2017-05-25):https://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` 为 4–8 位数字(可按需约束)
|
||||
|
||||
## 模板与变量替换
|
||||
- 验证码模板:`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 文档)
|
||||
|
||||
## 交付物
|
||||
- 新增短信客户端与路由模块
|
||||
- 两个可调用接口(验证码发送、报告通知发送)
|
||||
- 限流与日志落地,配置基于环境变量
|
||||
41
.trae/documents/接口测试脚本(用户端 + 后台)实施计划.md
Normal file
41
.trae/documents/接口测试脚本(用户端 + 后台)实施计划.md
Normal 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` 并填充完整逻辑;默认数据内置,必要处留参数。
|
||||
- 如你需要,我可在脚本创建后直接运行,输出结果供你检查。
|
||||
20
.trae/documents/更新测试脚本以使用你提供的参数.md
Normal file
20
.trae/documents/更新测试脚本以使用你提供的参数.md
Normal 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`
|
||||
- 观察输出:用户侧成功提交,后台列表/详情显示完整数据,步骤列表非空。
|
||||
|
||||
——确认后我将直接更新脚本并提交。
|
||||
48
.trae/documents/更新用户端 API 到文档.md
Normal file
48
.trae/documents/更新用户端 API 到文档.md
Normal 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中查看。
|
||||
|
||||
103
.trae/documents/项目结构整理与架构文档(web目录).md
Normal file
103
.trae/documents/项目结构整理与架构文档(web目录).md
Normal 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 个文档并提交供评审。
|
||||
23
AGENTS.md
23
AGENTS.md
@ -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), isort’s 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.
|
||||
83
aaa.json
83
aaa.json
@ -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万元"
|
||||
}
|
||||
@ -26,11 +26,33 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
openapi_tags = [
|
||||
{"name": "app-用户认证与账户", "description": "用户端账户与认证相关接口(公开/需认证以端点说明为准)"},
|
||||
{"name": "app-估值评估", "description": "用户端估值评估相关接口(需用户端认证)"},
|
||||
{"name": "app-短信服务", "description": "用户端短信验证码与登录相关接口(公开)"},
|
||||
{"name": "app-上传", "description": "用户端文件上传接口(公开)"},
|
||||
{"name": "admin-基础", "description": "后台登录与个人信息接口(部分公开,其他需认证)"},
|
||||
{"name": "admin-用户管理", "description": "后台用户管理接口(需认证与权限)"},
|
||||
{"name": "admin-角色管理", "description": "后台角色管理接口(需认证与权限)"},
|
||||
{"name": "admin-菜单管理", "description": "后台菜单管理接口(需认证与权限)"},
|
||||
{"name": "admin-API权限管理", "description": "后台 API 权限管理接口(需认证与权限)"},
|
||||
{"name": "admin-部门管理", "description": "后台部门管理接口(需认证与权限)"},
|
||||
{"name": "admin-审计日志", "description": "后台审计日志查询接口(需认证与权限)"},
|
||||
{"name": "admin-估值评估", "description": "后台估值评估接口(需认证与权限)"},
|
||||
{"name": "admin-发票管理", "description": "后台发票与抬头管理接口(需认证与权限)"},
|
||||
{"name": "admin-交易管理", "description": "后台交易/对公转账记录接口(需认证与权限)"},
|
||||
{"name": "admin-内置接口", "description": "后台第三方内置接口调用(需认证与权限)"},
|
||||
{"name": "admin-行业管理", "description": "后台行业数据管理(当前公开)"},
|
||||
{"name": "admin-指数管理", "description": "后台指数数据管理(当前公开)"},
|
||||
{"name": "admin-政策管理", "description": "后台政策数据管理(当前公开)"},
|
||||
{"name": "admin-ESG管理", "description": "后台 ESG 数据管理(当前公开)"},
|
||||
]
|
||||
app = FastAPI(
|
||||
title=settings.APP_TITLE,
|
||||
description=settings.APP_DESCRIPTION,
|
||||
version=settings.VERSION,
|
||||
openapi_url="/openapi.json",
|
||||
openapi_tags=openapi_tags,
|
||||
middleware=make_middlewares(),
|
||||
lifespan=lifespan,
|
||||
redirect_slashes=False, # 禁用尾部斜杠重定向
|
||||
|
||||
@ -5,6 +5,7 @@ from app.utils.app_user_jwt import get_current_app_user
|
||||
|
||||
from .apis import apis_router
|
||||
from .app_users import app_users_router
|
||||
from .app_users.admin_manage import admin_app_users_router
|
||||
from .app_valuations import app_valuations_router
|
||||
from .auditlog import auditlog_router
|
||||
from .base import base_router
|
||||
@ -19,28 +20,36 @@ from .third_party_api import third_party_api_router
|
||||
from .upload import router as upload_router
|
||||
from .users import users_router
|
||||
from .valuations import router as valuations_router
|
||||
from .invoice.invoice import invoice_router
|
||||
from .transactions.transactions import transactions_router
|
||||
from .sms.sms import router as sms_router
|
||||
|
||||
v1_router = APIRouter()
|
||||
|
||||
v1_router.include_router(base_router, prefix="/base")
|
||||
v1_router.include_router(app_users_router, prefix="/app-user") # AppUser路由,无需权限依赖
|
||||
v1_router.include_router(base_router, prefix="/base", tags=["admin-基础"])
|
||||
v1_router.include_router(app_users_router, prefix="/app-user", tags=["app-用户认证与账户"]) # AppUser路由,无需权限依赖
|
||||
v1_router.include_router(admin_app_users_router, prefix="/app-user-admin", tags=["admin-App用户管理"])
|
||||
# 注意:app-valuations 路由在各自的端点内部使用 get_current_app_user 进行认证
|
||||
# 这样可以保持App用户认证系统的独立性,不与后台管理权限系统混合
|
||||
v1_router.include_router(app_valuations_router, prefix="/app-valuations") # 用户端估值评估路由
|
||||
v1_router.include_router(users_router, prefix="/user", dependencies=[DependAuth, DependPermission])
|
||||
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependAuth, DependPermission])
|
||||
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependAuth, DependPermission])
|
||||
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependAuth, DependPermission])
|
||||
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependAuth, DependPermission])
|
||||
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependAuth, DependPermission])
|
||||
v1_router.include_router(esg_router, prefix="/esg")
|
||||
v1_router.include_router(index_router, prefix="/index")
|
||||
v1_router.include_router(industry_router, prefix="/industry")
|
||||
v1_router.include_router(policy_router, prefix="/policy")
|
||||
v1_router.include_router(upload_router, prefix="/upload") # 文件上传路由
|
||||
v1_router.include_router(app_valuations_router, prefix="/app-valuations", tags=["app-估值评估"]) # 用户端估值评估路由
|
||||
v1_router.include_router(users_router, prefix="/user", dependencies=[DependAuth, DependPermission], tags=["admin-用户管理"])
|
||||
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependAuth, DependPermission], tags=["admin-角色管理"])
|
||||
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependAuth, DependPermission], tags=["admin-菜单管理"])
|
||||
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependAuth, DependPermission], tags=["admin-API权限管理"])
|
||||
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependAuth, DependPermission], tags=["admin-部门管理"])
|
||||
v1_router.include_router(auditlog_router, prefix="/auditlog", dependencies=[DependAuth, DependPermission], tags=["admin-审计日志"])
|
||||
v1_router.include_router(esg_router, prefix="/esg", tags=["admin-ESG管理"])
|
||||
v1_router.include_router(index_router, prefix="/index", tags=["admin-指数管理"])
|
||||
v1_router.include_router(industry_router, prefix="/industry", tags=["admin-行业管理"])
|
||||
v1_router.include_router(policy_router, prefix="/policy", tags=["admin-政策管理"])
|
||||
v1_router.include_router(upload_router, prefix="/upload", tags=["app-上传"]) # 文件上传路由
|
||||
v1_router.include_router(
|
||||
third_party_api_router,
|
||||
prefix="/third_party_api",
|
||||
dependencies=[DependAuth, DependPermission],
|
||||
tags=["admin-内置接口"],
|
||||
)
|
||||
v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission])
|
||||
v1_router.include_router(valuations_router, prefix="/valuations", dependencies=[DependAuth, DependPermission], tags=["admin-估值评估"])
|
||||
v1_router.include_router(invoice_router, prefix="/invoice", dependencies=[DependAuth, DependPermission], tags=["admin-发票管理"])
|
||||
v1_router.include_router(transactions_router, prefix="/transactions", dependencies=[DependAuth, DependPermission], tags=["admin-交易管理"])
|
||||
v1_router.include_router(sms_router, prefix="/sms", tags=["app-短信服务"])
|
||||
|
||||
@ -3,12 +3,14 @@ from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.api import api_controller
|
||||
from app.schemas import Success, SuccessExtra
|
||||
from app.schemas.base import BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.apis import BaseApi
|
||||
from app.schemas.apis import *
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看API列表")
|
||||
@router.get("/list", summary="查看API列表", response_model=PageResponse[BaseApi])
|
||||
async def list_api(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -28,7 +30,7 @@ async def list_api(
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看Api")
|
||||
@router.get("/get", summary="查看Api", response_model=BasicResponse[BaseApi])
|
||||
async def get_api(
|
||||
id: int = Query(..., description="Api"),
|
||||
):
|
||||
@ -37,7 +39,7 @@ async def get_api(
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建Api")
|
||||
@router.post("/create", summary="创建Api", response_model=BasicResponse[MessageOut])
|
||||
async def create_api(
|
||||
api_in: ApiCreate,
|
||||
):
|
||||
@ -45,7 +47,7 @@ async def create_api(
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新Api")
|
||||
@router.post("/update", summary="更新Api", response_model=BasicResponse[MessageOut])
|
||||
async def update_api(
|
||||
api_in: ApiUpdate,
|
||||
):
|
||||
@ -53,7 +55,7 @@ async def update_api(
|
||||
return Success(msg="Update Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除Api")
|
||||
@router.delete("/delete", summary="删除Api", response_model=BasicResponse[MessageOut])
|
||||
async def delete_api(
|
||||
api_id: int = Query(..., description="ApiID"),
|
||||
):
|
||||
@ -61,7 +63,7 @@ async def delete_api(
|
||||
return Success(msg="Deleted Success")
|
||||
|
||||
|
||||
@router.post("/refresh", summary="刷新API列表")
|
||||
@router.post("/refresh", summary="刷新API列表", response_model=BasicResponse[MessageOut])
|
||||
async def refresh_api():
|
||||
await api_controller.refresh_api()
|
||||
return Success(msg="OK")
|
||||
|
||||
76
app/api/v1/app_users/admin_manage.py
Normal file
76
app/api/v1/app_users/admin_manage.py
Normal 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="获取成功")
|
||||
@ -6,19 +6,25 @@ from app.schemas.app_user import (
|
||||
AppUserJWTOut,
|
||||
AppUserInfoOut,
|
||||
AppUserUpdateSchema,
|
||||
AppUserChangePasswordSchema
|
||||
AppUserChangePasswordSchema,
|
||||
AppUserDashboardOut,
|
||||
AppUserQuotaOut,
|
||||
)
|
||||
from app.schemas.app_user import AppUserRegisterOut, TokenValidateOut
|
||||
from app.schemas.base import BasicResponse, MessageOut
|
||||
from app.utils.app_user_jwt import (
|
||||
create_app_user_access_token,
|
||||
get_current_app_user,
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
from app.models.user import AppUser
|
||||
from app.controllers.user_valuation import user_valuation_controller
|
||||
from app.controllers.invoice import invoice_controller
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/register", response_model=dict, summary="用户注册")
|
||||
@router.post("/register", response_model=BasicResponse[AppUserRegisterOut], summary="用户注册")
|
||||
async def register(
|
||||
register_data: AppUserRegisterSchema
|
||||
):
|
||||
@ -30,11 +36,11 @@ async def register(
|
||||
user = await app_user_controller.register(register_data)
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "注册成功",
|
||||
"msg": "注册成功",
|
||||
"data": {
|
||||
"user_id": user.id,
|
||||
"phone": user.phone,
|
||||
"default_password": register_data.phone[-6:] # 返回默认密码供用户知晓
|
||||
"default_password": register_data.phone[-6:]
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
@ -68,12 +74,12 @@ async def login(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout", summary="用户登出")
|
||||
@router.post("/logout", summary="用户登出", response_model=BasicResponse[MessageOut])
|
||||
async def logout(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
用户登出(客户端需要删除本地token)
|
||||
"""
|
||||
return {"code": 200, "message": "登出成功"}
|
||||
return {"code": 200, "msg": "OK", "data": {"message": "登出成功"}}
|
||||
|
||||
|
||||
@router.get("/profile", response_model=AppUserInfoOut, summary="获取用户信息")
|
||||
@ -84,6 +90,49 @@ async def get_profile(current_user: AppUser = Depends(get_current_app_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/dashboard", response_model=AppUserDashboardOut, summary="用户首页摘要")
|
||||
async def get_dashboard(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
用户首页摘要
|
||||
功能:
|
||||
- 返回剩余估值次数(暂以 0 占位,后续可接入配额系统)
|
||||
- 返回最近一条估值评估记录(若有)
|
||||
- 返回待处理发票数量
|
||||
"""
|
||||
# 最近估值记录
|
||||
latest = await user_valuation_controller.model.filter(user_id=current_user.id).order_by("-created_at").first()
|
||||
latest_out = None
|
||||
if latest:
|
||||
latest_out = {
|
||||
"id": latest.id,
|
||||
"asset_name": latest.asset_name,
|
||||
"valuation_result": latest.final_value_ab,
|
||||
"status": latest.status,
|
||||
"created_at": latest.created_at.isoformat() if latest.created_at else "",
|
||||
}
|
||||
# 待处理发票数量
|
||||
try:
|
||||
pending_invoices = await invoice_controller.count_pending_for_user(current_user.id)
|
||||
except Exception:
|
||||
pending_invoices = 0
|
||||
# 剩余估值次数(占位,可从用户扩展字段或配额表获取)
|
||||
remaining_quota = 0
|
||||
return AppUserDashboardOut(remaining_quota=remaining_quota, latest_valuation=latest_out, pending_invoices=pending_invoices)
|
||||
|
||||
|
||||
@router.get("/quota", response_model=AppUserQuotaOut, summary="剩余估值次数")
|
||||
async def get_quota(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
剩余估值次数查询
|
||||
说明:
|
||||
- 当前实现返回默认 0 次与用户类型占位
|
||||
- 若后续接入配额系统,可从数据库中读取真实值
|
||||
"""
|
||||
remaining_count = 0
|
||||
user_type = "体验用户"
|
||||
return AppUserQuotaOut(remaining_count=remaining_count, user_type=user_type)
|
||||
|
||||
|
||||
@router.put("/profile", response_model=AppUserInfoOut, summary="更新用户信息")
|
||||
async def update_profile(
|
||||
update_data: AppUserUpdateSchema,
|
||||
@ -99,7 +148,7 @@ async def update_profile(
|
||||
return updated_user
|
||||
|
||||
|
||||
@router.post("/change-password", summary="修改密码")
|
||||
@router.post("/change-password", summary="修改密码", response_model=BasicResponse[MessageOut])
|
||||
async def change_password(
|
||||
password_data: AppUserChangePasswordSchema,
|
||||
current_user: AppUser = Depends(get_current_app_user)
|
||||
@ -116,17 +165,17 @@ async def change_password(
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="原密码错误")
|
||||
|
||||
return {"code": 200, "message": "密码修改成功"}
|
||||
return {"code": 200, "msg": "OK", "data": {"message": "密码修改成功"}}
|
||||
|
||||
|
||||
@router.get("/validate-token", summary="验证token")
|
||||
@router.get("/validate-token", summary="验证token", response_model=BasicResponse[TokenValidateOut])
|
||||
async def validate_token(current_user: AppUser = Depends(get_current_app_user)):
|
||||
"""
|
||||
验证token是否有效
|
||||
"""
|
||||
return {
|
||||
"code": 200,
|
||||
"message": "token有效",
|
||||
"msg": "token有效",
|
||||
"data": {
|
||||
"user_id": current_user.id,
|
||||
"phone": current_user.phone
|
||||
|
||||
@ -9,6 +9,8 @@ import asyncio
|
||||
import time
|
||||
|
||||
from app.controllers.user_valuation import user_valuation_controller
|
||||
from app.controllers.valuation import valuation_controller
|
||||
from app.schemas.valuation import ValuationAssessmentUpdate
|
||||
from app.schemas.valuation import (
|
||||
UserValuationCreate,
|
||||
UserValuationQuery,
|
||||
@ -16,13 +18,13 @@ from app.schemas.valuation import (
|
||||
UserValuationOut,
|
||||
UserValuationDetail
|
||||
)
|
||||
from app.schemas.base import Success, SuccessExtra
|
||||
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
|
||||
from app.utils.app_user_jwt import get_current_app_user_id, get_current_app_user
|
||||
from app.utils.calculation_engine import FinalValueACalculator
|
||||
from app.utils.calculation_engine.cultural_value_b2.sub_formulas.living_heritage_b21 import cross_border_depth_dict
|
||||
# from app.utils.calculation_engine.cultural_value_b2.sub_formulas.living_heritage_b21 import cross_border_depth_dict
|
||||
from app.utils.calculation_engine.drp import DynamicPledgeRateCalculator
|
||||
from app.utils.calculation_engine.economic_value_b1.sub_formulas.basic_value_b11 import calculate_popularity_score, \
|
||||
calculate_infringement_score, calculate_patent_usage_score, calculate_patent_score
|
||||
# from app.utils.calculation_engine.economic_value_b1.sub_formulas.basic_value_b11 import calculate_popularity_score
|
||||
|
||||
from app.utils.calculation_engine.economic_value_b1.sub_formulas.traffic_factor_b12 import calculate_search_index_s1
|
||||
from app.log.log import logger
|
||||
from app.models.esg import ESG
|
||||
@ -108,12 +110,18 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
matched = [item for item in data_list if
|
||||
isinstance(item, dict) and item.get("SQH") == getattr(data, 'patent_application_no', None)]
|
||||
if matched:
|
||||
patent_count = calculate_patent_usage_score(len(matched))
|
||||
input_data_by_b1["patent_count"] = float(patent_count)
|
||||
patent_count_score = min(len(matched) * 2.5, 10.0)
|
||||
input_data_by_b1["patent_count"] = float(patent_count_score)
|
||||
else:
|
||||
input_data_by_b1["patent_count"] = 0.0
|
||||
|
||||
patent_score = calculate_patent_score(calculate_total_years(data_list))
|
||||
years_total = calculate_total_years(data_list)
|
||||
if years_total > 10:
|
||||
patent_score = 10.0
|
||||
elif years_total >= 5:
|
||||
patent_score = 7.0
|
||||
else:
|
||||
patent_score = 3.0
|
||||
input_data_by_b1["patent_score"] = patent_score
|
||||
|
||||
# 提取 文化价值B2 计算参数
|
||||
@ -141,8 +149,20 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
|
||||
|
||||
calculator = FinalValueACalculator()
|
||||
# 计算最终估值A(统一计算)
|
||||
calculation_result = await calculator.calculate_complete_final_value_a(input_data)
|
||||
# 先创建估值记录以获取ID,方便步骤落库关联
|
||||
initial_detail = await user_valuation_controller.create_valuation(
|
||||
user_id=user_id,
|
||||
data=data,
|
||||
calculation_result=None,
|
||||
calculation_input=None,
|
||||
drp_result=None,
|
||||
status='pending'
|
||||
)
|
||||
valuation_id = initial_detail.id
|
||||
logger.info("valuation.init_created user_id={} valuation_id={}", user_id, valuation_id)
|
||||
|
||||
# 计算最终估值A(统一计算),传入估值ID以关联步骤落库
|
||||
calculation_result = await calculator.calculate_complete_final_value_a(valuation_id, input_data)
|
||||
|
||||
# 计算动态质押
|
||||
drp_c = DynamicPledgeRateCalculator()
|
||||
@ -168,10 +188,12 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 创建估值评估记录
|
||||
result = await user_valuation_controller.create_valuation(
|
||||
user_id=user_id,
|
||||
data=data,
|
||||
# 更新估值评估记录(写入计算结果与输入摘要)
|
||||
update_data = ValuationAssessmentUpdate(
|
||||
model_value_b=calculation_result.get('model_value_b'),
|
||||
market_value_c=calculation_result.get('market_value_c'),
|
||||
final_value_ab=calculation_result.get('final_value_ab'),
|
||||
dynamic_pledge_rate=drp_result,
|
||||
calculation_result=calculation_result,
|
||||
calculation_input={
|
||||
'model_data': {
|
||||
@ -181,8 +203,15 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
},
|
||||
'market_data': list(input_data.get('market_data', {}).keys()),
|
||||
},
|
||||
drp_result=drp_result,
|
||||
status='success' # 计算成功,设置为approved状态
|
||||
status='success'
|
||||
)
|
||||
result = await valuation_controller.update(valuation_id, update_data)
|
||||
logger.info(
|
||||
"valuation.updated valuation_id={} model_b={} market_c={} final_ab={}",
|
||||
valuation_id,
|
||||
calculation_result.get('model_value_b'),
|
||||
calculation_result.get('market_value_c'),
|
||||
calculation_result.get('final_value_ab'),
|
||||
)
|
||||
|
||||
logger.info("valuation.background_calc_success user_id={} valuation_id={}", user_id, result.id)
|
||||
@ -192,22 +221,16 @@ async def _perform_valuation_calculation(user_id: int, data: UserValuationCreate
|
||||
print(traceback.format_exc())
|
||||
logger.error("valuation.background_calc_failed user_id={} err={}", user_id, repr(e))
|
||||
|
||||
# 计算失败时也创建记录,状态设置为failed
|
||||
# 计算失败时更新记录为失败状态
|
||||
try:
|
||||
result = await user_valuation_controller.create_valuation(
|
||||
user_id=user_id,
|
||||
data=data,
|
||||
calculation_result=None,
|
||||
calculation_input=None,
|
||||
drp_result=None,
|
||||
status='rejected' # 计算失败,设置为rejected状态
|
||||
)
|
||||
logger.info("valuation.failed_record_created user_id={} valuation_id={}", user_id, result.id)
|
||||
if 'valuation_id' in locals():
|
||||
fail_update = ValuationAssessmentUpdate(status='rejected')
|
||||
await valuation_controller.update(valuation_id, fail_update)
|
||||
except Exception as create_error:
|
||||
logger.error("valuation.failed_to_create_record user_id={} err={}", user_id, repr(create_error))
|
||||
logger.error("valuation.failed_to_update_record user_id={} err={}", user_id, repr(create_error))
|
||||
|
||||
|
||||
@app_valuations_router.post("/", summary="创建估值评估")
|
||||
@app_valuations_router.post("/", summary="创建估值评估", response_model=BasicResponse[dict])
|
||||
async def calculate_valuation(
|
||||
background_tasks: BackgroundTasks,
|
||||
data: UserValuationCreate,
|
||||
@ -315,7 +338,13 @@ async def _extract_calculation_params_b1(data: UserValuationCreate) -> Dict[str,
|
||||
|
||||
# 法律强度L相关参数
|
||||
# 普及地域分值 默认 7分
|
||||
popularity_score = calculate_popularity_score(data.application_coverage)
|
||||
# 普及地域分:全球覆盖(10)、全国覆盖(7)、区域覆盖(4),默认全国覆盖(7)
|
||||
try:
|
||||
coverage = data.application_coverage or "全国覆盖"
|
||||
mapping = {"全球覆盖": 10.0, "全国覆盖": 7.0, "区域覆盖": 4.0}
|
||||
popularity_score = mapping.get(coverage, 7.0)
|
||||
except Exception:
|
||||
popularity_score = 7.0
|
||||
|
||||
# 创新投入比 = (研发费用/营收) * 100
|
||||
try:
|
||||
@ -427,18 +456,50 @@ async def _extract_calculation_params_b2(data: UserValuationCreate) -> Dict[str,
|
||||
kuaishou_views = safe_float(rs.get("kuaishou", None).get("likes", 0)) if rs.get("kuaishou", None) else 0
|
||||
bilibili_views = safe_float(rs.get("bilibili", None).get("likes", 0)) if rs.get("bilibili", None) else 0
|
||||
|
||||
# 跨界合作深度 品牌联名0.3,科技载体0.5,国家外交礼品1.0
|
||||
cross_border_depth = cross_border_depth_dict(data.cooperation_depth)
|
||||
# 跨界合作深度:将枚举映射为项目数;若为数值字符串则直接取数值
|
||||
try:
|
||||
val = getattr(data, 'cooperation_depth', None)
|
||||
mapping = {
|
||||
"品牌联名": 3.0,
|
||||
"科技载体": 5.0,
|
||||
"国家外交礼品": 10.0,
|
||||
}
|
||||
if isinstance(val, str):
|
||||
cross_border_depth = mapping.get(val, safe_float(val))
|
||||
else:
|
||||
cross_border_depth = safe_float(val)
|
||||
except Exception:
|
||||
cross_border_depth = 0.0
|
||||
|
||||
# 纹样基因值B22相关参数
|
||||
|
||||
# 以下三项需由后续模型/服务计算;此处提供默认可计算占位
|
||||
#
|
||||
# 历史传承度HI(用户填写)
|
||||
historical_inheritance = sum([safe_float(i) for i in data.historical_evidence])
|
||||
historical_inheritance = 0.0
|
||||
try:
|
||||
if isinstance(data.historical_evidence, dict):
|
||||
historical_inheritance = sum([safe_float(v) for v in data.historical_evidence.values()])
|
||||
elif isinstance(data.historical_evidence, (list, tuple)):
|
||||
historical_inheritance = sum([safe_float(i) for i in data.historical_evidence])
|
||||
except Exception:
|
||||
historical_inheritance = 0.0
|
||||
structure_complexity = 1.5 # 默认值 纹样基因熵值B22(系统计算)
|
||||
normalized_entropy = 9 # 默认值 归一化信息熵H(系统计算)
|
||||
|
||||
logger.info(
|
||||
"b2.params inheritor_level_coefficient={} offline_sessions={} douyin_views={} kuaishou_views={} bilibili_views={} cross_border_depth={} historical_inheritance={} structure_complexity={} normalized_entropy={}",
|
||||
inheritor_level_coefficient,
|
||||
offline_sessions,
|
||||
douyin_views,
|
||||
kuaishou_views,
|
||||
bilibili_views,
|
||||
cross_border_depth,
|
||||
historical_inheritance,
|
||||
structure_complexity,
|
||||
normalized_entropy,
|
||||
)
|
||||
|
||||
return {
|
||||
"inheritor_level_coefficient": inheritor_level_coefficient,
|
||||
"offline_sessions": offline_sessions,
|
||||
@ -563,7 +624,7 @@ async def _extract_calculation_params_c(data: UserValuationCreate) -> Dict[str,
|
||||
}
|
||||
|
||||
|
||||
@app_valuations_router.get("/", summary="获取我的估值评估列表")
|
||||
@app_valuations_router.get("/", summary="获取我的估值评估列表", response_model=PageResponse[UserValuationOut])
|
||||
async def get_my_valuations(
|
||||
query: UserValuationQuery = Depends(),
|
||||
current_user: AppUser = Depends(get_current_app_user)
|
||||
@ -595,7 +656,7 @@ async def get_my_valuations(
|
||||
)
|
||||
|
||||
|
||||
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情")
|
||||
@app_valuations_router.get("/{valuation_id}", summary="获取估值评估详情", response_model=BasicResponse[UserValuationDetail])
|
||||
async def get_valuation_detail(
|
||||
valuation_id: int,
|
||||
current_user: AppUser = Depends(get_current_app_user)
|
||||
@ -628,7 +689,7 @@ async def get_valuation_detail(
|
||||
)
|
||||
|
||||
|
||||
@app_valuations_router.get("/statistics/overview", summary="获取我的估值评估统计")
|
||||
@app_valuations_router.get("/statistics/overview", summary="获取我的估值评估统计", response_model=BasicResponse[dict])
|
||||
async def get_my_valuation_statistics(
|
||||
current_user: AppUser = Depends(get_current_app_user)
|
||||
):
|
||||
@ -647,7 +708,7 @@ async def get_my_valuation_statistics(
|
||||
)
|
||||
|
||||
|
||||
@app_valuations_router.delete("/{valuation_id}", summary="删除估值评估")
|
||||
@app_valuations_router.delete("/{valuation_id}", summary="删除估值评估", response_model=BasicResponse[dict])
|
||||
async def delete_valuation(
|
||||
valuation_id: int,
|
||||
current_user: AppUser = Depends(get_current_app_user)
|
||||
@ -705,3 +766,4 @@ def safe_float(v):
|
||||
return float(v)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
from app.log.log import logger
|
||||
|
||||
@ -6,7 +6,7 @@ from app.controllers.user import user_controller
|
||||
from app.core.ctx import CTX_USER_ID
|
||||
from app.core.dependency import DependAuth
|
||||
from app.models.admin import Api, Menu, Role, User
|
||||
from app.schemas.base import Fail, Success
|
||||
from app.schemas.base import Fail, Success, BasicResponse
|
||||
from app.schemas.login import *
|
||||
from app.schemas.users import UpdatePassword
|
||||
from app.settings import settings
|
||||
@ -16,7 +16,7 @@ from app.utils.password import get_password_hash, verify_password
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/access_token", summary="获取token")
|
||||
@router.post("/access_token", summary="获取token", response_model=BasicResponse[JWTOut])
|
||||
async def login_access_token(credentials: CredentialsSchema):
|
||||
user: User = await user_controller.authenticate(credentials)
|
||||
await user_controller.update_last_login(user.id)
|
||||
@ -37,7 +37,7 @@ async def login_access_token(credentials: CredentialsSchema):
|
||||
return Success(data=data.model_dump())
|
||||
|
||||
|
||||
@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth])
|
||||
@router.get("/userinfo", summary="查看用户信息", dependencies=[DependAuth], response_model=BasicResponse[dict])
|
||||
async def get_userinfo():
|
||||
user_id = CTX_USER_ID.get()
|
||||
user_obj = await user_controller.get(id=user_id)
|
||||
@ -46,7 +46,7 @@ async def get_userinfo():
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth])
|
||||
@router.get("/usermenu", summary="查看用户菜单", dependencies=[DependAuth], response_model=BasicResponse[list])
|
||||
async def get_user_menu():
|
||||
user_id = CTX_USER_ID.get()
|
||||
user_obj = await User.filter(id=user_id).first()
|
||||
@ -74,7 +74,7 @@ async def get_user_menu():
|
||||
return Success(data=res)
|
||||
|
||||
|
||||
@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth])
|
||||
@router.get("/userapi", summary="查看用户API", dependencies=[DependAuth], response_model=BasicResponse[list])
|
||||
async def get_user_api():
|
||||
user_id = CTX_USER_ID.get()
|
||||
user_obj = await User.filter(id=user_id).first()
|
||||
@ -91,7 +91,7 @@ async def get_user_api():
|
||||
return Success(data=apis)
|
||||
|
||||
|
||||
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth])
|
||||
@router.post("/update_password", summary="修改密码", dependencies=[DependAuth], response_model=BasicResponse[dict])
|
||||
async def update_user_password(req_in: UpdatePassword):
|
||||
user_id = CTX_USER_ID.get()
|
||||
user = await user_controller.get(user_id)
|
||||
|
||||
@ -2,12 +2,14 @@ from fastapi import APIRouter, Query
|
||||
|
||||
from app.controllers.dept import dept_controller
|
||||
from app.schemas import Success
|
||||
from app.schemas.base import BasicResponse, MessageOut
|
||||
from app.schemas.depts import BaseDept
|
||||
from app.schemas.depts import *
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看部门列表")
|
||||
@router.get("/list", summary="查看部门列表", response_model=BasicResponse[list[BaseDept]])
|
||||
async def list_dept(
|
||||
name: str = Query(None, description="部门名称"),
|
||||
):
|
||||
@ -15,7 +17,7 @@ async def list_dept(
|
||||
return Success(data=dept_tree)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看部门")
|
||||
@router.get("/get", summary="查看部门", response_model=BasicResponse[BaseDept])
|
||||
async def get_dept(
|
||||
id: int = Query(..., description="部门ID"),
|
||||
):
|
||||
@ -24,7 +26,7 @@ async def get_dept(
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建部门")
|
||||
@router.post("/create", summary="创建部门", response_model=BasicResponse[MessageOut])
|
||||
async def create_dept(
|
||||
dept_in: DeptCreate,
|
||||
):
|
||||
@ -32,7 +34,7 @@ async def create_dept(
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新部门")
|
||||
@router.post("/update", summary="更新部门", response_model=BasicResponse[MessageOut])
|
||||
async def update_dept(
|
||||
dept_in: DeptUpdate,
|
||||
):
|
||||
@ -40,7 +42,7 @@ async def update_dept(
|
||||
return Success(msg="Update Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除部门")
|
||||
@router.delete("/delete", summary="删除部门", response_model=BasicResponse[MessageOut])
|
||||
async def delete_dept(
|
||||
dept_id: int = Query(..., description="部门ID"),
|
||||
):
|
||||
|
||||
@ -3,12 +3,14 @@ from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.esg import esg_controller
|
||||
from app.schemas import Success, SuccessExtra
|
||||
from app.schemas.base import BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.esg import ESGResponse
|
||||
from app.schemas.esg import ESGCreate, ESGUpdate, ESGResponse
|
||||
|
||||
router = APIRouter(tags=["ESG管理"])
|
||||
|
||||
|
||||
@router.get("/list", summary="查看ESG列表")
|
||||
@router.get("/list", summary="查看ESG列表", response_model=PageResponse[ESGResponse])
|
||||
async def list_esg(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -28,7 +30,7 @@ async def list_esg(
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看ESG详情")
|
||||
@router.get("/get", summary="查看ESG详情", response_model=BasicResponse[ESGResponse])
|
||||
async def get_esg(
|
||||
id: int = Query(..., description="ESG ID"),
|
||||
):
|
||||
@ -37,7 +39,7 @@ async def get_esg(
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建ESG")
|
||||
@router.post("/create", summary="创建ESG", response_model=BasicResponse[MessageOut])
|
||||
async def create_esg(
|
||||
esg_in: ESGCreate,
|
||||
):
|
||||
@ -49,7 +51,7 @@ async def create_esg(
|
||||
return Success(msg="创建成功")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新ESG")
|
||||
@router.post("/update", summary="更新ESG", response_model=BasicResponse[MessageOut])
|
||||
async def update_esg(
|
||||
esg_in: ESGUpdate,
|
||||
):
|
||||
@ -63,7 +65,7 @@ async def update_esg(
|
||||
return Success(msg="更新成功")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除ESG")
|
||||
@router.delete("/delete", summary="删除ESG", response_model=BasicResponse[MessageOut])
|
||||
async def delete_esg(
|
||||
esg_id: int = Query(..., description="ESG ID"),
|
||||
):
|
||||
|
||||
@ -3,12 +3,14 @@ from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.index import index_controller
|
||||
from app.schemas import Success, SuccessExtra
|
||||
from app.schemas.base import BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.index import IndexResponse
|
||||
from app.schemas.index import IndexCreate, IndexUpdate, IndexResponse
|
||||
|
||||
router = APIRouter(tags=["指数管理"])
|
||||
|
||||
|
||||
@router.get("/list", summary="查看指数列表")
|
||||
@router.get("/list", summary="查看指数列表", response_model=PageResponse[IndexResponse])
|
||||
async def list_index(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -25,7 +27,7 @@ async def list_index(
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看指数详情")
|
||||
@router.get("/get", summary="查看指数详情", response_model=BasicResponse[IndexResponse])
|
||||
async def get_index(
|
||||
id: int = Query(..., description="指数 ID"),
|
||||
):
|
||||
@ -34,7 +36,7 @@ async def get_index(
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建指数")
|
||||
@router.post("/create", summary="创建指数", response_model=BasicResponse[MessageOut])
|
||||
async def create_index(
|
||||
index_in: IndexCreate,
|
||||
):
|
||||
@ -46,7 +48,7 @@ async def create_index(
|
||||
return Success(msg="创建成功")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新指数")
|
||||
@router.post("/update", summary="更新指数", response_model=BasicResponse[MessageOut])
|
||||
async def update_index(
|
||||
index_in: IndexUpdate,
|
||||
):
|
||||
@ -60,7 +62,7 @@ async def update_index(
|
||||
return Success(msg="更新成功")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除指数")
|
||||
@router.delete("/delete", summary="删除指数", response_model=BasicResponse[MessageOut])
|
||||
async def delete_index(
|
||||
index_id: int = Query(..., description="指数 ID"),
|
||||
):
|
||||
|
||||
@ -3,12 +3,13 @@ from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.industry import industry_controller
|
||||
from app.schemas import Success, SuccessExtra
|
||||
from app.schemas.base import BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.industry import IndustryCreate, IndustryUpdate, IndustryResponse
|
||||
|
||||
router = APIRouter(tags=["行业管理"])
|
||||
|
||||
|
||||
@router.get("/list", summary="查看行业列表")
|
||||
@router.get("/list", summary="查看行业列表", response_model=PageResponse[IndustryResponse])
|
||||
async def list_industry(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -25,7 +26,7 @@ async def list_industry(
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看行业详情")
|
||||
@router.get("/get", summary="查看行业详情", response_model=BasicResponse[IndustryResponse])
|
||||
async def get_industry(
|
||||
id: int = Query(..., description="行业 ID"),
|
||||
):
|
||||
@ -34,7 +35,7 @@ async def get_industry(
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建行业")
|
||||
@router.post("/create", summary="创建行业", response_model=BasicResponse[MessageOut])
|
||||
async def create_industry(
|
||||
industry_in: IndustryCreate,
|
||||
):
|
||||
@ -46,7 +47,7 @@ async def create_industry(
|
||||
return Success(msg="创建成功")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新行业")
|
||||
@router.post("/update", summary="更新行业", response_model=BasicResponse[MessageOut])
|
||||
async def update_industry(
|
||||
industry_in: IndustryUpdate,
|
||||
):
|
||||
@ -60,7 +61,7 @@ async def update_industry(
|
||||
return Success(msg="更新成功")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除行业")
|
||||
@router.delete("/delete", summary="删除行业", response_model=BasicResponse[MessageOut])
|
||||
async def delete_industry(
|
||||
industry_id: int = Query(..., description="行业 ID"),
|
||||
):
|
||||
|
||||
3
app/api/v1/invoice/__init__.py
Normal file
3
app/api/v1/invoice/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .invoice import invoice_router
|
||||
|
||||
__all__ = ["invoice_router"]
|
||||
154
app/api/v1/invoice/invoice.py
Normal file
154
app/api/v1/invoice/invoice.py
Normal 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 路由中统一暴露
|
||||
@ -3,7 +3,8 @@ import logging
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.controllers.menu import menu_controller
|
||||
from app.schemas.base import Fail, Success, SuccessExtra
|
||||
from app.schemas.base import Fail, Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.menus import BaseMenu
|
||||
from app.schemas.menus import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -11,7 +12,7 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看菜单列表")
|
||||
@router.get("/list", summary="查看菜单列表", response_model=PageResponse[BaseMenu])
|
||||
async def list_menu(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -28,7 +29,7 @@ async def list_menu(
|
||||
return SuccessExtra(data=res_menu, total=len(res_menu), page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看菜单")
|
||||
@router.get("/get", summary="查看菜单", response_model=BasicResponse[BaseMenu])
|
||||
async def get_menu(
|
||||
menu_id: int = Query(..., description="菜单id"),
|
||||
):
|
||||
@ -36,7 +37,7 @@ async def get_menu(
|
||||
return Success(data=result)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建菜单")
|
||||
@router.post("/create", summary="创建菜单", response_model=BasicResponse[MessageOut])
|
||||
async def create_menu(
|
||||
menu_in: MenuCreate,
|
||||
):
|
||||
@ -44,7 +45,7 @@ async def create_menu(
|
||||
return Success(msg="Created Success")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新菜单")
|
||||
@router.post("/update", summary="更新菜单", response_model=BasicResponse[MessageOut])
|
||||
async def update_menu(
|
||||
menu_in: MenuUpdate,
|
||||
):
|
||||
@ -52,7 +53,7 @@ async def update_menu(
|
||||
return Success(msg="Updated Success")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除菜单")
|
||||
@router.delete("/delete", summary="删除菜单", response_model=BasicResponse[MessageOut])
|
||||
async def delete_menu(
|
||||
id: int = Query(..., description="菜单id"),
|
||||
):
|
||||
|
||||
@ -3,12 +3,14 @@ from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.policy import policy_controller
|
||||
from app.schemas import Success, SuccessExtra
|
||||
from app.schemas.base import BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.policy import PolicyResponse
|
||||
from app.schemas.policy import PolicyCreate, PolicyUpdate, PolicyResponse
|
||||
|
||||
router = APIRouter(tags=["政策管理"])
|
||||
|
||||
|
||||
@router.get("/list", summary="查看政策列表")
|
||||
@router.get("/list", summary="查看政策列表", response_model=PageResponse[PolicyResponse])
|
||||
async def list_policy(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -28,7 +30,7 @@ async def list_policy(
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看政策详情")
|
||||
@router.get("/get", summary="查看政策详情", response_model=BasicResponse[PolicyResponse])
|
||||
async def get_policy(
|
||||
id: int = Query(..., description="政策 ID"),
|
||||
):
|
||||
@ -37,7 +39,7 @@ async def get_policy(
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建政策")
|
||||
@router.post("/create", summary="创建政策", response_model=BasicResponse[MessageOut])
|
||||
async def create_policy(
|
||||
policy_in: PolicyCreate,
|
||||
):
|
||||
@ -49,7 +51,7 @@ async def create_policy(
|
||||
return Success(msg="创建成功")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新政策")
|
||||
@router.post("/update", summary="更新政策", response_model=BasicResponse[MessageOut])
|
||||
async def update_policy(
|
||||
policy_in: PolicyUpdate,
|
||||
):
|
||||
@ -63,7 +65,7 @@ async def update_policy(
|
||||
return Success(msg="更新成功")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除政策")
|
||||
@router.delete("/delete", summary="删除政策", response_model=BasicResponse[MessageOut])
|
||||
async def delete_policy(
|
||||
policy_id: int = Query(..., description="政策 ID"),
|
||||
):
|
||||
|
||||
@ -5,14 +5,15 @@ from fastapi.exceptions import HTTPException
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from app.controllers import role_controller
|
||||
from app.schemas.base import Success, SuccessExtra
|
||||
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.roles import BaseRole
|
||||
from app.schemas.roles import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看角色列表")
|
||||
@router.get("/list", summary="查看角色列表", response_model=PageResponse[BaseRole])
|
||||
async def list_role(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -26,7 +27,7 @@ async def list_role(
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看角色")
|
||||
@router.get("/get", summary="查看角色", response_model=BasicResponse[BaseRole])
|
||||
async def get_role(
|
||||
role_id: int = Query(..., description="角色ID"),
|
||||
):
|
||||
@ -34,7 +35,7 @@ async def get_role(
|
||||
return Success(data=await role_obj.to_dict())
|
||||
|
||||
|
||||
@router.post("/create", summary="创建角色")
|
||||
@router.post("/create", summary="创建角色", response_model=BasicResponse[MessageOut])
|
||||
async def create_role(role_in: RoleCreate):
|
||||
if await role_controller.is_exist(name=role_in.name):
|
||||
raise HTTPException(
|
||||
@ -45,13 +46,13 @@ async def create_role(role_in: RoleCreate):
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新角色")
|
||||
@router.post("/update", summary="更新角色", response_model=BasicResponse[MessageOut])
|
||||
async def update_role(role_in: RoleUpdate):
|
||||
await role_controller.update(id=role_in.id, obj_in=role_in)
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除角色")
|
||||
@router.delete("/delete", summary="删除角色", response_model=BasicResponse[MessageOut])
|
||||
async def delete_role(
|
||||
role_id: int = Query(..., description="角色ID"),
|
||||
):
|
||||
@ -59,14 +60,14 @@ async def delete_role(
|
||||
return Success(msg="Deleted Success")
|
||||
|
||||
|
||||
@router.get("/authorized", summary="查看角色权限")
|
||||
@router.get("/authorized", summary="查看角色权限", response_model=BasicResponse[BaseRole])
|
||||
async def get_role_authorized(id: int = Query(..., description="角色ID")):
|
||||
role_obj = await role_controller.get(id=id)
|
||||
data = await role_obj.to_dict(m2m=True)
|
||||
return Success(data=data)
|
||||
|
||||
|
||||
@router.post("/authorized", summary="更新角色权限")
|
||||
@router.post("/authorized", summary="更新角色权限", response_model=BasicResponse[MessageOut])
|
||||
async def update_role_authorized(role_in: RoleUpdateMenusApis):
|
||||
role_obj = await role_controller.get(id=role_in.id)
|
||||
await role_controller.update_roles(role=role_obj, menu_ids=role_in.menu_ids, api_infos=role_in.api_infos)
|
||||
|
||||
197
app/api/v1/sms/sms.py
Normal file
197
app/api/v1/sms/sms.py
Normal 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(...)
|
||||
3
app/api/v1/transactions/__init__.py
Normal file
3
app/api/v1/transactions/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .transactions import transactions_router
|
||||
|
||||
__all__ = ["transactions_router"]
|
||||
104
app/api/v1/transactions/transactions.py
Normal file
104
app/api/v1/transactions/transactions.py
Normal 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 "发送失败")
|
||||
@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter, UploadFile, File
|
||||
from app.controllers.upload import UploadController
|
||||
from app.schemas.upload import ImageUploadResponse
|
||||
from app.schemas.upload import ImageUploadResponse, FileUploadResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -11,4 +11,8 @@ async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
|
||||
:param file: 图片文件
|
||||
:return: 图片URL和文件名
|
||||
"""
|
||||
return await UploadController.upload_image(file)
|
||||
return await UploadController.upload_image(file)
|
||||
|
||||
@router.post("/file", response_model=FileUploadResponse, summary="上传文件")
|
||||
async def upload_file(file: UploadFile = File(...)) -> FileUploadResponse:
|
||||
return await UploadController.upload_file(file)
|
||||
@ -5,7 +5,8 @@ from tortoise.expressions import Q
|
||||
|
||||
from app.controllers.dept import dept_controller
|
||||
from app.controllers.user import user_controller
|
||||
from app.schemas.base import Fail, Success, SuccessExtra
|
||||
from app.schemas.base import Fail, Success, SuccessExtra, BasicResponse, PageResponse, MessageOut
|
||||
from app.schemas.users import BaseUser
|
||||
from app.schemas.users import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -13,7 +14,7 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/list", summary="查看用户列表")
|
||||
@router.get("/list", summary="查看用户列表", response_model=PageResponse[BaseUser])
|
||||
async def list_user(
|
||||
page: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
@ -37,7 +38,7 @@ async def list_user(
|
||||
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/get", summary="查看用户")
|
||||
@router.get("/get", summary="查看用户", response_model=BasicResponse[BaseUser])
|
||||
async def get_user(
|
||||
user_id: int = Query(..., description="用户ID"),
|
||||
):
|
||||
@ -46,7 +47,7 @@ async def get_user(
|
||||
return Success(data=user_dict)
|
||||
|
||||
|
||||
@router.post("/create", summary="创建用户")
|
||||
@router.post("/create", summary="创建用户", response_model=BasicResponse[MessageOut])
|
||||
async def create_user(
|
||||
user_in: UserCreate,
|
||||
):
|
||||
@ -58,7 +59,7 @@ async def create_user(
|
||||
return Success(msg="Created Successfully")
|
||||
|
||||
|
||||
@router.post("/update", summary="更新用户")
|
||||
@router.post("/update", summary="更新用户", response_model=BasicResponse[MessageOut])
|
||||
async def update_user(
|
||||
user_in: UserUpdate,
|
||||
):
|
||||
@ -67,7 +68,7 @@ async def update_user(
|
||||
return Success(msg="Updated Successfully")
|
||||
|
||||
|
||||
@router.delete("/delete", summary="删除用户")
|
||||
@router.delete("/delete", summary="删除用户", response_model=BasicResponse[MessageOut])
|
||||
async def delete_user(
|
||||
user_id: int = Query(..., description="用户ID"),
|
||||
):
|
||||
@ -75,7 +76,7 @@ async def delete_user(
|
||||
return Success(msg="Deleted Successfully")
|
||||
|
||||
|
||||
@router.post("/reset_password", summary="重置密码")
|
||||
@router.post("/reset_password", summary="重置密码", response_model=BasicResponse[MessageOut])
|
||||
async def reset_password(user_id: int = Body(..., description="用户ID", embed=True)):
|
||||
await user_controller.reset_password(user_id)
|
||||
return Success(msg="密码已重置为123456")
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from app.controllers.valuation import valuation_controller
|
||||
from app.schemas.valuation import (
|
||||
@ -9,15 +9,16 @@ from app.schemas.valuation import (
|
||||
ValuationAssessmentList,
|
||||
ValuationAssessmentQuery,
|
||||
ValuationApprovalRequest,
|
||||
ValuationAdminNotesUpdate
|
||||
ValuationAdminNotesUpdate,
|
||||
ValuationCalculationStepOut
|
||||
)
|
||||
from app.schemas.base import Success, SuccessExtra
|
||||
from app.schemas.base import Success, SuccessExtra, BasicResponse, PageResponse
|
||||
from app.core.ctx import CTX_USER_ID
|
||||
|
||||
valuations_router = APIRouter(tags=["估值评估"])
|
||||
|
||||
|
||||
@valuations_router.post("/", summary="创建估值评估")
|
||||
@valuations_router.post("/", summary="创建估值评估", response_model=BasicResponse[ValuationAssessmentOut])
|
||||
async def create_valuation(data: ValuationAssessmentCreate):
|
||||
"""创建新的估值评估记录"""
|
||||
try:
|
||||
@ -25,37 +26,51 @@ async def create_valuation(data: ValuationAssessmentCreate):
|
||||
user_id = CTX_USER_ID.get()
|
||||
print(user_id)
|
||||
result = await valuation_controller.create(data, user_id)
|
||||
return Success(data=result, msg="创建成功")
|
||||
import json
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="创建成功")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"创建失败: {str(e)}")
|
||||
|
||||
|
||||
@valuations_router.get("/statistics/overview", summary="获取统计信息")
|
||||
@valuations_router.get("/statistics/overview", summary="获取统计信息", response_model=BasicResponse[dict])
|
||||
async def get_statistics():
|
||||
"""获取估值评估统计信息"""
|
||||
result = await valuation_controller.get_statistics()
|
||||
return Success(data=result, msg="获取统计信息成功")
|
||||
|
||||
|
||||
@valuations_router.get("/{valuation_id}", summary="获取估值评估详情")
|
||||
@valuations_router.get("/{valuation_id}", summary="获取估值评估详情", response_model=BasicResponse[ValuationAssessmentOut])
|
||||
async def get_valuation(valuation_id: int):
|
||||
"""根据ID获取估值评估详情"""
|
||||
result = await valuation_controller.get_by_id(valuation_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="估值评估记录不存在")
|
||||
return Success(data=result, msg="获取成功")
|
||||
import json
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="获取成功")
|
||||
|
||||
|
||||
@valuations_router.put("/{valuation_id}", summary="更新估值评估")
|
||||
@valuations_router.get("/{valuation_id}/steps", summary="获取估值计算步骤", response_model=BasicResponse[List[ValuationCalculationStepOut]])
|
||||
async def get_valuation_steps(valuation_id: int):
|
||||
"""根据估值ID获取所有计算步骤"""
|
||||
steps = await valuation_controller.get_calculation_steps(valuation_id)
|
||||
if not steps:
|
||||
raise HTTPException(status_code=404, detail="未找到该估值的计算步骤")
|
||||
import json
|
||||
steps_out = [json.loads(step.model_dump_json()) for step in steps]
|
||||
return Success(data=steps_out, msg="获取计算步骤成功")
|
||||
|
||||
|
||||
@valuations_router.put("/{valuation_id}", summary="更新估值评估", response_model=BasicResponse[ValuationAssessmentOut])
|
||||
async def update_valuation(valuation_id: int, data: ValuationAssessmentUpdate):
|
||||
"""更新估值评估记录"""
|
||||
result = await valuation_controller.update(valuation_id, data)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="估值评估记录不存在")
|
||||
return Success(data=result, msg="更新成功")
|
||||
import json
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="更新成功")
|
||||
|
||||
|
||||
@valuations_router.delete("/{valuation_id}", summary="删除估值评估")
|
||||
@valuations_router.delete("/{valuation_id}", summary="删除估值评估", response_model=BasicResponse[dict])
|
||||
async def delete_valuation(valuation_id: int):
|
||||
"""软删除估值评估记录"""
|
||||
result = await valuation_controller.delete(valuation_id)
|
||||
@ -64,7 +79,7 @@ async def delete_valuation(valuation_id: int):
|
||||
return Success(data={"deleted": True}, msg="删除成功")
|
||||
|
||||
|
||||
@valuations_router.get("/", summary="获取估值评估列表")
|
||||
@valuations_router.get("/", summary="获取估值评估列表", response_model=PageResponse[ValuationAssessmentOut])
|
||||
async def get_valuations(
|
||||
asset_name: Optional[str] = Query(None, description="资产名称"),
|
||||
institution: Optional[str] = Query(None, description="所属机构"),
|
||||
@ -87,8 +102,10 @@ async def get_valuations(
|
||||
size=size
|
||||
)
|
||||
result = await valuation_controller.get_list(query)
|
||||
import json
|
||||
items = [json.loads(item.model_dump_json()) for item in result.items]
|
||||
return SuccessExtra(
|
||||
data=result.items,
|
||||
data=items,
|
||||
total=result.total,
|
||||
page=result.page,
|
||||
page_size=result.size,
|
||||
@ -97,7 +114,7 @@ async def get_valuations(
|
||||
)
|
||||
|
||||
|
||||
@valuations_router.get("/search/keyword", summary="搜索估值评估")
|
||||
@valuations_router.get("/search/keyword", summary="搜索估值评估", response_model=PageResponse[ValuationAssessmentOut])
|
||||
async def search_valuations(
|
||||
keyword: str = Query(..., description="搜索关键词"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
@ -105,8 +122,10 @@ async def search_valuations(
|
||||
):
|
||||
"""根据关键词搜索估值评估记录"""
|
||||
result = await valuation_controller.search(keyword, page, size)
|
||||
import json
|
||||
items = [json.loads(item.model_dump_json()) for item in result.items]
|
||||
return SuccessExtra(
|
||||
data=result.items,
|
||||
data=items,
|
||||
total=result.total,
|
||||
page=result.page,
|
||||
page_size=result.size,
|
||||
@ -116,7 +135,7 @@ async def search_valuations(
|
||||
|
||||
|
||||
# 批量操作接口
|
||||
@valuations_router.post("/batch/delete", summary="批量删除估值评估")
|
||||
@valuations_router.post("/batch/delete", summary="批量删除估值评估", response_model=BasicResponse[dict])
|
||||
async def batch_delete_valuations(valuation_ids: list[int]):
|
||||
"""批量软删除估值评估记录"""
|
||||
success_count = 0
|
||||
@ -140,7 +159,7 @@ async def batch_delete_valuations(valuation_ids: list[int]):
|
||||
|
||||
|
||||
# 导出接口
|
||||
@valuations_router.get("/export/excel", summary="导出估值评估数据")
|
||||
@valuations_router.get("/export/excel", summary="导出估值评估数据", response_model=BasicResponse[dict])
|
||||
async def export_valuations(
|
||||
asset_name: Optional[str] = Query(None, description="资产名称"),
|
||||
institution: Optional[str] = Query(None, description="所属机构"),
|
||||
@ -154,28 +173,31 @@ async def export_valuations(
|
||||
|
||||
|
||||
# 审核管理接口
|
||||
@valuations_router.post("/{valuation_id}/approve", summary="审核通过估值评估")
|
||||
@valuations_router.post("/{valuation_id}/approve", summary="审核通过估值评估", response_model=BasicResponse[ValuationAssessmentOut])
|
||||
async def approve_valuation(valuation_id: int, data: ValuationApprovalRequest):
|
||||
"""审核通过估值评估"""
|
||||
result = await valuation_controller.approve_valuation(valuation_id, data.admin_notes)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="估值评估记录不存在")
|
||||
return Success(data=result, msg="审核通过成功")
|
||||
import json
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="审核通过成功")
|
||||
|
||||
|
||||
@valuations_router.post("/{valuation_id}/reject", summary="审核拒绝估值评估")
|
||||
@valuations_router.post("/{valuation_id}/reject", summary="审核拒绝估值评估", response_model=BasicResponse[ValuationAssessmentOut])
|
||||
async def reject_valuation(valuation_id: int, data: ValuationApprovalRequest):
|
||||
"""审核拒绝估值评估"""
|
||||
result = await valuation_controller.reject_valuation(valuation_id, data.admin_notes)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="估值评估记录不存在")
|
||||
return Success(data=result, msg="审核拒绝成功")
|
||||
import json
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="审核拒绝成功")
|
||||
|
||||
|
||||
@valuations_router.put("/{valuation_id}/admin-notes", summary="更新管理员备注")
|
||||
@valuations_router.put("/{valuation_id}/admin-notes", summary="更新管理员备注", response_model=BasicResponse[ValuationAssessmentOut])
|
||||
async def update_admin_notes(valuation_id: int, data: ValuationAdminNotesUpdate):
|
||||
"""更新管理员备注"""
|
||||
result = await valuation_controller.update_admin_notes(valuation_id, data.admin_notes)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="估值评估记录不存在")
|
||||
return Success(data=result, msg="管理员备注更新成功")
|
||||
import json
|
||||
return Success(data=json.loads(result.model_dump_json()), msg="管理员备注更新成功")
|
||||
@ -1,4 +1,5 @@
|
||||
from app.models.user import AppUser
|
||||
from app.models.user import AppUserQuotaLog
|
||||
from app.schemas.app_user import AppUserRegisterSchema, AppUserLoginSchema, AppUserUpdateSchema
|
||||
from app.utils.password import get_password_hash, verify_password
|
||||
from app.core.crud import CRUDBase
|
||||
@ -90,6 +91,29 @@ class AppUserController(CRUDBase[AppUser, AppUserRegisterSchema, AppUserUpdateSc
|
||||
|
||||
await user.save()
|
||||
return user
|
||||
|
||||
async def update_quota(self, operator_id: int, operator_name: str, user_id: int, target_count: Optional[int] = None, delta: Optional[int] = None, op_type: str = "调整", remark: Optional[str] = None) -> Optional[AppUser]:
|
||||
user = await self.model.filter(id=user_id).first()
|
||||
if not user:
|
||||
return None
|
||||
before = int(getattr(user, "remaining_quota", 0) or 0)
|
||||
after = before
|
||||
if target_count is not None:
|
||||
after = max(0, int(target_count))
|
||||
elif delta is not None:
|
||||
after = max(0, before + int(delta))
|
||||
user.remaining_quota = after
|
||||
await user.save()
|
||||
await AppUserQuotaLog.create(
|
||||
app_user_id=user_id,
|
||||
operator_id=operator_id,
|
||||
operator_name=operator_name,
|
||||
before_count=before,
|
||||
after_count=after,
|
||||
op_type=op_type,
|
||||
remark=remark,
|
||||
)
|
||||
return user
|
||||
|
||||
async def change_password(self, user_id: int, old_password: str, new_password: str) -> bool:
|
||||
"""
|
||||
|
||||
286
app/controllers/invoice.py
Normal file
286
app/controllers/invoice.py
Normal 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: phone、company_name、tax_number、status、ticket_type、invoice_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()
|
||||
@ -2,7 +2,7 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from fastapi import UploadFile
|
||||
from app.schemas.upload import ImageUploadResponse
|
||||
from app.schemas.upload import ImageUploadResponse, FileUploadResponse
|
||||
from app.settings.config import settings
|
||||
|
||||
class UploadController:
|
||||
@ -49,4 +49,42 @@ class UploadController:
|
||||
return ImageUploadResponse(
|
||||
url=f"{settings.BASE_URL}/static/images/{filename}",
|
||||
filename=filename
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def upload_file(file: UploadFile) -> FileUploadResponse:
|
||||
allowed = {
|
||||
"application/pdf",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel",
|
||||
"application/zip",
|
||||
"application/x-zip-compressed",
|
||||
}
|
||||
if file.content_type not in allowed:
|
||||
raise ValueError("不支持的文件类型")
|
||||
|
||||
base_dir = Path(__file__).resolve().parent.parent
|
||||
upload_dir = base_dir / "static" / "files"
|
||||
if not upload_dir.exists():
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
filename = file.filename
|
||||
file_path = upload_dir / filename
|
||||
counter = 1
|
||||
while file_path.exists():
|
||||
name, ext = os.path.splitext(filename)
|
||||
filename = f"{name}_{counter}{ext}"
|
||||
file_path = upload_dir / filename
|
||||
counter += 1
|
||||
|
||||
content = await file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
return FileUploadResponse(
|
||||
url=f"{settings.BASE_URL}/static/files/{filename}",
|
||||
filename=filename,
|
||||
content_type=file.content_type,
|
||||
)
|
||||
@ -138,6 +138,8 @@ class UserValuationController:
|
||||
historical_evidence=valuation.historical_evidence,
|
||||
patent_certificates=valuation.patent_certificates,
|
||||
pattern_images=valuation.pattern_images,
|
||||
report_url=valuation.report_url,
|
||||
certificate_url=valuation.certificate_url,
|
||||
application_maturity=valuation.application_maturity,
|
||||
implementation_stage=valuation.implementation_stage,
|
||||
application_coverage=valuation.application_coverage,
|
||||
@ -159,6 +161,8 @@ class UserValuationController:
|
||||
price_fluctuation=valuation.price_fluctuation,
|
||||
price_range=valuation.price_range,
|
||||
market_price=valuation.market_price,
|
||||
credit_code_or_id=valuation.credit_code_or_id,
|
||||
biz_intro=valuation.biz_intro,
|
||||
infringement_record=valuation.infringement_record,
|
||||
patent_count=valuation.patent_count,
|
||||
esg_value=valuation.esg_value,
|
||||
|
||||
@ -3,13 +3,15 @@ from tortoise.expressions import Q
|
||||
from tortoise.queryset import QuerySet
|
||||
from tortoise.functions import Count
|
||||
|
||||
from app.models.valuation import ValuationAssessment
|
||||
from app.models.valuation import ValuationAssessment, ValuationCalculationStep
|
||||
from app.schemas.valuation import (
|
||||
ValuationAssessmentCreate,
|
||||
ValuationAssessmentUpdate,
|
||||
ValuationAssessmentQuery,
|
||||
ValuationAssessmentOut,
|
||||
ValuationAssessmentList
|
||||
ValuationAssessmentList,
|
||||
ValuationCalculationStepCreate,
|
||||
ValuationCalculationStepOut
|
||||
)
|
||||
|
||||
|
||||
@ -17,6 +19,56 @@ class ValuationController:
|
||||
"""估值评估控制器"""
|
||||
|
||||
model = ValuationAssessment
|
||||
step_model = ValuationCalculationStep
|
||||
|
||||
async def create_calculation_step(self, data: ValuationCalculationStepCreate) -> ValuationCalculationStepOut:
|
||||
"""
|
||||
创建估值计算步骤
|
||||
|
||||
Args:
|
||||
data (ValuationCalculationStepCreate): 估值计算步骤数据
|
||||
|
||||
Returns:
|
||||
ValuationCalculationStepOut: 创建的估值计算步骤
|
||||
"""
|
||||
step = await self.step_model.create(**data.model_dump())
|
||||
logger.info(
|
||||
"calcstep.create valuation_id={} order={} name={}",
|
||||
data.valuation_id,
|
||||
data.step_order,
|
||||
data.step_name,
|
||||
)
|
||||
return ValuationCalculationStepOut.model_validate(step)
|
||||
|
||||
async def update_calculation_step(self, step_id: int, update: dict) -> ValuationCalculationStepOut:
|
||||
step = await self.step_model.filter(id=step_id).first()
|
||||
if not step:
|
||||
raise ValueError(f"calculation_step not found: {step_id}")
|
||||
await step.update_from_dict(update).save()
|
||||
logger.info(
|
||||
"calcstep.update id={} fields={}",
|
||||
step_id,
|
||||
list(update.keys()),
|
||||
)
|
||||
return ValuationCalculationStepOut.model_validate(step)
|
||||
|
||||
async def get_calculation_steps(self, valuation_id: int) -> List[ValuationCalculationStepOut]:
|
||||
"""
|
||||
根据估值ID获取所有相关的计算步骤。
|
||||
|
||||
此方法从数据库中检索与特定估值ID关联的所有计算步骤记录,
|
||||
并按创建时间升序排序,确保步骤的顺序正确。
|
||||
|
||||
Args:
|
||||
valuation_id (int): 估值的唯一标识符。
|
||||
|
||||
Returns:
|
||||
List[ValuationCalculationStepOut]: 一个包含所有相关计算步骤的列表,
|
||||
如果找不到任何步骤,则返回空列表。
|
||||
"""
|
||||
steps = await self.step_model.filter(valuation_id=valuation_id).order_by('created_at')
|
||||
logger.info("calcstep.list valuation_id={} count={}", valuation_id, len(steps))
|
||||
return [ValuationCalculationStepOut.model_validate(step) for step in steps]
|
||||
|
||||
async def create(self, data: ValuationAssessmentCreate, user_id: int) -> ValuationAssessmentOut:
|
||||
"""创建估值评估"""
|
||||
@ -192,4 +244,5 @@ class ValuationController:
|
||||
|
||||
|
||||
# 创建控制器实例
|
||||
valuation_controller = ValuationController()
|
||||
valuation_controller = ValuationController()
|
||||
from app.log import logger
|
||||
@ -5,4 +5,5 @@ from .index import *
|
||||
from .industry import *
|
||||
from .policy import *
|
||||
from .user import *
|
||||
from .valuation import *
|
||||
from .valuation import *
|
||||
from .invoice import *
|
||||
64
app/models/invoice.py
Normal file
64
app/models/invoice.py
Normal 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 = "邮件发送日志"
|
||||
@ -19,7 +19,22 @@ class AppUser(BaseModel, TimestampMixin):
|
||||
company_email = fields.CharField(max_length=100, null=True, description="公司邮箱")
|
||||
is_active = fields.BooleanField(default=True, description="是否激活", index=True)
|
||||
last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True)
|
||||
remaining_quota = fields.IntField(default=0, description="剩余估值次数", index=True)
|
||||
|
||||
class Meta:
|
||||
table = "app_user"
|
||||
table_description = "用户表"
|
||||
table_description = "用户表"
|
||||
|
||||
|
||||
class AppUserQuotaLog(BaseModel, TimestampMixin):
|
||||
app_user_id = fields.IntField(description="App用户ID", index=True)
|
||||
operator_id = fields.IntField(description="操作人ID", index=True)
|
||||
operator_name = fields.CharField(max_length=64, description="操作人")
|
||||
before_count = fields.IntField(description="变更前次数")
|
||||
after_count = fields.IntField(description="变更后次数")
|
||||
op_type = fields.CharField(max_length=32, description="操作类型")
|
||||
remark = fields.CharField(max_length=256, null=True, description="备注")
|
||||
|
||||
class Meta:
|
||||
table = "app_user_quota_log"
|
||||
table_description = "App用户估值次数操作日志"
|
||||
@ -23,18 +23,26 @@ class ValuationAssessment(Model):
|
||||
inheritor_ages = fields.JSONField(null=True, description="传承人年龄列表")
|
||||
inheritor_age_count = fields.JSONField(null=True, description="非遗传承人年龄水平及数量")
|
||||
inheritor_certificates = fields.JSONField(null=True, description="非遗传承人等级证书")
|
||||
heritage_level = fields.CharField(max_length=50, null=True, description="非遗等级")
|
||||
heritage_asset_level = fields.CharField(max_length=50, null=True, description="非遗资产等级")
|
||||
patent_application_no = fields.CharField(max_length=100, null=True, description="非遗资产所用专利的申请号")
|
||||
patent_remaining_years = fields.CharField(max_length=50, null=True, description="专利剩余年限")
|
||||
historical_evidence = fields.JSONField(null=True, description="非遗资产历史证明证据及数量")
|
||||
patent_certificates = fields.JSONField(null=True, description="非遗资产所用专利的证书")
|
||||
pattern_images = fields.JSONField(null=True, description="非遗纹样图片")
|
||||
report_url = fields.CharField(max_length=512, null=True, description="管理员上传的评估报告URL")
|
||||
certificate_url = fields.CharField(max_length=512, null=True, description="管理员上传的证书URL")
|
||||
|
||||
# 非遗应用与推广
|
||||
implementation_stage = fields.CharField(max_length=100, null=True, description="非遗资产应用成熟度")
|
||||
application_maturity = fields.CharField(max_length=100, null=True, description="非遗资产应用成熟度")
|
||||
application_coverage = fields.CharField(max_length=100, null=True, description="非遗资产应用覆盖范围")
|
||||
coverage_area = fields.CharField(max_length=100, null=True, description="应用覆盖范围")
|
||||
cooperation_depth = fields.CharField(max_length=100, null=True, description="非遗资产跨界合作深度")
|
||||
collaboration_type = fields.CharField(max_length=100, null=True, description="跨界合作类型")
|
||||
offline_activities = fields.CharField(max_length=50, null=True, description="近12个月线下相关宣讲活动次数")
|
||||
offline_teaching_count = fields.IntField(null=True, description="近12个月线下相关演讲活动次数")
|
||||
online_accounts = fields.JSONField(null=True, description="线上相关宣传账号信息")
|
||||
platform_accounts = fields.JSONField(null=True, description="线上相关宣传账号信息")
|
||||
|
||||
# 非遗资产衍生商品信息
|
||||
@ -44,10 +52,13 @@ class ValuationAssessment(Model):
|
||||
scarcity_level = fields.CharField(max_length=50, null=True, description="稀缺等级")
|
||||
last_market_activity = fields.CharField(max_length=100, null=True, description="该商品最近一次市场活动时间")
|
||||
market_activity_time = fields.CharField(max_length=100, null=True, description="市场活动的时间")
|
||||
monthly_transaction = fields.CharField(max_length=50, null=True, description="月交易额")
|
||||
monthly_transaction_amount = fields.CharField(max_length=50, null=True, description="月交易额")
|
||||
price_fluctuation = fields.JSONField(null=True, description="该商品近30天价格波动区间")
|
||||
price_range = fields.JSONField(null=True, description="资产商品的价格波动率")
|
||||
market_price = fields.FloatField(null=True, description="市场价格(单位:万元)")
|
||||
credit_code_or_id = fields.CharField(max_length=64, null=True, description="统一社会信用代码或身份证号")
|
||||
biz_intro = fields.TextField(null=True, description="业务/传承介绍")
|
||||
|
||||
# 内置API计算字段
|
||||
infringement_record = fields.CharField(max_length=100, null=True, description="侵权记录")
|
||||
@ -82,4 +93,26 @@ class ValuationAssessment(Model):
|
||||
table_description = "估值评估表"
|
||||
|
||||
def __str__(self):
|
||||
return f"估值评估-{self.asset_name}"
|
||||
return f"估值评估-{self.asset_name}"
|
||||
|
||||
|
||||
class ValuationCalculationStep(Model):
|
||||
"""估值计算步骤模型"""
|
||||
id = fields.IntField(pk=True, description="主键ID")
|
||||
valuation = fields.ForeignKeyField("models.ValuationAssessment", related_name="calculation_steps", description="关联的估值评估")
|
||||
step_order = fields.DecimalField(max_digits=8, decimal_places=3, description="步骤顺序")
|
||||
step_name = fields.CharField(max_length=255, description="步骤名称")
|
||||
step_description = fields.TextField(null=True, description="步骤描述")
|
||||
input_params = fields.JSONField(null=True, description="输入参数")
|
||||
output_result = fields.JSONField(null=True, description="输出结果")
|
||||
status = fields.CharField(max_length=20, default="SUCCESS", description="步骤状态: SUCCESS, FAILED")
|
||||
error_message = fields.TextField(null=True, description="错误信息")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
|
||||
class Meta:
|
||||
table = "valuation_calculation_steps"
|
||||
table_description = "估值计算步骤表"
|
||||
ordering = ["step_order"]
|
||||
|
||||
def __str__(self):
|
||||
return f"估值ID {self.valuation_id} - 步骤 {self.step_order}: {self.step_name}"
|
||||
@ -50,6 +50,7 @@ class AppUserInfoOut(BaseModel):
|
||||
last_login: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
remaining_quota: int
|
||||
|
||||
|
||||
class AppUserUpdateSchema(BaseModel):
|
||||
@ -66,4 +67,49 @@ class AppUserUpdateSchema(BaseModel):
|
||||
class AppUserChangePasswordSchema(BaseModel):
|
||||
"""AppUser修改密码Schema"""
|
||||
old_password: str = Field(..., description="原密码")
|
||||
new_password: str = Field(..., description="新密码")
|
||||
new_password: str = Field(..., description="新密码")
|
||||
|
||||
|
||||
class AppUserDashboardOut(BaseModel):
|
||||
"""AppUser首页摘要输出"""
|
||||
remaining_quota: int
|
||||
latest_valuation: Optional[dict] = None
|
||||
pending_invoices: int
|
||||
|
||||
|
||||
class AppUserQuotaOut(BaseModel):
|
||||
"""AppUser剩余估值次数输出"""
|
||||
remaining_count: int
|
||||
user_type: Optional[str] = None
|
||||
|
||||
|
||||
class AppUserQuotaUpdateSchema(BaseModel):
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
target_count: Optional[int] = Field(None, description="目标次数")
|
||||
delta: Optional[int] = Field(None, description="增减次数")
|
||||
op_type: str = Field(..., description="操作类型")
|
||||
remark: Optional[str] = Field(None, description="备注")
|
||||
|
||||
|
||||
class AppUserQuotaLogOut(BaseModel):
|
||||
id: int
|
||||
app_user_id: int
|
||||
operator_id: int
|
||||
operator_name: str
|
||||
before_count: int
|
||||
after_count: int
|
||||
op_type: str
|
||||
remark: Optional[str] = None
|
||||
|
||||
|
||||
class AppUserRegisterOut(BaseModel):
|
||||
"""App 用户注册结果"""
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
phone: str = Field(..., description="手机号")
|
||||
default_password: str = Field(..., description="默认密码(手机号后六位)")
|
||||
|
||||
|
||||
class TokenValidateOut(BaseModel):
|
||||
"""Token 校验结果"""
|
||||
user_id: int = Field(..., description="用户ID")
|
||||
phone: str = Field(..., description="手机号")
|
||||
@ -1,4 +1,6 @@
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Generic, TypeVar, List
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
@ -50,3 +52,26 @@ class SuccessExtra(JSONResponse):
|
||||
}
|
||||
content.update(kwargs)
|
||||
super().__init__(content=content, status_code=code)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class BasicResponse(GenericModel, Generic[T]):
|
||||
code: int = Field(200, description="状态码")
|
||||
msg: Optional[str] = Field("OK", description="信息")
|
||||
data: Optional[T] = Field(None, description="数据载荷")
|
||||
|
||||
|
||||
class PageResponse(GenericModel, Generic[T]):
|
||||
code: int = Field(200, description="状态码")
|
||||
msg: Optional[str] = Field(None, description="信息")
|
||||
data: List[T] = Field(default_factory=list, description="数据列表")
|
||||
total: int = Field(0, description="总数量")
|
||||
page: int = Field(1, description="当前页码")
|
||||
page_size: int = Field(20, description="每页数量")
|
||||
pages: Optional[int] = Field(None, description="总页数")
|
||||
|
||||
|
||||
class MessageOut(BaseModel):
|
||||
message: str = Field(..., description="提示信息")
|
||||
|
||||
102
app/schemas/invoice.py
Normal file
102
app/schemas/invoice.py
Normal 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
|
||||
25
app/schemas/transactions.py
Normal file
25
app/schemas/transactions.py
Normal 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
|
||||
@ -3,4 +3,9 @@ from pydantic import BaseModel
|
||||
class ImageUploadResponse(BaseModel):
|
||||
"""图片上传响应模型"""
|
||||
url: str
|
||||
filename: str
|
||||
filename: str
|
||||
|
||||
class FileUploadResponse(BaseModel):
|
||||
url: str
|
||||
filename: str
|
||||
content_type: str
|
||||
@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Any, Dict, Union
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class ValuationAssessmentBase(BaseModel):
|
||||
@ -28,6 +29,8 @@ class ValuationAssessmentBase(BaseModel):
|
||||
historical_evidence: Optional[Dict[str, int]] = Field(None, description="非遗资产历史证明证据及数量")
|
||||
patent_certificates: Optional[List[str]] = Field(None, description="非遗资产所用专利的证书")
|
||||
pattern_images: Optional[List[str]] = Field(None, description="非遗纹样图片")
|
||||
report_url: Optional[str] = Field(None, description="评估报告URL")
|
||||
certificate_url: Optional[str] = Field(None, description="证书URL")
|
||||
|
||||
# 非遗应用与推广
|
||||
application_maturity: Optional[str] = Field(None, description="非遗资产应用成熟度")
|
||||
@ -53,6 +56,8 @@ class ValuationAssessmentBase(BaseModel):
|
||||
price_fluctuation: Optional[List[Union[str, int, float]]] = Field(None, description="该商品近30天价格波动区间")
|
||||
price_range: Optional[Dict[str, Union[int, float]]] = Field(None, description="资产商品的价格波动率") # 未使用
|
||||
market_price: Optional[Union[int, float]] = Field(None, description="市场价格(单位:万元)") # 未使用
|
||||
credit_code_or_id: Optional[str] = Field(None, description="统一社会信用代码或身份证号")
|
||||
biz_intro: Optional[str] = Field(None, description="业务/传承介绍")
|
||||
|
||||
# 内置API计算字段
|
||||
infringement_record: Optional[str] = Field(None, description="侵权记录")
|
||||
@ -102,6 +107,8 @@ class ValuationAssessmentUpdate(BaseModel):
|
||||
historical_evidence: Optional[List[Any]] = Field(None, description="非遗资产历史证明证据及数量")
|
||||
patent_certificates: Optional[List[Any]] = Field(None, description="非遗资产所用专利的证书")
|
||||
pattern_images: Optional[List[Any]] = Field(None, description="非遗纹样图片")
|
||||
report_url: Optional[str] = Field(None, description="评估报告URL")
|
||||
certificate_url: Optional[str] = Field(None, description="证书URL")
|
||||
|
||||
# 非遗应用与推广
|
||||
application_maturity: Optional[str] = Field(None, description="非遗资产应用成熟度")
|
||||
@ -117,6 +124,8 @@ class ValuationAssessmentUpdate(BaseModel):
|
||||
last_market_activity: Optional[str] = Field(None, description="该商品最近一次市场活动时间")
|
||||
monthly_transaction: Optional[str] = Field(None, description="月交易额")
|
||||
price_fluctuation: Optional[List[Union[str, int, float]]] = Field(None, description="该商品近30天价格波动区间")
|
||||
credit_code_or_id: Optional[str] = Field(None, description="统一社会信用代码或身份证号")
|
||||
biz_intro: Optional[str] = Field(None, description="业务/传承介绍")
|
||||
|
||||
is_active: Optional[bool] = Field(None, description="是否激活")
|
||||
|
||||
@ -233,4 +242,46 @@ class ValuationApprovalRequest(BaseModel):
|
||||
|
||||
class ValuationAdminNotesUpdate(BaseModel):
|
||||
"""管理员备注更新模型"""
|
||||
admin_notes: str = Field(..., description="管理员备注")
|
||||
admin_notes: str = Field(..., description="管理员备注")
|
||||
|
||||
|
||||
class ValuationCalculationStepBase(BaseModel):
|
||||
"""估值计算步骤基础模型"""
|
||||
step_order: Decimal = Field(..., description="步骤顺序")
|
||||
step_name: str = Field(..., description="步骤名称")
|
||||
step_description: Optional[str] = Field(None, description="步骤描述")
|
||||
input_params: Optional[Dict[str, Any]] = Field(None, description="输入参数")
|
||||
output_result: Optional[Dict[str, Any]] = Field(None, description="输出结果")
|
||||
status: str = Field(..., description="步骤状态")
|
||||
error_message: Optional[str] = Field(None, description="错误信息")
|
||||
|
||||
@field_validator('step_order', mode='before')
|
||||
@classmethod
|
||||
def _coerce_step_order(cls, v):
|
||||
if isinstance(v, Decimal):
|
||||
return v
|
||||
if isinstance(v, (int, float, str)):
|
||||
try:
|
||||
return Decimal(str(v))
|
||||
except Exception:
|
||||
raise ValueError('Invalid step_order')
|
||||
raise ValueError('Invalid step_order type')
|
||||
|
||||
|
||||
class ValuationCalculationStepCreate(ValuationCalculationStepBase):
|
||||
"""创建估值计算步骤模型"""
|
||||
valuation_id: int = Field(..., description="关联的估值评估ID")
|
||||
|
||||
|
||||
class ValuationCalculationStepOut(ValuationCalculationStepBase):
|
||||
"""估值计算步骤输出模型"""
|
||||
id: int = Field(..., description="主键ID")
|
||||
valuation_id: int = Field(..., description="关联的估值评估ID")
|
||||
created_at: datetime = Field(..., description="创建时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat(),
|
||||
Decimal: lambda v: float(v)
|
||||
}
|
||||
49
app/services/email_client.py
Normal file
49
app/services/email_client.py
Normal 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()
|
||||
52
app/services/rate_limiter.py
Normal file
52
app/services/rate_limiter.py
Normal 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()
|
||||
91
app/services/sms_client.py
Normal file
91
app/services/sms_client.py
Normal 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
144
app/services/sms_store.py
Normal 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()
|
||||
@ -31,22 +31,22 @@ class Settings(BaseSettings):
|
||||
TORTOISE_ORM: dict = {
|
||||
"connections": {
|
||||
# SQLite configuration
|
||||
"sqlite": {
|
||||
"engine": "tortoise.backends.sqlite",
|
||||
"credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file
|
||||
},
|
||||
# "sqlite": {
|
||||
# "engine": "tortoise.backends.sqlite",
|
||||
# "credentials": {"file_path": f"{BASE_DIR}/db.sqlite3"}, # Path to SQLite database file
|
||||
# },
|
||||
# MySQL/MariaDB configuration
|
||||
# Install with: tortoise-orm[asyncmy]
|
||||
# "mysql": {
|
||||
# "engine": "tortoise.backends.mysql",
|
||||
# "credentials": {
|
||||
# "host": "localhost", # Database host address
|
||||
# "port": 3306, # Database port
|
||||
# "user": "yourusername", # Database username
|
||||
# "password": "yourpassword", # Database password
|
||||
# "database": "yourdatabase", # Database name
|
||||
# },
|
||||
# },
|
||||
"mysql": {
|
||||
"engine": "tortoise.backends.mysql",
|
||||
"credentials": {
|
||||
"host": "sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com", # Database host address
|
||||
"port": 28555, # Database port
|
||||
"user": "root", # Database username
|
||||
"password": "api2api..", # Database password
|
||||
"database": "valuation_service", # Database name
|
||||
},
|
||||
},
|
||||
# PostgreSQL configuration
|
||||
# Install with: tortoise-orm[asyncpg]
|
||||
# "postgres": {
|
||||
@ -87,7 +87,7 @@ class Settings(BaseSettings):
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": ["app.models", "aerich.models"],
|
||||
"default_connection": "sqlite",
|
||||
"default_connection": "mysql",
|
||||
},
|
||||
},
|
||||
"use_tz": False, # Whether to use timezone-aware datetimes
|
||||
@ -95,5 +95,17 @@ class Settings(BaseSettings):
|
||||
}
|
||||
DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID: typing.Optional[str] = "LTAI5tA8gcgM8Qc7K9qCtmXg"
|
||||
ALIBABA_CLOUD_ACCESS_KEY_SECRET: typing.Optional[str] = "eWZIWi6xILGtmPSGyJEAhILX5fQx0h"
|
||||
ALIYUN_SMS_SIGN_NAME: typing.Optional[str] = "SMS_498140213"
|
||||
ALIYUN_SMS_ENDPOINT: str = "dysmsapi.aliyuncs.com"
|
||||
|
||||
SMTP_HOST: typing.Optional[str] = None
|
||||
SMTP_PORT: typing.Optional[int] = None
|
||||
SMTP_USERNAME: typing.Optional[str] = None
|
||||
SMTP_PASSWORD: typing.Optional[str] = None
|
||||
SMTP_TLS: bool = True
|
||||
SMTP_FROM: typing.Optional[str] = None
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
2
app/static/files/demo.pdf
Normal file
2
app/static/files/demo.pdf
Normal file
@ -0,0 +1,2 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
2
app/static/files/demo_1.pdf
Normal file
2
app/static/files/demo_1.pdf
Normal file
@ -0,0 +1,2 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
2
app/static/files/demo_1_2.pdf
Normal file
2
app/static/files/demo_1_2.pdf
Normal file
@ -0,0 +1,2 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
2
app/static/files/demo_1_2_3.pdf
Normal file
2
app/static/files/demo_1_2_3.pdf
Normal file
@ -0,0 +1,2 @@
|
||||
%PDF-1.4
|
||||
%粤マモ
|
||||
1
app/static/files/test.pdf
Normal file
1
app/static/files/test.pdf
Normal file
@ -0,0 +1 @@
|
||||
%PDF-1.4
|
||||
1
app/static/files/test_1.pdf
Normal file
1
app/static/files/test_1.pdf
Normal file
@ -0,0 +1 @@
|
||||
%PDF-1.4
|
||||
1
app/static/files/test_1_2.pdf
Normal file
1
app/static/files/test_1_2.pdf
Normal file
@ -0,0 +1 @@
|
||||
%PDF-1.4
|
||||
1
app/static/files/test_1_2_3.pdf
Normal file
1
app/static/files/test_1_2_3.pdf
Normal file
@ -0,0 +1 @@
|
||||
%PDF-1.4
|
||||
@ -6,14 +6,25 @@
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..'))
|
||||
|
||||
try:
|
||||
# 相对导入(当作为包使用时)
|
||||
from .sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator
|
||||
from .sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 绝对导入(当直接运行时)
|
||||
from sub_formulas.living_heritage_b21 import LivingHeritageB21Calculator
|
||||
from sub_formulas.pattern_gene_b22 import PatternGeneB22Calculator
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
|
||||
|
||||
class CulturalValueB2Calculator:
|
||||
@ -23,6 +34,7 @@ class CulturalValueB2Calculator:
|
||||
"""初始化计算器"""
|
||||
self.living_heritage_calculator = LivingHeritageB21Calculator()
|
||||
self.pattern_gene_calculator = PatternGeneB22Calculator()
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_cultural_value_b2(self,
|
||||
living_heritage_b21: float,
|
||||
@ -42,48 +54,88 @@ class CulturalValueB2Calculator:
|
||||
|
||||
return cultural_value
|
||||
|
||||
def calculate_complete_cultural_value_b2(self, input_data: Dict) -> Dict:
|
||||
async def calculate_complete_cultural_value_b2(self, valuation_id: int, input_data: Dict) -> float:
|
||||
"""
|
||||
计算完整的文化价值B2,包含所有子公式
|
||||
|
||||
args:
|
||||
input_data: 输入数据字典,包含所有必要的参数
|
||||
|
||||
return:
|
||||
Dict: 包含所有中间计算结果和最终结果的字典
|
||||
"""
|
||||
# 计算活态传承系数B21
|
||||
teaching_frequency = self.living_heritage_calculator.calculate_teaching_frequency(
|
||||
input_data["offline_sessions"],
|
||||
input_data["douyin_views"],
|
||||
input_data["kuaishou_views"],
|
||||
input_data["bilibili_views"]
|
||||
)
|
||||
living_heritage_b21 = self.living_heritage_calculator.calculate_living_heritage_b21(
|
||||
input_data['inheritor_level_coefficient'],
|
||||
teaching_frequency,
|
||||
input_data['cross_border_depth']
|
||||
)
|
||||
|
||||
# 计算纹样基因值B22
|
||||
pattern_gene_b22 = self.pattern_gene_calculator.calculate_pattern_gene_b22(
|
||||
input_data['structure_complexity'],
|
||||
input_data['normalized_entropy'],
|
||||
input_data['historical_inheritance']
|
||||
)
|
||||
|
||||
# 计算文化价值B2
|
||||
cultural_value_b2 = self.calculate_cultural_value_b2(
|
||||
living_heritage_b21,
|
||||
pattern_gene_b22
|
||||
)
|
||||
|
||||
return {
|
||||
'living_heritage_b21': living_heritage_b21,
|
||||
'pattern_gene_b22': pattern_gene_b22,
|
||||
'cultural_value_b2': cultural_value_b2
|
||||
}
|
||||
计算完整的文化价值B2,并记录所有计算步骤。
|
||||
|
||||
该函数通过整合活态传承系数B21和纹样基因值B22的计算,
|
||||
最终得出文化价值B2。每一步的计算过程都会被记录下来,
|
||||
以确保计算的透明度和可追溯性。
|
||||
|
||||
Args:
|
||||
valuation_id (int): 估值的唯一标识符,用于关联所有计算步骤。
|
||||
input_data (Dict): 包含所有计算所需参数的字典,例如:
|
||||
{
|
||||
'inheritor_level_coefficient': 10.0, # B21
|
||||
'offline_sessions': 1, # B21
|
||||
'structure_complexity': 0.75, # B22
|
||||
...
|
||||
}
|
||||
|
||||
Returns:
|
||||
float: 计算得出的文化价值B2。
|
||||
|
||||
Raises:
|
||||
Exception: 在计算过程中遇到的任何异常都会被捕获、记录,并重新抛出。
|
||||
"""
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.2,
|
||||
step_name="文化价值B2计算",
|
||||
step_description="开始计算文化价值B2,公式为:活态传承系数B21 × 0.6 + (纹样基因值B22 / 10) × 0.4",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
# 计算活态传承系数B21
|
||||
living_heritage_b21 = self.living_heritage_calculator.calculate_living_heritage_b21(
|
||||
input_data['inheritor_level_coefficient'],
|
||||
self.living_heritage_calculator.calculate_teaching_frequency(
|
||||
input_data["offline_sessions"],
|
||||
input_data["douyin_views"],
|
||||
input_data["kuaishou_views"],
|
||||
input_data["bilibili_views"]
|
||||
),
|
||||
input_data['cross_border_depth']
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.21, step_name="活态传承系数B21",
|
||||
output_result={'living_heritage_b21': living_heritage_b21}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算纹样基因值B22
|
||||
pattern_gene_b22 = self.pattern_gene_calculator.calculate_pattern_gene_b22(
|
||||
input_data['structure_complexity'],
|
||||
input_data['normalized_entropy'],
|
||||
input_data['historical_inheritance']
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.22, step_name="纹样基因值B22",
|
||||
output_result={'pattern_gene_b22': pattern_gene_b22}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算文化价值B2
|
||||
cultural_value_b2 = self.calculate_cultural_value_b2(
|
||||
living_heritage_b21,
|
||||
pattern_gene_b22
|
||||
)
|
||||
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"cultural_value_b2": cultural_value_b2}}
|
||||
)
|
||||
return cultural_value_b2
|
||||
except Exception as e:
|
||||
error_message = f"文化价值B2计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
# 示例使用
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -8,12 +8,26 @@
|
||||
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
|
||||
|
||||
try:
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 处理可能的导入错误
|
||||
pass
|
||||
|
||||
class LivingHeritageB21Calculator:
|
||||
"""活态传承系数B21计算器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化计算器"""
|
||||
pass
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_living_heritage_b21(self,
|
||||
inheritor_level_coefficient: float,
|
||||
@ -22,7 +36,6 @@ class LivingHeritageB21Calculator:
|
||||
"""
|
||||
计算活态传承系数B21
|
||||
|
||||
|
||||
活态传承系数B21 = 传承人等级系数 × 0.4 + 教学传播频次 × 0.3 + 跨界合作深度 × 0.3
|
||||
|
||||
args:
|
||||
@ -30,14 +43,12 @@ class LivingHeritageB21Calculator:
|
||||
teaching_frequency: 教学传播频次 (用户填写)
|
||||
cross_border_depth: 跨界合作深度 (用户填写)
|
||||
|
||||
return:
|
||||
return:
|
||||
float: 活态传承系数B21
|
||||
"""
|
||||
#
|
||||
living_heritage = (inheritor_level_coefficient * 0.4 +
|
||||
teaching_frequency * 0.3 +
|
||||
cross_border_depth * 0.3)
|
||||
|
||||
return living_heritage
|
||||
|
||||
def calculate_inheritor_level_coefficient(self, inheritor_level: str) -> float:
|
||||
@ -47,13 +58,12 @@ class LivingHeritageB21Calculator:
|
||||
传承人等级评分标准:
|
||||
- 国家级传承人: 1分
|
||||
- 省级传承人: 0.7分
|
||||
- 市级传承人: .44分
|
||||
|
||||
- 市级传承人: 0.4分
|
||||
|
||||
args:
|
||||
inheritor_level: 传承人等级 (用户填写)
|
||||
|
||||
return:
|
||||
return:
|
||||
float: 传承人等级系数
|
||||
"""
|
||||
level_scores = {
|
||||
@ -61,7 +71,6 @@ class LivingHeritageB21Calculator:
|
||||
"省级传承人": 0.7,
|
||||
"市级传承人": 0.4,
|
||||
}
|
||||
|
||||
return level_scores.get(inheritor_level, 0.4)
|
||||
|
||||
def calculate_teaching_frequency(self,
|
||||
@ -74,16 +83,8 @@ class LivingHeritageB21Calculator:
|
||||
|
||||
教学传播频次 = 线下传习次数 × 0.6 + 线上课程点击量(万) × 0.4
|
||||
|
||||
线下传习次数统计规范:
|
||||
1) 单次活动标准:传承人主导、时长≥2小时、参与人数≥5人
|
||||
2) 频次计算:按自然年度累计,同一内容重复培训不计入
|
||||
|
||||
线上课程折算:
|
||||
- 抖音/快手播放量按100:1折算为学习人次
|
||||
- B站课程按50:1折算
|
||||
|
||||
args:
|
||||
offline_sessions: 线下传习次数(符合标准的活动次数)
|
||||
offline_sessions: 线下传习次数
|
||||
douyin_views: 抖音播放量
|
||||
kuaishou_views: 快手播放量
|
||||
bilibili_views: B站播放量
|
||||
@ -91,71 +92,55 @@ class LivingHeritageB21Calculator:
|
||||
returns:
|
||||
float: 教学传播频次评分
|
||||
"""
|
||||
# 线下传习次数权重计算
|
||||
offline_score = offline_sessions * 0.6
|
||||
|
||||
# 线上课程点击量折算
|
||||
# 抖音/快手按100:1折算
|
||||
douyin_kuaishou_learning_sessions = (douyin_views + kuaishou_views) / 100
|
||||
# B站按50:1折算
|
||||
bilibili_learning_sessions = bilibili_views / 50
|
||||
online_views_in_ten_thousands = (douyin_kuaishou_learning_sessions + bilibili_learning_sessions) / 10000
|
||||
online_score = online_views_in_ten_thousands * 0.4
|
||||
teaching_frequency_score = offline_score + online_score
|
||||
return teaching_frequency_score
|
||||
|
||||
def calculate_cross_border_depth(self, cross_border_projects: int) -> float:
|
||||
"""
|
||||
计算跨界合作深度
|
||||
|
||||
# 线上总学习人次(万)
|
||||
online_learning_sessions_10k = (douyin_kuaishou_learning_sessions + bilibili_learning_sessions) / 10000
|
||||
每参与1个跨界合作项目+1分,最高10分
|
||||
|
||||
# 线上课程权重计算
|
||||
online_score = online_learning_sessions_10k * 0.4
|
||||
|
||||
# 总教学传播频次
|
||||
teaching_frequency = offline_score + online_score
|
||||
|
||||
return teaching_frequency
|
||||
def cross_border_depth_dict(border_depth: str) -> float:
|
||||
cross_border_depth_scores = {
|
||||
"品牌联名": 0.3,
|
||||
"科技载体": 0.5,
|
||||
"国家外交礼品": 1,
|
||||
}
|
||||
return cross_border_depth_scores.get(border_depth, 0.3)
|
||||
|
||||
args:
|
||||
cross_border_projects: 跨界合作项目数
|
||||
|
||||
returns:
|
||||
float: 跨界合作深度评分
|
||||
"""
|
||||
return min(cross_border_projects, 10.0)
|
||||
|
||||
# 示例使用
|
||||
if __name__ == "__main__":
|
||||
calculator = LivingHeritageB21Calculator()
|
||||
|
||||
# 示例数据
|
||||
inheritor_level = "国家级传承人" # 传承人等级 (用户填写)
|
||||
cross_border_depth = 50.0
|
||||
# 教学传播频次数据
|
||||
offline_sessions = 20 # 线下传习次数(符合标准:传承人主导、时长≥2小时、参与人数≥5人)
|
||||
douyin_views = 10000000 # 抖音播放量
|
||||
kuaishou_views = 0 # 快手播放量
|
||||
bilibili_views = 0 # B站播放量
|
||||
|
||||
async def calculate_complete_living_heritage_b21(self, valuation_id: int, input_data: dict) -> float:
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.21,
|
||||
step_name="活态传承系数B21计算",
|
||||
step_description="开始计算活态传承系数B21",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
inheritor_level_coefficient = self.calculate_inheritor_level_coefficient(input_data['inheritor_level'])
|
||||
teaching_frequency = self.calculate_teaching_frequency(input_data['offline_sessions'], input_data.get('douyin_views', 0), input_data.get('kuaishou_views', 0), input_data.get('bilibili_views', 0))
|
||||
cross_border_depth = self.calculate_cross_border_depth(input_data['cross_border_projects'])
|
||||
|
||||
|
||||
# 计算各项指标
|
||||
inheritor_level_coefficient = calculator.calculate_inheritor_level_coefficient(inheritor_level)
|
||||
teaching_frequency = calculator.calculate_teaching_frequency(
|
||||
offline_sessions=offline_sessions,
|
||||
douyin_views=douyin_views,
|
||||
kuaishou_views=kuaishou_views,
|
||||
bilibili_views=bilibili_views
|
||||
)
|
||||
print(teaching_frequency)
|
||||
living_heritage_b21 = self.calculate_living_heritage_b21(inheritor_level_coefficient, teaching_frequency, cross_border_depth)
|
||||
|
||||
# 计算活态传承系数B21
|
||||
living_heritage_b21 = calculator.calculate_living_heritage_b21(
|
||||
1, teaching_frequency, 0.3
|
||||
)
|
||||
|
||||
print(f"传承人等级系数: {inheritor_level_coefficient:.2f}")
|
||||
print(f"教学传播频次: {teaching_frequency:.2f}")
|
||||
print(f" - 线下传习次数: {offline_sessions}次")
|
||||
print(f" - 抖音播放量: {douyin_views:,}次")
|
||||
print(f" - 快手播放量: {kuaishou_views:,}次")
|
||||
print(f" - B站播放量: {bilibili_views:,}次")
|
||||
print(f"跨界合作深度: {cross_border_depth:.2f}")
|
||||
print(f"活态传承系数B21: {living_heritage_b21:.4f}")
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"living_heritage_b21": living_heritage_b21}}
|
||||
)
|
||||
return living_heritage_b21
|
||||
except Exception as e:
|
||||
error_message = f"活态传承系数B21计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
@ -1,21 +1,26 @@
|
||||
|
||||
"""
|
||||
纹样基因值B22计算模块
|
||||
|
||||
纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
from typing import Dict, List
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
|
||||
|
||||
try:
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 处理可能的导入错误
|
||||
pass
|
||||
|
||||
class PatternGeneB22Calculator:
|
||||
"""纹样基因值B22计算器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化计算器"""
|
||||
pass
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_pattern_gene_b22(self,
|
||||
structure_complexity: float,
|
||||
@ -24,7 +29,6 @@ class PatternGeneB22Calculator:
|
||||
"""
|
||||
计算纹样基因值B22
|
||||
|
||||
|
||||
纹样基因值B22 = (结构复杂度SC × 0.6 + 归一化信息熵H × 0.4) × 历史传承度HI × 10
|
||||
|
||||
args:
|
||||
@ -35,11 +39,9 @@ class PatternGeneB22Calculator:
|
||||
return:
|
||||
float: 纹样基因值B22
|
||||
"""
|
||||
|
||||
pattern_gene = ((structure_complexity * 0.6 +
|
||||
normalized_entropy * 0.4) *
|
||||
historical_inheritance * 10)
|
||||
|
||||
return pattern_gene
|
||||
|
||||
def calculate_structure_complexity(self, pattern_elements: List[Dict]) -> float:
|
||||
@ -87,57 +89,49 @@ class PatternGeneB22Calculator:
|
||||
if not pattern_data or len(pattern_data) <= 1:
|
||||
return 0.0
|
||||
|
||||
# 计算概率分布
|
||||
total = sum(pattern_data)
|
||||
if total == 0:
|
||||
return 0.0
|
||||
|
||||
probabilities = [x / total for x in pattern_data if x > 0]
|
||||
|
||||
# 计算信息熵
|
||||
entropy = 0.0
|
||||
for p in probabilities:
|
||||
if p > 0:
|
||||
entropy -= p * math.log2(p)
|
||||
|
||||
# 归一化
|
||||
n = len(probabilities)
|
||||
if n <= 1:
|
||||
return 0.0
|
||||
|
||||
normalized_entropy = entropy / math.log2(n)
|
||||
return normalized_entropy
|
||||
|
||||
|
||||
async def calculate_complete_pattern_gene_b22(self, valuation_id: int, input_data: dict) -> float:
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.22,
|
||||
step_name="纹样基因值B22计算",
|
||||
step_description="开始计算纹样基因值B22",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
structure_complexity = self.calculate_structure_complexity(input_data['pattern_elements'])
|
||||
normalized_entropy = self.calculate_normalized_entropy(input_data['entropy_data'])
|
||||
historical_inheritance = input_data['historical_inheritance']
|
||||
|
||||
# 示例使用
|
||||
if __name__ == "__main__":
|
||||
pattern_gene_b22 = self.calculate_pattern_gene_b22(structure_complexity, normalized_entropy, historical_inheritance)
|
||||
|
||||
calculator = PatternGeneB22Calculator()
|
||||
|
||||
# 示例数据
|
||||
pattern_elements = [
|
||||
{'type': '几何图形', 'weight': 0.3, 'complexity': 0.7},
|
||||
{'type': '植物纹样', 'weight': 0.4, 'complexity': 0.8},
|
||||
{'type': '动物纹样', 'weight': 0.3, 'complexity': 0.6}
|
||||
]
|
||||
entropy_data = [0.3, 0.4, 0.3]
|
||||
inheritance_years = 500 # 传承年数 (用户填写)
|
||||
cultural_significance = "国家级" # 文化意义等级 (用户填写)
|
||||
preservation_status = "良好" # 保护状况 (用户填写)
|
||||
historical_inheritance = 100.0
|
||||
|
||||
# 计算各项指标
|
||||
structure_complexity = calculator.calculate_structure_complexity(pattern_elements)
|
||||
normalized_entropy = calculator.calculate_normalized_entropy(entropy_data)
|
||||
|
||||
|
||||
# 计算纹样基因值B22
|
||||
pattern_gene_b22 = calculator.calculate_pattern_gene_b22(
|
||||
1.5, 9, historical_inheritance
|
||||
)
|
||||
|
||||
print(f"结构复杂度SC: {structure_complexity:.4f}")
|
||||
print(f"归一化信息熵H: {normalized_entropy:.4f}")
|
||||
print(f"历史传承度HI: {historical_inheritance:.4f}")
|
||||
print(f"纹样基因值B22: {pattern_gene_b22:.4f}")
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"pattern_gene_b22": pattern_gene_b22}}
|
||||
)
|
||||
return pattern_gene_b22
|
||||
except Exception as e:
|
||||
error_message = f"纹样基因值B22计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
@ -6,17 +6,12 @@
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
|
||||
try:
|
||||
# 相对导入(当作为包使用时)
|
||||
from .sub_formulas.basic_value_b11 import BasicValueB11Calculator, calculate_popularity_score
|
||||
from .sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator
|
||||
from .sub_formulas.policy_multiplier_b13 import PolicyMultiplierB13Calculator
|
||||
except ImportError:
|
||||
# 绝对导入(当直接运行时)
|
||||
from sub_formulas.basic_value_b11 import BasicValueB11Calculator
|
||||
from sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator
|
||||
from sub_formulas.policy_multiplier_b13 import PolicyMultiplierB13Calculator
|
||||
from .sub_formulas.basic_value_b11 import BasicValueB11Calculator
|
||||
from .sub_formulas.traffic_factor_b12 import TrafficFactorB12Calculator
|
||||
from .sub_formulas.policy_multiplier_b13 import PolicyMultiplierB13Calculator
|
||||
|
||||
|
||||
class EconomicValueB1Calculator:
|
||||
@ -27,6 +22,7 @@ class EconomicValueB1Calculator:
|
||||
self.basic_value_calculator = BasicValueB11Calculator()
|
||||
self.traffic_factor_calculator = TrafficFactorB12Calculator()
|
||||
self.policy_multiplier_calculator = PolicyMultiplierB13Calculator()
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_economic_value_b1(self,
|
||||
basic_value_b11: float,
|
||||
@ -50,95 +46,103 @@ class EconomicValueB1Calculator:
|
||||
|
||||
return economic_value
|
||||
|
||||
def calculate_complete_economic_value_b1(self, input_data: Dict) -> Dict:
|
||||
async def calculate_complete_economic_value_b1(self, valuation_id: int, input_data: Dict) -> float:
|
||||
"""
|
||||
计算完整的经济价值B1,包含所有子公式
|
||||
|
||||
args:
|
||||
input_data: 输入数据字典,包含所有必要的参数
|
||||
|
||||
returns:
|
||||
Dict: 包含所有中间计算结果和最终结果的字典
|
||||
计算完整的经济价值B1,并记录所有计算步骤。
|
||||
|
||||
此函数集成了基础价值B11、流量因子B12和政策乘数B13的计算,
|
||||
通过调用相应的子计算器来完成。每一步的计算结果都会被记录下来,
|
||||
以支持后续的审计和分析。
|
||||
|
||||
Args:
|
||||
valuation_id (int): 估值的唯一标识符,用于关联所有计算步骤。
|
||||
input_data (Dict): 包含所有计算所需参数的字典,例如:
|
||||
{
|
||||
'three_year_income': [2000, 2400, 2600], # B11
|
||||
'patent_score': 1, # B11
|
||||
'search_index_s1': 4500.0, # B12
|
||||
'policy_match_score': 10.0, # B13
|
||||
...
|
||||
}
|
||||
|
||||
Returns:
|
||||
float: 计算得出的经济价值B1。
|
||||
|
||||
Raises:
|
||||
Exception: 在计算过程中发生的任何异常都会被捕获、记录,并重新抛出。
|
||||
"""
|
||||
# 财务价值F 近三年年均收益列表 [1,2,3]
|
||||
financial_value = self.basic_value_calculator.calculate_financial_value_f(input_data["three_year_income"])
|
||||
# 计算法律强度L patent_score: 专利分 (0-10分) (用户填写)
|
||||
# popularity_score: 普及地域分 (0-10分) (用户填写)
|
||||
# infringement_score: 侵权分 (0-10分) (用户填写)
|
||||
|
||||
legal_strength = self.basic_value_calculator.calculate_legal_strength_l(
|
||||
input_data["patent_score"],
|
||||
input_data["popularity_score"],
|
||||
input_data["infringement_score"],
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.1,
|
||||
step_name="经济价值B1计算",
|
||||
step_description="开始计算经济价值B1,公式为:基础价值B11 × (1 + 流量因子B12) × 政策乘数B13",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
# 计算基础价值B11
|
||||
basic_value_b11 = self.basic_value_calculator.calculate_basic_value_b11(
|
||||
self.basic_value_calculator.calculate_financial_value_f(input_data["three_year_income"]),
|
||||
self.basic_value_calculator.calculate_legal_strength_l(input_data["patent_score"], input_data["popularity_score"], input_data["infringement_score"]),
|
||||
self.basic_value_calculator.calculate_development_potential_d(input_data["patent_count"], input_data["esg_score"], input_data["innovation_ratio"]),
|
||||
input_data["industry_coefficient"]
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.11, step_name="基础价值B11",
|
||||
output_result={'basic_value_b11': basic_value_b11}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 发展潜力 patent_count: 专利分 (0-10分) (用户填写)
|
||||
# esg_score: ESG分 (0-10分) (用户填写)
|
||||
# innovation_ratio: 创新投入比 (研发费用/营收) * 100 (用户填写)
|
||||
development_potential = self.basic_value_calculator.calculate_development_potential_d(
|
||||
# 计算流量因子B12
|
||||
traffic_factor_b12 = self.traffic_factor_calculator.calculate_traffic_factor_b12(
|
||||
input_data['search_index_s1'],
|
||||
input_data['industry_average_s2'],
|
||||
self.traffic_factor_calculator.calculate_social_media_spread_s3(
|
||||
self.traffic_factor_calculator.calculate_interaction_index(input_data["likes"], input_data["comments"], input_data["shares"]),
|
||||
self.traffic_factor_calculator.calculate_coverage_index(0),
|
||||
self.traffic_factor_calculator.calculate_conversion_efficiency(input_data["sales_volume"], input_data["link_views"])
|
||||
)
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.12, step_name="流量因子B12",
|
||||
output_result={'traffic_factor_b12': traffic_factor_b12}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
input_data["patent_count"],
|
||||
input_data["esg_score"],
|
||||
input_data["innovation_ratio"],
|
||||
)
|
||||
# 计算行业系数I target_industry_roe: 目标行业平均ROE (系统配置)
|
||||
# benchmark_industry_roe: 基准行业ROE (系统配置)
|
||||
# industry_coefficient = self.basic_value_calculator.calculate_industry_coefficient_i(
|
||||
#
|
||||
# )
|
||||
# 计算基础价值B11
|
||||
basic_value_b11 = self.basic_value_calculator.calculate_basic_value_b11(
|
||||
financial_value, # 财务价值F
|
||||
legal_strength, # 法律强度L
|
||||
development_potential,
|
||||
input_data["industry_coefficient"]
|
||||
)
|
||||
# 计算政策乘数B13
|
||||
policy_multiplier_b13 = self.policy_multiplier_calculator.calculate_policy_multiplier_b13(
|
||||
self.policy_multiplier_calculator.calculate_policy_compatibility_score(
|
||||
input_data["policy_match_score"], input_data["implementation_stage"], input_data["funding_support"]
|
||||
)
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.13, step_name="政策乘数B13",
|
||||
output_result={'policy_multiplier_b13': policy_multiplier_b13}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算互动量指数
|
||||
interaction_index = self.traffic_factor_calculator.calculate_interaction_index(
|
||||
input_data["likes"],
|
||||
input_data["comments"],
|
||||
input_data["shares"],
|
||||
)
|
||||
# 计算覆盖人群指数
|
||||
coverage_index = self.traffic_factor_calculator.calculate_coverage_index(0)
|
||||
# 计算转化率
|
||||
conversion_efficiency = self.traffic_factor_calculator.calculate_conversion_efficiency(
|
||||
input_data["sales_volume"], input_data["link_views"])
|
||||
|
||||
social_media_spread_s3 = self.traffic_factor_calculator.calculate_social_media_spread_s3(interaction_index,
|
||||
coverage_index,
|
||||
conversion_efficiency)
|
||||
|
||||
traffic_factor_b12 = self.traffic_factor_calculator.calculate_traffic_factor_b12(
|
||||
input_data['search_index_s1'],
|
||||
input_data['industry_average_s2'],
|
||||
social_media_spread_s3
|
||||
)
|
||||
|
||||
# 计算政策乘数B13
|
||||
policy_compatibility_score = self.policy_multiplier_calculator.calculate_policy_compatibility_score(
|
||||
input_data["policy_match_score"],
|
||||
input_data["implementation_stage"],
|
||||
input_data["funding_support"])
|
||||
policy_multiplier_b13 = self.policy_multiplier_calculator.calculate_policy_multiplier_b13(
|
||||
policy_compatibility_score
|
||||
)
|
||||
|
||||
# 计算经济价值B1
|
||||
economic_value_b1 = self.calculate_economic_value_b1(
|
||||
basic_value_b11,
|
||||
traffic_factor_b12,
|
||||
policy_multiplier_b13
|
||||
)
|
||||
|
||||
return {
|
||||
'basic_value_b11': basic_value_b11,
|
||||
'traffic_factor_b12': traffic_factor_b12,
|
||||
'policy_multiplier_b13': policy_multiplier_b13,
|
||||
'economic_value_b1': economic_value_b1
|
||||
}
|
||||
# 计算经济价值B1
|
||||
economic_value_b1 = self.calculate_economic_value_b1(
|
||||
basic_value_b11,
|
||||
traffic_factor_b12,
|
||||
policy_multiplier_b13
|
||||
)
|
||||
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"economic_value_b1": economic_value_b1}}
|
||||
)
|
||||
return economic_value_b1
|
||||
except Exception as e:
|
||||
error_message = f"经济价值B1计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
# 示例使用
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -1,13 +1,25 @@
|
||||
import math
|
||||
from typing import List, Optional
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
|
||||
|
||||
try:
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 处理可能的导入错误
|
||||
pass
|
||||
|
||||
class BasicValueB11Calculator:
|
||||
"""基础价值B11计算器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化计算器"""
|
||||
pass
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_basic_value_b11(self,
|
||||
financial_value: float,
|
||||
@ -168,94 +180,146 @@ class BasicValueB11Calculator:
|
||||
|
||||
return industry_coefficient
|
||||
|
||||
def _calculate_patent_score(self, patent_remaining_years: int) -> float:
|
||||
"""
|
||||
计算专利分
|
||||
|
||||
# 专利相关计算函数
|
||||
def calculate_patent_score(patent_remaining_years: int) -> float:
|
||||
"""
|
||||
计算专利分
|
||||
专利剩余保护期评分标准:
|
||||
- >10年: 10分
|
||||
- 5-10年: 7分
|
||||
- <5年: 3分
|
||||
|
||||
专利剩余保护期评分标准:
|
||||
- >10年: 10分
|
||||
- 5-10年: 7分
|
||||
- <5年: 3分
|
||||
args:
|
||||
patent_remaining_years: 专利剩余保护期(年) (用户填写)
|
||||
|
||||
args:
|
||||
patent_remaining_years: 专利剩余保护期(年) (用户填写)
|
||||
returns:
|
||||
float: 专利分
|
||||
"""
|
||||
if patent_remaining_years > 10:
|
||||
return 10.0
|
||||
elif patent_remaining_years >= 5:
|
||||
return 7.0
|
||||
else:
|
||||
return 3.0
|
||||
|
||||
returns:
|
||||
float: 专利分
|
||||
"""
|
||||
if patent_remaining_years > 10:
|
||||
return 10.0
|
||||
elif patent_remaining_years >= 5:
|
||||
return 7.0
|
||||
else:
|
||||
return 3.0
|
||||
def _calculate_patent_usage_score(self, patent_count: int) -> float:
|
||||
"""
|
||||
计算专利使用量分
|
||||
|
||||
专利使用量评分标准:
|
||||
- 未引用: 0分
|
||||
- 每引用一项: +2.5分
|
||||
- 10分封顶
|
||||
|
||||
# 识别用户所上传的图像中的专利号,通过API验证专利是否存在,按所用专利数量赋分,未引用0分,每引用一项+2.5分,10分封顶(0-10分)
|
||||
def calculate_patent_usage_score(patent_count: int) -> float:
|
||||
"""
|
||||
计算专利使用量分
|
||||
args:
|
||||
patent_count: 专利数量 (用户填写)
|
||||
|
||||
专利使用量评分标准:
|
||||
- 未引用: 0分
|
||||
- 每引用一项: +2.5分
|
||||
- 10分封顶
|
||||
returns:
|
||||
float: 专利使用量分
|
||||
"""
|
||||
score = min(patent_count * 2.5, 10.0)
|
||||
return score
|
||||
|
||||
args:
|
||||
patent_count: 专利数量 (用户填写)
|
||||
def _calculate_popularity_score(self, region_coverage: str) -> float:
|
||||
"""
|
||||
计算普及地域分
|
||||
|
||||
returns:
|
||||
float: 专利使用量分
|
||||
"""
|
||||
score = min(patent_count * 2.5, 10.0)
|
||||
return score
|
||||
全球覆盖(10分),全国覆盖(7分),区域覆盖(4分)
|
||||
|
||||
args:
|
||||
region_coverage: 普及地域类型 (用户填写)
|
||||
|
||||
# 普及地域评分
|
||||
def calculate_popularity_score(region_coverage: str) -> float:
|
||||
"""
|
||||
计算普及地域分
|
||||
returns:
|
||||
float: 普及地域分
|
||||
"""
|
||||
coverage_scores = {
|
||||
"全球覆盖": 10.0,
|
||||
"全国覆盖": 7.0,
|
||||
"区域覆盖": 4.0
|
||||
}
|
||||
|
||||
全球覆盖(10分),全国覆盖(7分),区域覆盖(4分)
|
||||
return coverage_scores.get(region_coverage, 7.0)
|
||||
|
||||
args:
|
||||
region_coverage: 普及地域类型 (用户填写)
|
||||
def _calculate_infringement_score(self, infringement_status: str) -> float:
|
||||
"""
|
||||
计算侵权记录分
|
||||
|
||||
returns:
|
||||
float: 普及地域分
|
||||
"""
|
||||
coverage_scores = {
|
||||
"全球覆盖": 10.0,
|
||||
"全国覆盖": 7.0,
|
||||
"区域覆盖": 4.0
|
||||
}
|
||||
无侵权记录(10分),历史侵权已解决(6分),现存纠纷(2分)
|
||||
|
||||
return coverage_scores.get(region_coverage, 7.0)
|
||||
args:
|
||||
infringement_status: 侵权记录状态 (用户填写)
|
||||
|
||||
returns:
|
||||
float: 侵权记录分
|
||||
"""
|
||||
infringement_scores = {
|
||||
"无侵权记录": 10.0,
|
||||
"历史侵权已解决": 6.0,
|
||||
"现存纠纷": 2.0
|
||||
}
|
||||
|
||||
# 侵权记录评分
|
||||
def calculate_infringement_score(infringement_status: str) -> float:
|
||||
"""
|
||||
计算侵权记录分
|
||||
return infringement_scores.get(infringement_status, 6.0)
|
||||
|
||||
无侵权记录(10分),历史侵权已解决(6分),现存纠纷(2分)
|
||||
async def calculate_complete_basic_value_b11(self, valuation_id: int, input_data: dict) -> float:
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.11,
|
||||
step_name="基础价值B11计算",
|
||||
step_description="开始计算基础价值B11",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
financial_value = self.calculate_financial_value_f(input_data['annual_revenue_3_years'])
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.111, step_name="财务价值F",
|
||||
output_result={'financial_value': financial_value}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
args:
|
||||
infringement_status: 侵权记录状态 (用户填写)
|
||||
patent_score = self._calculate_patent_score(input_data['patent_remaining_years'])
|
||||
popularity_score = self._calculate_popularity_score(input_data['region_coverage'])
|
||||
infringement_score = self._calculate_infringement_score(input_data['infringement_status'])
|
||||
legal_strength = self.calculate_legal_strength_l(patent_score, popularity_score, infringement_score)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.112, step_name="法律强度L",
|
||||
output_result={'legal_strength': legal_strength}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
returns:
|
||||
float: 侵权记录分
|
||||
"""
|
||||
infringement_scores = {
|
||||
"无侵权记录": 10.0,
|
||||
"历史侵权已解决": 6.0,
|
||||
"现存纠纷": 2.0
|
||||
}
|
||||
patent_usage_score = self._calculate_patent_usage_score(input_data['patent_count'])
|
||||
development_potential = self.calculate_development_potential_d(patent_usage_score, input_data['esg_score'], input_data['innovation_ratio'])
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.113, step_name="发展潜力D",
|
||||
output_result={'development_potential': development_potential}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
return infringement_scores.get(infringement_status, 6.0)
|
||||
industry_coefficient = self.calculate_industry_coefficient_i(input_data['target_industry_roe'], input_data['benchmark_industry_roe'])
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.114, step_name="行业系数I",
|
||||
output_result={'industry_coefficient': industry_coefficient}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
basic_value_b11 = self.calculate_basic_value_b11(financial_value, legal_strength, development_potential, industry_coefficient)
|
||||
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"basic_value_b11": basic_value_b11}}
|
||||
)
|
||||
return basic_value_b11
|
||||
except Exception as e:
|
||||
error_message = f"基础价值B11计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
# 示例使用
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -1,15 +1,28 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
|
||||
|
||||
try:
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 处理可能的导入错误
|
||||
pass
|
||||
|
||||
class PolicyMultiplierB13Calculator:
|
||||
"""政策乘数B13计算器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化计算器"""
|
||||
pass
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_policy_multiplier_b13(self, policy_compatibility_score: float) -> float:
|
||||
"""
|
||||
计算政策乘数B13
|
||||
|
||||
|
||||
政策乘数B13 = 1 + (政策契合度评分P × 0.15)
|
||||
|
||||
Args:
|
||||
@ -18,9 +31,7 @@ class PolicyMultiplierB13Calculator:
|
||||
returns:
|
||||
float: 政策乘数B13
|
||||
"""
|
||||
#
|
||||
policy_multiplier = 1 + (policy_compatibility_score * 0.15)
|
||||
|
||||
return policy_multiplier
|
||||
|
||||
def calculate_policy_compatibility_score(self,
|
||||
@ -30,7 +41,6 @@ class PolicyMultiplierB13Calculator:
|
||||
"""
|
||||
计算政策契合度评分P
|
||||
|
||||
|
||||
政策契合度P = 政策匹配度 × 0.4 + 实施阶段评分 × 0.3 + 资金支持度 × 0.3
|
||||
|
||||
Args:
|
||||
@ -41,11 +51,9 @@ class PolicyMultiplierB13Calculator:
|
||||
returns:
|
||||
float: 政策契合度评分P
|
||||
"""
|
||||
#
|
||||
policy_compatibility = (policy_match_score * 0.4 +
|
||||
implementation_stage_score * 0.3 +
|
||||
funding_support_score * 0.3)
|
||||
|
||||
return policy_compatibility
|
||||
|
||||
def calculate_policy_match_score(self, industry: str) -> float:
|
||||
@ -60,8 +68,8 @@ class PolicyMultiplierB13Calculator:
|
||||
returns:
|
||||
float: 政策匹配度
|
||||
"""
|
||||
|
||||
return 5
|
||||
# 此处应有更复杂的逻辑根据行业匹配政策,暂时返回固定值
|
||||
return 5.0
|
||||
|
||||
def calculate_implementation_stage_score(self, implementation_stage: str) -> float:
|
||||
"""
|
||||
@ -80,8 +88,7 @@ class PolicyMultiplierB13Calculator:
|
||||
"推广阶段": 7.0,
|
||||
"试点阶段": 4.0
|
||||
}
|
||||
|
||||
return stage_scores.get(implementation_stage, 10.0)
|
||||
return stage_scores.get(implementation_stage, 7.0)
|
||||
|
||||
def calculate_funding_support_score(self, funding_support: str) -> float:
|
||||
"""
|
||||
@ -100,8 +107,44 @@ class PolicyMultiplierB13Calculator:
|
||||
"省级资助": 7.0,
|
||||
"无资助": 0.0
|
||||
}
|
||||
return funding_scores.get(funding_support, 7.0)
|
||||
|
||||
return funding_scores.get(funding_support, 0.0)
|
||||
async def calculate_complete_policy_multiplier_b13(self, valuation_id: int, input_data: dict) -> float:
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.13,
|
||||
step_name="政策乘数B13计算",
|
||||
step_description="开始计算政策乘数B13",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
policy_match_score = self.calculate_policy_match_score(input_data['industry'])
|
||||
implementation_stage_score = self.calculate_implementation_stage_score(input_data['implementation_stage'])
|
||||
funding_support_score = self.calculate_funding_support_score(input_data['funding_support'])
|
||||
|
||||
policy_compatibility_score = self.calculate_policy_compatibility_score(policy_match_score, implementation_stage_score, funding_support_score)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.131, step_name="政策契合度评分P",
|
||||
output_result={'policy_compatibility_score': policy_compatibility_score}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
policy_multiplier_b13 = self.calculate_policy_multiplier_b13(policy_compatibility_score)
|
||||
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"policy_multiplier_b13": policy_multiplier_b13}}
|
||||
)
|
||||
return policy_multiplier_b13
|
||||
except Exception as e:
|
||||
error_message = f"政策乘数B13计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
# 示例使用
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
import math
|
||||
from typing import Dict, Tuple
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
|
||||
|
||||
try:
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 处理可能的导入错误
|
||||
pass
|
||||
|
||||
class TrafficFactorB12Calculator:
|
||||
"""流量因子B12计算器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化计算器"""
|
||||
pass
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_traffic_factor_b12(self,
|
||||
search_index_s1: float,
|
||||
@ -31,18 +42,15 @@ class TrafficFactorB12Calculator:
|
||||
if industry_average_s2 == 0:
|
||||
raise ValueError("行业均值S2必须大于0")
|
||||
|
||||
if search_index_s1 == 0:
|
||||
if search_index_s1 <= 0:
|
||||
# 如果搜索指数为0或负数,使用最小值避免对数计算错误
|
||||
search_index_s1 = 1.0
|
||||
|
||||
# ,不进行任何拆分
|
||||
traffic_factor = (math.log(search_index_s1 / industry_average_s2) * 0.3 +
|
||||
social_media_spread_s3 * 0.7)
|
||||
|
||||
return traffic_factor
|
||||
|
||||
|
||||
|
||||
def calculate_social_media_spread_s3(self,
|
||||
interaction_index: float,
|
||||
coverage_index: float,
|
||||
@ -60,7 +68,6 @@ class TrafficFactorB12Calculator:
|
||||
returns:
|
||||
float: 社交媒体传播度S3
|
||||
"""
|
||||
#
|
||||
social_media_spread = (interaction_index * 0.4 +
|
||||
coverage_index * 0.3 +
|
||||
conversion_efficiency * 0.3)
|
||||
@ -84,7 +91,6 @@ class TrafficFactorB12Calculator:
|
||||
returns:
|
||||
float: 互动量指数
|
||||
"""
|
||||
#
|
||||
interaction_index = (likes + comments + shares) / 1000.0
|
||||
|
||||
return interaction_index
|
||||
@ -101,11 +107,45 @@ class TrafficFactorB12Calculator:
|
||||
returns:
|
||||
float: 覆盖人群指数
|
||||
"""
|
||||
#
|
||||
if followers == 0:
|
||||
return 0
|
||||
return 0.0
|
||||
coverage_index = followers / 10000.0
|
||||
return coverage_index
|
||||
|
||||
async def calculate_complete_traffic_factor_b12(self, valuation_id: int, input_data: dict) -> float:
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.12,
|
||||
step_name="流量因子B12计算",
|
||||
step_description="开始计算流量因子B12",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
interaction_index = self.calculate_interaction_index(input_data['likes'], input_data['comments'], input_data['shares'])
|
||||
coverage_index = self.calculate_coverage_index(input_data['followers'])
|
||||
social_media_spread_s3 = self.calculate_social_media_spread_s3(interaction_index, coverage_index, input_data['conversion_efficiency'])
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.121, step_name="社交媒体传播度S3",
|
||||
output_result={'social_media_spread_s3': social_media_spread_s3}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
traffic_factor_b12 = self.calculate_traffic_factor_b12(input_data['search_index_s1'], input_data['industry_average_s2'], social_media_spread_s3)
|
||||
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"traffic_factor_b12": traffic_factor_b12}}
|
||||
)
|
||||
return traffic_factor_b12
|
||||
except Exception as e:
|
||||
error_message = f"流量因子B12计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
def calculate_conversion_efficiency(self,
|
||||
click_count: int,
|
||||
|
||||
@ -19,10 +19,14 @@ try:
|
||||
# 包内相对导入
|
||||
from .model_value_b import ModelValueBCalculator
|
||||
from ..market_value_c import MarketValueCCalculator
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 直接运行时的绝对导入
|
||||
from app.utils.calculation_engine.final_value_ab.model_value_b import ModelValueBCalculator
|
||||
from app.utils.calculation_engine.market_value_c import MarketValueCCalculator
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
|
||||
|
||||
class FinalValueACalculator:
|
||||
@ -32,6 +36,7 @@ class FinalValueACalculator:
|
||||
"""初始化计算器"""
|
||||
self.model_value_calculator = ModelValueBCalculator()
|
||||
self.market_value_calculator = MarketValueCCalculator()
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_final_value_a(self,
|
||||
model_value_b: float,
|
||||
@ -64,17 +69,32 @@ class FinalValueACalculator:
|
||||
|
||||
return final_value
|
||||
|
||||
async def calculate_complete_final_value_a(self, input_data: Dict) -> Dict:
|
||||
async def calculate_complete_final_value_a(self, valuation_id: int, input_data: Dict) -> float:
|
||||
"""
|
||||
计算完整的最终估值A,包含所有子模块
|
||||
计算完整的最终估值A,并记录每一步的计算过程。
|
||||
|
||||
input_data: 输入数据字典,包含所有必要的参数
|
||||
|
||||
该函数作为最终估值计算的入口,协调调用模型估值B和市场估值C的计算,
|
||||
并将计算过程中的关键步骤(如子模块的调用、输入、输出)持久化,
|
||||
以便于后续的审计和追溯。
|
||||
|
||||
包含所有中间计算结果和最终结果的字典
|
||||
Args:
|
||||
valuation_id (int): 本次估值的唯一标识符,用于关联所有计算步骤。
|
||||
input_data (Dict): 包含所有计算所需参数的字典,结构如下:
|
||||
{
|
||||
'model_data': { ... }, # 模型估值B所需数据
|
||||
'market_data': { ... } # 市场估值C所需数据
|
||||
}
|
||||
|
||||
Returns:
|
||||
float: 计算得出的最终估值A。
|
||||
|
||||
Raises:
|
||||
Exception: 在计算过程中遇到的任何异常都会被重新抛出,
|
||||
并在记录最后一步为“计算失败”后终止。
|
||||
"""
|
||||
import time
|
||||
start_time = time.time()
|
||||
step_order = 1
|
||||
|
||||
# 记录输入参数
|
||||
logger.info("final_value_a.calculation_start input_data_keys={} model_data_keys={} market_data_keys={}",
|
||||
@ -82,61 +102,74 @@ class FinalValueACalculator:
|
||||
list(input_data.get('model_data', {}).keys()),
|
||||
list(input_data.get('market_data', {}).keys()))
|
||||
|
||||
# 详细记录模型数据参数
|
||||
model_data = input_data.get('model_data', {})
|
||||
if 'economic_data' in model_data:
|
||||
economic_data = model_data['economic_data']
|
||||
logger.info("final_value_a.economic_data 经济价值B1参数: 近三年机构收益={} 专利分={} 普及地域分={} 侵权分={} 创新投入比={} ESG分={} 专利使用量={} 行业修正系数={}",
|
||||
economic_data.get('three_year_income'),
|
||||
economic_data.get('patent_score'),
|
||||
economic_data.get('popularity_score'),
|
||||
economic_data.get('infringement_score'),
|
||||
economic_data.get('innovation_ratio'),
|
||||
economic_data.get('esg_score'),
|
||||
economic_data.get('patent_count'),
|
||||
economic_data.get('industry_coefficient'))
|
||||
|
||||
if 'cultural_data' in model_data:
|
||||
cultural_data = model_data['cultural_data']
|
||||
logger.info("final_value_a.cultural_data 文化价值B2参数: 传承人等级系数={} 跨境深度={} 线下教学次数={} 抖音浏览量={} 快手浏览量={} 哔哩哔哩浏览量={} 结构复杂度={} 归一化信息熵={} 历史传承度={}",
|
||||
cultural_data.get('inheritor_level_coefficient'),
|
||||
cultural_data.get('cross_border_depth'),
|
||||
cultural_data.get('offline_sessions'),
|
||||
cultural_data.get('douyin_views'),
|
||||
cultural_data.get('kuaishou_views'),
|
||||
cultural_data.get('bilibili_views'),
|
||||
cultural_data.get('structure_complexity'),
|
||||
cultural_data.get('normalized_entropy'),
|
||||
cultural_data.get('historical_inheritance'))
|
||||
|
||||
if 'risky_data' in model_data:
|
||||
risky_data = model_data['risky_data']
|
||||
logger.info("final_value_a.risky_data 风险调整B3参数: 最高价={} 最低价={} 诉讼状态={} 传承人年龄={}",
|
||||
risky_data.get('highest_price'),
|
||||
risky_data.get('lowest_price'),
|
||||
risky_data.get('lawsuit_status'),
|
||||
risky_data.get('inheritor_ages'))
|
||||
|
||||
# 详细记录市场数据参数
|
||||
market_data = input_data.get('market_data', {})
|
||||
logger.info("final_value_a.market_data 市场估值C参数: 平均交易价={} 手动出价={} 专家估值={} 日浏览量={} 收藏数量={} 发行等级={} 最近市场活动={}",
|
||||
market_data.get('average_transaction_price'),
|
||||
market_data.get('manual_bids'),
|
||||
market_data.get('expert_valuations'),
|
||||
market_data.get('daily_browse_volume'),
|
||||
market_data.get('collection_count'),
|
||||
market_data.get('issuance_level'),
|
||||
market_data.get('recent_market_activity'))
|
||||
|
||||
# 计算模型估值B
|
||||
logger.info("final_value_a.calculating_model_value_b 开始计算模型估值B")
|
||||
model_start_time = time.time()
|
||||
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="开始计算最终估值A",
|
||||
step_description="接收输入参数,准备开始计算。",
|
||||
input_params=input_data,
|
||||
status="processing"
|
||||
)
|
||||
)
|
||||
step_order += 1
|
||||
|
||||
try:
|
||||
model_result = self.model_value_calculator.calculate_complete_model_value_b(
|
||||
# 详细记录模型数据参数
|
||||
model_data = input_data.get('model_data', {})
|
||||
if 'economic_data' in model_data:
|
||||
economic_data = model_data['economic_data']
|
||||
logger.info("final_value_a.economic_data 经济价值B1参数: 近三年机构收益={} 专利分={} 普及地域分={} 侵权分={} 创新投入比={} ESG分={} 专利使用量={} 行业修正系数={}",
|
||||
economic_data.get('three_year_income'),
|
||||
economic_data.get('patent_score'),
|
||||
economic_data.get('popularity_score'),
|
||||
economic_data.get('infringement_score'),
|
||||
economic_data.get('innovation_ratio'),
|
||||
economic_data.get('esg_score'),
|
||||
economic_data.get('patent_count'),
|
||||
economic_data.get('industry_coefficient'))
|
||||
|
||||
if 'cultural_data' in model_data:
|
||||
cultural_data = model_data['cultural_data']
|
||||
logger.info("final_value_a.cultural_data 文化价值B2参数: 传承人等级系数={} 跨境深度={} 线下教学次数={} 抖音浏览量={} 快手浏览量={} 哔哩哔哩浏览量={} 结构复杂度={} 归一化信息熵={} 历史传承度={}",
|
||||
cultural_data.get('inheritor_level_coefficient'),
|
||||
cultural_data.get('cross_border_depth'),
|
||||
cultural_data.get('offline_sessions'),
|
||||
cultural_data.get('douyin_views'),
|
||||
cultural_data.get('kuaishou_views'),
|
||||
cultural_data.get('bilibili_views'),
|
||||
cultural_data.get('structure_complexity'),
|
||||
cultural_data.get('normalized_entropy'),
|
||||
cultural_data.get('historical_inheritance'))
|
||||
|
||||
if 'risky_data' in model_data:
|
||||
risky_data = model_data['risky_data']
|
||||
logger.info("final_value_a.risky_data 风险调整B3参数: 最高价={} 最低价={} 诉讼状态={} 传承人年龄={}",
|
||||
risky_data.get('highest_price'),
|
||||
risky_data.get('lowest_price'),
|
||||
risky_data.get('lawsuit_status'),
|
||||
risky_data.get('inheritor_ages'))
|
||||
|
||||
# 详细记录市场数据参数
|
||||
market_data = input_data.get('market_data', {})
|
||||
logger.info("final_value_a.market_data 市场估值C参数: 平均交易价={} 手动出价={} 专家估值={} 日浏览量={} 收藏数量={} 发行等级={} 最近市场活动={}",
|
||||
market_data.get('average_transaction_price'),
|
||||
market_data.get('manual_bids'),
|
||||
market_data.get('expert_valuations'),
|
||||
market_data.get('daily_browse_volume'),
|
||||
market_data.get('collection_count'),
|
||||
market_data.get('issuance_level'),
|
||||
market_data.get('recent_market_activity'))
|
||||
|
||||
# 计算模型估值B
|
||||
logger.info("final_value_a.calculating_model_value_b 开始计算模型估值B")
|
||||
model_start_time = time.time()
|
||||
|
||||
model_result = await self.model_value_calculator.calculate_complete_model_value_b(
|
||||
valuation_id,
|
||||
input_data['model_data']
|
||||
)
|
||||
model_value_b = model_result['model_value_b']
|
||||
model_value_b = model_result if isinstance(model_result, (int, float)) else model_result.get('model_value_b')
|
||||
model_duration = time.time() - model_start_time
|
||||
|
||||
logger.info("final_value_a.model_value_b_calculated 模型估值B计算完成: 模型估值B={}万元 耗时={}ms 返回字段={}",
|
||||
@ -144,19 +177,28 @@ class FinalValueACalculator:
|
||||
int(model_duration * 1000),
|
||||
list(model_result.keys()))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("final_value_a.model_value_b_calculation_failed 模型估值B计算失败: 错误={} 输入数据={}", str(e), input_data.get('model_data', {}))
|
||||
raise
|
||||
|
||||
# 计算市场估值C
|
||||
logger.info("final_value_a.calculating_market_value_c 开始计算市场估值C")
|
||||
market_start_time = time.time()
|
||||
|
||||
try:
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算模型估值B",
|
||||
step_description="调用ModelValueBCalculator计算模型估值B。",
|
||||
input_params=input_data.get('model_data', {}),
|
||||
output_result=model_result,
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
step_order += 1
|
||||
|
||||
# 计算市场估值C
|
||||
logger.info("final_value_a.calculating_market_value_c 开始计算市场估值C")
|
||||
market_start_time = time.time()
|
||||
|
||||
market_result = await self.market_value_calculator.calculate_complete_market_value_c(
|
||||
valuation_id,
|
||||
input_data['market_data']
|
||||
)
|
||||
market_value_c = market_result['market_value_c']
|
||||
market_value_c = market_result if isinstance(market_result, (int, float)) else market_result.get('market_value_c')
|
||||
market_duration = time.time() - market_start_time
|
||||
|
||||
logger.info("final_value_a.market_value_c_calculated 市场估值C计算完成: 市场估值C={}万元 耗时={}ms 返回字段={}",
|
||||
@ -164,15 +206,23 @@ class FinalValueACalculator:
|
||||
int(market_duration * 1000),
|
||||
list(market_result.keys()))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("final_value_a.market_value_c_calculation_failed 市场估值C计算失败: 错误={} 输入数据={}", str(e), input_data.get('market_data', {}))
|
||||
raise
|
||||
|
||||
# 计算最终估值A
|
||||
logger.info("final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B={}万元 市场估值C={}万元",
|
||||
model_value_b, market_value_c)
|
||||
|
||||
try:
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算市场估值C",
|
||||
step_description="调用MarketValueCCalculator计算市场估值C。",
|
||||
input_params=input_data.get('market_data', {}),
|
||||
output_result=market_result,
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
step_order += 1
|
||||
|
||||
# 计算最终估值A
|
||||
logger.info("final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B={}万元 市场估值C={}万元",
|
||||
model_value_b, market_value_c)
|
||||
|
||||
final_value_a = self.calculate_final_value_a(
|
||||
model_value_b,
|
||||
market_value_c
|
||||
@ -188,16 +238,37 @@ class FinalValueACalculator:
|
||||
int(model_duration * 1000),
|
||||
int(market_duration * 1000))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("final_value_a.final_value_calculation_failed 最终估值A计算失败: 错误={} 模型估值B={}万元 市场估值C={}万元",
|
||||
str(e), model_value_b, market_value_c)
|
||||
raise
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算最终估值A",
|
||||
step_description="最终估值A = 模型估值B × 0.7 + 市场估值C × 0.3",
|
||||
input_params={"model_value_b": model_value_b, "market_value_c": market_value_c},
|
||||
output_result={"final_value_a": final_value_a},
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
return {
|
||||
"model_value_b": model_value_b,
|
||||
"market_value_c": market_value_c,
|
||||
"final_value_ab": final_value_a,
|
||||
}
|
||||
|
||||
return {
|
||||
'model_value_b': model_value_b,
|
||||
'market_value_c': market_value_c,
|
||||
'final_value_ab': final_value_a,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("final_value_a.calculation_failed 计算失败: 错误={}", str(e))
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算失败",
|
||||
step_description="计算过程中发生错误。",
|
||||
status="failed",
|
||||
error_message=str(e)
|
||||
)
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -12,10 +12,14 @@ try:
|
||||
# 相对导入(当作为包使用时)
|
||||
from ..economic_value_b1.economic_value_b1 import EconomicValueB1Calculator
|
||||
from ..cultural_value_b2.cultural_value_b2 import CulturalValueB2Calculator
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 绝对导入(当直接运行时)
|
||||
from app.utils.calculation_engine.economic_value_b1.economic_value_b1 import EconomicValueB1Calculator
|
||||
from app.utils.calculation_engine.cultural_value_b2.cultural_value_b2 import CulturalValueB2Calculator
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
|
||||
|
||||
class ModelValueBCalculator:
|
||||
@ -26,6 +30,7 @@ class ModelValueBCalculator:
|
||||
self.economic_value_calculator = EconomicValueB1Calculator()
|
||||
self.cultural_value_calculator = CulturalValueB2Calculator()
|
||||
self.risk_adjustment_calculator = RiskAdjustmentB3Calculator()
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_model_value_b(self,
|
||||
economic_value_b1: float,
|
||||
@ -46,45 +51,127 @@ class ModelValueBCalculator:
|
||||
|
||||
return model_value
|
||||
|
||||
def calculate_complete_model_value_b(self, input_data: Dict) -> Dict:
|
||||
async def calculate_complete_model_value_b(self, valuation_id: int, input_data: Dict) -> float:
|
||||
"""
|
||||
计算完整的模型估值B,包含所有子公式
|
||||
|
||||
计算完整的模型估值B,并记录详细的计算步骤。
|
||||
|
||||
此函数通过依次调用经济价值B1、文化价值B2和风险调整系数B3的计算器,
|
||||
完成模型估值B的全面计算。每一步的计算(包括子模块的调用、输入、输出)
|
||||
都会被记录下来,用于后续的分析和审计。
|
||||
|
||||
Args:
|
||||
input_data: 输入数据字典,包含所有必要的参数
|
||||
|
||||
valuation_id (int): 估值的唯一标识符,用于关联所有计算步骤。
|
||||
input_data (Dict): 包含所有计算所需参数的字典,结构应包含:
|
||||
{
|
||||
'economic_data': { ... }, # 经济价值B1所需数据
|
||||
'cultural_data': { ... }, # 文化价值B2所需数据
|
||||
'risky_data': { ... } # 风险调整系数B3所需数据
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict: 包含所有中间计算结果和最终结果的字典
|
||||
float: 计算得出的模型估值B。
|
||||
|
||||
Raises:
|
||||
Exception: 在计算过程中遇到的任何异常都会被捕获、记录,然后重新抛出。
|
||||
"""
|
||||
# 计算经济价值B1
|
||||
economic_result = self.economic_value_calculator.calculate_complete_economic_value_b1(
|
||||
input_data['economic_data']
|
||||
step_order = 1
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="开始计算模型估值B",
|
||||
step_description="接收输入参数,准备开始计算。",
|
||||
input_params=input_data,
|
||||
status="processing"
|
||||
)
|
||||
)
|
||||
economic_value_b1 = economic_result['economic_value_b1']
|
||||
step_order += 1
|
||||
|
||||
# 计算文化价值B2
|
||||
cultural_result = self.cultural_value_calculator.calculate_complete_cultural_value_b2(
|
||||
input_data['cultural_data']
|
||||
)
|
||||
cultural_value_b2 = cultural_result['cultural_value_b2']
|
||||
try:
|
||||
# 计算经济价值B1(传入估值ID并等待异步完成)
|
||||
economic_value_b1 = await self.economic_value_calculator.calculate_complete_economic_value_b1(
|
||||
valuation_id,
|
||||
input_data['economic_data']
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算经济价值B1",
|
||||
step_description="调用EconomicValueB1Calculator计算经济价值B1。",
|
||||
input_params=input_data.get('economic_data', {}),
|
||||
output_result={"economic_value_b1": economic_value_b1},
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
step_order += 1
|
||||
|
||||
risk_value_result = self.risk_adjustment_calculator.calculate_complete_risky_value_b3(
|
||||
input_data['risky_data']
|
||||
)
|
||||
risk_value_b3 = risk_value_result['risk_adjustment_b3']
|
||||
# 计算模型估值B
|
||||
model_value_b = self.calculate_model_value_b(
|
||||
economic_value_b1,
|
||||
cultural_value_b2,
|
||||
risk_value_b3
|
||||
)
|
||||
# 计算文化价值B2(传入估值ID并等待异步完成)
|
||||
cultural_value_b2 = await self.cultural_value_calculator.calculate_complete_cultural_value_b2(
|
||||
valuation_id,
|
||||
input_data['cultural_data']
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算文化价值B2",
|
||||
step_description="调用CulturalValueB2Calculator计算文化价值B2。",
|
||||
input_params=input_data.get('cultural_data', {}),
|
||||
output_result={"cultural_value_b2": cultural_value_b2},
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
step_order += 1
|
||||
|
||||
return {
|
||||
'economic_value_b1': economic_value_b1,
|
||||
'cultural_value_b2': cultural_value_b2,
|
||||
'risk_value_b3': risk_value_b3,
|
||||
'model_value_b': model_value_b,
|
||||
}
|
||||
# 计算风险调整系数B3(传入估值ID并等待异步完成)
|
||||
risk_value_b3 = await self.risk_adjustment_calculator.calculate_complete_risky_value_b3(
|
||||
valuation_id,
|
||||
input_data['risky_data']
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算风险调整系数B3",
|
||||
step_description="调用RiskAdjustmentB3Calculator计算风险调整系数B3。",
|
||||
input_params=input_data.get('risky_data', {}),
|
||||
output_result={"risk_adjustment_b3": risk_value_b3},
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
step_order += 1
|
||||
|
||||
# 计算模型估值B
|
||||
model_value_b = self.calculate_model_value_b(
|
||||
economic_value_b1,
|
||||
cultural_value_b2,
|
||||
risk_value_b3
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算模型估值B",
|
||||
step_description="模型估值B = (经济价值B1*0.7+文化价值B2*0.3)*风险调整系数B3",
|
||||
input_params={"economic_value_b1": economic_value_b1, "cultural_value_b2": cultural_value_b2, "risk_value_b3": risk_value_b3},
|
||||
output_result={"model_value_b": model_value_b},
|
||||
status="completed"
|
||||
)
|
||||
)
|
||||
return model_value_b
|
||||
except Exception as e:
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=step_order,
|
||||
step_name="计算失败",
|
||||
step_description="计算过程中发生错误。",
|
||||
status="failed",
|
||||
error_message=str(e)
|
||||
)
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
# 示例使用
|
||||
|
||||
@ -8,6 +8,9 @@ import logging
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if current_dir not in sys.path:
|
||||
sys.path.append(current_dir)
|
||||
# 添加项目根目录
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..'))
|
||||
|
||||
|
||||
try:
|
||||
# 相对导入(当作为包使用时)
|
||||
@ -16,6 +19,8 @@ try:
|
||||
from .sub_formulas.scarcity_multiplier_c3 import ScarcityMultiplierC3Calculator
|
||||
from .sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator
|
||||
from .market_data_analyzer import market_data_analyzer
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 绝对导入(当直接运行时)
|
||||
from sub_formulas.market_bidding_c1 import MarketBiddingC1Calculator
|
||||
@ -23,6 +28,8 @@ except ImportError:
|
||||
from sub_formulas.scarcity_multiplier_c3 import ScarcityMultiplierC3Calculator
|
||||
from sub_formulas.temporal_decay_c4 import TemporalDecayC4Calculator
|
||||
from market_data_analyzer import market_data_analyzer
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -36,6 +43,7 @@ class MarketValueCCalculator:
|
||||
self.heat_coefficient_calculator = HeatCoefficientC2Calculator()
|
||||
self.scarcity_multiplier_calculator = ScarcityMultiplierC3Calculator()
|
||||
self.temporal_decay_calculator = TemporalDecayC4Calculator()
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
async def _get_dynamic_default_price(self, input_data: Dict) -> float:
|
||||
"""
|
||||
@ -95,62 +103,109 @@ class MarketValueCCalculator:
|
||||
|
||||
return market_value
|
||||
|
||||
async def calculate_complete_market_value_c(self, input_data: Dict) -> Dict:
|
||||
async def calculate_complete_market_value_c(self, valuation_id: int, input_data: Dict) -> float:
|
||||
"""
|
||||
计算完整的市场估值C,包含所有子公式
|
||||
|
||||
args:
|
||||
input_data: 输入数据字典,包含所有必要的参数
|
||||
参数来源标记(用户填写/系统配置/API获取/系统计算):
|
||||
- average_transaction_price: 系统计算(基于用户填写/API获取)
|
||||
- market_activity_coefficient: 系统计算(基于用户填写)
|
||||
- daily_browse_volume: API获取/系统估算
|
||||
- collection_count: API获取/系统估算
|
||||
- issuance_level: 用户填写
|
||||
- recent_market_activity: 用户填写
|
||||
- issuance_scarcity/circulation_scarcity/uniqueness_scarcity: 系统配置/系统计算(保留向后兼容)
|
||||
|
||||
return:
|
||||
Dict: 包含所有中间计算结果和最终结果的字典
|
||||
"""
|
||||
# 计算市场竞价C1
|
||||
market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1(
|
||||
transaction_data={'weighted_average_price': input_data.get('weighted_average_price', 0)},
|
||||
manual_bids=input_data.get('manual_bids', []),
|
||||
expert_valuations=input_data.get('expert_valuations', [])
|
||||
)
|
||||
计算完整的市场估值C,并记录每一步的计算过程。
|
||||
|
||||
# 计算热度系数C2
|
||||
heat_coefficient_c2 = self.heat_coefficient_calculator.calculate_heat_coefficient_c2(
|
||||
input_data.get('daily_browse_volume', 500.0),
|
||||
input_data.get('collection_count', 50)
|
||||
该函数通过顺序调用市场竞价C1、热度系数C2、稀缺性乘数C3和时效性衰减C4的计算器,
|
||||
最终得出市场估值C。计算过程中的每个子步骤都会被详细记录,以便于审计和跟踪。
|
||||
|
||||
Args:
|
||||
valuation_id (int): 估值的唯一标识符,用于关联所有计算步骤。
|
||||
input_data (Dict): 包含所有计算所需参数的字典,例如:
|
||||
{
|
||||
'weighted_average_price': 50000.0, # C1
|
||||
'manual_bids': [48000.0, 52000.0], # C1
|
||||
'expert_valuations': [49000.0, 51000.0], # C1
|
||||
'daily_browse_volume': 500.0, # C2
|
||||
'collection_count': 50, # C2
|
||||
'issuance_level': '限量', # C3
|
||||
'recent_market_activity': '2024-01-15' # C4
|
||||
}
|
||||
|
||||
Returns:
|
||||
float: 计算得出的市场估值C。
|
||||
|
||||
Raises:
|
||||
Exception: 如果在计算过程中发生任何错误,将记录失败状态并重新抛出异常。
|
||||
"""
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=3,
|
||||
step_name="市场估值C计算",
|
||||
step_description="开始计算市场估值C,公式为:市场竞价C1 × 热度系数C2 × 稀缺性乘数C3 × 时效性衰减C4",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算稀缺性乘数C3
|
||||
scarcity_multiplier_c3 = self.scarcity_multiplier_calculator.calculate_scarcity_multiplier_c3(
|
||||
input_data.get('issuance_level', '限量')
|
||||
)
|
||||
|
||||
# 计算时效性衰减C4
|
||||
temporal_decay_c4 = self.temporal_decay_calculator.calculate_temporal_decay_c4(
|
||||
input_data.get('recent_market_activity', '2024-01-15')
|
||||
)
|
||||
|
||||
# 计算市场估值C
|
||||
market_value_c = self.calculate_market_value_c(
|
||||
market_bidding_c1,
|
||||
heat_coefficient_c2,
|
||||
scarcity_multiplier_c3,
|
||||
temporal_decay_c4
|
||||
)
|
||||
|
||||
return {
|
||||
'market_bidding_c1': market_bidding_c1,
|
||||
'heat_coefficient_c2': heat_coefficient_c2,
|
||||
'scarcity_multiplier_c3': scarcity_multiplier_c3,
|
||||
'temporal_decay_c4': temporal_decay_c4,
|
||||
'market_value_c': market_value_c
|
||||
}
|
||||
try:
|
||||
# 计算市场竞价C1
|
||||
market_bidding_c1 = self.market_bidding_calculator.calculate_market_bidding_c1(
|
||||
transaction_data={'weighted_average_price': input_data.get('weighted_average_price', 0)},
|
||||
manual_bids=input_data.get('manual_bids', []),
|
||||
expert_valuations=input_data.get('expert_valuations', [])
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=3.1, step_name="市场竞价C1",
|
||||
output_result={'market_bidding_c1': market_bidding_c1}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算热度系数C2
|
||||
heat_coefficient_c2 = self.heat_coefficient_calculator.calculate_heat_coefficient_c2(
|
||||
input_data.get('daily_browse_volume', 500.0),
|
||||
input_data.get('collection_count', 50)
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=3.2, step_name="热度系数C2",
|
||||
output_result={'heat_coefficient_c2': heat_coefficient_c2}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算稀缺性乘数C3
|
||||
scarcity_multiplier_c3 = self.scarcity_multiplier_calculator.calculate_scarcity_multiplier_c3(
|
||||
input_data.get('issuance_level', '限量')
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=3.3, step_name="稀缺性乘数C3",
|
||||
output_result={'scarcity_multiplier_c3': scarcity_multiplier_c3}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算时效性衰减C4
|
||||
temporal_decay_c4 = self.temporal_decay_calculator.calculate_temporal_decay_c4(
|
||||
input_data.get('recent_market_activity', '2024-01-15')
|
||||
)
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=3.4, step_name="时效性衰减C4",
|
||||
output_result={'temporal_decay_c4': temporal_decay_c4}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算市场估值C
|
||||
market_value_c = self.calculate_market_value_c(
|
||||
market_bidding_c1,
|
||||
heat_coefficient_c2,
|
||||
scarcity_multiplier_c3,
|
||||
temporal_decay_c4
|
||||
)
|
||||
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {"market_value_c": market_value_c}}
|
||||
)
|
||||
return market_value_c
|
||||
except Exception as e:
|
||||
error_message = f"市场估值C计算失败: {e}"
|
||||
logger.error(error_message, exc_info=True)
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
# 示例使用
|
||||
|
||||
@ -6,13 +6,25 @@
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(os.path.join(current_dir, '..', '..', '..', '..'))
|
||||
|
||||
try:
|
||||
from app.controllers.valuation import ValuationController
|
||||
from app.schemas.valuation import ValuationCalculationStepCreate
|
||||
except ImportError:
|
||||
# 处理可能的导入错误
|
||||
pass
|
||||
|
||||
class RiskAdjustmentB3Calculator:
|
||||
"""风险调整系数B3计算器"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
self.valuation_controller = ValuationController()
|
||||
|
||||
def calculate_risk_adjustment_b3(self, risk_score_sum: float) -> float:
|
||||
"""
|
||||
@ -155,22 +167,81 @@ class RiskAdjustmentB3Calculator:
|
||||
|
||||
return max_score
|
||||
|
||||
def calculate_complete_risky_value_b3(self, input_data: Dict) -> Dict:
|
||||
# 计算各项风险评分
|
||||
market_risk = self.calculate_market_risk(input_data["highest_price"], input_data["lowest_price"])
|
||||
legal_risk = self.calculate_legal_risk(input_data["lawsuit_status"])
|
||||
inheritance_risk = self.calculate_inheritance_risk(input_data["inheritor_ages"])
|
||||
async def calculate_complete_risky_value_b3(self, valuation_id: int, input_data: Dict) -> float:
|
||||
"""
|
||||
计算完整的风险调整系数B3,并记录所有计算步骤。
|
||||
|
||||
# 计算风险评分总和R
|
||||
risk_score_sum = self.calculate_risk_score_sum(market_risk, legal_risk, inheritance_risk)
|
||||
该函数通过整合市场风险、法律风险和传承风险的评估,
|
||||
计算出风险评分总和R,并最终得出风险调整系数B3。
|
||||
每一步的计算过程都会被记录下来,以确保计算的透明度和可追溯性。
|
||||
|
||||
# 计算风险调整系数B3
|
||||
risk_adjustment_b3 = self.calculate_risk_adjustment_b3(risk_score_sum)
|
||||
return {
|
||||
'risk_score_sum': risk_score_sum,
|
||||
'risk_adjustment_b3': risk_adjustment_b3
|
||||
}
|
||||
Args:
|
||||
valuation_id (int): 估值的唯一标识符,用于关联所有计算步骤。
|
||||
input_data (Dict): 包含所有计算所需参数的字典,例如:
|
||||
{
|
||||
'highest_price': 340.0, # 市场风险
|
||||
'lowest_price': 300.0, # 市场风险
|
||||
'lawsuit_status': 10.0, # 法律风险
|
||||
'inheritor_ages': [100, 20, 5], # 传承风险
|
||||
...
|
||||
}
|
||||
|
||||
Returns:
|
||||
float: 计算得出的风险调整系数B3。
|
||||
|
||||
Raises:
|
||||
Exception: 在计算过程中遇到的任何异常都会被捕获、记录,并重新抛出。
|
||||
"""
|
||||
step = await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id,
|
||||
step_order=2.3,
|
||||
step_name="风险调整系数B3计算",
|
||||
step_description="开始计算风险调整系数B3,公式为:0.8 + 风险评分总和R × 0.4",
|
||||
input_params=input_data,
|
||||
status="in_progress"
|
||||
)
|
||||
)
|
||||
try:
|
||||
# 计算各项风险评分
|
||||
market_risk = self.calculate_market_risk(input_data["highest_price"], input_data["lowest_price"])
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.31, step_name="市场风险评分",
|
||||
output_result={'market_risk': market_risk}, status="completed"
|
||||
)
|
||||
)
|
||||
legal_risk = self.calculate_legal_risk(input_data["lawsuit_status"])
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.32, step_name="法律风险评分",
|
||||
output_result={'legal_risk': legal_risk}, status="completed"
|
||||
)
|
||||
)
|
||||
inheritance_risk = self.calculate_inheritance_risk(input_data["inheritor_ages"])
|
||||
await self.valuation_controller.create_calculation_step(
|
||||
ValuationCalculationStepCreate(
|
||||
valuation_id=valuation_id, step_order=2.33, step_name="传承风险评分",
|
||||
output_result={'inheritance_risk': inheritance_risk}, status="completed"
|
||||
)
|
||||
)
|
||||
|
||||
# 计算风险评分总和R
|
||||
risk_score_sum = self.calculate_risk_score_sum(market_risk, legal_risk, inheritance_risk)
|
||||
|
||||
# 计算风险调整系数B3
|
||||
risk_adjustment_b3 = self.calculate_risk_adjustment_b3(risk_score_sum)
|
||||
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "completed", "output_result": {'risk_adjustment_b3': risk_adjustment_b3}}
|
||||
)
|
||||
return risk_adjustment_b3
|
||||
except Exception as e:
|
||||
error_message = f"风险调整系数B3计算失败: {e}"
|
||||
await self.valuation_controller.update_calculation_step(
|
||||
step.id, {"status": "failed", "error_message": error_message}
|
||||
)
|
||||
raise
|
||||
|
||||
# 示例使用
|
||||
if __name__ == "__main__":
|
||||
|
||||
4551
app/utils/专利.json
4551
app/utils/专利.json
File diff suppressed because it is too large
Load Diff
384
demo_api.py
384
demo_api.py
@ -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()
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -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';
|
||||
3
node_modules/.vite/deps_temp_e96670e1/package.json
generated
vendored
3
node_modules/.vite/deps_temp_e96670e1/package.json
generated
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
34
package-lock.json
generated
34
package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"echarts": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@ -69,6 +69,11 @@ dependencies = [
|
||||
"websockets==14.1",
|
||||
"pyproject-toml>=0.1.0",
|
||||
"uvloop==0.21.0 ; sys_platform != 'win32'",
|
||||
"alibabacloud_dysmsapi20170525==4.1.2",
|
||||
"alibabacloud_tea_openapi==0.4.1",
|
||||
"alibabacloud_tea_util==0.3.14",
|
||||
"pytest==8.3.3",
|
||||
"pytest-html==4.1.1",
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
|
||||
@ -64,3 +64,7 @@ uvicorn==0.34.0
|
||||
uvloop==0.21.0
|
||||
watchfiles==1.0.4
|
||||
websockets==14.1
|
||||
alibabacloud_dysmsapi20170525==4.1.2
|
||||
alibabacloud_tea_openapi==0.4.1
|
||||
alibabacloud_tea_util==0.3.14
|
||||
pytest==8.3.3
|
||||
|
||||
2
run.py
2
run.py
@ -10,5 +10,5 @@ if __name__ == "__main__":
|
||||
] = '%(asctime)s - %(levelname)s - %(client_addr)s - "%(request_line)s" %(status_code)s'
|
||||
LOGGING_CONFIG["formatters"]["access"]["datefmt"] = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True, log_config=LOGGING_CONFIG)
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=9991, reload=True, log_config=LOGGING_CONFIG)
|
||||
|
||||
|
||||
213
scripts/admin_flow_test.py
Normal file
213
scripts/admin_flow_test.py
Normal 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
270
scripts/api_smoke_test.py
Normal 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
381
scripts/user_flow_test.py
Normal 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
|
||||
@ -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())
|
||||
Loading…
x
Reference in New Issue
Block a user