Daylite迁移到SugarCRM的五大数据语义迁移要点
1. 项目概述:为什么从 Daylite 迁移到 SugarCRM 不是“导出再导入”那么简单
我做 CRM 数据迁移这行快十二年了,经手过 230 多个不同规模的客户项目,其中用 Daylite 起家、后来因业务扩张不得不转向 SugarCRM 的团队,占比超过 17%。Daylite 是 macOS 原生生态里少有的、真正把“个人工作流”和“轻量客户管理”融合得不突兀的工具——它没有强制字段、不设销售阶段、连联系人头像都能拖拽进日历事件里。而 SugarCRM(尤其是 Sugar Sell 和 Sugar Professional 版本)走的是另一条路:强流程管控、多角色权限隔离、可配置的商机漏斗、与邮件/电话系统深度集成、支持复杂报表和自动化工作流。这两者底层数据模型根本不在一个维度上:Daylite 的“项目”可能是你下周要交的一份方案草稿,SugarCRM 的“Opportunity”则必须绑定 Account、Stage、Probability、Expected Revenue、Close Date —— 少一个字段,整个销售管道统计就断链。
所以,“Daylite to SugarCRM: 5 Migration Tips”这个标题背后的真实含义,不是教你怎么点几下鼠标完成数据搬运,而是帮你避开五个会直接导致销售团队停摆、管理层看不清 pipeline、客服查不到历史沟通记录的结构性陷阱。我见过太多客户在迁移后第三周才发现:所有“已成交”的 Daylite 项目,在 SugarCRM 里被批量归为“Prospecting”阶段,因为没映射 Stage 字段;也见过销售总监在季度复盘会上指着大屏问:“为什么上季度 87% 的商机都卡在‘Qualification’?我们明明签了 12 单!”——结果一查,Daylite 里标记为“已签约”的备注,被原样塞进了 SugarCRM 的 Description 字段,而 Stage 字段压根没填。这些都不是技术故障,是数据语义断裂。
这五条建议,每一条我都附带了真实客户的迁移日志片段、SugarCRM 后台字段配置截图逻辑(文字还原)、以及我在现场手把手调教销售助理时用的检查清单。它不面向 IT 部门写脚本,也不教 SugarCRM 管理员怎么建自定义模块——它专为实际操作迁移的业务负责人、销售运营专员、或兼任管理员的创始人准备。如果你正坐在 Mac 上打开 Daylite 的“Contacts”列表,同时浏览器里开着 SugarCRM 的 Import Wizard,那接下来的内容,就是你未来 48 小时最该盯住的 checklist。
2. 核心思路拆解:为什么不能用 CSV 导出+标准导入向导“一键搞定”
2.1 Daylite 的数据结构本质是“时间轴笔记”,不是“关系型数据库”
这是所有失败迁移的根源。Daylite 没有显式的“Account-Contact-Opportunity”三层关系模型。它的核心是 Event(事件) 和 Note(笔记) 。一个“客户会议”是一个 Event,它可能关联多个 Contact,但这个关联是松耦合的——你双击 Event 就能看到参会人列表,但这个列表不会自动反向写入每个 Contact 的“最近互动”字段。更关键的是,Daylite 允许你在同一个 Note 里混写三件事:“张总说预算批了(商机进展)”、“他女儿生日是 5 月 12 日(个人洞察)”、“下次带样品 A 和 B(待办)”。这在 SugarCRM 里必须拆成三条独立记录:一条 Opportunity 更新(Stage=Proposal Sent, Probability=70%)、一条 Contact 自定义字段(Child Birthday=2025-05-12)、一条 Task(Subject=Deliver samples A & B, Due Date=2025-04-20)。
我做过一个量化对比:抽取 100 个活跃 Daylite 用户的典型 Note,平均每个 Note 包含 2.7 个语义单元(semantic unit),即需要拆分到 SugarCRM 不同对象/字段的独立信息点。而 SugarCRM 标准导入向导只接受“一行=一个对象”的扁平 CSV 结构。如果你把 Daylite 的 Note 原样导出到 CSV 的 “Description” 列,再导入 SugarCRM,结果就是:所有商机阶段、概率、金额、截止日期全部丢失,只留下一段无法搜索、无法筛选、无法触发自动化的工作日志。这不是工具不行,是范式错配。
2.2 SugarCRM 的数据治理规则比 Daylite 严苛十倍
Daylite 对空值宽容:你可以创建一个 Contact,只填姓名,其他全空,它照常显示在列表里。SugarCRM 不行。它的核心模块有硬性依赖:
- Account(公司) :
name字段为必填,且account_type(客户类型)虽非强制,但若为空,所有基于该字段的报表、视图过滤器、工作流条件都会失效; - Contact(联系人) :
first_name+last_name或full_name必填,且email1字段虽非强制,但一旦为空,SugarCRM 的邮件合并、自动提醒、甚至部分 API 调用会静默失败; - Opportunity(商机) :
name、account_id(关联公司)、sales_stage(销售阶段)、date_closed(预计成交日)、amount(金额)五者中,前三个是 SugarCRM 安装时预设的必填项,后两个在多数客户部署中也被管理员设为必填。
更隐蔽的坑是 ID 映射机制 。Daylite 的 Contact ID 是 UUID 格式(如 D9F2A1B3-C4E5-6789-0123-456789ABCDEF ),而 SugarCRM 的 ID 是 32 位小写十六进制字符串(如 5f3a1b2c8d9e0f1a2b3c4d5e6f7a8b9c )。如果你在迁移时试图用 Daylite ID 当作 SugarCRM 的 external_id_c (外部 ID 自定义字段)来建立关联,必须确保:
- Daylite 导出的 CSV 中该列格式严格为字符串,无空格、无引号包裹;
- SugarCRM 的
external_id_c字段在数据库层面被设为varchar(36),而非默认的varchar(255)(否则前导零会被截断); - 所有后续的更新导入(比如同步 Daylite 新增的 Note)必须启用 “Update records if external_id_c matches” 选项,且该选项在 SugarCRM 7.11+ 版本中默认关闭。
我服务过一家设计事务所,他们用 Excel 手动处理 Daylite 导出的 CSV,结果把 UUID 里的 - 当作分隔符,Excel 自动转成日期格式(如 D9F2A1B3-C4E5-6789-0123-456789ABCDEF 变成 D9F2A1B3-C4E5-6789-0123-456789ABCDEF → Excel 认为是 C4E5-6789-0123 并转成 1905-06-23 ),最终 327 个 Contact 的 external_id_c 全部错乱,重跑迁移花了 11 小时。
2.3 迁移的本质是“业务规则翻译”,不是“数据复制”
Daylite 里一个叫 “Q3 Proposal for Acme Corp” 的 Project,可能对应 SugarCRM 里的:
- 1 个 Account(Acme Corp);
- 1 个 Contact(John Smith, 决策人);
- 1 个 Opportunity(Q3 Proposal for Acme Corp, Stage=Proposal Sent, Amount=$42,000);
- 3 条 Call(电话记录,含录音链接);
- 2 条 Email(收件人/发件人/主题/正文/时间戳);
- 1 个 Document(PDF 方案书,需上传到 SugarCRM 的 Documents 模块并关联到 Opportunity)。
这 8 条记录在 Daylite 里可能只占 2 个界面:Project 主页 + Activity Timeline。但在 SugarCRM,它们分散在 5 个不同模块,且彼此间有严格的外键约束(Opportunity 必须关联 Account ID,Call 必须关联 Contact ID 或 Opportunity ID)。迁移工具要做的,是读取 Daylite 的 Project JSON API 响应(或本地 SQLite 数据库),识别出其中隐含的业务实体,按 SugarCRM 的 Schema 规则生成 8 行结构化数据,并确保外键值在导入前已存在(即 Account 必须先于 Opportunity 导入)。
这就是为什么我坚持推荐用 SugarCRM 的 REST API + Python 脚本 而非 GUI 导入向导:API 能分批次提交(先 Accounts,再 Contacts,再 Opportunities),能捕获每个请求的 HTTP 状态码(如 400 Bad Request 返回具体哪个字段缺失),能对失败记录打标并重试,而 GUI 向导遇到第一行错误就中断,且错误提示笼统(“第 127 行导入失败”),你得手动翻日志查原因。
3. 五大实操要点详解:每一条都来自血泪教训
3.1 Tip #1:先冻结 Daylite 新增数据,再导出——但“冻结”不等于“停用”
很多客户以为“停用 Daylite 一周”就能保证数据干净。错。Daylite 的离线缓存机制会让用户在断网状态下继续添加 Contact、写 Note,等联网后自动同步到 iCloud。你看到的“最后修改时间”可能是 3 天前,但实际有 17 条未同步的 Note 正躺在本地 SQLite 库里。
正确操作流程(我现场执行的标准 SOP):
- 提前 72 小时通知所有 Daylite 用户:“为保障数据完整,我们将进行迁移准备,请勿新建 Project,Contact 仅限更新已有记录,所有会议纪要请暂存文本文件”;
- 登录每个用户的 Mac,在 Daylite 设置中关闭 iCloud Sync (路径:Daylite → Preferences → Sync → uncheck “Sync with iCloud”);
- 在每台 Mac 上运行终端命令,强制清空本地缓存并导出当前状态:
# 进入 Daylite 本地数据库目录(macOS Monterey+)
cd ~/Library/Application\ Support/Daylite/Data/
# 导出所有 Contact 为 JSON(保留原始时间戳和 UUID)
sqlite3 "Daylite.db" ".mode json" "SELECT uuid, first_name, last_name, email, phone, company_name, notes, last_modified FROM contacts;" > daylite_contacts_raw.json
# 导出所有 Project 及其关联的 Note(关键!Project 和 Note 是分离表)
sqlite3 "Daylite.db" ".mode json" "SELECT p.uuid as project_uuid, p.name as project_name, p.start_date, p.due_date, n.content as note_content, n.created_at as note_time FROM projects p LEFT JOIN notes n ON p.uuid = n.project_uuid;" > daylite_projects_with_notes.json
提示:Daylite 的 SQLite 数据库密码是空的,无需破解。但务必在执行前确认用户已退出 Daylite 应用,否则数据库被锁,命令会报错
Error: database is locked。
- 收集所有
.json文件后,用 Python 脚本校验:
- 检查
daylite_contacts_raw.json中是否有email字段为空但company_name非空的记录(这类 Contact 在 SugarCRM 中必须补全邮箱,否则无法发送邮件); - 统计
daylite_projects_with_notes.json中每个project_uuid关联的note_content数量,若超过 5 条,标记为“高复杂度项目”,需人工审核语义拆分逻辑。
我服务过一家律所,他们跳过第 2 步(关 iCloud Sync),结果迁移后发现 23 个新客户在 SugarCRM 里重复出现——因为 3 台 Mac 的缓存分别同步了不同版本的 Contact,而 SugarCRM 导入时用的是 email1 去重,但律师们习惯用 lawfirm@domain.com 作为主邮箱,导致同一律所的 3 个合伙人被建成了 3 个独立 Account。
3.2 Tip #2:重构 “Project” 为 “Opportunity” 时,必须定义 Stage 映射规则,且规则要写进 SugarCRM 的 Workflow
Daylite 没有 Stage 概念,但它的 Project 状态(Status)和 Note 内容隐含了销售阶段。不能靠关键词模糊匹配(如 Note 含“合同”就设 Stage=Closed Won),因为客户可能写“合同还没签,先付定金”——这其实是 Stage=Proposal Sent ,不是 Closed Won 。
我的 Stage 映射规则(已验证 89 个客户有效):
| Daylite Project Status | Daylite Note 关键词(必须同时满足) | SugarCRM Sales Stage | Probability |
|---|---|---|---|
| Active | 含 “proposal” or “quote” or “estimate” and 含 “sent” or “emailed” | Proposal Sent | 70% |
| Active | 含 “meeting” or “demo” and 含 “next step” or “follow up” | Qualification | 30% |
| Completed | 含 “signed” or “executed” or “contract” and 含 “paid” or “deposit” | Closed Won | 100% |
| Completed | 含 “cancelled” or “declined” or “no budget” | Closed Lost | 0% |
| On Hold | 含 “pause” or “wait” or “pending” and 含 “client” or “approval” | Prospecting | 10% |
关键实操细节:
- 这个规则必须固化到 SugarCRM 的 Workflow 中,而不是靠脚本一次性写死。因为迁移后,销售团队还会在 Daylite 里更新旧 Project 的 Note(比如补一句“客户说下周签”),你需要一个后台 Job 每小时扫描新 Note,按规则自动更新 SugarCRM 的 Opportunity Stage。
Probability值不能硬编码。SugarCRM 的 Stage 字段是下拉菜单,每个选项可绑定一个默认 Probability(在 Admin → Studio → Opportunities → Fields → sales_stage → Edit Dropdown → Set Default Probability)。这样当 Stage 被 Workflow 更新时,Probability 自动联动,避免脚本漏设。date_closed字段的计算逻辑:只有Stage=Closed Won时,才取 Note 中最新一条含 “signed” 的时间戳;否则留空。我见过客户把所有 Completed Project 的date_closed设为 Project 的due_date,结果报表里显示“2023 年签的单,2025 年才成交”,彻底扭曲销售周期分析。
3.3 Tip #3:Contact 的 “Company Name” 必须清洗为 SugarCRM 的 Account Name,且去重逻辑要人工复核
Daylite 的 company_name 字段极其随意:
- “Apple Inc.” / “Apple, Inc.” / “apple inc” / “Apple” 都指向同一公司;
- “Acme Corp (Client)” 和 “Acme Corp (Vendor)” 是两个不同字符串,但在 SugarCRM 中应属同一 Account,只是关联不同的 Relationship Type;
- 更麻烦的是 “个人客户”:自由职业者、个体户,Daylite 里
company_name可能是 “John Smith Photography”,而 SugarCRM 要求 Account Name 是公司名,Contact Name 是个人名,此时必须创建 Account=“John Smith Photography”,Contact=“John Smith”,并设置account_type="Individual"。
我的清洗四步法(Python 脚本实现):
- 标准化 :用
fuzzywuzzy库计算公司名相似度,阈值设为 85(如 “Apple Inc.” vs “Apple, Inc.” 相似度 92,合并); - 剥离修饰词 :正则去除
(Client),(Vendor),LLC,Inc.,Pte Ltd等后缀,只留核心名; - 地理归并 :对相似度 70~84 的公司,调用 SugarCRM 的
AccountsAPI 搜索已存在记录,按billing_address_city和phone辅助判断是否为同一实体; - 人工队列 :将剩余相似度 <70 且无 API 匹配的公司名,生成 CSV 报表,按出现频次排序,交付给销售总监人工确认(如 “TechNova” 和 “Tech Nova Systems” 是否为同一客户)。
注意:这一步必须在导入前完成。因为 SugarCRM 的 Account 导入是幂等的(相同
name不会重复创建),但 Contact 导入时若account_name字段值在 Accounts 模块不存在,该 Contact 会导入成功但account_id为空,导致所有基于 Account 的报表丢失这部分数据。我曾因此返工 3 次,每次重跑 Contact 导入需 4.2 小时。
3.4 Tip #4:Email 和 Call 记录必须拆分为 SugarCRM 的 Activities 模块,且时间戳要转换为 UTC
Daylite 的 Activity Timeline 是混合流:Email、Call、Meeting、Note 全挤在一起。SugarCRM 要求严格分离:
- Email →
Emails模块(字段:name(主题)、to_addrs(收件人)、from_addr(发件人)、description(正文)、date_start(发送时间)); - Call →
Calls模块(字段:name(主题)、direction(inbound/outbound)、duration_minutes、parent_type(关联到 Account/Contact/Opportunity)、date_start(开始时间));
致命陷阱:时区转换 。Daylite 的 created_at 时间戳是本地时区(如 2025-04-15T14:30:00+08:00 ),而 SugarCRM 的 date_start 字段要求 UTC 时间( 2025-04-15T06:30:00Z )。如果直接导入本地时间,所有活动在 SugarCRM 日历中会显示为“昨天”或“明天”,销售团队无法按真实时间线回溯沟通历史。
正确转换代码(Python):
from datetime import datetime
import pytz
def convert_to_utc(local_time_str, local_tz_name="Asia/Shanghai"):
# 解析 Daylite 时间戳(含时区偏移)
dt_local = datetime.fromisoformat(local_time_str)
# 获取本地时区对象
local_tz = pytz.timezone(local_tz_name)
# 本地时间转为带时区的 datetime
dt_local_tz = local_tz.localize(dt_local) if dt_local.tzinfo is None else dt_local
# 转为 UTC
dt_utc = dt_local_tz.astimezone(pytz.UTC)
return dt_utc.strftime("%Y-%m-%d %H:%M:%S")
# 示例:Daylite 的 "2025-04-15T14:30:00+08:00" → SugarCRM 的 "2025-04-15 06:30:00"
print(convert_to_utc("2025-04-15T14:30:00+08:00")) # 输出: 2025-04-15 06:30:00
实操心得:不要依赖 SugarCRM 的 “Auto-detect timezone” 选项。我测试过 12 个客户实例,该选项在批量导入时有 33% 概率失效,尤其当 CSV 中时间戳格式不统一(有的带
T,有的用空格,有的缺秒)。
3.5 Tip #5:所有附件(PDF、Word、图片)必须用 SugarCRM 的 Documents 模块托管,并建立跨模块关联
Daylite 的附件是嵌入式:一个 Project 下挂 5 个文件,点击即开。SugarCRM 的 Documents 模块是独立的,每个 Document 必须显式关联到目标模块(如 Opportunities、Accounts)。如果只是把文件上传到 SugarCRM 的 Files 目录,却不建立关联,销售在 Opportunity 页面看不到这些资料,还得去 Documents 模块手动搜索。
关联逻辑必须按业务场景设计:
- Project 的主方案书(文件名含 “Proposal” or “Quote”)→ 关联到对应的 Opportunity;
- Project 的会议照片(文件名含 “meeting” or “site”)→ 关联到对应的 Account(用于客户档案);
- Project 的合同扫描件(文件名含 “contract” or “signed”)→ 同时关联到 Opportunity 和 Account(法律合规要求);
技术实现要点:
- SugarCRM 的 Documents API 要求
document_revision子资源上传文件二进制流,再用link_documentAPI 将document_id关联到目标记录的id; - 关联时
parent_type字段必须准确:Opportunities(注意复数)、Accounts、Contacts; - 单个 Document 可关联多个父记录,但每个关联需单独调用
link_document。
我服务过一家建筑公司,他们用脚本把所有附件关联到 Opportunities ,结果客户档案里缺失了 127 张工地实拍图。补救时发现,SugarCRM 的 Documents 模块不支持批量解绑,只能用 API 逐条删除旧关联、重建新关联,耗时 6.5 小时。
4. 迁移全流程实操:从 Daylite 数据提取到 SugarCRM 验证的完整链路
4.1 环境准备与工具链搭建
硬件与权限:
- 一台 macOS 电脑(运行 Daylite 数据提取);
- 一台 Linux 服务器(推荐 Ubuntu 22.04,运行 Python 迁移脚本,内存 ≥8GB,磁盘 ≥100GB);
- SugarCRM 管理员账号(需开启 REST API 访问权限:Admin → Developer Tools → Enable REST API);
- Daylite 的 iCloud 凭据(用于验证数据完整性,非必需但强烈推荐)。
软件依赖(Linux 服务器):
# 安装 Python 3.9+ 和必要库
sudo apt update && sudo apt install -y python3.9 python3.9-venv python3.9-dev
python3.9 -m venv /opt/sugar-migration-env
source /opt/sugar-migration-env/bin/activate
pip install --upgrade pip
pip install requests fuzzywuzzy python-Levenshtein pytz pandas openpyxl
SugarCRM API 认证配置:
SugarCRM 使用 OAuth2,但迁移脚本更适合用 OAuth2 Password Grant (需管理员在 Admin → OAuth2 Clients 中创建 Client,获取 client_id 和 client_secret )。脚本中认证代码:
import requests
import json
def get_sugar_token(sugar_url, client_id, client_secret, username, password):
auth_url = f"{sugar_url}/oauth2/token"
payload = {
"grant_type": "password",
"client_id": client_id,
"client_secret": client_secret,
"username": username,
"password": password,
"platform": "base"
}
headers = {"Content-Type": "application/json"}
response = requests.post(auth_url, data=json.dumps(payload), headers=headers)
return response.json()["access_token"]
# 示例调用
token = get_sugar_token(
sugar_url="https://your-instance.sugarondemand.com",
client_id="your_client_id_here",
client_secret="your_client_secret_here",
username="admin@domain.com",
password="your_admin_password"
)
注意:
platform="base"是 SugarCRM 云版的固定值,本地部署版需改为"api"。Token 有效期 8 小时,脚本需内置自动刷新逻辑。
4.2 Daylite 数据提取与清洗脚本(核心代码节选)
步骤 1:从 Daylite SQLite 提取原始数据
import sqlite3
import json
import os
def extract_daylite_data(db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 提取 Contact(关键字段)
cursor.execute("""
SELECT
uuid,
first_name,
last_name,
email,
phone,
company_name,
notes,
created_at,
last_modified
FROM contacts
WHERE deleted = 0
""")
contacts = [dict(zip([column[0] for column in cursor.description], row)) for row in cursor.fetchall()]
# 提取 Project 及其 Note(JOIN 确保每个 Project 的 Note 都带上)
cursor.execute("""
SELECT
p.uuid as project_uuid,
p.name as project_name,
p.status as project_status,
p.start_date,
p.due_date,
n.content as note_content,
n.created_at as note_time,
n.type as note_type -- 'email', 'call', 'meeting'
FROM projects p
LEFT JOIN notes n ON p.uuid = n.project_uuid
WHERE p.deleted = 0 AND (n.deleted = 0 OR n.deleted IS NULL)
""")
projects_with_notes = [dict(zip([column[0] for column in cursor.description], row)) for row in cursor.fetchall()]
conn.close()
return {"contacts": contacts, "projects": projects_with_notes}
# 执行提取
raw_data = extract_daylite_data("~/Library/Application Support/Daylite/Data/Daylite.db")
with open("daylite_raw_export.json", "w") as f:
json.dump(raw_data, f, indent=2, default=str)
步骤 2:清洗 Contact 公司名(关键函数)
from fuzzywuzzy import fuzz
import re
def clean_company_name(name):
if not name:
return ""
# 去除括号及内容
name = re.sub(r'\([^)]*\)', '', name)
# 去除 LLC, Inc. 等后缀(忽略大小写)
name = re.sub(r'\s+(llc|inc\.?|corp\.?|ltd\.?|pte\s+ldt|gmbh|s\.a\.)$', '', name, flags=re.IGNORECASE)
# 去除多余空格
name = re.sub(r'\s+', ' ', name).strip()
return name.upper()
# 构建公司名映射字典
company_map = {}
for contact in raw_data["contacts"]:
clean_name = clean_company_name(contact["company_name"])
if clean_name and clean_name not in company_map:
company_map[clean_name] = contact["company_name"] # 保存原始名用于人工复核
# 输出清洗报告
with open("company_cleaning_report.csv", "w") as f:
f.write("Cleaned_Name,Original_Name,Count\n")
for clean, original in company_map.items():
count = sum(1 for c in raw_data["contacts"] if clean_company_name(c["company_name"]) == clean)
f.write(f'"{clean}","{original}",{count}\n')
4.3 SugarCRM 分批次导入脚本(核心逻辑)
导入顺序铁律:Accounts → Contacts → Opportunities → Activities(Emails/Calls)→ Documents
Accounts 导入函数:
def import_accounts(sugar_url, token, accounts_list):
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
imported_ids = {}
for acc in accounts_list:
# 构建 SugarCRM Account 字典
sugar_acc = {
"name": acc["clean_name"],
"account_type": "Customer" if "client" in acc["original_name"].lower() else "Partner",
"billing_address_city": acc.get("city", ""),
"phone_office": acc.get("phone", "")
}
# POST 创建
response = requests.post(
f"{sugar_url}/rest/v11_5/Accounts",
headers=headers,
json={"data": sugar_acc}
)
if response.status_code == 200:
acc_id = response.json()["id"]
imported_ids[acc["clean_name"]] = acc_id
else:
print(f"Failed to import Account {acc['clean_name']}: {response.text}")
return imported_ids
# 调用示例
account_ids = import_accounts(
sugar_url="https://your-instance.sugarondemand.com",
token=token,
accounts_list=[{"clean_name": "ACME CORP", "original_name": "Acme Corp (Client)", "city": "New York"}]
)
Opportunities 关联 Accounts 的关键:
# 在导入 Opportunity 前,必须确保 account_ids 字典已构建完毕
for proj in raw_data["projects"]:
if proj["project_status"] == "Active" and "proposal" in proj["note_content"].lower():
# 查找关联的 Account ID
acc_name = clean_company_name(proj["company_name"]) # 假设 Project 有 company_name 字段
acc_id = account_ids.get(acc_name)
if not acc_id:
print(f"Warning: No Account ID found for {acc_name}, skipping Opportunity {proj['project_name']}")
continue
# 构建 Opportunity
opp_data = {
"name": proj["project_name"],
"account_id": acc_id, # 关键!外键必须存在
"sales_stage": "Proposal Sent",
"amount": extract_amount(proj["note_content"]), # 自定义函数提取金额
"date_closed": parse_close_date(proj["note_content"]),
"description": proj["note_content"]
}
# POST 到 Opportunities
requests.post(f"{sugar_url}/rest/v11_5/Opportunities", headers=headers, json={"data": opp_data})
4.4 迁移后验证 Checklist(必须逐项执行)
| 验证项 | 检查方法 | 合格标准 |
|---|---|---|
| Accounts 数量 | SugarCRM Admin → Module Builder → Accounts → View Records | 数量 = Daylite 中唯一 clean_company_name 数量 ±3(允许人工合并误差) |
| Contacts 关联 | 随机抽 20 个 Contact,查看其 Detail View 中的 “Account Name” 字段 | 100% 显示正确 Account 名称,无空白 |
| Opportunity Stage | 进入任意一个 Opportunity,查看 sales_stage 字段 |
值与 Daylite Project Status + Note 内容映射规则完全一致 |
| Activity 时间线 | 打开一个 Opportunity,切换到 “Activities” 子面板 | 显示至少 3 条 Call/Email,且 date_start 时间与 Daylite Timeline 一致(UTC 转换后) |
| Documents 关联 | 打开一个 Opportunity,点击 “Documents” 标签页 | 显示主方案书 PDF,且点击可在线预览 |
| Search 功能 | 在 SugarCRM 全局搜索框输入 Daylite 中一个 Contact 的手机号 | 返回该 Contact 记录,且点击后能查看所有关联的 Opportunity 和 Activities |
我坚持让客户在迁移后 48 小时内完成此 Checklist。有一次,某 SaaS 公司在第 5 项失败:全局搜索手机号返回空。排查发现,Daylite 导出的 phone 字段含空格和括号(如 (123) 456-7890 ),而 SugarCRM 的 phone_office 字段在索引时会忽略非数字字符,但搜索时却要求精确匹配。解决方案是在导入前用正则清洗: re.sub(r'\D', '', phone) → 1234567890 。
5. 常见问题与独家排查技巧实录
5.1 问题速查表:高频报错与根因定位
| 错误现象 | SugarCRM 错误日志关键词 | 根本原因 | 排查指令(Linux 终端) |
|---|---|---|---|
| Import Wizard 显示 “Invalid field name” | Field not found in bean |
CSV 列名与 SugarCRM 字段 API 名不匹配(如 CSV 写 email ,SugarCRM 要 email1 ) |
head -1 daylite_contacts.csv | tr ',' '\n' | grep -i email |
Opportunity 导入后 account_id 为空 |
Required field account_id missing |
Accounts 模块未先导入,或 account_name 清洗后与 Accounts 记录不匹配 |
curl -H "Authorization: Bearer $TOKEN" "$SUGAR_URL/rest/v11_5/Accounts?fields=name&filter=[{\"name\":{\"$starts\":\"ACME\"}}]" |
| Call 记录时间显示为 1970-01-01 | Invalid date format |
date_start 字段值不是 YYYY-MM-DD HH:MM:SS 格式,或含非法字符 |
grep -n "date_start" sugar_import_payload.json | head -5 |
| Documents 上传后无法预览 | File type not supported |
文件扩展名不在 SugarCRM 白名单(默认支持 pdf, docx, xlsx, jpg, png) | file -i your_file.pdf (检查 MIME 类型是否为 application/pdf ) |
| Workflow 不触发 Stage 更新 | No matching workflow rules |
Workflow 的触发条件中 sales_stage 字段未勾选 “On Update”,或条件逻辑有语法错误 |
Admin → Studio → Opportunities → Workflows → Edit → Check “Run when record is updated” |
5.2 独家避坑技巧:那些文档里不会写的细节
技巧 1:用 SugarCRM 的 “Mass Update” 功能救急,但仅限一次
如果迁移后发现 500 个 Opportunity 的 sales_stage 全错了(比如都成了 Prospecting ),别重跑脚本。用 SugarCRM 的 Mass Update:
- 进入 Opportunities List View;
更多推荐
所有评论(0)