本文把 DGJ2.0 中“配置报价、计算价格、机器人询价、人工报价、生成报价单、转销售单”六个阶段拆开说明。它们共享客户和商品数据,但不是同一个业务对象。
1. 业务目标
报价域解决的是:服务站面对不同客户和商品时,如何在可售、可查、可报价的前提下,快速给出可解释的价格,并让客户继续下单。
具体目标包括:
- 按服务站建立多套报价规则。
- 将客户关联到一套生效规则。
- 为规则下每个商品选择报价方式。
- 支持最近销售价、指导价、自定义价和组合兜底。
- 支持固定价与基于指导价/采购价的动态加价。
- 限制机器人对指定客户可展示的分类和品牌范围。
- 从 VIN、图片、关键词和车型中识别询价需求。
- 自动报价失败时转人工待报价。
- 将待报价转成正式报价单,再继续转销售单。
- 记录价格变更、最近报价和迁移过程,便于解释与追踪。
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 最容易犯的三个错误
- 把“机器人查询到了商品”理解成“商品一定有报价”。查询范围与价格规则是两次独立判断。
- 把 IM 工作台
saveQuote保存的会话价格理解成正式报价单。它首先写会话上下文并发消息,不等同于落SCM_OFFER_ORDER。 - 把
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.php | VIN、图片、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.php | V2 规则 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_version | V1/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 |
| 会话 Context | RobotV2 键值上下文 | 查询结果、车型、已报价价格 |
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 | 参数 | 算法 |
|---|---|---|---|
| 固定价 | 1 | fix_price | 直接使用固定金额 |
| 动态价 | 2 | price_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_str | PriceManager 最终策略的人类可读文案 |
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 | 无效/未知 |
| 1 | VIN 会话 |
| 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 状态
| 状态 | 枚举 | 含义 |
|---|---|---|
| 0 | STATUS_PENDING | 等待人工报价 |
| 1 | STATUS_PASS | 已转有效报价 |
| 2 | STATUS_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。生产必须确认:
offer_order_apply.id条件更新是否带status=0。- 正式报价主单
billNo是否唯一。 - 报价明细是否有来源申请唯一键。
- 事务是否覆盖申请更新和两张报价表插入。
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_timeoffer_price/offer_last_change_timequote_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_sent | IM 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-01 | isV2QuoteRule() 异常返回 false | 数据库异常可能静默切回旧价格 |
| R41-02 | 客户规则唯一性依赖生产约束 | 多关联时取值可能不确定 |
| R41-03 | 规则商品 64 分表必须先 setSid | 漏设 sid 返回空并仅记日志 |
| R41-04 | getGoodsPrices() 有动态 SQL IN 拼接 | invIds 必须严格为整数数组 |
| R41-05 | 特殊价查询默认站点配置始终 true | 所有自定义价都会查特殊价表,语义需确认 |
| R41-06 | 报价范围“空配置=全部” | 误删配置会扩大商品可见范围 |
| R41-07 | 范围更新先删后建 | 非事务调用或中途失败可能形成空范围 |
| R41-08 | 规则列表读时初始化默认规则 | 读接口有写副作用,并发需唯一键 |
| R41-09 | 规则删除硬删客户和商品关系 | 历史价格和事件仍引用旧规则 |
| R41-10 | 待报价先查再改 | 并发可能重复转正式报价 |
| R41-11 | 正式报价 obsolete 枚举分散 | 状态理解和兼容容易出错 |
| R41-12 | IM 报价先保存上下文再发消息 | 消息失败但本地显示已有今日报价 |
| R41-13 | IM 发送失败仍返回顶层报价成功 | 前端若忽略 message_sent 会误导坐席 |
| R41-14 | 今日报价按会话创建日筛选 | 跨日长会话的当日报价可能遗漏 |
| R41-15 | 会话报价未确认写 contact_price | 不一定参与最近报价和正式业务 |
| R41-16 | V1/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/RobotV2 | VIN/关键词/查询/报价/下单/IM 输出 |
| OfferOrderApplySer | 申请、作废、通过、正式报价、转销售、SAAS MQ |
| ImWorkbench | 会话上下文、消息发送、今日报价和并发 |
| 站点版本 | V1、迁移中、V2、失败回退和回切 |
47. 生产待确认项
t_quote_rule_contact是否有sid + contact_id唯一索引。t_quote_rule_goods_N是否有sid + quote_rule_id + inv_id唯一索引。t_quote_goods_special_price的站点开关为何在代码中固定为 true。- 正式报价主从表
price的生产金额单位和完整obsolete枚举。 - 待报价转正式报价时数据库事务和并发锁的真实范围。
- 待报价、正式报价和销售单之间的生产唯一来源字段。
- 各报价入口更新
t_contact_price.offer_price的完整清单。 - IM 会话人工报价是否应同步正式报价单或客户最近报价。
- 价格基础商品同步任务的调度频率、水位和失败告警。
- V1/V2 的实际站点比例和回退开关。
- 微信群、腾讯 IM、APP、PC 使用 PriceManager 的场景参数差异。
- SAAS 报价状态 MQ 的 routing key、重试、死信和消费者幂等。
- VIN/车型/物料服务的超时、缓存和降级策略。
- 报价范围新旧表的流量归属和迁移完成度。
- 报价、限价、活动价、VIP 价在最终
getFullPrice()中的完整优先级。
48. 证据索引
| 主题 | 代码路径 |
|---|---|
| 报价方式枚举 | application/KzData/Enums/QuoteRuleGoodsEnums.php |
| 待报价状态 | application/KzData/Enums/OfferOrderApplyEnums.php |
| 会话常量 | application/KzData/Enums/MoveRobotEnums.php |
| 规则 API | application/controllers/basedata/QuoteManager.php |
| 规则实现 | application/Services/BaseData/QuoteManagerSer.php |
| 最终价格算法 | application/Services/BaseData/QuoteManagerBaseSer.php |
| 价格编排 | application/Services/BaseData/PriceManagerSer.php |
| 内部价格 API | application/controllers/inner/PriceManager.php |
| 范围 API | application/controllers/moveMall/Wechat.php |
| 范围实现 | application/Services/MoveMall/WechatSer.php |
| 智能询价 | application/Services/MoveMall/SmartInquirySer.php |
| RobotV2 | application/Services/MoveMall/RobotV2/ |
| 待报价 API | application/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/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 规则配置 | basedata/QuoteManager/inner/PriceManager | sid、客户/商品范围、基准价、加价规则、有效期 | Quote/Price Controller | QuoteManagerSer/PriceManagerSer | quote rule ID | 报价规则和适用范围生效 |
| 人工报价 | sale/Offer/RapidOffer | 客户、VIN/车型、商品、数量、报价 | Offer Controller | Offer/Quote Service | offer order ID | 保存人工报价和明细 |
| 智能询价 | Wechat/OfferApply/RobotV2/AI | 会话、VIN、车型、关键词、客户 | MoveMall/Robot Controller | SmartInquirySer/AI/Robot Service | conversation/apply ID | 识别需求并产生候选报价 |
| 报价转销售 | inner/scm/InvSa | offer ID、选中商品、数量、确认价格 | Inner InvSa | SaOrderSer | offer + 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 -> new | rule 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,版本 +1 | before/after、operator |
| 转销售 | inner/scm/InvSa -> SaOrderSer | 销售事务 | 有效报价、未转数量 | SCM_SA_ORDER*、报价关系 | sale qty/price = confirmed offer;converted qty +n | offer+sale+SKU 数量价格 |
子模块追踪:quote-rule 报价规则配置
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存规则 | 基础资料新建/编辑/启停规则 | rule ID、scope、formula、valid time | application/controllers/basedata/QuoteManager.php -> application/Services/BaseData/QuoteManagerSer.php | 原规则、优先级、时间/范围冲突和使用情况 | 配置本地事务 rule/version/status old -> new,同步关系 | request ID + rule/version + operator | 冲突/旧版本零写入;停用不改历史报价快照 |
| 缓存生效 | 保存后报价仍用旧规则 | rule ID、cache/version | application/Services/BaseData/QuoteManagerBaseSer.php | DB 当前规则、缓存和更新时间 | commit 后事务外 cache old -> latest/deleted,DB 不变 | rule + DB/cache version + hit | DB 正确只补缓存;不重复保存规则 |
子模块追踪:quote-customer 客户绑定与报价范围
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 绑定范围 | 规则绑定客户/客户组/站点 | rule ID、contact/customer IDs、scope | application/Services/BaseData/QuoteManagerSer.php | 客户有效性、原范围、重叠规则和优先级 | 关系本地事务 old customer set -> new set | request ID + rule + customer count | 全量替换失败整批回滚;同客户冲突按优先级显式处理 |
| 命中回查 | 客户报价未命中预期规则 | customer/sid、rule candidates | application/models/bs/ContactPriceModel.php | 客户关系、范围、有效期和规则顺序 | 查询只读 不写;输出命中链 | request ID + customer + candidate/chosen rule | 先修关系/优先级,不直接覆盖最终价格 |
子模块追踪:quote-goods 商品配置与基础商品
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 配置商品 | 规则增删商品/分类/范围 | rule ID、SKU/invId、goods type/status | application/controllers/inner/PriceManager.php -> application/Services/BaseData/PriceManagerSer.php | 商品可用、基础价、原关系和重复键 | 规则商品本地事务 old set/status -> new | request ID + rule + SKU + count | 无基础商品/重复零写入;下架不改历史报价 |
| 商品回查 | 规则存在但商品未命中 | rule ID、SKU、goods relation | application/models/bs/QuoteRuleGoodsModel.php | 商品关系、有效状态、分类和基础价 | 查询只读 不写 | rule + SKU + relation/base price | 商品配置与客户范围分别核对,不混为一层 |
子模块追踪:quote-calculate 最终价格计算
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 计算报价 | 查询/下单前计算价格 | sid/customer、SKU、qty、scene/time | application/Services/BaseData/PriceManagerSer.php | 基础价、命中规则、客户范围、调整公式和边界 | 计算查询只读;final=base+rule adjustment,记录计算上下文 | request ID + base/rule IDs + inputs/result | 无价/公式非法明确失败,不写业务表 |
| 提交复核 | 保存正式报价/转销售前 | quote/offer ID、current rule version | application/Services/MoveMall/SmartInquirySer.php | 当前计算结果、人工覆盖、有效期和数量 | 报价本地事务固化 final price/source/version none -> snapshot | offer + SKU + final/source/version | 页面旧价以提交复核为准;历史快照不随规则变化 |
子模块追踪:quote-recent 最近报价与最近销售价
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 最近价查询 | 报价页面查看客户商品历史价 | customer/sid、SKU、time/order limit | application/models/bs/ContactPriceModel.php | 有效报价、销售明细、软删/退货和时间口径 | 查询只读 不写;分别返回 recent quote/sale price | request ID + customer/SKU + source bill/time | 最近报价不等于成交价;明确来源和日期 |
| 历史回查 | 最近价异常 | offer/sale billNo、SKU | application/Services/BaseData/PriceManagerSer.php | 报价状态、销售成交快照和排序字段 | 查询只读 不写 | source billNos + created/business time | 排除取消/无效单;不修改历史价迎合页面 |
子模块追踪:smart-input 智能询价输入识别
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 解析输入 | 文本/微信/IM 询价 | conversation/request ID、raw text hash | application/Services/MoveMall/SmartInquirySer.php | 用户站点、历史上下文、SKU/车型/数量词和媒体结果 | 解析事务外/会话本地事务写 normalized items none -> parsed | request + conversation + parsed item count | 无法识别进入待报价/澄清,不伪造商品 |
| 解析回查 | SKU/数量识别错误 | request ID、input/output tokens | application/Services/Ai/AiSer.php | 原输入摘要、AI 结果版本和人工修正 | 查询只读;人工修正本地事务 parsed old -> corrected | request + before/after normalized items | 敏感输入脱敏;旧 AI 回调不覆盖人工修正 |
子模块追踪:smart-decision 自动报价决策
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 决策 | 识别商品后判断自动/人工报价 | request/customer、items、scene | application/Services/MoveMall/SmartInquirySer.php | 商品匹配、规则命中、价格完整性、风险/置信阈值 | 决策查询只读;记录 decision auto/manual/clarify | request + item + rule/confidence + decision | 任一无价/低置信进入人工,不返回假价格 |
| 自动结果 | auto 生成报价回复/草稿 | request/offer ID、prices | application/Services/MoveMall/OfferOrderApplySer.php | 最终计算、有效期和幂等申请 | 报价本地事务 none -> auto quoted/pending | request + offer/apply + SKU prices | IM 发送失败只补消息;报价记录不重复生成 |
子模块追踪:offer-apply 待报价申请
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 创建申请 | 无法自动报价转人工待办 | apply ID、conversation/request、items | application/controllers/moveMall/OfferApply.php -> application/Services/MoveMall/OfferOrderApplySer.php | normalized items、用户、重复请求和已有正式报价 | 申请本地事务 insert 主明细,status none -> pending | request ID + apply/conversation + item count | 相同 request 幂等;解析不完整保留澄清态 |
| 分派处理 | 工作台领取/退回待报价 | apply ID、agent、action/version | application/controllers/moveMall/ImWorkbench.php | pending/processing、owner 和版本 | 本地事务 pending -> processing/completed/rejected、owner 更新 | apply + agent + action/version | 并发领取仅一人成功;旧操作零覆盖 |
子模块追踪:offer-order 正式报价单
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存报价 | 人工/自动确认正式报价 | offerNo、apply ID、SKU、price/qty/validUntil | application/controllers/sale/Offer.php -> application/Services/MoveMall/OfferOrderApplySer.php | 申请、商品、最终价、有效期和重复来源 | 报价本地事务 insert 主明细/关系,status none -> valid | request ID + offer/apply + SKU/price | 价格/商品非法整单回滚;来源申请只转一次 |
| 编辑失效 | 修改/关闭报价 | offerNo、action/version | application/controllers/sale/RapidOffer.php | 当前有效态、已转数量和版本 | 本地事务 price/status/version old -> new;已转部分不回退 | request ID + offer + old/new version | 过期/已转数量不可重复销售;历史快照保留 |
子模块追踪:offer-to-sale 正式报价转销售
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 转销售 | 用户确认有效报价 | offerNo、sale/source order、SKU、qty | application/controllers/inner/scm/InvSa.php | 报价有效期、未转数量、确认价、客户商品 | 销售与报价本地事务创建 SCM_SA_ORDER*,convertedQty old -> old+n | request ID + offer/sale + SKU/price | 超可转量/过期整单回滚;来源键防重复销售单 |
| 转单回查 | 销售单有/报价数量不对 | offer/sale billNos、relation | application/Services/MoveMall/OfferOrderApplySer.php | 销售主明细、报价关系和 convertedQty | 查询只读;数量/价格两端一致 | both billNos + relation + totals | 已有销售只补关系/统计,不重复建销售单 |
子模块追踪:offer-im IM 工作台人工报价
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 人工覆盖 | 客服在 IM 工作台改商品/价 | conversation/apply/offer、agent、version | application/controllers/moveMall/ImWorkbench.php | 会话 owner、申请/报价当前版本和自动建议 | 报价本地事务 price/source/version old -> manual/new/+1,写操作记录 | conversation + offer/apply + agent + before/after | 非 owner/旧版本零写入;自动回调不覆盖人工价 |
| 发送客户 | 确认报价通过 IM/微信发送 | offerNo、out message ID | application/Services/MoveMall/WechatSer.php | 正式报价快照、会话和发送状态 | 外部发送事务外;本地 record pending -> sent/failed | offer + conversation + out message ID | 发送失败只补消息,不重建/重算正式报价 |