在 AI-native revenue teams 的工作流里,CRM 不再只是销售录入数据的后台系统,而应该成为 AI 代理读取客户信息、推进商机、记录互动动作的数据底座。过去几年,销售团队开始把越来越多的 CRM 操作交给自动化工具或智能助手,但传统 CRM 的 API 大多围绕人工页面操作设计,字段多、权限复杂、文档分散,AI 客户端接入成本很高。MCP(Model Context Protocol)提供了一套标准化的工具与数据接入方式,于是出现了一类新项目:把 MCP server 与 CRM 能力构建在同一个服务里。Salestrics 正是这种思路的开源示例——它展示了如何做一个开放 MCP server,同时承担轻度 CRM 功能,面向 AI-native revenue teams。
这篇文章用工程视角做三件事:先拆解这类项目为什么成立、在协议层如何组织;然后用官方 SDK 搭建一个最小可运行的 MCP server + CRM 服务,包含客户查询、客户创建和商机阶段更新三个工具;最后讨论从 demo 走向生产时需要补齐的存储、权限、审计和安全边界。读完以后,你可以基于同样的结构自建一个面向 AI 客户端的业务数据服务,也能理解 Salestrics 这类项目在架构上到底做了什么。
1. 先理清这类项目在解决什么问题
1.1 传统 CRM 在 AI 接入上的核心痛点
传统 CRM 系统的问题是:它是为人在浏览器里操作而设计的,不是为 agent 直接调用而设计的。
销售代表登录 CRM 后,在一个表单里填客户名称、阶段、金额、下次跟进时间,这个过程对人类很自然。但 AI 代理接入时,它面对的是另一套世界:REST API 端点、分页参数、可变字段命名、对象关系、权限模型。它的目标是“帮我看看这个月所有 stage 是 negotiation 的商机”,“把某条商机的阶段从 proposal 改成 won”,“给某个客户创建一条跟进任务”。这些动作如果都要通过一层层 API 文档翻译,AI 应用开发成本会迅速上升。
更深层的问题是字段语义。CRM 里的stage字段在中文环境可能叫“阶段”,在数据库里是stage还是sales_stage,取值是proposal还是报价阶段,不同团队习惯完全不同。AI 客户端需要一套显式的接口描述,告诉它每个参数是什么意思、取值范围是什么、缺省值是什么。传统 REST API 的 OpenAPI 文档会有一定帮助,但通常不够模型友好,而且很多 CRM 的 API 权限模型是为了支持大量不同角色设计的,agent 接入时往往不知道当前凭据能做什么、不能做什么。
Salestrics 这类项目尝试改变这种结构:不把 AI 当作 CRM 外部的一个消费者,而是让 MCP server 直接暴露“销售数据模型”和“销售动作”给模型客户端。协议是最新的,数据描述是紧凑的,工具边界是清晰的。
1.2 MCP 为什么适合作为 CRM 的 AI 接入层
MCP 是 Anthropic 在 2024 年提出并开放的协议,它的目标不是替代某种 CRM API,而是建立模型与外部工具、数据源之间的标准连接方式。一个 MCP server 可以理解为一个“AI 可用的工具服务器”,它向模型客户端暴露三种核心能力:
- Tools(工具):模型可以主动调用的操作,比如查询客户列表、创建客户、修改商机阶段。
- Resources(资源):暴露给客户端的只读数据,比如一份客户名单、一段团队销售规范、一个 CSV 报表。
- Prompts(提示):模板化的提示信息,比如“帮我生成一封跟进邮件”的固定开头和字段要求。
对 CRM 场景来说,Tools 是核心。它等价于把传统 API 的动作包装成模型可以直接理解的原语。模型不需要知道 HTTP method、URL 路径、header 怎么拼,它只需要知道“有一个工具叫update_deal_stage,输入是customerId和stage,结果会返回客户最新状态”。
这一层标准化带来的好处是明显的:Claude Desktop、Cursor、自研 Agent 应用,只要实现了 MCP client,就能连接同一套 MCP server。不需要为每个前端客户端分别写一套 API 适配层。
1.3 为什么把 MCP server 和 CRM 合并成同一个服务
单独做一个 MCP 网关去对接已有 CRM 也是一种可行方案,很多大厂会在内部做这一步。但 Salestrics 选择把 MCP server 和 CRM 业务模型放在一起,这背后有几个实际取舍。
第一是降低部署复杂度。假设你有 CRM 系统、单独跑一个 MCP 适配服务,那你要部署两套服务,还要处理多跳认证、数据同步延迟、字段映射维护。合并成一个服务之后,MCP 工具直接操作业务对象,模型上下文和数据表之间只有一层领域逻辑。
第二是数据语义能保持原生。MCP tool 的参数就是 CRM 实体的字段,不需要通过 JSON Patch 或自建翻译层。AI 客户端看到的 schema 和开发者在数据库里理解的实体基本一致,排错链条更短。
第三是适合小团队和开源场景。一个 revenue team 可能只有几十个人,不需要大型 CRM 的完整复杂功能,但需要一套能被 AI 工具有效调用的轻量数据服务。合并设计让项目可以保持小而精,部署在团队内部,快速被 agent 使用。
不过这个设计也要付出代价:MCP server 一旦承担 CRM 业务逻辑,就要自己处理数据一致性、权限隔离、审计和并发控制。协议网关可以很薄,业务服务却必须是完整的。Salestrics 类项目是否适合生产环境,关键不看 MCP 部分写得多好,而是看 CRM 部分在工程上是否经得起真实销售数据考验。
2. 架构上拆解一个 MCP + CRM 服务
2.1 从模型客户端到数据表的完整链路
一个 MCP + CRM 服务从请求到数据表,通常经过这样一条链路:
模型客户端 -> MCP Client -> 传输层(stdio 或 HTTP)-> MCP Server -> 工具注册表 -> 领域服务 -> CRM 数据存储
每一层的作用:
- 模型客户端:发起对话的 AI 应用,它决定在什么时机调用哪个工具。
- MCP Client:负责与 MCP Server 建立会话、发送 JSON-RPC 请求、接收结果。
- 传输层:MCP 消息的载体。本地进程用 stdio,远程服务用 HTTP。
- MCP Server:核心运行时,负责协议握手、工具注册、请求分发。
- 工具注册表:
server.tool()注册的所有工具集合。 - 领域服务:业务规则所在位置,比如变更商机阶段时要校验阶段是否合法、是否写审计日志。
- CRM 数据存储:最终保存数据的地方,可能是内存 Map、SQLite、PostgreSQL。
在这条链路里,最容易想歪的地方是:把所有业务逻辑都堆在工具回调函数里。工具回调应该薄,只做参数转义、调用领域服务、返回结果。数据库访问、状态变更、权限判断要下沉到领域服务和存储层。否则项目一旦从 demo 变成真实系统,工具函数会迅速膨胀成无法维护的泥球。
2.2 传输方式应该选 stdio 还是 HTTP
MCP server 写好后,首先要回答的问题就是:通过什么方式让客户端连接。目前主流选择有两种,stdio 和 HTTP(Streamable HTTP,早期也常见 HTTP + SSE)。
| 传输方式 | 适用场景 | 优点 | 注意事项 |
|---|---|---|---|
| stdio | 本地桌面客户端、个人调试 | 无需网络端口、不暴露服务、配置简单 | stdout 被协议占用,日志必须写 stderr |
| HTTP | 远程服务、团队共享、部署在服务器 | 可多客户端连接、可统一认证、可接入已有网关 | 需要认证、限流、CORS、TLS 等生产配置 |
在 Salestrics 这类项目里,本地验证用 stdio 最方便。一个 Claude Desktop 客户端可以直接通过command: "node", args: ["/path/to/server.js"]启动本地 server。如果是多人销售团队共享的 CRM 服务,必须走 HTTP,因为数据不在某个人的笔记本上,而且团队需要统一权限和审计。
2.3 CRM 数据模型与 MCP 工具应该怎么映射
把 CRM 数据模型映射成 MCP 工具,本质上是把“对象 + 动作”转化为“工具 + 参数”。一个最小的 AI-native CRM 至少需要这几张实体表和对应工具:
| 实体 | 关键字段 | MCP 工具示例 | 读写方向 |
|---|---|---|---|
| Account 客户 | id, name, industry, owner | list_customers,create_customer | 读 / 写 |
| Contact 联系人 | id, account_id, name, email, phone | get_contact,create_contact | 读 / 写 |
| Lead 线索 | id, name, source, status | create_lead,update_lead_status | 读 / 写 |
| Opportunity 商机 | id, account_id, amount, stage, close_date | get_deal,update_deal_stage | 读 / 写 |
| Activity 活动 | id, type, subject, due_date, assignee | create_task,complete_task | 读 / 写 |
映射主要有两条规则。
- 查询用列表或详情工具,命名尽量用
list_xxx、get_xxx,参数要包含分页、筛选条件。 - 写操作直接把参数映射到实体字段,每个字段必须给出 describe 和 enum,否则模型不知道可能值。
还有一个容易被忽略的点:MCP Resources 在 CRM 里非常有用。除了工具之外,你可以把团队销售规范、客户分组规则、常用话术模板作为 Resources 暴露给模型。这样模型在调用工具前可以先读取资源,理解当前销售流程,而不是只能靠工具参数里那几行描述。
3. 搭建最小可运行的 MCP server + CRM 服务
3.1 环境准备与工程初始化
演示环境按以下版本准备工作,实际项目请以当前官方 SDK 的最新版本为准:
- Node.js 20 或更高版本,建议 22 LTS
- npm 10 或更高版本,也可以使用 pnpm
- TypeScript
- MCP TypeScript SDK:
@modelcontextprotocol/sdk - Zod:用于工具参数 schema 声明
创建工程并安装依赖:
mkdir salestrics-demo cd salestrics-demo npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript tsx @types/node@modelcontextprotocol/sdk是官方 MCP 开发包,提供McpServer、StdioServerTransport等核心能力。zod负责定义工具参数的 schema,SDK 会自动把 zod schema 转换成 MCP 协议需要的 JSON Schema 格式。tsx用于本地直接运行 TypeScript,免去每次编译。
3.2 项目结构与 TypeScript 配置
工程目录按清晰分层来组织,后续扩展存储层时会比较顺手:
salestrics-demo ├── src │ ├── db.ts │ └── index.ts ├── package.json └── tsconfig.jsonpackage.json需要设置"type": "module",这样 TypeScript 可以按 ESM 方式编译。scripts里提供开发、构建和启动命令:
{ "name": "salestrics-demo", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "tsx src/index.ts", "build": "tsc", "start": "node dist/index.js" } }tsconfig.json使用 NodeNext 模块解析。注意 MCP SDK 的 ESM 导入路径带.js后缀,使用 NodeNext 才能正确识别:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src", "strict": true, "esModuleInterop": true, "skipLibCheck": true } }3.3 数据层:先用内存 Map 模拟 CRM 表
在 demo 阶段,数据层使用一个Map模拟 CRM 数据库。这样做的好处是零配置、无外部服务,适合先把 MCP 协议链路跑通。生产环境必须替换为 SQLite 或 PostgreSQL,这一点在最后一章会展开。
src/db.ts定义客户实体和生产环境的扩展点:
export type Customer = { id: string; name: string; email?: string; stage: string; value: number; owner: string; createdAt: string; }; export type CreateCustomerInput = { name: string; email?: string; stage: string; value: number; owner: string; }; const customers = new Map<string, Customer>(); export function listCustomers(limit: number): Customer[] { return [...customers.values()].slice(0, limit); } export function getCustomer(id: string): Customer | undefined { return customers.get(id); } export function createCustomer(input: CreateCustomerInput): Customer { const customer: Customer = { id: crypto.randomUUID(), ...input, createdAt: new Date().toISOString(), }; customers.set(customer.id, customer); return customer; } export function updateCustomerStage(id: string, stage: string): Customer | undefined { const customer = customers.get(id); if (!customer) { return undefined; } customer.stage = stage; return customer; }这个文件可以看作一个内存仓储。每个函数都对应一种数据操作,后续替换成 SQL 或 ORM 时,工具层不需要改动,只要保证这些函数的语义一致。
3.4 MCP server:注册工具并启动
src/index.ts是核心入口,它做几件事:创建McpServer实例,注册 CRM 工具,连接 stdio transport。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { listCustomers, createCustomer, updateCustomerStage, } from "./db.js"; const server = new McpServer({ name: "salestrics-demo", version: "0.1.0", }); server.tool( "list_customers", "列出当前团队的客户列表,按创建时间倒序返回", { limit: z.number().int().min(1).max(100).optional().describe("返回条数,默认 20"), }, async ({ limit }) => { const rows = listCustomers(limit ?? 20); return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }], }; } ); server.tool( "create_customer", "创建一条新客户记录", { name: z.string().min(1).max(200).describe("客户名称"), email: z.string().email().optional().describe("客户邮箱"), stage: z.enum(["lead", "opportunity", "proposal", "won", "lost"]).default("lead").describe("客户当前阶段"), value: z.number().nonnegative().default(0).describe("预计金额,以用户币种为单位"), }, async (params) => { const customer = createCustomer({ name: params.name, email: params.email, stage: params.stage, value: params.value, owner: "current-user", }); return { content: [{ type: "text", text: JSON.stringify(customer, null, 2) }], }; } ); server.tool( "update_deal_stage", "更新一个客户的销售阶段", { customerId: z.string().min(1).describe("客户 ID"), stage: z.enum(["lead", "opportunity", "proposal", "won", "lost"]).describe("新的销售阶段"), }, async ({ customerId, stage }) => { const customer = updateCustomerStage(customerId, stage); if (!customer) { return { content: [{ type: "text", text: `客户 ${customerId} 不存在` }], isError: true, }; } return { content: [{ type: "text", text: JSON.stringify(customer, null, 2) }], }; } ); const transport = new StdioServerTransport(); await server.connect(transport); console.error("[salestrics-demo] MCP server is ready over stdio");这段代码是 MCP server 的最小骨架,理解它有四个关键点。
第一,server.tool()的第二个参数是工具描述。这个描述不是给人看的注释,而是给模型看的说明,会进入工具的 schema。要写得具体,说明工具返回值是什么、接收什么参数,否则模型很难在正确时机调用它。
第二,第三个参数是 zod schema 对象。SDK 会把 zod 对象转换为 MCP 工具参数所需的 JSON Schema。z.enum限制模型的输入必须在预设值内,这个约束比在函数内部做 if 判断更强,因为模型在发起调用前就能看到候选值。
第三,返回值统一包装成一个数组,第一项是type: "text"的文本内容。这个结构是 MCP 协议要求的结果格式,模型最终读取的就是这里面的文本。
第四,最后使用console.error打印服务启动日志,而不是console.log。原因是 stdio transport 模式下,stdout 被用来传输 JSON-RPC 协议消息,任何额外输出都会污染协议流,导致客户端解析失败。这一步错的人非常多,后面排查章节会再展开。
3.5 启动与基础验证
启动服务:
npm run dev如果一切正常,终端只会打印一行 stderr 日志:
[salestrics-demo] MCP server is ready over stdio注意,这个命令不会退出,它会一直等待客户端通过 stdin 发送 JSON-RPC 请求。这不是卡死,而是 stdio server 的正常行为。因为 stdout 被协议占用,所以不会看到普通 Web 服务那种请求日志。要验证工具是否正常,需要启动一个 MCP client 来连接它,下一章会介绍如何用 MCP Inspector 完成这一步。
4. 关键实现解释:工具注册、参数校验与输出格式
4.1 server.tool 是如何把 zod schema 变成 JSON Schema 的
server.tool()是 MCP TypeScript SDK 注册工具的入口。它的核心价值是:你写一份 zod schema,SDK 在注册阶段自动把它转成 JSON Schema,并注册到协议的能力列表中。当模型客户端请求工具列表时,它拿到的是标准 JSON Schema,而不是 zod 对象。
常见的 zod 类型和 JSON Schema 对应关系如下:
| zod 写法 | 生成的 JSON Schema 片段 | 含义 |
|---|---|---|
z.string().min(1).max(200) | {"type":"string","minLength":1,"maxLength":200} | 非空字符串 |
z.enum(["lead","won","lost"]) | {"type":"string","enum":["lead","won","lost"]} | 只能取这三个值 |
z.number().nonnegative() | {"type":"number","minimum":0} | 非负数 |
z.string().email() | {"type":"string","format":"email"} | 邮箱格式 |
.default("lead") | "default":"lead" | 模型不传时使用默认值 |
.optional() | "required":[]中不包含该字段 | 可选字段 |
把参数 schema 写清楚,是让 AI 客户端能正确使用工具的关键。很多工具调用失败,不是因为代码有 bug,而是 schema 里没有描述阶段字段的取值范围,模型传了一个stage: "completed",而代码只接受won和lost。
使用.describe()给每个字段补充自然语言说明也很有帮助。比如value字段如果只写z.number(),模型可能以为是客户数量。写成z.number().describe("预计金额,以用户币种为单位"),模型才能判断该传多大数字。
4.2 返回值为什么要包成 content 数组
MCP 的 ToolResult 结构不是一个简单的字符串,而是一个对象,包含:
{ "content": [ { "type": "text", "text": "{\"id\":\"...\",\"name\":\"Acme\",\"stage\":\"won\"}" } ], "isError": false }content是数组,意味着一个工具可以返回多种内容。最常用的是text;未来可以扩展到 image、resource link 等类型。isError用于标记调用是否失败。
在 CRM 场景里,返回 JSON 字符串是最直接的方式。由于 model 本身擅长解析 JSON,你不需要返回结构化表格,只要保证字符串是合法的 JSON,并且字段命名稳定,模型就能从中提取信息。
4.3 错误处理要让模型能理解,而不是返回内部异常
工具回调中一旦抛异常,MCP 客户端会收到协议错误,模型通常只能看到“Internal error”这种没有业务含义的信息。这在真实使用时基本不可用。
推荐做法是:业务异常不要 throw,而是返回带isError: true的结果。例如update_deal_stage里如果客户不存在,返回一段可读文本,同时标记 isError。这样模型能根据返回内容决定下一步,比如告诉用户“客户不存在”,或者建议先调用create_customer。
if (!customer) { return { content: [{ type: "text", text: `客户 ${customerId} 不存在` }], isError: true, }; }不要直接把数据库连接失败、堆栈信息返回给模型。给模型一个安全、有意义的错误摘要,完整堆栈写到 server 日志里,供人排查。
5. 在 MCP Inspector 与真实客户端里完成端到端验证
5.1 先用 MCP Inspector 做协议级验证
MCP Inspector 是官方提供的可视化调试工具。它启动一个本地调试前端,可以连接任意 MCP server,查看工具列表、资源列表,手动调用工具。这比自己写 MCP client 省事很多,是验证 MCP server 的优先选择。
先构建项目:
npm run build然后启动 Inspector 连接dist/index.js:
npx @modelcontextprotocol/inspector node dist/index.jsInspector 会在本地启动一个 Web UI,自动打开浏览器。连接成功后,在左侧页面可以看到:
- Tools 列表里有
list_customers、create_customer、update_deal_stage - 每个工具的参数 schema 会显示出来
- 可以直接填写参数,发起一次 tool call
手动调用一次create_customer,传入name、stage=won,返回结果里能看到生成后的客户对象。这个流程能确认协议握手、工具注册、参数校验、结果返回全部正常。
5.2 在 Claude Desktop 中接入本地 stdio 服务
Claude Desktop 支持通过配置文件接入 MCP server。配置文件位置因系统不同,通常在:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
把服务注册进去:
{ "mcpServers": { "salestrics-demo": { "command": "node", "args": ["/absolute/path/to/salestrics-demo/dist/index.js"] } } }注意args必须使用绝对路径。配置完成后重启 Claude Desktop,在对话中问“调用 create_customer 创建一个名为 Acme 的客户”,模型会识别到对应的 MCP 工具并尝试调用。启动的 server 进程由 Claude Desktop 管理,生命周期绑定在桌面应用上。
5.3 在 Cursor 中使用 MCP 工具
Cursor 的 MCP 配置入口在 Settings -> MCP,或者在开发者工具中找到 MCP 面板。添加一条新的命令型 MCP server:
node /absolute/path/to/salestrics-demo/dist/index.js添加成功后,Cursor 会启动该进程并建立连接。你可以直接在对话里让 AI 使用这些工具操作数据。
5.4 预期验证流程
推荐按以下顺序验证,能最快发现协议层问题:
- 在 Inspector 中调用
create_customer,创建一条客户记录。 - 在 Inspector 中调用
list_customers,确认刚才创建的客户出现在结果中。 - 调用
update_deal_stage,传入上一步返回的客户 ID,把 stage 改为won。 - 再次调用
list_customers,确认 stage 已更新。
预期结果中,create_customer返回类似这样的 JSON:
{ "id": "b7f4c1a0-3d6a-4f10-9d1a-6e2e0a3c5d4f", "name": "Acme", "email": "ops@acme.example", "stage": "won", "value": 120000, "owner": "current-user", "createdAt": "2026-01-01T00:00:00.000Z" }如果前三步都成功,说明 MCP 协议链路没问题。如果某一步失败,排查思路从第 6 章开始。
6. 常见问题排查与根因分析
6.1 客户端连接不上服务
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 客户端提示无法连接 MCP server | args路径错误或没有先构建 | 在终端手工运行配置的命令 | 确认dist/index.js存在,路径使用绝对路径 |
| server 启动后立即退出 | Node 版本过低或 SDK 依赖不兼容 | 查看 stderr 日志 | 升级到 Node 20+,重新安装依赖 |
| 配置文件改动不生效 | 修改后未重启客户端 | 检查配置文件格式是否合法 JSON | 重启客户端,确认配置被加载 |
| 协议流被日志串扰 | 在代码里使用了console.log打印业务信息 | 使用 Inspector 连接时观察解析错误 | 日志一律使用console.error或独立日志文件 |
最常见的根因还是console.log污染 stdout。MCP 在 stdio 传输模式下,stdout 是完全的协议通道,任何非 JSON-RPC 文本都会被客户端当作非法数据。排查时先确认代码里没有历史遗留的console.log,再检查启动配置。
6.2 工具列表为空或调用报错
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 客户端连接成功但看不到工具 | server.tool()注册逻辑存在语法或类型错误 | 查看 stderr 是否有异常堆栈 | 简化注册代码,确保工具注册在 connect 之前完成 |
| 调用工具返回参数校验错误 | schema 字段与模型猜测的值不一致 | 用 Inspector 查看工具 schema 详情 | 给参数增加describe,必要时用z.enum限定取值 |
| 工具调用超时 | 工具回调里有同步阻塞操作或未返回 | 检查 server 日志和耗时 | 保持回调轻薄,长任务异步化 |
| SDK 版本差异导致 API 不匹配 | 安装的版本与示例代码版本不一致 | 检查node_modules/@modelcontextprotocol/sdk版本 | 参考当前 SDK 文档调整server.tool或server.registerTool的用法 |
6.3 MCP 返回内容展示异常或解析失败
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 模型返回“无法读取数据” | 返回文本不是合法 JSON | 在 Inspector 中查看原始响应 | 使用JSON.stringify稳定输出 |
| 中文显示乱码 | 写入数据时编码不一致 | 检查存储层的字符编码 | 统一使用 UTF-8,写入时显式设置编码 |
| 模型误解了返回内容 | 返回字段缺少语义说明 | 让工具返回值包含简短字段名和常量值 | 在工具描述里写明字段含义,返回值保持精简 |
6.4 三个最容易踩的坑
第一个坑是使用console.log打印调试信息。现象是本地直接运行 server 正常,但一接入 Claude Desktop 或 Inspector 就连接失败。原因是 stdout 被协议占用。解决办法是调试日志都用console.error,或者接入专门的日志库输出到文件。
第二个坑是只验证 server 能启动,不验证工具调用。很多 MCP 示例项目能启动,但工具注册、参数 schema 转换、返回格式可能一直有问题。单独启动 server 无法发现这些问题,必须通过 Inspector 或真实客户端手动调用一次工具才能确认。这也是为什么本文在第 5 章强调端到端验证流程。
第三个坑是内存存储造成“数据丢”的错觉。进程重启后数据消失,不是代码 bug,而是数据层没有持久化。在 demo 阶段可以接受,但如果准备接入真实销售数据,要尽早把数据层从内存 Map 换成 SQLite 或 PostgreSQL,否则审计、并发和恢复都没法做。
7. 从演示走向生产:Salestrics 类项目的落地建议
7.1 把内存存储替换为 SQLite / PostgreSQL
内存 Map 只适合本地验证。生产环境至少需要:
- 持久化:数据不能因为进程重启而丢失。
- 事务:商机阶段变更和活动日志写入要能被回滚。