Archived
人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的: 件上就地改 · 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名, 于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 —— 而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。 · **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。 删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)—— 「没回传就是删了」会把一次不完整的回传变成一次删表。 · **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。 · 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。 判据与门 · 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来, 人填好的目标价它照样判「客户未提供」的毛病由此消掉。 · 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。 · 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开, 于是一单没打标就进了放行门。三个数全由基座现数。 · 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。 **指向两扇打不开的门,比不指还糟。** 界面不撒谎 · 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。 · 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、 往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。 · 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。 · 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。 · 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。 A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。 五道门禁全绿(971 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
+182
-173
@@ -1,9 +1,22 @@
|
||||
/**
|
||||
* 四域十九对象(docs/MVP示例工作流.html · 动态数据 + 静态底座;定价审批表落审批机制,不入台账)。
|
||||
* 主数据四域 —— **客户 / 项目 / 供应商 / 市场**(2026-07-31 用户定,照常规 CRM & ERP 的分法)。
|
||||
*
|
||||
* 划分原则(产品决策,2026-07-27):**流转单据(过程)归 project;沉淀资产按内容归
|
||||
* customer / intel / supplier**。声明只写活动**要用**的字段;物化走安装通道的声明合并
|
||||
* (最大化合并、只增不破坏),这里不建表 —— 建表是 ledger 的事。
|
||||
* 为什么主数据要单独立域、由**平台**播种,而不是跟着某个活动走:
|
||||
*
|
||||
* 台账原来只按「谁写的」分域(`quote_intake.*` / `quote_alternative.*` …)。那是**实现轴**:
|
||||
* 六个活动各有一张 verdict,一单的故事散在六处;而真正需要治理的那几张
|
||||
* (客户档案、历史价)被挤进一个叫 `shared` 的抽屉 —— 那个名字描述的是机制(跨活动共享),
|
||||
* 不是性质(主数据)。用机制命名,抽屉必然变杂物间。
|
||||
*
|
||||
* 更要紧的是**所有权**:主数据不是任何一个活动的产出。客户档案先于活动存在、
|
||||
* 活动退役了它还在;活动只是**读它做判断、提议更新它**。
|
||||
* 所以它由平台播种物化,不挂在谁的清单下 —— 活动装了卸了,主数据都在。
|
||||
*
|
||||
* 与三类性质的关系(见 `ledgerObjectKindSchema`):这里全是 `master`。
|
||||
* 单据(`record`)归各活动自己的域;派生指标(`derived`)另说 —— 它本不该手写。
|
||||
*
|
||||
* **现在是空占位**:结构先立起来,还没有活动往里写。
|
||||
* 表建出来但一行数据都没有 —— 治理台「主数据」那一区能看见它们等着被接上。
|
||||
*/
|
||||
import { objectDeclarationSchema } from '@contracts';
|
||||
import type { LedgerFieldType, ObjectDeclaration } from '@contracts';
|
||||
@@ -16,184 +29,180 @@ const f = (name: string, type: LedgerFieldType, label: string, primaryKey = fals
|
||||
masked: false,
|
||||
});
|
||||
|
||||
const obj = (domain: string, object: string, label: string, fields: ReturnType<typeof f>[]) =>
|
||||
objectDeclarationSchema.parse({ domain, object, label, fields });
|
||||
/** 主数据一律 `kind: 'master'`:按业务实体分行,没有单标识 */
|
||||
const master = (domain: string, object: string, label: string, fields: ReturnType<typeof f>[]) =>
|
||||
objectDeclarationSchema.parse({ domain, object, label, kind: 'master', fields });
|
||||
|
||||
export const MVP_OBJECTS: readonly ObjectDeclaration[] = [
|
||||
/* ── project:每一单的流转单据(位置签 输入/过程/输出) ─────────────── */
|
||||
obj('project', 'rfq_file', 'RFQ/BOM文件', [
|
||||
f('rfq_ref', 'text', '询价单号', true),
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('qty_eau', 'text', '数量/EAU'),
|
||||
f('brand', 'text', '厂牌'),
|
||||
f('analog_flag', 'boolean', 'Analog'),
|
||||
f('aec_cert', 'text', 'AEC认证'),
|
||||
f('target_price', 'number', '目标价'),
|
||||
f('lead_time', 'text', '交期'),
|
||||
]),
|
||||
obj('project', 'intake_record', '统一受理记录', [
|
||||
f('customer_code', 'text', '客户码', true),
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('brand', 'text', '厂牌'),
|
||||
f('eau', 'text', 'EAU'),
|
||||
f('type_tag', 'text', 'Type标'),
|
||||
f('target_price', 'number', '目标价'),
|
||||
f('status', 'text', '状态'),
|
||||
]),
|
||||
obj('project', 'pn_class', '料号分类表', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('type_tag', 'text', 'Type标'),
|
||||
f('basis', 'text', '分级依据'),
|
||||
f('history_hit', 'boolean', '历史价命中'),
|
||||
f('review_note', 'text', '复核结论'),
|
||||
]),
|
||||
/*
|
||||
* 受理结论 —— **一单一行**(主键只有询价单号),不是一料一行。
|
||||
*
|
||||
* 它承载 A1 出口的两枚去向旗。为什么必须单独一个对象、不能挂在按料号分行的
|
||||
* 「统一受理记录」上:出口载荷的标量字段取自 `landed.flat`,而 `flat` 是把这一单
|
||||
* **所有**落地行 reduce 展开成一个平对象(`wiring/engines.ts`)。旗挂在按料号分的行上,
|
||||
* 六个料号就有六行同名列,**最后合并的那行赢** —— 而归并规则是「任一条命中 C 即需选型」,
|
||||
* last-write-wins 会静默给出错的去向,网关照着错值分叉,没人看得出来。
|
||||
*
|
||||
* 一单一行就没有这个歧义:G1 / G2 读的是同一行的同一个值。
|
||||
* 逐料号的判定仍写「料号分类表」,这里只放归并后的结论。
|
||||
*/
|
||||
obj('project', 'intake_verdict', '受理结论', [
|
||||
f('rfq_ref', 'text', '询价单号', true),
|
||||
f('need_alternative', 'boolean', '需选型'),
|
||||
f('need_sourcing', 'boolean', '需寻源'),
|
||||
f('c_count', 'number', 'C类条数'),
|
||||
f('status', 'text', '状态'),
|
||||
]),
|
||||
obj('project', 'alt_request', '替代评估请求', [
|
||||
f('material', 'text', '初选物料', true),
|
||||
f('chip_type', 'text', '芯片类型'),
|
||||
f('spec_params', 'text', '参数规格'),
|
||||
f('scenario', 'text', '应用场景'),
|
||||
f('requester', 'text', '需求人'),
|
||||
]),
|
||||
obj('project', 'inquiry_request', '询价请求', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('brand', 'text', 'Brand'),
|
||||
f('era', 'text', 'ERA'),
|
||||
f('project_stage', 'text', '项目阶段'),
|
||||
f('accept_alt', 'boolean', '接受替代'),
|
||||
f('unit_usage', 'text', '单机用量'),
|
||||
f('tp', 'number', 'TP'),
|
||||
f('scenario', 'text', '场景'),
|
||||
]),
|
||||
obj('project', 'supplier_floor', '供应商底价表', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('supplier', 'text', '供应商', true),
|
||||
f('floor_price', 'number', '底价'),
|
||||
f('moq', 'text', 'MOQ'),
|
||||
f('lead_time', 'text', '交期'),
|
||||
]),
|
||||
obj('project', 'alt_benchmark', '替代对标表', [
|
||||
f('original_pn', 'text', '原型号', true),
|
||||
f('candidate_pn', 'text', '候选型号', true),
|
||||
f('alt_grade', 'text', '替代等级'),
|
||||
f('param_diff', 'text', '参数差异项'),
|
||||
f('diff_risk', 'text', '差异风险'),
|
||||
f('mod_eval', 'text', '改动评估'),
|
||||
f('verify_advice', 'text', '验证建议'),
|
||||
]),
|
||||
obj('project', 'sourcing_compare', '寻源比价表', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('supplier', 'text', '供应商', true),
|
||||
f('quote_price', 'number', '报价'),
|
||||
f('moq', 'text', 'MOQ'),
|
||||
f('lead_time', 'text', '交期'),
|
||||
f('channel_grade', 'text', '渠道等级'),
|
||||
f('condition', 'text', '成色'),
|
||||
f('verdict', 'text', '结论'),
|
||||
]),
|
||||
obj('project', 'quote_draft', '报价草案', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('proposed_price', 'number', '拟报单价'),
|
||||
f('margin', 'number', '毛利率'),
|
||||
f('deviation', 'number', '偏离度'),
|
||||
]),
|
||||
obj('project', 'formal_quote', '正式报价单', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('unit_price_cny', 'number', '单价CNY'),
|
||||
f('lead_time', 'text', '交期'),
|
||||
f('spq', 'text', 'SPQ'),
|
||||
f('moq', 'text', 'MOQ'),
|
||||
f('trade_terms', 'text', '贸易条款'),
|
||||
f('valid_until', 'date', '有效期'),
|
||||
]),
|
||||
obj('project', 'followup', '跟进提醒', [
|
||||
f('quote_no', 'text', '报价单号', true),
|
||||
f('customer_code', 'text', '客户'),
|
||||
f('d3', 'date', 'D+3'),
|
||||
f('d7', 'date', 'D+7'),
|
||||
f('reply_status', 'text', '回复状态'),
|
||||
]),
|
||||
obj('project', 'landing_stats', '报价落地率统计', [
|
||||
f('period', 'text', '期', true),
|
||||
f('landing_rate', 'number', '落地率'),
|
||||
f('type_a_hit', 'number', 'TypeA命中'),
|
||||
f('d3_reply_rate', 'number', 'D+3回复率'),
|
||||
f('return_rate', 'number', '退回率'),
|
||||
]),
|
||||
|
||||
/* ── customer:客户信息 ────────────────────────────────────────────── */
|
||||
obj('customer', 'profile', '客户资料库', [
|
||||
f('customer_code', 'text', '客户码', true),
|
||||
export const MASTER_OBJECTS: readonly ObjectDeclaration[] = [
|
||||
/* ── 客户:谁在买 ──────────────────────────────────────────────────── */
|
||||
master('customer', 'profile', '客户档案', [
|
||||
f('customer_code', 'text', '客户编码', true),
|
||||
f('name', 'text', '客户名称'),
|
||||
f('grade', 'text', '客户等级'),
|
||||
f('tags', 'text', '画像标签'),
|
||||
f('payment_terms', 'text', '付款条款'),
|
||||
f('industry', 'text', '行业属性'),
|
||||
f('risk_level', 'text', '风险等级'),
|
||||
f('annual_purchase', 'number', '年采购额'),
|
||||
f('industry', 'text', '所属行业'),
|
||||
f('region', 'text', '区域'),
|
||||
f('owner', 'text', '负责 BD'),
|
||||
f('payment_terms', 'text', '付款条件'),
|
||||
f('credit_limit', 'number', '信用额度'),
|
||||
/* 画像标签:价格敏感度、决策周期、常用条款 —— A7 归档时回写的就是这一列 */
|
||||
f('profile_tags', 'text', '画像标签'),
|
||||
f('status', 'text', '状态'),
|
||||
]),
|
||||
master('customer', 'contact', '客户联系人', [
|
||||
f('customer_code', 'text', '客户编码', true),
|
||||
f('contact_code', 'text', '联系人编码', true),
|
||||
f('name', 'text', '姓名'),
|
||||
f('title', 'text', '职务'),
|
||||
/* 邮箱与电话是个人信息 —— 主数据里必须标脱敏,治理面照此裁剪 */
|
||||
{ ...f('email', 'text', '邮箱'), masked: true },
|
||||
{ ...f('phone', 'text', '电话'), masked: true },
|
||||
]),
|
||||
master('customer', 'price_terms', '客户价格条款', [
|
||||
f('customer_code', 'text', '客户编码', true),
|
||||
f('category', 'text', '品类', true),
|
||||
f('target_margin', 'number', '目标毛利'),
|
||||
f('discount', 'number', '折扣'),
|
||||
f('valid_until', 'date', '有效期至'),
|
||||
]),
|
||||
|
||||
/* ── intel:市场与销售情报 ─────────────────────────────────────────── */
|
||||
obj('intel', 'history_price', '历史价格情报', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('quoted_at', 'timestamp', '报价日期', true),
|
||||
f('brand', 'text', '厂牌'),
|
||||
f('unit_price_cny', 'number', '单价CNY'),
|
||||
f('lead_time', 'text', '交期'),
|
||||
f('moq', 'text', 'MOQ'),
|
||||
f('valid_until', 'date', '有效期'),
|
||||
/* ── 项目:为什么买 —— 客户的产品项目,反复询价的载体 ───────────────── */
|
||||
master('project', 'profile', '项目档案', [
|
||||
f('project_code', 'text', '项目编码', true),
|
||||
f('customer_code', 'text', '所属客户'),
|
||||
f('name', 'text', '项目名称'),
|
||||
/* BOM 早期 / BOM 后期 / 量产 —— A3 的询价打法就按这个分 */
|
||||
f('stage', 'text', '项目阶段'),
|
||||
f('annual_volume', 'integer', '年用量预估'),
|
||||
f('start_at', 'date', '立项日期'),
|
||||
f('owner', 'text', '负责人'),
|
||||
f('status', 'text', '状态'),
|
||||
]),
|
||||
obj('intel', 'chip_spec', '芯片规格书', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('key_params', 'text', '关键参数'),
|
||||
master('project', 'bom', '项目 BOM', [
|
||||
f('project_code', 'text', '项目编码', true),
|
||||
f('pn', 'text', '料号', true),
|
||||
f('qty_per_unit', 'number', '单机用量'),
|
||||
f('eau', 'integer', '年需求量'),
|
||||
f('designed_in', 'boolean', '已导入设计'),
|
||||
f('status', 'text', '状态'),
|
||||
]),
|
||||
|
||||
/* ── 供应商:从哪买 ───────────────────────────────────────────────── */
|
||||
master('supplier', 'profile', '供应商档案', [
|
||||
f('supplier_code', 'text', '供应商编码', true),
|
||||
f('name', 'text', '供应商名称'),
|
||||
/* 原厂 / 授权代理 / 贸易商 —— 「现货场景不得选无库存转单商」判据读的就是它 */
|
||||
f('type', 'text', '供应商类型'),
|
||||
f('region', 'text', '区域'),
|
||||
f('rating', 'text', '评级'),
|
||||
f('payment_terms', 'text', '账期'),
|
||||
f('status', 'text', '状态'),
|
||||
]),
|
||||
master('supplier', 'line_card', '代理线卡', [
|
||||
f('supplier_code', 'text', '供应商编码', true),
|
||||
f('brand', 'text', '品牌', true),
|
||||
f('authorized', 'boolean', '是否授权'),
|
||||
f('lead_time_typ', 'text', '常规交期'),
|
||||
]),
|
||||
master('supplier', 'contact', '供应商联系人', [
|
||||
f('supplier_code', 'text', '供应商编码', true),
|
||||
f('contact_code', 'text', '联系人编码', true),
|
||||
f('name', 'text', '姓名'),
|
||||
{ ...f('email', 'text', '邮箱'), masked: true },
|
||||
{ ...f('phone', 'text', '电话'), masked: true },
|
||||
]),
|
||||
|
||||
/* ── 市场:买什么、行情如何 ──────────────────────────────────────────
|
||||
料号主数据放这一域:ERP 里它常自成一域(物料主数据),
|
||||
但这门生意关心的是它**在市场上什么状态**(生命周期、有没有货、什么价),
|
||||
跟行情摆在一起更顺手。要独立成域也说得通,记在这儿备考。 */
|
||||
master('market', 'part', '料号主数据', [
|
||||
f('pn', 'text', '料号', true),
|
||||
f('brand', 'text', '品牌'),
|
||||
f('category', 'text', '品类'),
|
||||
f('package', 'text', '封装'),
|
||||
f('version_date', 'date', '版本日期'),
|
||||
]),
|
||||
obj('intel', 'public_select', '公网选型库', [
|
||||
f('alt_relation', 'text', '替代关系', true),
|
||||
f('param_compare', 'text', '参数对比'),
|
||||
f('spec_ref', 'text', '规格书'),
|
||||
/* Active / NRND / EOL —— A1 三分类与 A2 选型都读它 */
|
||||
f('lifecycle', 'text', '生命周期'),
|
||||
f('price_ref', 'text', '价格参考'),
|
||||
f('rohs', 'boolean', 'RoHS'),
|
||||
f('datasheet_url', 'text', '规格书链接'),
|
||||
]),
|
||||
|
||||
/* ── supplier:供应商信息 ─────────────────────────────────────────── */
|
||||
obj('supplier', 'channel_dir', '供应商渠道目录', [
|
||||
f('supplier', 'text', '供应商', true),
|
||||
f('channel_type', 'text', '渠道类型'),
|
||||
f('brand_lines', 'text', '品牌线'),
|
||||
f('contact', 'text', '联系人'),
|
||||
master('market', 'price_history', '历史价格情报', [
|
||||
f('pn', 'text', '料号', true),
|
||||
f('quoted_at', 'date', '报价日期', true),
|
||||
f('customer_code', 'text', '客户编码'),
|
||||
f('unit_price', 'number', '成交/报出单价'),
|
||||
f('lead_time', 'text', '交期'),
|
||||
f('lost_reason', 'text', '未成交原因'),
|
||||
f('competitor_price', 'number', '竞对价位'),
|
||||
]),
|
||||
obj('supplier', 'dead_stock', '呆滞库存表', [
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('stock_qty', 'number', '库存量'),
|
||||
f('stock_age', 'text', '库龄'),
|
||||
f('cost_price', 'number', '成本价'),
|
||||
master('market', 'quote_index', '市场行情', [
|
||||
f('pn', 'text', '料号', true),
|
||||
f('observed_at', 'date', '观测日期', true),
|
||||
f('source', 'text', '来源'),
|
||||
f('price', 'number', '市场价'),
|
||||
f('stock', 'integer', '可见库存'),
|
||||
f('trend', 'text', '走势'),
|
||||
]),
|
||||
];
|
||||
|
||||
export function declarationsFor(objects: readonly `${string}.${string}`[]): ObjectDeclaration[] {
|
||||
return objects.map((key) => {
|
||||
const [domain, object] = key.split('.');
|
||||
const hit = MVP_OBJECTS.find((o) => o.domain === domain && o.object === object);
|
||||
if (hit === undefined) throw new Error(`未知对象:${key} —— 先在 MVP_OBJECTS 登记`);
|
||||
return hit;
|
||||
});
|
||||
}
|
||||
/** 四域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 */
|
||||
export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market'] as const;
|
||||
|
||||
/**
|
||||
* **样例客户** —— 让「老客户」那条路试得出来。
|
||||
*
|
||||
* 主数据四域建出来是空的,而 A1 的第一个状态有两条路:
|
||||
* 认得出客户就直接过、认不出才请人建档。库里一个客户都没有的话,
|
||||
* 两条样例跑起来一模一样 —— **第二条路根本试不到**。
|
||||
*
|
||||
* 所以随主数据一起播一位客户:样例 2 的来信就用他的邮箱发。
|
||||
* 只播这一位,且是**幂等的按主键写**:它是让样例跑得起来的最小夹具,
|
||||
* 不是往主数据里塞演示数据 —— 真实客户由 A1 建档时一位一位落进来。
|
||||
*/
|
||||
export const MASTER_SAMPLE_ROWS = [
|
||||
/*
|
||||
* **Itelma** —— 老客户测试案例的那一位(照 `JCAI_PRD/docs/活动执行页/` 两份目标件)。
|
||||
* 俄罗斯车规电子、项目制;客户码 CU-0142 就是需求件上写的「已命中客户资料库」那一条。
|
||||
*/
|
||||
{
|
||||
address: { domain: 'customer', object: 'profile' },
|
||||
key: { customer_code: 'CU-0142' },
|
||||
values: {
|
||||
customer_code: 'CU-0142',
|
||||
name: 'Itelma',
|
||||
grade: 'A',
|
||||
industry: '车规电子',
|
||||
region: '俄罗斯',
|
||||
payment_terms: 'EXW · CNY 含税',
|
||||
profile_tags: '项目制 · 按项目批量询价 · 认证要求严(AEC-Q200)',
|
||||
status: 'active',
|
||||
},
|
||||
},
|
||||
{
|
||||
address: { domain: 'customer', object: 'contact' },
|
||||
key: { customer_code: 'CU-0142', contact_code: 'CU-0142-01' },
|
||||
values: {
|
||||
customer_code: 'CU-0142',
|
||||
contact_code: 'CU-0142-01',
|
||||
name: 'Ekaterina Volkova',
|
||||
title: '采购部',
|
||||
// **认人的钥匙**:老客户样例的来信就从这个邮箱发,A1 第一步照它认得出
|
||||
email: 'e.volkova@itelma.ru',
|
||||
},
|
||||
},
|
||||
/*
|
||||
* 项目档案 —— 这一单询的是 MD14.1 这个项目的料。
|
||||
* 播它是因为需求件上「项目制」那句话得有出处:客户属性说得出「按项目询价」,
|
||||
* 库里就该有那个项目,否则那句话是装出来的。
|
||||
*/
|
||||
{
|
||||
address: { domain: 'project', object: 'profile' },
|
||||
key: { project_code: 'MD14.1' },
|
||||
values: {
|
||||
project_code: 'MD14.1',
|
||||
customer_code: 'CU-0142',
|
||||
name: 'MD14.1 车身控制器',
|
||||
stage: 'BOM 后期',
|
||||
status: 'active',
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user