本文把 DGJ2.0 中“配置报价、计算价格、机器人询价、人工报价、生成报价单、转销售单”六个阶段拆开说明。它们共享客户和商品数据,但不是同一个业务对象。

1. 业务目标

报价域解决的是:服务站面对不同客户和商品时,如何在可售、可查、可报价的前提下,快速给出可解释的价格,并让客户继续下单。

具体目标包括:

  1. 按服务站建立多套报价规则。
  2. 将客户关联到一套生效规则。
  3. 为规则下每个商品选择报价方式。
  4. 支持最近销售价、指导价、自定义价和组合兜底。
  5. 支持固定价与基于指导价/采购价的动态加价。
  6. 限制机器人对指定客户可展示的分类和品牌范围。
  7. 从 VIN、图片、关键词和车型中识别询价需求。
  8. 自动报价失败时转人工待报价。
  9. 将待报价转成正式报价单,再继续转销售单。
  10. 记录价格变更、最近报价和迁移过程,便于解释与追踪。

2. 一句话业务地图

flowchart LR
  A["客户与商品基础数据"] --> B["报价规则:怎么算价"]
  A --> C["报价范围:允许机器人展示什么"]
  B --> D["价格中心计算候选价"]
  C --> E["智能询价筛选候选商品"]
  D --> E
  E --> F{"是否自动得到可展示价格"}
  F -->|是| G["机器人/IM 返回报价"]
  F -->|否| H["待报价申请"]
  H --> I["人工填写价格"]
  I --> J["正式报价单"]
  G --> K["客户选择商品"]
  J --> K
  K --> L["销售单/采购补货"]

3. 六个核心概念不能混用

概念回答的问题主要表生命周期
报价规则这个客户的这个商品按什么方式算价t_quote_rule*长期配置
报价基础商品算价需要的指导价、采购价、库存等快照是什么t_quote_goods_base_{sid%64}持续同步
报价范围机器人允许给客户展示哪些分类/品牌t_scm_quote_range*长期配置
智能询价会话客户当前问了什么车、什么配件Robot V1/V2 会话和上下文约 1 小时
会话人工报价坐席在当前会话给某商品回复的价格CV_CTX_QUOTED_PRICES 上下文会话/当天查询
正式报价单可继续转销售单的业务单据t_scm_offer_order*业务单据

3.1 最容易犯的三个错误

  1. 把“机器人查询到了商品”理解成“商品一定有报价”。查询范围与价格规则是两次独立判断。
  2. 把 IM 工作台 saveQuote 保存的会话价格理解成正式报价单。它首先写会话上下文并发消息,不等同于落 SCM_OFFER_ORDER。
  3. 把 quote_type 理解成最终实际价格来源。组合规则可能配置为“最近售价 > 自定义价”,但最终实际使用的是自定义价,因此还要看 real_quote_type。

4. 系统边界

flowchart TD
  subgraph OPS["服务站后台"]
    Rule["报价规则管理"]
    Range["报价范围管理"]
    Apply["待报价管理"]
    Workbench["IM 人工工作台"]
  end
  subgraph DGJ["DGJ 领域服务"]
    QM["QuoteManagerSer"]
    PM["PriceManagerSer"]
    SI["SmartInquirySer / RobotV2"]
    OAS["OfferOrderApplySer"]
  end
  subgraph Data["数据层"]
    RuleData["规则/客户/商品/价格"]
    RangeData["范围/客户"]
    ConvData["会话/上下文"]
    OfferData["报价单"]
  end
  subgraph External["外部系统"]
    VIN["VIN/车型/物料服务"]
    IM["腾讯 IM / 微信机器人"]
    SAAS["SAAS 询价/销售"]
    Item["商品与价格数据源"]
  end
  Rule --> QM
  Range --> SI
  Apply --> OAS
  Workbench --> SI
  QM --> RuleData
  PM --> RuleData
  SI --> RangeData
  SI --> ConvData
  OAS --> OfferData
  SI --> VIN
  SI --> IM
  OAS --> SAAS
  Item --> RuleData

5. 主要代码入口

5.1 控制器

场景代码路径责任
报价规则后台application/controllers/basedata/QuoteManager.php规则、客户、商品、价格方式、导入导出、变更记录
内部价格接口application/controllers/inner/PriceManager.php检查有价、获取完整价格和 V2 扩展
销售报价搜索application/controllers/sale/Offer.php商品、车型、属性搜索
快速报价application/controllers/sale/RapidOffer.phpVIN、图片、OE、标准名称和精准搜索
微信报价范围application/controllers/moveMall/Wechat.php范围模板、配置、客户绑定
待报价application/controllers/moveMall/OfferApply.php列表、作废、转正式报价、转销售、发微信群
IM 人工报价application/controllers/moveMall/ImWorkbench.php商品价格信息、保存会话报价、查询会话/今日报价
内部特殊价application/controllers/inner/scm/InvSa.php现金价、充值价

5.2 核心 Service

Service责任
application/Services/BaseData/QuoteManagerSer.phpV2 规则 CRUD、客户绑定、商品规则、导入导出、快速定价、变更事件
application/Services/BaseData/QuoteManagerBaseSer.php价格计算、最近价、自定义价、动态加价和解释
application/Services/BaseData/PriceManagerSer.php新旧价格体系编排、场景价、完整价格
application/Services/MoveMall/SmartInquirySer.php微信机器人智能询价主链
application/Services/MoveMall/WechatSer.php报价范围及客户配置
application/Services/MoveMall/OfferOrderApplySer.php待报价、正式报价单、转销售单
application/Services/MoveMall/RobotV2/*多平台会话、意图、查询、状态和输出
application/Services/Ai/AiSer.php报价单消息发送等旧 AI 能力

6. 核心表总览

6.1 V2 报价规则域

逻辑表物理表用途关键字段
报价规则t_quote_rule规则主表sid,id,code,name,is_delete
规则客户t_quote_rule_contact一个客户关联规则sid,quote_rule_id,contact_id
规则商品t_quote_rule_goods_{sid%64}商品报价方式和配置quote_type,custom_price,price_base
报价基础商品t_quote_goods_base_{sid%64}算价快照guide_price,last_settle_price,stock_qty
客户价格t_contact_price_{sid%64}最近销售价/最近报价sale_price,offer_price,quote_rule_id
特殊价格t_quote_goods_special_price充值价、现金价sid,quote_rule_id,inv_id
变更事件t_quote_change_event一次显式/隐式价格变化sid,uid,event_type,event_time
变更明细t_quote_change_event_detail_{sid%64}商品前后价格和配置before_price,after_price,before_config
站点版本t_quote_station_versionV1/V2 当前版本current_version,migrate_session_id
迁移会话t_quote_migrate_session站点迁移状态migrate_type,status,error_msg
迁移日志t_quote_migrate_session_log分步骤日志session_id,step,content,error_msg

6.2 报价范围域

表用途关键字段
t_scm_quote_range范围模板主表sid,name,is_system,remark
t_scm_quote_range_config人可读的分类/品牌组合quote_range_id,category_ids,brand_ids
t_scm_quote_range_config_info展开后的分类品牌明细category_id,brand_id,quote_config_id
t_scm_quote_range_contact客户关联范围sid,quote_range_id,contact_id
t_scm_robot_quote_config旧机器人报价配置主表服务站、客户和配置元数据
t_scm_robot_quote_config_info旧机器人分类品牌白名单contact_id,category_id,brand_id
t_scm_robot_quote_replay_config机器人报价回复配置回复模板/配置

6.3 报价单与会话域

表/存储用途关键字段
t_scm_robot_offer_order_apply机器人待报价申请bu_id,bill_no,inv_id,num,status,conversation_key
t_scm_offer_order正式报价单主表sid,buId,billNo,billDate,obsolete
t_scm_offer_order_info报价单明细billNo,invId,skuId,price,buId,obsolete
t_robot_conversation_tencent_im腾讯 IM 会话sid,contact_id,status,created_at
会话 ContextRobotV2 键值上下文查询结果、车型、已报价价格

7. 分表与金额单位

7.1 分表规则

flowchart LR
  A["sid"] --> B["sid % 64"]
  B --> C["t_quote_rule_goods_N"]
  B --> D["t_quote_goods_base_N"]
  B --> E["t_contact_price_N"]
  B --> F["t_quote_change_event_detail_N"]

每次查询分表后仍要带 sid 条件。不能只依赖分片后缀,因为同一物理分片包含多个服务站。

7.2 金额单位

层单位证据
quote_rule_goods 自定义价分Model 字段注释、convertFen2Yuan
quote_goods_base 指导价/采购价分Model 字段注释
getGoodsPrices() 返回元方法注释和转换
PC 批量设置参数元校验范围 0.01~999999.99,入库前转换
IM saveQuote.price元字符串格式化为两位小数后写上下文
正式报价单 price需结合历史表 DDL 确认旧链路直接写前端 salePrice,生产口径待核验

金额排查必须先确认单位,禁止用“看起来像价格”猜测。

8. 报价规则数据模型

erDiagram
  QUOTE_RULE ||--o{ QUOTE_RULE_CONTACT : binds
  QUOTE_RULE ||--o{ QUOTE_RULE_GOODS : configures
  BS_CONTACT ||--o| QUOTE_RULE_CONTACT : assigned
  BS_GOODS ||--o{ QUOTE_RULE_GOODS : priced
  QUOTE_GOODS_BASE ||--|| BS_GOODS : snapshot_of
  QUOTE_RULE ||--o{ CONTACT_PRICE : groups_history
  BS_CONTACT ||--o{ CONTACT_PRICE : owns_history
  BS_GOODS ||--o{ CONTACT_PRICE : has_history
  QUOTE_CHANGE_EVENT ||--o{ QUOTE_CHANGE_EVENT_DETAIL : contains

8.1 业务约束

约束代码语义
规则名站内不能重复新增/编辑会按 sid + name + is_delete=0 检查
一个客户应只关联一套报价规则算价时查询单个 quote_rule_id;生产唯一键需确认
一个规则商品应只有一条配置业务键 sid + quote_rule_id + inv_id;批量 upsert 依赖数据库约束
删除规则是主表软删、关系和商品硬删ruleDelete 更新 is_delete 后删除关联
系统默认规则可在读列表时初始化ruleList() 先调用 initDefaultQuoteRule()

9. 报价方式字典

枚举:application/KzData/Enums/QuoteRuleGoodsEnums.php

quote_type文案价格来源无来源时
0不报价无has_price=false
1使用最近销售价当前客户最近销售价无价格
2使用指导价报价基础商品 guide_price指导价为 0 时无价格
3使用自定义价固定或动态 custom_price自定义价为 0 时无价格
4最近销售价(共用)当前客户优先,否则其他客户无可用历史则无价格
5最近销售价(同组)当前规则下客户的最近售价无可用历史则无价格
6同组最近售价 > 自定义价先同组最近售价,失败用自定义价两者都无则无价格
7客户最近售价 > 自定义价先当前客户最近售价,失败用自定义价两者都无则无价格

9.1 quote_type 与 real_quote_type

flowchart TD
  A["配置 quote_type"] --> B{"是否组合规则 6/7"}
  B -->|否| C["按配置来源取价"]
  B -->|是| D["先查对应最近售价"]
  D -->|找到| E["real_quote_type = 实际最近售价类型"]
  D -->|未找到| F["计算自定义价"]
  F -->|找到| G["real_quote_type = 3"]
  F -->|未找到| H["has_price = false"]
  C --> I["返回结果"]
  E --> I
  G --> I
  H --> I

排查前端文案或下单价格时,应同时记录 quote_type、real_quote_type、price 和 strategy。

10. 自定义价模型

10.1 两种自定义价

类型custom_price_type参数算法
固定价1fix_price直接使用固定金额
动态价2price_base,price_method,add_amount/add_percent,round_type基础价加金额或百分比后取整

10.2 动态价基础

price_base文案数据来源
1指导价quote_goods_base.guide_price
2采购价quote_goods_base 当前/最近采购价字段,具体选择由计算实现决定

10.3 加价方式

金额加价:raw = base_price + add_amount
比例加价:raw = base_price * (1 + add_percent / 100)

取整类型:

round_type结果
1四舍五入到整数
2四舍五入保留 1 位小数
3四舍五入到十位整数

10.4 计算链

flowchart TD
  A["读取规则商品配置"] --> B{"固定价还是动态价"}
  B -->|固定价| C["校验 fix_price 范围"]
  B -->|动态价| D{"指导价还是采购价"}
  D --> E["读取报价基础商品价格"]
  E --> F{"加金额还是加百分比"}
  F --> G["计算 raw price"]
  G --> H["按 round_type 取整"]
  C --> I["写 custom_price(分)"]
  H --> I

动态价是“配置变化后计算并保存最终 custom_price”还是“每次查询实时计算”,要结合具体更新入口看。buildQuoteRuleData() 和价格变更处理会重算,基础价变化后的异步/任务链必须纳入排查。

11. 最终价格计算主流程

核心:QuoteManagerBaseSer::getGoodsPrices()。

flowchart TD
  A["sid + contactId + invIds"] --> B["初始化所有商品 has_price=false"]
  B --> C{"是否显式传 quoteRuleId"}
  C -->|否| D["按 sid + contactId 查关联规则"]
  C -->|是| E["使用指定规则"]
  D --> F{"是否有规则"}
  E --> F
  F -->|否| Z["全部无价格"]
  F -->|是| G["从 t_quote_rule_goods_{sid%64} 查配置"]
  G --> H["按 quote_type 分组"]
  H --> I["查询最近销售价"]
  H --> J["查询指导价"]
  H --> K["计算/读取自定义价"]
  K --> L["附加充值价和现金价"]
  I --> M["组合规则执行兜底"]
  J --> N["合并结果"]
  L --> N
  M --> N
  N --> O["返回元、has_price、价格区间、配置/实际类型"]

11.1 返回结构

{
  "<invId>": {
    "has_price": true,
    "price": "128.00",
    "min_price": "120.00",
    "max_price": "135.00",
    "quote_type": 6,
    "real_quote_type": 5,
    "recharge_price": "126.00",
    "cash_price": "130.00"
  }
}

示例值只表达结构,不代表真实业务价格。

12. 最近销售价的三种范围

名称范围业务目的
当前客户最近售价sid + contactId + invId保持对单个客户的报价连续性
共用最近售价当前客户优先,再取站内其他客户无客户历史时仍可报价
同组最近售价当前报价规则关联客户范围连锁/客户组共享策略
flowchart TD
  A["需要最近售价"] --> B{"quote_type"}
  B -->|1| C["只查当前客户"]
  B -->|4| D["当前客户 -> 站内其他客户"]
  B -->|5| E["规则关联客户组"]
  B -->|6| E
  B -->|7| C
  C --> F["按最近变更时间和有效天数取值"]
  D --> F
  E --> F

站点最近价有效天数由配置读取;代码支持 days=0 表示不限制历史。排查时要记录实际 days 和起始时间,不能固定认为都是 90 天。90 天主要出现在管理端统计和导出文案。

13. 价格扩展信息

getGoodsPricesExt() 不直接等同最终价,它给前端展示可解释的候选信息:

字段含义
customPrice当前规则商品自定义价
recentPrice服务站维度最近售价
recentPriceDay距离该价格的天数
recentGroupPrice同规则组最近售价
recentGroupPriceDay距离天数
recentSalePrice当前客户最近售价
recentSalePriceDay距离天数
quote_strategy_strPriceManager 最终策略的人类可读文案
flowchart LR
  A["invIds"] --> B["站点最近售价"]
  C["contactId"] --> D["客户关联 quoteRuleId"]
  D --> E["自定义价"]
  D --> F["同组最近售价"]
  C --> G["客户最近售价"]
  B --> H["prices_ext"]
  E --> H
  F --> H
  G --> H

14. PriceManager 新旧版本编排

内部价格接口先调用 PriceManagerSer::getFullPrice(),再判断 QuoteManagerSer::isV2QuoteRule($sid)。

14.1 V2 判断

flowchart TD
  A["查 t_quote_station_version"] --> B{"存在记录且 current_version=2"}
  B -->|否| C["按 V1/旧价格逻辑"]
  B -->|是| D{"migrate_session_id 是否为空"}
  D -->|是| E["V2 生效"]
  D -->|否| F["查迁移会话 status"]
  F -->|3 成功| E
  F -->|其他/异常| C

isV2QuoteRule() 捕获异常后返回 false,因此数据库异常可能表现为静默回退 V1,而不是接口报错。排查新旧价格不一致时必须查该方法日志和版本表。

14.2 内部接口

检查是否有价格

POST /inner/pricemanager/checkhasprice
Content-Type: application/json

{
  "sid": 10001,
  "contactId": 20001,
  "invIds": [30001, 30002],
  "scene": "normal"
}

响应核心:

{
  "list": [
    {"inv_id": 30001, "has_price": true},
    {"inv_id": 30002, "has_price": false}
  ]
}

获取完整价格

POST /inner/pricemanager/getprices
Content-Type: application/json

{
  "sid": 10001,
  "contactId": 20001,
  "invIds": [30001],
  "scene": "offer"
}

返回包含 prices、is_v2;V2 时还包含 prices_ext。内部接口的真实鉴权由网关/基类决定,示例未包含凭证。

15. 报价规则后台 API

控制器路径按 CodeIgniter 规则映射,部署网关可能统一大小写或前缀,联调以真实路由为准。

15.1 接口总表

方法路径用途
POST/basedata/quotemanager/isv2quoterule查询站点报价版本
POST/basedata/quotemanager/rulelist规则列表,读时初始化默认规则
POST/basedata/quotemanager/ruledetail规则详情
POST/basedata/quotemanager/ruleset新增/编辑规则
POST/basedata/quotemanager/rulecopy复制规则和商品配置
POST/basedata/quotemanager/ruledelete删除规则及关系
POST/basedata/quotemanager/contactselectlist可关联客户列表
POST/basedata/quotemanager/rulecontactset保存客户关联
POST/basedata/quotemanager/selectgoodslist报价基础商品列表
POST/basedata/quotemanager/batchsetquotetype批量设置报价方式
POST/basedata/quotemanager/batchsetcustomprice批量设置自定义价
POST/basedata/quotemanager/importcustomfixprice导入固定价
POST/basedata/quotemanager/exportgoods导出规则商品
POST/basedata/quotemanager/recentquotelist最近报价列表
POST/basedata/quotemanager/getcalprocess查看价格计算过程
POST/basedata/quotemanager/quotechangelist价格变更事件
POST/basedata/quotemanager/getchangeloglist变更明细
POST/basedata/quotemanager/getquickpricelist快速定价数据
POST/basedata/quotemanager/batchquickfixprice按统计价批量固定价

15.2 新增规则请求

POST /basedata/quotemanager/ruleset
Content-Type: application/json

{
  "sid": 10001,
  "id": 0,
  "name": "连锁客户报价",
  "description": "同组最近售价优先"
}

后端生成规则编码,站内有效规则名不能重复。

15.3 批量设置报价方式

POST /basedata/quotemanager/batchsetquotetype
Content-Type: application/json

{
  "sid": 10001,
  "user_id": 90001,
  "quote_rule_id": 40001,
  "inv_ids": [30001, 30002],
  "quote_type": 6
}

流程:校验规则和最大设置数,读取报价基础商品及旧配置,构建 upsert 数据,按 2000 条分批写入,最后写价格变更事件。

15.4 设置动态自定义价

POST /basedata/quotemanager/batchsetcustomprice
Content-Type: application/json

{
  "sid": 10001,
  "user_id": 90001,
  "quote_rule_id": 40001,
  "inv_ids": [30001],
  "custom_price_type": 2,
  "fix_price": 0,
  "price_base": 1,
  "price_method": 2,
  "add_amount": 0,
  "add_percent": 8,
  "round_type": 1
}

16. 规则 CRUD 流程

sequenceDiagram
  participant UI as PC 报价规则
  participant C as QuoteManager Controller
  participant S as QuoteManagerSer
  participant R as t_quote_rule
  participant RC as t_quote_rule_contact
  participant RG as t_quote_rule_goods_N
  UI->>C: ruleSet(name, description)
  C->>S: ruleSet
  S->>R: 检查同名并新增/更新
  UI->>C: ruleContactSet(contactIds)
  C->>S: 保存客户关系
  S->>RC: 删除旧关系/批量建立新关系
  UI->>C: batchSetQuoteType/customPrice
  C->>S: 配置商品
  S->>RG: upsert 商品报价配置
  S->>S: 写价格变更事件

16.1 删除规则

ruleDelete() 在事务中:主规则 is_delete=1,客户关系硬删除,规则商品分表硬删除。风险是历史 contact_price.quote_rule_id、报价变更事件和已生成报价不会同步删除,历史查询必须容忍规则主表已软删。

17. 客户绑定规则

17.1 业务语义

一个客户是否能得到 V2 报价,首先取决于 t_quote_rule_contact 是否存在关联。没有关联时 getGoodsPrices() 直接返回所有商品无价格,即使规则商品表中已经配置价格。

flowchart TD
  A["客户请求报价"] --> B["按 sid + contactId 查规则"]
  B -->|无关联| C["V2 规则不提供价格"]
  B -->|有关联| D["查该规则下商品配置"]
  D -->|无商品行| C
  D -->|有商品行| E["按 quote_type 计算"]

17.2 变更客户规则的影响

  • 客户下一次报价会使用新规则。
  • contact_price 中历史行带有旧/新 quote_rule_id,同组价格范围会变化。
  • 正在进行中的机器人会话是否重新取规则,取决于查询时点和缓存。
  • 已发出的会话报价和正式报价单不应被追溯改价。

18. 商品配置与基础商品

报价规则商品不是商品主数据。t_quote_goods_base_{sid%64} 是为了报价查询建立的站点级基础快照,包含分类、品牌、SKU、指导价、采购价、销量、库存和限价信息。

flowchart LR
  A["商品中心/BS_GOODS"] --> B["QuoteGoodsBase 任务同步"]
  C["指导价/结算价"] --> B
  D["库存"] --> B
  E["销量/车型适配"] --> B
  B --> F["t_quote_goods_base_N"]
  F --> G["规则商品选择"]
  F --> H["自定义动态价计算"]
  F --> I["报价范围分类品牌"]

报价异常时必须先判断是规则错,还是基础商品快照过期。直接改规则无法修复错误的指导价、采购价或库存快照。

19. 快速定价

快速定价从大数据价格分布中提供候选值,包括最低/最高销售价、客户数最多价、订单数最多价、省/市/全国高频价等,随后由用户批量设置固定价。

候选字段含义
min_sale_price销售最低价
max_sale_price销售最高价
most_contact_price客户数最多价
most_order_price订单数最多价
province_max_price省维度高频/最高销量价
city_max_price市维度高频/最高销量价
national_max_price全国维度高频/最高销量价
flowchart TD
  A["大数据价格统计"] --> B["getQuickPriceList"]
  B --> C["用户选择 use_price 字段"]
  C --> D["batchQuickFixPrice"]
  D --> E["写规则商品固定自定义价"]
  E --> F["写价格变更事件"]

统计价是配置建议,不是下单时实时价。批量应用前要看数据日期、样本量和异常价格。

20. 报价范围模型

报价范围只控制机器人查询/展示边界,不负责计算最终价格。

erDiagram
  QUOTE_RANGE ||--o{ QUOTE_RANGE_CONFIG : contains
  QUOTE_RANGE_CONFIG ||--o{ QUOTE_RANGE_CONFIG_INFO : expands
  QUOTE_RANGE ||--o{ QUOTE_RANGE_CONTACT : assigned
  BS_CONTACT ||--o| QUOTE_RANGE_CONTACT : uses

20.1 系统范围

系统模板通过特殊 ID 表达。客户没有显式范围关联时,代码将其理解为系统默认范围;绑定系统模板时会删除客户显式关系。

20.2 分类和品牌的全部标识

配置支持特殊 TAG_ALL:

分类品牌语义
全部全部系统默认全范围,配置行可能不落明细
指定分类全部品牌允许分类下全部品牌
全部分类指定品牌允许该品牌跨分类,但组合重复有额外校验
指定分类指定品牌精确组合

batchQuoteRangeConfig() 会先删除该范围原配置和明细,再全量重建。因此调用必须在事务中,单独调用时也要确认外层事务。

21. 报价范围 API

方法路径用途
POST/movemall/wechat/getquoterangeselect范围下拉,包含系统模板
POST/movemall/wechat/getquoterangelist范围列表和客户数
POST/movemall/wechat/setquoterange新增/编辑范围和配置
POST/movemall/wechat/bindquoterangecontact客户绑定范围
POST/movemall/wechat/getquoterangedetail范围详情
POST/movemall/wechat/configset同时更新范围和客户机器人配置

21.1 新建范围请求

POST /movemall/wechat/setquoterange
Content-Type: application/json

{
  "sid": 10001,
  "uid": 90001,
  "id": 0,
  "name": "轮胎重点品牌",
  "remark": "仅用于指定客户群",
  "quote_config": [
    {
      "category_ids": [501],
      "brand_ids": [601, 602]
    }
  ]
}

21.2 保存流程

sequenceDiagram
  participant UI as 报价范围页面
  participant W as WechatSer
  participant Range as 范围主表
  participant Config as 配置主表
  participant Info as 展开明细
  UI->>W: setQuoteRange
  W->>W: 校验同范围内分类+品牌组合无重复
  W->>Range: 新增/更新主表
  W->>Config: 删除当前范围旧配置
  W->>Info: 删除当前范围旧明细
  loop 每组配置
    W->>Config: 插入逗号形式配置
    W->>Info: 展开并 batchInsertOrIgnore
  end
  W-->>UI: commit

22. 旧机器人白名单

RobotQuoteConfigInfoModel::getQuoteWhiteList() 返回:

[category_id => [brand_id => true]]

空配置表示全部允许;-1 可表示全部分类或全部品牌。这个“空即全部”语义在迁移、删除和异常时非常重要:误删明细可能把限制范围扩大成全量,而不是变成无权限。

flowchart TD
  A["读取客户白名单"] --> B{"配置是否为空"}
  B -->|是| C["全部允许"]
  B -->|否| D{"命中 全分类+全品牌"}
  D -->|是| C
  D -->|否| E{"命中 全分类+指定品牌"}
  E -->|是| C
  E -->|否| F{"命中 指定分类+全品牌/指定品牌"}
  F -->|是| C
  F -->|否| G["过滤商品"]

23. 智能询价总流程

sequenceDiagram
  participant Customer as 客户
  participant IM as 微信/腾讯IM
  participant Adapter as 消息适配器
  participant Robot as SmartInquiry/RobotV2
  participant VIN as VIN/车型服务
  participant Goods as 商品查询
  participant Range as 报价范围
  participant Price as PriceManager
  participant Apply as 待报价
  Customer->>IM: VIN/图片/关键词/语音
  IM->>Adapter: 标准化消息和 msgId
  Adapter->>Robot: 建立/恢复会话
  Robot->>VIN: 识别 VIN 或车型
  VIN-->>Robot: 单车型/多车型/失败
  Robot->>Goods: 按车型和关键词查商品
  Goods-->>Robot: 候选商品
  Robot->>Range: 按客户过滤分类/品牌
  Robot->>Price: 获取库存和价格
  alt 有可展示商品和价格
    Robot->>IM: 商品询价结果
  else 需要人工
    Robot->>Apply: 创建待报价申请
    Robot->>IM: 告知等待人工报价
  end

24. 输入识别

输入识别方式主要分支
17 位 VIN 文本VIN_REGEX,排除销售/出库单前缀解析车型,可能多车型
VIN 图片图片下载/OCR/VIN 解析图片失败、无 VIN、VIN 无车型
商品关键词关键词、同义词、分类别名分类、品牌、规格、方位
车型描述车型解析和确认唯一车型或用户选择
原厂产品码/OE产品码查询精确/多结果/无结果
无车商品无车关键词和属性轮胎、电池、油品等特定流程
语音语音转文本后进入关键词链转写失败或歧义

会话有效期常量为 3600 秒,报价回复时间为 600 秒。旧链路还存在 MSGTTL=600、LINKTTL=7200,不同链路的超时含义不能混用。

25. 会话状态与动作

25.1 会话类型

值含义
0无效/未知
1VIN 会话
2意图+分类,已标记废弃
3无车查询
4无车电池

25.2 动作

值动作
0不处理
1查询
2下单
3只允许下单,不允许重新查询
4多车型选择
stateDiagram-v2
  [*] --> WaitingInput
  WaitingInput --> ParsingVehicle: VIN/车型
  WaitingInput --> ParsingKeyword: 商品词
  ParsingVehicle --> WaitingCarSelect: 多车型
  WaitingCarSelect --> Querying: 用户选择
  ParsingVehicle --> Querying: 唯一车型
  ParsingKeyword --> Querying
  Querying --> Quoted: 找到商品和价格
  Querying --> WaitingManualQuote: 无价/需人工
  Quoted --> PlacingOrder: 用户回复序号
  Quoted --> Querying: 继续询价
  WaitingManualQuote --> Quoted: 人工报价
  PlacingOrder --> Completed
  WaitingInput --> Expired: 超时
  Quoted --> Expired: 超时

26. 自动报价决策

flowchart TD
  A["候选商品"] --> B{"客户是否允许询报价"}
  B -->|否| X["不自动报价/提示未开通"]
  B -->|是| C{"商品在报价范围"}
  C -->|否| Y["过滤"]
  C -->|是| D{"商品可售且库存规则通过"}
  D -->|否| Y
  D -->|是| E["调用价格中心"]
  E --> F{"has_price=true"}
  F -->|是| G["展示价格和商品"]
  F -->|否| H{"是否允许人工待报价"}
  H -->|是| I["创建待报价申请"]
  H -->|否| J["只展示无价/联系人工提示"]

自动报价不是只调用一个价格方法,还受客户开关、范围、商品状态、库存、限价和展示规则影响。

27. 待报价申请

表:t_scm_robot_offer_order_apply。

27.1 状态

状态枚举含义
0STATUS_PENDING等待人工报价
1STATUS_PASS已转有效报价
2STATUS_INVALID作废
stateDiagram-v2
  [*] --> Pending: 机器人/询价创建
  Pending --> Pass: 人工填写价格并转报价单
  Pending --> Invalid: 人工作废
  Pending --> Pass: 直接转销售流程中的有效化
  Pass --> [*]
  Invalid --> [*]

27.2 列表接口

POST /movemall/offerapply/quotations
Content-Type: application/json

{
  "page": 1,
  "rows": 20,
  "buId": 20001,
  "status": 0
}

列表会关联商品返回 SKU 和名称,并可合并客户最近正式报价。

27.3 转正式报价请求

控制器接收 postData JSON 字符串:

POST /movemall/offerapply/pass
Content-Type: application/x-www-form-urlencoded

postData=[{"id":70001,"salePrice":128.00}]

校验要求每项都有整数 id 和浮点 salePrice。

28. 待报价转正式报价单

sequenceDiagram
  participant UI as 待报价页面
  participant S as OfferOrderApplySer
  participant Apply as 待报价表
  participant Contact as 客户
  participant Goods as 商品
  participant Offer as 正式报价主/明细
  UI->>S: passOrders(id, salePrice)
  S->>Apply: 仅查 status=Pending 的申请
  S->>Contact: 校验客户存在且启用
  S->>Goods: 读取 SKU、热销/指导价等信息
  S->>S: 校验价格业务限制
  S->>Apply: 批量更新为 Pass
  S->>Offer: 按申请 bill_no 生成主单
  S->>Offer: 插入报价明细和 salePrice
  S-->>UI: 报价单生成成功

28.1 关键业务校验

  • 申请仍为等待报价,防止重复转单。
  • 客户未删除、未禁用。
  • 所选申请要满足同客户等组合约束。
  • 热销商品售价不能超过指导价的代码分支需按当前规则验证。
  • 主单和明细创建成功后才能将申请视为完成。

28.2 并发风险

代码先查 Pending,再批量更新和插入。若没有行锁、条件更新或业务唯一键,两个请求可能同时读到 Pending。生产必须确认:

  1. offer_order_apply.id 条件更新是否带 status=0。
  2. 正式报价主单 billNo 是否唯一。
  3. 报价明细是否有来源申请唯一键。
  4. 事务是否覆盖申请更新和两张报价表插入。

29. 正式报价单

29.1 主从结构

erDiagram
  OFFER_ORDER ||--o{ OFFER_ORDER_INFO : contains
  BS_CONTACT ||--o{ OFFER_ORDER : receives
  BS_GOODS ||--o{ OFFER_ORDER_INFO : quoted
  OFFER_ORDER_APPLY }o--|| OFFER_ORDER_INFO : converted_to

29.2 obsolete 多重语义

旧报价链路使用 obsolete 区分有效、待报价和作废等状态。代码中可见:

  • findOrderInfoByIds() 查询 obsolete=0 且未删除的有效明细。
  • findByBillNo() 查询 obsolete=2 的待报价数据。
  • 转销售时会将相应报价主/明细更新为有效或已处理状态。

完整枚举未在统一 Enums 中集中定义,生产历史值和前端文案需要结合 DDL/页面确认。这是待治理点。

30. 正式报价转销售单

sequenceDiagram
  participant UI as 报价/待报价页面
  participant Apply as OfferOrderApplySer
  participant Offer as 报价单
  participant InvSa as InvSaService
  participant Sale as 销售单
  participant MQ as SAAS 状态消息
  UI->>Apply: toSaleOrder(postData)
  Apply->>Apply: 校验待报价未重复转单
  Apply->>Offer: 激活/读取报价主从数据
  Apply->>InvSa: 构建销售开单商品和报价来源
  InvSa->>Sale: 创建销售单/后续出库链
  Apply->>Apply: 更新待报价状态
  Apply->>MQ: 同步 SAAS 订单状态

30.1 数量和价格

机器人等待报价转销售时,代码注释明确“数量使用报价单数量”。排查转单差异要对比:待报价 num、正式报价明细数量、销售单数量、库存可用数量和最终成交价。

30.2 状态一致性

若销售单创建成功但待报价状态更新失败,可能再次转单;若本地事务提交但 SAAS MQ 失败,外部状态会滞后。必须用来源报价单号/申请 ID 做幂等和补偿。

31. IM 工作台人工报价

31.1 获取商品报价信息

工作台会返回库存、销售价和今日已报价。今日报价来自同一 sid + contactId 当天所有腾讯 IM 会话的 CV_CTX_QUOTED_PRICES,同物料取最新时间。

31.2 保存报价 API

POST /movemall/imworkbench/savequote
Content-Type: application/json

{
  "conversation_id": 80001,
  "inv_id": 30001,
  "price": "128.00",
  "msg_id": "source-message-id"
}

31.3 数据流

sequenceDiagram
  participant UI as IM 工作台
  participant C as ImWorkbench
  participant Conv as 会话表
  participant Context as ConversationContext
  participant IM as IM Center
  UI->>C: saveQuote(conversation, inv, price, msgId)
  C->>Conv: 验证会话存在且 active
  C->>Context: 读取历史查询商品和车型
  C->>C: 找不到时按 invId 补查商品
  C->>Context: 覆盖 invId 对应 quoted price
  C->>IM: 发送 product_inquiry_result
  IM-->>C: 成功或异常
  C-->>UI: success=true, message_sent=bool

31.4 上下文结构

{
  "invId_30001": {
    "invId": 30001,
    "price": "128.00",
    "quotedAt": "2026-01-01 10:00:00",
    "quotedBy": "operator"
  }
}

31.5 已识别的一致性风险

上下文先保存,IM 发送异常被内部 catch,接口仍返回业务成功并通过 message_sent=false 表达发送失败。因此:

  • 页面若只看顶层成功提示,坐席可能以为客户已收到。
  • 重试发送会再次覆盖 quotedAt,今日最新报价时间变化。
  • 没有看到基于 msg_id + inv_id 的本地唯一幂等记录。
  • 会话报价没有自动落正式报价单。

32. IM 报价消息结构

{
  "bizType": "product_inquiry_result",
  "bizContext": {
    "conversationId": 80001,
    "conversationStatus": 1,
    "sourceMsgId": "source-message-id"
  },
  "bizData": {
    "inquiryOrderInfo": {"id": 80001},
    "products": [
      {
        "categoryName": "示例分类",
        "items": [
          {
            "invId": 30001,
            "name": "示例商品",
            "price": "128.00",
            "usage": "1",
            "tags": []
          }
        ]
      }
    ],
    "carInfo": null
  }
}

sourceMsgId 用于把报价结果关联回客户原消息,不能用新的发送消息 ID 替代。

33. 今日报价查询

flowchart TD
  A["sid + contactId"] --> B["查当天创建的全部 IM 会话"]
  B --> C["逐会话读取 CV_CTX_QUOTED_PRICES"]
  C --> D["按 invId 合并"]
  D --> E["同商品取 quotedAt 最新"]
  E --> F["按时间倒序返回"]

风险:按会话 created_at 判断“今日”,跨日但仍活跃会话中的当日报价可能被排除;服务器时区必须一致;字符串时间比较依赖统一格式。

34. 最近报价与最近销售价更新

t_contact_price_{sid%64} 同时保存:

  • sale_price / sale_last_change_time
  • offer_price / offer_last_change_time
  • quote_rule_id
flowchart LR
  A["正式报价/报价变更"] --> B["offer_price + time"]
  C["销售成交"] --> D["sale_price + time"]
  E["客户规则变更"] --> F["quote_rule_id"]
  B --> G["t_contact_price_N"]
  D --> G
  F --> G
  G --> H["未来报价规则计算"]

需要继续确认每种报价入口是否都更新 offer_price。IM 会话上下文本身不等于 contact_price,不能默认会参与未来最近报价统计。

35. 价格变更事件

35.1 显式与隐式

event_type含义例子
1显式变更用户批量修改报价方式或自定义价
2隐式变更指导价/采购价变化导致动态价重算
sequenceDiagram
  participant Source as 用户操作/基础价变更
  participant QM as QuoteManagerSer
  participant RuleGoods as 规则商品
  participant Event as 变更事件
  participant Detail as 变更明细分表
  Source->>QM: 触发价格重算
  QM->>RuleGoods: 读取 before
  QM->>RuleGoods: 写 after
  QM->>Event: 新建 event
  QM->>Detail: 保存 before/after 价格与配置

变更日志用于解释“为什么今天报价变了”,但前提是所有入口都调用统一事件记录。直接 SQL、迁移或旧链路修改可能没有事件。

36. 导入导出

36.1 固定价导入

模板至少包含:

字段必填校验
物料编码是必须匹配报价基础商品
自定义价是0.01~999999.99 元
充值价特定模板金额和业务开关
现金价特定模板金额和业务开关

36.2 导出字段

可包含 SKU、分类、品牌、商品、系列、月销量、适配车系、车型价格范围、采购价、指导价、近 90 天销售价/报价、自定义定价方式、最终自定义价和报价方式。

导入导出通用安全参见 20_导入导出和Excel模板.md。报价专题额外要验证金额单位、公式单元格、重复 SKU、规则归属和变更事件。

37. 版本迁移

stateDiagram-v2
  [*] --> V1
  V1 --> Queued: 创建迁移会话
  Queued --> Migrating
  Migrating --> Success: status=3
  Migrating --> Failed: status=4
  Success --> V2: current_version=2 且会话成功
  Failed --> V1
  V2 --> V1: 回切

迁移内容包括客户分类、客户关系、加价/固定价规则、非价格规则、最近销售价规则、规则商品和站点版本。详细执行治理见 40_数据修复_迁移脚本与临时任务治理.md。

38. 关键缓存和异步链

类型用途风险
Redis 站点开关/白名单新旧页面、APP 报价、微信群询报价缓存与数据库版本不一致
车型品牌系列缓存报价筛选下拉1 小时缓存可能延迟基础数据变化
机器人会话缓存/上下文当前 VIN、车型、商品和报价TTL、并发覆盖、跨日
价格变更处理基础价变化后重算自定义价消息丢失会保留旧价
SAAS 状态 MQ报价/转销售状态同步本地成功外部滞后

39. 完整排查入口

用户报告“商品没有价格”时,按以下顺序排查:

flowchart TD
  A["确认 sid/contactId/invId/scene"] --> B{"站点实际走 V1 还是 V2"}
  B --> C{"客户是否关联报价规则"}
  C -->|否| X["补规则关联或确认应无价"]
  C -->|是| D{"规则商品分表是否有配置"}
  D -->|否| Y["同步基础商品并配置报价方式"]
  D -->|是| E{"quote_type 是否为 0"}
  E -->|是| Z["业务配置为不报价"]
  E -->|否| F{"所需价格源是否存在且有效"}
  F -->|否| G["检查最近价时间/指导价/采购价/自定义价"]
  F -->|是| H{"PriceManager 是否被场景、限价、库存覆盖"}
  H -->|是| I["检查完整价格策略"]
  H -->|否| J{"机器人是否又被报价范围过滤"}
  J -->|是| K["检查范围和客户绑定"]
  J -->|否| L["检查接口缓存和前端展示"]

40. 场景化排查表

现象首查再查
PC 有价,机器人无商品报价范围、客户询报价开关分类/品牌 ID、机器人白名单
PC 有商品但无价客户规则、规则商品 quote_type最近价有效期、基础指导/采购价
同客户两端价格不同请求 scene、V1/V2活动/VIP/现金充值价覆盖
配置自定义价后没变化分片、金额单位、变更是否成功动态价重算、缓存、版本回退
最近销售价不对contact_price 时间和客户报价方式 1/4/5 的范围差异
待报价重复生成申请业务唯一键、会话重试消息幂等和并发
转报价单重复Pending 条件更新和锁报价单号唯一键
IM 显示报价成功但客户没收到message_sentIM Provider 日志、sourceMsgId
今日报价查不到会话创建日期和时区上下文 key/JSON/会话 ID
迁移后又走旧价站点版本和迁移会话状态isV2QuoteRule 异常静默回退

41. 只读 SQL 模板

41.1 站点版本

SELECT sid, current_version, migrate_session_id, operator, error_msg, modify_time
FROM t_quote_station_version
WHERE sid = :sid;

SELECT id, sid, migrate_type, status, start_time, end_time, error_msg
FROM t_quote_migrate_session
WHERE id = :migrate_session_id;

41.2 客户规则

SELECT rc.sid, rc.contact_id, rc.quote_rule_id, r.name, r.is_delete
FROM t_quote_rule_contact rc
LEFT JOIN t_quote_rule r
  ON r.sid = rc.sid AND r.id = rc.quote_rule_id
WHERE rc.sid = :sid
  AND rc.contact_id = :contact_id;

41.3 商品规则

SELECT sid, quote_rule_id, inv_id,
       quote_type, custom_price_type,
       fix_price, custom_price,
       price_base, price_method, round_type,
       add_amount, add_percent
FROM <t_quote_rule_goods_sid_mod_64>
WHERE sid = :sid
  AND quote_rule_id = :quote_rule_id
  AND inv_id IN (:inv_ids);

41.4 基础商品

SELECT sid, inv_id, sku_id, category_id, brand_id,
       guide_price, last_settle_price, current_settle_price,
       has_stock, stock_qty, is_limit_price, modify_time
FROM <t_quote_goods_base_sid_mod_64>
WHERE sid = :sid
  AND inv_id IN (:inv_ids);

41.5 客户历史价

SELECT sid, contact_id, inv_id, quote_rule_id,
       sale_price, sale_last_change_time,
       offer_price, offer_last_change_time
FROM <t_contact_price_sid_mod_64>
WHERE sid = :sid
  AND contact_id = :contact_id
  AND inv_id IN (:inv_ids)
ORDER BY GREATEST(sale_last_change_time, offer_last_change_time) DESC;

41.6 报价范围

SELECT r.id, r.name, c.category_ids, c.brand_ids
FROM t_scm_quote_range r
LEFT JOIN t_scm_quote_range_config c
  ON c.sid = r.sid AND c.quote_range_id = r.id
WHERE r.sid = :sid
  AND r.id = :quote_range_id;

SELECT quote_range_id, contact_id
FROM t_scm_quote_range_contact
WHERE sid = :sid
  AND contact_id = :contact_id;

41.7 待报价和正式报价

SELECT id, bu_id, bill_no, inv_id, num, status, conversation_key, created_time
FROM t_scm_robot_offer_order_apply
WHERE id IN (:apply_ids);

SELECT o.id, o.sid, o.buId, o.billNo, o.billDate, o.obsolete,
       i.id AS info_id, i.invId, i.skuId, i.price, i.obsolete AS info_obsolete
FROM t_scm_offer_order o
JOIN t_scm_offer_order_info i ON i.billNo = o.billNo AND i.sid = o.sid
WHERE o.sid = :sid
  AND o.billNo = :bill_no;

SQL 仅作字段和关系模板,生产 DDL、字段名和分片必须先核验。

42. 代码检索命令

rg -n "function getGoodsPrices|getRecentSalePrices|calculateCustomPrice" \
  application/Services/BaseData/QuoteManagerBaseSer.php

rg -n "function ruleSet|batchSetQuoteType|batchSetCustomPrice|addChangeEventRecord" \
  application/Services/BaseData/QuoteManagerSer.php

rg -n "getQuoteRange|setQuoteRange|batchQuoteRangeConfig|bindQuoteRangeContact" \
  application/controllers/moveMall/Wechat.php application/Services/MoveMall/WechatSer.php

rg -n "applyOffer|passOrders|toSaleOrders|invalidOrders" \
  application/Services/MoveMall/OfferOrderApplySer.php

rg -n "saveQuote|getTodayQuotes|CV_CTX_QUOTED_PRICES|sourceMsgId" \
  application/controllers/moveMall/ImWorkbench.php application/Services/MoveMall/RobotV2

rg -n "QUOTE_TYPE_|CUSTOM_PRICE_TYPE_|PRICE_BASE_|ROUND_TYPE_" \
  application/KzData/Enums/QuoteRuleGoodsEnums.php

43. 故障树

flowchart TD
  A["报价结果错误"] --> B{"是查不到商品还是没有价格"}
  B -->|查不到商品| C["商品搜索/车型/VIN/报价范围/可售库存"]
  B -->|有商品无价格| D["版本/客户规则/商品配置/价格源"]
  B -->|价格数值错误| E["金额单位/quote_type/real_quote_type/基础价/取整"]
  B -->|回复未送达| F["会话状态/IM发送/sourceMsgId/message_sent"]
  B -->|无法转单| G["申请状态/客户状态/报价单/库存/销售校验"]
  C --> H["确认具体失败层"]
  D --> H
  E --> H
  F --> H
  G --> H

44. 已识别风险

编号风险影响
R41-01isV2QuoteRule() 异常返回 false数据库异常可能静默切回旧价格
R41-02客户规则唯一性依赖生产约束多关联时取值可能不确定
R41-03规则商品 64 分表必须先 setSid漏设 sid 返回空并仅记日志
R41-04getGoodsPrices() 有动态 SQL IN 拼接invIds 必须严格为整数数组
R41-05特殊价查询默认站点配置始终 true所有自定义价都会查特殊价表,语义需确认
R41-06报价范围“空配置=全部”误删配置会扩大商品可见范围
R41-07范围更新先删后建非事务调用或中途失败可能形成空范围
R41-08规则列表读时初始化默认规则读接口有写副作用,并发需唯一键
R41-09规则删除硬删客户和商品关系历史价格和事件仍引用旧规则
R41-10待报价先查再改并发可能重复转正式报价
R41-11正式报价 obsolete 枚举分散状态理解和兼容容易出错
R41-12IM 报价先保存上下文再发消息消息失败但本地显示已有今日报价
R41-13IM 发送失败仍返回顶层报价成功前端若忽略 message_sent 会误导坐席
R41-14今日报价按会话创建日筛选跨日长会话的当日报价可能遗漏
R41-15会话报价未确认写 contact_price不一定参与最近报价和正式业务
R41-16V1/V2、旧范围和新规则并存同一请求可能因开关/迁移状态走不同链路
R41-17报价基础商品为异步快照价格/库存过期会导致规则正确但结果错误
R41-18金额单位跨层不同分元混用可造成百倍价格错误

45. 回归矩阵

45.1 报价方式

  • [ ] 不报价返回 has_price=false。
  • [ ] 当前客户最近售价有值和无值。
  • [ ] 共用最近售价的客户优先和站内兜底。
  • [ ] 同组最近售价只取规则关联客户。
  • [ ] 指导价为正和为 0。
  • [ ] 固定自定义价上下边界。
  • [ ] 动态价按指导价/采购价。
  • [ ] 金额加价和百分比加价。
  • [ ] 三种取整方式。
  • [ ] 组合规则 6/7 的最近价命中和自定义兜底。
  • [ ] 现金价、充值价为正和为 0。

45.2 规则配置

  • [ ] 新增、编辑、同名冲突、复制、删除。
  • [ ] 一个客户从旧规则切到新规则。
  • [ ] 商品批量设置 1 条、2000 条边界和超限。
  • [ ] 导入成功、部分错误、重复 SKU、金额格式错误。
  • [ ] 价格变更事件前后值和操作人正确。
  • [ ] 基础价变化触发动态价隐式变更。

45.3 报价范围

  • [ ] 系统默认范围。
  • [ ] 指定分类+指定品牌。
  • [ ] 指定分类+全部品牌。
  • [ ] 全部分类+指定品牌。
  • [ ] 重复组合拒绝。
  • [ ] 客户切换范围和恢复系统模板。
  • [ ] 空配置不会被误理解为禁止全部。

45.4 智能询价

  • [ ] VIN 文本、VIN 图片、单车型、多车型、解析失败。
  • [ ] 关键词、同义词、品牌、规格、方位。
  • [ ] 无车轮胎、电池、油品。
  • [ ] 有商品有价、有商品无价、无商品、有价无库存。
  • [ ] 客户未开通询报价。
  • [ ] 会话 10 分钟报价等待和 1 小时过期。
  • [ ] 用户回复序号下单。

45.5 待报价和转单

  • [ ] 创建待报价、重复消息幂等。
  • [ ] 作废后不能通过。
  • [ ] 已通过不能重复通过。
  • [ ] 客户禁用/删除。
  • [ ] 热销商品价格限制。
  • [ ] 多明细同客户合并规则。
  • [ ] 正式报价主从数量、价格一致。
  • [ ] 转销售数量、价格、来源单号一致。
  • [ ] SAAS 消息失败后的补偿。

45.6 IM 人工报价

  • [ ] 会话不存在、非 active、商品不存在。
  • [ ] 价格 0、负数、非数字和超大值。
  • [ ] 缺 msg_id。
  • [ ] 同物料重复报价取最新。
  • [ ] 上下文保存成功、IM 发送成功。
  • [ ] 上下文成功、IM 发送失败时页面明确提示。
  • [ ] 跨日会话的今日报价。
  • [ ] 并发坐席对同物料报价。

46. 改动风险和最小回归

改动位置最小回归
QuoteRuleGoodsEnums规则管理、价格计算、导入导出、前端文案、迁移
getGoodsPrices()所有 8 种报价方式、PriceManager、机器人、销售开单
最近价查询当前客户/共用/同组、有效期、分表、历史价格
动态价算法两种基础、两种加价、三种取整、基础价变更
报价范围系统范围、客户绑定、分类品牌组合、机器人过滤
SmartInquiry/RobotV2VIN/关键词/查询/报价/下单/IM 输出
OfferOrderApplySer申请、作废、通过、正式报价、转销售、SAAS MQ
ImWorkbench会话上下文、消息发送、今日报价和并发
站点版本V1、迁移中、V2、失败回退和回切

47. 生产待确认项

  1. t_quote_rule_contact 是否有 sid + contact_id 唯一索引。
  2. t_quote_rule_goods_N 是否有 sid + quote_rule_id + inv_id 唯一索引。
  3. t_quote_goods_special_price 的站点开关为何在代码中固定为 true。
  4. 正式报价主从表 price 的生产金额单位和完整 obsolete 枚举。
  5. 待报价转正式报价时数据库事务和并发锁的真实范围。
  6. 待报价、正式报价和销售单之间的生产唯一来源字段。
  7. 各报价入口更新 t_contact_price.offer_price 的完整清单。
  8. IM 会话人工报价是否应同步正式报价单或客户最近报价。
  9. 价格基础商品同步任务的调度频率、水位和失败告警。
  10. V1/V2 的实际站点比例和回退开关。
  11. 微信群、腾讯 IM、APP、PC 使用 PriceManager 的场景参数差异。
  12. SAAS 报价状态 MQ 的 routing key、重试、死信和消费者幂等。
  13. VIN/车型/物料服务的超时、缓存和降级策略。
  14. 报价范围新旧表的流量归属和迁移完成度。
  15. 报价、限价、活动价、VIP 价在最终 getFullPrice() 中的完整优先级。

48. 证据索引

主题代码路径
报价方式枚举application/KzData/Enums/QuoteRuleGoodsEnums.php
待报价状态application/KzData/Enums/OfferOrderApplyEnums.php
会话常量application/KzData/Enums/MoveRobotEnums.php
规则 APIapplication/controllers/basedata/QuoteManager.php
规则实现application/Services/BaseData/QuoteManagerSer.php
最终价格算法application/Services/BaseData/QuoteManagerBaseSer.php
价格编排application/Services/BaseData/PriceManagerSer.php
内部价格 APIapplication/controllers/inner/PriceManager.php
范围 APIapplication/controllers/moveMall/Wechat.php
范围实现application/Services/MoveMall/WechatSer.php
智能询价application/Services/MoveMall/SmartInquirySer.php
RobotV2application/Services/MoveMall/RobotV2/
待报价 APIapplication/controllers/moveMall/OfferApply.php
待报价实现application/Services/MoveMall/OfferOrderApplySer.php
IM 人工报价application/controllers/moveMall/ImWorkbench.php
报价商品分表application/models/bs/QuoteRuleGoodsModel.php
客户价格分表application/models/bs/ContactPriceModel.php
迁移application/Services/BaseData/QuoteManagerMigrate.php

49. 最终理解

flowchart LR
  A["规则正确"] --> F["可解释报价"]
  B["范围正确"] --> F
  C["商品/价格快照及时"] --> F
  D["会话和消息可靠"] --> F
  E["报价单与销售转化幂等"] --> F

报价系统的正确性不是“算出一个数字”这么简单。它要求:客户拿到的是自己应有的规则,商品处于允许展示的范围,价格来源和有效期可解释,人工与自动报价不会互相覆盖,报价转销售时数量和价格不变,并且任何一次价格变化都能追溯到规则、基础价、操作人或历史成交。

请求-日志-数据变更追踪卡

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
规则配置basedata/QuoteManager/inner/PriceManagersid、客户/商品范围、基准价、加价规则、有效期Quote/Price ControllerQuoteManagerSer/PriceManagerSerquote rule ID报价规则和适用范围生效
人工报价sale/Offer/RapidOffer客户、VIN/车型、商品、数量、报价Offer ControllerOffer/Quote Serviceoffer order ID保存人工报价和明细
智能询价Wechat/OfferApply/RobotV2/AI会话、VIN、车型、关键词、客户MoveMall/Robot ControllerSmartInquirySer/AI/Robot Serviceconversation/apply ID识别需求并产生候选报价
报价转销售inner/scm/InvSaoffer ID、选中商品、数量、确认价格Inner InvSaSaOrderSeroffer + sale bill报价快照转销售订单

日志证据矩阵

| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 规则命中 | QuoteManager/PriceManager Service | request_id、sid、customer、SKU、rule ID | 唯一可解释规则和基准价 | 多规则冲突、过期/范围误命中 | rule ID 进入报价计算 | | 智能识别 | SmartInquiry/Robot/AI | conversation/request ID、VIN、scenario | 候选商品和置信/来源可追踪 | VIN 解析/AI 超时、场景错 | request ID 关联 offer apply | | 报价落库 | Offer Service | offer ID、SKU、price source、operator | 主明细 commit | 人工/自动覆盖、价格过期 | offer ID 查 SCM_OFFER_ORDER | | 转销售 | InvSa/SaOrderSer | offer ID、sale billNo、SKU、price/qty | 数量价格快照保持一致 | 重复转换、转单价格被重算 | offer/sale 双向关系 |

环节数据变更台账

步骤代码位置事务读取事实写入表/缓存/MQ字段或数量变化回查证据
保存规则QuoteManagerSer配置事务原规则、范围、有效期报价规则/范围表、缓存rule/version/status old -> newrule ID、操作人、更新时间
计算报价PriceManagerSer/SmartInquiry只读基础价、规则、客户、历史成交查询日志/上下文quote = base + rule adjustment,业务表不变base/rule ID、计算明细
保存报价Offer/OfferApply Service报价事务候选商品和最终人工/自动选择SCM_OFFER_ORDER/明细/申请insert;price/source/validUntil 快照offer ID、SKU、操作者
人工覆盖ImWorkbench/Offer Service报价事务当前版本、人工输入报价明细/操作记录price old -> new,source -> manual,版本 +1before/after、operator
转销售inner/scm/InvSa -> SaOrderSer销售事务有效报价、未转数量SCM_SA_ORDER*、报价关系sale qty/price = confirmed offer;converted qty +noffer+sale+SKU 数量价格

子模块追踪:quote-rule 报价规则配置

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
保存规则基础资料新建/编辑/启停规则rule ID、scope、formula、valid timeapplication/controllers/basedata/QuoteManager.php -> application/Services/BaseData/QuoteManagerSer.php原规则、优先级、时间/范围冲突和使用情况配置本地事务 rule/version/status old -> new,同步关系request ID + rule/version + operator冲突/旧版本零写入;停用不改历史报价快照
缓存生效保存后报价仍用旧规则rule ID、cache/versionapplication/Services/BaseData/QuoteManagerBaseSer.phpDB 当前规则、缓存和更新时间commit 后事务外 cache old -> latest/deleted,DB 不变rule + DB/cache version + hitDB 正确只补缓存;不重复保存规则

子模块追踪:quote-customer 客户绑定与报价范围

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
绑定范围规则绑定客户/客户组/站点rule ID、contact/customer IDs、scopeapplication/Services/BaseData/QuoteManagerSer.php客户有效性、原范围、重叠规则和优先级关系本地事务 old customer set -> new setrequest ID + rule + customer count全量替换失败整批回滚;同客户冲突按优先级显式处理
命中回查客户报价未命中预期规则customer/sid、rule candidatesapplication/models/bs/ContactPriceModel.php客户关系、范围、有效期和规则顺序查询只读 不写;输出命中链request ID + customer + candidate/chosen rule先修关系/优先级,不直接覆盖最终价格

子模块追踪:quote-goods 商品配置与基础商品

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
配置商品规则增删商品/分类/范围rule ID、SKU/invId、goods type/statusapplication/controllers/inner/PriceManager.php -> application/Services/BaseData/PriceManagerSer.php商品可用、基础价、原关系和重复键规则商品本地事务 old set/status -> newrequest ID + rule + SKU + count无基础商品/重复零写入;下架不改历史报价
商品回查规则存在但商品未命中rule ID、SKU、goods relationapplication/models/bs/QuoteRuleGoodsModel.php商品关系、有效状态、分类和基础价查询只读 不写rule + SKU + relation/base price商品配置与客户范围分别核对,不混为一层

子模块追踪:quote-calculate 最终价格计算

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
计算报价查询/下单前计算价格sid/customer、SKU、qty、scene/timeapplication/Services/BaseData/PriceManagerSer.php基础价、命中规则、客户范围、调整公式和边界计算查询只读;final=base+rule adjustment,记录计算上下文request ID + base/rule IDs + inputs/result无价/公式非法明确失败,不写业务表
提交复核保存正式报价/转销售前quote/offer ID、current rule versionapplication/Services/MoveMall/SmartInquirySer.php当前计算结果、人工覆盖、有效期和数量报价本地事务固化 final price/source/version none -> snapshotoffer + SKU + final/source/version页面旧价以提交复核为准;历史快照不随规则变化

子模块追踪:quote-recent 最近报价与最近销售价

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
最近价查询报价页面查看客户商品历史价customer/sid、SKU、time/order limitapplication/models/bs/ContactPriceModel.php有效报价、销售明细、软删/退货和时间口径查询只读 不写;分别返回 recent quote/sale pricerequest ID + customer/SKU + source bill/time最近报价不等于成交价;明确来源和日期
历史回查最近价异常offer/sale billNo、SKUapplication/Services/BaseData/PriceManagerSer.php报价状态、销售成交快照和排序字段查询只读 不写source billNos + created/business time排除取消/无效单;不修改历史价迎合页面

子模块追踪:smart-input 智能询价输入识别

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
解析输入文本/微信/IM 询价conversation/request ID、raw text hashapplication/Services/MoveMall/SmartInquirySer.php用户站点、历史上下文、SKU/车型/数量词和媒体结果解析事务外/会话本地事务写 normalized items none -> parsedrequest + conversation + parsed item count无法识别进入待报价/澄清,不伪造商品
解析回查SKU/数量识别错误request ID、input/output tokensapplication/Services/Ai/AiSer.php原输入摘要、AI 结果版本和人工修正查询只读;人工修正本地事务 parsed old -> correctedrequest + before/after normalized items敏感输入脱敏;旧 AI 回调不覆盖人工修正

子模块追踪:smart-decision 自动报价决策

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
决策识别商品后判断自动/人工报价request/customer、items、sceneapplication/Services/MoveMall/SmartInquirySer.php商品匹配、规则命中、价格完整性、风险/置信阈值决策查询只读;记录 decision auto/manual/clarifyrequest + item + rule/confidence + decision任一无价/低置信进入人工,不返回假价格
自动结果auto 生成报价回复/草稿request/offer ID、pricesapplication/Services/MoveMall/OfferOrderApplySer.php最终计算、有效期和幂等申请报价本地事务 none -> auto quoted/pendingrequest + offer/apply + SKU pricesIM 发送失败只补消息;报价记录不重复生成

子模块追踪:offer-apply 待报价申请

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
创建申请无法自动报价转人工待办apply ID、conversation/request、itemsapplication/controllers/moveMall/OfferApply.php -> application/Services/MoveMall/OfferOrderApplySer.phpnormalized items、用户、重复请求和已有正式报价申请本地事务 insert 主明细,status none -> pendingrequest ID + apply/conversation + item count相同 request 幂等;解析不完整保留澄清态
分派处理工作台领取/退回待报价apply ID、agent、action/versionapplication/controllers/moveMall/ImWorkbench.phppending/processing、owner 和版本本地事务 pending -> processing/completed/rejected、owner 更新apply + agent + action/version并发领取仅一人成功;旧操作零覆盖

子模块追踪:offer-order 正式报价单

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
保存报价人工/自动确认正式报价offerNo、apply ID、SKU、price/qty/validUntilapplication/controllers/sale/Offer.php -> application/Services/MoveMall/OfferOrderApplySer.php申请、商品、最终价、有效期和重复来源报价本地事务 insert 主明细/关系,status none -> validrequest ID + offer/apply + SKU/price价格/商品非法整单回滚;来源申请只转一次
编辑失效修改/关闭报价offerNo、action/versionapplication/controllers/sale/RapidOffer.php当前有效态、已转数量和版本本地事务 price/status/version old -> new;已转部分不回退request ID + offer + old/new version过期/已转数量不可重复销售;历史快照保留

子模块追踪:offer-to-sale 正式报价转销售

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
转销售用户确认有效报价offerNo、sale/source order、SKU、qtyapplication/controllers/inner/scm/InvSa.php报价有效期、未转数量、确认价、客户商品销售与报价本地事务创建 SCM_SA_ORDER*,convertedQty old -> old+nrequest ID + offer/sale + SKU/price超可转量/过期整单回滚;来源键防重复销售单
转单回查销售单有/报价数量不对offer/sale billNos、relationapplication/Services/MoveMall/OfferOrderApplySer.php销售主明细、报价关系和 convertedQty查询只读;数量/价格两端一致both billNos + relation + totals已有销售只补关系/统计,不重复建销售单

子模块追踪:offer-im IM 工作台人工报价

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
人工覆盖客服在 IM 工作台改商品/价conversation/apply/offer、agent、versionapplication/controllers/moveMall/ImWorkbench.php会话 owner、申请/报价当前版本和自动建议报价本地事务 price/source/version old -> manual/new/+1,写操作记录conversation + offer/apply + agent + before/after非 owner/旧版本零写入;自动回调不覆盖人工价
发送客户确认报价通过 IM/微信发送offerNo、out message IDapplication/Services/MoveMall/WechatSer.php正式报价快照、会话和发送状态外部发送事务外;本地 record pending -> sent/failedoffer + conversation + out message ID发送失败只补消息,不重建/重算正式报价