Files
dealerhub/PROGRESS_BATCH_BC.md

251 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# dealerhub · 批次 B + C 实施报告(AI 差异化 + 商业化闭环)
> **依据**:《后续改造实施计划.md》批次 B(P1 AI 差异化 #7–9)与批次 C(P2 商业化基建 #10–13)。
> **结果**:两批次全部落地。全量回归 **268 passed**(批次 A 后 160 → 新增 108 例),
> `manage.py check` 0 issues,`makemigrations --check` 无待生成迁移,`npm run build` 通过,
> Playwright 真实浏览器逐项验证。**最小可售闭环达成**(P0 引擎 + 前端可见 + 套餐计费 + 官网 + 演示账套)。
---
## 批次 B · AI 差异化(新增 `apps/ai/`)
### B1 · AI 应收风险预警 ★ 主打差异化
**对外话术**:唯一做应收风控的进销存——竞品 AI 全在开单/问答,没人做应收风险。
**三因子加权评分(`apps/ai/risk.py`,纯统计,不依赖 LLM)**
| 因子 | 权重 | 算法 |
|---|---|---|
| 回款周期漂移 | 40% | 近 90 天平均回款天数 vs 历史基线(近一年、排除近期样本);比值 ≤1 不计分,1.0→1.5 线性映射到 0→100 |
| 欠款趋势 | 35% | 本月未结 vs 上月同口径;增长 0→50% 线性映射;上月无欠款本月有 → 50 分(新增欠款) |
| 开单频次骤降 | 25% | 有欠款但 >30 天未开单;30 天 0 分,90 天 100 分,中间线性 |
**分档**:high ≥70 / medium 40–69 / low <40。**回款周期用收款核销(Allocation)的最晚核销日 − 应收开单日**近似,未结清的不参与该因子(走欠款趋势因子)。
**落点**:
- `notify` 新增 `risk_score` 规则类型(threshold = 分数线,默认 70),`check_risk_score_alerts` 按(客户+当日)去重发预警,挂入 `run_all_alert_checks`
- 默认预警规则新增 `ai_risk_score`(阈值 70)
- API:`GET /ai/risk/ranking/`(TOP N)、`/ai/risk/customer/<id>/`(三因子明细)、`/ai/risk/advice/<id>/`(催收建议)、`POST /ai/risk/scan/`
- 前端:大盘"应收风险 TOP5"卡片(风险进度条 + 档位 + 未结 + 回款周期/欠款趋势两列理由 + 催收建议弹窗)
**LLM 只做增值**:有 KEY 时把统计理由交给 LLM 生成一句人话建议;无 KEY 时 `advice` 直接返回统计理由,`llm_enhanced=false`(不 500、不空转)。
### B2 · AI 开单(`apps/ai/orders.py`)
链路:`粘贴文本 → LLM 抽取 [{name,barcode,qty,unit}] → 匹配商品档案 → 前端确认 → 回填开单表`
**匹配优先级**:`barcode 精确(100) → 商品编码精确(100) → 品名精确(100) → 品名包含(85,取最短匹配名) → 相似度兜底(difflib ≥0.6)`;未命中的进 `unmatched` 单独返回。
**单位与取价**:抽取到的单位名映射到 `UnitConversion`,命中则返回该单位的 `unit_id/unit_name/price`(价 = 基本价 × rate),前端复用 A1 的取价与最低售价逻辑。
**无 KEY 行为**(验收要求):默认 **抛 `LlmUnavailable` → 400 + `code=llm_unavailable`**(明确报错,绝不 500);同时提供 `allow_rule_fallback=true` 走**规则兜底**(正则抓"商品名+数量+单位",支持中文数字"两箱/十瓶")——前端在 400 后弹确认框,用户可选择用兜底模式。
**配额**(`apps/ai/usage.py`):`AiUsage` 流水(租户+kind+账期),`check_quota` 超限抛 `QuotaExceeded` → **403 + `code=quota_exceeded` + 升级引导**。配额来源优先走 billing(见 C1),未装 billing 时回落 `settings.AI_FREE_MONTHLY_QUOTA`。
### B3 · AI 经营问答(`apps/ai/ask.py`)
**安全设计(计划硬要求:只读、不给 LLM 写权限)**:取数完全由**白名单工具**完成,LLM 只拿到聚合后的数字:
- 意图识别:`sales / receivable / inventory / purchase / profit / ranking / risk`,关键词命中;都没命中给"全景四项"
- 7 个工具全部只读 ORM 且强制 `tenant=tenant`(越权查他租户不可能,有专项测试)
- 无 LLM 时返回**统计口径结构化回答**(`llm_enhanced=false`),功能不空转
- 前端:大盘"🤖 老板参谋"入口 + 5 个预设问题 + 回答区(标注"AI 生成"或"统计口径")
---
## 批次 C · 商业化基建
### C1 · 套餐/试用/配额(新增 `apps/billing/`)
**套餐定义(`Plan.limits` 是唯一事实来源)**
| | 免费版 | 基础版 | 专业版 |
|---|---|---|---|
| 价格 | ¥0 | **¥998/月** | **¥4800/月** |
| 用户 / 商品 / 月单据 | 1 / 100 / 50 | 5 / 3000 / 2000 | 50 / 100000 / 不限 |
| AI 录单 / 问答 | 10 / 20 | 200 / 500 | 2000 / 5000 |
| 批次效期 / 总账 | ✓ / ✓ | ✓ / ✓ | ✓ / ✓ |
| 订货商城 | ✗ | ✗ | ✓ |
> 定价依据(清单 #10):卡在金蝶 698 与用友 1500 之间偏上,凭"带总账 + AI 风控"打差异。
**订阅(`Subscription`)**:`trial(30 天全功能) / active / expired`;`effective_limits()` 在试用过期或已过期时**回落 free 配额**(数据保留)。
**配额校验统一入口**:`billing.quota.check_and_count(tenant, kind, delta)`
- 数量型:`users` / `products` / `bills_monthly`
- 功能开关:`batch_managed` / `finance_ledger` / `storefront`
- AI 次数:委托 `apps.ai.usage.check_quota`(单一实现,billing 只提供配额数字)
- 超限 → `QuotaExceeded.as_dict()`:`{code, kind, detail, used, limit, plan_code, plan_name, upgrade_url}`
**真实拦截点(不是纸面)**:
1. `BaseTenantViewSet.acreate` 读 `quota_kind` 声明 → `ProductViewSet.quota_kind = "products"`
2. 销售/采购 `create-bill` 入口 → `bills_monthly`
3. AI 两个入口 → `ai_parse_order` / `ai_ask`
4. 前端 axios 拦截器 → 403 `quota_exceeded` 弹**升级引导弹窗**(列差距 + 跳套餐页,同 kind 60 秒内不重复弹)
**试用到期**:`billing.tasks.expire_trials()`(Dramatiq cron 每日)扫描试用与付费到期 → 降级 free + 发通知。
**API**:`GET /billing/plans/`(公开,官网价格页用)、`/billing/subscription/`(用量快照)、`POST /billing/subscribe/`、`GET /billing/quota/?kind=`。
**前端**:新页"套餐与用量"(当前套餐 + 试用倒计时 + 各配额进度条 + 功能开关 + 三档对比卡片一键切换)。
### C2 · 演示账套(`manage.py seed_demo`)
**食品经销商场景**,突出已交付能力:
- **5 个商品**:多单位(瓶/箱/件、箱/提)、批次管理(鲜奶 21 天/优倍 15 天保质期)、称重(kg 基本单位)、最低售价
- **5 个客户**:分层信用额度(3 万–8 万),含一个**风险户 C1005**
- **3 张进货单**:含两个批次(近效期 10 天 / 正常 14 天)用于演示临期预警
- **6 张销售单**:整件/散包混合录入 + 抹零(元/角两档)+ 批次商品 FEFO 跨批分摊 + 风险户大额赊销
- **5 笔历史应收**:覆盖账龄 **0-30 / 31-60 / 61-90 / 91-180 / 181-365** 五桶(账龄图不是一根柱)
- **幂等**:所有对象按 code/bill_no `update_or_create`;`--reset` 清空重建
**免注册体验**:
- `POST /api/v1/demo/enter/`(公开)→ 签发 2 小时 JWT + 返回只读提示;未初始化返回 503 + 明确指引
- **只读保护由中间件强制**(`DemoReadOnlyMiddleware`):租户 `demo` 的一切非 GET 请求 → 403 `demo_read_only`,**不依赖前端自觉**(手工 curl 也拦得住,有专项测试)
- 前端登录页"🎬 免注册体验演示账套"按钮 + 侧栏"只读演示"徽标 + 403 明确提示
### C3 · 营销官网(新增 `apps/website/`,`/site/*`)
| 页面 | 内容 | SEO |
|---|---|---|
| `/site/` 首页 | H1"AI 开单 + 应收风控的经销商进销存"、三大 AI 卖点(含"独家"标注)、6 项基础能力、**6 角色价值区块**、价格预览、演示入口 | title/desc/OG/canonical/JSON-LD(SoftwareApplication) |
| `/site/pricing/` | 三档卡片 + **功能对比矩阵**(数据源 = `Plan.limits`,与配额校验同源)+ 三条说明 | 同上 |
| `/site/industry/food/` | 食品:批次账 + FEFO + 临期预警 + 多单位 + 信用额度(**均为已交付**) | 行业关键词 |
| `/site/industry/hardware/` | 五金:换算率 + 三档抹零 + 最低售价 + 价格历史参考 | 同上 |
| `/site/industry/autoparts/` | 汽配:当前可用能力 + **SN 追溯明确标注"规划中/尚未交付"** | 同上 |
**"不写纸面功能"的执行**:价格页每个勾选对应已交付代码;行业页每条卖点映射到一个已有测试;未交付的(订货商城在专业版、汽配 SN)标注规划中。测试 `test_autoparts_page_marks_unshipped_as_planned` 专门守这条。
**移动端**:全站响应式(860px 断点),viewport meta,单文件内联 CSS(无外部依赖,加载快)。
---
## 验证汇总
### 1. 后端回归
```
$ python -m pytest tests/ -p no:cacheprovider
268 passed in 5.06s # 批次 A 后 160 → +108(B1 23 + B2/B3 25 + C1 25 + C2 14 + C3 21)
$ python manage.py check
System check identified no issues (0 silenced).
$ python manage.py makemigrations --check --dry-run
No changes detected
```
### 2. `npm run build`
```
✓ built in 5.84s
dist/assets/Dashboard-*.js 5.41 kB ← AI 风险卡 + 老板参谋
dist/assets/SalesBills-*.js 15.95 kB ← +AI 录单对话框
dist/assets/Billing-*.js ~6 kB ← 套餐与用量页
```
### 3. Playwright 真实浏览器(Granian ASGI)
| 场景 | 结果 |
|---|---|
| 大盘"应收风险 TOP5" | 王五批发 95 分/高风险/¥2,300,三因子理由分列(回款 45 天 vs 5 天、欠款 +53%、90 天未开单) |
| 催收建议弹窗 | 无 KEY → 显示统计口径理由,`llm_enhanced=false` |
| 老板参谋 | 预设"谁欠钱最多"→ 回答"本月销售额 ¥1,723.00(3 单);应收未结 ¥4,023.00,其中 30 天以上 ¥1,500.00;库存估值 ¥1,180.06;本月净利润 ¥1,182.27",标注"统计口径"、取数 4 项 |
| 免注册体验 | 登录页按钮 → 直进演示账套;侧栏显示"只读演示"徽标 |
| 演示只读保护 | 读接口 200(大盘/销售单/账龄/AI 风险),写接口 403 `demo_read_only` |
| 套餐与用量页 | 当前套餐"免费版/试用中"、试用期至 2026-10-10、商品 5/100 进度条、AI 录单 0/10、功能开关 ✓批次效期 ✓总账 ✗订货商城 |
| 官网首页 | H1 + 三大 AI 卖点(独家/省时/参谋)+ 6 能力卡 + 6 角色 + 价格预览 + CTA |
| 官网价格页 | 三档卡片 + 对比矩阵(1/5/50 用户、100/3,000/100,000 商品、专业版"不限"、订货商城 ✓/— 正确) |
### 4. 关键接口手测
```
POST /api/v1/demo/enter/ → 200 {tenant: demo, read_only: true, access: <JWT>}
POST /api/v1/catalog/products/ (demo) → 403 {code: demo_read_only}
GET /api/v1/ai/risk/ranking/ → 95 分客户 + 三因子理由
POST /api/v1/ai/ask/ → intents: [sales, receivable, inventory, profit]
POST /api/v1/ai/parse-order/ (无KEY) → 400 {code: llm_unavailable}
GET /api/v1/billing/plans/ → free/basic/pro(998 / 4800)
GET /api/v1/billing/quota/?kind=products → 403 + upgrade_url(满额时)
GET /site/pricing/ → 200 14KB(对比矩阵与 Plan.limits 同源)
```
---
## 变更文件清单
```
backend/
├── apps/ai/ [新] AI 应用
│ ├── risk.py 三因子应收风险评分(纯统计)
│ ├── orders.py 文本抽取 + 商品匹配 + 规则兜底
│ ├── ask.py 白名单只读工具 + 统计口径回答
│ ├── llm.py OpenAI 兼容客户端(无 KEY 全降级)
│ ├── usage.py AiUsage 流水 + 配额(异常类与 billing 统一)
│ ├── views.py·urls.py 7 个端点
│ └── models.py (转发 usage.AiUsage,满足 Django 发现约定)
├── apps/billing/ [新] 套餐与计费
│ ├── models.py Plan / Subscription + 三档预置 + 试用/升级
│ ├── quota.py check_and_count 统一入口 + 用量快照
│ ├── tasks.py 试用/订阅到期降级 + 通知
│ └── views.py·urls.py 4 个端点
├── apps/website/ [新] 营销官网
│ ├── views.py 首页/价格页/3 行业页(价格读 Plan.limits)
│ └── templates/website/ base / home / pricing / industry(全响应式 + SEO)
├── apps/core/demo.py [新] 免注册进入演示账套
├── apps/core/middleware.py [改] +DemoReadOnlyMiddleware
├── apps/core/viewset.py [改] acreate 配额拦截(quota_kind)
├── apps/core/management/commands/seed_demo.py [新] 幂等演示账套
├── apps/catalog/views.py [改] ProductViewSet.quota_kind
├── apps/sales/views.py·purchase/views.py [改] 建单入口配额校验
├── apps/notify/models.py·services.py [改] risk_score 规则 + 扫描 + 默认规则
├── config/settings/base.py [改] +apps.ai/billing/website + AI 配置
├── config/urls.py [改] +/site/ 与 /demo/enter/
├── apps/*/migrations/ [新] ai / billing
└── tests/test_ai_risk.py·test_ai_order_ask.py·test_billing.py·test_demo.py·test_website.py [新] 108 例
frontend/src/
├── pages/Billing.vue [新] 套餐与用量(用量进度 + 三档对比)
├── pages/Dashboard.vue [改] +应收风险 TOP5 + 老板参谋对话框
├── pages/SalesBills.vue [改] +AI 录单对话框(含兜底确认)
├── pages/Login.vue [改] +免注册体验按钮
├── layouts/MainLayout.vue [改] +只读演示徽标 + 套餐菜单
├── api/client.js [改] +配额升级引导 + 只读演示 403 提示
└── router/index.js [改] +/billing
frontend/dist/ [重建]
```
---
## 踩坑记录(供后续参考)
1. **异常类跨模块复用**:`billing.QuotaExceeded` 与 `ai.QuotaExceeded` 必须能被视图层一处 catch。做法是 ai 侧继承 billing 的类;但父类构造签名不同,`__init__` 里要直接调 `Exception.__init__`,否则 TypeError(本轮踩到)。
2. **服务层自生成单号 vs 幂等**:`create_sales_bill` / `create_purchase_bill` 自己生成单号,演示数据要固定单号只能创建后 `update(bill_no=...)` 回写。**若后续有更多幂等需求,建议给服务层加可选 `bill_no` 参数**。
3. **Django 模型发现约定**:模型必须定义在 `models.py`(或 `models/` 包)里才会被 `makemigrations` 发现;放在 `usage.py` 需要加一个 `models.py` 转发。另外 app 目录**必须有 `migrations/` 包**,否则 `makemigrations` 静默报 "No changes detected"。
4. **DRF `url_path` 唯一**:同一路径不能挂两个 action(后者覆盖前者,前者变 405)。同路径多方法要合并成一个 action 按 `request.method` 分派。
5. **DRF 保留参数 `format`**:`?format=` 会走内容协商,找不到对应 renderer 直接 404。自定义输出格式请换参数名(本轮用 `?view=html`)。
6. **adrf async 视图**:惰性 FK 访问与 `refresh_from_db()` 都必须包在 `sync_to_async` 里,否则 `SynchronousOnlyOperation`。
7. **Granian on Windows**:不支持多 worker(自动回落 1);改动后重启务必 `netstat` 确认端口只有一个 PID,否则旧实例抢答会让"改了没生效"。
8. **演示数据要留额度余量**:`seed_demo` 的历史应收会占用客户信用额度,幂等重跑时可能被信用引擎拦住——演示客户额度要给足(本轮从 3 万提到 3–8 万)。
---
## 里程碑对照(计划 §五)
| 里程碑 | 计划内容 | 实际 |
|---|---|---|
| M-A | 批次 A 完成,前端接入全部 P0 能力 | ✅ `PROGRESS_BATCH_A.md`,160 passed |
| M-B | 批次 B 完成,三个 AI 卖点可演示(含无 KEY 降级) | ✅ 本报告,B1/B2/B3 全部可演示,降级路径有专项测试 |
| M-C | 批次 C 完成,**最小可售闭环达成** | ✅ 官网可访问、价格页公开、演示账套可进、free 版配额真实拦截 |
| M-D | 订货商城/移动开单随第一个付费客户上线 | ⏳ 未开始(批次 D,计划中即为"按客户驱动") |
**最小可售闭环 = P0 引擎 + 批次 A 前端 + B1 风控 + 批次 C** → **已达成**。
---
## 下一步(批次 D,按客户驱动)
1. **D1 B2B 订货商城 H5**(`apps/storefront/`):客户授权可见商品 + 等级价复用 `quote_price` + 自助下单生成草稿 + 接 C1 配额(`storefront` 开关已就位)
2. **D2 业务员移动开单**:H5 复用 D1 框架 + A1 取价/单位/抹零组件
3. **D3 行业专版**:序列号 SN(3C)/ 辅助属性(服装)/ 套装拆件(建材)
4. **D4 税率**:`SalesBillLine.tax_rate` + 价内价外口径 + 凭证科目拆分(**多币种建议从话术删除**,清单问题 7 要求)
5. **打印扩展**:三联送货单 / 58mm 小票 / 对账单二维码印在销售单上(串起 A3 分享链路)
> 注意:D 系列启动前建议先把**线上(192.168.5.7)镜像重建**,让批次 A/B/C 的成果先上线,再按客户反馈决定 D 的优先级。