diff --git a/data/migration.sql b/data/migration.sql new file mode 100644 index 0000000..3919f62 --- /dev/null +++ b/data/migration.sql @@ -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条 +-- - 已成交客户: 诺因智能 +-- ============================================================ diff --git a/frontend/css/style.css b/frontend/css/style.css index 7a06632..2d75d08 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -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; diff --git a/frontend/index.html b/frontend/index.html index 2004b64..10e4695 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ CRM客户管理系统 - + @@ -285,7 +285,7 @@ 客户名称 - 是否试用 + 状态 开始时间 结束时间 创建时间 @@ -993,9 +993,9 @@ - - - + + + \ No newline at end of file diff --git a/frontend/js/main.js b/frontend/js/main.js index c69160c..f51999b 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -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 = ` - `; @@ -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 () { - @@ -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); diff --git a/frontend/js/trial-periods-page.js b/frontend/js/trial-periods-page.js index 7dc6eb9..bd4da3d 100644 --- a/frontend/js/trial-periods-page.js +++ b/frontend/js/trial-periods-page.js @@ -335,7 +335,7 @@ function renderTrialPeriodsTable() { - @@ -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); diff --git a/internal/handlers/auth_handler.go b/internal/handlers/auth_handler.go index d6b7c33..7a5ed93 100644 --- a/internal/handlers/auth_handler.go +++ b/internal/handlers/auth_handler.go @@ -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小时过期 }) diff --git a/migrate_to_mysql.go b/migrate_to_mysql.go new file mode 100644 index 0000000..d8f8ce4 --- /dev/null +++ b/migrate_to_mysql.go @@ -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) + } +}