upload_sql
This commit is contained in:
parent
5c50ff0c2b
commit
872434b09c
241
data/migration.sql
Normal file
241
data/migration.sql
Normal file
@ -0,0 +1,241 @@
|
||||
-- ============================================================
|
||||
-- CRM 系统数据库迁移脚本
|
||||
-- 生成时间: 2026-01-26
|
||||
-- 说明: 从JSON文件迁移到MySQL数据库
|
||||
-- 数据统计: 50条客户记录, 16条跟进记录, 7条试用期记录
|
||||
-- ============================================================
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS `crm_db`
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `crm_db`;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 客户信息表 (customers)
|
||||
-- 来源: customers.json
|
||||
-- ============================================================
|
||||
DROP TABLE IF EXISTS `customers`;
|
||||
CREATE TABLE `customers` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '客户唯一标识',
|
||||
`created_at` DATETIME NOT NULL COMMENT '创建时间',
|
||||
`customer_name` VARCHAR(255) NOT NULL COMMENT '客户名称',
|
||||
`intended_product` VARCHAR(100) DEFAULT NULL COMMENT '意向产品/日期',
|
||||
`version` VARCHAR(50) DEFAULT NULL COMMENT '版本号',
|
||||
`description` TEXT COMMENT '问题描述',
|
||||
`solution` TEXT COMMENT '解决方案',
|
||||
`type` VARCHAR(50) DEFAULT NULL COMMENT '类型(反馈/功能问题/需求/咨询)',
|
||||
`module` VARCHAR(100) DEFAULT NULL COMMENT '模块(数据生成/模型工坊/数据空间/数据集/镜像管理)',
|
||||
`status_progress` VARCHAR(50) DEFAULT '进行中' COMMENT '状态进度(已完成/进行中/待排期)',
|
||||
`reporter` VARCHAR(100) DEFAULT NULL COMMENT '报告人',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_customer_name` (`customer_name`),
|
||||
INDEX `idx_created_at` (`created_at`),
|
||||
INDEX `idx_status_progress` (`status_progress`),
|
||||
INDEX `idx_type` (`type`),
|
||||
INDEX `idx_module` (`module`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户信息表';
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 客户跟进表 (followups)
|
||||
-- 来源: followups.json
|
||||
-- ============================================================
|
||||
DROP TABLE IF EXISTS `followups`;
|
||||
CREATE TABLE `followups` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '跟进记录唯一标识',
|
||||
`created_at` DATETIME NOT NULL COMMENT '创建时间',
|
||||
`customer_name` VARCHAR(255) NOT NULL COMMENT '客户名称',
|
||||
`deal_status` VARCHAR(50) DEFAULT '未成交' COMMENT '成交状态(已成交/未成交)',
|
||||
`customer_level` VARCHAR(10) DEFAULT 'C' COMMENT '客户等级(A/B/C)',
|
||||
`industry` VARCHAR(100) DEFAULT NULL COMMENT '行业',
|
||||
`follow_up_time` DATETIME DEFAULT NULL COMMENT '跟进时间',
|
||||
`notification_sent` TINYINT(1) DEFAULT 0 COMMENT '是否已发送通知(0:否 1:是)',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_customer_name` (`customer_name`),
|
||||
INDEX `idx_created_at` (`created_at`),
|
||||
INDEX `idx_deal_status` (`deal_status`),
|
||||
INDEX `idx_customer_level` (`customer_level`),
|
||||
INDEX `idx_follow_up_time` (`follow_up_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户跟进表';
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 客户试用期表 (trial_periods)
|
||||
-- 来源: trial_periods.json
|
||||
-- ============================================================
|
||||
DROP TABLE IF EXISTS `trial_periods`;
|
||||
CREATE TABLE `trial_periods` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '试用期记录唯一标识',
|
||||
`customer_name` VARCHAR(255) NOT NULL COMMENT '客户名称',
|
||||
`start_time` DATETIME NOT NULL COMMENT '试用开始时间',
|
||||
`end_time` DATETIME NOT NULL COMMENT '试用结束时间',
|
||||
`is_trial` TINYINT(1) DEFAULT 1 COMMENT '是否试用中(0:否 1:是)',
|
||||
`created_at` DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_customer_name` (`customer_name`),
|
||||
INDEX `idx_start_time` (`start_time`),
|
||||
INDEX `idx_end_time` (`end_time`),
|
||||
INDEX `idx_is_trial` (`is_trial`),
|
||||
INDEX `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户试用期表';
|
||||
|
||||
-- ============================================================
|
||||
-- 插入客户信息数据 (customers) - 共50条记录
|
||||
-- ============================================================
|
||||
INSERT INTO `customers` (`id`, `created_at`, `customer_name`, `intended_product`, `version`, `description`, `solution`, `type`, `module`, `status_progress`, `reporter`) VALUES
|
||||
('ecc0cdb8cc0f4777bf465ed604615706', '2026-01-13 18:56:41', '杰克', '2026-01-13', '1.9.4', '客户希望:根据参考图将瑕疵迁移到当前图片上,表示目前生成瑕疵图都困难,迁移更吃力;\n反馈:comfyui里通过图生图的方式还能生成比较满意的图片,但是批量生成任务瑕疵图片效果不太好。', '已帮客户训练lora、输出对应场景的提示词已文档形式交付给客户\n', '反馈', '数据生成', '已完成', ''),
|
||||
('d299725547a4497cab919f51bb9a9656', '2026-01-12 19:12:29', '予芯', '2025/12/22', '1.9.4', '训练失败看不到错误日志', '已解决', '功能问题', '模型工坊', '已完成', ''),
|
||||
('33d02d8cef4e45ddb5ca5d9d62939739', '2026-01-12 19:12:29', '诺因智能', '2025/12/23', '1.9.4', '客户端上传大文件失败', '技术优化,修复中', '功能问题', '数据空间', '已完成', ''),
|
||||
('1e27d5a740ef4204bcda26ae62f949b3', '2026-01-12 19:12:29', '良业', '2025-12-23', '1.9.4', '数据集发布感觉操作繁琐,如果在模型工坊里直接选择需要训练的图片会更方便', '', '反馈', '数据集', '已完成', ''),
|
||||
('5e3ce4f5c2ca4bec84242e9539c34d2b', '2026-01-12 19:12:29', '斯蒂尔', '2025-12-25', '1.9.4', '1.试用账号图片生成速度比较慢(2分钟1张图)\n2.生图效果不理想', '试用资源有限', '反馈', '数据生成', '已完成', ''),
|
||||
('880de2c25e1540c09408083debc28c36', '2026-01-12 19:12:29', '求之', '2025-12-26', '1.9.4', '1.训练过程不太透明,以及无法提前终止模型训练\n2.训练过程无法断点继续训练;\n3.试用账号资源有限,训练时长过长', '1、训练过程的透明化(日志化)和目前的易用化,有一些产品设计矛盾,之前主要还是在做易用化。\n接下来我们会在更透明,更强的客户对过程的操控粒度上提升。\n2、试用资源有限', '反馈', '模型工坊', '进行中', ''),
|
||||
('b67ef18dd5384eb1b1b45473b110a565', '2026-01-12 19:12:29', '诺因智能', '2025-12-26', '1.9.4', '训练进程显示什么时候上线', '训练日志可视化,我们大约需要一个月的时间可以满足上线使用', '需求', '模型工坊', '进行中', ''),
|
||||
('126b78e8321248f0ab168eb1284c745d', '2026-01-12 19:12:29', '诺因智能', '2025-12-26', '1.9.4', '(igev模型)全量训练是指从0开始? 增量是在现在的ckpt上做微调是吗? ', '全量训练:在加载开源基础的权重上进行增量训练\n增量训练:在您的数据上训练的checkpoint上进行的增量训练', '咨询', '模型工坊', '已完成', ''),
|
||||
('e47573d35b95439aaf63126148271885', '2026-01-12 19:12:29', '斯蒂尔', '2025-12-26', '1.9.4', '数据生成效果改进优化方式?试用账号配置卡数', '4090,24g,然后生图是单卡单线程\n目前系统上挂载了2张GPU', '咨询', '数据生成', '已完成', ''),
|
||||
('299d843ea730491997c4cae0e056baae', '2026-01-12 19:12:29', '良业', '2025-12-26', '1.9.4', '1、训练时需要添加测试图,无法理解,模型分析结果可以用训练集\n2、训练失败时需提示原因,方便下一步操作。', '1、训练过程中,根据训练集的数据来更新模型参数;在通过验证集来验证;\n2、训练失败有日志可以查看,您滚动下屏幕至右侧可以看到哈', '咨询', '模型工坊', '已完成', ''),
|
||||
('599c4a26023e4f48908de2145e716cdc', '2026-01-12 19:12:29', '良业', '2025-12-29', '1.9.4', '1.试了几个模型,没训练出来,感觉不出效果\n', '补充数据集', '咨询', '模型工坊', '已完成', ''),
|
||||
('50dba87f8f3c4f2c9a013f302ee07816', '2026-01-12 19:12:29', '诺因智能', '2025-12-29', '1.9.4', '1.总轮数与拓展参数(num_steps)不一致的时候,以哪个为准结束训练', '目前StereoNet-V2.5支持使用steps参数,其他的还是以epoch为准', '咨询', '模型工坊', '已完成', ''),
|
||||
('564e214d8ea244a4a72b521cd0b42504', '2026-01-12 19:12:29', '良业', '2025-12-30', '1.9.4', '户外同一个草坪测试,用yolo检测640*480图片,检测时间<100ms,分割精度<10像素,出错率1/10万,使用x3可以吗?', '100ms可以', '咨询', '模型工坊', '已完成', ''),
|
||||
('f9a2e729845d490aa3c2e74cc61260ac', '2026-01-12 19:12:29', '良业', '2025-12-30', '1.9.4', '训练任务无效果', '数据集少了,补数后需要保存快照', '咨询', '模型工坊', '已完成', ''),
|
||||
('5ae2913365e34f75b37dfdb4eb835081', '2026-01-12 19:12:29', '诺因智能', '2025-12-30', '1.9.4', '现在平台上的V2.5就可以,需要完成的ckpt,这个预计什么时候可以更新啊', '', '咨询', '模型工坊', '已完成', ''),
|
||||
('02c7e508f59140f996aeaa5ae729edf1', '2026-01-12 19:12:29', '诺因智能', '2025-12-31', '1.9.4', '境外网站的加速,下载部分数据集时速度会很慢', '', '反馈', '数据集', '进行中', ''),
|
||||
('65871e42c7004a3abe1eb7e5c1d818fb', '2026-01-12 19:12:29', '诺因智能', '2025-12-31', '1.9.4', '诺因智能需要完整的模型 checkpoint,而不仅仅是 backbone 部分', '', '需求', '模型工坊', '已完成', ''),
|
||||
('82ae50b5051a4957b160407c7ccdfb0e', '2026-01-12 19:12:29', '诺因智能', '2025-12-31', '1.9.4', '模型推理的指标对比按钮无法选择', '勾选多个版本进行选择', '反馈', '模型工坊', '进行中', ''),
|
||||
('45077caed030466dbb04b1f2cc6229b8', '2026-01-12 19:12:29', '诺因智能', '2025-12-31', '1.9.4', '推理评测可以增加定量参数,推理的时候能否增加一下epe衡量模型精度的指标', '', '需求', '模型工坊', '进行中', ''),
|
||||
('8db8d7cd971c4fb89a42a36a38d52c4f', '2026-01-12 19:12:29', '雷沃', '2026/1/9', '1.9.4', '工作流调试出现的是黑图', '重启comfyui服务后正常', '功能问题', '数据生成', '已完成', ''),
|
||||
('f1c13410c97c44f8a530ba5429967c20', '2026-01-12 19:12:29', '杰克', '2026-01-09', '1.9.4', '数据空间上传zip失败', '', '功能问题', '数据空间', '已完成', ''),
|
||||
('43eaae16007948a0a57543a0d88da065', '2026-01-13 16:57:24', '雷沃', '2026-01-13', '1.9.4', '客户想推一个镜像到咱们的仓库,上传一个私有模型,结果一直报错:unauthorized: project lovol-trial not found ', '重新配置镜像仓库', '咨询', '镜像管理', '已完成', ''),
|
||||
('395853baa43b496b94ea0b1ef763a288', '2026-01-13 17:22:11', '有你同创', '2026-01-12', '1.9.4', '数据空间上传数据集失败', '回复客户先使用二进制文件上传', '咨询', '数据空间', '已完成', ''),
|
||||
('fa49ba80265340c59cf9ac4bd0fc58cc', '2026-01-14 15:53:30', '创客', '2026-01-12', '1.9.4', '客户表示仅使用平台的yolo模型进行板端推理,其他功能不cover', '', '需求', '模型工坊', '已完成', ''),
|
||||
('bbd0b902bde74b7485d54e78ed406065', '2026-01-14 16:49:58', '诺因智能', '2026-01-14', '1.9.4', '试用账号数据迁移至正式版本', '', '需求', '数据空间', '进行中', ''),
|
||||
('dfaba68d526d4344873e653ea2263c61', '2026-01-15 15:21:07', '诺因智能', '2026-01-15', '1.9.4', '客户反馈深度估计数据集上传失败', '已触达客户数据集的命名格式要按照平台规范填写 ', '反馈', '数据集', '已完成', ''),
|
||||
('36b757f470be40178309badfd73edc19', '2026-01-15 16:49:38', '智绘科技有限公司', '2026-01-15', '1.9.4', '体验数据闭环平台两周,已开通robogo平台账号信息', '已开通', '咨询', '数据集', '已完成', ''),
|
||||
('445bc822793c4a88a57628d211904147', '2026-01-16 13:35:12', '创客', '2026-01-16', '1.9.4', ' 推理评测结果指标低', '1、训练集太少,建议增加训练集和训练轮数\n2、模型欠拟合,模型学习的特征少;导致推理评测时测试集指标不理想', '反馈', '模型工坊', '已完成', ''),
|
||||
('78cedb9de2e3448e900e9023e6fc0055', '2026-01-16 14:48:52', '雷沃', '2026-01-16', '1.9.4', '【数据挖掘】客户表示以图搜图功能很有必要,有助于数据清洗和特定数据集的建立帮助很大,希望能叠加提取功能,能够利用以图搜图建了特定数据集 ', '规划中', '需求', '数据集', '进行中', ''),
|
||||
('2fc4583c22f84e918a6cc5c5600e0d7a', '2026-01-16 14:56:41', '雷沃', '2026-01-16', '1.9.4', '客户希望:语义分割需要支持标注微调', '规划中', '需求', '数据集', '进行中', ''),
|
||||
('8318da5ebb544e25b3115ceac7f84547', '2026-01-16 15:13:12', '雷沃', '2026-01-16', '1.9.4', '客户表示:数据生成,自动标注,模型训练 无进度条展示', '已回复客户,模型训练下个版本支持日志可视化;生成和自动标注在规划中', '需求', '数据生成', '进行中', ''),
|
||||
('d7a4c3d31b044d6dacb0698722fe45a9', '2026-01-16 15:17:33', '雷沃', '2026-01-16', '1.9.4', '客户反馈:模型训练部分,只有轮数、宽度、高度、学习率可调,其他拓展参数填写不生效', '规划中', '反馈', '模型工坊', '进行中', ''),
|
||||
('14ceace7c6504a80a31150585245fa8d', '2026-01-16 15:22:35', '雷沃', '2026-01-16', '1.9.4', '客户反馈:部署推理只适配X5,想支持s100板子', '已为客户引流robogo平台', '反馈', '模型工坊', '已完成', ''),
|
||||
('cdbe6646c31f4dbba24371a233f2df55', '2026-01-16 15:28:25', '雷沃', '2026-01-16', '1.9.4', '客户反馈:版本快照部分,可以增加备注功能,更新数据集版本后添加更新内容,便于管理和版本回滚', '规划中', '反馈', '数据集', '进行中', ''),
|
||||
('c478ccd0f8c94a818e576dc35a307e4b', '2026-01-16 15:29:19', '雷沃', '2026-01-16', '1.9.4', '客户反馈:调试工作流-调试环境有时打开很慢(10分钟以上)', '已回复客户试用账号资源有限', '反馈', '数据生成', '已完成', ''),
|
||||
('f40d2463275f4789b0a6a553eb459c2e', '2026-01-16 15:32:04', '雷沃', '2026-01-16', '1.9.4', '客户反馈:对车头数据集进行ai辅助标注;框选的效果优于描边。', '已回复客户:后面会上SAM3 ', '反馈', '数据集', '进行中', ''),
|
||||
('d296ac26a2754f90bdb2cdb3ae98d69d', '2026-01-16 15:34:35', '雷沃', '2026-01-16', '1.9.4', '客户反馈:建立行人数据集,对不同的标签(person、people、pedestrain)进行标注,person标签有标注,people和pedestrain标注几乎没有以及标注失败', '由于Ground dino 未续费,临时回复客户:标注功能在维护升级', '反馈', '数据集', '已完成', ''),
|
||||
('3b6b4fd6099b402d942648142d25c67f', '2026-01-16 15:39:17', '雷沃', '2026-01-16', '1.9.4', '客户反馈:数据生成的部分照片跟预期还是差距\n客户预期:生成车\n实际:生成墙体、路面等不相关的图片', '登录客户账号查看,批量生成任务的prompt太单一 ', '反馈', '数据生成', '已完成', ''),
|
||||
('56b11e93e2084fe7943894b7282c93fa', '2026-01-16 19:11:10', '雷沃', '2026-01-16', '1.9.4', '客户表示仍想继续试用数据闭环平台', '已为客户续费一周,截止时间下周5(1月23号)', '需求', '数据生成', '已完成', ''),
|
||||
('c9ea17dd1d434cd2a0f3bc2a9330d7dc', '2026-01-19 11:03:00', '有你同创', '2026-01-16', '1.9.4', '客户反馈:\n1、数据集创建失败;\n2、数据集创建数量限制了1000张图片', '协程死锁导致插入失败,调整批量大小和增加超时控制避免死锁', '功能问题', '数据集', '已完成', ''),
|
||||
('2a88dc53377446958f317e5418f55940', '2026-01-20 11:24:38', '杰克', '2026-01-19', '1.9.4', '客户反馈:Gemini根据提供的prompt的生图效果相对较好,将gemini生图效果较好的prompt放到地瓜平台也试了下,生成图片里面有很多的"杂质"', '已回复客户:平台comfyui可以支持多种模型,后面也会接入gemini', '反馈', '数据生成', '进行中', ''),
|
||||
('2eaed1848b2b465e81c1b23582ecbea3', '2026-01-20 15:46:29', '智绘科技有限公司', '2026-01-20', '1.9.4', '客户反馈:标注一些图像,微调了dinox后,再用微调的模型进行自动标注没输出结果', '已安抚客户:付费的模型目前只对付费用户开放,先向上申请下额度,引导客户体验下其他功能', '反馈', '数据集', '已完成', ''),
|
||||
('4e0f9d1f0b3647e780cd031085027806', '2026-01-20 15:50:58', '有你同创', '2026-01-20', '1.9.4', '客户咨询:\n1、对于负样本图片的处理,现在平台有支持吗?主要增加一些负样本减少误检\n2、负样本怎么进行上传?', '已回复客户:\n1、支持\n2、可以放在一个数据集的文件夹里面', '咨询', '数据集', '已完成', ''),
|
||||
('74d96f1b6219460d80d5cf659862375e', '2026-01-20 18:01:12', '有你同创', '2026-01-20', '1.9.4', '客户反馈:使用ground dino 1.6pro 标注不生效', '已安抚客户:付费的模型目前暂时只对付费用户开放,您可以使用1.0 swinB 进行标注。', '反馈', '数据集', '已完成', ''),
|
||||
('0b0ed317c418455094a7132d495c153d', '2026-01-21 10:48:40', '杰克', '2026-01-21', '1.9.4', '客户反馈:数据空间的数据没办法移动到素材库,还得单独重新上传一遍到素材库', '已回复客户:因不同的数据隐私和数据安全考虑。后续开通正式账号,前提是给我们开调试账号的授权下,素材库和数据空间数据共享可以一起调优或者配合', '反馈', '数据空间', '已完成', ''),
|
||||
('9498c7fcaa5442b09268ebcb08b380fa', '2026-01-21 16:01:38', '智绘科技有限公司', '2026-01-21', '1.9.4', '客户反馈:端侧推理的任务运行十几个小时仍未成功', '已解决并回复客户:试用资源有限,手动调整队列的优先级', '反馈', '模型工坊', '已完成', ''),
|
||||
('747a4999a0cc426cb01ddbfed662433a', '2026-01-22 17:43:02', '有你同创', '2026-01-22', '1.9.4', '客户表示:手动标注体验没有labelme 操作方便', '已回复客户:手动标注已经在重新设计了,整体交互后面会和labelme保持一致', '需求', '数据集', '待排期', ''),
|
||||
('5a2dd11c118b41f588fa0dc7f4151977', '2026-01-23 15:29:49', '有你同创', '2026-01-23', '1.9.4', '客户反馈:查看训练实时日志报错', '已安抚客户:由于平台升级发版,引导客户重新跑训练试试', '反馈', '模型工坊', '已完成', ''),
|
||||
('ce1437fae330472784464983294f1bbb', '2026-01-26 10:51:58', '有你同创', '2026-01-25', '1.9.5', '客户反馈:平台会偶尔报:TypeError: Failed to fetch dynamically imported module:', '已引导客户:清除浏览器缓存重试', '反馈', '数据生成', '已完成', '');
|
||||
|
||||
-- ============================================================
|
||||
-- 插入客户跟进数据 (followups) - 共16条记录
|
||||
-- ============================================================
|
||||
INSERT INTO `followups` (`id`, `created_at`, `customer_name`, `deal_status`, `customer_level`, `industry`, `follow_up_time`, `notification_sent`) VALUES
|
||||
('30fa49832f40408e9d0d6eff3f5587bf', '2026-01-14 15:54:18', '创客', '未成交', 'C', '无', '2026-01-14 15:54:00', 1),
|
||||
('ad2e4f8ff56446ecab988e9806df6be4', '2026-01-14 16:03:22', '雷沃', '未成交', 'C', '无', '2026-01-14 16:03:00', 1),
|
||||
('af653bee68584694b1b0165afe954380', '2026-01-14 16:03:37', '诺因智能', '已成交', 'B', '无', '2026-01-14 16:03:00', 1),
|
||||
('bacdf2c1c2ad443eb814d902ad219ae5', '2026-01-15 15:23:04', '雷沃', '未成交', 'C', '无', '2026-01-15 18:00:00', 1),
|
||||
('fc6dca1345a5480181210d0fa1ab5f05', '2026-01-16 13:23:51', '雷沃', '未成交', 'C', '无', '2026-01-16 18:00:00', 1),
|
||||
('54903e1b80484a44bc613e7e051517ab', '2026-01-16 13:24:36', '诺因智能', '已成交', 'A', '无', '2026-01-16 18:00:00', 1),
|
||||
('c66fc68cac124ceb84b7af6fc52a5c09', '2026-01-20 10:52:19', '有你同创', '未成交', 'C', '无', '2026-01-20 11:00:00', 1),
|
||||
('fc154f2a8e214c8aa728f0bf34b5539b', '2026-01-20 10:59:06', '杰克', '未成交', 'A', '无', '2026-01-20 11:00:00', 1),
|
||||
('2023f1d60fbf4f7cbb61800a92b8cd65', '2026-01-20 15:18:07', '杰克', '未成交', 'A', '无', '2026-01-20 16:00:00', 1),
|
||||
('9a801d40eb1641fb996f4cae5160259a', '2026-01-21 16:49:24', '杰克', '未成交', 'A', '无', '2026-01-21 17:00:00', 1),
|
||||
('9d30b4aaf1cd489ca3fb023ad2a68413', '2026-01-21 17:03:28', '创客', '未成交', 'C', '无', '2026-01-21 18:00:00', 1),
|
||||
('ad71a20316dc40b38b4e4d71ae07f266', '2026-01-22 16:01:28', '创客', '未成交', 'C', '无', '2026-01-22 17:00:00', 1),
|
||||
('b4f017c09a2248db958cfc6103261068', '2026-01-22 16:01:44', '有你同创', '未成交', 'C', '无', '2026-01-22 17:00:00', 1),
|
||||
('9f145b1fb4fd4331979e288cf16bb88c', '2026-01-22 16:02:05', '雷沃', '未成交', 'C', '无', '2026-01-22 17:00:00', 1),
|
||||
('87ccd2494c284782b532d4c224d855d8', '2026-01-23 16:05:19', '雷沃', '未成交', 'C', '无', '2026-01-23 18:00:00', 1),
|
||||
('03563c64d89b42ebb39d999a5965989f', '2026-01-23 16:05:37', '创客', '未成交', 'C', '无', '2026-01-23 18:00:00', 1);
|
||||
|
||||
-- ============================================================
|
||||
-- 插入客户试用期数据 (trial_periods) - 共7条记录
|
||||
-- ============================================================
|
||||
INSERT INTO `trial_periods` (`id`, `customer_name`, `start_time`, `end_time`, `is_trial`, `created_at`) VALUES
|
||||
('41e38867a7d94745921f8ab9985533c3', '雷沃', '2026-01-05 17:18:00', '2026-01-23 18:20:00', 1, '2026-01-13 17:19:13'),
|
||||
('2751cd4ae9f54bf5a336bc3c7e6b7b6a', '有你同创', '2026-01-07 17:22:00', '2026-01-25 18:30:00', 1, '2026-01-13 17:22:45'),
|
||||
('d8c235c7c8424ba79b4fdee597e17368', '杰克', '2026-01-08 15:51:00', '2026-01-21 15:51:00', 1, '2026-01-14 15:51:43'),
|
||||
('39d7ab1a0ae44fbaaf079416ea9a4bf6', '诺因智能', '2026-01-05 15:51:00', '2026-01-16 18:20:00', 1, '2026-01-14 15:52:00'),
|
||||
('b24d7c84b1bd4e4bb993f562995258b4', '创客', '2026-01-12 15:53:00', '2026-01-23 15:53:00', 1, '2026-01-14 15:53:47'),
|
||||
('b045c9165ed847f3ba0c0d078c25bfed', '智绘科技有限公司', '2026-01-15 16:49:00', '2026-01-30 18:00:00', 1, '2026-01-15 16:50:17'),
|
||||
('27b2aaf88ac2484fa209750c14ec1047', '睿尔曼', '2026-01-22 14:57:00', '2026-02-06 18:00:00', 1, '2026-01-22 14:57:30');
|
||||
|
||||
-- ============================================================
|
||||
-- 创建视图 (可选)
|
||||
-- ============================================================
|
||||
|
||||
-- 活跃试用客户视图
|
||||
CREATE OR REPLACE VIEW `v_active_trials` AS
|
||||
SELECT
|
||||
tp.id,
|
||||
tp.customer_name,
|
||||
tp.start_time,
|
||||
tp.end_time,
|
||||
DATEDIFF(tp.end_time, NOW()) AS days_remaining,
|
||||
tp.created_at
|
||||
FROM `trial_periods` tp
|
||||
WHERE tp.is_trial = 1 AND tp.end_time > NOW();
|
||||
|
||||
-- 客户问题统计视图
|
||||
CREATE OR REPLACE VIEW `v_customer_issues_summary` AS
|
||||
SELECT
|
||||
customer_name,
|
||||
COUNT(*) AS total_issues,
|
||||
SUM(CASE WHEN status_progress = '已完成' THEN 1 ELSE 0 END) AS completed_issues,
|
||||
SUM(CASE WHEN status_progress = '进行中' THEN 1 ELSE 0 END) AS ongoing_issues,
|
||||
SUM(CASE WHEN status_progress = '待排期' THEN 1 ELSE 0 END) AS pending_issues
|
||||
FROM `customers`
|
||||
GROUP BY customer_name
|
||||
ORDER BY total_issues DESC;
|
||||
|
||||
-- 模块问题统计视图
|
||||
CREATE OR REPLACE VIEW `v_module_issues_summary` AS
|
||||
SELECT
|
||||
module,
|
||||
type,
|
||||
COUNT(*) AS issue_count,
|
||||
SUM(CASE WHEN status_progress = '已完成' THEN 1 ELSE 0 END) AS completed,
|
||||
SUM(CASE WHEN status_progress = '进行中' THEN 1 ELSE 0 END) AS ongoing,
|
||||
SUM(CASE WHEN status_progress = '待排期' THEN 1 ELSE 0 END) AS pending
|
||||
FROM `customers`
|
||||
WHERE module IS NOT NULL AND module != ''
|
||||
GROUP BY module, type
|
||||
ORDER BY module, issue_count DESC;
|
||||
|
||||
-- 客户跟进统计视图
|
||||
CREATE OR REPLACE VIEW `v_followup_summary` AS
|
||||
SELECT
|
||||
customer_name,
|
||||
COUNT(*) AS total_followups,
|
||||
MAX(deal_status) AS latest_deal_status,
|
||||
MAX(customer_level) AS customer_level,
|
||||
MAX(follow_up_time) AS last_follow_up_time
|
||||
FROM `followups`
|
||||
GROUP BY customer_name
|
||||
ORDER BY last_follow_up_time DESC;
|
||||
|
||||
-- 成交客户列表视图
|
||||
CREATE OR REPLACE VIEW `v_closed_deals` AS
|
||||
SELECT DISTINCT
|
||||
f.customer_name,
|
||||
f.customer_level,
|
||||
f.industry,
|
||||
f.follow_up_time AS deal_time
|
||||
FROM `followups` f
|
||||
WHERE f.deal_status = '已成交'
|
||||
ORDER BY f.follow_up_time DESC;
|
||||
|
||||
-- ============================================================
|
||||
-- 数据迁移完成
|
||||
-- 统计信息:
|
||||
-- - 客户记录: 50条
|
||||
-- - 跟进记录: 16条
|
||||
-- - 试用期记录: 7条
|
||||
-- - 已成交客户: 诺因智能
|
||||
-- ============================================================
|
||||
@ -816,11 +816,26 @@ td.overflow-cell {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
.delete-btn:hover:not(.disabled) {
|
||||
background-color: #c0392b;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Disabled delete button style */
|
||||
.delete-btn.disabled,
|
||||
.delete-followup-btn.disabled {
|
||||
background-color: #bdc3c7;
|
||||
color: #7f8c8d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.delete-btn.disabled:hover,
|
||||
.delete-followup-btn.disabled:hover {
|
||||
background-color: #bdc3c7;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Dashboard Stats */
|
||||
.dashboard-stats {
|
||||
display: grid;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CRM客户管理系统</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1.2">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
|
||||
@ -285,7 +285,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>客户名称</th>
|
||||
<th>是否试用</th>
|
||||
<th>状态</th>
|
||||
<th>开始时间</th>
|
||||
<th>结束时间</th>
|
||||
<th>创建时间</th>
|
||||
@ -993,9 +993,9 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/static/js/trial-periods.js?v=1.1"></script>
|
||||
<script src="/static/js/trial-periods-page.js?v=1.1"></script>
|
||||
<script src="/static/js/main.js?v=1.1"></script>
|
||||
<script src="/static/js/trial-periods.js?v=1.2"></script>
|
||||
<script src="/static/js/trial-periods-page.js?v=1.2"></script>
|
||||
<script src="/static/js/main.js?v=1.2"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -17,6 +17,44 @@ async function authenticatedFetch(url, options = {}) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// 解析 JWT Token 获取用户信息
|
||||
function parseJwtToken(token) {
|
||||
try {
|
||||
if (!token) return null;
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (e) {
|
||||
console.error('Error parsing JWT token:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户角色
|
||||
function getUserRole() {
|
||||
const token = localStorage.getItem('crmToken');
|
||||
const payload = parseJwtToken(token);
|
||||
return payload ? payload.role : null;
|
||||
}
|
||||
|
||||
// 获取当前用户名
|
||||
function getUsername() {
|
||||
const token = localStorage.getItem('crmToken');
|
||||
const payload = parseJwtToken(token);
|
||||
return payload ? payload.username : null;
|
||||
}
|
||||
|
||||
// 检查当前用户是否可以删除数据
|
||||
// admin 用户 (role: viewer) 不能删除
|
||||
// administrator 用户 (role: admin) 可以删除
|
||||
function canDelete() {
|
||||
const role = getUserRole();
|
||||
return role === 'admin';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 登录守卫
|
||||
const token = localStorage.getItem('crmToken');
|
||||
@ -793,11 +831,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
const actionTd = document.createElement('td');
|
||||
actionTd.classList.add('action-cell');
|
||||
|
||||
// 根据用户权限决定删除按钮是否可用
|
||||
const deleteDisabled = !canDelete();
|
||||
const deleteClass = deleteDisabled ? 'action-btn delete-btn disabled' : 'action-btn delete-btn';
|
||||
const deleteTitle = deleteDisabled ? '无删除权限' : '删除';
|
||||
|
||||
actionTd.innerHTML = `
|
||||
<button class="action-btn edit-btn" data-id="${customer.id}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="action-btn delete-btn" data-id="${customer.id}">
|
||||
<button class="${deleteClass}" data-id="${customer.id}" title="${deleteTitle}" ${deleteDisabled ? 'disabled' : ''}>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
`;
|
||||
@ -815,7 +859,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||||
document.querySelectorAll('.delete-btn:not(.disabled)').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const customerId = this.getAttribute('data-id');
|
||||
deleteCustomer(customerId);
|
||||
@ -1845,7 +1889,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
<button class="action-btn edit-followup-btn" data-id="${followUp.id}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="action-btn delete-followup-btn" data-id="${followUp.id}">
|
||||
<button class="action-btn delete-followup-btn ${!canDelete() ? 'disabled' : ''}" data-id="${followUp.id}" title="${!canDelete() ? '无删除权限' : '删除'}" ${!canDelete() ? 'disabled' : ''}>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@ -1862,8 +1906,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
});
|
||||
|
||||
// Add delete event listeners
|
||||
document.querySelectorAll('.delete-followup-btn').forEach(btn => {
|
||||
// Add delete event listeners (only for users with delete permission)
|
||||
document.querySelectorAll('.delete-followup-btn:not(.disabled)').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const followUpId = this.getAttribute('data-id');
|
||||
deleteFollowUp(followUpId);
|
||||
|
||||
@ -335,7 +335,7 @@ function renderTrialPeriodsTable() {
|
||||
<button class="action-btn edit-btn" data-id="${period.id}" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="action-btn delete-btn" data-id="${period.id}" title="删除">
|
||||
<button class="action-btn delete-btn ${!canDelete() ? 'disabled' : ''}" data-id="${period.id}" title="${!canDelete() ? '无删除权限' : '删除'}" ${!canDelete() ? 'disabled' : ''}>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@ -352,7 +352,7 @@ function renderTrialPeriodsTable() {
|
||||
});
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('.delete-btn').forEach(btn => {
|
||||
tbody.querySelectorAll('.delete-btn:not(.disabled)').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const periodId = this.getAttribute('data-id');
|
||||
deleteTrialPeriodFromPage(periodId);
|
||||
|
||||
@ -43,15 +43,30 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 定义用户账户和角色
|
||||
// admin: 只读用户,不能删除数据
|
||||
// administrator: 管理员,拥有完全控制权限
|
||||
type UserInfo struct {
|
||||
Password string
|
||||
Role string
|
||||
}
|
||||
|
||||
users := map[string]UserInfo{
|
||||
"admin": {Password: "digua666", Role: "viewer"}, // 只读用户
|
||||
"administrator": {Password: "digua888", Role: "admin"}, // 管理员
|
||||
}
|
||||
|
||||
// 验证用户名和密码
|
||||
if req.Username != "admin" || req.Password != "digua666" {
|
||||
user, exists := users[req.Username]
|
||||
if !exists || user.Password != req.Password {
|
||||
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成 JWT Token
|
||||
// 生成 JWT Token,包含角色信息
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"username": req.Username,
|
||||
"role": user.Role,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(), // 24小时过期
|
||||
})
|
||||
|
||||
|
||||
387
migrate_to_mysql.go
Normal file
387
migrate_to_mysql.go
Normal file
@ -0,0 +1,387 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// Customer 客户信息结构
|
||||
type Customer struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
CustomerName string `json:"customerName"`
|
||||
IntendedProduct string `json:"intendedProduct"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Solution string `json:"solution"`
|
||||
Type string `json:"type"`
|
||||
Module string `json:"module"`
|
||||
StatusProgress string `json:"statusProgress"`
|
||||
Reporter string `json:"reporter"`
|
||||
}
|
||||
|
||||
// Followup 客户跟进结构
|
||||
type Followup struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
CustomerName string `json:"customerName"`
|
||||
DealStatus string `json:"dealStatus"`
|
||||
CustomerLevel string `json:"customerLevel"`
|
||||
Industry string `json:"industry"`
|
||||
FollowUpTime string `json:"followUpTime"`
|
||||
NotificationSent bool `json:"notificationSent"`
|
||||
}
|
||||
|
||||
// TrialPeriod 试用期结构
|
||||
type TrialPeriod struct {
|
||||
ID string `json:"id"`
|
||||
CustomerName string `json:"customerName"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
IsTrial bool `json:"isTrial"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// 数据库配置
|
||||
type DBConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Database string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 默认数据库配置
|
||||
config := DBConfig{
|
||||
Host: "localhost",
|
||||
Port: 3306,
|
||||
User: "root",
|
||||
Password: "", // 请修改为实际密码
|
||||
Database: "crm_db",
|
||||
}
|
||||
|
||||
// 从环境变量读取配置
|
||||
if host := os.Getenv("DB_HOST"); host != "" {
|
||||
config.Host = host
|
||||
}
|
||||
if user := os.Getenv("DB_USER"); user != "" {
|
||||
config.User = user
|
||||
}
|
||||
if pwd := os.Getenv("DB_PASSWORD"); pwd != "" {
|
||||
config.Password = pwd
|
||||
}
|
||||
if db := os.Getenv("DB_NAME"); db != "" {
|
||||
config.Database = db
|
||||
}
|
||||
|
||||
// JSON 文件路径
|
||||
dataDir := "./data"
|
||||
if len(os.Args) > 1 {
|
||||
dataDir = os.Args[1]
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
config.User, config.Password, config.Host, config.Port, config.Database)
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
log.Fatalf("连接数据库失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Fatalf("数据库连接测试失败: %v", err)
|
||||
}
|
||||
log.Println("✅ 数据库连接成功")
|
||||
|
||||
// 创建表
|
||||
if err := createTables(db); err != nil {
|
||||
log.Fatalf("创建表失败: %v", err)
|
||||
}
|
||||
log.Println("✅ 数据表创建成功")
|
||||
|
||||
// 迁移客户数据
|
||||
customersFile := fmt.Sprintf("%s/customers.json", dataDir)
|
||||
if err := migrateCustomers(db, customersFile); err != nil {
|
||||
log.Printf("⚠️ 迁移客户数据失败: %v", err)
|
||||
} else {
|
||||
log.Println("✅ 客户数据迁移成功")
|
||||
}
|
||||
|
||||
// 迁移跟进数据
|
||||
followupsFile := fmt.Sprintf("%s/followups.json", dataDir)
|
||||
if err := migrateFollowups(db, followupsFile); err != nil {
|
||||
log.Printf("⚠️ 迁移跟进数据失败: %v", err)
|
||||
} else {
|
||||
log.Println("✅ 跟进数据迁移成功")
|
||||
}
|
||||
|
||||
// 迁移试用期数据
|
||||
trialsFile := fmt.Sprintf("%s/trial_periods.json", dataDir)
|
||||
if err := migrateTrialPeriods(db, trialsFile); err != nil {
|
||||
log.Printf("⚠️ 迁移试用期数据失败: %v", err)
|
||||
} else {
|
||||
log.Println("✅ 试用期数据迁移成功")
|
||||
}
|
||||
|
||||
log.Println("🎉 数据迁移完成!")
|
||||
}
|
||||
|
||||
func createTables(db *sql.DB) error {
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS customers (
|
||||
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
customer_name VARCHAR(255) NOT NULL,
|
||||
intended_product VARCHAR(100),
|
||||
version VARCHAR(50),
|
||||
description TEXT,
|
||||
solution TEXT,
|
||||
type VARCHAR(50),
|
||||
module VARCHAR(100),
|
||||
status_progress VARCHAR(50) DEFAULT '进行中',
|
||||
reporter VARCHAR(100),
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_status_progress (status_progress)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS followups (
|
||||
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
customer_name VARCHAR(255) NOT NULL,
|
||||
deal_status VARCHAR(50) DEFAULT '未成交',
|
||||
customer_level VARCHAR(10) DEFAULT 'C',
|
||||
industry VARCHAR(100),
|
||||
follow_up_time DATETIME,
|
||||
notification_sent TINYINT(1) DEFAULT 0,
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_follow_up_time (follow_up_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS trial_periods (
|
||||
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||||
customer_name VARCHAR(255) NOT NULL,
|
||||
start_time DATETIME NOT NULL,
|
||||
end_time DATETIME NOT NULL,
|
||||
is_trial TINYINT(1) DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_end_time (end_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
if _, err := db.Exec(query); err != nil {
|
||||
return fmt.Errorf("执行SQL失败: %v\nSQL: %s", err, query)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseTime(timeStr string) (time.Time, error) {
|
||||
// 尝试多种时间格式
|
||||
formats := []string{
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
"2006-01-02T15:04:05.999999999Z07:00",
|
||||
"2006-01-02T15:04:05Z",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02",
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, timeStr); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("无法解析时间: %s", timeStr)
|
||||
}
|
||||
|
||||
func migrateCustomers(db *sql.DB, filepath string) error {
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取文件失败: %v", err)
|
||||
}
|
||||
|
||||
var customers []Customer
|
||||
if err := json.Unmarshal(data, &customers); err != nil {
|
||||
return fmt.Errorf("解析JSON失败: %v", err)
|
||||
}
|
||||
|
||||
stmt, err := db.Prepare(`
|
||||
INSERT INTO customers (id, created_at, customer_name, intended_product, version,
|
||||
description, solution, type, module, status_progress, reporter)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
customer_name = VALUES(customer_name),
|
||||
intended_product = VALUES(intended_product),
|
||||
version = VALUES(version),
|
||||
description = VALUES(description),
|
||||
solution = VALUES(solution),
|
||||
type = VALUES(type),
|
||||
module = VALUES(module),
|
||||
status_progress = VALUES(status_progress),
|
||||
reporter = VALUES(reporter)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备SQL语句失败: %v", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
successCount := 0
|
||||
for _, c := range customers {
|
||||
createdAt, _ := parseTime(c.CreatedAt)
|
||||
|
||||
_, err := stmt.Exec(
|
||||
c.ID, createdAt, c.CustomerName, c.IntendedProduct, c.Version,
|
||||
c.Description, c.Solution, c.Type, c.Module, c.StatusProgress, c.Reporter,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("插入客户记录失败 [%s]: %v", c.CustomerName, err)
|
||||
continue
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
log.Printf(" 已迁移 %d/%d 条客户记录", successCount, len(customers))
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateFollowups(db *sql.DB, filepath string) error {
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取文件失败: %v", err)
|
||||
}
|
||||
|
||||
var followups []Followup
|
||||
if err := json.Unmarshal(data, &followups); err != nil {
|
||||
return fmt.Errorf("解析JSON失败: %v", err)
|
||||
}
|
||||
|
||||
stmt, err := db.Prepare(`
|
||||
INSERT INTO followups (id, created_at, customer_name, deal_status, customer_level,
|
||||
industry, follow_up_time, notification_sent)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
customer_name = VALUES(customer_name),
|
||||
deal_status = VALUES(deal_status),
|
||||
customer_level = VALUES(customer_level),
|
||||
industry = VALUES(industry),
|
||||
follow_up_time = VALUES(follow_up_time),
|
||||
notification_sent = VALUES(notification_sent)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备SQL语句失败: %v", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
successCount := 0
|
||||
for _, f := range followups {
|
||||
createdAt, _ := parseTime(f.CreatedAt)
|
||||
followUpTime, _ := parseTime(f.FollowUpTime)
|
||||
|
||||
notificationSent := 0
|
||||
if f.NotificationSent {
|
||||
notificationSent = 1
|
||||
}
|
||||
|
||||
_, err := stmt.Exec(
|
||||
f.ID, createdAt, f.CustomerName, f.DealStatus, f.CustomerLevel,
|
||||
f.Industry, followUpTime, notificationSent,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("插入跟进记录失败 [%s]: %v", f.CustomerName, err)
|
||||
continue
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
log.Printf(" 已迁移 %d/%d 条跟进记录", successCount, len(followups))
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateTrialPeriods(db *sql.DB, filepath string) error {
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取文件失败: %v", err)
|
||||
}
|
||||
|
||||
var trials []TrialPeriod
|
||||
if err := json.Unmarshal(data, &trials); err != nil {
|
||||
return fmt.Errorf("解析JSON失败: %v", err)
|
||||
}
|
||||
|
||||
stmt, err := db.Prepare(`
|
||||
INSERT INTO trial_periods (id, customer_name, start_time, end_time, is_trial, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
customer_name = VALUES(customer_name),
|
||||
start_time = VALUES(start_time),
|
||||
end_time = VALUES(end_time),
|
||||
is_trial = VALUES(is_trial)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("准备SQL语句失败: %v", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
successCount := 0
|
||||
for _, t := range trials {
|
||||
startTime, _ := parseTime(t.StartTime)
|
||||
endTime, _ := parseTime(t.EndTime)
|
||||
createdAt, _ := parseTime(t.CreatedAt)
|
||||
|
||||
isTrial := 0
|
||||
if t.IsTrial {
|
||||
isTrial = 1
|
||||
}
|
||||
|
||||
_, err := stmt.Exec(
|
||||
t.ID, t.CustomerName, startTime, endTime, isTrial, createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("插入试用期记录失败 [%s]: %v", t.CustomerName, err)
|
||||
continue
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
log.Printf(" 已迁移 %d/%d 条试用期记录", successCount, len(trials))
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 设置日志格式
|
||||
log.SetFlags(log.Ltime)
|
||||
|
||||
// 打印使用说明
|
||||
if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
|
||||
fmt.Println(`CRM JSON 到 MySQL 数据迁移工具
|
||||
|
||||
用法:
|
||||
go run migrate_to_mysql.go [data目录路径]
|
||||
|
||||
环境变量:
|
||||
DB_HOST - 数据库主机 (默认: localhost)
|
||||
DB_USER - 数据库用户 (默认: root)
|
||||
DB_PASSWORD - 数据库密码 (默认: 空)
|
||||
DB_NAME - 数据库名称 (默认: crm_db)
|
||||
|
||||
示例:
|
||||
# 使用默认配置
|
||||
go run migrate_to_mysql.go ./data
|
||||
|
||||
# 使用环境变量配置
|
||||
DB_HOST=127.0.0.1 DB_USER=root DB_PASSWORD=123456 go run migrate_to_mysql.go ./data
|
||||
`)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user