Files
dealerhub/PROGRESS_BATCH_BC.md

17 KiB
Raw Permalink Blame History

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 的优先级。