/** * 主数据四域 —— **客户 / 项目 / 供应商 / 市场**(2026-07-31 用户定,照常规 CRM & ERP 的分法)。 * * 为什么主数据要单独立域、由**平台**播种,而不是跟着某个活动走: * * 台账原来只按「谁写的」分域(`quote_intake.*` / `quote_alternative.*` …)。那是**实现轴**: * 六个活动各有一张 verdict,一单的故事散在六处;而真正需要治理的那几张 * (客户档案、历史价)被挤进一个叫 `shared` 的抽屉 —— 那个名字描述的是机制(跨活动共享), * 不是性质(主数据)。用机制命名,抽屉必然变杂物间。 * * 更要紧的是**所有权**:主数据不是任何一个活动的产出。客户档案先于活动存在、 * 活动退役了它还在;活动只是**读它做判断、提议更新它**。 * 所以它由平台播种物化,不挂在谁的清单下 —— 活动装了卸了,主数据都在。 * * 与三类性质的关系(见 `ledgerObjectKindSchema`):这里全是 `master`。 * 单据(`record`)归各活动自己的域;派生指标(`derived`)另说 —— 它本不该手写。 * * **现在是空占位**:结构先立起来,还没有活动往里写。 * 表建出来但一行数据都没有 —— 治理台「主数据」那一区能看见它们等着被接上。 */ import { objectDeclarationSchema } from '@contracts'; import type { LedgerFieldType, ObjectDeclaration } from '@contracts'; const f = (name: string, type: LedgerFieldType, label: string, primaryKey = false) => ({ name, type, label, primaryKey, masked: false, }); /** 主数据一律 `kind: 'master'`:按业务实体分行,没有单标识 */ const master = (domain: string, object: string, label: string, fields: ReturnType[]) => objectDeclarationSchema.parse({ domain, object, label, kind: 'master', fields }); export const MASTER_OBJECTS: readonly ObjectDeclaration[] = [ /* ── 客户:谁在买 ──────────────────────────────────────────────────── */ master('customer', 'profile', '客户档案', [ f('customer_code', 'text', '客户编码', true), f('name', 'text', '客户名称'), f('grade', 'text', '客户等级'), 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', '有效期至'), ]), /* ── 项目:为什么买 —— 客户的产品项目,反复询价的载体 ───────────────── */ 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', '状态'), ]), 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', '封装'), /* Active / NRND / EOL —— A1 三分类与 A2 选型都读它 */ f('lifecycle', 'text', '生命周期'), f('rohs', 'boolean', 'RoHS'), f('datasheet_url', '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', '竞对价位'), ]), 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 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', /* * E04(关系覆盖与关键人经营)字段 —— 2026-08-20 补齐,让「客户已经跑过 E04、 * 找缺口」这条分支第一次能真被样例跑通(此前这几列全空,每轮都只能从零起草)。 * decision_chain 只覆盖了 bt-01(采购层),bt-02/bt-03 是刻意留的缺口。 */ cust_type: 'Tier1 OEM', coverage_score: 35, decision_chain: { nodes: [ { node: 'bt-01', btLevel: 'bt-01', required: true, contactCode: 'CU-0142-01', covered: true }, { node: 'bt-02', btLevel: 'bt-02', required: true, contactCode: '', covered: false }, { node: 'bt-03', btLevel: 'bt-03', required: true, contactCode: '', covered: false }, ], }, interaction_timeline: { entries: [ { date: '2026-06-15', type: '邮件报价跟进', contactCode: 'CU-0142-01', note: 'MD14.1 项目 BOM 报价确认' }, { date: '2026-07-20', type: '电话沟通', contactCode: 'CU-0142-01', note: '交期与付款方式确认' }, ], }, }, }, { 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', // E04 字段 —— 对应上面 decision_chain 的 bt-01 节点 role: '采购', decision_node: 'bt-01', influence: 'high', relationship: 3, comm_pref: 'Email', key_person: true, }, }, /* * 项目档案 —— 这一单询的是 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', }, }, /* * **CU-0087** —— E04(关系覆盖与关键人经营)"关键人离职/调岗"样例专用夹具 * (2026-08-20 补):此前这条样例引用的 CU-0087 在种子数据里根本不存在, * 是一处遗留 bug。这里种一位客户 + 两位联系人,decision_chain 的 bt-01 * (采购层)当下由陈刚覆盖 —— 样例说的正是"这个人要走了,这个节点要重新覆盖"。 */ { address: { domain: 'customer', object: 'profile' }, key: { customer_code: 'CU-0087' }, values: { customer_code: 'CU-0087', name: '深圳精密电子有限公司', grade: 'B', industry: '工业自动化', region: '中国·深圳', payment_terms: 'T/T 30 天', profile_tags: '批量稳定 · 价格敏感', status: 'active', cust_type: '模组厂', coverage_score: 55, decision_chain: { nodes: [ { node: 'bt-01', btLevel: 'bt-01', required: true, contactCode: 'CU-0087-01', covered: true }, { node: 'bt-02', btLevel: 'bt-02', required: true, contactCode: 'CU-0087-02', covered: true }, { node: 'bt-03', btLevel: 'bt-03', required: false, contactCode: '', covered: false }, ], }, interaction_timeline: { entries: [{ date: '2026-05-10', type: '现场拜访', contactCode: 'CU-0087-01', note: '年度框架协议签署' }], }, }, }, { address: { domain: 'customer', object: 'contact' }, key: { customer_code: 'CU-0087', contact_code: 'CU-0087-01' }, values: { customer_code: 'CU-0087', contact_code: 'CU-0087-01', name: 'Chen Gang', title: '采购部长', // **样例说的就是这个人**:「关键人离职/调岗」引用的正是这一位 email: 'chen.gang@sz-precision.com', role: '采购', decision_node: 'bt-01', influence: 'high', relationship: 4, comm_pref: 'Email', key_person: true, }, }, { address: { domain: 'customer', object: 'contact' }, key: { customer_code: 'CU-0087', contact_code: 'CU-0087-02' }, values: { customer_code: 'CU-0087', contact_code: 'CU-0087-02', name: 'Li Wei', title: '技术总监', email: 'li.wei@sz-precision.com', role: '技术决策人', decision_node: 'bt-02', influence: 'medium', relationship: 3, comm_pref: 'Email', key_person: false, }, }, /* * I01(品牌形象与销售工具包管理)读的 L-01 宏观市场台账 —— 两条,呼应 I01 的两条 * 事件驱动样例(「进入俄罗斯市场」「E04 请求定向素材」),让「读市场信号 → 拟内容」 * 这条分支第一次有真数据可读,不必每轮都读空手。 */ { address: { domain: 'market', object: 'signal' }, key: { signal_id: 'MS-2026-0001' }, values: { signal_id: 'MS-2026-0001', signal_type: 'industry_trend', title: '俄罗斯车规电子本土化替代加速', content: '受供应链管制影响,俄罗斯本土车企与 Tier1 加快引入非欧美芯片供应商,车规级 AEC-Q200 认证成为准入门槛,采购决策周期缩短、更看重供货稳定性承诺。', source: '行业报告', signal_date: '2026-06-01', market_region: '俄罗斯', industry: '汽车电子', chip_category: 'MCU', importance: 4, impact_dir: 'opportunity', signal_status: 'evaluated', version: '2026-Q2', review_note: '', }, }, { address: { domain: 'market', object: 'signal' }, key: { signal_id: 'MS-2026-0002' }, values: { signal_id: 'MS-2026-0002', signal_type: 'customer_market', title: 'Itelma 系客户新一轮车身控制器项目招标', content: 'Itelma 及同类俄罗斯 Tier1 客户密集启动车身控制器新项目招标,价值传递素材需突出认证齐全度与批量交付能力,系统方案白皮书是决策链高层最看重的材料形式。', source: '订阅', signal_date: '2026-07-10', market_region: '俄罗斯', industry: '汽车电子', chip_category: 'MCU', importance: 5, impact_dir: 'opportunity', signal_status: 'evaluated', version: '2026-Q2', review_note: '', }, }, ] as const;