Compare commits
2 Commits
a0e857b115
...
1dd9a313e6
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dd9a313e6 | |||
| f536178428 |
98
.trae/documents/修复后台管理接口与统一上传接口的改造方案.md
Normal file
98
.trae/documents/修复后台管理接口与统一上传接口的改造方案.md
Normal file
@ -0,0 +1,98 @@
|
||||
## 背景与确认
|
||||
|
||||
* 备注为“用户维度”,按指示在 `app_user` 增加备注字段并贯通列表与配额调整流程。
|
||||
|
||||
* 操作记录需补充时间字段以便前端展示。
|
||||
|
||||
* 交易管理列表需支持“提交时间区间、状态、手机号、公司名称、公司税号”搜索。
|
||||
|
||||
* 上传接口从两个合并为一个,统一入参与返回结构。
|
||||
|
||||
## 改动总览
|
||||
|
||||
* 模型:为 `AppUser` 增加 `notes: CharField(256, null=True)`。
|
||||
|
||||
* 接口:
|
||||
|
||||
* `admin-App用户管理`:列表返回 `notes`;配额调整将 `remark` 同步写入 `AppUser.notes`;配额日志返回 `created_at`。
|
||||
|
||||
* `交易管理`:对公转账列表增加“提交时间区间”筛选。
|
||||
|
||||
* `上传`:新增统一上传接口 `/api/v1/upload/upload`(或 `/api/v1/upload`),老接口内部复用统一实现保留兼容。
|
||||
|
||||
* 迁移:使用现有 Aerich 流程生成并升级迁移。
|
||||
|
||||
## 详细实施
|
||||
|
||||
### 1. App 用户备注(用户维度)
|
||||
|
||||
* 文件:`app/models/user.py`
|
||||
|
||||
* 在 `AppUser` 增加字段 `notes = fields.CharField(max_length=256, null=True, description="备注")`。
|
||||
|
||||
* 文件:`app/api/v1/app_users/admin_manage.py`
|
||||
|
||||
* `/list`:返回 `notes = u.notes or ""`。
|
||||
|
||||
* `/quota`:接收 `remark` 后,除写入 `AppUserQuotaLog`
|
||||
|
||||
### 2. 操作记录时间字段
|
||||
|
||||
* 文件:`app/schemas/app_user.py`
|
||||
|
||||
* `AppUserQuotaLogOut` 增加 `created_at: str`。
|
||||
|
||||
* 文件:`app/api/v1/app_users/admin_manage.py`
|
||||
|
||||
* `/ {user_id}/quota-logs`:为每条日志填充 `created_at = r.created_at.isoformat()` 返回。
|
||||
|
||||
### 3. 交易管理列表搜索增强
|
||||
|
||||
* 文件:`app/controllers/invoice.py`
|
||||
|
||||
* `list_receipts(...)`:增加对 `PaymentReceipt.created_at` 的区间筛选:
|
||||
|
||||
* 支持 `created_at`(数组,毫秒时间戳)或 `submitted_start/submitted_end`(ISO 或毫秒)。
|
||||
|
||||
* 文件:`app/api/v1/transactions/transactions.py`
|
||||
|
||||
* `list_receipts`:路由签名增加上述可选查询参数并传入控制器。
|
||||
|
||||
### 4. 上传接口统一
|
||||
|
||||
* 文件:`app/controllers/upload.py`
|
||||
|
||||
* 新增 `upload_any(file: UploadFile)`:
|
||||
|
||||
* `image/*` 保存到 `static/images`;其他受支持类型保存到 `static/files`。
|
||||
|
||||
* 返回 `{ url, filename, content_type }`。
|
||||
|
||||
* 文件:`app/api/v1/upload/upload.py`
|
||||
|
||||
* 新增 `/upload`(或 `/upload/upload` 按路由前缀安排):统一入口;
|
||||
|
||||
* 旧 `/image`、`/file` 内部调用 `upload_any` 保持兼容。
|
||||
|
||||
* 文件:`app/schemas/upload.py`
|
||||
|
||||
* 若需要,补充统一响应模型,或复用 `FileUploadResponse`(包含 `content_type`)。
|
||||
|
||||
## 迁移与兼容
|
||||
|
||||
* Aerich:`init_db` 已集成迁移流程,生成迁移后执行 `upgrade`,自动创建 `app_user.notes` 列。
|
||||
|
||||
* 旧接口兼容:上传旧路径保留;交易列表与日志返回仅新增字段,不影响既有消费逻辑。
|
||||
|
||||
## 验证与测试
|
||||
|
||||
* 备注更新:调用 `/app-user-admin/quota` 传 `remark`,再查 `/app-user-admin/list` 验证 `notes`;查 `/app-user-admin/{id}/quota-logs` 验证 `created_at` 存在。
|
||||
|
||||
* 交易筛选:造两条不同日期的凭证,分别用时间区间查询命中与不命中。
|
||||
|
||||
* 上传统一:上传 PNG 与 PDF 验证保存路径与返回结构;旧接口路由到统一实现成功。
|
||||
|
||||
## 交付
|
||||
|
||||
* 提交代码与迁移;更新接口文档的路由说明与字段变化;前端无需大改即可使用新返回的 `notes` 与日志 `created_at`,交易列表按“凭证时间”筛选可用。
|
||||
|
||||
63
.trae/documents/准备估值测算 API 提交数据.md
Normal file
63
.trae/documents/准备估值测算 API 提交数据.md
Normal file
@ -0,0 +1,63 @@
|
||||
## 接口定位
|
||||
- 用户端提交路径:`POST /v1/app-valuations/`(处理函数在 `app/api/v1/app_valuations/app_valuations.py:234`)
|
||||
- 后台计算任务:`_perform_valuation_calculation`(`app/api/v1/app_valuations/app_valuations.py:40`)
|
||||
- 计算输入映射:
|
||||
- B1 提取:`_extract_calculation_params_b1`(`app/api/v1/app_valuations/app_valuations.py:325`)
|
||||
- B2 提取:`_extract_calculation_params_b2`(`app/api/v1/app_valuations/app_valuations.py:430`)
|
||||
- B3 提取:`_extract_calculation_params_b3`(`app/api/v1/app_valuations/app_valuations.py:517`)
|
||||
- C 提取:`_extract_calculation_params_c`(`app/api/v1/app_valuations/app_valuations.py:532`)
|
||||
- 请求体模型:`UserValuationCreate` 基于 `ValuationAssessmentBase`(`app/schemas/valuation.py:7`、`app/schemas/valuation.py:153`)
|
||||
|
||||
## 必填/推荐字段
|
||||
- 基础:`asset_name`、`institution`、`industry`
|
||||
- 财务(B1):`three_year_income`、`rd_investment`、`annual_revenue`、`funding_status`、`application_coverage`、`implementation_stage`、`platform_accounts(douyin.likes/comments/shares)`、`sales_volume`、`link_views`
|
||||
- 文化(B2):`inheritor_level`、`offline_activities`、`cooperation_depth`、`historical_evidence`
|
||||
- 风险(B3):`price_fluctuation`、`inheritor_age_count`
|
||||
- 市场(C):`scarcity_level` 或 `circulation`、`market_activity_time`
|
||||
- 质押率辅助:`heritage_asset_level`(用于动态质押率)
|
||||
|
||||
## 字段规范化
|
||||
- 数组字段统一为数值数组:`three_year_income`、`price_fluctuation`
|
||||
- 金额/计数按模型定义传字符串(系统内有安全转换):`annual_revenue`、`rd_investment`、`sales_volume`、`link_views`、`offline_activities`
|
||||
- 平台账号字段示例:`{"douyin": {"account": "...", "likes": "...", "comments": "...", "shares": "..."}}`
|
||||
- 覆盖范围枚举:`全球覆盖/全国覆盖/区域覆盖`(未提供时默认全国覆盖,参见 `app/api/v1/app_valuations/app_valuations.py:343-347`)
|
||||
|
||||
## 提交载荷示例(基于“马王堆”数据)
|
||||
```json
|
||||
{
|
||||
"asset_name": "马王堆",
|
||||
"institution": "成都文化产权交易所",
|
||||
"industry": "文化艺术业",
|
||||
"annual_revenue": "10000",
|
||||
"rd_investment": "6000",
|
||||
"three_year_income": [8000, 9000, 9500],
|
||||
"funding_status": "省级资助",
|
||||
"inheritor_level": "省级传承人",
|
||||
"inheritor_age_count": [200, 68, 20],
|
||||
"heritage_asset_level": "纳入《国家文化数字化战略清单》",
|
||||
"historical_evidence": {"artifacts": 58, "ancient_literature": 789, "inheritor_testimony": 100},
|
||||
"implementation_stage": "成熟应用",
|
||||
"application_coverage": "全国覆盖",
|
||||
"cooperation_depth": "0",
|
||||
"offline_activities": "20",
|
||||
"platform_accounts": {
|
||||
"douyin": {"account": "成都文交所", "likes": "500000", "comments": "89222", "shares": "97412"}
|
||||
},
|
||||
"sales_volume": "60000",
|
||||
"link_views": "350000",
|
||||
"circulation": "3",
|
||||
"scarcity_level": "流通:总发行份数 >1000份,或二级市场流通率 ≥ 5%",
|
||||
"market_activity_time": "近一周",
|
||||
"monthly_transaction_amount": "月交易额>100万<500万",
|
||||
"price_fluctuation": [402, 445]
|
||||
}
|
||||
```
|
||||
|
||||
## 验证步骤(确认后执行)
|
||||
- 发送上述 JSON 到 `POST /v1/app-valuations/`,观察返回 `status`、`id`
|
||||
- 通过 `GET /v1/app-valuations/{id}` 查看写入的 `calculation_input` 与 `model_value_b/market_value_c/final_value_ab/dynamic_pledge_rate`
|
||||
- 若需批量提交,按相同字段规范为每条记录生成载荷并调用接口(可提供脚本)
|
||||
|
||||
## 可能的扩展
|
||||
- 无抖音 `views` 字段时已用 `link_views`,如需更精确的 C 值,可补充 `views`(参见 `app/api/v1/app_valuations/app_valuations.py:566-585`)
|
||||
- `application_coverage` 为非枚举值时将按默认 7 分处理(见 `app/api/v1/app_valuations/app_valuations.py:343-347`)
|
||||
60
.trae/documents/初始化交易记录数据并在后台可见的实施方案.md
Normal file
60
.trae/documents/初始化交易记录数据并在后台可见的实施方案.md
Normal file
@ -0,0 +1,60 @@
|
||||
## 目标
|
||||
- 创建若干“交易记录”(对公转账付款凭证)测试数据,并确保后台管理可以正常查看、筛选与分页。
|
||||
- 全流程可回滚,严格隔离开发与生产数据。
|
||||
|
||||
## 关键假设
|
||||
- 交易记录以 `PaymentReceipt`(关联 `Invoice`)为核心:后端模型位于 `app/models/invoice.py`。
|
||||
- 后端已提供发票与凭证相关 API:
|
||||
- 发票:`GET/POST /api/v1/invoice/*`(`app/api/v1/invoice/invoice.py`)
|
||||
- 凭证:`POST /api/v1/invoice/{id}/receipt`、列表 `GET /api/v1/transactions/receipts`(`app/api/v1/transactions/transactions.py`)。
|
||||
- 现有后台页已覆盖发票列表 `web/src/views/transaction/invoice/index.vue`,尚无“凭证专属列表”页面。
|
||||
|
||||
## 数据建模与来源
|
||||
- `Invoice`:发票记录(开票状态、类型、抬头信息、公司税号、银行账户等)。
|
||||
- `PaymentReceipt`:对公转账付款凭证(字段含 `invoice_id`、`url`、`note`、`verified`、`created_at`)。
|
||||
- 为保证后台可见,需同时插入可查询的 `Invoice` 与其关联的 `PaymentReceipt`。
|
||||
|
||||
## 实施路径
|
||||
### 方案 A:通过现有 API 批量生成(零代码改动,首选)
|
||||
- 步骤:
|
||||
- 调用 `POST /api/v1/invoice/create` 批量创建 5–10 条覆盖不同类型/状态的发票(含公司名、税号、银行账户等)。
|
||||
- 对每条发票调用 `POST /api/v1/invoice/{id}/receipt` 上传/登记 1–2 条付款凭证(`file_url` 或 `url`、`note`、`verified`)。
|
||||
- 可选:通过 `POST /api/v1/transactions/send-email` 生成邮件日志,验证通知链路。
|
||||
- 优势:不改动代码,快速、低风险;与现有权限和审计链路一致。
|
||||
|
||||
### 方案 B:后端种子脚本(仅开发环境,便于重复初始化)
|
||||
- 在 `app/core/init_app.py` 新增 `init_demo_transactions()`:
|
||||
- 仅在开发环境执行(如 `ENV=dev`);避免污染生产。
|
||||
- 批量创建 `Invoice` 测试数据,再为每条 `Invoice` 创建若干 `PaymentReceipt`。
|
||||
- 为可识别性,在 `note` 中加入 `DEMO` 标签或新增布尔字段(若允许)。
|
||||
- 将该方法纳入现有 `init_data()` 的“开发模式”分支;保留一键清理逻辑(删除 `DEMO` 标记数据)。
|
||||
|
||||
## 前端后台展示
|
||||
- 新增后台页“交易记录”:`web/src/views/transaction/receipts/index.vue`(Naive UI):
|
||||
- 数据源:`GET /api/v1/transactions/receipts`;支持分页与筛选(手机号、公司名、税号、状态、类型等)。
|
||||
- 列:凭证时间、公司名称、税号、转账备注(`note`)、审核状态(`verified`)、关联发票 ID/类型,查看详情。
|
||||
- 行为:查看详情(`GET /api/v1/transactions/receipts/{id}`),跳转到发票详情。
|
||||
- 菜单与权限:
|
||||
- 在后端 `init_menus()` 增加“交易记录”菜单,并为管理员角色在 `init_roles()` 授权;在 `init_apis()` 注册 `receipts` 相关 API。
|
||||
- 备选轻改:在 `invoice/index.vue` 增加“凭证数/链接”列与详情入口,先实现可见性,后续再拆分独立列表页。
|
||||
|
||||
## 验证步骤
|
||||
- 数据验证:
|
||||
- 通过 API 查询:`GET /api/v1/invoice/list` 与 `GET /api/v1/transactions/receipts`,确认条数、筛选与分页正确。
|
||||
- 随机抽样验证 `PaymentReceipt` 与对应 `Invoice` 关联完整、字段齐备。
|
||||
- 前端验证:
|
||||
- 后台页加载正常、列渲染与筛选可用;详情跳转与状态标签正确。
|
||||
- 安全验证:
|
||||
- 在生产环境禁用种子逻辑;标记测试数据,提供清理。
|
||||
|
||||
## 回滚与清理
|
||||
- 提供清理脚本/接口:按 `DEMO` 标记或 ID 范围批量删除测试发票与凭证。
|
||||
- 菜单与权限变更可回退至原状态(移除菜单、撤销授权)。
|
||||
|
||||
## 交付物
|
||||
- (A)一组 API 请求示例(可直接运行)生成测试交易与凭证。
|
||||
- (B)可选:开发环境种子函数与清理脚本。
|
||||
- (C)前端“交易记录”后台页(或在发票页增加凭证列的最小改动)。
|
||||
|
||||
## 备注
|
||||
- 编码时为新增函数与接口补充函数级注释(功能、参数、返回值说明),遵循现有代码风格与安全规范。
|
||||
25
.trae/documents/新增发送邮件测试脚本(Shell).md
Normal file
25
.trae/documents/新增发送邮件测试脚本(Shell).md
Normal file
@ -0,0 +1,25 @@
|
||||
## 目标
|
||||
- 提供一个可在本机直接运行的 Shell 脚本,测试 `POST /api/v1/transactions/send-email`,支持本地文件上传与远程文件 URL,两种模式均可验证。
|
||||
|
||||
## 实现方式
|
||||
- 新增 `scripts/send_email_test.sh`:
|
||||
- 参数:
|
||||
- `-t <token>` 后台 token(必填)
|
||||
- `-e <email>` 收件人邮箱(必填)
|
||||
- `-s <subject>` 邮件主题(可选)
|
||||
- `-b <body>` 邮件正文(必填)
|
||||
- `-f <file_path>` 本地附件路径(可选)
|
||||
- `-u <file_url>` 远程附件 URL(可选,与 `-f` 互斥)
|
||||
- `-a <base_api>` 基础地址,默认 `http://127.0.0.1:9999/api/v1`
|
||||
- 逻辑:
|
||||
- 若提供 `-f`,使用 `curl -F` 构造 `multipart/form-data` 表单项:`email/subject/body/file`
|
||||
- 否则以 JSON 发送:`email/subject/body`,可选 `file_url`
|
||||
- 输出:直接打印服务端返回的 JSON
|
||||
|
||||
## 验证
|
||||
- 无附件:`./scripts/send_email_test.sh -t <token> -e zfc9393@163.com -s 测试 -b 测试代码`
|
||||
- 本地附件:`./scripts/send_email_test.sh -t <token> -e zfc9393@163.com -s 测试 -b 测试代码 -f ./demo.pdf`
|
||||
- 远程附件:`./scripts/send_email_test.sh -t <token> -e zfc9393@163.com -s 测试 -b 测试代码 -u https://example.com/demo.pdf`
|
||||
|
||||
## 安全
|
||||
- 不写入任何敏感信息到仓库;脚本仅通过命令行参数接收 token 与内容。
|
||||
101
.trae/documents/蜀锦估值计算流程核对.md
Normal file
101
.trae/documents/蜀锦估值计算流程核对.md
Normal file
@ -0,0 +1,101 @@
|
||||
目标
|
||||
|
||||
* 在 `蜀锦估值计算流程核对.md` 中补充并修复:历史传承度 HI=18 的明确记录,以及由此引发的 B22、B2、模型估值 B、最终估值 A 的新数值与计算公式。保留现有内容,同时在文末新增“修正:HI=18 后的计算”分节,按“参数 | 步骤 | 计算结果 | 计算公式”的统一格式展示。
|
||||
|
||||
修改要点
|
||||
|
||||
* 新增分节:`## 修正:历史传承度 HI=18 后的计算`
|
||||
|
||||
* 插入子段落:
|
||||
|
||||
1. 历史传承度 HI(参数/步骤/计算结果/计算公式)→ HI=18
|
||||
2. 纹样基因 B22(使用 SC=1.5, H=9, HI=18)→ B22=810
|
||||
3. 文化价值 B2(B21=9.37804, B22=810)→ B2=38.026824
|
||||
4. 模型估值 B(B1≈519.8, B2=38.026824, B3=0.92)→ B≈346.9 万元
|
||||
5. 市场估值 C(复核不变)→ C=9452.0 万元
|
||||
6. 最终估值 A(使用新 B 与 C)→ A≈3078.43 万元
|
||||
|
||||
* 每一段落严格附加“计算公式”行,确保审阅可落地。
|
||||
|
||||
输出格式与一致性
|
||||
|
||||
* 所有行使用中文标签与反引号包裹关键数值/公式。
|
||||
|
||||
* 不删除原有章节,仅在文末追加修正;与日志与现有参数保持一致。
|
||||
|
||||
执行后期望
|
||||
|
||||
* 文档清晰体现 HI 修正后的计算链路与结果,审阅时可直接对照参数与公式核验。
|
||||
|
||||
## 修正:历史传承度 HI=18 后的计算
|
||||
|
||||
**历史传承度 HI**
|
||||
|
||||
* 参数: `historical_evidence={"artifacts":2,"ancient_literature":5,"inheritor_testimony":5,"modern_research":6}`
|
||||
|
||||
* 步骤: 解析为对象→对各项做安全数值化→求和
|
||||
|
||||
* 计算结果: `HI=18`
|
||||
|
||||
* 计算公式: `HI = sum(safe_float(v) for v in historical_evidence)`
|
||||
|
||||
**纹样基因 B22(文化 B2 子项)**
|
||||
|
||||
* 参数: `SC=1.5`,`H=9`,`HI=18`
|
||||
|
||||
* 步骤: 复杂度与熵线性合成×历史度×10
|
||||
|
||||
* 计算结果: `B22=810`
|
||||
|
||||
* 计算公式: `B22 = (SC×0.6 + H×0.4) × HI × 10`(`(1.5×0.6 + 9×0.4) × 18 × 10 = 4.5 × 18 × 10 = 810`)
|
||||
|
||||
**文化价值 B2(合成)**
|
||||
|
||||
* 参数: `B21=9.37804`,`B22=810`
|
||||
|
||||
* 步骤: `B2 = B21×0.6 + (B22/10)×0.4`
|
||||
|
||||
* 计算结果: `B2=38.026824`
|
||||
|
||||
* 计算公式: `B2 = B21×0.6 + (B22/10)×0.4`(`9.37804×0.6 + (810/10)×0.4 = 5.626824 + 32.4 = 38.026824`)
|
||||
|
||||
**模型估值 B**
|
||||
|
||||
* 参数: `B1≈519.8`,`B2=38.026824`,`B3=0.92`
|
||||
|
||||
* 步骤: 经济与文化加权后乘风险系数
|
||||
|
||||
* 计算结果: `B≈346.9` 万元
|
||||
|
||||
* 计算公式: `B = (B1×0.7 + B2×0.3) × B3`(`(519.8×0.7 + 38.026824×0.3) × 0.92 ≈ (363.86 + 11.408) × 0.92 ≈ 346.9`)
|
||||
|
||||
**市场估值 C**
|
||||
|
||||
* 参数: `C1=2780`,`C2=2.0`,`C3=1.7`,`C4=1.0`
|
||||
|
||||
* 步骤: 乘法聚合 `C = C1 × C2 × C3 × C4`
|
||||
|
||||
* 计算结果: `C=9452.0` 万元
|
||||
|
||||
* 计算公式: `C = C1 × C2 × C3 × C4`
|
||||
|
||||
**C1 市场竞价(平均交易价,C 子项)**
|
||||
|
||||
* 参数: `bids=[3980,1580,2780]`;`weighted_average_price=None`;`expert_valuations=[]`
|
||||
|
||||
* 步骤: 平均价缺失→排序出价→取中位数
|
||||
|
||||
* 计算结果: `C1=2780` 元
|
||||
|
||||
* 计算公式: `C1 = median(bids)`;奇数 `n` → `sorted_bids[n//2]`;偶数 `n` → `(sorted_bids[n//2-1] + sorted_bids[n//2]) / 2`
|
||||
|
||||
**最终估值 A**
|
||||
|
||||
* 参数: `B≈346.9`,`C=9452.0`
|
||||
|
||||
* 步骤: 固定权重合成
|
||||
|
||||
* 计算结果: `A≈3078.43` 万元
|
||||
|
||||
* 计算公式: `A = 0.7×B + 0.3×C`(`0.7×346.9 + 0.3×9452 ≈ 242.83 + 2835.6 ≈ 3078.43`)
|
||||
|
||||
23
.trae/documents/说明发票状态含义与拒绝原因说明.md
Normal file
23
.trae/documents/说明发票状态含义与拒绝原因说明.md
Normal file
@ -0,0 +1,23 @@
|
||||
## 目标
|
||||
- 以简明清晰的中文,给出发票状态(pending、invoiced、rejected、refunded)的标准释义。
|
||||
- 重点补充“rejected(已拒绝)”的常见触发原因与处理建议,便于运营与审核同口径使用。
|
||||
|
||||
## 来源与现状
|
||||
- 状态字段来源:`app/models/invoice.py:32`(`pending|invoiced|rejected|refunded`)。
|
||||
- 后台页面状态标签:`web/src/views/transaction/invoice/index.vue:82-91`。
|
||||
|
||||
## 输出内容
|
||||
- 提供四个状态的含义与可执行动作:
|
||||
- pending:未开票,等待处理;可进行开票或退款;需核验资料。
|
||||
- invoiced:已开票,发票已生成;可查看重发邮件等;不可再提交同笔开票。
|
||||
- rejected:已拒绝,审核未通过;说明常见拒绝原因与后续处理方式。
|
||||
- refunded:已退款,已取消并完成退款;如需再次开票需走新流程。
|
||||
- “rejected”典型原因分类与处理建议:
|
||||
- 企业信息不一致/缺失;税号无效或格式不符;银行账户异常;发票类型不合规;重复或异常申请;付款凭证无法核验等。
|
||||
- 建议:更正信息后重新提交;提供有效凭证;必要时走退款流程。
|
||||
|
||||
## 可选改进(待确认)
|
||||
- 在后台页面增加“拒绝原因”提示(若未来增加原因字段),或在详情中补充统一口径的原因文案与建议。
|
||||
|
||||
## 交付方式
|
||||
- 立即以文本形式给出状态释义与“rejected”原因说明,便于直接使用。
|
||||
70
.trae/documents/调整 Nginx 上传大小与超时以支持大文件.md
Normal file
70
.trae/documents/调整 Nginx 上传大小与超时以支持大文件.md
Normal file
@ -0,0 +1,70 @@
|
||||
## 目标
|
||||
- 提升 Nginx 代理下的上传体积与超时,解决大文件上传被限制的问题(413/超时)。
|
||||
|
||||
## 现状
|
||||
- 当前配置文件:`deploy/web.conf` 使用 `location ^~ /api/` 反向代理到后端 `http://127.0.0.1:9999`,未设置上传大小和超时。
|
||||
|
||||
## 修改内容
|
||||
- 在 `server {}` 块中增加统一的体积限制。
|
||||
- 在 `location ^~ /api/` 中增加长超时与关闭请求缓冲(提升大文件上传稳定性)。
|
||||
|
||||
## 具体改动(覆盖式替换关键片段)
|
||||
- 在 `server_name localhost;` 后增加:
|
||||
```
|
||||
client_max_body_size 50m;
|
||||
```
|
||||
- 在 `location ^~ /api/ { ... }` 内追加:
|
||||
```
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_request_buffering off;
|
||||
```
|
||||
|
||||
## 变更后的完整结构示例
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
client_max_body_size 50m;
|
||||
|
||||
location = /docs {
|
||||
proxy_pass http://127.0.0.1:9999/docs;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location = /redoc {
|
||||
proxy_pass http://127.0.0.1:9999/redoc;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location = /openapi.json {
|
||||
proxy_pass http://127.0.0.1:9999/openapi.json;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location / {
|
||||
root /opt/vue-fastapi-admin/web/dist;
|
||||
index index.html index.htm;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
location ^~ /api/ {
|
||||
proxy_pass http://127.0.0.1:9999;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 发布与验证
|
||||
- 检查配置:`nginx -t`
|
||||
- 重新加载:`nginx -s reload`
|
||||
- 测试上传:使用 30–40MB 文件调用 `POST /api/v1/upload/file`,确认不再出现 413 或上传超时;同时测试 `POST /api/v1/upload/image` 大图。
|
||||
|
||||
## 备注
|
||||
- 如需更大体积可将 `client_max_body_size` 调整为 `100m` 或更高;若上层云负载均衡也有限制,需要同步放宽。
|
||||
- 后续可在应用层增加最大体积限制与提示,避免无界上传占用过多资源。
|
||||
36
.trae/documents/配置阿里企业邮箱SMTP并验证发送.md
Normal file
36
.trae/documents/配置阿里企业邮箱SMTP并验证发送.md
Normal file
@ -0,0 +1,36 @@
|
||||
## 目标
|
||||
- 配置并启用阿里企业邮箱 SMTP,完成服务端发送邮件功能验证(含附件/远程文件)。
|
||||
|
||||
## 所需配置
|
||||
- 配置项位置:`app/settings/config.py:103-109`(`SMTP_HOST/SMTP_PORT/SMTP_USERNAME/SMTP_PASSWORD/SMTP_TLS/SMTP_FROM`)
|
||||
- 建议取值(不要写入仓库,使用环境变量注入):
|
||||
- `SMTP_HOST=smtp.qiye.aliyun.com`
|
||||
- `SMTP_PORT=465`(SSL 直连)
|
||||
- `SMTP_TLS=false`(465 对应 SSL;如用 587 则改为 `SMTP_TLS=true` 并 `SMTP_PORT=587`)
|
||||
- `SMTP_FROM=value@cdcee.net`
|
||||
- `SMTP_USERNAME=value@cdcee.net`
|
||||
- `SMTP_PASSWORD=<授权码>`(你提供的授权码)
|
||||
- 安全要求:不将授权码写入代码或仓库;仅通过环境变量或部署系统秘密管理器设置。
|
||||
|
||||
## 实现检查
|
||||
- 发送实现:`app/services/email_client.py:12-49`
|
||||
- 当 `SMTP_TLS=false` 时使用 `SMTP_SSL(host, 465)`;为 `true` 时使用 `SMTP(host, 587).starttls()`
|
||||
- 发送成功返回 `{status: 'OK'}`,失败返回 `{status: 'FAIL', error}`
|
||||
- 接口:`POST /api/v1/transactions/send-email`(`app/api/v1/transactions/transactions.py:62-104`)
|
||||
- 支持直接上传附件(`file`)或通过 `file_url` 拉取远程文件,记录日志到 `email_send_log`
|
||||
|
||||
## 验证步骤
|
||||
1. 获取后台 token:`POST /api/v1/base/access_token`(admin 账号)
|
||||
2. 发送纯文本邮件(无附件):
|
||||
- `POST /api/v1/transactions/send-email`,JSON:`{email, subject, body}`,携带 `token` 头
|
||||
3. 发送带附件(file_url):
|
||||
- JSON:`{email, subject, body, file_url: 'https://...'}`;或使用 `multipart/form-data` 上传 `file`
|
||||
4. 结果期望:返回 `{"status":"OK","log_id":...}`;失败时查看错误内容并修正配置。
|
||||
|
||||
## 可选增强
|
||||
- 为 `email_client.send` 增加更明确的错误分类与超时提示(保留现有结构)。
|
||||
- 提供健康检查端点:尝试建立 SMTP 连接并返回诊断信息(仅管理员角色可访问)。
|
||||
|
||||
## 回滚与安全
|
||||
- 变更仅在环境变量层面,可随时回滚;不改代码,不提交授权码。
|
||||
- 若切换至 587,需同时改为 `SMTP_TLS=true` 并确保上游网络允许 STARTTLS。
|
||||
29
aaa.json
Normal file
29
aaa.json
Normal file
@ -0,0 +1,29 @@
|
||||
2025-11-17 18:13:15.766 | INFO | app.api.v1.app_valuations.app_valuations:calculate_valuation:284 - valuation.task_queued user_id=30 asset_name=蜀锦 industry=纺织业
|
||||
2025-11-17 18:13:15.768 | INFO | app.api.v1.app_valuations.app_valuations:_perform_valuation_calculation:44 - valuation.calc_start user_id=30 asset_name=蜀锦 industry=纺织业
|
||||
2025-11-17 18:13:15 - INFO - 14.145.4.28:0 - "POST /api/v1/app-valuations/ HTTP/1.0" 200 OK
|
||||
2025-11-17 18:13:16,048 - INFO - API请求成功: dajiala.web_search
|
||||
2025-11-17 18:13:16,049 - ERROR - 微信指数API返回错误: {'code': 20001, 'msg': '金额不足,请充值', 'data': ''}
|
||||
2025-11-17 18:13:16,049 - WARNING - 没有指数数据用于计算平均值
|
||||
2025-11-17 18:13:16.049 | INFO | app.api.v1.app_valuations.app_valuations:_extract_calculation_params_b1:336 - 资产 '蜀锦' 的微信指数近30天平均值: 0.0
|
||||
2025-11-17 18:13:16,051 - INFO - 行业 纺织业 S2计算: S2=2200.0
|
||||
{'orderNo': '202511171813162420295', 'rc': '0001', 'msg': '查询成功,无数据'}
|
||||
2025-11-17 18:13:16,827 - INFO - API请求成功: chinaz.judgement
|
||||
{'orderNo': '202511171813169260297', 'rc': '0002', 'msg': '查询企业不存在,请检查后再试'}
|
||||
2025-11-17 18:13:17,428 - INFO - API请求成功: chinaz.patent
|
||||
2025-11-17 18:13:17.428 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:80 - final_value_a.calculation_start input_data_keys=['model_data', 'market_data'] model_data_keys=['economic_data', 'cultural_data', 'risky_data'] market_data_keys=['weighted_average_price', 'manual_bids', 'expert_valuations', 'daily_browse_volume', 'collection_count', 'issuance_level', 'recent_market_activity']
|
||||
2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:89 - final_value_a.economic_data 经济价值B1参数: 近三年机构收益=[169.0, 169.0, 169.0] 专利分=3.0 普及地域分=7.0 侵权分=0.0 创新投入比=18.93491124260355 ESG分=5.0 专利使用量=0.0 行业修正系数=-0.5
|
||||
2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:101 - final_value_a.cultural_data 文化价值B2参数: 传承人等级系数=0.7 跨境深度=0.3 线下教学次数=50.0 抖音浏览量=67000.0 快手浏览量=0 哔哩哔哩浏览量=0 结构复杂度=1.5 归一化信息熵=9 历史传承度=0.0
|
||||
2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:114 - final_value_a.risky_data 风险调整B3参数: 最高价=3980.0 最低价=1580.0 诉讼状态=0.0 传承人年龄=[0, 0, 2]
|
||||
2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:122 - final_value_a.market_data 市场估值C参数: 平均交易价=None 手动出价=[3980.0, 1580.0, 2780.0] 专家估值=[] 日浏览量=296000.0 收藏数量=67000 发行等级=限量:总发行份数 ≤100份 最近市场活动=近一周
|
||||
2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:132 - final_value_a.calculating_model_value_b 开始计算模型估值B
|
||||
2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:142 - final_value_a.model_value_b_calculated 模型估值B计算完成: 模型估值B=336.37180882339413万元 耗时=0ms 返回字段=['economic_value_b1', 'cultural_value_b2', 'risk_value_b3', 'model_value_b']
|
||||
2025-11-17 18:13:17.429 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:152 - final_value_a.calculating_market_value_c 开始计算市场估值C
|
||||
浏览热度分:
|
||||
2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:162 - final_value_a.market_value_c_calculated 市场估值C计算完成: 市场估值C=9452.0万元 耗时=0ms 返回字段=['market_bidding_c1', 'heat_coefficient_c2', 'scarcity_multiplier_c3', 'temporal_decay_c4', 'market_value_c']
|
||||
2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:172 - final_value_a.calculating_final_value_a 开始计算最终估值A: 模型估值B=336.37180882339413万元 市场估值C=9452.0万元
|
||||
2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_final_value_a:50 - final_value_a.calculate_final_value_a 开始计算最终估值A: 模型估值B=336.37180882339413万元 市场估值C=9452.0万元
|
||||
2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_final_value_a:57 - final_value_a.weighted_values 加权计算: 模型估值B加权值=235.46026617637588万元(权重0.7) 市场估值C加权值=2835.6万元(权重0.3)
|
||||
2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_final_value_a:62 - final_value_a.final_calculation 最终估值A计算: 模型估值B=336.37180882339413万元 市场估值C=9452.0万元 模型加权值=235.46026617637588万元 市场加权值=2835.6万元 最终估值AB=3071.060266176376万元
|
||||
2025-11-17 18:13:17.430 | INFO | app.utils.calculation_engine.final_value_ab.final_value_a:calculate_complete_final_value_a:183 - final_value_a.calculation_completed 最终估值A计算完成: 最终估值AB=3071.060266176376万元 模型估值B=336.37180882339413万元 市场估值C=9452.0万元 总耗时=1ms 模型计算耗时=0ms 市场计算耗时=0ms
|
||||
2025-11-17 18:13:17.430 | INFO | app.api.v1.app_valuations.app_valuations:_perform_valuation_calculation:160 - valuation.calc_done user_id=30 duration_ms=1662 model_value_b=336.37180882339413 market_value_c=9452.0 final_value_ab=3071.060266176376
|
||||
Traceback (most recent call last):
|
||||
@ -32,7 +32,7 @@ async def list_app_users(
|
||||
"phone": u.phone,
|
||||
"wechat": u.alias,
|
||||
"created_at": u.created_at.isoformat() if u.created_at else "",
|
||||
"notes": "",
|
||||
"notes": getattr(u, "notes", "") or "",
|
||||
"remaining_count": int(getattr(u, "remaining_quota", 0) or 0),
|
||||
"user_type": None,
|
||||
})
|
||||
@ -52,6 +52,9 @@ async def update_quota(payload: AppUserQuotaUpdateSchema, operator=Depends(AuthC
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if payload.remark is not None:
|
||||
user.notes = payload.remark
|
||||
await user.save()
|
||||
return Success(data={"user_id": user.id, "remaining_quota": user.remaining_quota}, msg="调整成功")
|
||||
|
||||
|
||||
@ -70,6 +73,7 @@ async def quota_logs(user_id: int, page: int = Query(1, ge=1), page_size: int =
|
||||
after_count=r.after_count,
|
||||
op_type=r.op_type,
|
||||
remark=r.remark,
|
||||
created_at=r.created_at.isoformat() if r.created_at else "",
|
||||
) for r in rows
|
||||
]
|
||||
data_items = [m.model_dump() for m in models]
|
||||
|
||||
@ -58,9 +58,12 @@ async def send_code(payload: SendCodeRequest) -> SendResponse:
|
||||
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)
|
||||
otp = store.generate_code()
|
||||
store.set_code(payload.phone, otp)
|
||||
from app.settings import settings
|
||||
if settings.SMS_DEBUG_LOG_CODE:
|
||||
logger.info("sms.code generated phone={} code={}", payload.phone, otp)
|
||||
res = sms_client.send_code(payload.phone, otp)
|
||||
code = res.get("Code") or res.get("ResponseCode")
|
||||
rid = res.get("RequestId") or res.get("MessageId")
|
||||
if code == "OK":
|
||||
@ -194,4 +197,4 @@ async def sms_login(payload: SMSLoginRequest) -> SMSLoginResponse:
|
||||
return SMSLoginResponse(user=user_info, token=token_out)
|
||||
class VerifyCodeRequest(BaseModel):
|
||||
phone: str = Field(...)
|
||||
code: str = Field(...)
|
||||
code: str = Field(...)
|
||||
|
||||
@ -24,6 +24,9 @@ async def list_receipts(
|
||||
status: Optional[str] = Query(None),
|
||||
ticket_type: Optional[str] = Query(None),
|
||||
invoice_type: Optional[str] = Query(None),
|
||||
created_at: Optional[list[int]] = Query(None),
|
||||
submitted_start: Optional[str] = Query(None),
|
||||
submitted_end: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100),
|
||||
):
|
||||
@ -40,6 +43,9 @@ async def list_receipts(
|
||||
status=status,
|
||||
ticket_type=ticket_type,
|
||||
invoice_type=invoice_type,
|
||||
created_at=created_at,
|
||||
submitted_start=submitted_start,
|
||||
submitted_end=submitted_end,
|
||||
)
|
||||
return SuccessExtra(
|
||||
data=result["items"],
|
||||
@ -101,4 +107,25 @@ async def send_email(data: SendEmailRequest, file: Optional[UploadFile] = File(N
|
||||
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 "发送失败")
|
||||
return Success(data={"status": status, "log_id": log.id, "error": error}, msg="发送成功" if status == "OK" else "发送失败")
|
||||
|
||||
|
||||
@transactions_router.get("/smtp-config", summary="SMTP配置状态", response_model=BasicResponse[dict])
|
||||
async def smtp_config_status():
|
||||
configured = all([
|
||||
settings.SMTP_HOST,
|
||||
settings.SMTP_PORT,
|
||||
settings.SMTP_FROM,
|
||||
settings.SMTP_USERNAME,
|
||||
settings.SMTP_PASSWORD,
|
||||
])
|
||||
data = {
|
||||
"host": bool(settings.SMTP_HOST),
|
||||
"port": bool(settings.SMTP_PORT),
|
||||
"from": bool(settings.SMTP_FROM),
|
||||
"username": bool(settings.SMTP_USERNAME),
|
||||
"password": bool(settings.SMTP_PASSWORD),
|
||||
"tls": settings.SMTP_TLS,
|
||||
"configured": configured,
|
||||
}
|
||||
return Success(data=data, msg="OK")
|
||||
@ -15,4 +15,8 @@ async def upload_image(file: UploadFile = File(...)) -> ImageUploadResponse:
|
||||
|
||||
@router.post("/file", response_model=FileUploadResponse, summary="上传文件")
|
||||
async def upload_file(file: UploadFile = File(...)) -> FileUploadResponse:
|
||||
return await UploadController.upload_file(file)
|
||||
return await UploadController.upload_file(file)
|
||||
|
||||
@router.post("/upload", response_model=FileUploadResponse, summary="统一上传接口")
|
||||
async def upload(file: UploadFile = File(...)) -> FileUploadResponse:
|
||||
return await UploadController.upload_any(file)
|
||||
@ -188,6 +188,42 @@ class InvoiceController(CRUDBase[Invoice, InvoiceCreate, InvoiceUpdate]):
|
||||
if filters.get("invoice_type"):
|
||||
qs = qs.filter(invoice__invoice_type=filters["invoice_type"])
|
||||
|
||||
# 时间区间筛选(凭证提交时间)
|
||||
created_range = filters.get("created_at")
|
||||
submitted_start = filters.get("submitted_start")
|
||||
submitted_end = filters.get("submitted_end")
|
||||
if created_range and isinstance(created_range, (list, tuple)) and len(created_range) == 2:
|
||||
try:
|
||||
# 前端可能传毫秒时间戳
|
||||
start_ms = int(created_range[0])
|
||||
end_ms = int(created_range[1])
|
||||
from datetime import datetime
|
||||
start_dt = datetime.fromtimestamp(start_ms / 1000)
|
||||
end_dt = datetime.fromtimestamp(end_ms / 1000)
|
||||
qs = qs.filter(created_at__gte=start_dt, created_at__lte=end_dt)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
from datetime import datetime
|
||||
def parse_time(v):
|
||||
try:
|
||||
iv = int(v)
|
||||
return datetime.fromtimestamp(iv / 1000)
|
||||
except Exception:
|
||||
try:
|
||||
# ISO 字符串
|
||||
return datetime.fromisoformat(v)
|
||||
except Exception:
|
||||
return None
|
||||
if submitted_start:
|
||||
s_dt = parse_time(submitted_start)
|
||||
if s_dt:
|
||||
qs = qs.filter(created_at__gte=s_dt)
|
||||
if submitted_end:
|
||||
e_dt = parse_time(submitted_end)
|
||||
if e_dt:
|
||||
qs = qs.filter(created_at__lte=e_dt)
|
||||
|
||||
total = await qs.count()
|
||||
rows = await qs.order_by("-created_at").offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
|
||||
@ -87,4 +87,16 @@ class UploadController:
|
||||
url=f"{settings.BASE_URL}/static/files/{filename}",
|
||||
filename=filename,
|
||||
content_type=file.content_type,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def upload_any(file: UploadFile) -> FileUploadResponse:
|
||||
"""
|
||||
统一上传入口,自动识别图片与非图片类型。
|
||||
返回统一结构:url, filename, content_type
|
||||
"""
|
||||
if file.content_type and file.content_type.startswith("image/"):
|
||||
img = await UploadController.upload_image(file)
|
||||
return FileUploadResponse(url=img.url, filename=img.filename, content_type=file.content_type or "image")
|
||||
# 非图片类型复用原文件上传校验
|
||||
return await UploadController.upload_file(file)
|
||||
@ -23,6 +23,7 @@ from app.core.exceptions import (
|
||||
)
|
||||
from app.log import logger
|
||||
from app.models.admin import Api, Menu, Role
|
||||
from app.models.invoice import Invoice, PaymentReceipt
|
||||
from app.schemas.menus import MenuType
|
||||
from app.settings.config import settings
|
||||
|
||||
@ -237,6 +238,45 @@ async def init_menus():
|
||||
redirect="",
|
||||
)
|
||||
|
||||
# 创建交易管理菜单
|
||||
transaction_menu = await Menu.create(
|
||||
menu_type=MenuType.CATALOG,
|
||||
name="交易管理",
|
||||
path="/transaction",
|
||||
order=3,
|
||||
parent_id=0,
|
||||
icon="carbon:wallet",
|
||||
is_hidden=False,
|
||||
component="Layout",
|
||||
keepalive=False,
|
||||
redirect="/transaction/invoice",
|
||||
)
|
||||
transaction_children = [
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="发票管理",
|
||||
path="invoice",
|
||||
order=1,
|
||||
parent_id=transaction_menu.id,
|
||||
icon="mdi:file-document-outline",
|
||||
is_hidden=False,
|
||||
component="/transaction/invoice",
|
||||
keepalive=False,
|
||||
),
|
||||
Menu(
|
||||
menu_type=MenuType.MENU,
|
||||
name="交易记录",
|
||||
path="receipts",
|
||||
order=2,
|
||||
parent_id=transaction_menu.id,
|
||||
icon="mdi:receipt-text-outline",
|
||||
is_hidden=False,
|
||||
component="/transaction/receipts",
|
||||
keepalive=False,
|
||||
),
|
||||
]
|
||||
await Menu.bulk_create(transaction_children)
|
||||
|
||||
|
||||
async def init_apis():
|
||||
apis = await api_controller.model.exists()
|
||||
@ -287,9 +327,123 @@ async def init_roles():
|
||||
await user_role.apis.add(*basic_apis)
|
||||
|
||||
|
||||
async def init_demo_transactions():
|
||||
"""
|
||||
创建开发环境演示用的发票与交易记录(付款凭证)数据。
|
||||
|
||||
功能:
|
||||
- 在无现有付款凭证数据时,批量生成若干 `Invoice` 与关联的 `PaymentReceipt`。
|
||||
- 仅在调试模式下执行,避免污染生产环境。
|
||||
|
||||
参数: 无
|
||||
|
||||
返回: `None`,异步执行插入操作。
|
||||
"""
|
||||
if not settings.DEBUG:
|
||||
return
|
||||
|
||||
has_receipt = await PaymentReceipt.exists()
|
||||
if has_receipt:
|
||||
return
|
||||
|
||||
demo_invoices = []
|
||||
demo_payloads = [
|
||||
{
|
||||
"ticket_type": "electronic",
|
||||
"invoice_type": "normal",
|
||||
"phone": "13800000001",
|
||||
"email": "demo1@example.com",
|
||||
"company_name": "演示科技有限公司",
|
||||
"tax_number": "91310000MA1DEMO01",
|
||||
"register_address": "上海市浦东新区演示路 100 号",
|
||||
"register_phone": "021-88880001",
|
||||
"bank_name": "招商银行上海分行",
|
||||
"bank_account": "6214830000000001",
|
||||
"status": "pending",
|
||||
"wechat": "demo_wechat_01",
|
||||
},
|
||||
{
|
||||
"ticket_type": "paper",
|
||||
"invoice_type": "special",
|
||||
"phone": "13800000002",
|
||||
"email": "demo2@example.com",
|
||||
"company_name": "示例信息技术股份有限公司",
|
||||
"tax_number": "91310000MA1DEMO02",
|
||||
"register_address": "北京市海淀区知春路 66 号",
|
||||
"register_phone": "010-66660002",
|
||||
"bank_name": "中国银行北京分行",
|
||||
"bank_account": "6216610000000002",
|
||||
"status": "invoiced",
|
||||
"wechat": "demo_wechat_02",
|
||||
},
|
||||
{
|
||||
"ticket_type": "electronic",
|
||||
"invoice_type": "special",
|
||||
"phone": "13800000003",
|
||||
"email": "demo3@example.com",
|
||||
"company_name": "华夏制造有限公司",
|
||||
"tax_number": "91310000MA1DEMO03",
|
||||
"register_address": "广州市天河区高新大道 8 号",
|
||||
"register_phone": "020-77770003",
|
||||
"bank_name": "建设银行广州分行",
|
||||
"bank_account": "6227000000000003",
|
||||
"status": "rejected",
|
||||
"wechat": "demo_wechat_03",
|
||||
},
|
||||
{
|
||||
"ticket_type": "paper",
|
||||
"invoice_type": "normal",
|
||||
"phone": "13800000004",
|
||||
"email": "demo4@example.com",
|
||||
"company_name": "泰岳网络科技有限公司",
|
||||
"tax_number": "91310000MA1DEMO04",
|
||||
"register_address": "杭州市滨江区科技大道 1 号",
|
||||
"register_phone": "0571-55550004",
|
||||
"bank_name": "农业银行杭州分行",
|
||||
"bank_account": "6228480000000004",
|
||||
"status": "refunded",
|
||||
"wechat": "demo_wechat_04",
|
||||
},
|
||||
{
|
||||
"ticket_type": "electronic",
|
||||
"invoice_type": "normal",
|
||||
"phone": "13800000005",
|
||||
"email": "demo5@example.com",
|
||||
"company_name": "星云数据有限公司",
|
||||
"tax_number": "91310000MA1DEMO05",
|
||||
"register_address": "成都市高新区软件园 9 号楼",
|
||||
"register_phone": "028-33330005",
|
||||
"bank_name": "工商银行成都分行",
|
||||
"bank_account": "6222020000000005",
|
||||
"status": "pending",
|
||||
"wechat": "demo_wechat_05",
|
||||
},
|
||||
]
|
||||
|
||||
for payload in demo_payloads:
|
||||
inv = await Invoice.create(**payload)
|
||||
demo_invoices.append(inv)
|
||||
|
||||
for idx, inv in enumerate(demo_invoices, start=1):
|
||||
await PaymentReceipt.create(
|
||||
invoice=inv,
|
||||
url=f"https://example.com/demo-receipt-{idx}-a.png",
|
||||
note="DEMO 凭证 A",
|
||||
verified=(inv.status == "invoiced"),
|
||||
)
|
||||
if idx % 2 == 0:
|
||||
await PaymentReceipt.create(
|
||||
invoice=inv,
|
||||
url=f"https://example.com/demo-receipt-{idx}-b.png",
|
||||
note="DEMO 凭证 B",
|
||||
verified=False,
|
||||
)
|
||||
|
||||
|
||||
async def init_data():
|
||||
await init_db()
|
||||
await init_superuser()
|
||||
await init_menus()
|
||||
await init_apis()
|
||||
await init_roles()
|
||||
await init_demo_transactions()
|
||||
|
||||
@ -20,6 +20,7 @@ class AppUser(BaseModel, TimestampMixin):
|
||||
is_active = fields.BooleanField(default=True, description="是否激活", index=True)
|
||||
last_login = fields.DatetimeField(null=True, description="最后登录时间", index=True)
|
||||
remaining_quota = fields.IntField(default=0, description="剩余估值次数", index=True)
|
||||
notes = fields.CharField(max_length=256, null=True, description="备注")
|
||||
|
||||
class Meta:
|
||||
table = "app_user"
|
||||
|
||||
@ -100,6 +100,7 @@ class AppUserQuotaLogOut(BaseModel):
|
||||
after_count: int
|
||||
op_type: str
|
||||
remark: Optional[str] = None
|
||||
created_at: str
|
||||
|
||||
|
||||
class AppUserRegisterOut(BaseModel):
|
||||
|
||||
@ -28,10 +28,10 @@ class EmailClient:
|
||||
msg.attach(part)
|
||||
|
||||
if settings.SMTP_TLS:
|
||||
server = smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT)
|
||||
server = smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT, timeout=30)
|
||||
server.starttls()
|
||||
else:
|
||||
server = smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT)
|
||||
server = smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT, timeout=30)
|
||||
try:
|
||||
if settings.SMTP_USERNAME and settings.SMTP_PASSWORD:
|
||||
server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD)
|
||||
|
||||
@ -24,14 +24,24 @@ class SMSClient:
|
||||
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")
|
||||
from alibabacloud_credentials.client import Client as CredentialClient # type: ignore
|
||||
use_chain = bool(settings.ALIYUN_USE_DEFAULT_CREDENTIALS) or (not settings.ALIBABA_CLOUD_ACCESS_KEY_ID or not settings.ALIBABA_CLOUD_ACCESS_KEY_SECRET)
|
||||
if not use_chain and (not settings.ALIBABA_CLOUD_ACCESS_KEY_ID or not settings.ALIBABA_CLOUD_ACCESS_KEY_SECRET):
|
||||
raise RuntimeError("短信凭证未配置:请设置 ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET 或启用默认凭据链")
|
||||
if not settings.ALIYUN_SMS_SIGN_NAME:
|
||||
raise RuntimeError("短信签名未配置:请设置 ALIYUN_SMS_SIGN_NAME")
|
||||
config = open_api_models.Config(
|
||||
access_key_id=settings.ALIBABA_CLOUD_ACCESS_KEY_ID,
|
||||
access_key_secret=settings.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
|
||||
)
|
||||
if str(settings.ALIYUN_SMS_SIGN_NAME).upper().startswith("SMS_"):
|
||||
raise RuntimeError("短信签名配置错误:签名不应为模板码")
|
||||
if settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY and settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT and settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY == settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT:
|
||||
raise RuntimeError("短信模板配置错误:验证码模板与报告模板重复")
|
||||
if use_chain:
|
||||
credential = CredentialClient()
|
||||
config = open_api_models.Config(credential=credential)
|
||||
else:
|
||||
config = open_api_models.Config(
|
||||
access_key_id=settings.ALIBABA_CLOUD_ACCESS_KEY_ID,
|
||||
access_key_secret=settings.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
|
||||
)
|
||||
config.endpoint = settings.ALIYUN_SMS_ENDPOINT
|
||||
self.client = DysmsClient(config)
|
||||
|
||||
@ -54,7 +64,8 @@ class SMSClient:
|
||||
template_code=template_code,
|
||||
template_param=json.dumps(template_param or {}),
|
||||
)
|
||||
logger.info("sms.send start phone={} template={}", phone, template_code)
|
||||
|
||||
logger.info("sms.send start phone={} sign={} template={}", phone, settings.ALIYUN_SMS_SIGN_NAME, template_code)
|
||||
try:
|
||||
resp = self.client.send_sms(req)
|
||||
body = resp.body.to_map() if hasattr(resp, "body") else {}
|
||||
@ -74,7 +85,10 @@ class SMSClient:
|
||||
Returns:
|
||||
返回体映射字典
|
||||
"""
|
||||
return self.send_by_template(phone, "SMS_498190229", {"code": code})
|
||||
key = settings.ALIYUN_SMS_TEMPLATE_PARAM_CODE_KEY or "code"
|
||||
template = settings.ALIYUN_SMS_TEMPLATE_CODE_VERIFY or "SMS_498190229"
|
||||
logger.info("sms.send_code using key={} template={} phone={}", key, template, phone)
|
||||
return self.send_by_template(phone, template, {key: code})
|
||||
|
||||
def send_report(self, phone: str) -> Dict[str, Any]:
|
||||
"""发送报告通知短信
|
||||
@ -85,7 +99,9 @@ class SMSClient:
|
||||
Returns:
|
||||
返回体映射字典
|
||||
"""
|
||||
return self.send_by_template(phone, "SMS_498140213", {})
|
||||
template = settings.ALIYUN_SMS_TEMPLATE_CODE_REPORT or "SMS_498140213"
|
||||
logger.info("sms.send_report using template={} phone={}", template, phone)
|
||||
return self.send_by_template(phone, template, {})
|
||||
|
||||
|
||||
sms_client = SMSClient()
|
||||
@ -4,6 +4,8 @@ from datetime import date
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
|
||||
from app.settings import settings
|
||||
|
||||
class VerificationStore:
|
||||
def __init__(self, code_ttl_seconds: int = 300, minute_window: int = 60, daily_limit: int = 10, max_failures: int = 5, lock_seconds: int = 3600) -> None:
|
||||
"""验证码与限流存储
|
||||
@ -28,12 +30,14 @@ class VerificationStore:
|
||||
self.failures: Dict[str, Dict[str, float]] = {}
|
||||
|
||||
def generate_code(self) -> str:
|
||||
"""生成6位数字验证码
|
||||
"""生成数字验证码
|
||||
|
||||
Returns:
|
||||
六位数字字符串
|
||||
指定位数的数字字符串
|
||||
"""
|
||||
return f"{random.randint(0, 999999):06d}"
|
||||
digits = int(getattr(settings, "SMS_CODE_DIGITS", 6) or 6)
|
||||
max_val = (10 ** digits) - 1
|
||||
return f"{random.randint(0, max_val):0{digits}d}"
|
||||
|
||||
def set_code(self, phone: str, code: str) -> None:
|
||||
"""设置验证码与过期时间
|
||||
|
||||
@ -97,8 +97,14 @@ class Settings(BaseSettings):
|
||||
|
||||
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_SIGN_NAME: typing.Optional[str] = "成都文化产权交易所"
|
||||
ALIYUN_SMS_ENDPOINT: str = "dysmsapi.aliyuncs.com"
|
||||
ALIYUN_SMS_TEMPLATE_CODE_VERIFY: typing.Optional[str] = "SMS_498140213"
|
||||
ALIYUN_SMS_TEMPLATE_CODE_REPORT: typing.Optional[str] = "SMS_49190229"
|
||||
SMS_CODE_DIGITS: int = 6
|
||||
SMS_DEBUG_LOG_CODE: bool = True
|
||||
ALIYUN_USE_DEFAULT_CREDENTIALS: bool = False
|
||||
ALIYUN_SMS_TEMPLATE_PARAM_CODE_KEY: typing.Optional[str] = "code"
|
||||
|
||||
SMTP_HOST: typing.Optional[str] = None
|
||||
SMTP_PORT: typing.Optional[int] = None
|
||||
|
||||
@ -1,6 +1,21 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
location = /docs {
|
||||
proxy_pass http://127.0.0.1:9999/docs;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location = /redoc {
|
||||
proxy_pass http://127.0.0.1:9999/redoc;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location = /openapi.json {
|
||||
proxy_pass http://127.0.0.1:9999/openapi.json;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
location / {
|
||||
root /opt/vue-fastapi-admin/web/dist;
|
||||
index index.html index.htm;
|
||||
@ -8,6 +23,8 @@ server {
|
||||
}
|
||||
location ^~ /api/ {
|
||||
proxy_pass http://127.0.0.1:9999;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
}
|
||||
@ -68,3 +68,4 @@ alibabacloud_dysmsapi20170525==4.1.2
|
||||
alibabacloud_tea_openapi==0.4.1
|
||||
alibabacloud_tea_util==0.3.14
|
||||
pytest==8.3.3
|
||||
aiomysql
|
||||
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=9991, reload=True, log_config=LOGGING_CONFIG)
|
||||
uvicorn.run("app:app", host="0.0.0.0", port=9999, reload=True, log_config=LOGGING_CONFIG)
|
||||
|
||||
|
||||
103
scripts/send_email_test.py
Normal file
103
scripts/send_email_test.py
Normal file
@ -0,0 +1,103 @@
|
||||
import os
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
def _parse_bool(value: Optional[str]) -> bool:
|
||||
"""
|
||||
功能: 将环境变量中的布尔字符串解析为布尔值
|
||||
参数: value (Optional[str]) - 环境变量字符串值, 如 "true", "1", "yes"
|
||||
返回: bool - 解析后的布尔值
|
||||
"""
|
||||
if value is None:
|
||||
return False
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "y"}
|
||||
|
||||
|
||||
def get_smtp_config() -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
功能: 从环境变量读取SMTP配置并返回配置字典
|
||||
参数: 无
|
||||
返回: Dict[str, Optional[str]] - 包含host、port、from、username、password、tls等键的配置
|
||||
"""
|
||||
host = os.environ.get("SMTP_HOST", "smtp.qiye.aliyun.com")
|
||||
port_str = os.environ.get("SMTP_PORT", "465")
|
||||
from_addr = os.environ.get("SMTP_FROM","value@cdcee.net")
|
||||
username = os.environ.get("SMTP_USERNAME","value@cdcee.net")
|
||||
password = os.environ.get("SMTP_PASSWORD","PPXbILdGlRCn2VOx")
|
||||
tls = _parse_bool(os.environ.get("SMTP_TLS"))
|
||||
|
||||
port = None
|
||||
if port_str:
|
||||
try:
|
||||
port = int(port_str)
|
||||
except Exception:
|
||||
port = None
|
||||
|
||||
return {
|
||||
"host": host,
|
||||
"port": port,
|
||||
"from": from_addr,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"tls": tls,
|
||||
}
|
||||
|
||||
|
||||
def send_test_email(to_email: str, subject: Optional[str], body: str) -> Dict[str, str]:
|
||||
"""
|
||||
功能: 使用SMTP配置发送测试邮件到指定邮箱
|
||||
参数: to_email (str) - 收件人邮箱; subject (Optional[str]) - 邮件主题; body (str) - 邮件正文内容
|
||||
返回: Dict[str, str] - 发送结果字典, 包含status("OK"/"FAIL")与error(失败信息)
|
||||
"""
|
||||
cfg = get_smtp_config()
|
||||
if not cfg["host"] or not cfg["port"] or not cfg["from"]:
|
||||
return {"status": "FAIL", "error": "SMTP 未配置: 需设置 SMTP_HOST/SMTP_PORT/SMTP_FROM"}
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = cfg["from"]
|
||||
msg["To"] = to_email
|
||||
msg["Subject"] = subject or "估值服务通知"
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
|
||||
server = None
|
||||
try:
|
||||
if cfg["tls"]:
|
||||
server = smtplib.SMTP(cfg["host"], cfg["port"], timeout=30)
|
||||
server.starttls()
|
||||
else:
|
||||
server = smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=30)
|
||||
|
||||
if cfg["username"] and cfg["password"]:
|
||||
server.login(cfg["username"], cfg["password"])
|
||||
|
||||
server.sendmail(cfg["from"], [to_email], msg.as_string())
|
||||
server.quit()
|
||||
return {"status": "OK"}
|
||||
except Exception as e:
|
||||
try:
|
||||
if server:
|
||||
server.quit()
|
||||
except Exception:
|
||||
pass
|
||||
return {"status": "FAIL", "error": str(e)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
to = "zfc9393@163.com"
|
||||
subject = "测试邮件"
|
||||
body = "这是一封测试邮件,用于验证SMTP配置。"
|
||||
|
||||
cfg = get_smtp_config()
|
||||
print({
|
||||
"host": cfg["host"],
|
||||
"port": cfg["port"],
|
||||
"from": cfg["from"],
|
||||
"username_set": bool(cfg["username"]),
|
||||
"password_set": bool(cfg["password"]),
|
||||
"tls": cfg["tls"],
|
||||
})
|
||||
result = send_test_email(to, subject, body)
|
||||
print(result)
|
||||
54
scripts/send_email_test.sh
Normal file
54
scripts/send_email_test.sh
Normal file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TOKEN=""
|
||||
EMAIL=""
|
||||
SUBJECT=""
|
||||
BODY=""
|
||||
FILE_PATH=""
|
||||
FILE_URL=""
|
||||
BASE_API="http://127.0.0.1:9999/api/v1"
|
||||
|
||||
while getopts ":t:e:s:b:f:u:a:" opt; do
|
||||
case "$opt" in
|
||||
t) TOKEN="$OPTARG" ;;
|
||||
e) EMAIL="$OPTARG" ;;
|
||||
s) SUBJECT="$OPTARG" ;;
|
||||
b) BODY="$OPTARG" ;;
|
||||
f) FILE_PATH="$OPTARG" ;;
|
||||
u) FILE_URL="$OPTARG" ;;
|
||||
a) BASE_API="$OPTARG" ;;
|
||||
*) echo "Invalid option: -$OPTARG"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TOKEN" || -z "$EMAIL" || -z "$BODY" ]]; then
|
||||
echo "Usage: $0 -t <token> -e <email> -b <body> [-s <subject>] [-f <file_path> | -u <file_url>] [-a <base_api>]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
URL="$BASE_API/transactions/send-email"
|
||||
|
||||
if [[ -n "$FILE_PATH" ]]; then
|
||||
if [[ ! -f "$FILE_PATH" ]]; then
|
||||
echo "File not found: $FILE_PATH"
|
||||
exit 1
|
||||
fi
|
||||
curl -s -X POST "$URL" \
|
||||
-H "accept: application/json" \
|
||||
-H "token: $TOKEN" \
|
||||
-F "email=$EMAIL" \
|
||||
-F "subject=$SUBJECT" \
|
||||
-F "body=$BODY" \
|
||||
-F "file=@$FILE_PATH" | jq -r '.' 2>/dev/null || true
|
||||
else
|
||||
PAYLOAD="{\"email\":\"$EMAIL\",\"subject\":\"$SUBJECT\",\"body\":\"$BODY\"}"
|
||||
if [[ -n "$FILE_URL" ]]; then
|
||||
PAYLOAD="{\"email\":\"$EMAIL\",\"subject\":\"$SUBJECT\",\"body\":\"$BODY\",\"file_url\":\"$FILE_URL\"}"
|
||||
fi
|
||||
curl -s -X POST "$URL" \
|
||||
-H "accept: application/json" \
|
||||
-H "token: $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" | jq -r '.' 2>/dev/null || true
|
||||
fi
|
||||
@ -276,6 +276,120 @@ const mockValuationDetails = valuationRecords.map((record) => ({
|
||||
...record,
|
||||
}))
|
||||
|
||||
const mockAppUsers = [
|
||||
{
|
||||
id: 11111111,
|
||||
phone: '15021982682',
|
||||
wechat: 'f1498480844',
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
notes: '测试用户1',
|
||||
remaining_count: 1,
|
||||
user_type: '体验用户',
|
||||
},
|
||||
{
|
||||
id: 11111112,
|
||||
phone: '13800138002',
|
||||
wechat: 'wx_limming2024',
|
||||
created_at: '2024-02-20T14:20:00Z',
|
||||
notes: '付费用户',
|
||||
remaining_count: 5,
|
||||
user_type: '付费用户',
|
||||
},
|
||||
{
|
||||
id: 11111113,
|
||||
phone: '13800138003',
|
||||
wechat: null,
|
||||
created_at: '2024-03-10T08:45:00Z',
|
||||
notes: null,
|
||||
remaining_count: 0,
|
||||
user_type: '体验用户',
|
||||
},
|
||||
{
|
||||
id: 11111114,
|
||||
phone: '13800138004',
|
||||
wechat: 'chenjun_vip',
|
||||
created_at: '2024-04-05T11:30:00Z',
|
||||
notes: 'VIP用户',
|
||||
remaining_count: 10,
|
||||
user_type: 'VIP',
|
||||
},
|
||||
{
|
||||
id: 11111115,
|
||||
phone: '13800138005',
|
||||
wechat: 'liuxia888',
|
||||
created_at: '2024-05-12T16:15:00Z',
|
||||
notes: '体验用户',
|
||||
remaining_count: 3,
|
||||
user_type: '体验用户',
|
||||
},
|
||||
{
|
||||
id: 11111116,
|
||||
phone: '13800138006',
|
||||
wechat: null,
|
||||
created_at: '2024-06-18T09:00:00Z',
|
||||
notes: '新注册用户',
|
||||
remaining_count: 2,
|
||||
user_type: '体验用户',
|
||||
},
|
||||
{
|
||||
id: 11111117,
|
||||
phone: '13800138007',
|
||||
wechat: 'zhaolei2024',
|
||||
created_at: '2024-07-22T12:45:00Z',
|
||||
notes: null,
|
||||
remaining_count: 0,
|
||||
user_type: '体验用户',
|
||||
},
|
||||
{
|
||||
id: 11111118,
|
||||
phone: '13800138008',
|
||||
wechat: 'sunmei_user',
|
||||
created_at: '2024-08-30T15:20:00Z',
|
||||
notes: '活跃用户',
|
||||
remaining_count: 7,
|
||||
user_type: 'VIP',
|
||||
},
|
||||
]
|
||||
|
||||
const defaultInvoiceHeaders = [
|
||||
{
|
||||
company_name: '成都文创科技有限公司',
|
||||
tax_number: '91510100MA7XYZ1234',
|
||||
register_address: '四川省成都市高新区天府三街666号',
|
||||
register_phone: '028-66666666',
|
||||
bank_name: '招商银行成都分行',
|
||||
bank_account: '6225 6666 8888 0000',
|
||||
email: 'finance@scwenchuang.com',
|
||||
},
|
||||
{
|
||||
company_name: '天府文化发展有限公司',
|
||||
tax_number: '91510100678912345K',
|
||||
register_address: '四川省成都市武侯区科华北路88号',
|
||||
register_phone: '028-12345678',
|
||||
bank_name: '中国工商银行成都分行',
|
||||
bank_account: '6212 8888 0000 9999',
|
||||
email: 'invoice@tfculture.com',
|
||||
},
|
||||
]
|
||||
|
||||
const defaultOperationLogs = [
|
||||
{
|
||||
time: '2025-10-31 18:30:30',
|
||||
operator: 'admin',
|
||||
records: ['剩余估值次数:0 -> 1', '类型:付费估值', '备注:新用户'],
|
||||
},
|
||||
{
|
||||
time: '2025-10-31 18:30:30',
|
||||
operator: 'admin',
|
||||
records: ['剩余估值次数:2 -> 1', '类型:付费估值', '备注:退款'],
|
||||
},
|
||||
{
|
||||
time: '2025-10-31 18:30:30',
|
||||
operator: 'admin',
|
||||
records: ['用户备注:111111111111 -> 22222222222222222222'],
|
||||
},
|
||||
]
|
||||
|
||||
export default {
|
||||
login: (data) => request.post('/base/access_token', data, { noNeedToken: true }),
|
||||
getUserInfo: () => request.get('/base/userinfo'),
|
||||
@ -316,17 +430,299 @@ export default {
|
||||
// auditlog
|
||||
getAuditLogList: (params = {}) => request.get('/auditlog/list', { params }),
|
||||
// app users (客户端用户管理) - 使用现有的后端接口
|
||||
getAppUserList: (params = {}) => request.get('/app-user-admin/list', { params }),
|
||||
updateAppUserQuota: (data = {}) => request.post('/app-user-admin/quota', data),
|
||||
getAppUserQuotaLogs: ({ user_id, ...params } = {}) =>
|
||||
request.get(`/app-user-admin/${user_id}/quota-logs`, { params }),
|
||||
getAppUserList: (params = {}) => {
|
||||
// 模拟分页和搜索
|
||||
let filteredUsers = [...mockAppUsers]
|
||||
|
||||
// 手机号搜索
|
||||
if (params.phone) {
|
||||
filteredUsers = filteredUsers.filter(user =>
|
||||
user.phone.includes(params.phone)
|
||||
)
|
||||
}
|
||||
|
||||
// 微信号搜索
|
||||
if (params.wechat) {
|
||||
filteredUsers = filteredUsers.filter(user =>
|
||||
user.wechat && user.wechat.includes(params.wechat)
|
||||
)
|
||||
}
|
||||
|
||||
// 注册时间筛选(日期范围)
|
||||
if (params.created_at && Array.isArray(params.created_at) && params.created_at.length === 2) {
|
||||
const [startTime, endTime] = params.created_at
|
||||
filteredUsers = filteredUsers.filter(user => {
|
||||
if (!user.created_at) return false
|
||||
const userTime = new Date(user.created_at).getTime()
|
||||
return userTime >= startTime && userTime <= endTime
|
||||
})
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const page = Number(params.page) || 1
|
||||
const pageSize = Number(params.page_size) || 10
|
||||
const startIndex = (page - 1) * pageSize
|
||||
const endIndex = startIndex + pageSize
|
||||
const paginatedUsers = filteredUsers.slice(startIndex, endIndex)
|
||||
|
||||
// 返回 Promise 模拟异步请求
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
data: paginatedUsers,
|
||||
total: filteredUsers.length,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
getAppUserById: (params = {}) =>
|
||||
new Promise((resolve) => {
|
||||
const id = Number(params.id)
|
||||
const user = mockAppUsers.find((item) => item.id === id) || {}
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
baseInfo: {
|
||||
id: user.id,
|
||||
phone: user.phone,
|
||||
wechat: user.wechat,
|
||||
register_time: user.created_at,
|
||||
notes: user.notes,
|
||||
remaining_count: user.remaining_count,
|
||||
user_type: user.user_type || '体验用户',
|
||||
},
|
||||
invoiceHeaders: defaultInvoiceHeaders,
|
||||
operationLogs: defaultOperationLogs,
|
||||
})
|
||||
}, 300)
|
||||
}),
|
||||
createAppUser: (data = {}) => request.post('/app-user/register', data),
|
||||
updateAppUser: (data = {}) => request.post('/app-user/update', data),
|
||||
deleteAppUser: (params = {}) => request.delete('/app-user/delete', { params }),
|
||||
// invoice (交易管理-对公转账记录)
|
||||
getInvoiceList: (params = {}) => request.get('/transactions/receipts', { params }),
|
||||
getInvoiceById: (params = {}) => request.get(`/transactions/receipts/${params.id}`, { params }),
|
||||
sendInvoice: (data = {}) => request.post('/transactions/send-email', data),
|
||||
// invoice (开票记录)
|
||||
getInvoiceList: (params = {}) => {
|
||||
// Mock 数据
|
||||
const mockInvoices = [
|
||||
{
|
||||
id: 1001,
|
||||
created_at: '2024-11-10T09:30:00Z',
|
||||
ticket_type: 'electronic',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@company1.com',
|
||||
company_name: '北京科技有限公司',
|
||||
tax_number: '91110000123456789A',
|
||||
register_address: '北京市朝阳区科技园区A座1001室',
|
||||
register_phone: '010-12345678',
|
||||
bank_name: '中国工商银行北京分行',
|
||||
bank_account: '6222021234567890123',
|
||||
invoice_type: 'special',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 1002,
|
||||
created_at: '2024-11-09T14:20:00Z',
|
||||
ticket_type: 'paper',
|
||||
phone: '13800138002',
|
||||
email: 'lisi@company2.com',
|
||||
company_name: '上海贸易股份有限公司',
|
||||
tax_number: '91310000987654321B',
|
||||
register_address: '上海市浦东新区金融街B座2002室',
|
||||
register_phone: '021-87654321',
|
||||
bank_name: '中国建设银行上海分行',
|
||||
bank_account: '6217001234567890124',
|
||||
invoice_type: 'normal',
|
||||
status: 'invoiced'
|
||||
},
|
||||
{
|
||||
id: 1003,
|
||||
created_at: '2024-11-08T16:45:00Z',
|
||||
ticket_type: 'electronic',
|
||||
phone: '13800138003',
|
||||
email: 'wangwu@company3.com',
|
||||
company_name: '深圳创新科技有限公司',
|
||||
tax_number: '91440300456789012C',
|
||||
register_address: '深圳市南山区高新技术园C座3003室',
|
||||
register_phone: '0755-23456789',
|
||||
bank_name: '招商银行深圳分行',
|
||||
bank_account: '6214851234567890125',
|
||||
invoice_type: 'special',
|
||||
status: 'rejected'
|
||||
},
|
||||
{
|
||||
id: 1004,
|
||||
created_at: '2024-11-07T11:15:00Z',
|
||||
ticket_type: 'paper',
|
||||
phone: '13800138004',
|
||||
email: 'zhaoliu@company4.com',
|
||||
company_name: '广州制造业集团有限公司',
|
||||
tax_number: '91440100789012345D',
|
||||
register_address: '广州市天河区商务中心D座4004室',
|
||||
register_phone: '020-34567890',
|
||||
bank_name: '中国银行广州分行',
|
||||
bank_account: '6013821234567890126',
|
||||
invoice_type: 'normal',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 1005,
|
||||
created_at: '2024-11-06T08:30:00Z',
|
||||
ticket_type: 'electronic',
|
||||
phone: '13800138005',
|
||||
email: 'sunqi@company5.com',
|
||||
company_name: '杭州互联网科技有限公司',
|
||||
tax_number: '91330100012345678E',
|
||||
register_address: '杭州市西湖区互联网小镇E座5005室',
|
||||
register_phone: '0571-45678901',
|
||||
bank_name: '浙商银行杭州分行',
|
||||
bank_account: '6228481234567890127',
|
||||
invoice_type: 'special',
|
||||
status: 'invoiced'
|
||||
},
|
||||
{
|
||||
id: 1006,
|
||||
created_at: '2024-11-05T13:20:00Z',
|
||||
ticket_type: 'paper',
|
||||
phone: '13800138006',
|
||||
email: 'zhouba@company6.com',
|
||||
company_name: '成都软件开发有限公司',
|
||||
tax_number: '91510100345678901F',
|
||||
register_address: '成都市高新区软件园F座6006室',
|
||||
register_phone: '028-56789012',
|
||||
bank_name: '中国农业银行成都分行',
|
||||
bank_account: '6230521234567890128',
|
||||
invoice_type: 'normal',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 1007,
|
||||
created_at: '2024-11-04T15:45:00Z',
|
||||
ticket_type: 'electronic',
|
||||
phone: '13800138007',
|
||||
email: 'wujiu@company7.com',
|
||||
company_name: '武汉新能源科技有限公司',
|
||||
tax_number: '91420100678901234G',
|
||||
register_address: '武汉市江汉区新能源产业园G座7007室',
|
||||
register_phone: '027-67890123',
|
||||
bank_name: '交通银行武汉分行',
|
||||
bank_account: '6222601234567890129',
|
||||
invoice_type: 'special',
|
||||
status: 'invoiced'
|
||||
},
|
||||
{
|
||||
id: 1008,
|
||||
created_at: '2024-11-03T10:10:00Z',
|
||||
ticket_type: 'paper',
|
||||
phone: '13800138008',
|
||||
email: 'zhengshi@company8.com',
|
||||
company_name: '西安电子商务有限公司',
|
||||
tax_number: '91610100901234567H',
|
||||
register_address: '西安市雁塔区电商产业园H座8008室',
|
||||
register_phone: '029-78901234',
|
||||
bank_name: '中信银行西安分行',
|
||||
bank_account: '6217711234567890130',
|
||||
invoice_type: 'normal',
|
||||
status: 'refunded'
|
||||
},
|
||||
{
|
||||
id: 1009,
|
||||
created_at: '2024-11-02T14:30:00Z',
|
||||
ticket_type: 'electronic',
|
||||
phone: '13800138009',
|
||||
email: 'wangwu@company9.com',
|
||||
company_name: '天津物流科技有限公司',
|
||||
tax_number: '91120000234567890I',
|
||||
register_address: '天津市滨海新区物流园I座9009室',
|
||||
register_phone: '022-89012345',
|
||||
bank_name: '民生银行天津分行',
|
||||
bank_account: '6226221234567890131',
|
||||
invoice_type: 'special',
|
||||
status: 'refunded'
|
||||
},
|
||||
{
|
||||
id: 1010,
|
||||
created_at: '2024-11-01T11:45:00Z',
|
||||
ticket_type: 'paper',
|
||||
phone: '13800138010',
|
||||
email: 'liuliu@company10.com',
|
||||
company_name: '重庆智能制造有限公司',
|
||||
tax_number: '91500000567890123J',
|
||||
register_address: '重庆市渝北区智能制造园J座1010室',
|
||||
register_phone: '023-90123456',
|
||||
bank_name: '华夏银行重庆分行',
|
||||
bank_account: '6228881234567890132',
|
||||
invoice_type: 'normal',
|
||||
status: 'rejected'
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟分页和搜索
|
||||
let filteredInvoices = [...mockInvoices]
|
||||
|
||||
// 手机号搜索
|
||||
if (params.phone) {
|
||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
||||
invoice.phone.includes(params.phone)
|
||||
)
|
||||
}
|
||||
|
||||
// 公司名称搜索
|
||||
if (params.company_name) {
|
||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
||||
invoice.company_name.includes(params.company_name)
|
||||
)
|
||||
}
|
||||
|
||||
// 公司税号搜索
|
||||
if (params.tax_number) {
|
||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
||||
invoice.tax_number.includes(params.tax_number)
|
||||
)
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (params.status) {
|
||||
filteredInvoices = filteredInvoices.filter(invoice =>
|
||||
invoice.status === params.status
|
||||
)
|
||||
}
|
||||
|
||||
// 提交时间筛选
|
||||
if (params.created_at && Array.isArray(params.created_at) && params.created_at.length === 2) {
|
||||
const [startDate, endDate] = params.created_at
|
||||
filteredInvoices = filteredInvoices.filter(invoice => {
|
||||
const invoiceDate = new Date(invoice.created_at)
|
||||
return invoiceDate >= new Date(startDate) && invoiceDate <= new Date(endDate)
|
||||
})
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const page = params.page || 1
|
||||
const pageSize = params.page_size || 10
|
||||
const startIndex = (page - 1) * pageSize
|
||||
const endIndex = startIndex + pageSize
|
||||
const paginatedInvoices = filteredInvoices.slice(startIndex, endIndex)
|
||||
|
||||
// 返回 Promise 模拟异步请求
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
data: paginatedInvoices,
|
||||
total: filteredInvoices.length,
|
||||
page: page,
|
||||
page_size: pageSize
|
||||
})
|
||||
}, 300) // 模拟网络延迟
|
||||
})
|
||||
},
|
||||
getInvoiceById: (params = {}) => request.get('/invoice/detail', { params }),
|
||||
createInvoice: (data = {}) => request.post('/invoice/create', data),
|
||||
updateInvoice: (data = {}) => request.post('/invoice/update', data),
|
||||
deleteInvoice: (params = {}) => request.delete('/invoice/delete', { params }),
|
||||
updateInvoiceStatus: (data = {}) => request.post('/invoice/update-status', data),
|
||||
remindInvoice: (data = {}) => request.post('/invoice/remind', data),
|
||||
refundInvoice: (data = {}) => request.post('/invoice/refund', data),
|
||||
sendInvoice: (data = {}) => request.post('/invoice/send', data),
|
||||
// valuation (估值评估)
|
||||
getValuationList: (params = {}) => {
|
||||
// 模拟分页和搜索
|
||||
|
||||
167
web/src/views/transaction/receipts/index.vue
Normal file
167
web/src/views/transaction/receipts/index.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<script setup>
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import { NTag, NButton, NInput, NSelect, NDatePicker } from 'naive-ui'
|
||||
|
||||
import CommonPage from '@/components/page/CommonPage.vue'
|
||||
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
|
||||
import CrudTable from '@/components/table/CrudTable.vue'
|
||||
|
||||
import { formatDate } from '@/utils'
|
||||
import api from '@/api'
|
||||
|
||||
defineOptions({ name: '交易记录' })
|
||||
|
||||
const $table = ref(null)
|
||||
const queryItems = ref({})
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '未开票', value: 'pending' },
|
||||
{ label: '已开票', value: 'invoiced' },
|
||||
{ label: '已退款', value: 'refunded' },
|
||||
{ label: '已拒绝', value: 'rejected' },
|
||||
]
|
||||
|
||||
const invoiceTypeOptions = [
|
||||
{ label: '增值税普通发票', value: 'normal' },
|
||||
{ label: '增值税专用发票', value: 'special' },
|
||||
]
|
||||
|
||||
const ticketTypeOptions = [
|
||||
{ label: '纸质发票', value: 'paper' },
|
||||
{ label: '电子发票', value: 'electronic' },
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
$table.value?.handleSearch()
|
||||
})
|
||||
|
||||
const renderStatus = (status) => {
|
||||
const statusMap = {
|
||||
pending: { type: 'warning', text: '未开票' },
|
||||
invoiced: { type: 'success', text: '已开票' },
|
||||
refunded: { type: 'info', text: '已退款' },
|
||||
rejected: { type: 'error', text: '已拒绝' },
|
||||
}
|
||||
const config = statusMap[status] || { type: 'default', text: '未知' }
|
||||
return h(NTag, { type: config.type }, { default: () => config.text })
|
||||
}
|
||||
|
||||
const renderInvoiceType = (type) => {
|
||||
const typeMap = { normal: '增值税普通发票', special: '增值税专用发票' }
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
const renderTicketType = (type) => {
|
||||
const typeMap = { paper: '纸质发票', electronic: '电子发票' }
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', key: 'id', width: 60, align: 'center', render(row) { return row?.receipt?.id || '' } },
|
||||
{ title: '凭证时间', key: 'submitted_at', width: 120, align: 'center', render(row) { return formatDate(row.submitted_at) } },
|
||||
{ title: '公司名称', key: 'company_name', width: 160, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '公司税号', key: 'tax_number', width: 160, align: 'center', ellipsis: { tooltip: true } },
|
||||
{ title: '手机号', key: 'phone', width: 120, align: 'center' },
|
||||
{ title: '微信号', key: 'wechat', width: 120, align: 'center' },
|
||||
{ title: '开票类型', key: 'invoice_type', width: 120, align: 'center', render(row) { return renderInvoiceType(row.invoice_type) } },
|
||||
{ title: '供票类型', key: 'ticket_type', width: 100, align: 'center', render(row) { return renderTicketType(row.ticket_type) } },
|
||||
{ title: '备注', key: 'note', width: 140, align: 'center', render(row) { return row?.receipt?.note || '' } },
|
||||
{ title: '核验', key: 'verified', width: 80, align: 'center', render(row) { return h(NTag, { type: row?.receipt?.verified ? 'success' : 'warning' }, { default: () => (row?.receipt?.verified ? '已核验' : '待核验') }) } },
|
||||
{ title: '凭证', key: 'url', width: 100, align: 'center', render(row) { return h(NButton, { text: true, type: 'info', onClick: () => window.open(row?.receipt?.url, '_blank') }, { default: () => '打开' }) } },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonPage show-footer title="交易记录">
|
||||
<CrudTable
|
||||
ref="$table"
|
||||
v-model:query-items="queryItems"
|
||||
:columns="columns"
|
||||
:get-data="api.getReceiptList"
|
||||
>
|
||||
<template #queryBar>
|
||||
<QueryBarItem label="凭证时间" :label-width="80">
|
||||
<NDatePicker
|
||||
v-model:value="queryItems.created_at"
|
||||
type="daterange"
|
||||
clearable
|
||||
placeholder="请选择凭证时间"
|
||||
style="width: 280px"
|
||||
@update:value="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="手机号" :label-width="80">
|
||||
<NInput
|
||||
v-model:value="queryItems.phone"
|
||||
clearable
|
||||
type="text"
|
||||
placeholder="请输入手机号"
|
||||
style="width: 200px"
|
||||
@keypress.enter="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="微信号" :label-width="80">
|
||||
<NInput
|
||||
v-model:value="queryItems.wechat"
|
||||
clearable
|
||||
type="text"
|
||||
placeholder="请输入微信号"
|
||||
style="width: 200px"
|
||||
@keypress.enter="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="公司名称" :label-width="80">
|
||||
<NInput
|
||||
v-model:value="queryItems.company_name"
|
||||
clearable
|
||||
type="text"
|
||||
placeholder="请输入公司名称"
|
||||
style="width: 200px"
|
||||
@keypress.enter="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="公司税号" :label-width="80">
|
||||
<NInput
|
||||
v-model:value="queryItems.tax_number"
|
||||
clearable
|
||||
type="text"
|
||||
placeholder="请输入公司税号"
|
||||
style="width: 200px"
|
||||
@keypress.enter="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="状态" :label-width="80">
|
||||
<NSelect
|
||||
v-model:value="queryItems.status"
|
||||
:options="statusOptions"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@update:value="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="开票类型" :label-width="80">
|
||||
<NSelect
|
||||
v-model:value="queryItems.invoice_type"
|
||||
:options="invoiceTypeOptions"
|
||||
placeholder="请选择开票类型"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@update:value="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
<QueryBarItem label="供票类型" :label-width="80">
|
||||
<NSelect
|
||||
v-model:value="queryItems.ticket_type"
|
||||
:options="ticketTypeOptions"
|
||||
placeholder="请选择供票类型"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@update:value="$table?.handleSearch()"
|
||||
/>
|
||||
</QueryBarItem>
|
||||
</template>
|
||||
</CrudTable>
|
||||
</CommonPage>
|
||||
</template>
|
||||
9
估值字段.txt
9
估值字段.txt
@ -37,8 +37,8 @@
|
||||
|
||||
|
||||
export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
docker build -t zfc931912343/bindbox-game:v1.0 .
|
||||
docker push zfc931912343/bindbox-game:v1.0
|
||||
docker build -t zfc931912343/guzhi-fastapi-admin:v1.5 .
|
||||
docker push zfc931912343/guzhi-fastapi-admin:v1.5
|
||||
|
||||
|
||||
# 运行容器
|
||||
@ -68,4 +68,7 @@ docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4 &&
|
||||
|
||||
|
||||
|
||||
docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4 && docker rm -f guzhi && docker run -itd --name=guzhi -p 8080:80 -v ~/guzhi-data/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped --memory=2g --cpus=1.0 -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4
|
||||
docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4 && docker rm -f guzhi && docker run -itd --name=guzhi -p 8080:80 -v ~/guzhi-data/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped --memory=2g --cpus=1.0 -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4
|
||||
|
||||
|
||||
docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.5 && docker rm -f guzhi_dev && docker run -itd --name=guzhi_dev -p 9990:80 -v ~/guzhi-data-dev/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.5
|
||||
Loading…
x
Reference in New Issue
Block a user