feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
0
backend/scripts/__init__.py
Normal file
0
backend/scripts/__init__.py
Normal file
215
backend/scripts/add_admin_exam_data.sql
Normal file
215
backend/scripts/add_admin_exam_data.sql
Normal file
@@ -0,0 +1,215 @@
|
||||
-- 为admin用户(id=2)添加考试和错题数据
|
||||
-- 方便直接使用admin账号测试成绩报告和错题本功能
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- ========================================
|
||||
-- 一、插入admin的考试记录(包含三轮得分)
|
||||
-- ========================================
|
||||
|
||||
-- 考试1:皮肤生理学基础(完成三轮,成绩优秀)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 1, '皮肤生理学基础 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-01 09:00:00', '2025-10-01 10:25:00', 60,
|
||||
85, 95, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试2:医美产品知识与应用(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 2, '医美产品知识与应用 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-03 14:00:00', '2025-10-03 15:30:00', 60,
|
||||
78, 90, 95, 95, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试3:美容仪器操作与维护(完成两轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 3, '美容仪器操作与维护 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-05 10:00:00', '2025-10-05 11:10:00', 60,
|
||||
92, 100, NULL, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试4:医美项目介绍与咨询(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 4, '医美项目介绍与咨询 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-06 15:00:00', '2025-10-06 16:20:00', 60,
|
||||
72, 85, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试5:轻医美销售技巧(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 5, '轻医美销售技巧 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-07 09:30:00', '2025-10-07 11:00:00', 60,
|
||||
88, 95, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试6:客户服务与投诉处理(完成两轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 6, '客户服务与投诉处理 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-08 14:00:00', '2025-10-08 15:15:00', 60,
|
||||
95, 100, NULL, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试7:社媒营销与私域运营(完成一轮,不及格)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 7, '社媒营销与私域运营 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-09 10:00:00', '2025-10-09 10:48:00', 60,
|
||||
58, NULL, NULL, 58, FALSE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试8:卫生消毒与感染控制(完成三轮,最近的考试)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 9, '卫生消毒与感染控制 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-11 09:00:00', '2025-10-11 10:18:00', 60,
|
||||
90, 95, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试9:美容心理学(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
2, 10, '美容心理学 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-12 08:30:00', '2025-10-12 09:55:00', 60,
|
||||
82, 90, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- ========================================
|
||||
-- 二、插入admin的错题记录
|
||||
-- ========================================
|
||||
|
||||
-- 获取刚插入的考试ID
|
||||
SET @admin_exam1 = (SELECT id FROM exams WHERE user_id=2 AND course_id=1 ORDER BY id DESC LIMIT 1);
|
||||
SET @admin_exam2 = (SELECT id FROM exams WHERE user_id=2 AND course_id=2 ORDER BY id DESC LIMIT 1);
|
||||
SET @admin_exam4 = (SELECT id FROM exams WHERE user_id=2 AND course_id=4 ORDER BY id DESC LIMIT 1);
|
||||
SET @admin_exam5 = (SELECT id FROM exams WHERE user_id=2 AND course_id=5 ORDER BY id DESC LIMIT 1);
|
||||
SET @admin_exam7 = (SELECT id FROM exams WHERE user_id=2 AND course_id=7 ORDER BY id DESC LIMIT 1);
|
||||
SET @admin_exam9 = (SELECT id FROM exams WHERE user_id=2 AND course_id=9 ORDER BY id DESC LIMIT 1);
|
||||
|
||||
-- 皮肤生理学基础 - 第一轮2道错题
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(2, @admin_exam1, '表皮层最外层的细胞是?\nA. 基底细胞\nB. 角质细胞\nC. 黑色素细胞\nD. 朗格汉斯细胞', 'B', 'A', 'single', '2025-10-01 09:15:00'),
|
||||
(2, @admin_exam1, '真皮层的主要成分包括哪些?(多选)\nA. 胶原蛋白\nB. 弹性蛋白\nC. 透明质酸\nD. 角质蛋白', 'A,B,C', 'A,B', 'multiple', '2025-10-01 09:28:00');
|
||||
|
||||
-- 医美产品知识与应用 - 第一轮3道错题
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(2, @admin_exam2, '肉毒素的作用机制是?', '阻断神经肌肉接头处的信号传递,使肌肉松弛,从而减少动态皱纹', '收缩肌肉', 'essay', '2025-10-03 14:18:00'),
|
||||
(2, @admin_exam2, '光子嫩肤术后___天内避免高温环境', '7', '3', 'blank', '2025-10-03 14:35:00'),
|
||||
(2, @admin_exam2, '所有类型的色斑都可以用激光去除', '错误', '正确', 'judge', '2025-10-03 14:42:00');
|
||||
|
||||
-- 医美项目介绍与咨询 - 第一轮3道错题
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(2, @admin_exam4, '面部埋线提升的维持时间通常是?\nA. 3-6个月\nB. 6-12个月\nC. 12-24个月\nD. 永久', 'C', 'B', 'single', '2025-10-06 15:20:00'),
|
||||
(2, @admin_exam4, '水光针注射的最佳频率是___,共___次为一个疗程', '每3-4周一次,3-5次', '每周一次,10次', 'blank', '2025-10-06 15:38:00'),
|
||||
(2, @admin_exam4, '请说明如何向客户介绍热玛吉项目的原理和效果', '原理:利用射频能量深入真皮层和筋膜层,刺激胶原蛋白重组再生。效果:紧致提升、改善皱纹、轮廓重塑,效果可持续1-2年。适合25岁以上肌肤松弛人群', '射频加热皮肤,可以紧致', 'essay', '2025-10-06 15:55:00');
|
||||
|
||||
-- 轻医美销售技巧 - 第一轮2道错题
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(2, @admin_exam5, '有效挖掘客户需求的提问技巧包括?(多选)\nA. 开放式提问\nB. 封闭式确认\nC. 诱导式提问\nD. 深入式追问', 'A,B,D', 'A,B', 'multiple', '2025-10-07 09:52:00'),
|
||||
(2, @admin_exam5, '客户说"太贵了"时,最佳应对策略是先___客户真正的顾虑', '了解', '降价', 'blank', '2025-10-07 10:15:00');
|
||||
|
||||
-- 社媒营销与私域运营 - 第一轮5道错题(不及格)
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(2, @admin_exam7, '私域流量的核心价值在于?\nA. 降低获客成本\nB. 提高复购率\nC. 建立品牌忠诚度\nD. 以上都是', 'D', 'B', 'single', '2025-10-09 10:10:00'),
|
||||
(2, @admin_exam7, '有效的社群运营需要具备哪些要素?(多选)\nA. 明确定位\nB. 持续输出\nC. 互动反馈\nD. 硬性推销', 'A,B,C', 'A,B', 'multiple', '2025-10-09 10:18:00'),
|
||||
(2, @admin_exam7, '朋友圈营销应该每天发布10条以上内容', '错误', '正确', 'judge', '2025-10-09 10:25:00'),
|
||||
(2, @admin_exam7, '社群活跃度下降时,应采取的措施包括___和___', '话题引导,福利刺激', '不管它,等着看', 'blank', '2025-10-09 10:32:00'),
|
||||
(2, @admin_exam7, '请设计一个针对医美客户的短视频内容策略', '策略要点:1.专业科普(解答常见疑问)2.案例展示(真实效果对比)3.专家访谈(增强信任)4.互动活动(提升参与)5.客户见证(口碑传播)。发布频率:每周3-5条,时间选择晚上7-9点黄金时段', '每天发产品广告', 'essay', '2025-10-09 10:40:00');
|
||||
|
||||
-- 卫生消毒与感染控制 - 第一轮1道错题
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(2, @admin_exam9, '医疗器械的消毒等级分为哪几类?(多选)\nA. 高水平消毒\nB. 中水平消毒\nC. 低水平消毒\nD. 无菌处理', 'A,B,C,D', 'A,B,C', 'multiple', '2025-10-11 09:25:00');
|
||||
|
||||
-- ========================================
|
||||
-- 三、验证结果
|
||||
-- ========================================
|
||||
|
||||
-- admin的考试统计
|
||||
SELECT
|
||||
'admin考试统计' as info,
|
||||
COUNT(*) as total_exams,
|
||||
COUNT(round1_score) as has_round1,
|
||||
COUNT(round2_score) as has_round2,
|
||||
COUNT(round3_score) as has_round3,
|
||||
ROUND(AVG(round1_score), 1) as avg_round1,
|
||||
ROUND(AVG(round2_score), 1) as avg_round2,
|
||||
ROUND(AVG(round3_score), 1) as avg_round3
|
||||
FROM exams
|
||||
WHERE user_id = 2 AND round1_score IS NOT NULL;
|
||||
|
||||
-- admin的错题统计
|
||||
SELECT
|
||||
'admin错题统计' as info,
|
||||
COUNT(*) as total_mistakes,
|
||||
COUNT(DISTINCT exam_id) as distinct_exams,
|
||||
COUNT(DISTINCT question_type) as distinct_types
|
||||
FROM exam_mistakes
|
||||
WHERE user_id = 2;
|
||||
|
||||
-- 按题型统计admin的错题
|
||||
SELECT
|
||||
question_type,
|
||||
COUNT(*) as count
|
||||
FROM exam_mistakes
|
||||
WHERE user_id = 2 AND question_type IS NOT NULL
|
||||
GROUP BY question_type
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- 显示admin最近5条考试记录
|
||||
SELECT
|
||||
id,
|
||||
exam_name,
|
||||
round1_score as 第一轮,
|
||||
round2_score as 第二轮,
|
||||
round3_score as 第三轮,
|
||||
is_passed as 是否通过,
|
||||
DATE_FORMAT(start_time, '%m-%d %H:%i') as 考试时间
|
||||
FROM exams
|
||||
WHERE user_id = 2 AND round1_score IS NOT NULL
|
||||
ORDER BY start_time DESC
|
||||
LIMIT 10;
|
||||
|
||||
164
backend/scripts/add_admin_learning_data.sql
Normal file
164
backend/scripts/add_admin_learning_data.sql
Normal file
@@ -0,0 +1,164 @@
|
||||
-- ============================================
|
||||
-- 为 superadmin 和 admin 添加学习记录
|
||||
-- ============================================
|
||||
|
||||
USE `kaopeilian`;
|
||||
|
||||
-- 设置用户ID
|
||||
SET @superadmin_id = 4;
|
||||
SET @admin_id = 2;
|
||||
|
||||
-- 获取训练场景ID
|
||||
SET @scene1_id = (SELECT id FROM training_scenes WHERE name = 'Python编程助手' LIMIT 1);
|
||||
SET @scene2_id = (SELECT id FROM training_scenes WHERE name = '面试模拟' LIMIT 1);
|
||||
SET @scene3_id = (SELECT id FROM training_scenes WHERE name = '项目讨论' LIMIT 1);
|
||||
|
||||
-- 获取课程ID
|
||||
SET @course_id = 4;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 为 superadmin 添加训练会话记录(高级管理员,学习记录较多)
|
||||
-- ============================================
|
||||
INSERT INTO training_sessions (
|
||||
user_id,
|
||||
scene_id,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_seconds,
|
||||
status,
|
||||
total_score,
|
||||
evaluation_result,
|
||||
created_by
|
||||
) VALUES
|
||||
-- 30天的丰富学习记录
|
||||
(@superadmin_id, @scene1_id, '2025-08-23 09:00:00', '2025-08-23 10:30:00', 5400, 'COMPLETED', 95, '{"feedback": "架构设计优秀"}', @superadmin_id),
|
||||
(@superadmin_id, @scene3_id, '2025-08-25 14:00:00', '2025-08-25 16:00:00', 7200, 'COMPLETED', 98, '{"feedback": "系统设计能力出色"}', @superadmin_id),
|
||||
(@superadmin_id, @scene2_id, '2025-08-28 10:00:00', '2025-08-28 11:30:00', 5400, 'COMPLETED', 96, '{"feedback": "技术面试官水平"}', @superadmin_id),
|
||||
(@superadmin_id, @scene1_id, '2025-09-01 09:00:00', '2025-09-01 10:00:00', 3600, 'COMPLETED', 94, '{"feedback": "代码审查能力强"}', @superadmin_id),
|
||||
(@superadmin_id, @scene3_id, '2025-09-03 13:30:00', '2025-09-03 15:30:00', 7200, 'COMPLETED', 97, '{"feedback": "技术决策合理"}', @superadmin_id),
|
||||
(@superadmin_id, @scene1_id, '2025-09-05 10:00:00', '2025-09-05 11:00:00', 3600, 'COMPLETED', 95, '{"feedback": "性能优化出色"}', @superadmin_id),
|
||||
(@superadmin_id, @scene2_id, '2025-09-08 14:00:00', '2025-09-08 15:30:00', 5400, 'COMPLETED', 93, '{"feedback": "面试策略成熟"}', @superadmin_id),
|
||||
(@superadmin_id, @scene3_id, '2025-09-10 09:00:00', '2025-09-10 11:00:00', 7200, 'COMPLETED', 96, '{"feedback": "项目管理经验丰富"}', @superadmin_id),
|
||||
(@superadmin_id, @scene1_id, '2025-09-12 10:00:00', '2025-09-12 11:30:00', 5400, 'COMPLETED', 98, '{"feedback": "技术深度令人印象深刻"}', @superadmin_id),
|
||||
(@superadmin_id, @scene2_id, '2025-09-14 14:00:00', '2025-09-14 15:00:00', 3600, 'COMPLETED', 95, '{"feedback": "团队管理能力优秀"}', @superadmin_id),
|
||||
(@superadmin_id, @scene3_id, '2025-09-16 09:30:00', '2025-09-16 11:30:00', 7200, 'COMPLETED', 97, '{"feedback": "架构视野开阔"}', @superadmin_id),
|
||||
(@superadmin_id, @scene1_id, '2025-09-18 10:00:00', '2025-09-18 11:00:00', 3600, 'COMPLETED', 96, '{"feedback": "最佳实践掌握透彻"}', @superadmin_id),
|
||||
(@superadmin_id, @scene2_id, '2025-09-20 14:00:00', '2025-09-20 15:30:00', 5400, 'COMPLETED', 94, '{"feedback": "人才评估准确"}', @superadmin_id),
|
||||
(@superadmin_id, @scene3_id, '2025-09-21 09:00:00', '2025-09-21 11:00:00', 7200, 'COMPLETED', 98, '{"feedback": "技术方案完善"}', @superadmin_id),
|
||||
(@superadmin_id, @scene1_id, '2025-09-22 10:00:00', '2025-09-22 11:30:00', 5400, 'COMPLETED', 97, '{"feedback": "持续学习精神可嘉"}', @superadmin_id);
|
||||
|
||||
-- ============================================
|
||||
-- 2. 为 admin 添加训练会话记录(普通管理员,学习记录适中)
|
||||
-- ============================================
|
||||
INSERT INTO training_sessions (
|
||||
user_id,
|
||||
scene_id,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_seconds,
|
||||
status,
|
||||
total_score,
|
||||
evaluation_result,
|
||||
created_by
|
||||
) VALUES
|
||||
-- 20天的学习记录
|
||||
(@admin_id, @scene1_id, '2025-09-03 09:00:00', '2025-09-03 10:00:00', 3600, 'COMPLETED', 88, '{"feedback": "基础扎实"}', @admin_id),
|
||||
(@admin_id, @scene2_id, '2025-09-05 14:00:00', '2025-09-05 15:00:00', 3600, 'COMPLETED', 85, '{"feedback": "面试技巧良好"}', @admin_id),
|
||||
(@admin_id, @scene3_id, '2025-09-08 10:00:00', '2025-09-08 11:30:00', 5400, 'COMPLETED', 90, '{"feedback": "项目理解到位"}', @admin_id),
|
||||
(@admin_id, @scene1_id, '2025-09-10 09:30:00', '2025-09-10 10:30:00', 3600, 'COMPLETED', 87, '{"feedback": "进步明显"}', @admin_id),
|
||||
(@admin_id, @scene2_id, '2025-09-12 14:00:00', '2025-09-12 15:00:00', 3600, 'COMPLETED', 89, '{"feedback": "沟通能力提升"}', @admin_id),
|
||||
(@admin_id, @scene3_id, '2025-09-15 10:00:00', '2025-09-15 11:30:00', 5400, 'COMPLETED', 91, '{"feedback": "方案设计合理"}', @admin_id),
|
||||
(@admin_id, @scene1_id, '2025-09-17 09:00:00', '2025-09-17 10:00:00', 3600, 'COMPLETED', 86, '{"feedback": "代码质量不错"}', @admin_id),
|
||||
(@admin_id, @scene2_id, '2025-09-19 14:00:00', '2025-09-19 15:00:00', 3600, 'COMPLETED', 88, '{"feedback": "表达清晰"}', @admin_id),
|
||||
(@admin_id, @scene3_id, '2025-09-21 10:00:00', '2025-09-21 11:30:00', 5400, 'COMPLETED', 92, '{"feedback": "项目管理有进步"}', @admin_id),
|
||||
(@admin_id, @scene1_id, '2025-09-22 09:00:00', '2025-09-22 10:00:00', 3600, 'COMPLETED', 90, '{"feedback": "技术理解深入"}', @admin_id);
|
||||
|
||||
-- ============================================
|
||||
-- 3. 为管理员添加考试记录
|
||||
-- ============================================
|
||||
|
||||
-- superadmin 的考试记录(成绩优秀)
|
||||
INSERT INTO exams (
|
||||
user_id,
|
||||
course_id,
|
||||
exam_name,
|
||||
question_count,
|
||||
total_score,
|
||||
pass_score,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_minutes,
|
||||
score,
|
||||
is_passed,
|
||||
status,
|
||||
questions,
|
||||
answers
|
||||
) VALUES
|
||||
-- superadmin 的考试(高分)
|
||||
(@superadmin_id, @course_id, 'Python高级特性测试', 40, 100, 80,
|
||||
'2025-09-01 09:00:00', '2025-09-01 10:00:00', 60, 98, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40]',
|
||||
'{}'),
|
||||
|
||||
(@superadmin_id, @course_id, 'Python架构设计测试', 50, 100, 85,
|
||||
'2025-09-10 10:00:00', '2025-09-10 11:30:00', 90, 96, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50]',
|
||||
'{}'),
|
||||
|
||||
(@superadmin_id, @course_id, 'Python性能优化测试', 35, 100, 80,
|
||||
'2025-09-20 14:00:00', '2025-09-20 15:15:00', 75, 95, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35]',
|
||||
'{}'),
|
||||
|
||||
-- admin 的考试记录(成绩良好)
|
||||
(@admin_id, @course_id, 'Python基础测试', 30, 100, 60,
|
||||
'2025-09-05 14:00:00', '2025-09-05 15:00:00', 60, 85, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]',
|
||||
'{}'),
|
||||
|
||||
(@admin_id, @course_id, 'Python进阶测试', 35, 100, 70,
|
||||
'2025-09-15 10:00:00', '2025-09-15 11:15:00', 75, 88, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35]',
|
||||
'{}');
|
||||
|
||||
-- ============================================
|
||||
-- 查询验证
|
||||
-- ============================================
|
||||
|
||||
-- superadmin 的统计
|
||||
SELECT
|
||||
'superadmin训练统计' as type,
|
||||
COUNT(DISTINCT DATE(start_time)) as learning_days,
|
||||
ROUND(SUM(duration_seconds) / 3600, 1) as total_hours,
|
||||
COUNT(*) as session_count,
|
||||
ROUND(AVG(total_score), 1) as avg_score
|
||||
FROM training_sessions
|
||||
WHERE user_id = @superadmin_id;
|
||||
|
||||
SELECT
|
||||
'superadmin考试统计' as type,
|
||||
COUNT(*) as exam_count,
|
||||
SUM(question_count) as total_questions,
|
||||
ROUND(AVG(score), 1) as avg_score
|
||||
FROM exams
|
||||
WHERE user_id = @superadmin_id AND status = 'completed';
|
||||
|
||||
-- admin 的统计
|
||||
SELECT
|
||||
'admin训练统计' as type,
|
||||
COUNT(DISTINCT DATE(start_time)) as learning_days,
|
||||
ROUND(SUM(duration_seconds) / 3600, 1) as total_hours,
|
||||
COUNT(*) as session_count,
|
||||
ROUND(AVG(total_score), 1) as avg_score
|
||||
FROM training_sessions
|
||||
WHERE user_id = @admin_id;
|
||||
|
||||
SELECT
|
||||
'admin考试统计' as type,
|
||||
COUNT(*) as exam_count,
|
||||
SUM(question_count) as total_questions,
|
||||
ROUND(AVG(score), 1) as avg_score
|
||||
FROM exams
|
||||
WHERE user_id = @admin_id AND status = 'completed';
|
||||
|
||||
SELECT '管理员学习数据添加完成!' as message;
|
||||
290
backend/scripts/add_exam_and_mistakes_demo_data.sql
Normal file
290
backend/scripts/add_exam_and_mistakes_demo_data.sql
Normal file
@@ -0,0 +1,290 @@
|
||||
-- 模拟数据脚本:考试成绩和错题记录
|
||||
-- 创建时间:2025-10-12
|
||||
-- 说明:为成绩报告和错题本页面添加演示数据
|
||||
-- 场景:轻医美连锁品牌员工培训考试
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- ========================================
|
||||
-- 一、插入考试记录(包含三轮得分)
|
||||
-- ========================================
|
||||
|
||||
-- 用户5(consultant_001)的考试记录
|
||||
-- 考试1:皮肤生理学基础(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 1, '皮肤生理学基础 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-05 09:00:00', '2025-10-05 10:30:00', 60,
|
||||
70, 85, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试2:医美产品知识与应用(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 2, '医美产品知识与应用 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-06 14:00:00', '2025-10-06 15:20:00', 60,
|
||||
65, 80, 90, 90, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试3:美容仪器操作与维护(完成两轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 3, '美容仪器操作与维护 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-07 10:00:00', '2025-10-07 11:15:00', 60,
|
||||
80, 95, NULL, 95, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试4:医美项目介绍与咨询(完成一轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 4, '医美项目介绍与咨询 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-08 15:00:00', '2025-10-08 15:45:00', 60,
|
||||
55, NULL, NULL, 55, FALSE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试5:轻医美销售技巧(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 5, '轻医美销售技巧 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-09 09:30:00', '2025-10-09 11:00:00', 60,
|
||||
75, 90, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试6:客户服务与投诉处理(完成三轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 6, '客户服务与投诉处理 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-10 14:00:00', '2025-10-10 15:30:00', 60,
|
||||
85, 95, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试7:社媒营销与私域运营(完成两轮)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 7, '社媒营销与私域运营 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-11 10:00:00', '2025-10-11 11:10:00', 60,
|
||||
60, 75, NULL, 75, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- 考试8:卫生消毒与感染控制(完成三轮,最近的考试)
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES (
|
||||
5, 9, '卫生消毒与感染控制 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-12 09:00:00', '2025-10-12 10:20:00', 60,
|
||||
90, 100, 100, 100, TRUE, 'submitted'
|
||||
);
|
||||
|
||||
-- ========================================
|
||||
-- 二、插入错题记录
|
||||
-- ========================================
|
||||
|
||||
-- 获取刚插入的考试ID(使用最后8条记录)
|
||||
SET @exam1_id = (SELECT id FROM exams WHERE user_id=5 AND course_id=1 ORDER BY id DESC LIMIT 1);
|
||||
SET @exam2_id = (SELECT id FROM exams WHERE user_id=5 AND course_id=2 ORDER BY id DESC LIMIT 1);
|
||||
SET @exam4_id = (SELECT id FROM exams WHERE user_id=5 AND course_id=4 ORDER BY id DESC LIMIT 1);
|
||||
SET @exam5_id = (SELECT id FROM exams WHERE user_id=5 AND course_id=5 ORDER BY id DESC LIMIT 1);
|
||||
SET @exam7_id = (SELECT id FROM exams WHERE user_id=5 AND course_id=7 ORDER BY id DESC LIMIT 1);
|
||||
|
||||
-- 考试1的错题(皮肤生理学基础 - 第一轮3道错题)
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(5, @exam1_id, '人体皮肤分为哪几层?', '表皮层、真皮层、皮下组织三层', '表皮和真皮两层', 'essay', '2025-10-05 09:15:00'),
|
||||
(5, @exam1_id, '关于玻尿酸的作用,以下哪项描述是正确的?\nA. 只能用于填充\nB. 具有保湿和填充双重作用\nC. 不能用于面部\nD. 只适合年轻人使用', 'B', 'A', 'single', '2025-10-05 09:25:00'),
|
||||
(5, @exam1_id, '敏感肌肤的客户可以使用含酒精的护肤品', '错误', '正确', 'judge', '2025-10-05 09:35:00');
|
||||
|
||||
-- 考试2的错题(医美产品知识与应用 - 第一轮4道错题)
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(5, @exam2_id, '光子嫩肤的主要功效包括哪些?(多选)\nA. 美白淡斑\nB. 收缩毛孔\nC. 去除皱纹\nD. 改善红血丝', 'A,B,D', 'A,B,C', 'multiple', '2025-10-06 14:10:00'),
|
||||
(5, @exam2_id, '水光针的主要成分是___', '透明质酸(玻尿酸)', '胶原蛋白', 'blank', '2025-10-06 14:20:00'),
|
||||
(5, @exam2_id, '热玛吉适用于所有肤质', '正确', '错误', 'judge', '2025-10-06 14:30:00'),
|
||||
(5, @exam2_id, '超声刀的作用原理是什么?', '通过高强度聚焦超声波能量作用于筋膜层,促进胶原蛋白再生,达到紧致提拉效果', '利用超声波震动按摩皮肤', 'essay', '2025-10-06 14:40:00');
|
||||
|
||||
-- 考试4的错题(医美项目介绍与咨询 - 第一轮5道错题,未通过)
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(5, @exam4_id, '面部提升项目适合的年龄段是?\nA. 25岁以下\nB. 25-35岁\nC. 35岁以上\nD. 所有年龄段', 'C', 'B', 'single', '2025-10-08 15:10:00'),
|
||||
(5, @exam4_id, '皮秒激光可以治疗哪些皮肤问题?(多选)\nA. 色斑\nB. 痘印\nC. 毛孔粗大\nD. 皮肤松弛', 'A,B,C', 'A,B', 'multiple', '2025-10-08 15:15:00'),
|
||||
(5, @exam4_id, '果酸焕肤后需要严格防晒', '正确', '错误', 'judge', '2025-10-08 15:20:00'),
|
||||
(5, @exam4_id, '光子嫩肤一个疗程通常需要___次治疗,间隔___周', '3-5次,3-4周', '5-8次,2周', 'blank', '2025-10-08 15:25:00'),
|
||||
(5, @exam4_id, '请简述如何向客户介绍肉毒素除皱项目的优势和注意事项', '优势:快速见效、微创无痕、可逆性强、针对性强。注意事项:需选择正规品牌、术后避免按摩、孕妇和哺乳期禁用、过敏体质需提前告知', '肉毒素可以除皱,效果很好,没有副作用', 'essay', '2025-10-08 15:30:00');
|
||||
|
||||
-- 考试5的错题(轻医美销售技巧 - 第一轮3道错题)
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(5, @exam5_id, '在销售咨询中,FABE销售法则中的F代表?\nA. Features(特征)\nB. Functions(功能)\nC. Facts(事实)\nD. Feelings(感受)', 'A', 'C', 'single', '2025-10-09 09:45:00'),
|
||||
(5, @exam5_id, '有效的销售话术应该具备哪些特点?(多选)\nA. 专业准确\nB. 简单易懂\nC. 夸大效果\nD. 针对性强', 'A,B,D', 'A,B', 'multiple', '2025-10-09 09:55:00'),
|
||||
(5, @exam5_id, '客户提出价格异议时,第一步应该是___客户的关注点', '倾听和理解', '立即降价', 'blank', '2025-10-09 10:05:00');
|
||||
|
||||
-- 考试7的错题(社媒营销与私域运营 - 第一轮4道错题)
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(5, @exam7_id, '私域流量运营的核心是?\nA. 大量加粉\nB. 建立信任关系\nC. 频繁推销\nD. 打折促销', 'B', 'A', 'single', '2025-10-11 10:15:00'),
|
||||
(5, @exam7_id, '短视频内容策划应遵循哪些原则?(多选)\nA. 垂直专业\nB. 持续更新\nC. 互动性强\nD. 纯广告推广', 'A,B,C', 'A,B', 'multiple', '2025-10-11 10:25:00'),
|
||||
(5, @exam7_id, '企业微信的客户标签管理可以提升运营效率', '正确', '错误', 'judge', '2025-10-11 10:35:00'),
|
||||
(5, @exam7_id, '请简述如何设计一个有效的会员转化路径', '步骤:1.引流获客(短视频/直播)2.建立信任(专业内容分享)3.激活需求(案例展示/体验活动)4.促成转化(限时优惠/专属福利)5.持续运营(定期回访/会员权益)', '做活动、发优惠券', 'essay', '2025-10-11 10:45:00');
|
||||
|
||||
-- ========================================
|
||||
-- 三、插入更多用户的考试数据(丰富数据)
|
||||
-- ========================================
|
||||
|
||||
-- 用户6(其他美容顾问)的考试记录
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES
|
||||
(6, 1, '皮肤生理学基础 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-03 10:00:00', '2025-10-03 11:20:00', 60,
|
||||
88, 95, 100, 100, TRUE, 'submitted'),
|
||||
(6, 2, '医美产品知识与应用 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-04 14:30:00', '2025-10-04 15:40:00', 60,
|
||||
92, 100, NULL, 100, TRUE, 'submitted'),
|
||||
(6, 6, '客户服务与投诉处理 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-07 09:00:00', '2025-10-07 10:15:00', 60,
|
||||
78, 90, 95, 95, TRUE, 'submitted');
|
||||
|
||||
-- 用户7的考试记录
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes,
|
||||
round1_score, round2_score, round3_score, score, is_passed, status
|
||||
) VALUES
|
||||
(7, 3, '美容仪器操作与维护 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-05 11:00:00', '2025-10-05 12:10:00', 60,
|
||||
82, 90, 100, 100, TRUE, 'submitted'),
|
||||
(7, 5, '轻医美销售技巧 - 动态考试', 10, 100.0, 60.0,
|
||||
'2025-10-09 15:00:00', '2025-10-09 16:15:00', 60,
|
||||
70, 85, 90, 90, TRUE, 'submitted');
|
||||
|
||||
-- ========================================
|
||||
-- 四、为新增考试添加对应的错题记录
|
||||
-- ========================================
|
||||
|
||||
-- 用户6的错题(皮肤生理学基础 - 第一轮1道错题)
|
||||
SET @exam6_1 = (SELECT id FROM exams WHERE user_id=6 AND course_id=1 ORDER BY id DESC LIMIT 1);
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(6, @exam6_1, '皮肤的PH值正常范围是?\nA. 3.5-4.5\nB. 4.5-6.5\nC. 6.5-7.5\nD. 7.5-8.5', 'B', 'C', 'single', '2025-10-03 10:20:00');
|
||||
|
||||
-- 用户6的错题(医美产品知识与应用 - 第一轮1道错题)
|
||||
SET @exam6_2 = (SELECT id FROM exams WHERE user_id=6 AND course_id=2 ORDER BY id DESC LIMIT 1);
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(6, @exam6_2, '玻尿酸注射后___小时内不能沾水', '24', '12', 'blank', '2025-10-04 14:50:00');
|
||||
|
||||
-- 用户6的错题(客户服务与投诉处理 - 第一轮2道错题)
|
||||
SET @exam6_3 = (SELECT id FROM exams WHERE user_id=6 AND course_id=6 ORDER BY id DESC LIMIT 1);
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(6, @exam6_3, '处理客户投诉的黄金原则是?(多选)\nA. 及时回应\nB. 真诚道歉\nC. 快速解决\nD. 推卸责任', 'A,B,C', 'A,B', 'multiple', '2025-10-07 09:20:00'),
|
||||
(6, @exam6_3, '客户投诉时,应该先___,再___', '倾听,解决', '解释,辩解', 'blank', '2025-10-07 09:30:00');
|
||||
|
||||
-- 用户7的错题(美容仪器操作与维护 - 第一轮2道错题)
|
||||
SET @exam7_1 = (SELECT id FROM exams WHERE user_id=7 AND course_id=3 ORDER BY id DESC LIMIT 1);
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(7, @exam7_1, '射频仪器的工作原理是?\nA. 激光热效应\nB. 电磁波热效应\nC. 超声波振动\nD. 机械摩擦', 'B', 'A', 'single', '2025-10-05 11:20:00'),
|
||||
(7, @exam7_1, '仪器消毒应该在每次使用___进行', '前后', '前', 'blank', '2025-10-05 11:35:00');
|
||||
|
||||
-- 用户7的错题(轻医美销售技巧 - 第一轮3道错题)
|
||||
SET @exam7_2 = (SELECT id FROM exams WHERE user_id=7 AND course_id=5 ORDER BY id DESC LIMIT 1);
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_content, correct_answer, user_answer, question_type, created_at
|
||||
) VALUES
|
||||
(7, @exam7_2, '成交话术中,假设成交法的核心是?\nA. 直接要求客户付款\nB. 假设客户已经同意购买\nC. 给客户压力\nD. 降价促销', 'B', 'A', 'single', '2025-10-09 15:25:00'),
|
||||
(7, @exam7_2, '顾客异议处理时应避免的做法包括?(多选)\nA. 打断顾客说话\nB. 否认顾客观点\nC. 耐心倾听\nD. 与顾客争辩', 'A,B,D', 'A,B', 'multiple', '2025-10-09 15:35:00'),
|
||||
(7, @exam7_2, '请列举3种常用的促成交易的方法', '1.假设成交法 2.二选一法 3.优惠刺激法 4.紧迫感营造法(任选3种)', '降价、送礼品', 'essay', '2025-10-09 15:45:00');
|
||||
|
||||
-- ========================================
|
||||
-- 五、验证插入结果
|
||||
-- ========================================
|
||||
|
||||
-- 统计考试记录
|
||||
SELECT
|
||||
'考试记录统计' as category,
|
||||
COUNT(*) as total,
|
||||
COUNT(round1_score) as has_round1,
|
||||
COUNT(round2_score) as has_round2,
|
||||
COUNT(round3_score) as has_round3,
|
||||
AVG(round1_score) as avg_round1_score
|
||||
FROM exams
|
||||
WHERE user_id IN (5, 6, 7);
|
||||
|
||||
-- 统计错题记录
|
||||
SELECT
|
||||
'错题记录统计' as category,
|
||||
COUNT(*) as total,
|
||||
COUNT(DISTINCT exam_id) as distinct_exams,
|
||||
COUNT(DISTINCT question_type) as distinct_types
|
||||
FROM exam_mistakes
|
||||
WHERE user_id IN (5, 6, 7);
|
||||
|
||||
-- 按课程统计错题
|
||||
SELECT
|
||||
c.name as course_name,
|
||||
COUNT(em.id) as mistake_count
|
||||
FROM exam_mistakes em
|
||||
JOIN exams e ON em.exam_id = e.id
|
||||
JOIN courses c ON e.course_id = c.id
|
||||
WHERE em.user_id IN (5, 6, 7)
|
||||
GROUP BY c.id, c.name
|
||||
ORDER BY mistake_count DESC;
|
||||
|
||||
-- 按题型统计错题
|
||||
SELECT
|
||||
question_type,
|
||||
COUNT(*) as count
|
||||
FROM exam_mistakes
|
||||
WHERE user_id IN (5, 6, 7) AND question_type IS NOT NULL
|
||||
GROUP BY question_type
|
||||
ORDER BY count DESC;
|
||||
|
||||
-- 显示最近5条考试记录(包含三轮得分)
|
||||
SELECT
|
||||
id,
|
||||
exam_name,
|
||||
round1_score,
|
||||
round2_score,
|
||||
round3_score,
|
||||
score,
|
||||
is_passed,
|
||||
DATE_FORMAT(start_time, '%Y-%m-%d %H:%i') as start_time
|
||||
FROM exams
|
||||
WHERE user_id = 5
|
||||
ORDER BY start_time DESC
|
||||
LIMIT 5;
|
||||
|
||||
78
backend/scripts/add_exam_data.sql
Normal file
78
backend/scripts/add_exam_data.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- ============================================
|
||||
-- 为 testuser 添加考试记录
|
||||
-- ============================================
|
||||
|
||||
USE `kaopeilian`;
|
||||
|
||||
-- 设置 testuser 的 ID
|
||||
SET @test_user_id = 1;
|
||||
|
||||
-- 获取第一个课程ID(Python基础课程)
|
||||
SET @course_id = 4;
|
||||
|
||||
-- 添加考试记录(exams 表是用户的考试实例)
|
||||
INSERT INTO exams (
|
||||
user_id,
|
||||
course_id,
|
||||
exam_name,
|
||||
question_count,
|
||||
total_score,
|
||||
pass_score,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_minutes,
|
||||
score,
|
||||
is_passed,
|
||||
status,
|
||||
questions,
|
||||
answers
|
||||
) VALUES
|
||||
-- 第一次考试(15天前)
|
||||
(@test_user_id, @course_id, 'Python基础测试-第1次', 20, 100, 60,
|
||||
'2025-09-07 10:00:00', '2025-09-07 10:50:00', 50, 72, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]',
|
||||
'{"1":"A","2":"B","3":"A","4":"C","5":"A","6":"B","7":"A","8":"D","9":"A","10":"B","11":"A","12":"C","13":"A","14":"B","15":"A","16":"C","17":"A","18":"B","19":"A","20":"D"}'),
|
||||
|
||||
-- 第二次考试(10天前)
|
||||
(@test_user_id, @course_id, 'Python基础测试-第2次', 20, 100, 60,
|
||||
'2025-09-12 14:00:00', '2025-09-12 14:45:00', 45, 85, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]',
|
||||
'{"1":"A","2":"A","3":"A","4":"A","5":"A","6":"B","7":"A","8":"A","9":"A","10":"B","11":"A","12":"A","13":"A","14":"B","15":"A","16":"A","17":"A","18":"B","19":"A","20":"A"}'),
|
||||
|
||||
-- 第三次考试(5天前)
|
||||
(@test_user_id, @course_id, 'Python进阶测试', 25, 100, 70,
|
||||
'2025-09-17 09:00:00', '2025-09-17 10:15:00', 75, 78, 1, 'completed',
|
||||
'[21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45]',
|
||||
'{"21":"A","22":"B","23":"A","24":"C","25":"A","26":"B","27":"A","28":"D","29":"A","30":"B","31":"A","32":"C","33":"A","34":"B","35":"A","36":"C","37":"A","38":"B","39":"A","40":"D","41":"A","42":"B","43":"A","44":"C","45":"A"}'),
|
||||
|
||||
-- 第四次考试(3天前)
|
||||
(@test_user_id, @course_id, 'Python项目实战测试', 30, 100, 80,
|
||||
'2025-09-19 13:30:00', '2025-09-19 15:00:00', 90, 92, 1, 'completed',
|
||||
'[46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75]',
|
||||
'{"46":"A","47":"A","48":"A","49":"A","50":"A","51":"B","52":"A","53":"A","54":"A","55":"B","56":"A","57":"A","58":"A","59":"B","60":"A","61":"A","62":"A","63":"B","64":"A","65":"A","66":"A","67":"A","68":"A","69":"A","70":"A","71":"B","72":"A","73":"A","74":"A","75":"A"}'),
|
||||
|
||||
-- 最近的考试(昨天)
|
||||
(@test_user_id, @course_id, 'Python综合测试', 50, 100, 85,
|
||||
'2025-09-21 10:00:00', '2025-09-21 11:50:00', 110, 95, 1, 'completed',
|
||||
'[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50]',
|
||||
'{"1":"A","2":"A","3":"A","4":"A","5":"A","6":"A","7":"A","8":"A","9":"A","10":"A","11":"A","12":"A","13":"A","14":"A","15":"A","16":"A","17":"A","18":"A","19":"A","20":"A","21":"A","22":"A","23":"A","24":"A","25":"A","26":"B","27":"A","28":"A","29":"A","30":"B","31":"A","32":"A","33":"A","34":"B","35":"A","36":"A","37":"A","38":"B","39":"A","40":"A","41":"A","42":"B","43":"A","44":"A","45":"A","46":"A","47":"A","48":"A","49":"A","50":"A"}');
|
||||
|
||||
-- 如果需要添加答题详情(exam_results 表),可以根据需要补充
|
||||
-- 这里简化处理,因为统计接口主要用 exams 表的数据
|
||||
|
||||
-- 查询验证
|
||||
SELECT
|
||||
COUNT(*) as exam_count,
|
||||
ROUND(AVG(score), 1) as avg_score,
|
||||
MIN(score) as min_score,
|
||||
MAX(score) as max_score
|
||||
FROM exams
|
||||
WHERE user_id = @test_user_id AND status = 'completed';
|
||||
|
||||
-- 计算总练习题数(所有考试的题目总和)
|
||||
SELECT
|
||||
SUM(question_count) as total_practice_questions
|
||||
FROM exams
|
||||
WHERE user_id = @test_user_id AND status = 'completed';
|
||||
|
||||
SELECT '考试数据添加完成!' as message;
|
||||
88
backend/scripts/add_exam_tables.sql
Normal file
88
backend/scripts/add_exam_tables.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- 创建考试相关表
|
||||
|
||||
-- 1. 创建题目表
|
||||
CREATE TABLE IF NOT EXISTS `questions` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`course_id` INT NOT NULL,
|
||||
`question_type` VARCHAR(20) NOT NULL COMMENT '题目类型: single_choice, multiple_choice, true_false, fill_blank, essay',
|
||||
`title` TEXT NOT NULL COMMENT '题目标题',
|
||||
`content` TEXT NULL COMMENT '题目内容',
|
||||
`options` JSON NULL COMMENT '选项(JSON格式)',
|
||||
`correct_answer` TEXT NOT NULL COMMENT '正确答案',
|
||||
`explanation` TEXT NULL COMMENT '答案解释',
|
||||
`score` FLOAT DEFAULT 10.0 COMMENT '分值',
|
||||
`difficulty` VARCHAR(10) DEFAULT 'medium' COMMENT '难度等级: easy, medium, hard',
|
||||
`tags` JSON NULL COMMENT '标签(JSON格式)',
|
||||
`usage_count` INT DEFAULT 0 COMMENT '使用次数',
|
||||
`correct_count` INT DEFAULT 0 COMMENT '答对次数',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否启用',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_course_id` (`course_id`),
|
||||
KEY `idx_question_type` (`question_type`),
|
||||
KEY `idx_difficulty` (`difficulty`),
|
||||
KEY `idx_is_active` (`is_active`),
|
||||
CONSTRAINT `fk_questions_course` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. 创建考试记录表
|
||||
CREATE TABLE IF NOT EXISTS `exams` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT NOT NULL,
|
||||
`course_id` INT NOT NULL,
|
||||
`exam_name` VARCHAR(255) NOT NULL COMMENT '考试名称',
|
||||
`question_count` INT DEFAULT 10 COMMENT '题目数量',
|
||||
`total_score` FLOAT DEFAULT 100.0 COMMENT '总分',
|
||||
`pass_score` FLOAT DEFAULT 60.0 COMMENT '及格分',
|
||||
`start_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间',
|
||||
`end_time` DATETIME NULL COMMENT '结束时间',
|
||||
`duration_minutes` INT DEFAULT 60 COMMENT '考试时长(分钟)',
|
||||
`score` FLOAT NULL COMMENT '得分',
|
||||
`is_passed` BOOLEAN NULL COMMENT '是否通过',
|
||||
`status` VARCHAR(20) DEFAULT 'started' COMMENT '状态: started, submitted, timeout',
|
||||
`questions` JSON NULL COMMENT '题目数据(JSON格式)',
|
||||
`answers` JSON NULL COMMENT '答案数据(JSON格式)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_course_id` (`course_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
CONSTRAINT `fk_exams_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_exams_course` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. 创建考试结果详情表
|
||||
CREATE TABLE IF NOT EXISTS `exam_results` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`exam_id` INT NOT NULL,
|
||||
`question_id` INT NOT NULL,
|
||||
`user_answer` TEXT NULL COMMENT '用户答案',
|
||||
`is_correct` BOOLEAN DEFAULT FALSE COMMENT '是否正确',
|
||||
`score` FLOAT DEFAULT 0.0 COMMENT '得分',
|
||||
`answer_time` INT NULL COMMENT '答题时长(秒)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_exam_id` (`exam_id`),
|
||||
KEY `idx_question_id` (`question_id`),
|
||||
CONSTRAINT `fk_exam_results_exam` FOREIGN KEY (`exam_id`) REFERENCES `exams` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_exam_results_question` FOREIGN KEY (`question_id`) REFERENCES `questions` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. 插入测试题目数据
|
||||
INSERT INTO `questions` (`course_id`, `question_type`, `title`, `content`, `options`, `correct_answer`, `explanation`, `score`, `difficulty`, `tags`) VALUES
|
||||
(1, 'single_choice', 'Python中哪个关键字用于定义函数?', NULL, '{"A": "def", "B": "function", "C": "fun", "D": "define"}', 'A', 'Python使用def关键字来定义函数', 10.0, 'easy', '["python", "基础", "函数"]'),
|
||||
(1, 'single_choice', 'Python中列表和元组的主要区别是什么?', NULL, '{"A": "列表是有序的,元组是无序的", "B": "列表可变,元组不可变", "C": "列表只能存储数字,元组可以存储任何类型", "D": "没有区别"}', 'B', '列表是可变的(mutable),而元组是不可变的(immutable)', 10.0, 'medium', '["python", "数据结构"]'),
|
||||
(1, 'single_choice', '以下哪个不是Python的内置数据类型?', NULL, '{"A": "list", "B": "dict", "C": "array", "D": "tuple"}', 'C', 'array不是Python的内置数据类型,需要导入array模块', 10.0, 'medium', '["python", "数据类型"]'),
|
||||
(1, 'true_false', 'Python是一种编译型语言', NULL, NULL, 'false', 'Python是一种解释型语言,不需要编译成机器码', 10.0, 'easy', '["python", "基础"]'),
|
||||
(1, 'true_false', 'Python支持多重继承', NULL, NULL, 'true', 'Python支持多重继承,一个类可以继承多个父类', 10.0, 'medium', '["python", "面向对象"]');
|
||||
|
||||
-- 5. 插入更多测试题目(如果需要)
|
||||
INSERT INTO `questions` (`course_id`, `question_type`, `title`, `content`, `options`, `correct_answer`, `explanation`, `score`, `difficulty`, `tags`) VALUES
|
||||
(1, 'single_choice', 'Python中的装饰器是什么?', NULL, '{"A": "一种设计模式", "B": "用于修改函数或类行为的函数", "C": "一种数据结构", "D": "一种循环结构"}', 'B', '装饰器是一个接受函数作为参数并返回新函数的函数', 15.0, 'hard', '["python", "高级特性", "装饰器"]'),
|
||||
(1, 'single_choice', '以下哪个方法用于向列表末尾添加元素?', NULL, '{"A": "add()", "B": "insert()", "C": "append()", "D": "extend()"}', 'C', 'append()方法用于向列表末尾添加单个元素', 10.0, 'easy', '["python", "列表", "方法"]'),
|
||||
(1, 'multiple_choice', 'Python中哪些是可变数据类型?', '请选择所有正确答案', '{"A": "list", "B": "tuple", "C": "dict", "D": "str", "E": "set"}', '["A", "C", "E"]', 'list、dict和set是可变数据类型,而tuple和str是不可变的', 15.0, 'medium', '["python", "数据类型"]'),
|
||||
(1, 'fill_blank', 'Python中使用____关键字定义类', NULL, NULL, 'class', '使用class关键字定义类', 10.0, 'easy', '["python", "面向对象"]'),
|
||||
(1, 'essay', '请解释Python中的GIL(全局解释器锁)是什么,以及它对多线程编程的影响', NULL, NULL, 'GIL是Python解释器的一个机制,同一时刻只允许一个线程执行Python字节码。这对CPU密集型的多线程程序性能有负面影响,但对I/O密集型程序影响较小。', 'GIL确保了Python对象的线程安全,但限制了多线程的并行性能', 20.0, 'hard', '["python", "高级", "并发"]');
|
||||
109
backend/scripts/add_school_major_fields.py
Normal file
109
backend/scripts/add_school_major_fields.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
添加用户表的学校和专业字段
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import aiomysql
|
||||
import os
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.append(str(project_root))
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
# SQL脚本
|
||||
ADD_FIELDS_SQL = """
|
||||
-- 添加 school 和 major 字段到 users 表
|
||||
ALTER TABLE users
|
||||
ADD COLUMN school VARCHAR(100) COMMENT '学校' AFTER bio,
|
||||
ADD COLUMN major VARCHAR(100) COMMENT '专业' AFTER school;
|
||||
"""
|
||||
|
||||
async def execute_sql():
|
||||
"""执行SQL脚本"""
|
||||
try:
|
||||
# 从环境变量或配置中获取数据库连接信息
|
||||
# 优先使用环境变量
|
||||
database_url = os.getenv("DATABASE_URL", settings.DATABASE_URL)
|
||||
|
||||
# 解析数据库连接字符串
|
||||
# 格式: mysql+aiomysql://root:root@localhost:3306/kaopeilian
|
||||
if database_url.startswith("mysql+aiomysql://"):
|
||||
url = database_url.replace("mysql+aiomysql://", "")
|
||||
else:
|
||||
url = database_url
|
||||
|
||||
# 解析连接参数
|
||||
auth_host = url.split("@")[1]
|
||||
user_pass = url.split("@")[0]
|
||||
host_port_db = auth_host.split("/")
|
||||
host_port = host_port_db[0].split(":")
|
||||
|
||||
user = user_pass.split(":")[0]
|
||||
password = user_pass.split(":")[1]
|
||||
host = host_port[0]
|
||||
port = int(host_port[1]) if len(host_port) > 1 else 3306
|
||||
database = host_port_db[1].split("?")[0] if len(host_port_db) > 1 else "kaopeilian"
|
||||
|
||||
print(f"连接数据库: {host}:{port}/{database}")
|
||||
|
||||
# 创建数据库连接
|
||||
conn = await aiomysql.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
user=user,
|
||||
password=password,
|
||||
db=database,
|
||||
charset='utf8mb4'
|
||||
)
|
||||
|
||||
async with conn.cursor() as cursor:
|
||||
# 检查字段是否已存在
|
||||
check_sql = """
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = %s
|
||||
AND TABLE_NAME = 'users'
|
||||
AND COLUMN_NAME IN ('school', 'major')
|
||||
"""
|
||||
await cursor.execute(check_sql, (database,))
|
||||
existing_columns = [row[0] for row in await cursor.fetchall()]
|
||||
|
||||
if 'school' in existing_columns or 'major' in existing_columns:
|
||||
print("字段已存在,跳过添加")
|
||||
if 'school' in existing_columns:
|
||||
print("- school 字段已存在")
|
||||
if 'major' in existing_columns:
|
||||
print("- major 字段已存在")
|
||||
else:
|
||||
# 执行SQL脚本
|
||||
await cursor.execute(ADD_FIELDS_SQL)
|
||||
await conn.commit()
|
||||
print("成功添加 school 和 major 字段到 users 表")
|
||||
|
||||
# 显示表结构
|
||||
show_sql = "DESC users"
|
||||
await cursor.execute(show_sql)
|
||||
columns = await cursor.fetchall()
|
||||
|
||||
print("\n当前 users 表结构:")
|
||||
print("-" * 80)
|
||||
for col in columns:
|
||||
print(f"{col[0]:20} {col[1]:30} {'NULL' if col[2] == 'YES' else 'NOT NULL':10} {col[5] or ''}")
|
||||
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("开始添加 school 和 major 字段...")
|
||||
asyncio.run(execute_sql())
|
||||
print("\n执行完成!")
|
||||
63
backend/scripts/add_training_data.sql
Normal file
63
backend/scripts/add_training_data.sql
Normal file
@@ -0,0 +1,63 @@
|
||||
-- ============================================
|
||||
-- 为 testuser 添加训练会话记录
|
||||
-- ============================================
|
||||
|
||||
USE `kaopeilian`;
|
||||
|
||||
-- 获取 testuser 的 ID (应该是 1)
|
||||
SET @test_user_id = 1;
|
||||
|
||||
-- 1. 先插入训练场景
|
||||
INSERT INTO training_scenes (name, description, category, ai_config, status, is_public, is_deleted, created_by) VALUES
|
||||
('Python编程助手', '帮助学员解决Python编程问题', '技术辅导', '{"bot_id": "python_assistant_bot"}', 'ACTIVE', 1, 0, 1),
|
||||
('面试模拟', '模拟技术面试场景', '职业发展', '{"bot_id": "interview_simulator_bot"}', 'ACTIVE', 1, 0, 1),
|
||||
('项目讨论', '项目方案讨论和优化', '项目管理', '{"bot_id": "project_discussion_bot"}', 'ACTIVE', 1, 0, 1);
|
||||
|
||||
-- 获取场景ID
|
||||
SET @scene1_id = LAST_INSERT_ID();
|
||||
SET @scene2_id = @scene1_id + 1;
|
||||
SET @scene3_id = @scene1_id + 2;
|
||||
|
||||
-- 2. 添加训练会话记录(分布在最近15天)
|
||||
INSERT INTO training_sessions (
|
||||
user_id,
|
||||
scene_id,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_seconds,
|
||||
status,
|
||||
total_score,
|
||||
evaluation_result,
|
||||
created_by
|
||||
) VALUES
|
||||
-- 第一周
|
||||
(@test_user_id, @scene1_id, '2025-09-08 09:00:00', '2025-09-08 09:45:00', 2700, 'COMPLETED', 85, '{"feedback": "表现良好"}', @test_user_id),
|
||||
(@test_user_id, @scene2_id, '2025-09-09 14:00:00', '2025-09-09 15:00:00', 3600, 'COMPLETED', 88, '{"feedback": "面试表现不错"}', @test_user_id),
|
||||
(@test_user_id, @scene1_id, '2025-09-10 10:00:00', '2025-09-10 10:30:00', 1800, 'COMPLETED', 82, '{"feedback": "继续努力"}', @test_user_id),
|
||||
(@test_user_id, @scene3_id, '2025-09-11 13:30:00', '2025-09-11 15:00:00', 5400, 'COMPLETED', 90, '{"feedback": "项目思路清晰"}', @test_user_id),
|
||||
(@test_user_id, @scene1_id, '2025-09-12 09:30:00', '2025-09-12 10:10:00', 2400, 'COMPLETED', 87, '{"feedback": "代码质量提升"}', @test_user_id),
|
||||
|
||||
-- 第二周
|
||||
(@test_user_id, @scene2_id, '2025-09-13 14:30:00', '2025-09-13 15:20:00', 3000, 'COMPLETED', 85, '{"feedback": "算法掌握良好"}', @test_user_id),
|
||||
(@test_user_id, @scene1_id, '2025-09-14 10:00:00', '2025-09-14 10:35:00', 2100, 'COMPLETED', 89, '{"feedback": "优秀"}', @test_user_id),
|
||||
(@test_user_id, @scene3_id, '2025-09-15 13:00:00', '2025-09-15 14:15:00', 4500, 'COMPLETED', 91, '{"feedback": "架构设计合理"}', @test_user_id),
|
||||
(@test_user_id, @scene1_id, '2025-09-16 09:15:00', '2025-09-16 10:00:00', 2700, 'COMPLETED', 86, '{"feedback": "进步明显"}', @test_user_id),
|
||||
(@test_user_id, @scene2_id, '2025-09-17 14:00:00', '2025-09-17 14:55:00', 3300, 'COMPLETED', 88, '{"feedback": "表达清晰"}', @test_user_id),
|
||||
|
||||
-- 最近几天
|
||||
(@test_user_id, @scene1_id, '2025-09-18 10:30:00', '2025-09-18 11:00:00', 1800, 'COMPLETED', 90, '{"feedback": "基础扎实"}', @test_user_id),
|
||||
(@test_user_id, @scene3_id, '2025-09-19 13:00:00', '2025-09-19 14:20:00', 4800, 'COMPLETED', 92, '{"feedback": "解决方案优秀"}', @test_user_id),
|
||||
(@test_user_id, @scene1_id, '2025-09-20 09:00:00', '2025-09-20 09:40:00', 2400, 'COMPLETED', 87, '{"feedback": "继续保持"}', @test_user_id),
|
||||
(@test_user_id, @scene2_id, '2025-09-21 14:00:00', '2025-09-21 15:00:00', 3600, 'COMPLETED', 89, '{"feedback": "面试准备充分"}', @test_user_id),
|
||||
(@test_user_id, @scene1_id, '2025-09-22 10:00:00', '2025-09-22 10:45:00', 2700, 'COMPLETED', 91, '{"feedback": "代码优雅"}', @test_user_id);
|
||||
|
||||
-- 查询验证
|
||||
SELECT
|
||||
COUNT(DISTINCT DATE(start_time)) as learning_days,
|
||||
ROUND(SUM(duration_seconds) / 3600, 1) as total_hours,
|
||||
COUNT(*) as session_count,
|
||||
ROUND(AVG(total_score), 1) as avg_score
|
||||
FROM training_sessions
|
||||
WHERE user_id = @test_user_id;
|
||||
|
||||
SELECT '训练数据添加完成!' as message;
|
||||
23
backend/scripts/alter_exam_mistakes_add_question_type.sql
Normal file
23
backend/scripts/alter_exam_mistakes_add_question_type.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- 增量脚本:为exam_mistakes表增加question_type字段
|
||||
-- 创建时间:2025-10-12
|
||||
-- 说明:支持错题按题型筛选和统计
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 1. 为exam_mistakes表增加question_type字段
|
||||
ALTER TABLE exam_mistakes
|
||||
ADD COLUMN question_type VARCHAR(20) NULL COMMENT '题型(single/multiple/judge/blank/essay)' AFTER user_answer;
|
||||
|
||||
-- 2. 添加索引(可选,提升查询性能)
|
||||
ALTER TABLE exam_mistakes
|
||||
ADD INDEX idx_question_type (question_type);
|
||||
|
||||
-- 3. 验证结果
|
||||
DESCRIBE exam_mistakes;
|
||||
|
||||
-- 4. 统计当前错题数据
|
||||
SELECT
|
||||
COUNT(*) as total_mistakes,
|
||||
COUNT(question_type) as has_question_type
|
||||
FROM exam_mistakes;
|
||||
|
||||
36
backend/scripts/alter_exams_add_rounds.sql
Normal file
36
backend/scripts/alter_exams_add_rounds.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- 增量脚本:为exams表增加三轮得分字段
|
||||
-- 创建时间:2025-10-12
|
||||
-- 说明:简化三轮考试机制,一条考试记录存储三轮得分
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 1. 为exams表增加三轮得分字段
|
||||
ALTER TABLE exams
|
||||
ADD COLUMN round1_score FLOAT NULL COMMENT '第一轮得分' AFTER score,
|
||||
ADD COLUMN round2_score FLOAT NULL COMMENT '第二轮得分' AFTER round1_score,
|
||||
ADD COLUMN round3_score FLOAT NULL COMMENT '第三轮得分' AFTER round2_score;
|
||||
|
||||
-- 2. 为已存在的考试记录设置默认值(将score复制到round1_score)
|
||||
UPDATE exams
|
||||
SET round1_score = score
|
||||
WHERE score IS NOT NULL AND round1_score IS NULL;
|
||||
|
||||
-- 3. 验证结果
|
||||
SELECT
|
||||
COUNT(*) as total_exams,
|
||||
COUNT(round1_score) as has_round1,
|
||||
COUNT(round2_score) as has_round2,
|
||||
COUNT(round3_score) as has_round3
|
||||
FROM exams;
|
||||
|
||||
-- 输出结果示例
|
||||
SELECT
|
||||
id,
|
||||
exam_name,
|
||||
score,
|
||||
round1_score,
|
||||
round2_score,
|
||||
round3_score
|
||||
FROM exams
|
||||
LIMIT 5;
|
||||
|
||||
10
backend/scripts/alter_users_email_nullable.sql
Normal file
10
backend/scripts/alter_users_email_nullable.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- 修改users表email字段为可空
|
||||
-- 用于支持员工同步功能,部分员工可能没有邮箱
|
||||
|
||||
-- 修改email字段为可空
|
||||
ALTER TABLE users MODIFY COLUMN email VARCHAR(100) NULL COMMENT '邮箱';
|
||||
|
||||
-- 验证修改
|
||||
DESCRIBE users;
|
||||
|
||||
|
||||
80
backend/scripts/apply_sql_file.py
Normal file
80
backend/scripts/apply_sql_file.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
执行指定的 SQL 文件到当前配置的数据库(使用与后端一致的连接)。
|
||||
|
||||
用法:
|
||||
cd kaopeilian-backend && python3 scripts/apply_sql_file.py scripts/init_database_unified.sql
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 确保可导入应用配置
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
def load_sql_statements(sql_path: Path) -> list[str]:
|
||||
"""读取 SQL 文件并按语句拆分(简单分号分割,忽略注释)。"""
|
||||
raw = sql_path.read_text(encoding="utf-8")
|
||||
# 去掉 -- 开头的行注释
|
||||
lines = []
|
||||
for line in raw.splitlines():
|
||||
striped = line.strip()
|
||||
if not striped:
|
||||
continue
|
||||
if striped.startswith("--"):
|
||||
continue
|
||||
lines.append(line)
|
||||
|
||||
content = "\n".join(lines)
|
||||
|
||||
# 简单分割;注意保留分号作为语句结束标记
|
||||
statements: list[str] = []
|
||||
current = []
|
||||
for ch in content:
|
||||
current.append(ch)
|
||||
if ch == ";":
|
||||
stmt = "".join(current).strip()
|
||||
if stmt:
|
||||
statements.append(stmt)
|
||||
current = []
|
||||
# 可能没有以分号结束的尾部
|
||||
tail = "".join(current).strip()
|
||||
if tail:
|
||||
statements.append(tail)
|
||||
return [s for s in statements if s]
|
||||
|
||||
|
||||
async def apply_sql(sql_file: str):
|
||||
settings = get_settings()
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
|
||||
sql_path = Path(sql_file)
|
||||
if not sql_path.exists():
|
||||
raise FileNotFoundError(f"SQL 文件不存在: {sql_path}")
|
||||
|
||||
statements = load_sql_statements(sql_path)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
# 执行每条语句
|
||||
for stmt in statements:
|
||||
await conn.execute(text(stmt))
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python3 scripts/apply_sql_file.py <sql_file>")
|
||||
sys.exit(1)
|
||||
sql_file = sys.argv[1]
|
||||
await apply_sql(sql_file)
|
||||
print("SQL 执行完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
144
backend/scripts/backup_database.sh
Executable file
144
backend/scripts/backup_database.sh
Executable file
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 考陪练系统数据库自动备份脚本
|
||||
# 作者: AI Assistant
|
||||
# 日期: 2025-09-23
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# 配置变量
|
||||
BACKUP_DIR="/root/aiedu/kaopeilian-backend/backups"
|
||||
CONTAINER_NAME="kaopeilian_mysql"
|
||||
DB_NAME="kaopeilian"
|
||||
DB_USER="root"
|
||||
DB_PASSWORD="Kaopeilian2025!@#"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="${BACKUP_DIR}/kaopeilian_backup_${DATE}.sql"
|
||||
LOG_FILE="${BACKUP_DIR}/backup.log"
|
||||
RETENTION_DAYS=30 # 保留30天的备份
|
||||
|
||||
# 创建备份目录
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "${LOG_FILE}"
|
||||
}
|
||||
|
||||
# 检查Docker容器是否运行
|
||||
check_container() {
|
||||
if ! docker ps | grep -q "${CONTAINER_NAME}"; then
|
||||
log "ERROR: MySQL容器 ${CONTAINER_NAME} 未运行"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 执行数据库备份
|
||||
backup_database() {
|
||||
log "开始备份Docker容器中的数据库 ${DB_NAME}..."
|
||||
|
||||
# 检查容器是否运行
|
||||
if ! docker ps | grep -q "${CONTAINER_NAME}"; then
|
||||
log "ERROR: MySQL容器 ${CONTAINER_NAME} 未运行"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 使用docker exec执行mysqldump,将输出重定向到宿主机文件
|
||||
if docker exec "${CONTAINER_NAME}" mysqldump \
|
||||
-u"${DB_USER}" \
|
||||
-p"${DB_PASSWORD}" \
|
||||
--single-transaction \
|
||||
--routines \
|
||||
--triggers \
|
||||
--events \
|
||||
--hex-blob \
|
||||
--default-character-set=utf8mb4 \
|
||||
--lock-tables=false \
|
||||
--add-drop-database \
|
||||
--create-options \
|
||||
"${DB_NAME}" > "${BACKUP_FILE}" 2>/dev/null; then
|
||||
|
||||
# 检查备份文件大小
|
||||
if [ -f "${BACKUP_FILE}" ] && [ -s "${BACKUP_FILE}" ]; then
|
||||
BACKUP_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
|
||||
log "备份完成: ${BACKUP_FILE} (大小: ${BACKUP_SIZE})"
|
||||
|
||||
# 验证备份文件内容(检查是否包含CREATE DATABASE语句)
|
||||
if grep -q "CREATE DATABASE" "${BACKUP_FILE}"; then
|
||||
log "备份文件验证成功"
|
||||
return 0
|
||||
else
|
||||
log "WARNING: 备份文件可能不完整"
|
||||
return 0 # 仍然算作成功,但记录警告
|
||||
fi
|
||||
else
|
||||
log "ERROR: 备份文件为空或不存在"
|
||||
rm -f "${BACKUP_FILE}"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "ERROR: 数据库备份失败"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 压缩备份文件
|
||||
compress_backup() {
|
||||
if [ -f "${BACKUP_FILE}" ]; then
|
||||
log "压缩备份文件..."
|
||||
gzip "${BACKUP_FILE}"
|
||||
COMPRESSED_FILE="${BACKUP_FILE}.gz"
|
||||
COMPRESSED_SIZE=$(du -h "${COMPRESSED_FILE}" | cut -f1)
|
||||
log "压缩完成: ${COMPRESSED_FILE} (大小: ${COMPRESSED_SIZE})"
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理过期备份
|
||||
cleanup_old_backups() {
|
||||
log "清理 ${RETENTION_DAYS} 天前的备份文件..."
|
||||
find "${BACKUP_DIR}" -name "kaopeilian_backup_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
|
||||
find "${BACKUP_DIR}" -name "kaopeilian_backup_*.sql" -mtime +${RETENTION_DAYS} -delete
|
||||
log "过期备份清理完成"
|
||||
}
|
||||
|
||||
# 发送备份状态通知(可选)
|
||||
send_notification() {
|
||||
local status=$1
|
||||
local message=$2
|
||||
|
||||
# 这里可以集成邮件、钉钉、微信等通知方式
|
||||
log "通知: ${status} - ${message}"
|
||||
|
||||
# 示例:写入状态文件供监控系统读取
|
||||
echo "{\"timestamp\":\"$(date -Iseconds)\",\"status\":\"${status}\",\"message\":\"${message}\"}" > "${BACKUP_DIR}/backup_status.json"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
log "========== 数据库备份开始 =========="
|
||||
|
||||
# 检查容器状态
|
||||
check_container
|
||||
|
||||
# 执行备份
|
||||
if backup_database; then
|
||||
# 压缩备份
|
||||
compress_backup
|
||||
|
||||
# 清理过期备份
|
||||
cleanup_old_backups
|
||||
|
||||
# 发送成功通知
|
||||
send_notification "SUCCESS" "数据库备份成功完成"
|
||||
log "========== 数据库备份完成 =========="
|
||||
exit 0
|
||||
else
|
||||
# 发送失败通知
|
||||
send_notification "FAILED" "数据库备份失败"
|
||||
log "========== 数据库备份失败 =========="
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
353
backend/scripts/binlog_rollback_tool.py
Normal file
353
backend/scripts/binlog_rollback_tool.py
Normal file
@@ -0,0 +1,353 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MySQL Binlog 回滚工具
|
||||
用于考培练系统的数据库回滚操作
|
||||
|
||||
功能:
|
||||
1. 解析Binlog文件
|
||||
2. 生成反向SQL语句
|
||||
3. 执行数据回滚
|
||||
4. 支持时间范围和表过滤
|
||||
|
||||
使用方法:
|
||||
python scripts/binlog_rollback_tool.py --help
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
import aiomysql
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BinlogRollbackTool:
|
||||
"""Binlog回滚工具类"""
|
||||
|
||||
def __init__(self,
|
||||
host: str = "localhost",
|
||||
port: int = 3306,
|
||||
user: str = "root",
|
||||
password: str = "root",
|
||||
database: str = "kaopeilian"):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.database = database
|
||||
self.connection = None
|
||||
|
||||
async def connect(self):
|
||||
"""连接到MySQL数据库"""
|
||||
try:
|
||||
self.connection = await aiomysql.connect(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
db=self.database,
|
||||
charset='utf8mb4'
|
||||
)
|
||||
logger.info(f"✅ 成功连接到数据库 {self.database}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 数据库连接失败: {e}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""关闭数据库连接"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
logger.info("🔒 数据库连接已关闭")
|
||||
|
||||
async def get_binlog_files(self) -> List[Dict]:
|
||||
"""获取Binlog文件列表"""
|
||||
cursor = await self.connection.cursor()
|
||||
await cursor.execute("SHOW BINARY LOGS")
|
||||
result = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
|
||||
binlog_files = []
|
||||
for row in result:
|
||||
binlog_files.append({
|
||||
'name': row[0],
|
||||
'size': row[1],
|
||||
'encrypted': row[2] if len(row) > 2 else False
|
||||
})
|
||||
|
||||
logger.info(f"📋 找到 {len(binlog_files)} 个Binlog文件")
|
||||
return binlog_files
|
||||
|
||||
async def get_binlog_position_by_time(self, target_time: datetime) -> Optional[str]:
|
||||
"""根据时间获取Binlog位置"""
|
||||
cursor = await self.connection.cursor()
|
||||
|
||||
# 获取所有Binlog文件
|
||||
binlog_files = await self.get_binlog_files()
|
||||
|
||||
for binlog_file in binlog_files:
|
||||
try:
|
||||
# 使用mysqlbinlog解析文件,查找时间点
|
||||
cmd = [
|
||||
'mysqlbinlog',
|
||||
'--start-datetime', target_time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'--stop-datetime', (target_time + timedelta(seconds=1)).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
f'/var/lib/mysql/{binlog_file["name"]}'
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
logger.info(f"📍 在 {binlog_file['name']} 中找到时间点 {target_time}")
|
||||
return binlog_file['name']
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 解析 {binlog_file['name']} 时出错: {e}")
|
||||
continue
|
||||
|
||||
logger.warning(f"⚠️ 未找到时间点 {target_time} 对应的Binlog位置")
|
||||
return None
|
||||
|
||||
def parse_binlog_to_sql(self,
|
||||
binlog_file: str,
|
||||
start_time: Optional[datetime] = None,
|
||||
stop_time: Optional[datetime] = None,
|
||||
tables: Optional[List[str]] = None) -> str:
|
||||
"""解析Binlog文件生成SQL语句"""
|
||||
|
||||
# 构建mysqlbinlog命令
|
||||
cmd = ['mysqlbinlog', '--base64-output=decode-rows', '-v']
|
||||
|
||||
if start_time:
|
||||
cmd.extend(['--start-datetime', start_time.strftime('%Y-%m-%d %H:%M:%S')])
|
||||
|
||||
if stop_time:
|
||||
cmd.extend(['--stop-datetime', stop_time.strftime('%Y-%m-%d %H:%M:%S')])
|
||||
|
||||
# 添加数据库过滤
|
||||
cmd.extend(['--database', self.database])
|
||||
|
||||
# 添加表过滤
|
||||
if tables:
|
||||
for table in tables:
|
||||
cmd.extend(['--table', table])
|
||||
|
||||
# 添加Binlog文件路径
|
||||
cmd.append(f'/var/lib/mysql/{binlog_file}')
|
||||
|
||||
logger.info(f"🔍 执行命令: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"❌ mysqlbinlog执行失败: {result.stderr}")
|
||||
return ""
|
||||
|
||||
return result.stdout
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("❌ mysqlbinlog执行超时")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"❌ mysqlbinlog执行异常: {e}")
|
||||
return ""
|
||||
|
||||
def generate_reverse_sql(self, binlog_sql: str) -> List[str]:
|
||||
"""生成反向SQL语句"""
|
||||
reverse_sqls = []
|
||||
|
||||
# 解析INSERT语句,转换为DELETE
|
||||
insert_pattern = r'INSERT INTO `([^`]+)` \(([^)]+)\) VALUES \((.+)\);'
|
||||
for match in re.finditer(insert_pattern, binlog_sql, re.MULTILINE):
|
||||
table = match.group(1)
|
||||
columns = match.group(2)
|
||||
values = match.group(3)
|
||||
|
||||
# 构建WHERE条件
|
||||
where_conditions = []
|
||||
column_list = [col.strip().strip('`') for col in columns.split(',')]
|
||||
value_list = [val.strip().strip("'") for val in values.split(',')]
|
||||
|
||||
for col, val in zip(column_list, value_list):
|
||||
if val != 'NULL':
|
||||
where_conditions.append(f"`{col}` = '{val}'")
|
||||
|
||||
if where_conditions:
|
||||
delete_sql = f"DELETE FROM `{table}` WHERE {' AND '.join(where_conditions)};"
|
||||
reverse_sqls.append(delete_sql)
|
||||
|
||||
# 解析UPDATE语句,生成反向UPDATE
|
||||
update_pattern = r'UPDATE `([^`]+)` SET (.+) WHERE (.+);'
|
||||
for match in re.finditer(update_pattern, binlog_sql, re.MULTILINE):
|
||||
table = match.group(1)
|
||||
set_clause = match.group(2)
|
||||
where_clause = match.group(3)
|
||||
|
||||
# 这里需要从Binlog中提取原始值,ROW格式的Binlog包含@1, @2等变量
|
||||
# 简化处理:生成警告信息
|
||||
reverse_sqls.append(f"-- 需要手动处理UPDATE语句: UPDATE `{table}` SET {set_clause} WHERE {where_clause};")
|
||||
|
||||
# 解析DELETE语句,转换为INSERT
|
||||
delete_pattern = r'DELETE FROM `([^`]+)` WHERE (.+);'
|
||||
for match in re.finditer(delete_pattern, binlog_sql, re.MULTILINE):
|
||||
table = match.group(1)
|
||||
where_clause = match.group(2)
|
||||
|
||||
# 简化处理:生成警告信息
|
||||
reverse_sqls.append(f"-- 需要手动处理DELETE语句: INSERT INTO `{table}` ... WHERE {where_clause};")
|
||||
|
||||
return reverse_sqls
|
||||
|
||||
async def execute_rollback_sql(self, sql_statements: List[str], dry_run: bool = True) -> bool:
|
||||
"""执行回滚SQL语句"""
|
||||
if not sql_statements:
|
||||
logger.warning("⚠️ 没有需要执行的SQL语句")
|
||||
return True
|
||||
|
||||
if dry_run:
|
||||
logger.info("🔍 模拟执行模式 - 以下SQL语句将被执行:")
|
||||
for i, sql in enumerate(sql_statements, 1):
|
||||
logger.info(f"{i:3d}. {sql}")
|
||||
return True
|
||||
|
||||
cursor = await self.connection.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
await cursor.execute("START TRANSACTION")
|
||||
logger.info("🔄 开始回滚事务")
|
||||
|
||||
for i, sql in enumerate(sql_statements, 1):
|
||||
if sql.strip().startswith('--'):
|
||||
logger.info(f"⏭️ 跳过注释: {sql}")
|
||||
continue
|
||||
|
||||
try:
|
||||
await cursor.execute(sql)
|
||||
logger.info(f"✅ 执行成功 ({i}/{len(sql_statements)}): {sql[:100]}...")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 执行失败 ({i}/{len(sql_statements)}): {sql}")
|
||||
logger.error(f" 错误信息: {e}")
|
||||
raise
|
||||
|
||||
# 提交事务
|
||||
await cursor.execute("COMMIT")
|
||||
logger.info("✅ 回滚事务提交成功")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# 回滚事务
|
||||
await cursor.execute("ROLLBACK")
|
||||
logger.error(f"❌ 回滚事务失败,已回滚: {e}")
|
||||
return False
|
||||
finally:
|
||||
await cursor.close()
|
||||
|
||||
async def rollback_by_time(self,
|
||||
target_time: datetime,
|
||||
tables: Optional[List[str]] = None,
|
||||
dry_run: bool = True) -> bool:
|
||||
"""根据时间点进行回滚"""
|
||||
logger.info(f"🎯 开始回滚到时间点: {target_time}")
|
||||
|
||||
# 查找对应的Binlog文件
|
||||
binlog_file = await self.get_binlog_position_by_time(target_time)
|
||||
if not binlog_file:
|
||||
logger.error("❌ 未找到对应的Binlog文件")
|
||||
return False
|
||||
|
||||
# 解析Binlog生成SQL
|
||||
binlog_sql = self.parse_binlog_to_sql(
|
||||
binlog_file=binlog_file,
|
||||
start_time=target_time,
|
||||
tables=tables
|
||||
)
|
||||
|
||||
if not binlog_sql:
|
||||
logger.error("❌ 解析Binlog失败")
|
||||
return False
|
||||
|
||||
# 生成反向SQL
|
||||
reverse_sqls = self.generate_reverse_sql(binlog_sql)
|
||||
|
||||
if not reverse_sqls:
|
||||
logger.warning("⚠️ 未生成反向SQL语句")
|
||||
return True
|
||||
|
||||
# 执行回滚
|
||||
return await self.execute_rollback_sql(reverse_sqls, dry_run)
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='MySQL Binlog 回滚工具')
|
||||
parser.add_argument('--host', default='localhost', help='MySQL主机地址')
|
||||
parser.add_argument('--port', type=int, default=3306, help='MySQL端口')
|
||||
parser.add_argument('--user', default='root', help='MySQL用户名')
|
||||
parser.add_argument('--password', default='root', help='MySQL密码')
|
||||
parser.add_argument('--database', default='kaopeilian', help='数据库名')
|
||||
parser.add_argument('--time', required=True, help='回滚到的时间点 (格式: YYYY-MM-DD HH:MM:SS)')
|
||||
parser.add_argument('--tables', nargs='*', help='指定要回滚的表名')
|
||||
parser.add_argument('--execute', action='store_true', help='实际执行回滚(默认只模拟)')
|
||||
parser.add_argument('--list-binlogs', action='store_true', help='列出所有Binlog文件')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建回滚工具实例
|
||||
tool = BinlogRollbackTool(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
user=args.user,
|
||||
password=args.password,
|
||||
database=args.database
|
||||
)
|
||||
|
||||
try:
|
||||
await tool.connect()
|
||||
|
||||
if args.list_binlogs:
|
||||
# 列出Binlog文件
|
||||
binlog_files = await tool.get_binlog_files()
|
||||
print("\n📋 Binlog文件列表:")
|
||||
for i, file_info in enumerate(binlog_files, 1):
|
||||
print(f"{i:2d}. {file_info['name']} ({file_info['size']} bytes)")
|
||||
return
|
||||
|
||||
# 解析时间参数
|
||||
try:
|
||||
target_time = datetime.strptime(args.time, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
logger.error("❌ 时间格式错误,请使用: YYYY-MM-DD HH:MM:SS")
|
||||
return
|
||||
|
||||
# 执行回滚
|
||||
dry_run = not args.execute
|
||||
success = await tool.rollback_by_time(
|
||||
target_time=target_time,
|
||||
tables=args.tables,
|
||||
dry_run=dry_run
|
||||
)
|
||||
|
||||
if success:
|
||||
if dry_run:
|
||||
logger.info("🔍 模拟执行完成,使用 --execute 参数实际执行回滚")
|
||||
else:
|
||||
logger.info("✅ 回滚操作完成")
|
||||
else:
|
||||
logger.error("❌ 回滚操作失败")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 程序执行异常: {e}")
|
||||
finally:
|
||||
await tool.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
123
backend/scripts/check_backup_status.sh
Executable file
123
backend/scripts/check_backup_status.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 考陪练系统数据库备份状态检查脚本
|
||||
# 作者: AI Assistant
|
||||
# 日期: 2025-09-23
|
||||
|
||||
BACKUP_DIR="/root/aiedu/kaopeilian-backend/backups"
|
||||
LOG_FILE="${BACKUP_DIR}/check_status.log"
|
||||
|
||||
# 日志函数
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "${LOG_FILE}"
|
||||
}
|
||||
|
||||
# 检查最近的备份文件
|
||||
check_recent_backup() {
|
||||
local recent_backup=$(find "${BACKUP_DIR}" -name "kaopeilian_backup_*.sql.gz" -type f -mtime -1 | sort | tail -1)
|
||||
|
||||
if [ -n "${recent_backup}" ]; then
|
||||
local backup_time=$(stat -c %Y "${recent_backup}")
|
||||
local current_time=$(date +%s)
|
||||
local time_diff=$((current_time - backup_time))
|
||||
local hours_diff=$((time_diff / 3600))
|
||||
|
||||
log "最近备份: $(basename ${recent_backup})"
|
||||
log "备份时间: $(stat -c %y "${recent_backup}")"
|
||||
log "距离现在: ${hours_diff} 小时前"
|
||||
|
||||
if [ ${hours_diff} -gt 2 ]; then
|
||||
log "WARNING: 备份文件超过2小时未更新"
|
||||
return 1
|
||||
else
|
||||
log "备份状态: 正常"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
log "ERROR: 未找到最近的备份文件"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查备份文件大小
|
||||
check_backup_size() {
|
||||
local recent_backup=$(find "${BACKUP_DIR}" -name "kaopeilian_backup_*.sql.gz" -type f -mtime -1 | sort | tail -1)
|
||||
|
||||
if [ -n "${recent_backup}" ]; then
|
||||
local backup_size=$(stat -c %s "${recent_backup}")
|
||||
local backup_size_mb=$((backup_size / 1024 / 1024))
|
||||
|
||||
log "备份文件大小: ${backup_size_mb}MB"
|
||||
|
||||
if [ ${backup_size_mb} -lt 1 ]; then
|
||||
log "WARNING: 备份文件过小,可能不完整"
|
||||
return 1
|
||||
else
|
||||
log "备份文件大小: 正常"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查定时任务状态
|
||||
check_cron_status() {
|
||||
if crontab -l | grep -q "backup_database.sh"; then
|
||||
log "定时任务: 已配置"
|
||||
return 0
|
||||
else
|
||||
log "ERROR: 定时任务未配置"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查systemd定时器状态
|
||||
check_systemd_timer() {
|
||||
if systemctl is-active kaopeilian-backup.timer >/dev/null 2>&1; then
|
||||
log "Systemd定时器: 运行中"
|
||||
|
||||
# 获取下次执行时间
|
||||
local next_run=$(systemctl list-timers kaopeilian-backup.timer --no-pager | grep kaopeilian-backup.timer | awk '{print $1, $2}')
|
||||
log "下次执行时间: ${next_run}"
|
||||
|
||||
return 0
|
||||
else
|
||||
log "ERROR: Systemd定时器未运行"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查Docker容器状态
|
||||
check_docker_container() {
|
||||
if docker ps | grep -q "kaopeilian_mysql"; then
|
||||
log "MySQL容器: 运行中"
|
||||
return 0
|
||||
else
|
||||
log "ERROR: MySQL容器未运行"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 主检查函数
|
||||
main() {
|
||||
log "========== 备份状态检查开始 =========="
|
||||
|
||||
local exit_code=0
|
||||
|
||||
# 执行各项检查
|
||||
check_recent_backup || exit_code=1
|
||||
check_backup_size || exit_code=1
|
||||
check_cron_status || exit_code=1
|
||||
check_systemd_timer || exit_code=1
|
||||
check_docker_container || exit_code=1
|
||||
|
||||
if [ ${exit_code} -eq 0 ]; then
|
||||
log "========== 备份状态检查完成: 全部正常 =========="
|
||||
else
|
||||
log "========== 备份状态检查完成: 发现问题 =========="
|
||||
fi
|
||||
|
||||
return ${exit_code}
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
178
backend/scripts/check_database_status.py
Normal file
178
backend/scripts/check_database_status.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
检查数据库现状脚本
|
||||
用于探索users、teams、exams、practice_sessions表的数据情况
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.core.logger import logger
|
||||
from app.models.user import User, Team, UserTeam
|
||||
from app.models.exam import Exam
|
||||
from app.models.practice import PracticeSession
|
||||
from app.models.position_member import PositionMember
|
||||
|
||||
|
||||
async def check_database(db: AsyncSession):
|
||||
"""检查数据库数据"""
|
||||
|
||||
print("=" * 60)
|
||||
print("数据库状态检查")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 检查用户数据
|
||||
print("\n【用户表 (users)】")
|
||||
result = await db.execute(select(func.count()).select_from(User))
|
||||
total_users = result.scalar() or 0
|
||||
print(f"总用户数: {total_users}")
|
||||
|
||||
if total_users > 0:
|
||||
# 按角色统计
|
||||
result = await db.execute(
|
||||
select(User.role, func.count()).group_by(User.role)
|
||||
)
|
||||
print("按角色统计:")
|
||||
for role, count in result.all():
|
||||
print(f" - {role}: {count}")
|
||||
|
||||
# 显示部分用户
|
||||
result = await db.execute(
|
||||
select(User).limit(5)
|
||||
)
|
||||
users = result.scalars().all()
|
||||
print("\n前5个用户:")
|
||||
for user in users:
|
||||
print(f" - ID:{user.id}, 用户名:{user.username}, 姓名:{user.full_name}, 角色:{user.role}")
|
||||
|
||||
# 2. 检查团队数据
|
||||
print("\n【团队表 (teams)】")
|
||||
result = await db.execute(select(func.count()).select_from(Team))
|
||||
total_teams = result.scalar() or 0
|
||||
print(f"总团队数: {total_teams}")
|
||||
|
||||
if total_teams > 0:
|
||||
result = await db.execute(select(Team).limit(5))
|
||||
teams = result.scalars().all()
|
||||
print("团队列表:")
|
||||
for team in teams:
|
||||
print(f" - ID:{team.id}, 名称:{team.name}, 代码:{team.code}, 类型:{team.team_type}")
|
||||
|
||||
# 3. 检查用户团队关联
|
||||
print("\n【用户团队关联表 (user_teams)】")
|
||||
result = await db.execute(select(func.count()).select_from(UserTeam))
|
||||
total_relations = result.scalar() or 0
|
||||
print(f"总关联记录数: {total_relations}")
|
||||
|
||||
if total_relations > 0:
|
||||
# 查询前5条关联记录
|
||||
result = await db.execute(
|
||||
select(UserTeam, User.full_name, Team.name)
|
||||
.join(User, UserTeam.user_id == User.id)
|
||||
.join(Team, UserTeam.team_id == Team.id)
|
||||
.limit(5)
|
||||
)
|
||||
print("关联记录示例:")
|
||||
for user_team, user_name, team_name in result.all():
|
||||
print(f" - 用户:{user_name}, 团队:{team_name}, 角色:{user_team.role}")
|
||||
|
||||
# 4. 检查岗位成员
|
||||
print("\n【岗位成员表 (position_members)】")
|
||||
result = await db.execute(
|
||||
select(func.count()).select_from(PositionMember).where(
|
||||
PositionMember.is_deleted == False
|
||||
)
|
||||
)
|
||||
total_position_members = result.scalar() or 0
|
||||
print(f"总岗位成员数: {total_position_members}")
|
||||
|
||||
# 5. 检查考试记录
|
||||
print("\n【考试表 (exams)】")
|
||||
result = await db.execute(select(func.count()).select_from(Exam))
|
||||
total_exams = result.scalar() or 0
|
||||
print(f"总考试记录数: {total_exams}")
|
||||
|
||||
if total_exams > 0:
|
||||
# 按状态统计
|
||||
result = await db.execute(
|
||||
select(Exam.status, func.count()).group_by(Exam.status)
|
||||
)
|
||||
print("按状态统计:")
|
||||
for status, count in result.all():
|
||||
print(f" - {status}: {count}")
|
||||
|
||||
# 显示最近5条考试记录
|
||||
result = await db.execute(
|
||||
select(Exam, User.full_name)
|
||||
.join(User, Exam.user_id == User.id)
|
||||
.order_by(Exam.created_at.desc())
|
||||
.limit(5)
|
||||
)
|
||||
print("\n最近5条考试记录:")
|
||||
for exam, user_name in result.all():
|
||||
print(f" - 用户:{user_name}, 课程ID:{exam.course_id}, 状态:{exam.status}, "
|
||||
f"分数:{exam.round1_score}, 时间:{exam.created_at}")
|
||||
|
||||
# 6. 检查陪练记录
|
||||
print("\n【陪练会话表 (practice_sessions)】")
|
||||
result = await db.execute(select(func.count()).select_from(PracticeSession))
|
||||
total_sessions = result.scalar() or 0
|
||||
print(f"总陪练记录数: {total_sessions}")
|
||||
|
||||
if total_sessions > 0:
|
||||
# 按状态统计
|
||||
result = await db.execute(
|
||||
select(PracticeSession.status, func.count()).group_by(PracticeSession.status)
|
||||
)
|
||||
print("按状态统计:")
|
||||
for status, count in result.all():
|
||||
print(f" - {status}: {count}")
|
||||
|
||||
# 显示最近5条陪练记录
|
||||
result = await db.execute(
|
||||
select(PracticeSession, User.full_name)
|
||||
.join(User, PracticeSession.user_id == User.id)
|
||||
.order_by(PracticeSession.start_time.desc())
|
||||
.limit(5)
|
||||
)
|
||||
print("\n最近5条陪练记录:")
|
||||
for session, user_name in result.all():
|
||||
print(f" - 用户:{user_name}, 场景:{session.scene_name}, 状态:{session.status}, "
|
||||
f"时长:{session.duration_seconds}秒, 时间:{session.start_time}")
|
||||
|
||||
# 总结
|
||||
print("\n" + "=" * 60)
|
||||
print("检查总结:")
|
||||
print("=" * 60)
|
||||
if total_users == 0:
|
||||
print("❌ 数据库为空,需要注入测试数据")
|
||||
elif total_teams == 0 or total_relations == 0:
|
||||
print("⚠️ 有用户但无团队数据,需要创建团队")
|
||||
elif total_exams == 0:
|
||||
print("⚠️ 有用户和团队但无考试记录,需要创建考试数据")
|
||||
elif total_sessions == 0:
|
||||
print("⚠️ 有用户和团队但无陪练记录,需要创建陪练数据")
|
||||
else:
|
||||
print("✅ 数据库有基本数据,可以直接使用")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
await check_database(db)
|
||||
except Exception as e:
|
||||
logger.error(f"检查数据库失败: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
78
backend/scripts/cleanup_users.py
Normal file
78
backend/scripts/cleanup_users.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
用户清理脚本
|
||||
删除除admin以外的所有用户,为员工同步做准备
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy import select, delete
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.models.user import User
|
||||
from app.core.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def cleanup_users():
|
||||
"""
|
||||
删除除admin以外的所有用户
|
||||
保留username='admin'的管理员账号
|
||||
"""
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
# 查询要删除的用户
|
||||
stmt = select(User).where(User.username != 'admin', User.is_deleted == False)
|
||||
result = await db.execute(stmt)
|
||||
users_to_delete = result.scalars().all()
|
||||
|
||||
if not users_to_delete:
|
||||
logger.info("没有需要删除的用户")
|
||||
return
|
||||
|
||||
logger.info(f"准备删除 {len(users_to_delete)} 个用户")
|
||||
for user in users_to_delete:
|
||||
logger.info(f" - ID: {user.id}, 用户名: {user.username}, 姓名: {user.full_name}")
|
||||
|
||||
# 确认删除
|
||||
print("\n⚠️ 警告: 将删除以上用户(保留admin)")
|
||||
confirm = input("确认删除?(yes/no): ")
|
||||
|
||||
if confirm.lower() != 'yes':
|
||||
logger.info("取消删除操作")
|
||||
return
|
||||
|
||||
# 执行软删除
|
||||
for user in users_to_delete:
|
||||
user.is_deleted = True
|
||||
logger.info(f"已软删除用户: {user.username}")
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✅ 成功删除 {len(users_to_delete)} 个用户")
|
||||
|
||||
# 显示剩余用户
|
||||
stmt = select(User).where(User.is_deleted == False)
|
||||
result = await db.execute(stmt)
|
||||
remaining_users = result.scalars().all()
|
||||
|
||||
logger.info(f"\n剩余用户数量: {len(remaining_users)}")
|
||||
for user in remaining_users:
|
||||
logger.info(f" - ID: {user.id}, 用户名: {user.username}, 角色: {user.role}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理用户失败: {str(e)}")
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("用户清理脚本")
|
||||
print("=" * 60)
|
||||
asyncio.run(cleanup_users())
|
||||
|
||||
35
backend/scripts/create_course_exam_settings.sql
Normal file
35
backend/scripts/create_course_exam_settings.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- 创建课程考试设置表
|
||||
CREATE TABLE IF NOT EXISTS course_exam_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
course_id INT NOT NULL UNIQUE COMMENT '课程ID',
|
||||
single_choice_count INT NOT NULL DEFAULT 10 COMMENT '单选题数量',
|
||||
multiple_choice_count INT NOT NULL DEFAULT 5 COMMENT '多选题数量',
|
||||
true_false_count INT NOT NULL DEFAULT 5 COMMENT '判断题数量',
|
||||
fill_blank_count INT NOT NULL DEFAULT 0 COMMENT '填空题数量',
|
||||
duration_minutes INT NOT NULL DEFAULT 60 COMMENT '考试时长(分钟)',
|
||||
difficulty_level INT NOT NULL DEFAULT 3 COMMENT '难度系数(1-5)',
|
||||
passing_score INT NOT NULL DEFAULT 60 COMMENT '及格分数',
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用',
|
||||
show_answer_immediately BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否立即显示答案',
|
||||
allow_retake BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否允许重考',
|
||||
max_retake_times INT COMMENT '最大重考次数',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by INT COMMENT '创建人ID',
|
||||
updated_by INT COMMENT '更新人ID',
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted_at DATETIME,
|
||||
deleted_by INT COMMENT '删除人ID',
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id),
|
||||
INDEX ix_course_exam_settings_id (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程考试设置表';
|
||||
|
||||
-- 插入示例课程考试设置
|
||||
INSERT INTO course_exam_settings (course_id, single_choice_count, multiple_choice_count, true_false_count, duration_minutes)
|
||||
VALUES
|
||||
(1, 15, 8, 5, 60),
|
||||
(2, 20, 10, 10, 90),
|
||||
(3, 10, 5, 5, 45),
|
||||
(4, 12, 6, 8, 60);
|
||||
|
||||
SELECT 'Course exam settings table created successfully!' as message;
|
||||
79
backend/scripts/create_practice_analysis_tables.sql
Normal file
79
backend/scripts/create_practice_analysis_tables.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- 陪练分析报告功能数据库表
|
||||
-- 创建时间:2025-10-13
|
||||
|
||||
-- 1. 陪练会话表
|
||||
CREATE TABLE IF NOT EXISTS `practice_sessions` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` VARCHAR(50) NOT NULL UNIQUE COMMENT '会话ID(如PS006)',
|
||||
`user_id` INT NOT NULL COMMENT '学员ID',
|
||||
`scene_id` INT COMMENT '场景ID',
|
||||
`scene_name` VARCHAR(200) COMMENT '场景名称',
|
||||
`scene_type` VARCHAR(50) COMMENT '场景类型:phone/face/complaint等',
|
||||
`conversation_id` VARCHAR(100) COMMENT 'Coze对话ID',
|
||||
|
||||
-- 会话时间信息
|
||||
`start_time` DATETIME NOT NULL COMMENT '开始时间',
|
||||
`end_time` DATETIME COMMENT '结束时间',
|
||||
`duration_seconds` INT DEFAULT 0 COMMENT '时长(秒)',
|
||||
`turns` INT DEFAULT 0 COMMENT '对话轮次',
|
||||
`status` VARCHAR(20) DEFAULT 'in_progress' COMMENT '状态:in_progress/completed/canceled',
|
||||
|
||||
-- 审计字段
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (scene_id) REFERENCES practice_scenes(id) ON DELETE SET NULL,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_start_time (start_time),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练会话表';
|
||||
|
||||
-- 2. 对话记录表
|
||||
CREATE TABLE IF NOT EXISTS `practice_dialogues` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` VARCHAR(50) NOT NULL COMMENT '会话ID',
|
||||
`speaker` VARCHAR(20) NOT NULL COMMENT '说话人:user/ai',
|
||||
`content` TEXT NOT NULL COMMENT '对话内容',
|
||||
`timestamp` DATETIME NOT NULL COMMENT '时间戳',
|
||||
`sequence` INT NOT NULL COMMENT '顺序号(从1开始)',
|
||||
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_sequence (session_id, sequence)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练对话记录表';
|
||||
|
||||
-- 3. 分析报告表
|
||||
CREATE TABLE IF NOT EXISTS `practice_reports` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` VARCHAR(50) NOT NULL UNIQUE COMMENT '会话ID',
|
||||
|
||||
-- AI分析结果(JSON存储)
|
||||
`total_score` INT COMMENT '综合得分(0-100)',
|
||||
`score_breakdown` JSON COMMENT '分数细分:开场技巧、需求挖掘等',
|
||||
`ability_dimensions` JSON COMMENT '能力维度:沟通表达、倾听理解等',
|
||||
`dialogue_review` JSON COMMENT '对话复盘(标注亮点话术/金牌话术)',
|
||||
`suggestions` JSON COMMENT '改进建议',
|
||||
|
||||
-- Dify工作流信息
|
||||
`workflow_run_id` VARCHAR(100) COMMENT 'Dify工作流运行ID',
|
||||
`task_id` VARCHAR(100) COMMENT 'Dify任务ID',
|
||||
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_session_id (session_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练分析报告表';
|
||||
|
||||
-- 验证表创建
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
TABLE_ROWS,
|
||||
TABLE_COMMENT
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME IN ('practice_sessions', 'practice_dialogues', 'practice_reports');
|
||||
|
||||
107
backend/scripts/create_practice_scenes.sql
Normal file
107
backend/scripts/create_practice_scenes.sql
Normal file
@@ -0,0 +1,107 @@
|
||||
-- 创建陪练场景表
|
||||
CREATE TABLE IF NOT EXISTS `practice_scenes` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '场景名称',
|
||||
`description` TEXT COMMENT '场景描述',
|
||||
`type` VARCHAR(50) NOT NULL COMMENT '场景类型: phone/face/complaint/after-sales/product-intro',
|
||||
`difficulty` VARCHAR(50) NOT NULL COMMENT '难度等级: beginner/junior/intermediate/senior/expert',
|
||||
`status` VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/inactive',
|
||||
`background` TEXT COMMENT '场景背景设定',
|
||||
`ai_role` TEXT COMMENT 'AI角色描述',
|
||||
`objectives` JSON COMMENT '练习目标数组',
|
||||
`keywords` JSON COMMENT '关键词数组',
|
||||
`duration` INT DEFAULT 10 COMMENT '预计时长(分钟)',
|
||||
`usage_count` INT DEFAULT 0 COMMENT '使用次数',
|
||||
`rating` DECIMAL(3,1) DEFAULT 0.0 COMMENT '评分',
|
||||
`created_by` INT COMMENT '创建人ID',
|
||||
`updated_by` INT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE,
|
||||
`deleted_at` DATETIME,
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_difficulty (difficulty),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练场景表';
|
||||
|
||||
-- 插入初始场景数据(5个场景,涵盖不同类型和难度)
|
||||
INSERT INTO `practice_scenes` (name, description, type, difficulty, status, background, ai_role, objectives, keywords, duration, created_by, updated_by) VALUES
|
||||
(
|
||||
'初次电话拜访客户',
|
||||
'模拟首次通过电话联系潜在客户的场景,练习专业的电话销售技巧',
|
||||
'phone',
|
||||
'beginner',
|
||||
'active',
|
||||
'你是一名轻医美品牌的销售专员,需要通过电话联系一位从未接触过的潜在客户。客户是某美容院的老板,你需要在短时间内引起她的兴趣,介绍你们的产品和服务。',
|
||||
'AI扮演一位忙碌的美容院老板,对推销电话比较抵触,但如果销售人员能够快速切入她的需求点(如提升业绩、吸引客户、新项目),她会愿意继续交谈。她关注产品效果、价格和培训支持。',
|
||||
'["学会专业的电话开场白", "快速建立信任关系", "有效探询客户需求", "预约下次沟通时间"]',
|
||||
'["开场白", "需求挖掘", "时间管理", "预约技巧"]',
|
||||
10,
|
||||
1,
|
||||
1
|
||||
),
|
||||
(
|
||||
'处理价格异议',
|
||||
'练习如何应对客户对产品价格的质疑和异议,强调价值而非价格',
|
||||
'face',
|
||||
'intermediate',
|
||||
'active',
|
||||
'客户对你们的轻医美产品很感兴趣,已经了解了产品功能和效果,但认为价格太高,超出了她的预算。你需要通过价值塑造和对比分析来化解价格异议。',
|
||||
'AI扮演一位对价格非常敏感的美容院老板,她会不断提出价格异议,例如"太贵了"、"竞品便宜一半"、"能不能再便宜点"。但如果销售人员能够有效展示产品价值、投资回报率和长期收益,她会逐渐被说服。',
|
||||
'["掌握价值塑造技巧", "学会处理价格异议", "提升谈判能力", "展示投资回报率"]',
|
||||
'["异议处理", "价值塑造", "谈判技巧", "ROI分析"]',
|
||||
15,
|
||||
1,
|
||||
1
|
||||
),
|
||||
(
|
||||
'客户投诉处理',
|
||||
'模拟客户对产品或服务不满的投诉场景,练习专业的投诉处理技巧',
|
||||
'complaint',
|
||||
'intermediate',
|
||||
'active',
|
||||
'一位客户购买了你们的轻医美产品后,使用效果不理想,打电话来投诉。她情绪比较激动,认为产品宣传与实际不符,要求退款。你需要安抚客户情绪,了解问题根源,并提供合理的解决方案。',
|
||||
'AI扮演一位情绪激动的客户,她对产品效果不满意,觉得被欺骗了。她会表达强烈的不满和质疑,但如果客服人员能够真诚道歉、耐心倾听、专业分析问题原因并提供切实可行的解决方案,她的态度会逐渐缓和。',
|
||||
'["掌握情绪安抚技巧", "学会倾听和共情", "分析问题并提供解决方案", "挽回客户信任"]',
|
||||
'["投诉处理", "情绪管理", "问题分析", "客户挽回"]',
|
||||
12,
|
||||
1,
|
||||
1
|
||||
),
|
||||
(
|
||||
'产品功能介绍',
|
||||
'练习向客户详细介绍轻医美产品的功能特点和优势',
|
||||
'product-intro',
|
||||
'junior',
|
||||
'active',
|
||||
'客户对你们的轻医美产品有一定了解,现在希望你详细介绍产品的核心功能、技术原理、使用方法和效果保证。客户比较专业,会提出一些技术性问题。',
|
||||
'AI扮演一位专业的美容院经营者,她对轻医美产品有一定了解,会提出具体的技术问题和需求。例如询问产品成分、作用机理、适用人群、操作流程、注意事项等。她希望得到专业、详细、真实的回答。',
|
||||
'["清晰介绍产品功能和原理", "突出产品优势和差异化", "专业回答技术问题", "建立专业形象"]',
|
||||
'["产品介绍", "功能展示", "优势分析", "技术解答"]',
|
||||
12,
|
||||
1,
|
||||
1
|
||||
),
|
||||
(
|
||||
'售后服务咨询',
|
||||
'模拟客户咨询售后服务的场景,练习专业的售后服务沟通技巧',
|
||||
'after-sales',
|
||||
'beginner',
|
||||
'active',
|
||||
'客户已经购买了你们的轻医美产品,现在打电话咨询售后服务相关问题,包括产品使用方法、遇到的小问题、培训支持、配件购买等。你需要提供专业、耐心、周到的售后服务。',
|
||||
'AI扮演一位已购买产品的美容院老板,她在使用产品过程中遇到一些问题或疑问,希望得到专业的指导和帮助。她的态度比较友好,但希望得到快速、有效的解决方案。她会根据服务质量评价品牌。',
|
||||
'["掌握产品使用指导技巧", "快速解决客户问题", "提供专业培训支持", "增强客户满意度和忠诚度"]',
|
||||
'["售后服务", "使用指导", "问题解决", "客户维护"]',
|
||||
10,
|
||||
1,
|
||||
1
|
||||
);
|
||||
|
||||
-- 查询验证
|
||||
SELECT id, name, type, difficulty, status FROM practice_scenes WHERE is_deleted = FALSE;
|
||||
|
||||
|
||||
|
||||
198
backend/scripts/create_practice_table.py
Normal file
198
backend/scripts/create_practice_table.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建陪练场景表并插入初始数据 - 简单版本
|
||||
"""
|
||||
import pymysql
|
||||
import sys
|
||||
|
||||
# 数据库连接配置
|
||||
DB_CONFIG = {
|
||||
'host': 'localhost',
|
||||
'port': 3306,
|
||||
'user': 'root',
|
||||
'password': 'nj861021', # 开发环境密码
|
||||
'database': 'kaopeilian',
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
|
||||
# SQL语句
|
||||
CREATE_TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS `practice_scenes` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '场景名称',
|
||||
`description` TEXT COMMENT '场景描述',
|
||||
`type` VARCHAR(50) NOT NULL COMMENT '场景类型',
|
||||
`difficulty` VARCHAR(50) NOT NULL COMMENT '难度等级',
|
||||
`status` VARCHAR(20) DEFAULT 'active' COMMENT '状态',
|
||||
`background` TEXT COMMENT '场景背景设定',
|
||||
`ai_role` TEXT COMMENT 'AI角色描述',
|
||||
`objectives` JSON COMMENT '练习目标数组',
|
||||
`keywords` JSON COMMENT '关键词数组',
|
||||
`duration` INT DEFAULT 10 COMMENT '预计时长(分钟)',
|
||||
`usage_count` INT DEFAULT 0 COMMENT '使用次数',
|
||||
`rating` DECIMAL(3,1) DEFAULT 0.0 COMMENT '评分',
|
||||
`created_by` INT COMMENT '创建人ID',
|
||||
`updated_by` INT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE,
|
||||
`deleted_at` DATETIME,
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_difficulty (difficulty),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练场景表'
|
||||
"""
|
||||
|
||||
# 初始场景数据
|
||||
SCENES = [
|
||||
{
|
||||
'name': '初次电话拜访客户',
|
||||
'description': '模拟首次通过电话联系潜在客户的场景,练习专业的电话销售技巧',
|
||||
'type': 'phone',
|
||||
'difficulty': 'beginner',
|
||||
'status': 'active',
|
||||
'background': '你是一名轻医美品牌的销售专员,需要通过电话联系一位从未接触过的潜在客户。客户是某美容院的老板,你需要在短时间内引起她的兴趣,介绍你们的产品和服务。',
|
||||
'ai_role': 'AI扮演一位忙碌的美容院老板,对推销电话比较抵触,但如果销售人员能够快速切入她的需求点(如提升业绩、吸引客户、新项目),她会愿意继续交谈。她关注产品效果、价格和培训支持。',
|
||||
'objectives': '["学会专业的电话开场白", "快速建立信任关系", "有效探询客户需求", "预约下次沟通时间"]',
|
||||
'keywords': '["开场白", "需求挖掘", "时间管理", "预约技巧"]',
|
||||
'duration': 10
|
||||
},
|
||||
{
|
||||
'name': '处理价格异议',
|
||||
'description': '练习如何应对客户对产品价格的质疑和异议,强调价值而非价格',
|
||||
'type': 'face',
|
||||
'difficulty': 'intermediate',
|
||||
'status': 'active',
|
||||
'background': '客户对你们的轻医美产品很感兴趣,已经了解了产品功能和效果,但认为价格太高,超出了她的预算。你需要通过价值塑造和对比分析来化解价格异议。',
|
||||
'ai_role': 'AI扮演一位对价格非常敏感的美容院老板,她会不断提出价格异议,例如"太贵了"、"竞品便宜一半"、"能不能再便宜点"。但如果销售人员能够有效展示产品价值、投资回报率和长期收益,她会逐渐被说服。',
|
||||
'objectives': '["掌握价值塑造技巧", "学会处理价格异议", "提升谈判能力", "展示投资回报率"]',
|
||||
'keywords': '["异议处理", "价值塑造", "谈判技巧", "ROI分析"]',
|
||||
'duration': 15
|
||||
},
|
||||
{
|
||||
'name': '客户投诉处理',
|
||||
'description': '模拟客户对产品或服务不满的投诉场景,练习专业的投诉处理技巧',
|
||||
'type': 'complaint',
|
||||
'difficulty': 'intermediate',
|
||||
'status': 'active',
|
||||
'background': '一位客户购买了你们的轻医美产品后,使用效果不理想,打电话来投诉。她情绪比较激动,认为产品宣传与实际不符,要求退款。你需要安抚客户情绪,了解问题根源,并提供合理的解决方案。',
|
||||
'ai_role': 'AI扮演一位情绪激动的客户,她对产品效果不满意,觉得被欺骗了。她会表达强烈的不满和质疑,但如果客服人员能够真诚道歉、耐心倾听、专业分析问题原因并提供切实可行的解决方案,她的态度会逐渐缓和。',
|
||||
'objectives': '["掌握情绪安抚技巧", "学会倾听和共情", "分析问题并提供解决方案", "挽回客户信任"]',
|
||||
'keywords': '["投诉处理", "情绪管理", "问题分析", "客户挽回"]',
|
||||
'duration': 12
|
||||
},
|
||||
{
|
||||
'name': '产品功能介绍',
|
||||
'description': '练习向客户详细介绍轻医美产品的功能特点和优势',
|
||||
'type': 'product-intro',
|
||||
'difficulty': 'junior',
|
||||
'status': 'active',
|
||||
'background': '客户对你们的轻医美产品有一定了解,现在希望你详细介绍产品的核心功能、技术原理、使用方法和效果保证。客户比较专业,会提出一些技术性问题。',
|
||||
'ai_role': 'AI扮演一位专业的美容院经营者,她对轻医美产品有一定了解,会提出具体的技术问题和需求。例如询问产品成分、作用机理、适用人群、操作流程、注意事项等。她希望得到专业、详细、真实的回答。',
|
||||
'objectives': '["清晰介绍产品功能和原理", "突出产品优势和差异化", "专业回答技术问题", "建立专业形象"]',
|
||||
'keywords': '["产品介绍", "功能展示", "优势分析", "技术解答"]',
|
||||
'duration': 12
|
||||
},
|
||||
{
|
||||
'name': '售后服务咨询',
|
||||
'description': '模拟客户咨询售后服务的场景,练习专业的售后服务沟通技巧',
|
||||
'type': 'after-sales',
|
||||
'difficulty': 'beginner',
|
||||
'status': 'active',
|
||||
'background': '客户已经购买了你们的轻医美产品,现在打电话咨询售后服务相关问题,包括产品使用方法、遇到的小问题、培训支持、配件购买等。你需要提供专业、耐心、周到的售后服务。',
|
||||
'ai_role': 'AI扮演一位已购买产品的美容院老板,她在使用产品过程中遇到一些问题或疑问,希望得到专业的指导和帮助。她的态度比较友好,但希望得到快速、有效的解决方案。她会根据服务质量评价品牌。',
|
||||
'objectives': '["掌握产品使用指导技巧", "快速解决客户问题", "提供专业培训支持", "增强客户满意度和忠诚度"]',
|
||||
'keywords': '["售后服务", "使用指导", "问题解决", "客户维护"]',
|
||||
'duration': 10
|
||||
}
|
||||
]
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
try:
|
||||
print("=" * 60)
|
||||
print("陪练场景表创建和初始数据插入")
|
||||
print("=" * 60)
|
||||
|
||||
# 连接数据库
|
||||
print("\n📝 连接数据库...")
|
||||
connection = pymysql.connect(**DB_CONFIG)
|
||||
cursor = connection.cursor()
|
||||
|
||||
# 创建表
|
||||
print("📝 创建practice_scenes表...")
|
||||
cursor.execute(CREATE_TABLE_SQL)
|
||||
print("✅ 表创建成功")
|
||||
|
||||
# 检查是否已有数据
|
||||
cursor.execute("SELECT COUNT(*) FROM practice_scenes WHERE is_deleted = FALSE")
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
if count > 0:
|
||||
print(f"\n⚠️ 表中已有 {count} 条数据,是否要插入新数据?")
|
||||
print(" 提示:如果数据已存在,将跳过插入")
|
||||
|
||||
# 插入数据
|
||||
insert_sql = """
|
||||
INSERT INTO practice_scenes
|
||||
(name, description, type, difficulty, status, background, ai_role, objectives, keywords, duration, created_by, updated_by)
|
||||
VALUES (%(name)s, %(description)s, %(type)s, %(difficulty)s, %(status)s, %(background)s, %(ai_role)s, %(objectives)s, %(keywords)s, %(duration)s, 1, 1)
|
||||
"""
|
||||
|
||||
print(f"\n📝 插入 {len(SCENES)} 个初始场景...")
|
||||
inserted = 0
|
||||
for scene in SCENES:
|
||||
try:
|
||||
cursor.execute(insert_sql, scene)
|
||||
inserted += 1
|
||||
print(f" ✅ 插入场景: {scene['name']}")
|
||||
except pymysql.err.IntegrityError as e:
|
||||
if "Duplicate entry" in str(e):
|
||||
print(f" ⚠️ 场景已存在: {scene['name']}")
|
||||
else:
|
||||
raise
|
||||
|
||||
# 提交事务
|
||||
connection.commit()
|
||||
print(f"\n✅ 成功插入 {inserted} 个场景")
|
||||
|
||||
# 查询验证
|
||||
print("\n📝 查询验证...")
|
||||
cursor.execute("SELECT id, name, type, difficulty, status FROM practice_scenes WHERE is_deleted = FALSE")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
print(f"\n当前场景列表(共 {len(rows)} 个):")
|
||||
print("-" * 80)
|
||||
print(f"{'ID':<5} {'名称':<30} {'类型':<15} {'难度':<15} {'状态':<10}")
|
||||
print("-" * 80)
|
||||
for row in rows:
|
||||
print(f"{row[0]:<5} {row[1]:<30} {row[2]:<15} {row[3]:<15} {row[4]:<10}")
|
||||
|
||||
# 关闭连接
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ 陪练场景表创建和数据插入完成!")
|
||||
print("=" * 60)
|
||||
|
||||
except pymysql.err.OperationalError as e:
|
||||
print(f"\n❌ 数据库连接失败: {e}")
|
||||
print(" 请检查:")
|
||||
print(" 1. MySQL服务是否启动")
|
||||
print(" 2. 数据库配置是否正确")
|
||||
print(" 3. 数据库kaopeilian是否存在")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
170
backend/scripts/create_test_data.py
Normal file
170
backend/scripts/create_test_data.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
创建测试数据脚本
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.core.logger import logger
|
||||
from app.core.security import get_password_hash
|
||||
from app.models.user import Team, User
|
||||
from app.schemas.user import UserCreate
|
||||
from app.services.user_service import UserService
|
||||
|
||||
|
||||
async def create_test_users(db: AsyncSession):
|
||||
"""创建测试用户"""
|
||||
user_service = UserService(db)
|
||||
|
||||
# 测试用户数据
|
||||
test_users = [
|
||||
{
|
||||
"username": "admin",
|
||||
"email": "admin@kaopeilian.com",
|
||||
"password": "admin123",
|
||||
"full_name": "系统管理员",
|
||||
"role": "admin",
|
||||
},
|
||||
{
|
||||
"username": "trainee1",
|
||||
"email": "trainee1@kaopeilian.com",
|
||||
"password": "trainee123",
|
||||
"full_name": "张三",
|
||||
"phone": "13800138001",
|
||||
"role": "trainee",
|
||||
},
|
||||
{
|
||||
"username": "trainee2",
|
||||
"email": "trainee2@kaopeilian.com",
|
||||
"password": "trainee123",
|
||||
"full_name": "李四",
|
||||
"phone": "13800138002",
|
||||
"role": "trainee",
|
||||
},
|
||||
]
|
||||
|
||||
created_users = []
|
||||
for user_data in test_users:
|
||||
# 检查用户是否已存在
|
||||
existing_user = await user_service.get_by_username(user_data["username"])
|
||||
if existing_user:
|
||||
logger.info(f"用户 {user_data['username']} 已存在,跳过创建")
|
||||
created_users.append(existing_user)
|
||||
else:
|
||||
# 创建用户
|
||||
user_create = UserCreate(**user_data)
|
||||
user = await user_service.create_user(obj_in=user_create)
|
||||
created_users.append(user)
|
||||
logger.info(f"创建用户: {user.username} ({user.role})")
|
||||
|
||||
return created_users
|
||||
|
||||
|
||||
async def create_test_teams(db: AsyncSession, users: list[User]):
|
||||
"""创建测试团队"""
|
||||
# 获取管理员
|
||||
admin = next(u for u in users if u.role == "admin")
|
||||
|
||||
# 检查团队是否已存在
|
||||
from sqlalchemy import select
|
||||
result = await db.execute(
|
||||
select(Team).where(Team.code == "TECH")
|
||||
)
|
||||
existing_team = result.scalar_one_or_none()
|
||||
|
||||
if not existing_team:
|
||||
# 创建技术部
|
||||
tech_team = Team(
|
||||
name="技术部",
|
||||
code="TECH",
|
||||
description="负责产品研发和技术支持",
|
||||
team_type="department",
|
||||
leader_id=admin.id,
|
||||
created_by=admin.id,
|
||||
)
|
||||
db.add(tech_team)
|
||||
await db.commit()
|
||||
await db.refresh(tech_team)
|
||||
logger.info(f"创建团队: {tech_team.name}")
|
||||
|
||||
# 创建前端组
|
||||
frontend_team = Team(
|
||||
name="前端开发组",
|
||||
code="TECH-FE",
|
||||
description="负责前端开发",
|
||||
team_type="project",
|
||||
parent_id=tech_team.id,
|
||||
created_by=admin.id,
|
||||
)
|
||||
db.add(frontend_team)
|
||||
|
||||
# 创建后端组
|
||||
backend_team = Team(
|
||||
name="后端开发组",
|
||||
code="TECH-BE",
|
||||
description="负责后端开发",
|
||||
team_type="project",
|
||||
parent_id=tech_team.id,
|
||||
created_by=admin.id,
|
||||
)
|
||||
db.add(backend_team)
|
||||
|
||||
await db.commit()
|
||||
logger.info("创建子团队: 前端开发组、后端开发组")
|
||||
|
||||
# 将用户加入团队
|
||||
user_service = UserService(db)
|
||||
|
||||
# 学员加入子团队
|
||||
for user in users:
|
||||
if user.role == "trainee":
|
||||
if "1" in user.username:
|
||||
await user_service.add_user_to_team(
|
||||
user_id=user.id,
|
||||
team_id=frontend_team.id,
|
||||
role="member"
|
||||
)
|
||||
else:
|
||||
await user_service.add_user_to_team(
|
||||
user_id=user.id,
|
||||
team_id=backend_team.id,
|
||||
role="member"
|
||||
)
|
||||
|
||||
logger.info("用户已加入相应团队")
|
||||
else:
|
||||
logger.info("团队已存在,跳过创建")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
logger.info("开始创建测试数据...")
|
||||
|
||||
# 创建测试用户
|
||||
users = await create_test_users(db)
|
||||
|
||||
# 创建测试团队
|
||||
await create_test_teams(db, users)
|
||||
|
||||
logger.info("测试数据创建完成!")
|
||||
logger.info("\n可用的测试账号:")
|
||||
logger.info("管理员 - 用户名: admin, 密码: admin123")
|
||||
logger.info("学员 - 用户名: trainee1, 密码: trainee123")
|
||||
logger.info("学员 - 用户名: trainee2, 密码: trainee123")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建测试数据失败: {str(e)}")
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
267
backend/scripts/fix_chinese_data.py
Normal file
267
backend/scripts/fix_chinese_data.py
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
修复数据库中的中文乱码问题
|
||||
重新插入正确的中文数据
|
||||
"""
|
||||
import asyncio
|
||||
import aiomysql
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
async def fix_chinese_data():
|
||||
"""修复中文数据"""
|
||||
print("🔧 开始修复数据库中的中文乱码问题...")
|
||||
|
||||
# 正确的课程数据
|
||||
correct_courses = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "皮肤生理学基础",
|
||||
"description": "学习皮肤结构、功能和常见问题,为专业护理打下基础",
|
||||
"category": "technology",
|
||||
"status": "published",
|
||||
"duration_hours": 16.0,
|
||||
"difficulty_level": 2,
|
||||
"tags": ["皮肤学", "基础理论", "必修课"],
|
||||
"sort_order": 100,
|
||||
"is_featured": True
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "医美产品知识与应用",
|
||||
"description": "全面了解各类医美产品的成分、功效和适用人群",
|
||||
"category": "technology",
|
||||
"status": "published",
|
||||
"duration_hours": 20.0,
|
||||
"difficulty_level": 3,
|
||||
"tags": ["产品知识", "医美", "专业技能"],
|
||||
"sort_order": 110,
|
||||
"is_featured": True
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "美容仪器操作与维护",
|
||||
"description": "掌握各类美容仪器的操作方法、注意事项和日常维护",
|
||||
"category": "technology",
|
||||
"status": "published",
|
||||
"duration_hours": 24.0,
|
||||
"difficulty_level": 3,
|
||||
"tags": ["仪器操作", "实操技能", "设备维护"],
|
||||
"sort_order": 120,
|
||||
"is_featured": False
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "医美项目介绍与咨询",
|
||||
"description": "详细了解各类医美项目的原理、效果和适应症",
|
||||
"category": "technology",
|
||||
"status": "published",
|
||||
"duration_hours": 30.0,
|
||||
"difficulty_level": 4,
|
||||
"tags": ["医美项目", "专业咨询", "风险告知"],
|
||||
"sort_order": 170,
|
||||
"is_featured": True
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "轻医美销售技巧",
|
||||
"description": "学习专业的销售话术、客户需求分析和成交技巧",
|
||||
"category": "business",
|
||||
"status": "published",
|
||||
"duration_hours": 16.0,
|
||||
"difficulty_level": 2,
|
||||
"tags": ["销售技巧", "客户沟通", "业绩提升"],
|
||||
"sort_order": 130,
|
||||
"is_featured": True
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "客户服务与投诉处理",
|
||||
"description": "提升服务意识,掌握客户投诉处理的方法和技巧",
|
||||
"category": "business",
|
||||
"status": "published",
|
||||
"duration_hours": 12.0,
|
||||
"difficulty_level": 2,
|
||||
"tags": ["客户服务", "危机处理", "沟通技巧"],
|
||||
"sort_order": 140,
|
||||
"is_featured": False
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "社媒营销与私域运营",
|
||||
"description": "学习如何通过社交媒体进行品牌推广和客户维护",
|
||||
"category": "business",
|
||||
"status": "published",
|
||||
"duration_hours": 16.0,
|
||||
"difficulty_level": 2,
|
||||
"tags": ["社媒营销", "私域流量", "客户维护"],
|
||||
"sort_order": 180,
|
||||
"is_featured": False
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "门店运营管理",
|
||||
"description": "学习门店日常管理、团队建设和业绩管理",
|
||||
"category": "management",
|
||||
"status": "published",
|
||||
"duration_hours": 20.0,
|
||||
"difficulty_level": 3,
|
||||
"tags": ["门店管理", "团队管理", "运营策略"],
|
||||
"sort_order": 160,
|
||||
"is_featured": False
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "卫生消毒与感染控制",
|
||||
"description": "学习医美机构的卫生标准和消毒流程,确保服务安全",
|
||||
"category": "general",
|
||||
"status": "published",
|
||||
"duration_hours": 8.0,
|
||||
"difficulty_level": 1,
|
||||
"tags": ["卫生安全", "消毒规范", "合规管理"],
|
||||
"sort_order": 150,
|
||||
"is_featured": True
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Python编程基础",
|
||||
"description": "Python语言入门课程,适合零基础学员",
|
||||
"category": "technology",
|
||||
"status": "published",
|
||||
"duration_hours": 40.0,
|
||||
"difficulty_level": 2,
|
||||
"tags": ["Python", "编程基础", "入门"],
|
||||
"sort_order": 200,
|
||||
"is_featured": False
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "数据分析基础",
|
||||
"description": "学习数据分析方法和工具,提升数据驱动决策能力",
|
||||
"category": "technology",
|
||||
"status": "published",
|
||||
"duration_hours": 32.0,
|
||||
"difficulty_level": 3,
|
||||
"tags": ["数据分析", "Excel", "可视化"],
|
||||
"sort_order": 210,
|
||||
"is_featured": False
|
||||
}
|
||||
]
|
||||
|
||||
# 正确的岗位数据
|
||||
correct_positions = [
|
||||
{"id": 1, "name": "区域经理", "code": "region_manager", "description": "负责多家门店的运营管理和业绩达成", "status": "active", "skills": ["团队管理", "业绩分析", "战略规划", "客户关系"], "level": "expert", "sort_order": 10},
|
||||
{"id": 2, "name": "店长", "code": "store_manager", "description": "负责门店日常运营管理,团队建设和业绩达成", "status": "active", "skills": ["门店管理", "团队建设", "销售管理", "客户维护"], "level": "senior", "sort_order": 20},
|
||||
{"id": 3, "name": "美容顾问", "code": "beauty_consultant", "description": "为客户提供专业的美容咨询和方案设计", "status": "active", "skills": ["产品知识", "销售技巧", "方案设计", "客户沟通"], "level": "intermediate", "sort_order": 30},
|
||||
{"id": 4, "name": "医美咨询师", "code": "medical_beauty_consultant", "description": "提供医疗美容项目咨询和方案制定", "status": "active", "skills": ["医美知识", "风险评估", "方案设计", "合规意识"], "level": "senior", "sort_order": 35},
|
||||
{"id": 5, "name": "美容技师", "code": "beauty_therapist", "description": "为客户提供专业的美容护理服务", "status": "active", "skills": ["护肤技术", "仪器操作", "手法技巧", "服务意识"], "level": "intermediate", "sort_order": 40},
|
||||
{"id": 6, "name": "护士", "code": "nurse", "description": "协助医生进行医美项目操作,负责术后护理", "status": "active", "skills": ["护理技术", "无菌操作", "应急处理", "医疗知识"], "level": "intermediate", "sort_order": 45},
|
||||
{"id": 7, "name": "前台接待", "code": "receptionist", "description": "负责客户接待、预约管理和前台事务", "status": "active", "skills": ["接待礼仪", "沟通能力", "信息管理", "服务意识"], "level": "junior", "sort_order": 50},
|
||||
{"id": 8, "name": "市场专员", "code": "marketing_specialist", "description": "负责门店营销活动策划和执行", "status": "active", "skills": ["活动策划", "社媒运营", "数据分析", "创意设计"], "level": "intermediate", "sort_order": 60}
|
||||
]
|
||||
|
||||
# 正确的用户数据
|
||||
correct_users = [
|
||||
{"id": 1, "username": "superadmin", "full_name": "超级管理员"},
|
||||
{"id": 2, "username": "admin", "full_name": "系统管理员"},
|
||||
{"id": 3, "username": "testuser", "full_name": "测试学员"}
|
||||
]
|
||||
|
||||
try:
|
||||
# 连接数据库
|
||||
conn = await aiomysql.connect(
|
||||
host="localhost",
|
||||
port=3306,
|
||||
user="root",
|
||||
password="root",
|
||||
db="kaopeilian",
|
||||
charset="utf8mb4",
|
||||
use_unicode=True,
|
||||
init_command="SET character_set_client=utf8mb4, character_set_connection=utf8mb4, character_set_results=utf8mb4"
|
||||
)
|
||||
cursor = await conn.cursor()
|
||||
|
||||
print("✅ 数据库连接成功")
|
||||
|
||||
# 开始事务
|
||||
await cursor.execute("START TRANSACTION")
|
||||
|
||||
# 更新课程数据
|
||||
print("\n📚 更新课程数据...")
|
||||
for course in correct_courses:
|
||||
sql = """
|
||||
UPDATE courses SET
|
||||
name = %s,
|
||||
description = %s,
|
||||
tags = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
"""
|
||||
tags_json = str(course["tags"]).replace("'", '"')
|
||||
await cursor.execute(sql, (course["name"], course["description"], tags_json, course["id"]))
|
||||
print(f" ✅ 更新课程: {course['name']}")
|
||||
|
||||
# 更新岗位数据
|
||||
print("\n👥 更新岗位数据...")
|
||||
for position in correct_positions:
|
||||
sql = """
|
||||
UPDATE positions SET
|
||||
name = %s,
|
||||
description = %s,
|
||||
skills = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
"""
|
||||
skills_json = str(position["skills"]).replace("'", '"')
|
||||
await cursor.execute(sql, (position["name"], position["description"], skills_json, position["id"]))
|
||||
print(f" ✅ 更新岗位: {position['name']}")
|
||||
|
||||
# 更新用户数据
|
||||
print("\n👤 更新用户数据...")
|
||||
for user in correct_users:
|
||||
sql = """
|
||||
UPDATE users SET
|
||||
full_name = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
"""
|
||||
await cursor.execute(sql, (user["full_name"], user["id"]))
|
||||
print(f" ✅ 更新用户: {user['full_name']}")
|
||||
|
||||
# 提交事务
|
||||
await cursor.execute("COMMIT")
|
||||
print("\n✅ 所有数据更新完成!")
|
||||
|
||||
# 验证更新结果
|
||||
print("\n🔍 验证更新结果...")
|
||||
await cursor.execute("SELECT id, name FROM courses LIMIT 3")
|
||||
courses = await cursor.fetchall()
|
||||
for course_id, course_name in courses:
|
||||
print(f" 课程 {course_id}: {course_name}")
|
||||
|
||||
await cursor.execute("SELECT id, name FROM positions LIMIT 3")
|
||||
positions = await cursor.fetchall()
|
||||
for pos_id, pos_name in positions:
|
||||
print(f" 岗位 {pos_id}: {pos_name}")
|
||||
|
||||
await cursor.execute("SELECT id, full_name FROM users LIMIT 3")
|
||||
users = await cursor.fetchall()
|
||||
for user_id, user_name in users:
|
||||
print(f" 用户 {user_id}: {user_name}")
|
||||
|
||||
await cursor.close()
|
||||
conn.close()
|
||||
|
||||
print("\n🎉 中文乱码修复完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 修复失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(fix_chinese_data())
|
||||
606
backend/scripts/init_database_unified.sql
Normal file
606
backend/scripts/init_database_unified.sql
Normal file
@@ -0,0 +1,606 @@
|
||||
-- ============================================
|
||||
-- 考培练系统数据库初始化脚本
|
||||
-- 版本:1.0.0
|
||||
-- 更新时间:2024-12
|
||||
-- ============================================
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS `kaopeilian` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE `kaopeilian`;
|
||||
|
||||
-- ============================================
|
||||
-- 一、用户管理模块
|
||||
-- ============================================
|
||||
|
||||
-- 1.1 用户表
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||
`email` VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
|
||||
`phone` VARCHAR(20) UNIQUE COMMENT '手机号',
|
||||
`password_hash` VARCHAR(200) NOT NULL COMMENT '密码哈希',
|
||||
`full_name` VARCHAR(100) COMMENT '姓名',
|
||||
`gender` VARCHAR(10) COMMENT '性别: male/female',
|
||||
`avatar_url` VARCHAR(500) COMMENT '头像URL',
|
||||
`bio` TEXT COMMENT '个人简介',
|
||||
`school` VARCHAR(100) COMMENT '学校',
|
||||
`major` VARCHAR(100) COMMENT '专业',
|
||||
`role` VARCHAR(20) DEFAULT 'trainee' COMMENT '系统角色: admin, manager, trainee',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否激活',
|
||||
`is_verified` BOOLEAN DEFAULT FALSE COMMENT '是否验证',
|
||||
`last_login_at` DATETIME COMMENT '最后登录时间',
|
||||
`password_changed_at` DATETIME COMMENT '密码修改时间',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_is_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- 1.2 团队表
|
||||
CREATE TABLE IF NOT EXISTS `teams` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(100) NOT NULL UNIQUE COMMENT '团队名称',
|
||||
`code` VARCHAR(50) NOT NULL UNIQUE COMMENT '团队代码',
|
||||
`description` TEXT COMMENT '团队描述',
|
||||
`team_type` VARCHAR(50) DEFAULT 'department' COMMENT '团队类型: department, project, study_group',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否激活',
|
||||
`leader_id` INT COMMENT '负责人ID',
|
||||
`parent_id` INT COMMENT '父团队ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (leader_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (parent_id) REFERENCES teams(id) ON DELETE CASCADE,
|
||||
INDEX idx_team_type (team_type),
|
||||
INDEX idx_is_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队表';
|
||||
|
||||
-- 1.3 用户团队关联表
|
||||
CREATE TABLE IF NOT EXISTS `user_teams` (
|
||||
`user_id` INT NOT NULL,
|
||||
`team_id` INT NOT NULL,
|
||||
`role` VARCHAR(50) DEFAULT 'member' COMMENT '团队角色: member, leader',
|
||||
`joined_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
|
||||
PRIMARY KEY (user_id, team_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户团队关联表';
|
||||
|
||||
-- ============================================
|
||||
-- 二、组织与岗位管理模块
|
||||
-- ============================================
|
||||
|
||||
-- 2.0 岗位表
|
||||
CREATE TABLE IF NOT EXISTS `positions` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '岗位名称',
|
||||
`code` VARCHAR(100) NOT NULL UNIQUE COMMENT '岗位编码',
|
||||
`description` TEXT COMMENT '岗位描述',
|
||||
`parent_id` INT NULL COMMENT '上级岗位ID',
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive',
|
||||
`skills` JSON NULL COMMENT '核心技能',
|
||||
`level` VARCHAR(20) NULL COMMENT '岗位等级: junior/intermediate/senior/expert',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序',
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME NULL COMMENT '删除时间',
|
||||
`created_by` INT NULL COMMENT '创建人ID',
|
||||
`updated_by` INT NULL COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES positions(id) ON DELETE SET NULL,
|
||||
INDEX idx_positions_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='岗位表';
|
||||
|
||||
-- 插入轻医美连锁岗位(按层级关系)
|
||||
-- 注意:需要按顺序插入以正确设置parent_id
|
||||
|
||||
-- 第一层:区域经理
|
||||
INSERT INTO positions (name, code, description, status, skills, level, sort_order) VALUES
|
||||
('区域经理', 'region_manager', '负责多家门店的运营管理和业绩达成', 'active', '["团队管理", "业绩分析", "战略规划", "客户关系"]', 'expert', 10);
|
||||
|
||||
-- 获取区域经理ID
|
||||
SET @region_manager_id = LAST_INSERT_ID();
|
||||
|
||||
-- 第二层:店长
|
||||
INSERT INTO positions (name, code, description, parent_id, status, skills, level, sort_order) VALUES
|
||||
('店长', 'store_manager', '负责门店日常运营管理,团队建设和业绩达成', @region_manager_id, 'active', '["门店管理", "团队建设", "销售管理", "客户维护"]', 'senior', 20);
|
||||
|
||||
-- 获取店长ID
|
||||
SET @store_manager_id = LAST_INSERT_ID();
|
||||
|
||||
-- 第三层:各职能岗位
|
||||
INSERT INTO positions (name, code, description, parent_id, status, skills, level, sort_order) VALUES
|
||||
('美容顾问', 'beauty_consultant', '为客户提供专业的美容咨询和方案设计', @store_manager_id, 'active', '["产品知识", "销售技巧", "方案设计", "客户沟通"]', 'intermediate', 30),
|
||||
('医美咨询师', 'medical_beauty_consultant', '提供医疗美容项目咨询和方案制定', @store_manager_id, 'active', '["医美知识", "风险评估", "方案设计", "合规意识"]', 'senior', 35),
|
||||
('美容技师', 'beauty_therapist', '为客户提供专业的美容护理服务', @store_manager_id, 'active', '["护肤技术", "仪器操作", "手法技巧", "服务意识"]', 'intermediate', 40),
|
||||
('护士', 'nurse', '协助医生进行医美项目操作,负责术后护理', @store_manager_id, 'active', '["护理技术", "无菌操作", "应急处理", "医疗知识"]', 'intermediate', 45),
|
||||
('前台接待', 'receptionist', '负责客户接待、预约管理和前台事务', @store_manager_id, 'active', '["接待礼仪", "沟通能力", "信息管理", "服务意识"]', 'junior', 50),
|
||||
('市场专员', 'marketing_specialist', '负责门店营销活动策划和执行', @store_manager_id, 'active', '["活动策划", "社媒运营", "数据分析", "创意设计"]', 'intermediate', 60);
|
||||
|
||||
-- ============================================
|
||||
-- 二、课程管理模块
|
||||
-- ============================================
|
||||
|
||||
-- 2.1 课程表
|
||||
CREATE TABLE IF NOT EXISTS `courses` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '课程名称',
|
||||
`description` TEXT COMMENT '课程描述',
|
||||
`category` ENUM('technology', 'management', 'business', 'general') DEFAULT 'general' COMMENT '课程分类',
|
||||
`status` ENUM('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '课程状态',
|
||||
`cover_image` VARCHAR(500) COMMENT '封面图片URL',
|
||||
`duration_hours` FLOAT COMMENT '课程时长(小时)',
|
||||
`difficulty_level` INT COMMENT '难度等级(1-5)',
|
||||
`tags` JSON COMMENT '标签列表',
|
||||
`published_at` DATETIME COMMENT '发布时间',
|
||||
`publisher_id` INT COMMENT '发布人ID',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序顺序',
|
||||
`is_featured` BOOLEAN DEFAULT FALSE COMMENT '是否推荐',
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME COMMENT '删除时间',
|
||||
`created_by` INT COMMENT '创建人ID',
|
||||
`updated_by` INT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_is_featured (is_featured),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
INDEX idx_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程表';
|
||||
|
||||
-- 2.2 课程资料表
|
||||
CREATE TABLE IF NOT EXISTS `course_materials` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`course_id` INT NOT NULL COMMENT '课程ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '资料名称',
|
||||
`description` TEXT COMMENT '资料描述',
|
||||
`file_url` VARCHAR(500) NOT NULL COMMENT '文件URL',
|
||||
`file_type` VARCHAR(50) NOT NULL COMMENT '文件类型',
|
||||
`file_size` INT NOT NULL COMMENT '文件大小(字节)',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序顺序',
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME COMMENT '删除时间',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
|
||||
INDEX idx_course_id (course_id),
|
||||
INDEX idx_is_deleted (is_deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程资料表';
|
||||
|
||||
-- 2.3 知识点表
|
||||
CREATE TABLE IF NOT EXISTS `knowledge_points` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`course_id` INT NOT NULL COMMENT '课程ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '知识点名称',
|
||||
`description` TEXT COMMENT '知识点描述',
|
||||
`parent_id` INT COMMENT '父知识点ID',
|
||||
`level` INT DEFAULT 1 COMMENT '层级深度',
|
||||
`path` VARCHAR(500) COMMENT '路径(如: 1.2.3)',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序顺序',
|
||||
`weight` FLOAT DEFAULT 1.0 COMMENT '权重',
|
||||
`is_required` BOOLEAN DEFAULT TRUE COMMENT '是否必修',
|
||||
`estimated_hours` FLOAT COMMENT '预计学习时间(小时)',
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME COMMENT '删除时间',
|
||||
`created_by` INT NULL COMMENT '创建人ID',
|
||||
`updated_by` INT NULL COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parent_id) REFERENCES knowledge_points(id) ON DELETE CASCADE,
|
||||
INDEX idx_course_id (course_id),
|
||||
INDEX idx_parent_id (parent_id),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
INDEX idx_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='知识点表';
|
||||
|
||||
-- 2.4 成长路径表
|
||||
CREATE TABLE IF NOT EXISTS `growth_paths` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '路径名称',
|
||||
`description` TEXT COMMENT '路径描述',
|
||||
`target_role` VARCHAR(100) COMMENT '目标角色',
|
||||
`courses` JSON COMMENT '课程列表[{course_id, order, is_required}]',
|
||||
`estimated_duration_days` INT COMMENT '预计完成天数',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否启用',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序顺序',
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME COMMENT '删除时间',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_is_active (is_active),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
INDEX idx_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='成长路径表';
|
||||
|
||||
-- 资料知识点关联表
|
||||
CREATE TABLE `material_knowledge_points` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`material_id` INT NOT NULL COMMENT '资料ID',
|
||||
`knowledge_point_id` INT NOT NULL COMMENT '知识点ID',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序顺序',
|
||||
`is_ai_generated` BOOLEAN DEFAULT FALSE COMMENT '是否AI生成',
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME COMMENT '删除时间',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `idx_material_knowledge` (`material_id`, `knowledge_point_id`),
|
||||
FOREIGN KEY (material_id) REFERENCES course_materials(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (knowledge_point_id) REFERENCES knowledge_points(id) ON DELETE CASCADE,
|
||||
INDEX idx_material_id (material_id),
|
||||
INDEX idx_knowledge_point_id (knowledge_point_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资料知识点关联表';
|
||||
|
||||
-- ============================================
|
||||
-- 三、考试模块
|
||||
-- ============================================
|
||||
|
||||
-- 3.1 题目表
|
||||
CREATE TABLE IF NOT EXISTS `questions` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`course_id` INT NOT NULL COMMENT '课程ID',
|
||||
`question_type` VARCHAR(20) NOT NULL COMMENT '题目类型: single_choice, multiple_choice, true_false, fill_blank, essay',
|
||||
`title` TEXT NOT NULL COMMENT '题目标题',
|
||||
`content` TEXT COMMENT '题目内容',
|
||||
`options` JSON COMMENT '选项(JSON格式)',
|
||||
`correct_answer` TEXT NOT NULL COMMENT '正确答案',
|
||||
`explanation` TEXT COMMENT '答案解释',
|
||||
`score` FLOAT DEFAULT 10.0 COMMENT '分值',
|
||||
`difficulty` VARCHAR(10) DEFAULT 'medium' COMMENT '难度等级: easy, medium, hard',
|
||||
`tags` JSON COMMENT '标签(JSON格式)',
|
||||
`usage_count` INT DEFAULT 0 COMMENT '使用次数',
|
||||
`correct_count` INT DEFAULT 0 COMMENT '答对次数',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否启用',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
|
||||
INDEX idx_course_id (course_id),
|
||||
INDEX idx_question_type (question_type),
|
||||
INDEX idx_difficulty (difficulty),
|
||||
INDEX idx_is_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='题目表';
|
||||
|
||||
-- 3.2 考试记录表
|
||||
CREATE TABLE IF NOT EXISTS `exams` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||
`course_id` INT NOT NULL COMMENT '课程ID',
|
||||
`exam_name` VARCHAR(255) NOT NULL COMMENT '考试名称',
|
||||
`question_count` INT DEFAULT 10 COMMENT '题目数量',
|
||||
`total_score` FLOAT DEFAULT 100.0 COMMENT '总分',
|
||||
`pass_score` FLOAT DEFAULT 60.0 COMMENT '及格分',
|
||||
`start_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间',
|
||||
`end_time` DATETIME COMMENT '结束时间',
|
||||
`duration_minutes` INT DEFAULT 60 COMMENT '考试时长(分钟)',
|
||||
`score` FLOAT COMMENT '得分',
|
||||
`is_passed` BOOLEAN COMMENT '是否通过',
|
||||
`status` VARCHAR(20) DEFAULT 'started' COMMENT '状态: started, submitted, timeout',
|
||||
`questions` JSON COMMENT '题目数据(JSON格式)',
|
||||
`answers` JSON COMMENT '答案数据(JSON格式)',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_course_id (course_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='考试记录表';
|
||||
|
||||
-- 3.3 考试结果详情表
|
||||
CREATE TABLE IF NOT EXISTS `exam_results` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`exam_id` INT NOT NULL COMMENT '考试ID',
|
||||
`question_id` INT NOT NULL COMMENT '题目ID',
|
||||
`user_answer` TEXT COMMENT '用户答案',
|
||||
`is_correct` BOOLEAN DEFAULT FALSE COMMENT '是否正确',
|
||||
`score` FLOAT DEFAULT 0.0 COMMENT '得分',
|
||||
`answer_time` INT COMMENT '答题时长(秒)',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE,
|
||||
INDEX idx_exam_id (exam_id),
|
||||
INDEX idx_question_id (question_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='考试结果详情表';
|
||||
|
||||
-- ============================================
|
||||
-- 四、陪练模块
|
||||
-- ============================================
|
||||
|
||||
-- 4.1 陪练场景表
|
||||
CREATE TABLE IF NOT EXISTS `training_scenes` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '场景名称',
|
||||
`description` TEXT COMMENT '场景描述',
|
||||
`category` VARCHAR(50) NOT NULL COMMENT '场景分类',
|
||||
`ai_config` JSON COMMENT 'AI配置(如Coze Bot ID等)',
|
||||
`prompt_template` TEXT COMMENT '提示词模板',
|
||||
`evaluation_criteria` JSON COMMENT '评估标准',
|
||||
`status` ENUM('DRAFT', 'ACTIVE', 'INACTIVE') DEFAULT 'DRAFT' COMMENT '场景状态',
|
||||
`is_public` BOOLEAN DEFAULT TRUE COMMENT '是否公开',
|
||||
`required_level` INT COMMENT '所需用户等级',
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
`deleted_at` DATETIME COMMENT '删除时间',
|
||||
`created_by` BIGINT COMMENT '创建人ID',
|
||||
`updated_by` BIGINT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_is_public (is_public),
|
||||
INDEX idx_is_deleted (is_deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练场景表';
|
||||
|
||||
-- 4.2 陪练会话表
|
||||
CREATE TABLE IF NOT EXISTS `training_sessions` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||
`scene_id` INT NOT NULL COMMENT '场景ID',
|
||||
`coze_conversation_id` VARCHAR(100) COMMENT 'Coze会话ID',
|
||||
`start_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间',
|
||||
`end_time` DATETIME COMMENT '结束时间',
|
||||
`duration_seconds` INT COMMENT '持续时长(秒)',
|
||||
`status` ENUM('CREATED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'ERROR') DEFAULT 'CREATED' COMMENT '会话状态',
|
||||
`session_config` JSON COMMENT '会话配置',
|
||||
`total_score` FLOAT COMMENT '总分',
|
||||
`evaluation_result` JSON COMMENT '评估结果详情',
|
||||
`created_by` BIGINT COMMENT '创建人ID',
|
||||
`updated_by` BIGINT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (scene_id) REFERENCES training_scenes(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_scene_id (scene_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练会话表';
|
||||
|
||||
-- 4.3 陪练消息表
|
||||
CREATE TABLE IF NOT EXISTS `training_messages` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` INT NOT NULL COMMENT '会话ID',
|
||||
`role` ENUM('USER', 'ASSISTANT', 'SYSTEM') NOT NULL COMMENT '消息角色',
|
||||
`type` ENUM('TEXT', 'VOICE', 'SYSTEM') NOT NULL COMMENT '消息类型',
|
||||
`content` TEXT NOT NULL COMMENT '消息内容',
|
||||
`voice_url` VARCHAR(500) COMMENT '语音文件URL',
|
||||
`voice_duration` FLOAT COMMENT '语音时长(秒)',
|
||||
`message_metadata` JSON COMMENT '消息元数据',
|
||||
`coze_message_id` VARCHAR(100) COMMENT 'Coze消息ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (session_id) REFERENCES training_sessions(id) ON DELETE CASCADE,
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_role (role)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练消息表';
|
||||
|
||||
-- 4.4 陪练报告表
|
||||
CREATE TABLE IF NOT EXISTS `training_reports` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`session_id` INT NOT NULL UNIQUE COMMENT '会话ID',
|
||||
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||
`overall_score` FLOAT NOT NULL COMMENT '总体得分',
|
||||
`dimension_scores` JSON NOT NULL COMMENT '各维度得分',
|
||||
`strengths` JSON NOT NULL COMMENT '优势点',
|
||||
`weaknesses` JSON NOT NULL COMMENT '待改进点',
|
||||
`suggestions` JSON NOT NULL COMMENT '改进建议',
|
||||
`detailed_analysis` TEXT COMMENT '详细分析',
|
||||
`transcript` TEXT COMMENT '对话文本记录',
|
||||
`statistics` JSON COMMENT '统计数据',
|
||||
`created_by` BIGINT COMMENT '创建人ID',
|
||||
`updated_by` BIGINT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (session_id) REFERENCES training_sessions(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练报告表';
|
||||
|
||||
-- ============================================
|
||||
-- 五、初始测试数据
|
||||
-- ============================================
|
||||
|
||||
-- 插入测试用户
|
||||
INSERT INTO users (username, email, phone, password_hash, full_name, role, is_active, is_verified) VALUES
|
||||
('superadmin', 'superadmin@kaopeilian.com', '13800000001', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '超级管理员', 'admin', TRUE, TRUE),
|
||||
('admin', 'admin@kaopeilian.com', '13800000002', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '系统管理员', 'admin', TRUE, TRUE),
|
||||
('testuser', 'testuser@kaopeilian.com', '13800000003', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '测试学员', 'trainee', TRUE, TRUE);
|
||||
|
||||
-- 插入测试团队
|
||||
INSERT INTO teams (name, code, description, team_type, leader_id) VALUES
|
||||
('技术部', 'TECH', '负责技术研发和维护', 'department', 2),
|
||||
('产品部', 'PROD', '负责产品设计和规划', 'department', 2),
|
||||
('Python学习小组', 'PY_GROUP', 'Python技术学习交流', 'study_group', 2);
|
||||
|
||||
-- 插入用户团队关联
|
||||
INSERT INTO user_teams (user_id, team_id, role) VALUES
|
||||
(2, 1, 'leader'),
|
||||
(2, 2, 'leader'),
|
||||
(3, 1, 'member'),
|
||||
(3, 3, 'member');
|
||||
|
||||
-- 插入轻医美相关课程
|
||||
INSERT INTO courses (name, description, category, status, duration_hours, difficulty_level, tags, is_featured, sort_order, published_at) VALUES
|
||||
-- 技术类课程
|
||||
('皮肤生理学基础', '学习皮肤结构、功能和常见问题,为专业护理打下基础', 'technology', 'published', 16, 2, '["皮肤学", "基础理论", "必修课"]', TRUE, 100, NOW()),
|
||||
('医美产品知识与应用', '全面了解各类医美产品的成分、功效和适用人群', 'technology', 'published', 20, 3, '["产品知识", "医美", "专业技能"]', TRUE, 110, NOW()),
|
||||
('美容仪器操作与维护', '掌握各类美容仪器的操作方法、注意事项和日常维护', 'technology', 'published', 24, 3, '["仪器操作", "实操技能", "设备维护"]', FALSE, 120, NOW()),
|
||||
('医美项目介绍与咨询', '详细了解各类医美项目的原理、效果和适应症', 'technology', 'published', 30, 4, '["医美项目", "专业咨询", "风险告知"]', TRUE, 170, NOW()),
|
||||
|
||||
-- 业务类课程
|
||||
('轻医美销售技巧', '学习专业的销售话术、客户需求分析和成交技巧', 'business', 'published', 16, 2, '["销售技巧", "客户沟通", "业绩提升"]', TRUE, 130, NOW()),
|
||||
('客户服务与投诉处理', '提升服务意识,掌握客户投诉处理的方法和技巧', 'business', 'published', 12, 2, '["客户服务", "危机处理", "沟通技巧"]', FALSE, 140, NOW()),
|
||||
('社媒营销与私域运营', '学习如何通过社交媒体进行品牌推广和客户维护', 'business', 'published', 16, 2, '["社媒营销", "私域流量", "客户维护"]', FALSE, 180, NOW()),
|
||||
|
||||
-- 管理类课程
|
||||
('门店运营管理', '学习门店日常管理、团队建设和业绩管理', 'management', 'published', 20, 3, '["门店管理", "团队管理", "运营策略"]', FALSE, 160, NOW()),
|
||||
|
||||
-- 通用类课程
|
||||
('卫生消毒与感染控制', '学习医美机构的卫生标准和消毒流程,确保服务安全', 'general', 'published', 8, 1, '["卫生安全", "消毒规范", "合规管理"]', TRUE, 150, NOW()),
|
||||
|
||||
-- 保留原有的技术课程作为选修参考
|
||||
('Python编程基础', 'Python语言入门课程,适合零基础学员', 'technology', 'published', 40, 2, '["Python", "编程基础", "入门"]', FALSE, 200, NOW()),
|
||||
('数据分析基础', '学习数据分析方法和工具,提升数据驱动决策能力', 'technology', 'published', 32, 3, '["数据分析", "Excel", "可视化"]', FALSE, 210, NOW());
|
||||
|
||||
-- 为第一个课程添加资料
|
||||
INSERT INTO course_materials (course_id, name, description, file_url, file_type, file_size) VALUES
|
||||
(1, 'Python基础教程.pdf', 'Python编程基础教程文档', '/uploads/python-basics.pdf', 'pdf', 2048000),
|
||||
(1, '课程视频1', '第一章节视频教程', '/uploads/video1.mp4', 'mp4', 104857600);
|
||||
|
||||
-- 为第一个课程添加知识点
|
||||
INSERT INTO knowledge_points (course_id, name, description, parent_id, level, weight, estimated_hours) VALUES
|
||||
(1, 'Python环境搭建', '学习如何安装和配置Python开发环境', NULL, 1, 1.0, 2),
|
||||
(1, 'Python基础语法', '学习Python的基本语法规则', NULL, 1, 2.0, 8),
|
||||
(1, '变量和数据类型', '了解Python中的变量和基本数据类型', 2, 2, 1.5, 3),
|
||||
(1, '控制流程', '学习条件语句和循环结构', 2, 2, 1.5, 4);
|
||||
|
||||
-- 插入测试题目
|
||||
INSERT INTO questions (course_id, question_type, title, content, options, correct_answer, explanation, score, difficulty, tags) VALUES
|
||||
(1, 'single_choice', 'Python中哪个关键字用于定义函数?', NULL, '{"A": "def", "B": "function", "C": "fun", "D": "define"}', 'A', 'Python使用def关键字来定义函数', 10.0, 'easy', '["python", "基础", "函数"]'),
|
||||
(1, 'single_choice', 'Python中列表和元组的主要区别是什么?', NULL, '{"A": "列表是有序的,元组是无序的", "B": "列表可变,元组不可变", "C": "列表只能存储数字,元组可以存储任何类型", "D": "没有区别"}', 'B', '列表是可变的(mutable),而元组是不可变的(immutable)', 10.0, 'medium', '["python", "数据结构"]'),
|
||||
(1, 'true_false', 'Python是一种编译型语言', NULL, NULL, 'false', 'Python是一种解释型语言,不需要编译成机器码', 10.0, 'easy', '["python", "基础"]'),
|
||||
(1, 'fill_blank', 'Python中使用____关键字定义类', NULL, NULL, 'class', '使用class关键字定义类', 10.0, 'easy', '["python", "面向对象"]');
|
||||
|
||||
-- 插入陪练场景
|
||||
INSERT INTO training_scenes (name, description, category, ai_config, status, is_public) VALUES
|
||||
('Python编程助手', '帮助学员解决Python编程问题', '技术辅导', '{"bot_id": "python_assistant_bot"}', 'active', TRUE),
|
||||
('面试模拟', '模拟技术面试场景', '职业发展', '{"bot_id": "interview_simulator_bot"}', 'active', TRUE),
|
||||
('项目讨论', '项目方案讨论和优化', '项目管理', '{"bot_id": "project_discussion_bot"}', 'draft', TRUE);
|
||||
|
||||
-- 插入成长路径
|
||||
INSERT INTO growth_paths (name, description, target_role, courses, estimated_duration_days) VALUES
|
||||
('Python工程师成长路径', '从入门到精通的Python学习路径', 'Python开发工程师', '[{"course_id": 1, "order": 1, "is_required": true}, {"course_id": 4, "order": 2, "is_required": true}]', 90),
|
||||
('技术管理者路径', '技术人员转型管理岗位', '技术经理', '[{"course_id": 2, "order": 1, "is_required": true}, {"course_id": 3, "order": 2, "is_required": false}]', 60);
|
||||
|
||||
-- ============================================
|
||||
-- 六、创建视图(可选)
|
||||
-- ============================================
|
||||
|
||||
-- 用户课程进度视图
|
||||
CREATE OR REPLACE VIEW v_user_course_progress AS
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
u.username,
|
||||
c.id as course_id,
|
||||
c.name as course_name,
|
||||
COUNT(DISTINCT e.id) as exam_count,
|
||||
AVG(e.score) as avg_score,
|
||||
MAX(e.score) as best_score
|
||||
FROM users u
|
||||
CROSS JOIN courses c
|
||||
LEFT JOIN exams e ON e.user_id = u.id AND e.course_id = c.id AND e.status = 'submitted'
|
||||
GROUP BY u.id, c.id;
|
||||
|
||||
-- ============================================
|
||||
-- 七、岗位成员和课程关联表
|
||||
-- ============================================
|
||||
|
||||
-- 创建岗位成员关联表
|
||||
CREATE TABLE IF NOT EXISTS position_members (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
position_id INT NOT NULL COMMENT '岗位ID',
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
role VARCHAR(50) COMMENT '成员角色(预留字段)',
|
||||
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by INT COMMENT '创建人ID',
|
||||
updated_by INT COMMENT '更新人ID',
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted_at DATETIME,
|
||||
FOREIGN KEY (position_id) REFERENCES positions(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
UNIQUE KEY uix_position_user (position_id, user_id, is_deleted),
|
||||
INDEX ix_position_members_id (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='岗位成员关联表';
|
||||
|
||||
-- 创建岗位课程关联表
|
||||
CREATE TABLE IF NOT EXISTS position_courses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
position_id INT NOT NULL COMMENT '岗位ID',
|
||||
course_id INT NOT NULL COMMENT '课程ID',
|
||||
course_type VARCHAR(20) NOT NULL DEFAULT 'required' COMMENT '课程类型',
|
||||
priority INT DEFAULT 0 COMMENT '优先级/排序',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by INT COMMENT '创建人ID',
|
||||
updated_by INT COMMENT '更新人ID',
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted_at DATETIME,
|
||||
FOREIGN KEY (position_id) REFERENCES positions(id),
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id),
|
||||
UNIQUE KEY uix_position_course (position_id, course_id, is_deleted),
|
||||
INDEX ix_position_courses_id (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='岗位课程关联表';
|
||||
|
||||
-- 创建课程考试设置表
|
||||
CREATE TABLE IF NOT EXISTS course_exam_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
course_id INT NOT NULL UNIQUE COMMENT '课程ID',
|
||||
single_choice_count INT NOT NULL DEFAULT 10 COMMENT '单选题数量',
|
||||
multiple_choice_count INT NOT NULL DEFAULT 5 COMMENT '多选题数量',
|
||||
true_false_count INT NOT NULL DEFAULT 5 COMMENT '判断题数量',
|
||||
fill_blank_count INT NOT NULL DEFAULT 0 COMMENT '填空题数量',
|
||||
duration_minutes INT NOT NULL DEFAULT 60 COMMENT '考试时长(分钟)',
|
||||
difficulty_level INT NOT NULL DEFAULT 3 COMMENT '难度系数(1-5)',
|
||||
passing_score INT NOT NULL DEFAULT 60 COMMENT '及格分数',
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用',
|
||||
show_answer_immediately BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否立即显示答案',
|
||||
allow_retake BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否允许重考',
|
||||
max_retake_times INT COMMENT '最大重考次数',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by INT COMMENT '创建人ID',
|
||||
updated_by INT COMMENT '更新人ID',
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted_at DATETIME,
|
||||
deleted_by INT COMMENT '删除人ID',
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id),
|
||||
INDEX ix_course_exam_settings_id (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程考试设置表';
|
||||
|
||||
-- 插入岗位成员样例数据(使用已存在的用户)
|
||||
INSERT INTO position_members (position_id, user_id) VALUES
|
||||
(2, 2), -- 店长:admin
|
||||
(3, 3); -- 美容顾问:testuser
|
||||
|
||||
-- 插入岗位课程关联数据
|
||||
-- 需要根据实际插入的课程ID来设置,这里使用子查询获取课程ID
|
||||
|
||||
-- 店长必修课程
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 2, id, 'required', 1 FROM courses WHERE name = '门店运营管理' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 2, id, 'required', 2 FROM courses WHERE name = '轻医美销售技巧' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 2, id, 'required', 3 FROM courses WHERE name = '客户服务与投诉处理' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 2, id, 'required', 4 FROM courses WHERE name = '卫生消毒与感染控制' AND is_deleted = FALSE LIMIT 1;
|
||||
|
||||
-- 美容顾问必修课程
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 3, id, 'required', 1 FROM courses WHERE name = '皮肤生理学基础' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 3, id, 'required', 2 FROM courses WHERE name = '医美产品知识与应用' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 3, id, 'required', 3 FROM courses WHERE name = '轻医美销售技巧' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 3, id, 'required', 4 FROM courses WHERE name = '客户服务与投诉处理' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 3, id, 'optional', 5 FROM courses WHERE name = '社媒营销与私域运营' AND is_deleted = FALSE LIMIT 1;
|
||||
|
||||
-- 美容技师必修课程
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 4, id, 'required', 1 FROM courses WHERE name = '皮肤生理学基础' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 4, id, 'required', 2 FROM courses WHERE name = '美容仪器操作与维护' AND is_deleted = FALSE LIMIT 1;
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority)
|
||||
SELECT 4, id, 'required', 3 FROM courses WHERE name = '卫生消毒与感染控制' AND is_deleted = FALSE LIMIT 1;
|
||||
|
||||
-- ============================================
|
||||
-- 八、输出完成信息
|
||||
-- ============================================
|
||||
|
||||
SELECT '数据库初始化完成!' as message;
|
||||
SELECT '默认账号-超级管理员:superadmin / Superadmin123!' as info1;
|
||||
SELECT '默认账号-系统管理员:admin / Admin123!' as info2;
|
||||
SELECT '默认账号-测试学员:testuser / TestPass123!' as info3;
|
||||
64
backend/scripts/init_db.py
Normal file
64
backend/scripts/init_db.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
初始化数据库脚本
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logger import logger
|
||||
|
||||
|
||||
async def create_database():
|
||||
"""创建数据库(如果不存在)"""
|
||||
# 解析数据库URL
|
||||
db_url_parts = settings.DATABASE_URL.split('/')
|
||||
db_name = db_url_parts[-1].split('?')[0]
|
||||
db_url_without_db = '/'.join(db_url_parts[:-1])
|
||||
|
||||
# 连接到MySQL服务器(不指定数据库)
|
||||
engine = create_async_engine(db_url_without_db, echo=True)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
# 检查数据库是否存在
|
||||
result = await conn.execute(
|
||||
text(f"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '{db_name}'")
|
||||
)
|
||||
exists = result.scalar() is not None
|
||||
|
||||
if not exists:
|
||||
# 创建数据库
|
||||
await conn.execute(text(f"CREATE DATABASE IF NOT EXISTS `{db_name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"))
|
||||
logger.info(f"数据库 {db_name} 创建成功")
|
||||
else:
|
||||
logger.info(f"数据库 {db_name} 已存在")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
try:
|
||||
# 创建数据库
|
||||
await create_database()
|
||||
|
||||
# 运行迁移
|
||||
logger.info("开始运行数据库迁移...")
|
||||
os.system("cd /workspace/kaopeilian-backend && alembic upgrade head")
|
||||
|
||||
logger.info("数据库初始化完成!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
113
backend/scripts/init_db.sql
Normal file
113
backend/scripts/init_db.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS kaopeilian CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 课程表
|
||||
CREATE TABLE IF NOT EXISTS courses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL COMMENT '课程名称',
|
||||
description TEXT COMMENT '课程描述',
|
||||
category ENUM('technology', 'management', 'business', 'general') DEFAULT 'general' COMMENT '课程分类',
|
||||
status ENUM('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '课程状态',
|
||||
cover_image VARCHAR(500) COMMENT '封面图片URL',
|
||||
duration_hours FLOAT COMMENT '课程时长(小时)',
|
||||
difficulty_level INT COMMENT '难度等级(1-5)',
|
||||
tags JSON COMMENT '标签列表',
|
||||
published_at DATETIME COMMENT '发布时间',
|
||||
publisher_id INT COMMENT '发布人ID',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序顺序',
|
||||
is_featured BOOLEAN DEFAULT FALSE COMMENT '是否推荐',
|
||||
is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
deleted_at DATETIME COMMENT '删除时间',
|
||||
created_by INT COMMENT '创建人ID',
|
||||
updated_by INT COMMENT '更新人ID',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_is_featured (is_featured),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
INDEX idx_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';
|
||||
|
||||
-- 课程资料表
|
||||
CREATE TABLE IF NOT EXISTS course_materials (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
course_id INT NOT NULL COMMENT '课程ID',
|
||||
name VARCHAR(200) NOT NULL COMMENT '资料名称',
|
||||
description TEXT COMMENT '资料描述',
|
||||
file_url VARCHAR(500) NOT NULL COMMENT '文件URL',
|
||||
file_type VARCHAR(50) NOT NULL COMMENT '文件类型',
|
||||
file_size INT NOT NULL COMMENT '文件大小(字节)',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序顺序',
|
||||
is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
deleted_at DATETIME COMMENT '删除时间',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
|
||||
INDEX idx_course_id (course_id),
|
||||
INDEX idx_is_deleted (is_deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程资料表';
|
||||
|
||||
-- 知识点表
|
||||
CREATE TABLE IF NOT EXISTS knowledge_points (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
course_id INT NOT NULL COMMENT '课程ID',
|
||||
name VARCHAR(200) NOT NULL COMMENT '知识点名称',
|
||||
description TEXT COMMENT '知识点描述',
|
||||
parent_id INT COMMENT '父知识点ID',
|
||||
level INT DEFAULT 1 COMMENT '层级深度',
|
||||
path VARCHAR(500) COMMENT '路径(如: 1.2.3)',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序顺序',
|
||||
weight FLOAT DEFAULT 1.0 COMMENT '权重',
|
||||
is_required BOOLEAN DEFAULT TRUE COMMENT '是否必修',
|
||||
estimated_hours FLOAT COMMENT '预计学习时间(小时)',
|
||||
is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
deleted_at DATETIME COMMENT '删除时间',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parent_id) REFERENCES knowledge_points(id) ON DELETE CASCADE,
|
||||
INDEX idx_course_id (course_id),
|
||||
INDEX idx_parent_id (parent_id),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
INDEX idx_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识点表';
|
||||
|
||||
-- 成长路径表
|
||||
CREATE TABLE IF NOT EXISTS growth_paths (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL COMMENT '路径名称',
|
||||
description TEXT COMMENT '路径描述',
|
||||
target_role VARCHAR(100) COMMENT '目标角色',
|
||||
courses JSON COMMENT '课程列表[{course_id, order, is_required}]',
|
||||
estimated_duration_days INT COMMENT '预计完成天数',
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
|
||||
sort_order INT DEFAULT 0 COMMENT '排序顺序',
|
||||
is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
|
||||
deleted_at DATETIME COMMENT '删除时间',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
INDEX idx_is_active (is_active),
|
||||
INDEX idx_is_deleted (is_deleted),
|
||||
INDEX idx_sort_order (sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长路径表';
|
||||
|
||||
-- 插入测试数据
|
||||
INSERT INTO courses (name, description, category, status, difficulty_level, tags, is_featured) VALUES
|
||||
('Python编程基础', 'Python语言入门课程,适合零基础学员', 'technology', 'published', 2, '["Python", "编程基础", "入门"]', TRUE),
|
||||
('项目管理实战', '学习现代项目管理方法和工具', 'management', 'published', 3, '["项目管理", "敏捷", "Scrum"]', FALSE),
|
||||
('商务沟通技巧', '提升职场沟通能力', 'business', 'draft', 2, '["沟通", "软技能", "职场"]', FALSE);
|
||||
|
||||
-- 为第一个课程添加资料
|
||||
INSERT INTO course_materials (course_id, name, description, file_url, file_type, file_size) VALUES
|
||||
(1, 'Python基础教程.pdf', 'Python编程基础教程文档', '/uploads/python-basics.pdf', 'pdf', 2048000),
|
||||
(1, '课程视频1', '第一章节视频教程', '/uploads/video1.mp4', 'mp4', 104857600);
|
||||
|
||||
-- 为第一个课程添加知识点
|
||||
INSERT INTO knowledge_points (course_id, name, description, parent_id, level, weight, estimated_hours) VALUES
|
||||
(1, 'Python环境搭建', '学习如何安装和配置Python开发环境', NULL, 1, 1.0, 2),
|
||||
(1, 'Python基础语法', '学习Python的基本语法规则', NULL, 1, 2.0, 8),
|
||||
(1, '变量和数据类型', '了解Python中的变量和基本数据类型', 2, 2, 1.5, 3),
|
||||
(1, '控制流程', '学习条件语句和循环结构', 2, 2, 1.5, 4);
|
||||
49
backend/scripts/init_project.sh
Executable file
49
backend/scripts/init_project.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "初始化考培练系统后端项目..."
|
||||
|
||||
# 创建虚拟环境
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "创建虚拟环境..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
|
||||
# 升级pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# 安装依赖
|
||||
echo "安装项目依赖..."
|
||||
pip install -r requirements/dev.txt
|
||||
|
||||
# 复制环境变量文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "创建环境变量文件..."
|
||||
cp .env.example .env
|
||||
echo "请编辑 .env 文件配置必要的环境变量"
|
||||
fi
|
||||
|
||||
# 创建必要的目录
|
||||
echo "创建必要的目录..."
|
||||
mkdir -p logs uploads
|
||||
|
||||
# 初始化pre-commit
|
||||
if command -v pre-commit &> /dev/null; then
|
||||
echo "配置pre-commit hooks..."
|
||||
pre-commit install
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "项目初始化完成!"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo "1. 编辑 .env 文件,配置数据库和AI平台信息"
|
||||
echo "2. 启动MySQL和Redis服务"
|
||||
echo "3. 运行 'alembic init migrations' 初始化数据库迁移"
|
||||
echo "4. 运行 'alembic revision --autogenerate -m \"initial\"' 创建初始迁移"
|
||||
echo "5. 运行 'alembic upgrade head' 应用迁移"
|
||||
echo "6. 运行 'make run-dev' 启动开发服务器"
|
||||
echo ""
|
||||
echo "祝开发顺利! 🚀"
|
||||
394
backend/scripts/kaopeilian_rollback.py
Normal file
394
backend/scripts/kaopeilian_rollback.py
Normal file
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
考培练系统 - 专用数据库回滚工具
|
||||
针对轻医美连锁业务场景的快速回滚方案
|
||||
|
||||
功能:
|
||||
1. 用户数据回滚
|
||||
2. 课程数据回滚
|
||||
3. 考试数据回滚
|
||||
4. 岗位数据回滚
|
||||
5. 基于Binlog的精确回滚
|
||||
|
||||
使用方法:
|
||||
python scripts/kaopeilian_rollback.py --help
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
import aiomysql
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class KaopeilianRollbackTool:
|
||||
"""考培练系统专用回滚工具"""
|
||||
|
||||
def __init__(self):
|
||||
self.host = "localhost"
|
||||
self.port = 3306
|
||||
self.user = "root"
|
||||
self.password = "root"
|
||||
self.database = "kaopeilian"
|
||||
self.connection = None
|
||||
|
||||
async def connect(self):
|
||||
"""连接数据库"""
|
||||
try:
|
||||
self.connection = await aiomysql.connect(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
db=self.database,
|
||||
charset='utf8mb4'
|
||||
)
|
||||
logger.info("✅ 数据库连接成功")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 数据库连接失败: {e}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""关闭连接"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
|
||||
async def get_recent_operations(self, hours: int = 24) -> List[Dict]:
|
||||
"""获取最近的操作记录"""
|
||||
cursor = await self.connection.cursor(aiomysql.DictCursor)
|
||||
|
||||
# 查询最近更新的记录
|
||||
queries = [
|
||||
{
|
||||
'table': 'users',
|
||||
'sql': f"""
|
||||
SELECT id, username, full_name, updated_at, 'user' as type
|
||||
FROM users
|
||||
WHERE updated_at >= DATE_SUB(NOW(), INTERVAL {hours} HOUR)
|
||||
ORDER BY updated_at DESC
|
||||
"""
|
||||
},
|
||||
{
|
||||
'table': 'courses',
|
||||
'sql': f"""
|
||||
SELECT id, name, status, updated_at, 'course' as type
|
||||
FROM courses
|
||||
WHERE updated_at >= DATE_SUB(NOW(), INTERVAL {hours} HOUR)
|
||||
ORDER BY updated_at DESC
|
||||
"""
|
||||
},
|
||||
{
|
||||
'table': 'exams',
|
||||
'sql': f"""
|
||||
SELECT id, user_id, course_id, exam_name, score, updated_at, 'exam' as type
|
||||
FROM exams
|
||||
WHERE updated_at >= DATE_SUB(NOW(), INTERVAL {hours} HOUR)
|
||||
ORDER BY updated_at DESC
|
||||
"""
|
||||
},
|
||||
{
|
||||
'table': 'positions',
|
||||
'sql': f"""
|
||||
SELECT id, name, code, status, updated_at, 'position' as type
|
||||
FROM positions
|
||||
WHERE updated_at >= DATE_SUB(NOW(), INTERVAL {hours} HOUR)
|
||||
ORDER BY updated_at DESC
|
||||
"""
|
||||
}
|
||||
]
|
||||
|
||||
all_operations = []
|
||||
for query in queries:
|
||||
try:
|
||||
await cursor.execute(query['sql'])
|
||||
results = await cursor.fetchall()
|
||||
all_operations.extend(results)
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ 查询 {query['table']} 表失败: {e}")
|
||||
|
||||
await cursor.close()
|
||||
return all_operations
|
||||
|
||||
async def create_data_backup(self, table: str, record_id: int) -> Dict:
|
||||
"""创建单条记录的备份"""
|
||||
cursor = await self.connection.cursor(aiomysql.DictCursor)
|
||||
|
||||
try:
|
||||
await cursor.execute(f"SELECT * FROM {table} WHERE id = %s", (record_id,))
|
||||
record = await cursor.fetchone()
|
||||
|
||||
if record:
|
||||
backup = {
|
||||
'table': table,
|
||||
'record_id': record_id,
|
||||
'data': dict(record),
|
||||
'backup_time': datetime.now().isoformat()
|
||||
}
|
||||
logger.info(f"✅ 已备份 {table} 表记录 ID: {record_id}")
|
||||
return backup
|
||||
else:
|
||||
logger.warning(f"⚠️ 未找到 {table} 表记录 ID: {record_id}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 备份 {table} 表记录失败: {e}")
|
||||
return None
|
||||
finally:
|
||||
await cursor.close()
|
||||
|
||||
async def restore_from_backup(self, backup: Dict) -> bool:
|
||||
"""从备份恢复数据"""
|
||||
if not backup:
|
||||
return False
|
||||
|
||||
cursor = await self.connection.cursor()
|
||||
|
||||
try:
|
||||
# 开始事务
|
||||
await cursor.execute("START TRANSACTION")
|
||||
|
||||
table = backup['table']
|
||||
data = backup['data']
|
||||
record_id = backup['record_id']
|
||||
|
||||
# 构建UPDATE语句
|
||||
set_clauses = []
|
||||
values = []
|
||||
|
||||
for key, value in data.items():
|
||||
if key != 'id': # 跳过主键
|
||||
set_clauses.append(f"`{key}` = %s")
|
||||
values.append(value)
|
||||
|
||||
if set_clauses:
|
||||
sql = f"UPDATE `{table}` SET {', '.join(set_clauses)} WHERE id = %s"
|
||||
values.append(record_id)
|
||||
|
||||
await cursor.execute(sql, values)
|
||||
await cursor.execute("COMMIT")
|
||||
|
||||
logger.info(f"✅ 已恢复 {table} 表记录 ID: {record_id}")
|
||||
return True
|
||||
else:
|
||||
await cursor.execute("ROLLBACK")
|
||||
logger.warning(f"⚠️ 没有可恢复的字段")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
await cursor.execute("ROLLBACK")
|
||||
logger.error(f"❌ 恢复数据失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
await cursor.close()
|
||||
|
||||
async def soft_delete_rollback(self, table: str, record_id: int) -> bool:
|
||||
"""软删除回滚"""
|
||||
cursor = await self.connection.cursor()
|
||||
|
||||
try:
|
||||
# 检查表是否有软删除字段
|
||||
await cursor.execute(f"SHOW COLUMNS FROM {table} LIKE 'is_deleted'")
|
||||
has_soft_delete = await cursor.fetchone()
|
||||
|
||||
if not has_soft_delete:
|
||||
logger.warning(f"⚠️ {table} 表没有软删除字段")
|
||||
return False
|
||||
|
||||
# 恢复软删除的记录
|
||||
await cursor.execute("START TRANSACTION")
|
||||
|
||||
sql = f"""
|
||||
UPDATE `{table}`
|
||||
SET is_deleted = FALSE, deleted_at = NULL
|
||||
WHERE id = %s
|
||||
"""
|
||||
await cursor.execute(sql, (record_id,))
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
await cursor.execute("COMMIT")
|
||||
logger.info(f"✅ 已恢复软删除记录 {table} ID: {record_id}")
|
||||
return True
|
||||
else:
|
||||
await cursor.execute("ROLLBACK")
|
||||
logger.warning(f"⚠️ 未找到要恢复的记录 {table} ID: {record_id}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
await cursor.execute("ROLLBACK")
|
||||
logger.error(f"❌ 软删除回滚失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
await cursor.close()
|
||||
|
||||
async def rollback_user_operation(self, user_id: int, operation_type: str = "update") -> bool:
|
||||
"""回滚用户操作"""
|
||||
logger.info(f"🔄 开始回滚用户操作: ID={user_id}, 类型={operation_type}")
|
||||
|
||||
if operation_type == "delete":
|
||||
return await self.soft_delete_rollback("users", user_id)
|
||||
else:
|
||||
# 对于更新操作,需要从Binlog或其他方式获取原始数据
|
||||
logger.warning("⚠️ 用户更新操作回滚需要手动处理")
|
||||
return False
|
||||
|
||||
async def rollback_course_operation(self, course_id: int, operation_type: str = "update") -> bool:
|
||||
"""回滚课程操作"""
|
||||
logger.info(f"🔄 开始回滚课程操作: ID={course_id}, 类型={operation_type}")
|
||||
|
||||
if operation_type == "delete":
|
||||
return await self.soft_delete_rollback("courses", course_id)
|
||||
else:
|
||||
logger.warning("⚠️ 课程更新操作回滚需要手动处理")
|
||||
return False
|
||||
|
||||
async def rollback_exam_operation(self, exam_id: int) -> bool:
|
||||
"""回滚考试操作"""
|
||||
logger.info(f"🔄 开始回滚考试操作: ID={exam_id}")
|
||||
|
||||
cursor = await self.connection.cursor()
|
||||
|
||||
try:
|
||||
await cursor.execute("START TRANSACTION")
|
||||
|
||||
# 删除考试结果详情
|
||||
await cursor.execute("DELETE FROM exam_results WHERE exam_id = %s", (exam_id,))
|
||||
|
||||
# 删除考试记录
|
||||
await cursor.execute("DELETE FROM exams WHERE id = %s", (exam_id,))
|
||||
|
||||
await cursor.execute("COMMIT")
|
||||
logger.info(f"✅ 已回滚考试记录 ID: {exam_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
await cursor.execute("ROLLBACK")
|
||||
logger.error(f"❌ 考试回滚失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
await cursor.close()
|
||||
|
||||
async def rollback_position_operation(self, position_id: int, operation_type: str = "update") -> bool:
|
||||
"""回滚岗位操作"""
|
||||
logger.info(f"🔄 开始回滚岗位操作: ID={position_id}, 类型={operation_type}")
|
||||
|
||||
if operation_type == "delete":
|
||||
return await self.soft_delete_rollback("positions", position_id)
|
||||
else:
|
||||
logger.warning("⚠️ 岗位更新操作回滚需要手动处理")
|
||||
return False
|
||||
|
||||
async def list_recent_changes(self, hours: int = 24):
|
||||
"""列出最近的变更"""
|
||||
logger.info(f"📋 最近 {hours} 小时的数据变更:")
|
||||
|
||||
operations = await self.get_recent_operations(hours)
|
||||
|
||||
if not operations:
|
||||
logger.info("📝 没有找到最近的变更记录")
|
||||
return
|
||||
|
||||
# 按类型分组显示
|
||||
by_type = {}
|
||||
for op in operations:
|
||||
op_type = op.get('type', 'unknown')
|
||||
if op_type not in by_type:
|
||||
by_type[op_type] = []
|
||||
by_type[op_type].append(op)
|
||||
|
||||
for op_type, ops in by_type.items():
|
||||
print(f"\n🔸 {op_type.upper()} 类型变更 ({len(ops)} 条):")
|
||||
print("-" * 60)
|
||||
|
||||
for op in ops[:10]: # 只显示前10条
|
||||
if op_type == 'user':
|
||||
print(f" ID: {op['id']}, 用户名: {op['username']}, 姓名: {op['full_name']}, 时间: {op['updated_at']}")
|
||||
elif op_type == 'course':
|
||||
print(f" ID: {op['id']}, 课程: {op['name']}, 状态: {op['status']}, 时间: {op['updated_at']}")
|
||||
elif op_type == 'exam':
|
||||
print(f" ID: {op['id']}, 考试: {op['exam_name']}, 分数: {op['score']}, 时间: {op['updated_at']}")
|
||||
elif op_type == 'position':
|
||||
print(f" ID: {op['id']}, 岗位: {op['name']}, 状态: {op['status']}, 时间: {op['updated_at']}")
|
||||
|
||||
if len(ops) > 10:
|
||||
print(f" ... 还有 {len(ops) - 10} 条记录")
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='考培练系统 - 专用数据库回滚工具')
|
||||
parser.add_argument('--list', action='store_true', help='列出最近的变更')
|
||||
parser.add_argument('--hours', type=int, default=24, help='查看最近N小时的变更 (默认24小时)')
|
||||
parser.add_argument('--rollback-user', type=int, help='回滚用户操作 (用户ID)')
|
||||
parser.add_argument('--rollback-course', type=int, help='回滚课程操作 (课程ID)')
|
||||
parser.add_argument('--rollback-exam', type=int, help='回滚考试操作 (考试ID)')
|
||||
parser.add_argument('--rollback-position', type=int, help='回滚岗位操作 (岗位ID)')
|
||||
parser.add_argument('--operation-type', choices=['update', 'delete'], default='update', help='操作类型')
|
||||
parser.add_argument('--execute', action='store_true', help='实际执行回滚')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tool = KaopeilianRollbackTool()
|
||||
|
||||
try:
|
||||
await tool.connect()
|
||||
|
||||
if args.list:
|
||||
await tool.list_recent_changes(args.hours)
|
||||
|
||||
elif args.rollback_user:
|
||||
if args.execute:
|
||||
success = await tool.rollback_user_operation(args.rollback_user, args.operation_type)
|
||||
if success:
|
||||
logger.info("✅ 用户回滚完成")
|
||||
else:
|
||||
logger.error("❌ 用户回滚失败")
|
||||
else:
|
||||
logger.info(f"🔍 模拟回滚用户 ID: {args.rollback_user}, 类型: {args.operation_type}")
|
||||
logger.info("使用 --execute 参数实际执行")
|
||||
|
||||
elif args.rollback_course:
|
||||
if args.execute:
|
||||
success = await tool.rollback_course_operation(args.rollback_course, args.operation_type)
|
||||
if success:
|
||||
logger.info("✅ 课程回滚完成")
|
||||
else:
|
||||
logger.error("❌ 课程回滚失败")
|
||||
else:
|
||||
logger.info(f"🔍 模拟回滚课程 ID: {args.rollback_course}, 类型: {args.operation_type}")
|
||||
logger.info("使用 --execute 参数实际执行")
|
||||
|
||||
elif args.rollback_exam:
|
||||
if args.execute:
|
||||
success = await tool.rollback_exam_operation(args.rollback_exam)
|
||||
if success:
|
||||
logger.info("✅ 考试回滚完成")
|
||||
else:
|
||||
logger.error("❌ 考试回滚失败")
|
||||
else:
|
||||
logger.info(f"🔍 模拟回滚考试 ID: {args.rollback_exam}")
|
||||
logger.info("使用 --execute 参数实际执行")
|
||||
|
||||
elif args.rollback_position:
|
||||
if args.execute:
|
||||
success = await tool.rollback_position_operation(args.rollback_position, args.operation_type)
|
||||
if success:
|
||||
logger.info("✅ 岗位回滚完成")
|
||||
else:
|
||||
logger.error("❌ 岗位回滚失败")
|
||||
else:
|
||||
logger.info(f"🔍 模拟回滚岗位 ID: {args.rollback_position}, 类型: {args.operation_type}")
|
||||
logger.info("使用 --execute 参数实际执行")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 程序执行异常: {e}")
|
||||
finally:
|
||||
await tool.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
317
backend/scripts/migrate_env_to_db.py
Normal file
317
backend/scripts/migrate_env_to_db.py
Normal file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
租户配置迁移脚本
|
||||
|
||||
功能:将各租户的 .env 文件配置迁移到 kaopeilian_admin 数据库
|
||||
|
||||
使用方法:
|
||||
python scripts/migrate_env_to_db.py
|
||||
|
||||
说明:
|
||||
1. 读取各租户的 .env 文件
|
||||
2. 创建租户记录
|
||||
3. 将配置写入 tenant_configs 表
|
||||
4. 保留原 .env 文件作为备份
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import pymysql
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
# ============================================
|
||||
# 配置
|
||||
# ============================================
|
||||
|
||||
# 管理库连接配置
|
||||
ADMIN_DB_CONFIG = {
|
||||
"host": os.getenv("ADMIN_DB_HOST", "120.79.247.16"),
|
||||
"port": int(os.getenv("ADMIN_DB_PORT", "3309")),
|
||||
"user": os.getenv("ADMIN_DB_USER", "root"),
|
||||
"password": os.getenv("ADMIN_DB_PASSWORD", "ProdMySQL2025!@#"),
|
||||
"db": os.getenv("ADMIN_DB_NAME", "kaopeilian_admin"),
|
||||
"charset": "utf8mb4",
|
||||
}
|
||||
|
||||
# 租户配置
|
||||
TENANTS = [
|
||||
{
|
||||
"code": "demo",
|
||||
"name": "演示版",
|
||||
"display_name": "考培练系统-演示版",
|
||||
"domain": "aiedu.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.production",
|
||||
"industry": "medical_beauty",
|
||||
},
|
||||
{
|
||||
"code": "hua",
|
||||
"name": "华尔倍丽",
|
||||
"display_name": "华尔倍丽-考培练系统",
|
||||
"domain": "hua.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.hua",
|
||||
"industry": "medical_beauty",
|
||||
},
|
||||
{
|
||||
"code": "yy",
|
||||
"name": "杨扬宠物",
|
||||
"display_name": "杨扬宠物-考培练系统",
|
||||
"domain": "yy.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.yy",
|
||||
"industry": "pet",
|
||||
},
|
||||
{
|
||||
"code": "hl",
|
||||
"name": "武汉禾丽",
|
||||
"display_name": "武汉禾丽-考培练系统",
|
||||
"domain": "hl.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.hl",
|
||||
"industry": "medical_beauty",
|
||||
},
|
||||
{
|
||||
"code": "xy",
|
||||
"name": "芯颜定制",
|
||||
"display_name": "芯颜定制-考培练系统",
|
||||
"domain": "xy.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.xy",
|
||||
"industry": "medical_beauty",
|
||||
},
|
||||
{
|
||||
"code": "fw",
|
||||
"name": "飞沃",
|
||||
"display_name": "飞沃-考培练系统",
|
||||
"domain": "fw.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.fw",
|
||||
"industry": "medical_beauty",
|
||||
},
|
||||
{
|
||||
"code": "ex",
|
||||
"name": "恩喜成都总院",
|
||||
"display_name": "恩喜成都总院-考培练系统",
|
||||
"domain": "ex.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.ex",
|
||||
"industry": "medical_beauty",
|
||||
},
|
||||
{
|
||||
"code": "kpl",
|
||||
"name": "瑞小美",
|
||||
"display_name": "瑞小美-考培练系统",
|
||||
"domain": "kpl.ireborn.com.cn",
|
||||
"env_file": "/root/aiedu/kaopeilian-backend/.env.kpl",
|
||||
"industry": "medical_beauty",
|
||||
},
|
||||
]
|
||||
|
||||
# 配置键到分组的映射
|
||||
CONFIG_MAPPING = {
|
||||
# 数据库配置
|
||||
"MYSQL_HOST": ("database", "string", False),
|
||||
"MYSQL_PORT": ("database", "int", False),
|
||||
"MYSQL_USER": ("database", "string", False),
|
||||
"MYSQL_PASSWORD": ("database", "string", True),
|
||||
"MYSQL_DATABASE": ("database", "string", False),
|
||||
# Redis配置
|
||||
"REDIS_HOST": ("redis", "string", False),
|
||||
"REDIS_PORT": ("redis", "int", False),
|
||||
"REDIS_DB": ("redis", "int", False),
|
||||
"REDIS_URL": ("redis", "string", False),
|
||||
# 安全配置
|
||||
"SECRET_KEY": ("security", "string", True),
|
||||
"CORS_ORIGINS": ("security", "json", False),
|
||||
# Dify配置
|
||||
"DIFY_API_KEY": ("dify", "string", True),
|
||||
"DIFY_EXAM_GENERATOR_API_KEY": ("dify", "string", True),
|
||||
"DIFY_PRACTICE_API_KEY": ("dify", "string", True),
|
||||
"DIFY_COURSE_CHAT_API_KEY": ("dify", "string", True),
|
||||
"DIFY_YANJI_ANALYSIS_API_KEY": ("dify", "string", True),
|
||||
# Coze配置
|
||||
"COZE_PRACTICE_BOT_ID": ("coze", "string", False),
|
||||
"COZE_BROADCAST_WORKFLOW_ID": ("coze", "string", False),
|
||||
"COZE_BROADCAST_SPACE_ID": ("coze", "string", False),
|
||||
"COZE_BROADCAST_BOT_ID": ("coze", "string", False),
|
||||
"COZE_OAUTH_CLIENT_ID": ("coze", "string", False),
|
||||
"COZE_OAUTH_PUBLIC_KEY_ID": ("coze", "string", False),
|
||||
"COZE_OAUTH_PRIVATE_KEY_PATH": ("coze", "string", False),
|
||||
# AI配置
|
||||
"AI_PRIMARY_API_KEY": ("ai", "string", True),
|
||||
"AI_PRIMARY_BASE_URL": ("ai", "string", False),
|
||||
"AI_FALLBACK_API_KEY": ("ai", "string", True),
|
||||
"AI_FALLBACK_BASE_URL": ("ai", "string", False),
|
||||
"AI_DEFAULT_MODEL": ("ai", "string", False),
|
||||
"AI_TIMEOUT": ("ai", "int", False),
|
||||
# 言迹配置
|
||||
"YANJI_CLIENT_ID": ("yanji", "string", False),
|
||||
"YANJI_CLIENT_SECRET": ("yanji", "string", True),
|
||||
"YANJI_TENANT_ID": ("yanji", "string", False),
|
||||
"YANJI_ESTATE_ID": ("yanji", "string", False),
|
||||
# 其他配置
|
||||
"APP_NAME": ("basic", "string", False),
|
||||
"PUBLIC_DOMAIN": ("basic", "string", False),
|
||||
}
|
||||
|
||||
|
||||
def parse_env_file(filepath: str) -> Dict[str, str]:
|
||||
"""解析 .env 文件"""
|
||||
config = {}
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
print(f" 警告: 文件不存在 {filepath}")
|
||||
return config
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
# 跳过注释和空行
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
# 解析 KEY=VALUE
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# 去除引号
|
||||
if (value.startswith('"') and value.endswith('"')) or \
|
||||
(value.startswith("'") and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def create_tenant(cursor, tenant: Dict) -> int:
|
||||
"""创建租户记录,返回租户ID"""
|
||||
# 检查是否已存在
|
||||
cursor.execute(
|
||||
"SELECT id FROM tenants WHERE code = %s",
|
||||
(tenant["code"],)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
print(f" 租户已存在,ID: {row['id']}")
|
||||
return row["id"]
|
||||
|
||||
# 创建新租户
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tenants (code, name, display_name, domain, industry, status, created_by)
|
||||
VALUES (%s, %s, %s, %s, %s, 'active', 1)
|
||||
""",
|
||||
(tenant["code"], tenant["name"], tenant["display_name"], tenant["domain"], tenant["industry"])
|
||||
)
|
||||
|
||||
tenant_id = cursor.lastrowid
|
||||
print(f" 创建租户成功,ID: {tenant_id}")
|
||||
return tenant_id
|
||||
|
||||
|
||||
def migrate_config(cursor, tenant_id: int, config: Dict[str, str]) -> Tuple[int, int]:
|
||||
"""迁移配置到数据库"""
|
||||
inserted = 0
|
||||
updated = 0
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in CONFIG_MAPPING:
|
||||
continue
|
||||
|
||||
config_group, value_type, is_secret = CONFIG_MAPPING[key]
|
||||
|
||||
# 检查是否已存在
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id FROM tenant_configs
|
||||
WHERE tenant_id = %s AND config_group = %s AND config_key = %s
|
||||
""",
|
||||
(tenant_id, config_group, key)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row:
|
||||
# 更新
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE tenant_configs
|
||||
SET config_value = %s, value_type = %s, is_encrypted = %s, updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(value, value_type, is_secret, row["id"])
|
||||
)
|
||||
updated += 1
|
||||
else:
|
||||
# 插入
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tenant_configs
|
||||
(tenant_id, config_group, config_key, config_value, value_type, is_encrypted)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(tenant_id, config_group, key, value, value_type, is_secret)
|
||||
)
|
||||
inserted += 1
|
||||
|
||||
return inserted, updated
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("=" * 60)
|
||||
print("租户配置迁移脚本")
|
||||
print("=" * 60)
|
||||
print(f"\n目标数据库: {ADMIN_DB_CONFIG['host']}:{ADMIN_DB_CONFIG['port']}/{ADMIN_DB_CONFIG['db']}")
|
||||
print(f"待迁移租户: {len(TENANTS)} 个\n")
|
||||
|
||||
# 连接数据库
|
||||
conn = pymysql.connect(**ADMIN_DB_CONFIG, cursorclass=pymysql.cursors.DictCursor)
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
total_inserted = 0
|
||||
total_updated = 0
|
||||
|
||||
for tenant in TENANTS:
|
||||
print(f"\n处理租户: {tenant['name']} ({tenant['code']})")
|
||||
print(f" 环境文件: {tenant['env_file']}")
|
||||
|
||||
# 解析 .env 文件
|
||||
config = parse_env_file(tenant['env_file'])
|
||||
print(f" 读取配置: {len(config)} 项")
|
||||
|
||||
# 创建租户
|
||||
tenant_id = create_tenant(cursor, tenant)
|
||||
|
||||
# 迁移配置
|
||||
if config:
|
||||
inserted, updated = migrate_config(cursor, tenant_id, config)
|
||||
print(f" 迁移结果: 新增 {inserted} 项, 更新 {updated} 项")
|
||||
total_inserted += inserted
|
||||
total_updated += updated
|
||||
else:
|
||||
print(" 跳过迁移(无配置)")
|
||||
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("迁移完成!")
|
||||
print(f"总计: 新增 {total_inserted} 项, 更新 {total_updated} 项")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"\n错误: {e}")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
384
backend/scripts/migrate_prompts_to_db.py
Normal file
384
backend/scripts/migrate_prompts_to_db.py
Normal file
@@ -0,0 +1,384 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI 提示词迁移脚本
|
||||
|
||||
功能:将代码中的 AI 提示词迁移到数据库
|
||||
|
||||
使用方法:
|
||||
python scripts/migrate_prompts_to_db.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pymysql
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
# ============================================
|
||||
# 配置
|
||||
# ============================================
|
||||
|
||||
ADMIN_DB_CONFIG = {
|
||||
"host": os.getenv("ADMIN_DB_HOST", "120.79.247.16"),
|
||||
"port": int(os.getenv("ADMIN_DB_PORT", "3309")),
|
||||
"user": os.getenv("ADMIN_DB_USER", "root"),
|
||||
"password": os.getenv("ADMIN_DB_PASSWORD", "ProdMySQL2025!@#"),
|
||||
"db": os.getenv("ADMIN_DB_NAME", "kaopeilian_admin"),
|
||||
"charset": "utf8mb4",
|
||||
}
|
||||
|
||||
|
||||
# ============================================
|
||||
# 提示词定义
|
||||
# ============================================
|
||||
|
||||
PROMPTS = [
|
||||
{
|
||||
"code": "knowledge_analysis",
|
||||
"name": "知识点分析",
|
||||
"description": "从课程资料中提取和分析知识点,支持PDF/Word/文本等格式",
|
||||
"module": "course",
|
||||
"system_prompt": """# 角色
|
||||
你是一个文件拆解高手,擅长将用户提交的内容进行精准拆分,拆分后的内容做个简单的优化处理使其更具可读性,但要尽量使用原文的原词原句。
|
||||
|
||||
## 技能
|
||||
### 技能 1: 内容拆分
|
||||
1. 当用户提交内容后,拆分为多段。
|
||||
2. 对拆分后的内容做简单优化,使其更具可读性,比如去掉奇怪符号(如换行符、乱码),若语句不通顺,或格式原因导致错位,则重新表达。用户可能会提交录音转文字的内容,因此可能是有错字的,注意修复这些小瑕疵。
|
||||
3. 优化过程中,尽量使用原文的原词原句,特别是话术类,必须保持原有的句式、保持原词原句,而不是重构。
|
||||
4. 注意是拆分而不是重写,不需要润色,尽量不做任何处理。
|
||||
5. 输出到 content。
|
||||
|
||||
### 技能 2: 为每一个选段概括一个标题
|
||||
1. 为每个拆分出来的选段概括一个标题,并输出到 title。
|
||||
|
||||
### 技能 3: 为每一个选段说明与主题的关联
|
||||
1. 详细说明这一段与全文核心主题的关联,并输出到 topic_relation。
|
||||
|
||||
### 技能 4: 为每一个选段打上一个类型标签
|
||||
1. 用户提交的内容很有可能是一个课程、一篇讲义、一个产品的说明书,通常是用户希望他公司的员工或高管学习的知识。
|
||||
2. 用户通常是医疗美容机构或轻医美、生活美容连锁品牌。
|
||||
3. 你要为每个选段打上一个知识类型的标签,最好是这几个类型中的一个:"理论知识", "诊断设计", "操作步骤", "沟通话术", "案例分析", "注意事项", "技巧方法", "客诉处理"。当然你也可以为这个选段匹配一个更适合的。
|
||||
|
||||
## 输出要求(严格按要求输出)
|
||||
请直接输出一个纯净的 JSON 数组(Array),不要包含 Markdown 标记(如 ```json),也不要包含任何解释性文字。格式如下:
|
||||
|
||||
[
|
||||
{
|
||||
"title": "知识点标题",
|
||||
"content": "知识点内容",
|
||||
"topic_relation": "知识点与主题的关系",
|
||||
"type": "知识点类型"
|
||||
}
|
||||
]
|
||||
|
||||
## 限制
|
||||
- 仅围绕用户提交的内容进行拆分和关联标注,不涉及其他无关内容。
|
||||
- 拆分后的内容必须最大程度保持与原文一致。
|
||||
- 关联说明需清晰合理。
|
||||
- 不论如何,不要拆分超过 20 段!""",
|
||||
"user_prompt_template": """课程主题:{course_name}
|
||||
|
||||
## 用户提交的内容:
|
||||
|
||||
{content}
|
||||
|
||||
## 注意
|
||||
|
||||
- 以json的格式输出
|
||||
- 不论如何,不要拆分超过20 段!""",
|
||||
"variables": ["course_name", "content"],
|
||||
"model_recommendation": "gemini-3-flash-preview",
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
},
|
||||
{
|
||||
"code": "exam_generator",
|
||||
"name": "试题生成器",
|
||||
"description": "根据知识点自动生成考试题目,支持单选、多选、判断、填空、问答题型",
|
||||
"module": "exam",
|
||||
"system_prompt": """# 角色
|
||||
你是一个专业的试题生成器,能够根据给定的知识点内容生成高质量的考试题目。
|
||||
|
||||
## 技能
|
||||
### 技能 1: 生成单选题
|
||||
- 每道题有4个选项(A、B、C、D)
|
||||
- 只有一个正确答案
|
||||
- 选项设计要有迷惑性但不能有歧义
|
||||
|
||||
### 技能 2: 生成多选题
|
||||
- 每道题有4个选项(A、B、C、D)
|
||||
- 有2-4个正确答案
|
||||
- 考察综合理解能力
|
||||
|
||||
### 技能 3: 生成判断题
|
||||
- 陈述一个观点,判断对错
|
||||
- 答案为"对"或"错"
|
||||
|
||||
### 技能 4: 生成填空题
|
||||
- 在关键位置设置空白
|
||||
- 答案明确唯一
|
||||
|
||||
### 技能 5: 生成问答题
|
||||
- 开放性问题
|
||||
- 需要组织语言回答
|
||||
|
||||
## 输出格式
|
||||
请直接输出 JSON 数组,格式如下:
|
||||
|
||||
[
|
||||
{
|
||||
"question_type": "single_choice",
|
||||
"title": "题目内容",
|
||||
"options": ["A. 选项1", "B. 选项2", "C. 选项3", "D. 选项4"],
|
||||
"correct_answer": "A",
|
||||
"explanation": "答案解析",
|
||||
"difficulty": "easy/medium/hard"
|
||||
}
|
||||
]
|
||||
|
||||
## 限制
|
||||
- 题目必须与给定知识点相关
|
||||
- 难度要适中,兼顾基础和提升
|
||||
- 表述清晰准确,无歧义""",
|
||||
"user_prompt_template": """请根据以下知识点内容生成试题:
|
||||
|
||||
{content}
|
||||
|
||||
要求:
|
||||
- 单选题 {single_choice_count} 道
|
||||
- 多选题 {multiple_choice_count} 道
|
||||
- 判断题 {true_false_count} 道
|
||||
- 填空题 {fill_blank_count} 道
|
||||
- 问答题 {essay_count} 道
|
||||
|
||||
难度系数:{difficulty_level}(1-5,1最简单)
|
||||
|
||||
请以 JSON 格式输出题目列表。""",
|
||||
"variables": ["content", "single_choice_count", "multiple_choice_count", "true_false_count", "fill_blank_count", "essay_count", "difficulty_level"],
|
||||
"model_recommendation": "gemini-3-flash-preview",
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
},
|
||||
{
|
||||
"code": "course_chat",
|
||||
"name": "课程对话",
|
||||
"description": "与课程知识点进行智能对话,回答学员问题",
|
||||
"module": "course",
|
||||
"system_prompt": """# 角色
|
||||
你是一位专业的课程助教,负责回答学员关于课程内容的问题。
|
||||
|
||||
## 职责
|
||||
1. 准确回答与课程相关的问题
|
||||
2. 用通俗易懂的语言解释复杂概念
|
||||
3. 提供实用的学习建议
|
||||
4. 关联相关知识点帮助理解
|
||||
|
||||
## 原则
|
||||
- 回答要准确、专业
|
||||
- 语言要友好、易懂
|
||||
- 适当举例说明
|
||||
- 如果问题超出课程范围,礼貌说明
|
||||
|
||||
## 回复格式
|
||||
- 保持简洁明了
|
||||
- 可以使用列表、分点等结构化方式
|
||||
- 重要内容可以加粗强调""",
|
||||
"user_prompt_template": """课程名称:{course_name}
|
||||
|
||||
课程知识点:
|
||||
{knowledge_content}
|
||||
|
||||
学员问题:{query}
|
||||
|
||||
请根据课程知识点回答学员的问题。""",
|
||||
"variables": ["course_name", "knowledge_content", "query"],
|
||||
"model_recommendation": "gemini-3-flash-preview",
|
||||
"max_tokens": 2048,
|
||||
"temperature": 0.7,
|
||||
},
|
||||
{
|
||||
"code": "ability_analysis",
|
||||
"name": "能力分析",
|
||||
"description": "基于智能工牌对话数据分析员工能力并推荐课程",
|
||||
"module": "ability",
|
||||
"system_prompt": """# 角色
|
||||
你是一位专业的人才发展顾问,负责根据员工的对话记录分析其能力并推荐提升课程。
|
||||
|
||||
## 分析维度
|
||||
1. **专业知识**:产品知识、行业知识、技术能力
|
||||
2. **沟通能力**:表达清晰度、倾听能力、情绪管理
|
||||
3. **销售技巧**:需求挖掘、异议处理、促成能力
|
||||
4. **服务意识**:客户关怀、问题解决、满意度维护
|
||||
|
||||
## 输出格式
|
||||
请输出 JSON 格式的分析结果:
|
||||
|
||||
{
|
||||
"overall_score": 75,
|
||||
"dimensions": [
|
||||
{"name": "专业知识", "score": 80, "comment": "评价"},
|
||||
{"name": "沟通能力", "score": 70, "comment": "评价"}
|
||||
],
|
||||
"strengths": ["优势1", "优势2"],
|
||||
"weaknesses": ["待提升1", "待提升2"],
|
||||
"recommendations": [
|
||||
{"course_name": "推荐课程", "reason": "推荐理由"}
|
||||
]
|
||||
}""",
|
||||
"user_prompt_template": """员工信息:
|
||||
- 姓名:{employee_name}
|
||||
- 岗位:{position}
|
||||
|
||||
对话记录:
|
||||
{conversation_records}
|
||||
|
||||
可选课程列表:
|
||||
{available_courses}
|
||||
|
||||
请分析该员工的能力并推荐适合的课程。""",
|
||||
"variables": ["employee_name", "position", "conversation_records", "available_courses"],
|
||||
"model_recommendation": "gemini-3-flash-preview",
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.7,
|
||||
},
|
||||
{
|
||||
"code": "practice_scene",
|
||||
"name": "陪练场景生成",
|
||||
"description": "根据课程内容生成陪练场景和对话",
|
||||
"module": "practice",
|
||||
"system_prompt": """# 角色
|
||||
你是一位专业的培训场景设计师,负责为员工陪练设计模拟对话场景。
|
||||
|
||||
## 职责
|
||||
1. 根据课程内容设计真实场景
|
||||
2. 模拟客户各种可能的提问和反应
|
||||
3. 设计合理的对话流程
|
||||
4. 提供评估标准
|
||||
|
||||
## 场景类型
|
||||
- 电话销售场景
|
||||
- 面对面咨询场景
|
||||
- 客户投诉处理场景
|
||||
- 售后服务场景
|
||||
- 产品介绍场景
|
||||
|
||||
## 输出格式
|
||||
请输出 JSON 格式:
|
||||
|
||||
{
|
||||
"scene_name": "场景名称",
|
||||
"scene_type": "场景类型",
|
||||
"background": "场景背景",
|
||||
"customer_profile": "客户画像",
|
||||
"dialogue_flow": [
|
||||
{"role": "customer", "content": "客户话术"},
|
||||
{"role": "employee", "content": "员工话术"}
|
||||
],
|
||||
"evaluation_points": ["评估要点1", "评估要点2"]
|
||||
}""",
|
||||
"user_prompt_template": """课程名称:{course_name}
|
||||
|
||||
课程知识点:
|
||||
{knowledge_content}
|
||||
|
||||
请为这些知识点设计一个{scene_type}的陪练场景。
|
||||
|
||||
难度:{difficulty}
|
||||
预计时长:{duration}分钟""",
|
||||
"variables": ["course_name", "knowledge_content", "scene_type", "difficulty", "duration"],
|
||||
"model_recommendation": "gemini-3-flash-preview",
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.8,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("=" * 60)
|
||||
print("AI 提示词迁移脚本")
|
||||
print("=" * 60)
|
||||
print(f"\n目标数据库: {ADMIN_DB_CONFIG['host']}:{ADMIN_DB_CONFIG['port']}/{ADMIN_DB_CONFIG['db']}")
|
||||
print(f"待迁移提示词: {len(PROMPTS)} 个\n")
|
||||
|
||||
conn = pymysql.connect(**ADMIN_DB_CONFIG, cursorclass=pymysql.cursors.DictCursor)
|
||||
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
inserted = 0
|
||||
updated = 0
|
||||
|
||||
for prompt in PROMPTS:
|
||||
print(f"处理提示词: {prompt['name']} ({prompt['code']})")
|
||||
|
||||
# 检查是否已存在
|
||||
cursor.execute(
|
||||
"SELECT id, version FROM ai_prompts WHERE code = %s",
|
||||
(prompt["code"],)
|
||||
)
|
||||
existing = cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
# 更新
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE ai_prompts SET
|
||||
name = %s,
|
||||
description = %s,
|
||||
module = %s,
|
||||
system_prompt = %s,
|
||||
user_prompt_template = %s,
|
||||
variables = %s,
|
||||
model_recommendation = %s,
|
||||
max_tokens = %s,
|
||||
temperature = %s,
|
||||
updated_by = 1
|
||||
WHERE id = %s
|
||||
""",
|
||||
(prompt["name"], prompt["description"], prompt["module"],
|
||||
prompt["system_prompt"], prompt["user_prompt_template"],
|
||||
json.dumps(prompt["variables"]),
|
||||
prompt["model_recommendation"], prompt["max_tokens"],
|
||||
prompt["temperature"], existing["id"])
|
||||
)
|
||||
print(f" 更新成功,ID: {existing['id']}")
|
||||
updated += 1
|
||||
else:
|
||||
# 插入
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO ai_prompts
|
||||
(code, name, description, module, system_prompt, user_prompt_template,
|
||||
variables, model_recommendation, max_tokens, temperature, is_system, created_by)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, TRUE, 1)
|
||||
""",
|
||||
(prompt["code"], prompt["name"], prompt["description"], prompt["module"],
|
||||
prompt["system_prompt"], prompt["user_prompt_template"],
|
||||
json.dumps(prompt["variables"]),
|
||||
prompt["model_recommendation"], prompt["max_tokens"], prompt["temperature"])
|
||||
)
|
||||
print(f" 插入成功,ID: {cursor.lastrowid}")
|
||||
inserted += 1
|
||||
|
||||
conn.commit()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("迁移完成!")
|
||||
print(f"新增: {inserted} 个, 更新: {updated} 个")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"\n错误: {e}")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
329
backend/scripts/mock_data_beauty.sql
Normal file
329
backend/scripts/mock_data_beauty.sql
Normal file
@@ -0,0 +1,329 @@
|
||||
-- ============================================
|
||||
-- 轻医美+生活美容连锁机构模拟数据
|
||||
-- 版本:1.0.0
|
||||
-- 创建时间:2025-01-20
|
||||
-- ============================================
|
||||
|
||||
USE `kaopeilian`;
|
||||
|
||||
-- 清理已有测试数据(保留初始的superadmin、admin、testuser)
|
||||
DELETE FROM training_reports WHERE id > 0;
|
||||
DELETE FROM training_messages WHERE id > 0;
|
||||
DELETE FROM training_sessions WHERE id > 0;
|
||||
DELETE FROM exam_results WHERE id > 0;
|
||||
DELETE FROM exams WHERE id > 0;
|
||||
DELETE FROM questions WHERE course_id > 4;
|
||||
DELETE FROM knowledge_points WHERE course_id > 4;
|
||||
DELETE FROM course_materials WHERE course_id > 4;
|
||||
DELETE FROM user_teams WHERE user_id > 3;
|
||||
DELETE FROM teams WHERE id > 3;
|
||||
DELETE FROM courses WHERE id > 4;
|
||||
DELETE FROM training_scenes WHERE id > 3;
|
||||
DELETE FROM growth_paths WHERE id > 2;
|
||||
DELETE FROM users WHERE id > 3;
|
||||
|
||||
-- ============================================
|
||||
-- 一、用户数据(轻医美+生活美容机构人员)
|
||||
-- ============================================
|
||||
|
||||
-- 管理层
|
||||
INSERT INTO users (username, email, phone, hashed_password, full_name, role, is_active, is_verified, bio) VALUES
|
||||
('zhangyun', 'zhangyun@beauty.com', '13800138001', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '张云', 'admin', TRUE, TRUE, '集团总经理,20年美容行业经验'),
|
||||
('lixiaoli', 'lixiaoli@beauty.com', '13800138002', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '李晓丽', 'manager', TRUE, TRUE, '华东区域经理,负责上海、江苏、浙江区域'),
|
||||
('wangmei', 'wangmei@beauty.com', '13800138003', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '王梅', 'manager', TRUE, TRUE, '华南区域经理,负责广东、福建区域'),
|
||||
|
||||
-- 医美部门
|
||||
('drchen', 'drchen@beauty.com', '13800138004', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '陈医生', 'manager', TRUE, TRUE, '医美技术总监,皮肤科主治医师'),
|
||||
('liujing', 'liujing@beauty.com', '13800138005', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '刘静', 'trainee', TRUE, TRUE, '资深医美顾问,5年从业经验'),
|
||||
('zhangmin', 'zhangmin@beauty.com', '13800138006', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '张敏', 'trainee', TRUE, TRUE, '医美技师,擅长光电项目操作'),
|
||||
('sunhui', 'sunhui@beauty.com', '13800138007', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '孙慧', 'trainee', TRUE, TRUE, '医美技师,专注水光针注射'),
|
||||
|
||||
-- 美容部门
|
||||
('zhaoxue', 'zhaoxue@beauty.com', '13800138008', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '赵雪', 'manager', TRUE, TRUE, '美容部主管,国家高级美容师'),
|
||||
('yangli', 'yangli@beauty.com', '13800138009', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '杨丽', 'trainee', TRUE, TRUE, '资深美容顾问,擅长皮肤管理方案设计'),
|
||||
('huangting', 'huangting@beauty.com', '13800138010', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '黄婷', 'trainee', TRUE, TRUE, '美容师,专注面部护理'),
|
||||
('linwei', 'linwei@beauty.com', '13800138011', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '林薇', 'trainee', TRUE, TRUE, '美容师,擅长身体SPA'),
|
||||
('chenyu', 'chenyu@beauty.com', '13800138012', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '陈雨', 'trainee', TRUE, TRUE, '美容师,专注问题性皮肤护理'),
|
||||
|
||||
-- 客服部门
|
||||
('wujuan', 'wujuan@beauty.com', '13800138013', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '吴娟', 'manager', TRUE, TRUE, '客服部经理,负责客户关系管理'),
|
||||
('zhoufang', 'zhoufang@beauty.com', '13800138014', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '周芳', 'trainee', TRUE, TRUE, '客户经理,负责VIP客户维护'),
|
||||
('xujing', 'xujing@beauty.com', '13800138015', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '徐静', 'trainee', TRUE, TRUE, '前台接待,形象气质佳'),
|
||||
|
||||
-- 各分店人员
|
||||
('liuyan', 'liuyan@beauty.com', '13800138016', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '刘燕', 'manager', TRUE, TRUE, '静安店店长'),
|
||||
('zhangna', 'zhangna@beauty.com', '13800138017', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '张娜', 'trainee', TRUE, TRUE, '静安店美容顾问'),
|
||||
('wangxin', 'wangxin@beauty.com', '13800138018', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '王欣', 'trainee', TRUE, TRUE, '静安店美容师'),
|
||||
('lihong', 'lihong@beauty.com', '13800138019', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '李红', 'manager', TRUE, TRUE, '徐汇店店长'),
|
||||
('zhaoli', 'zhaoli@beauty.com', '13800138020', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyNiQdP/sQj6C6', '赵丽', 'trainee', TRUE, TRUE, '徐汇店医美顾问');
|
||||
|
||||
-- ============================================
|
||||
-- 二、团队数据(机构组织架构)
|
||||
-- ============================================
|
||||
|
||||
-- 公司总部
|
||||
INSERT INTO teams (name, code, description, team_type, leader_id) VALUES
|
||||
('集团总部', 'HQ', '轻医美生活美容集团总部', 'department', 4),
|
||||
('医美事业部', 'MEDICAL', '负责所有医美项目的运营和技术支持', 'department', 7),
|
||||
('美容事业部', 'BEAUTY', '负责传统美容项目的运营和培训', 'department', 11),
|
||||
('客服中心', 'SERVICE', '负责客户服务和关系维护', 'department', 16),
|
||||
|
||||
-- 区域团队
|
||||
('华东区域', 'EAST', '负责上海、江苏、浙江区域运营', 'department', 5),
|
||||
('华南区域', 'SOUTH', '负责广东、福建区域运营', 'department', 6),
|
||||
|
||||
-- 门店团队
|
||||
('静安旗舰店', 'JINGAN', '上海静安区旗舰店', 'department', 19),
|
||||
('徐汇精品店', 'XUHUI', '上海徐汇区精品店', 'department', 22),
|
||||
|
||||
-- 专项小组
|
||||
('医美技术委员会', 'MED_TECH', '负责医美技术标准制定和培训', 'study_group', 7),
|
||||
('美容技术研究组', 'BEAUTY_RES', '负责美容新技术研究和推广', 'study_group', 11),
|
||||
('服务标准化小组', 'SERVICE_STD', '负责服务流程标准化', 'study_group', 16);
|
||||
|
||||
-- ============================================
|
||||
-- 三、用户团队关联
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO user_teams (user_id, team_id, role) VALUES
|
||||
-- 总经理管理总部
|
||||
(4, 4, 'leader'),
|
||||
-- 区域经理
|
||||
(5, 8, 'leader'),
|
||||
(6, 9, 'leader'),
|
||||
-- 部门负责人
|
||||
(7, 5, 'leader'),
|
||||
(11, 6, 'leader'),
|
||||
(16, 7, 'leader'),
|
||||
-- 医美部门成员
|
||||
(7, 12, 'leader'),
|
||||
(8, 5, 'member'),
|
||||
(9, 5, 'member'),
|
||||
(10, 5, 'member'),
|
||||
-- 美容部门成员
|
||||
(11, 13, 'leader'),
|
||||
(12, 6, 'member'),
|
||||
(13, 6, 'member'),
|
||||
(14, 6, 'member'),
|
||||
(15, 6, 'member'),
|
||||
-- 客服部门成员
|
||||
(17, 7, 'member'),
|
||||
(18, 7, 'member'),
|
||||
-- 门店团队
|
||||
(19, 10, 'leader'),
|
||||
(20, 10, 'member'),
|
||||
(21, 10, 'member'),
|
||||
(22, 11, 'leader'),
|
||||
(23, 11, 'member');
|
||||
|
||||
-- ============================================
|
||||
-- 四、课程数据(美容行业培训课程)
|
||||
-- ============================================
|
||||
|
||||
DELETE FROM courses WHERE id > 4;
|
||||
|
||||
INSERT INTO courses (name, description, category, status, cover_image, duration_hours, difficulty_level, tags, published_at, is_featured, created_by) VALUES
|
||||
-- 基础课程
|
||||
('美容基础理论', '美容行业入门必修课,包含皮肤生理学、美容营养学等基础知识', 'general', 'published', '/uploads/course/beauty_basic.jpg', 20, 1, '["美容基础", "皮肤管理", "理论知识"]', '2024-01-15 10:00:00', TRUE, 4),
|
||||
('轻医美项目认知', '了解主流轻医美项目的原理、适应症和操作流程', 'technology', 'published', '/uploads/course/medical_beauty.jpg', 30, 2, '["轻医美", "水光针", "光电项目"]', '2024-01-20 14:00:00', TRUE, 7),
|
||||
('销售心理学与话术', '掌握美容行业销售技巧,提升业绩转化能力', 'business', 'published', '/uploads/course/sales_skill.jpg', 15, 2, '["销售技巧", "客户心理", "话术"]', '2024-02-01 09:00:00', TRUE, 16),
|
||||
('产品知识大全', '全面了解各类护肤品成分、功效和搭配方案', 'technology', 'published', '/uploads/course/product_knowledge.jpg', 25, 2, '["产品知识", "成分分析", "护肤"]', '2024-02-10 11:00:00', FALSE, 11),
|
||||
('客户服务标准流程', '标准化服务流程培训,提升客户满意度', 'management', 'published', '/uploads/course/service_process.jpg', 12, 1, '["服务流程", "客户体验", "标准化"]', '2024-02-15 15:00:00', TRUE, 16),
|
||||
('卫生安全操作规范', '医美和美容项目的卫生安全标准培训', 'general', 'published', '/uploads/course/safety_standard.jpg', 8, 1, '["安全规范", "卫生标准", "操作流程"]', '2024-02-20 10:00:00', TRUE, 7),
|
||||
|
||||
-- 进阶课程
|
||||
('面部美容手法精修', '深入学习各种面部护理手法和技巧', 'technology', 'published', '/uploads/course/facial_technique.jpg', 40, 3, '["面部护理", "手法技巧", "实操"]', '2024-03-01 14:00:00', FALSE, 11),
|
||||
('光电仪器操作认证', '各类美容仪器的原理和操作技巧培训', 'technology', 'published', '/uploads/course/device_operation.jpg', 35, 3, '["仪器操作", "光电美容", "认证培训"]', '2024-03-10 09:00:00', TRUE, 7),
|
||||
('问题性皮肤管理', '针对各类问题性皮肤的诊断和护理方案', 'technology', 'published', '/uploads/course/problem_skin.jpg', 30, 4, '["问题皮肤", "痘痘", "敏感肌", "诊断"]', '2024-03-15 11:00:00', FALSE, 11),
|
||||
('VIP客户管理艺术', '高端客户的维护技巧和管理策略', 'management', 'published', '/uploads/course/vip_management.jpg', 18, 3, '["VIP管理", "客户维护", "高端服务"]', '2024-03-20 15:00:00', FALSE, 16),
|
||||
|
||||
-- 管理课程
|
||||
('美容店务管理', '美容门店的日常运营和管理技巧', 'management', 'published', '/uploads/course/store_management.jpg', 20, 3, '["店务管理", "运营", "团队建设"]', '2024-04-01 10:00:00', FALSE, 19),
|
||||
('团队激励与培养', '如何打造高效的美容服务团队', 'management', 'published', '/uploads/course/team_building.jpg', 15, 3, '["团队管理", "员工激励", "人才培养"]', '2024-04-10 14:00:00', FALSE, 4);
|
||||
|
||||
-- ============================================
|
||||
-- 五、课程资料
|
||||
-- ============================================
|
||||
|
||||
-- 为新课程添加资料
|
||||
INSERT INTO course_materials (course_id, name, description, file_url, file_type, file_size, sort_order) VALUES
|
||||
-- 美容基础理论
|
||||
(5, '皮肤生理学基础.pdf', '详细介绍皮肤结构和生理功能', '/uploads/materials/skin_physiology.pdf', 'pdf', 5242880, 1),
|
||||
(5, '美容营养学.pdf', '营养与美容的关系', '/uploads/materials/beauty_nutrition.pdf', 'pdf', 3145728, 2),
|
||||
(5, '基础理论视频课程', '完整的理论知识讲解', '/uploads/materials/theory_video.mp4', 'mp4', 209715200, 3),
|
||||
|
||||
-- 轻医美项目认知
|
||||
(6, '水光针操作指南.pdf', '水光针注射标准操作流程', '/uploads/materials/hydra_injection.pdf', 'pdf', 4194304, 1),
|
||||
(6, '光电项目原理详解.ppt', '各类光电项目的原理和效果', '/uploads/materials/photoelectric.ppt', 'ppt', 10485760, 2),
|
||||
(6, '操作演示视频', '真人操作演示教学', '/uploads/materials/operation_demo.mp4', 'mp4', 314572800, 3),
|
||||
|
||||
-- 销售心理学与话术
|
||||
(7, '客户心理分析.pdf', '不同类型客户的心理特征', '/uploads/materials/customer_psychology.pdf', 'pdf', 2097152, 1),
|
||||
(7, '标准话术手册.doc', '各场景标准话术模板', '/uploads/materials/sales_scripts.doc', 'doc', 1048576, 2),
|
||||
(7, '销售实战案例.mp4', '优秀销售案例分享', '/uploads/materials/sales_cases.mp4', 'mp4', 157286400, 3);
|
||||
|
||||
-- ============================================
|
||||
-- 六、知识点体系
|
||||
-- ============================================
|
||||
|
||||
-- 美容基础理论知识点
|
||||
INSERT INTO knowledge_points (course_id, name, description, parent_id, level, path, sort_order, weight, is_required, estimated_hours) VALUES
|
||||
(5, '皮肤生理学', '了解皮肤的结构和功能', NULL, 1, '1', 1, 2.0, TRUE, 5),
|
||||
(5, '皮肤结构', '表皮、真皮、皮下组织的构成', 25, 2, '1.1', 1, 1.5, TRUE, 2),
|
||||
(5, '皮肤类型判断', '干性、油性、混合性、敏感性皮肤的特征', 25, 2, '1.2', 2, 1.5, TRUE, 1.5),
|
||||
(5, '美容营养学', '营养素对皮肤的影响', NULL, 1, '2', 2, 1.5, TRUE, 4),
|
||||
(5, '维生素与美容', '各类维生素的美容功效', 28, 2, '2.1', 1, 1.0, TRUE, 2),
|
||||
|
||||
-- 轻医美项目知识点
|
||||
(6, '注射类项目', '各类注射美容项目介绍', NULL, 1, '1', 1, 2.5, TRUE, 10),
|
||||
(6, '水光针技术', '水光针的原理和操作要点', 30, 2, '1.1', 1, 2.0, TRUE, 4),
|
||||
(6, '肉毒素注射', '肉毒素的作用原理和注射技巧', 30, 2, '1.2', 2, 2.0, TRUE, 3),
|
||||
(6, '光电类项目', '光电美容技术详解', NULL, 1, '2', 2, 2.5, TRUE, 12),
|
||||
(6, '激光美容', '各类激光的原理和应用', 33, 2, '2.1', 1, 2.0, TRUE, 5);
|
||||
|
||||
-- ============================================
|
||||
-- 七、考试题目(美容行业相关)
|
||||
-- ============================================
|
||||
|
||||
-- 美容基础理论题目
|
||||
INSERT INTO questions (course_id, question_type, title, content, options, correct_answer, explanation, score, difficulty, tags) VALUES
|
||||
(5, 'single_choice', '皮肤最外层的结构是?', NULL, '{"A": "真皮层", "B": "表皮层", "C": "皮下组织", "D": "基底层"}', 'B', '皮肤由外到内分为表皮层、真皮层和皮下组织', 10.0, 'easy', '["皮肤结构", "基础知识"]'),
|
||||
(5, 'single_choice', '以下哪种维生素被称为"美容维生素"?', NULL, '{"A": "维生素A", "B": "维生素B", "C": "维生素C", "D": "维生素D"}', 'C', '维生素C具有抗氧化、美白、促进胶原蛋白合成的作用', 10.0, 'easy', '["营养学", "维生素"]'),
|
||||
(5, 'true_false', '油性皮肤不需要补水', NULL, NULL, 'false', '油性皮肤也需要补水,缺水会导致皮肤分泌更多油脂', 10.0, 'medium', '["皮肤类型", "护理误区"]'),
|
||||
(5, 'fill_blank', '皮肤的pH值呈____性', NULL, NULL, '弱酸', '健康皮肤的pH值在4.5-6.5之间,呈弱酸性', 10.0, 'easy', '["皮肤生理"]'),
|
||||
|
||||
-- 轻医美项目题目
|
||||
(6, 'single_choice', '水光针注射的层次是?', NULL, '{"A": "表皮层", "B": "真皮浅层", "C": "真皮深层", "D": "皮下组织"}', 'B', '水光针通常注射在真皮浅层,有利于营养成分的吸收', 10.0, 'medium', '["水光针", "注射技术"]'),
|
||||
(6, 'single_choice', '肉毒素的作用原理是?', NULL, '{"A": "填充凹陷", "B": "阻断神经肌肉传导", "C": "刺激胶原再生", "D": "溶解脂肪"}', 'B', '肉毒素通过阻断神经肌肉传导,使肌肉放松,从而减少皱纹', 10.0, 'medium', '["肉毒素", "作用原理"]'),
|
||||
(6, 'multiple_choice', '以下哪些是光子嫩肤的适应症?(多选)', NULL, '{"A": "色斑", "B": "毛孔粗大", "C": "红血丝", "D": "深度皱纹"}', '["A", "B", "C"]', '光子嫩肤适用于浅表性皮肤问题,对深度皱纹效果有限', 15.0, 'hard', '["光电项目", "适应症"]'),
|
||||
|
||||
-- 销售技巧题目
|
||||
(7, 'single_choice', '面对犹豫不决的客户,最好的策略是?', NULL, '{"A": "立即降价", "B": "强调限时优惠", "C": "了解顾虑并解答", "D": "推荐更贵的项目"}', 'C', '了解客户的具体顾虑并针对性解答,建立信任更重要', 10.0, 'medium', '["销售技巧", "客户心理"]'),
|
||||
(7, 'true_false', '销售时应该尽量推荐最贵的产品和项目', NULL, NULL, 'false', '应该根据客户的实际需求和消费能力推荐合适的产品', 10.0, 'easy', '["销售原则", "职业道德"]'),
|
||||
|
||||
-- 产品知识题目
|
||||
(8, 'single_choice', '玻尿酸的主要功效是?', NULL, '{"A": "美白", "B": "保湿", "C": "去角质", "D": "控油"}', 'B', '玻尿酸是优秀的保湿成分,能吸收自身重量1000倍的水分', 10.0, 'easy', '["成分知识", "功效"]'),
|
||||
(8, 'fill_blank', '视黄醇是维生素____的衍生物', NULL, NULL, 'A', '视黄醇(Retinol)是维生素A的衍生物,具有抗老功效', 10.0, 'medium', '["成分知识", "维生素"]'),
|
||||
|
||||
-- 服务流程题目
|
||||
(9, 'single_choice', '客户到店后的第一步应该是?', NULL, '{"A": "推销产品", "B": "热情接待并了解需求", "C": "直接带去护理", "D": "要求办卡"}', 'B', '良好的接待和需求了解是优质服务的开始', 10.0, 'easy', '["服务流程", "接待"]'),
|
||||
(9, 'true_false', '护理过程中可以接听私人电话', NULL, NULL, 'false', '护理过程中应专注于客户,避免接听私人电话', 10.0, 'easy', '["服务规范", "职业素养"]'),
|
||||
|
||||
-- 安全规范题目
|
||||
(10, 'single_choice', '医美项目操作前必须进行的步骤是?', NULL, '{"A": "皮肤测试", "B": "签署知情同意书", "C": "拍照存档", "D": "以上都是"}', 'D', '医美项目需要做好充分的术前准备和风险告知', 10.0, 'medium', '["安全规范", "操作流程"]'),
|
||||
(10, 'multiple_choice', '以下哪些属于无菌操作的要求?(多选)', NULL, '{"A": "戴无菌手套", "B": "使用一次性耗材", "C": "操作台面消毒", "D": "戴口罩"}', '["A", "B", "C", "D"]', '无菌操作需要全方位的防护和消毒措施', 15.0, 'medium', '["无菌操作", "卫生标准"]');
|
||||
|
||||
-- ============================================
|
||||
-- 八、AI陪练场景(美容行业场景)
|
||||
-- ============================================
|
||||
|
||||
DELETE FROM training_scenes WHERE id > 3;
|
||||
|
||||
INSERT INTO training_scenes (name, description, category, ai_config, prompt_template, evaluation_criteria, status, is_public, created_by) VALUES
|
||||
('客户咨询接待', '模拟接待到店客户,了解需求并推荐合适的项目', '客户服务',
|
||||
'{"bot_id": "beauty_consultant_bot", "temperature": 0.7}',
|
||||
'你是一位专业的美容顾问,需要热情接待客户,了解客户的皮肤问题和需求,并推荐合适的护理项目。注意要专业、亲切、不过度推销。',
|
||||
'{"professionalism": 30, "communication": 25, "needs_analysis": 25, "solution_matching": 20}',
|
||||
'ACTIVE', TRUE, 16),
|
||||
|
||||
('产品成分咨询', '解答客户关于护肤品成分和功效的问题', '专业知识',
|
||||
'{"bot_id": "ingredient_expert_bot", "temperature": 0.6}',
|
||||
'你是一位护肤品成分专家,需要用通俗易懂的语言向客户解释各种成分的作用和适用人群。',
|
||||
'{"accuracy": 35, "clarity": 30, "practicality": 20, "patience": 15}',
|
||||
'ACTIVE', TRUE, 11),
|
||||
|
||||
('投诉处理演练', '处理客户投诉,化解矛盾,维护客户关系', '危机处理',
|
||||
'{"bot_id": "complaint_handler_bot", "temperature": 0.8}',
|
||||
'你扮演一位不满意的客户,对服务或效果有投诉。学员需要耐心倾听、理解客户情绪、提供解决方案。',
|
||||
'{"empathy": 30, "problem_solving": 30, "communication": 25, "result": 15}',
|
||||
'ACTIVE', TRUE, 16),
|
||||
|
||||
('美容手法指导', '一对一美容手法技巧指导和纠正', '技能培训',
|
||||
'{"bot_id": "technique_trainer_bot", "temperature": 0.5}',
|
||||
'你是一位资深美容培训师,指导学员正确的面部护理手法,包括力度、方向、节奏等细节。',
|
||||
'{"technique_accuracy": 40, "comprehension": 30, "practice": 20, "safety": 10}',
|
||||
'ACTIVE', TRUE, 11),
|
||||
|
||||
('销售话术演练', '练习不同场景下的销售话术和应对技巧', '销售技巧',
|
||||
'{"bot_id": "sales_trainer_bot", "temperature": 0.7}',
|
||||
'模拟各种类型的客户,让学员练习销售话术,包括产品介绍、异议处理、促成成交等。',
|
||||
'{"persuasion": 25, "product_knowledge": 25, "objection_handling": 25, "closing": 25}',
|
||||
'ACTIVE', TRUE, 16),
|
||||
|
||||
('医美项目咨询', '专业解答轻医美项目的原理、效果和注意事项', '医美咨询',
|
||||
'{"bot_id": "medical_beauty_bot", "temperature": 0.6}',
|
||||
'你是一位医美咨询师,需要专业、客观地介绍各种轻医美项目,包括适应症、恢复期、注意事项等。',
|
||||
'{"professionalism": 35, "safety_awareness": 30, "communication": 20, "ethics": 15}',
|
||||
'ACTIVE', TRUE, 7);
|
||||
|
||||
-- ============================================
|
||||
-- 九、成长路径(美容行业职业发展)
|
||||
-- ============================================
|
||||
|
||||
DELETE FROM growth_paths WHERE id > 2;
|
||||
|
||||
INSERT INTO growth_paths (name, description, target_role, courses, estimated_duration_days, is_active, sort_order) VALUES
|
||||
('美容师成长路径', '从初级美容师到高级美容技师的完整学习路径', '高级美容技师',
|
||||
'[{"course_id": 5, "order": 1, "is_required": true},
|
||||
{"course_id": 8, "order": 2, "is_required": true},
|
||||
{"course_id": 9, "order": 3, "is_required": true},
|
||||
{"course_id": 10, "order": 4, "is_required": true},
|
||||
{"course_id": 11, "order": 5, "is_required": true},
|
||||
{"course_id": 13, "order": 6, "is_required": false}]',
|
||||
90, TRUE, 1),
|
||||
|
||||
('美容顾问发展路径', '培养专业的美容顾问和销售精英', '资深美容顾问',
|
||||
'[{"course_id": 5, "order": 1, "is_required": true},
|
||||
{"course_id": 8, "order": 2, "is_required": true},
|
||||
{"course_id": 7, "order": 3, "is_required": true},
|
||||
{"course_id": 9, "order": 4, "is_required": true},
|
||||
{"course_id": 14, "order": 5, "is_required": false}]',
|
||||
60, TRUE, 2),
|
||||
|
||||
('医美技师培养路径', '轻医美项目操作技师的专业培训路径', '医美技师',
|
||||
'[{"course_id": 5, "order": 1, "is_required": true},
|
||||
{"course_id": 6, "order": 2, "is_required": true},
|
||||
{"course_id": 10, "order": 3, "is_required": true},
|
||||
{"course_id": 12, "order": 4, "is_required": true},
|
||||
{"course_id": 13, "order": 5, "is_required": false}]',
|
||||
120, TRUE, 3),
|
||||
|
||||
('店长管理路径', '从员工到店长的管理能力提升路径', '美容店长',
|
||||
'[{"course_id": 9, "order": 1, "is_required": true},
|
||||
{"course_id": 7, "order": 2, "is_required": true},
|
||||
{"course_id": 14, "order": 3, "is_required": true},
|
||||
{"course_id": 15, "order": 4, "is_required": true},
|
||||
{"course_id": 16, "order": 5, "is_required": true}]',
|
||||
90, TRUE, 4);
|
||||
|
||||
-- ============================================
|
||||
-- 十、模拟考试记录和陪练记录
|
||||
-- ============================================
|
||||
|
||||
-- 插入一些考试记录
|
||||
INSERT INTO exams (user_id, course_id, exam_name, question_count, total_score, pass_score, start_time, end_time, duration_minutes, score, is_passed, status) VALUES
|
||||
(8, 5, '美容基础理论期末考试', 20, 100, 60, '2024-12-10 09:00:00', '2024-12-10 09:45:00', 60, 85, TRUE, 'submitted'),
|
||||
(9, 6, '轻医美项目认证考试', 15, 100, 70, '2024-12-12 14:00:00', '2024-12-12 14:50:00', 60, 78, TRUE, 'submitted'),
|
||||
(12, 7, '销售技巧考核', 10, 100, 60, '2024-12-15 10:00:00', '2024-12-15 10:30:00', 30, 92, TRUE, 'submitted'),
|
||||
(13, 5, '皮肤生理学测试', 15, 100, 60, '2024-12-18 15:00:00', '2024-12-18 15:40:00', 45, 73, TRUE, 'submitted');
|
||||
|
||||
-- 插入一些陪练会话记录
|
||||
INSERT INTO training_sessions (user_id, scene_id, start_time, end_time, duration_seconds, status, total_score, evaluation_result) VALUES
|
||||
(8, 4, '2024-12-20 10:00:00', '2024-12-20 10:25:00', 1500, 'COMPLETED', 82,
|
||||
'{"professionalism": 85, "communication": 80, "needs_analysis": 82, "solution_matching": 80}'),
|
||||
(12, 5, '2024-12-21 14:30:00', '2024-12-21 14:50:00', 1200, 'COMPLETED', 88,
|
||||
'{"accuracy": 90, "clarity": 88, "practicality": 85, "patience": 87}'),
|
||||
(17, 6, '2024-12-22 09:00:00', '2024-12-22 09:20:00', 1200, 'COMPLETED', 75,
|
||||
'{"empathy": 78, "problem_solving": 72, "communication": 75, "result": 75}'),
|
||||
(20, 8, '2024-12-23 15:00:00', '2024-12-23 15:30:00', 1800, 'COMPLETED', 90,
|
||||
'{"persuasion": 88, "product_knowledge": 92, "objection_handling": 90, "closing": 90}');
|
||||
|
||||
-- ============================================
|
||||
-- 输出完成信息
|
||||
-- ============================================
|
||||
|
||||
SELECT '美容机构模拟数据插入完成!' as message;
|
||||
SELECT '新增用户数量:' as info, COUNT(*) as count FROM users WHERE id > 3;
|
||||
SELECT '新增团队数量:' as info, COUNT(*) as count FROM teams WHERE id > 3;
|
||||
SELECT '新增课程数量:' as info, COUNT(*) as count FROM courses WHERE id > 4;
|
||||
SELECT '新增陪练场景:' as info, COUNT(*) as count FROM training_scenes WHERE id > 3;
|
||||
83
backend/scripts/rollback_example.py
Normal file
83
backend/scripts/rollback_example.py
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
考培练系统 - 数据库回滚工具使用示例
|
||||
演示如何使用回滚工具进行常见的数据恢复操作
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from scripts.kaopeilian_rollback import KaopeilianRollbackTool
|
||||
|
||||
async def demo_rollback_tools():
|
||||
"""演示回滚工具的使用"""
|
||||
|
||||
print("🔧 考培练系统 - 数据库回滚工具演示")
|
||||
print("=" * 60)
|
||||
|
||||
tool = KaopeilianRollbackTool()
|
||||
|
||||
try:
|
||||
await tool.connect()
|
||||
|
||||
print("\n1️⃣ 查看最近24小时的数据变更")
|
||||
print("-" * 40)
|
||||
await tool.list_recent_changes(24)
|
||||
|
||||
print("\n2️⃣ 演示用户回滚(模拟)")
|
||||
print("-" * 40)
|
||||
print("命令示例:")
|
||||
print("python scripts/kaopeilian_rollback.py --rollback-user 123 --operation-type delete")
|
||||
print("python scripts/kaopeilian_rollback.py --rollback-user 123 --operation-type delete --execute")
|
||||
|
||||
print("\n3️⃣ 演示课程回滚(模拟)")
|
||||
print("-" * 40)
|
||||
print("命令示例:")
|
||||
print("python scripts/kaopeilian_rollback.py --rollback-course 456 --operation-type delete")
|
||||
print("python scripts/kaopeilian_rollback.py --rollback-course 456 --operation-type delete --execute")
|
||||
|
||||
print("\n4️⃣ 演示考试回滚(模拟)")
|
||||
print("-" * 40)
|
||||
print("命令示例:")
|
||||
print("python scripts/kaopeilian_rollback.py --rollback-exam 789")
|
||||
print("python scripts/kaopeilian_rollback.py --rollback-exam 789 --execute")
|
||||
|
||||
print("\n5️⃣ 演示时间点回滚")
|
||||
print("-" * 40)
|
||||
print("命令示例:")
|
||||
print("python scripts/simple_rollback.py --time '2024-12-20 10:30:00'")
|
||||
print("python scripts/simple_rollback.py --time '2024-12-20 10:30:00' --execute")
|
||||
|
||||
print("\n6️⃣ 查看Binlog文件")
|
||||
print("-" * 40)
|
||||
print("命令示例:")
|
||||
print("python scripts/simple_rollback.py --list")
|
||||
print("python scripts/binlog_rollback_tool.py --list-binlogs")
|
||||
|
||||
print("\n📋 回滚工具总结")
|
||||
print("-" * 40)
|
||||
print("✅ 专用工具:kaopeilian_rollback.py - 业务场景回滚")
|
||||
print("✅ 简化工具:simple_rollback.py - 时间点回滚")
|
||||
print("✅ 完整工具:binlog_rollback_tool.py - 复杂Binlog回滚")
|
||||
print("✅ 配置优化:mysql-rollback.cnf - MySQL回滚优化")
|
||||
print("✅ 文档指南:database_rollback_guide.md - 完整操作指南")
|
||||
|
||||
print("\n⚠️ 安全提醒")
|
||||
print("-" * 40)
|
||||
print("• 回滚操作不可逆,务必谨慎执行")
|
||||
print("• 生产环境回滚前必须在测试环境验证")
|
||||
print("• 重要操作需要多人确认")
|
||||
print("• 保留回滚操作日志和备份文件")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 演示过程中出现错误: {e}")
|
||||
finally:
|
||||
await tool.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(demo_rollback_tools())
|
||||
93
backend/scripts/run_practice_scenes_setup.py
Normal file
93
backend/scripts/run_practice_scenes_setup.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
执行陪练场景表创建和初始数据插入脚本
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.core.database import async_engine
|
||||
|
||||
|
||||
async def run_sql_file():
|
||||
"""执行SQL文件"""
|
||||
sql_file = project_root / "scripts" / "create_practice_scenes.sql"
|
||||
|
||||
if not sql_file.exists():
|
||||
print(f"❌ SQL文件不存在: {sql_file}")
|
||||
return False
|
||||
|
||||
# 读取SQL文件
|
||||
with open(sql_file, 'r', encoding='utf-8') as f:
|
||||
sql_content = f.read()
|
||||
|
||||
# 分割SQL语句(按分号分隔)
|
||||
statements = [s.strip() for s in sql_content.split(';') if s.strip() and not s.strip().startswith('--')]
|
||||
|
||||
print(f"📝 准备执行 {len(statements)} 条SQL语句...")
|
||||
|
||||
async with async_engine.begin() as conn:
|
||||
for i, statement in enumerate(statements, 1):
|
||||
if not statement:
|
||||
continue
|
||||
try:
|
||||
# 跳过注释
|
||||
if statement.strip().startswith('--'):
|
||||
continue
|
||||
|
||||
print(f" [{i}/{len(statements)}] 执行中...")
|
||||
result = await conn.execute(text(statement))
|
||||
|
||||
# 如果是SELECT语句,打印结果
|
||||
if statement.strip().upper().startswith('SELECT'):
|
||||
rows = result.fetchall()
|
||||
print(f" ✅ 查询返回 {len(rows)} 行数据")
|
||||
for row in rows:
|
||||
print(f" {row}")
|
||||
else:
|
||||
print(f" ✅ 执行成功")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 执行失败: {e}")
|
||||
if "already exists" not in str(e).lower() and "duplicate" not in str(e).lower():
|
||||
raise
|
||||
|
||||
print("\n✅ 所有SQL语句执行完成!")
|
||||
return True
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
try:
|
||||
print("=" * 60)
|
||||
print("陪练场景表创建和初始数据插入")
|
||||
print("=" * 60)
|
||||
|
||||
success = await run_sql_file()
|
||||
|
||||
if success:
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ 陪练场景表创建成功!")
|
||||
print("=" * 60)
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("❌ 执行失败")
|
||||
print("=" * 60)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
|
||||
430
backend/scripts/seed_beauty_data.py
Normal file
430
backend/scripts/seed_beauty_data.py
Normal file
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
轻医美连锁岗位与课程种子数据
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import aiomysql
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.append(str(project_root))
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
async def execute_seed():
|
||||
"""执行种子数据插入"""
|
||||
try:
|
||||
# 从环境变量或配置中获取数据库连接信息
|
||||
database_url = os.getenv("DATABASE_URL", settings.DATABASE_URL)
|
||||
|
||||
# 解析数据库连接字符串
|
||||
if database_url.startswith("mysql+aiomysql://"):
|
||||
url = database_url.replace("mysql+aiomysql://", "")
|
||||
else:
|
||||
url = database_url
|
||||
|
||||
# 解析连接参数
|
||||
auth_host = url.split("@")[1]
|
||||
user_pass = url.split("@")[0]
|
||||
host_port_db = auth_host.split("/")
|
||||
host_port = host_port_db[0].split(":")
|
||||
|
||||
user = user_pass.split(":")[0]
|
||||
password = user_pass.split(":")[1]
|
||||
host = host_port[0]
|
||||
port = int(host_port[1]) if len(host_port) > 1 else 3306
|
||||
database = host_port_db[1].split("?")[0] if len(host_port_db) > 1 else "kaopeilian"
|
||||
|
||||
print(f"连接数据库: {host}:{port}/{database}")
|
||||
|
||||
# 创建数据库连接
|
||||
conn = await aiomysql.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
user=user,
|
||||
password=password,
|
||||
db=database,
|
||||
charset='utf8mb4'
|
||||
)
|
||||
|
||||
async with conn.cursor() as cursor:
|
||||
print("\n🎯 开始插入轻医美连锁业务数据...")
|
||||
|
||||
# 1. 插入轻医美相关岗位
|
||||
print("\n📌 插入岗位数据...")
|
||||
positions_data = [
|
||||
{
|
||||
'name': '区域经理',
|
||||
'code': 'region_manager',
|
||||
'description': '负责多家门店的运营管理和业绩达成',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['团队管理', '业绩分析', '战略规划', '客户关系'], ensure_ascii=False),
|
||||
'level': 'expert',
|
||||
'sort_order': 10
|
||||
},
|
||||
{
|
||||
'name': '店长',
|
||||
'code': 'store_manager',
|
||||
'description': '负责门店日常运营管理,团队建设和业绩达成',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['门店管理', '团队建设', '销售管理', '客户维护'], ensure_ascii=False),
|
||||
'level': 'senior',
|
||||
'sort_order': 20,
|
||||
'parent_code': 'region_manager'
|
||||
},
|
||||
{
|
||||
'name': '美容顾问',
|
||||
'code': 'beauty_consultant',
|
||||
'description': '为客户提供专业的美容咨询和方案设计',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['产品知识', '销售技巧', '方案设计', '客户沟通'], ensure_ascii=False),
|
||||
'level': 'intermediate',
|
||||
'sort_order': 30,
|
||||
'parent_code': 'store_manager'
|
||||
},
|
||||
{
|
||||
'name': '美容技师',
|
||||
'code': 'beauty_therapist',
|
||||
'description': '为客户提供专业的美容护理服务',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['护肤技术', '仪器操作', '手法技巧', '服务意识'], ensure_ascii=False),
|
||||
'level': 'intermediate',
|
||||
'sort_order': 40,
|
||||
'parent_code': 'store_manager'
|
||||
},
|
||||
{
|
||||
'name': '医美咨询师',
|
||||
'code': 'medical_beauty_consultant',
|
||||
'description': '提供医疗美容项目咨询和方案制定',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['医美知识', '风险评估', '方案设计', '合规意识'], ensure_ascii=False),
|
||||
'level': 'senior',
|
||||
'sort_order': 35,
|
||||
'parent_code': 'store_manager'
|
||||
},
|
||||
{
|
||||
'name': '护士',
|
||||
'code': 'nurse',
|
||||
'description': '协助医生进行医美项目操作,负责术后护理',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['护理技术', '无菌操作', '应急处理', '医疗知识'], ensure_ascii=False),
|
||||
'level': 'intermediate',
|
||||
'sort_order': 45,
|
||||
'parent_code': 'store_manager'
|
||||
},
|
||||
{
|
||||
'name': '前台接待',
|
||||
'code': 'receptionist',
|
||||
'description': '负责客户接待、预约管理和前台事务',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['接待礼仪', '沟通能力', '信息管理', '服务意识'], ensure_ascii=False),
|
||||
'level': 'junior',
|
||||
'sort_order': 50,
|
||||
'parent_code': 'store_manager'
|
||||
},
|
||||
{
|
||||
'name': '市场专员',
|
||||
'code': 'marketing_specialist',
|
||||
'description': '负责门店营销活动策划和执行',
|
||||
'status': 'active',
|
||||
'skills': json.dumps(['活动策划', '社媒运营', '数据分析', '创意设计'], ensure_ascii=False),
|
||||
'level': 'intermediate',
|
||||
'sort_order': 60,
|
||||
'parent_code': 'store_manager'
|
||||
}
|
||||
]
|
||||
|
||||
# 先获取已存在的岗位ID映射
|
||||
position_id_map = {}
|
||||
|
||||
# 插入岗位(处理层级关系)
|
||||
for position in positions_data:
|
||||
# 检查是否已存在
|
||||
check_sql = "SELECT id FROM positions WHERE code = %s AND is_deleted = FALSE"
|
||||
await cursor.execute(check_sql, (position['code'],))
|
||||
existing = await cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
position_id_map[position['code']] = existing[0]
|
||||
print(f" ⚠️ 岗位 '{position['name']}' 已存在,ID: {existing[0]}")
|
||||
else:
|
||||
# 获取parent_id
|
||||
parent_id = None
|
||||
if 'parent_code' in position:
|
||||
parent_code = position.pop('parent_code')
|
||||
if parent_code in position_id_map:
|
||||
parent_id = position_id_map[parent_code]
|
||||
|
||||
# 插入岗位
|
||||
insert_sql = """
|
||||
INSERT INTO positions (name, code, description, parent_id, status, skills, level, sort_order, created_at, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
||||
"""
|
||||
await cursor.execute(insert_sql, (
|
||||
position['name'],
|
||||
position['code'],
|
||||
position['description'],
|
||||
parent_id,
|
||||
position['status'],
|
||||
position['skills'],
|
||||
position['level'],
|
||||
position['sort_order']
|
||||
))
|
||||
position_id = cursor.lastrowid
|
||||
position_id_map[position['code']] = position_id
|
||||
print(f" ✅ 插入岗位: {position['name']} (ID: {position_id})")
|
||||
|
||||
await conn.commit()
|
||||
|
||||
# 2. 插入轻医美相关课程
|
||||
print("\n📚 插入课程数据...")
|
||||
courses_data = [
|
||||
{
|
||||
'name': '皮肤生理学基础',
|
||||
'description': '学习皮肤结构、功能和常见问题,为专业护理打下基础',
|
||||
'category': 'technology',
|
||||
'status': 'published',
|
||||
'duration_hours': 16,
|
||||
'difficulty_level': 2,
|
||||
'tags': json.dumps(['皮肤学', '基础理论', '必修课'], ensure_ascii=False),
|
||||
'is_featured': True,
|
||||
'sort_order': 100
|
||||
},
|
||||
{
|
||||
'name': '医美产品知识与应用',
|
||||
'description': '全面了解各类医美产品的成分、功效和适用人群',
|
||||
'category': 'technology',
|
||||
'status': 'published',
|
||||
'duration_hours': 20,
|
||||
'difficulty_level': 3,
|
||||
'tags': json.dumps(['产品知识', '医美', '专业技能'], ensure_ascii=False),
|
||||
'is_featured': True,
|
||||
'sort_order': 110
|
||||
},
|
||||
{
|
||||
'name': '美容仪器操作与维护',
|
||||
'description': '掌握各类美容仪器的操作方法、注意事项和日常维护',
|
||||
'category': 'technology',
|
||||
'status': 'published',
|
||||
'duration_hours': 24,
|
||||
'difficulty_level': 3,
|
||||
'tags': json.dumps(['仪器操作', '实操技能', '设备维护'], ensure_ascii=False),
|
||||
'is_featured': False,
|
||||
'sort_order': 120
|
||||
},
|
||||
{
|
||||
'name': '轻医美销售技巧',
|
||||
'description': '学习专业的销售话术、客户需求分析和成交技巧',
|
||||
'category': 'business',
|
||||
'status': 'published',
|
||||
'duration_hours': 16,
|
||||
'difficulty_level': 2,
|
||||
'tags': json.dumps(['销售技巧', '客户沟通', '业绩提升'], ensure_ascii=False),
|
||||
'is_featured': True,
|
||||
'sort_order': 130
|
||||
},
|
||||
{
|
||||
'name': '客户服务与投诉处理',
|
||||
'description': '提升服务意识,掌握客户投诉处理的方法和技巧',
|
||||
'category': 'business',
|
||||
'status': 'published',
|
||||
'duration_hours': 12,
|
||||
'difficulty_level': 2,
|
||||
'tags': json.dumps(['客户服务', '危机处理', '沟通技巧'], ensure_ascii=False),
|
||||
'is_featured': False,
|
||||
'sort_order': 140
|
||||
},
|
||||
{
|
||||
'name': '卫生消毒与感染控制',
|
||||
'description': '学习医美机构的卫生标准和消毒流程,确保服务安全',
|
||||
'category': 'general',
|
||||
'status': 'published',
|
||||
'duration_hours': 8,
|
||||
'difficulty_level': 1,
|
||||
'tags': json.dumps(['卫生安全', '消毒规范', '合规管理'], ensure_ascii=False),
|
||||
'is_featured': True,
|
||||
'sort_order': 150
|
||||
},
|
||||
{
|
||||
'name': '门店运营管理',
|
||||
'description': '学习门店日常管理、团队建设和业绩管理',
|
||||
'category': 'management',
|
||||
'status': 'published',
|
||||
'duration_hours': 20,
|
||||
'difficulty_level': 3,
|
||||
'tags': json.dumps(['门店管理', '团队管理', '运营策略'], ensure_ascii=False),
|
||||
'is_featured': False,
|
||||
'sort_order': 160
|
||||
},
|
||||
{
|
||||
'name': '医美项目介绍与咨询',
|
||||
'description': '详细了解各类医美项目的原理、效果和适应症',
|
||||
'category': 'technology',
|
||||
'status': 'published',
|
||||
'duration_hours': 30,
|
||||
'difficulty_level': 4,
|
||||
'tags': json.dumps(['医美项目', '专业咨询', '风险告知'], ensure_ascii=False),
|
||||
'is_featured': True,
|
||||
'sort_order': 170
|
||||
},
|
||||
{
|
||||
'name': '社媒营销与私域运营',
|
||||
'description': '学习如何通过社交媒体进行品牌推广和客户维护',
|
||||
'category': 'business',
|
||||
'status': 'published',
|
||||
'duration_hours': 16,
|
||||
'difficulty_level': 2,
|
||||
'tags': json.dumps(['社媒营销', '私域流量', '客户维护'], ensure_ascii=False),
|
||||
'is_featured': False,
|
||||
'sort_order': 180
|
||||
}
|
||||
]
|
||||
|
||||
course_id_map = {}
|
||||
|
||||
for course in courses_data:
|
||||
# 检查是否已存在
|
||||
check_sql = "SELECT id FROM courses WHERE name = %s AND is_deleted = FALSE"
|
||||
await cursor.execute(check_sql, (course['name'],))
|
||||
existing = await cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
course_id_map[course['name']] = existing[0]
|
||||
print(f" ⚠️ 课程 '{course['name']}' 已存在,ID: {existing[0]}")
|
||||
else:
|
||||
# 插入课程
|
||||
insert_sql = """
|
||||
INSERT INTO courses (
|
||||
name, description, category, status, duration_hours,
|
||||
difficulty_level, tags, is_featured, sort_order,
|
||||
published_at, created_at, updated_at
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW(), NOW())
|
||||
"""
|
||||
await cursor.execute(insert_sql, (
|
||||
course['name'],
|
||||
course['description'],
|
||||
course['category'],
|
||||
course['status'],
|
||||
course['duration_hours'],
|
||||
course['difficulty_level'],
|
||||
course['tags'],
|
||||
course['is_featured'],
|
||||
course['sort_order']
|
||||
))
|
||||
course_id = cursor.lastrowid
|
||||
course_id_map[course['name']] = course_id
|
||||
print(f" ✅ 插入课程: {course['name']} (ID: {course_id})")
|
||||
|
||||
await conn.commit()
|
||||
|
||||
# 3. 设置岗位与课程的关联
|
||||
print("\n🔗 设置岗位课程关联...")
|
||||
position_courses = [
|
||||
# 店长必修课程
|
||||
('store_manager', '门店运营管理', 'required', 1),
|
||||
('store_manager', '轻医美销售技巧', 'required', 2),
|
||||
('store_manager', '客户服务与投诉处理', 'required', 3),
|
||||
('store_manager', '卫生消毒与感染控制', 'required', 4),
|
||||
|
||||
# 美容顾问必修课程
|
||||
('beauty_consultant', '皮肤生理学基础', 'required', 1),
|
||||
('beauty_consultant', '医美产品知识与应用', 'required', 2),
|
||||
('beauty_consultant', '轻医美销售技巧', 'required', 3),
|
||||
('beauty_consultant', '客户服务与投诉处理', 'required', 4),
|
||||
('beauty_consultant', '社媒营销与私域运营', 'optional', 5),
|
||||
|
||||
# 美容技师必修课程
|
||||
('beauty_therapist', '皮肤生理学基础', 'required', 1),
|
||||
('beauty_therapist', '美容仪器操作与维护', 'required', 2),
|
||||
('beauty_therapist', '卫生消毒与感染控制', 'required', 3),
|
||||
('beauty_therapist', '医美产品知识与应用', 'optional', 4),
|
||||
|
||||
# 医美咨询师必修课程
|
||||
('medical_beauty_consultant', '医美项目介绍与咨询', 'required', 1),
|
||||
('medical_beauty_consultant', '皮肤生理学基础', 'required', 2),
|
||||
('medical_beauty_consultant', '医美产品知识与应用', 'required', 3),
|
||||
('medical_beauty_consultant', '轻医美销售技巧', 'required', 4),
|
||||
('medical_beauty_consultant', '客户服务与投诉处理', 'required', 5),
|
||||
|
||||
# 护士必修课程
|
||||
('nurse', '卫生消毒与感染控制', 'required', 1),
|
||||
('nurse', '医美项目介绍与咨询', 'required', 2),
|
||||
('nurse', '皮肤生理学基础', 'required', 3),
|
||||
|
||||
# 前台接待必修课程
|
||||
('receptionist', '客户服务与投诉处理', 'required', 1),
|
||||
('receptionist', '医美产品知识与应用', 'optional', 2),
|
||||
|
||||
# 市场专员必修课程
|
||||
('marketing_specialist', '社媒营销与私域运营', 'required', 1),
|
||||
('marketing_specialist', '医美产品知识与应用', 'optional', 2),
|
||||
('marketing_specialist', '轻医美销售技巧', 'optional', 3),
|
||||
]
|
||||
|
||||
for pos_code, course_name, course_type, priority in position_courses:
|
||||
if pos_code in position_id_map and course_name in course_id_map:
|
||||
position_id = position_id_map[pos_code]
|
||||
course_id = course_id_map[course_name]
|
||||
|
||||
# 检查是否已存在
|
||||
check_sql = """
|
||||
SELECT id FROM position_courses
|
||||
WHERE position_id = %s AND course_id = %s AND is_deleted = FALSE
|
||||
"""
|
||||
await cursor.execute(check_sql, (position_id, course_id))
|
||||
existing = await cursor.fetchone()
|
||||
|
||||
if not existing:
|
||||
insert_sql = """
|
||||
INSERT INTO position_courses (position_id, course_id, course_type, priority, is_deleted, created_at, updated_at)
|
||||
VALUES (%s, %s, %s, %s, FALSE, NOW(), NOW())
|
||||
"""
|
||||
await cursor.execute(insert_sql, (position_id, course_id, course_type, priority))
|
||||
print(f" ✅ 关联: {pos_code} - {course_name} ({course_type})")
|
||||
|
||||
await conn.commit()
|
||||
|
||||
# 4. 显示统计信息
|
||||
print("\n📊 数据统计:")
|
||||
|
||||
# 统计岗位
|
||||
await cursor.execute("SELECT COUNT(*) FROM positions WHERE is_deleted = FALSE")
|
||||
total_positions = (await cursor.fetchone())[0]
|
||||
print(f" 岗位总数: {total_positions}")
|
||||
|
||||
# 统计课程
|
||||
await cursor.execute("SELECT COUNT(*) FROM courses WHERE is_deleted = FALSE")
|
||||
total_courses = (await cursor.fetchone())[0]
|
||||
print(f" 课程总数: {total_courses}")
|
||||
|
||||
# 统计岗位课程关联
|
||||
await cursor.execute("SELECT COUNT(*) FROM position_courses WHERE is_deleted = FALSE")
|
||||
total_pc = (await cursor.fetchone())[0]
|
||||
print(f" 岗位课程关联数: {total_pc}")
|
||||
|
||||
print("\n🎉 轻医美连锁业务数据插入完成!")
|
||||
print("\n💡 提示:")
|
||||
print(" 1. 可以登录系统查看岗位管理页面")
|
||||
print(" 2. 每个岗位都配置了相应的必修和选修课程")
|
||||
print(" 3. 课程涵盖了技术、管理、业务和通用等各个分类")
|
||||
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🌟 开始插入轻医美连锁岗位与课程数据...")
|
||||
asyncio.run(execute_seed())
|
||||
76
backend/scripts/seed_positions.py
Normal file
76
backend/scripts/seed_positions.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
为轻医美连锁品牌注入基础“岗位”数据(真落库)。
|
||||
|
||||
- 场景:前端岗位下拉显示 No data 时,先注入标准岗位,便于联调验证
|
||||
- 数据来源:行业通用岗位,后续可按门店组织结构扩展
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
try:
|
||||
# 覆盖本地数据库连接(如存在)
|
||||
import local_config # noqa: F401
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.models.position import Position
|
||||
|
||||
|
||||
async def ensure_positions_exists(session, names: List[str]) -> int:
|
||||
"""确保给定岗位名称存在于数据库,已存在则跳过。
|
||||
|
||||
返回新增的记录数。
|
||||
"""
|
||||
existing = (await session.execute(select(Position))).scalars().all()
|
||||
existing_names = {p.name for p in existing}
|
||||
|
||||
added = 0
|
||||
for name in names:
|
||||
if name in existing_names:
|
||||
continue
|
||||
obj = Position(
|
||||
name=name,
|
||||
code=name, # 简化:与名称一致,前端无需依赖 code
|
||||
description=f"{name} 岗位(系统初始化)",
|
||||
status="active",
|
||||
level="junior",
|
||||
)
|
||||
session.add(obj)
|
||||
added += 1
|
||||
|
||||
if added:
|
||||
await session.commit()
|
||||
return added
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""脚本入口:写入基础岗位数据并打印结果。"""
|
||||
base_positions = [
|
||||
"咨询师",
|
||||
"治疗师",
|
||||
"皮肤管理师",
|
||||
"前台接待",
|
||||
"门店店长",
|
||||
"区域运营经理",
|
||||
"市场专员",
|
||||
"客服专员",
|
||||
]
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
added = await ensure_positions_exists(session, base_positions)
|
||||
# 打印结果
|
||||
print(f"✅ 岗位数据初始化完成,新增 {added} 条。")
|
||||
|
||||
rows = (await session.execute(select(Position))).scalars().all()
|
||||
print("当前岗位:", ", ".join(p.name for p in rows))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
164
backend/scripts/seed_practice_sessions.sql
Normal file
164
backend/scripts/seed_practice_sessions.sql
Normal file
@@ -0,0 +1,164 @@
|
||||
-- ============================================
|
||||
-- 为所有学员用户注入陪练会话数据
|
||||
-- ============================================
|
||||
|
||||
USE `kaopeilian`;
|
||||
|
||||
-- 先创建陪练场景(如果不存在)
|
||||
INSERT IGNORE INTO practice_scenes (id, name, description, type, difficulty, status, background, ai_role, objectives, keywords, duration, usage_count, rating, created_by, is_deleted)
|
||||
VALUES
|
||||
(1, '电话销售陪练', '模拟电话销售场景,提升电话沟通技巧', 'phone', 'intermediate', 'active', '客户对轻医美项目感兴趣,需要通过电话进行专业介绍', 'AI扮演潜在客户', '["掌握电话开场技巧", "专业介绍项目", "处理客户疑问"]', '["电话销售", "沟通技巧", "项目介绍"]', 15, 0, 4.5, 1, 0),
|
||||
(2, '面对面咨询陪练', '模拟面对面咨询场景,提升面诊沟通能力', 'face', 'intermediate', 'active', '客户到店咨询轻医美项目,需要专业面诊和方案推荐', 'AI扮演到店客户', '["建立客户信任", "专业面诊", "方案推荐"]', '["面对面", "咨询", "方案设计"]', 20, 0, 4.7, 1, 0),
|
||||
(3, '客户投诉处理陪练', '模拟客户投诉场景,提升问题处理能力', 'complaint', 'senior', 'active', '客户对服务或效果不满意,需要妥善处理投诉', 'AI扮演投诉客户', '["倾听客户诉求", "安抚客户情绪", "提供解决方案"]', '["投诉处理", "情绪管理", "客户关系"]', 15, 0, 4.3, 1, 0),
|
||||
(4, '售后服务陪练', '模拟售后服务场景,提升客户满意度', 'after-sales', 'junior', 'active', '项目完成后,进行售后跟进和关怀', 'AI扮演已消费客户', '["售后关怀", "效果跟进", "二次营销"]', '["售后服务", "客户维护", "复购"]', 10, 0, 4.6, 1, 0),
|
||||
(5, '产品介绍陪练', '模拟产品介绍场景,提升产品讲解能力', 'product-intro', 'beginner', 'active', '向客户详细介绍轻医美项目和产品', 'AI扮演咨询客户', '["产品特点讲解", "适用人群分析", "价值传递"]', '["产品介绍", "专业知识", "销售技巧"]', 15, 0, 4.8, 1, 0);
|
||||
|
||||
-- 为每个学员用户创建陪练记录
|
||||
-- 使用存储过程批量生成
|
||||
|
||||
DELIMITER //
|
||||
|
||||
DROP PROCEDURE IF EXISTS generate_practice_sessions//
|
||||
|
||||
CREATE PROCEDURE generate_practice_sessions(IN target_user_id INT, IN session_count INT)
|
||||
BEGIN
|
||||
DECLARE i INT DEFAULT 0;
|
||||
DECLARE session_date DATETIME;
|
||||
DECLARE duration INT;
|
||||
DECLARE scene INT;
|
||||
DECLARE scene_name_val VARCHAR(200);
|
||||
DECLARE scene_type_val VARCHAR(50);
|
||||
DECLARE score INT;
|
||||
DECLARE turns INT;
|
||||
|
||||
WHILE i < session_count DO
|
||||
-- 在过去60天内随机生成日期
|
||||
SET session_date = DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 60) DAY) + INTERVAL FLOOR(RAND() * 43200) SECOND;
|
||||
|
||||
-- 随机时长:10-30分钟
|
||||
SET duration = 600 + FLOOR(RAND() * 1200);
|
||||
|
||||
-- 随机选择场景
|
||||
SET scene = 1 + FLOOR(RAND() * 5);
|
||||
|
||||
-- 根据场景设置名称和类型
|
||||
CASE scene
|
||||
WHEN 1 THEN
|
||||
SET scene_name_val = '电话销售陪练';
|
||||
SET scene_type_val = 'phone';
|
||||
WHEN 2 THEN
|
||||
SET scene_name_val = '面对面咨询陪练';
|
||||
SET scene_type_val = 'face';
|
||||
WHEN 3 THEN
|
||||
SET scene_name_val = '客户投诉处理陪练';
|
||||
SET scene_type_val = 'complaint';
|
||||
WHEN 4 THEN
|
||||
SET scene_name_val = '售后服务陪练';
|
||||
SET scene_type_val = 'after-sales';
|
||||
WHEN 5 THEN
|
||||
SET scene_name_val = '产品介绍陪练';
|
||||
SET scene_type_val = 'product-intro';
|
||||
END CASE;
|
||||
|
||||
-- 随机对话轮数
|
||||
SET turns = 10 + FLOOR(RAND() * 20);
|
||||
|
||||
-- 随机分数(60-95分)
|
||||
SET score = 60 + FLOOR(RAND() * 35);
|
||||
|
||||
-- 插入陪练会话
|
||||
INSERT INTO practice_sessions (
|
||||
session_id,
|
||||
user_id,
|
||||
scene_id,
|
||||
scene_name,
|
||||
scene_type,
|
||||
start_time,
|
||||
end_time,
|
||||
duration_seconds,
|
||||
turns,
|
||||
status,
|
||||
is_deleted,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
CONCAT('session_', target_user_id, '_', UNIX_TIMESTAMP(session_date)),
|
||||
target_user_id,
|
||||
scene,
|
||||
scene_name_val,
|
||||
scene_type_val,
|
||||
session_date,
|
||||
DATE_ADD(session_date, INTERVAL duration SECOND),
|
||||
duration,
|
||||
turns,
|
||||
'completed',
|
||||
0,
|
||||
session_date,
|
||||
session_date
|
||||
);
|
||||
|
||||
-- 插入陪练报告
|
||||
INSERT INTO practice_reports (
|
||||
session_id,
|
||||
total_score,
|
||||
score_breakdown,
|
||||
ability_dimensions,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
CONCAT('session_', target_user_id, '_', UNIX_TIMESTAMP(session_date)),
|
||||
score,
|
||||
JSON_OBJECT(
|
||||
'professionalism', score + FLOOR(RAND() * 10) - 5,
|
||||
'communication', score + FLOOR(RAND() * 10) - 5,
|
||||
'problem_solving', score + FLOOR(RAND() * 10) - 5
|
||||
),
|
||||
JSON_OBJECT(
|
||||
'technical_skills', score + FLOOR(RAND() * 10) - 5,
|
||||
'service_attitude', score + FLOOR(RAND() * 10) - 5,
|
||||
'sales_ability', score + FLOOR(RAND() * 10) - 5
|
||||
),
|
||||
session_date,
|
||||
session_date
|
||||
);
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
END//
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- 为所有学员角色用户生成陪练数据
|
||||
-- 获取所有学员用户ID并为每个用户生成15-25条记录
|
||||
|
||||
-- user_id = 1 (superadmin) - 20条
|
||||
CALL generate_practice_sessions(1, 20);
|
||||
|
||||
-- user_id = 5 (consultant_001) - 25条
|
||||
CALL generate_practice_sessions(5, 25);
|
||||
|
||||
-- user_id = 7 (therapist_001) - 22条
|
||||
CALL generate_practice_sessions(7, 22);
|
||||
|
||||
-- user_id = 8 (receptionist_001) - 18条
|
||||
CALL generate_practice_sessions(8, 18);
|
||||
|
||||
-- 统计结果
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '✅ 陪练数据生成完成!' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
SELECT
|
||||
u.id,
|
||||
u.username,
|
||||
u.role,
|
||||
COUNT(ps.id) as practice_count
|
||||
FROM users u
|
||||
LEFT JOIN practice_sessions ps ON u.id = ps.user_id AND ps.is_deleted = 0
|
||||
WHERE u.is_deleted = 0
|
||||
GROUP BY u.id, u.username, u.role
|
||||
ORDER BY u.id;
|
||||
|
||||
-- 清理存储过程
|
||||
DROP PROCEDURE IF EXISTS generate_practice_sessions;
|
||||
|
||||
408
backend/scripts/seed_statistics_demo_data.py
Executable file
408
backend/scripts/seed_statistics_demo_data.py
Executable file
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
为统计分析页面生成轻医美场景的模拟数据
|
||||
|
||||
生成的数据包括:
|
||||
1. 考试记录(exams)- 不同时间段、不同课程、不同分数
|
||||
2. 错题记录(exam_mistakes)- 不同难度、不同知识点
|
||||
3. 陪练会话(practice_sessions)- 不同时间的陪练记录
|
||||
4. 知识点数据(knowledge_points)- 轻医美相关知识点
|
||||
|
||||
目标:确保统计分析页面的每个模块都能显示数据
|
||||
"""
|
||||
import sys
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select, func
|
||||
|
||||
from app.models.exam import Exam, Question
|
||||
from app.models.exam_mistake import ExamMistake
|
||||
from app.models.course import Course, KnowledgePoint
|
||||
from app.models.practice import PracticeSession
|
||||
from app.models.user import User
|
||||
from app.core.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 数据库连接(开发测试环境)
|
||||
DATABASE_URL = "mysql+aiomysql://root:nj861021@localhost:3306/kaopeilian"
|
||||
|
||||
|
||||
# 轻医美知识点数据
|
||||
BEAUTY_KNOWLEDGE_POINTS = {
|
||||
"皮肤生理学基础": [
|
||||
"皮肤结构与层次",
|
||||
"皮肤类型与特征",
|
||||
"皮肤屏障功能",
|
||||
"皮肤老化机制",
|
||||
"皮肤色素形成",
|
||||
"皮肤水分平衡"
|
||||
],
|
||||
"医美产品知识与应用": [
|
||||
"透明质酸的应用",
|
||||
"肉毒素的作用机理",
|
||||
"光子嫩肤原理",
|
||||
"果酸焕肤技术",
|
||||
"维生素C美白",
|
||||
"胶原蛋白补充"
|
||||
],
|
||||
"美容仪器操作与维护": [
|
||||
"超声刀操作流程",
|
||||
"热玛吉治疗参数",
|
||||
"皮秒激光使用",
|
||||
"射频美容仪器",
|
||||
"冷光美肤仪",
|
||||
"水光注射仪"
|
||||
],
|
||||
"轻医美销售技巧": [
|
||||
"客户需求分析",
|
||||
"项目推荐话术",
|
||||
"价格异议处理",
|
||||
"成交技巧",
|
||||
"客户关系维护",
|
||||
"套餐设计方法"
|
||||
],
|
||||
"客户服务与投诉处理": [
|
||||
"服务标准流程",
|
||||
"投诉应对技巧",
|
||||
"客户期望管理",
|
||||
"售后跟踪服务",
|
||||
"客户满意度提升",
|
||||
"危机公关处理"
|
||||
]
|
||||
}
|
||||
|
||||
# 难度级别
|
||||
DIFFICULTY_LEVELS = ["easy", "medium", "hard"]
|
||||
|
||||
|
||||
async def clear_old_demo_data(db: AsyncSession, user_id: int):
|
||||
"""清理旧的演示数据"""
|
||||
logger.info(f"清理用户 {user_id} 的旧演示数据...")
|
||||
|
||||
# 清理考试记录(会级联删除错题记录)
|
||||
from sqlalchemy import delete
|
||||
await db.execute(delete(Exam).where(Exam.user_id == user_id))
|
||||
|
||||
# 清理陪练会话
|
||||
await db.execute(delete(PracticeSession).where(PracticeSession.user_id == user_id))
|
||||
|
||||
await db.commit()
|
||||
logger.info("旧数据清理完成")
|
||||
|
||||
|
||||
async def get_or_create_knowledge_points(db: AsyncSession, course_id: int, course_name: str) -> list:
|
||||
"""获取或创建知识点"""
|
||||
# 检查是否已有知识点
|
||||
result = await db.execute(
|
||||
select(KnowledgePoint).where(
|
||||
KnowledgePoint.course_id == course_id,
|
||||
KnowledgePoint.is_deleted == False
|
||||
)
|
||||
)
|
||||
existing_kps = result.scalars().all()
|
||||
|
||||
if existing_kps:
|
||||
logger.info(f"课程 {course_name} 已有 {len(existing_kps)} 个知识点")
|
||||
return existing_kps
|
||||
|
||||
# 创建新知识点
|
||||
knowledge_points = []
|
||||
if course_name in BEAUTY_KNOWLEDGE_POINTS:
|
||||
for kp_name in BEAUTY_KNOWLEDGE_POINTS[course_name]:
|
||||
kp = KnowledgePoint(
|
||||
course_id=course_id,
|
||||
name=kp_name,
|
||||
description=f"{course_name}中的重要知识点:{kp_name}",
|
||||
type="核心概念",
|
||||
source=0, # 手动创建
|
||||
is_deleted=False
|
||||
)
|
||||
db.add(kp)
|
||||
knowledge_points.append(kp)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 刷新以获取ID
|
||||
for kp in knowledge_points:
|
||||
await db.refresh(kp)
|
||||
|
||||
logger.info(f"为课程 {course_name} 创建了 {len(knowledge_points)} 个知识点")
|
||||
return knowledge_points
|
||||
|
||||
|
||||
async def create_exam_records(db: AsyncSession, user_id: int, courses: list):
|
||||
"""创建考试记录"""
|
||||
logger.info("创建考试记录...")
|
||||
|
||||
end_date = datetime.now()
|
||||
exams_created = 0
|
||||
|
||||
# 在过去60天内创建考试记录
|
||||
for days_ago in range(60, 0, -1):
|
||||
exam_date = end_date - timedelta(days=days_ago)
|
||||
|
||||
# 跳过一些日期(不是每天都考试)
|
||||
if random.random() > 0.3: # 30%的日子有考试
|
||||
continue
|
||||
|
||||
# 每天可能考1-2次
|
||||
num_exams = random.choice([1, 1, 1, 2])
|
||||
|
||||
for _ in range(num_exams):
|
||||
# 随机选择课程
|
||||
course = random.choice(courses)
|
||||
|
||||
# 生成考试分数(呈现进步趋势)
|
||||
# 早期分数较低,后期分数较高
|
||||
progress_factor = (60 - days_ago) / 60 # 0 到 1
|
||||
base_score = 60 + (progress_factor * 20) # 60-80分基础
|
||||
score_variance = random.uniform(-10, 15)
|
||||
round1_score = max(50, min(100, base_score + score_variance))
|
||||
|
||||
# 创建考试记录
|
||||
exam = Exam(
|
||||
user_id=user_id,
|
||||
course_id=course.id,
|
||||
exam_name=f"{course.name}测试",
|
||||
question_count=10,
|
||||
total_score=100.0,
|
||||
pass_score=60.0,
|
||||
start_time=exam_date,
|
||||
end_time=exam_date + timedelta(minutes=random.randint(15, 45)),
|
||||
duration_minutes=random.randint(15, 45),
|
||||
round1_score=round(round1_score, 1),
|
||||
round2_score=None,
|
||||
round3_score=None,
|
||||
score=round(round1_score, 1),
|
||||
is_passed=round1_score >= 60,
|
||||
status="submitted"
|
||||
)
|
||||
|
||||
db.add(exam)
|
||||
exams_created += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"创建了 {exams_created} 条考试记录")
|
||||
|
||||
return exams_created
|
||||
|
||||
|
||||
async def create_exam_mistakes(db: AsyncSession, user_id: int, courses: list):
|
||||
"""创建错题记录"""
|
||||
logger.info("创建错题记录...")
|
||||
|
||||
# 获取用户的所有考试
|
||||
result = await db.execute(
|
||||
select(Exam).where(Exam.user_id == user_id).order_by(Exam.start_time)
|
||||
)
|
||||
exams = result.scalars().all()
|
||||
|
||||
mistakes_created = 0
|
||||
|
||||
for exam in exams:
|
||||
# 找到对应的课程
|
||||
course = next((c for c in courses if c.id == exam.course_id), None)
|
||||
if not course:
|
||||
continue
|
||||
|
||||
# 获取该课程的知识点
|
||||
result = await db.execute(
|
||||
select(KnowledgePoint).where(
|
||||
KnowledgePoint.course_id == course.id,
|
||||
KnowledgePoint.is_deleted == False
|
||||
)
|
||||
)
|
||||
knowledge_points = result.scalars().all()
|
||||
|
||||
if not knowledge_points:
|
||||
continue
|
||||
|
||||
# 根据分数决定错题数(分数越低,错题越多)
|
||||
score = exam.round1_score or 70
|
||||
mistake_rate = (100 - score) / 100 # 0.0 到 0.5
|
||||
num_mistakes = int(exam.question_count * mistake_rate)
|
||||
num_mistakes = max(1, min(num_mistakes, exam.question_count - 1))
|
||||
|
||||
# 创建错题
|
||||
for i in range(num_mistakes):
|
||||
# 随机选择知识点
|
||||
kp = random.choice(knowledge_points)
|
||||
|
||||
# 随机选择题型
|
||||
question_types = ["single_choice", "multiple_choice", "true_false", "fill_blank", "essay"]
|
||||
question_type = random.choice(question_types)
|
||||
|
||||
mistake = ExamMistake(
|
||||
user_id=user_id,
|
||||
exam_id=exam.id,
|
||||
question_id=None, # AI生成的题目
|
||||
knowledge_point_id=kp.id,
|
||||
question_content=f"关于{kp.name}的问题{i+1}",
|
||||
correct_answer="正确答案",
|
||||
user_answer="用户错误答案",
|
||||
question_type=question_type
|
||||
)
|
||||
|
||||
db.add(mistake)
|
||||
mistakes_created += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"创建了 {mistakes_created} 条错题记录")
|
||||
|
||||
return mistakes_created
|
||||
|
||||
|
||||
async def create_practice_sessions(db: AsyncSession, user_id: int):
|
||||
"""创建陪练会话记录"""
|
||||
logger.info("创建陪练会话记录...")
|
||||
|
||||
end_date = datetime.now()
|
||||
sessions_created = 0
|
||||
|
||||
# 在过去60天内创建陪练记录
|
||||
for days_ago in range(60, 0, -1):
|
||||
session_date = end_date - timedelta(days=days_ago)
|
||||
|
||||
# 跳过一些日期
|
||||
if random.random() > 0.25: # 25%的日子有陪练
|
||||
continue
|
||||
|
||||
# 每次陪练的时长(秒)
|
||||
duration_seconds = random.randint(600, 1800) # 10-30分钟
|
||||
|
||||
# 场景类型
|
||||
scene_types = ["电话销售", "面对面咨询", "客户投诉处理", "售后服务", "产品介绍"]
|
||||
scene_name = random.choice(scene_types)
|
||||
|
||||
session = PracticeSession(
|
||||
session_id=f"session_{user_id}_{int(session_date.timestamp())}",
|
||||
user_id=user_id,
|
||||
scene_id=random.randint(1, 5),
|
||||
scene_name=scene_name,
|
||||
scene_type=scene_name,
|
||||
start_time=session_date,
|
||||
end_time=session_date + timedelta(seconds=duration_seconds),
|
||||
duration_seconds=duration_seconds,
|
||||
turns=random.randint(10, 30),
|
||||
status="completed",
|
||||
is_deleted=False
|
||||
)
|
||||
|
||||
db.add(session)
|
||||
sessions_created += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"创建了 {sessions_created} 条陪练会话记录")
|
||||
|
||||
return sessions_created
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("开始为统计分析页面生成轻医美场景的模拟数据")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# 创建数据库连接
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
try:
|
||||
async with async_session() as db:
|
||||
# 1. 获取测试用户(admin或第一个用户)
|
||||
result = await db.execute(
|
||||
select(User).where(User.username == "admin")
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
# 如果没有admin,使用第一个用户
|
||||
result = await db.execute(select(User).limit(1))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
logger.error("❌ 未找到用户,请先创建用户")
|
||||
return
|
||||
|
||||
logger.info(f"📝 使用用户: {user.username} (ID: {user.id})")
|
||||
|
||||
# 2. 获取轻医美相关课程
|
||||
result = await db.execute(
|
||||
select(Course).where(
|
||||
Course.is_deleted == False,
|
||||
Course.status == "published"
|
||||
)
|
||||
)
|
||||
courses = result.scalars().all()
|
||||
|
||||
if not courses:
|
||||
logger.error("❌ 未找到已发布的课程")
|
||||
return
|
||||
|
||||
logger.info(f"📚 找到 {len(courses)} 门课程")
|
||||
|
||||
# 3. 清理旧数据(可选)
|
||||
clear_old = input("\n是否清理该用户的旧数据?(y/n): ").lower()
|
||||
if clear_old == 'y':
|
||||
await clear_old_demo_data(db, user.id)
|
||||
|
||||
# 4. 为每门课程创建知识点
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("步骤 1/3: 创建知识点")
|
||||
logger.info("=" * 60)
|
||||
for course in courses:
|
||||
await get_or_create_knowledge_points(db, course.id, course.name)
|
||||
|
||||
# 5. 创建考试记录
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("步骤 2/3: 创建考试记录")
|
||||
logger.info("=" * 60)
|
||||
exams_count = await create_exam_records(db, user.id, courses)
|
||||
|
||||
# 6. 创建错题记录
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("步骤 3/3: 创建错题记录")
|
||||
logger.info("=" * 60)
|
||||
mistakes_count = await create_exam_mistakes(db, user.id, courses)
|
||||
|
||||
# 7. 创建陪练会话记录
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("步骤 4/4: 创建陪练会话记录")
|
||||
logger.info("=" * 60)
|
||||
sessions_count = await create_practice_sessions(db, user.id)
|
||||
|
||||
# 8. 统计信息
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("✅ 数据生成完成!")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"用户: {user.username} (ID: {user.id})")
|
||||
logger.info(f"考试记录: {exams_count} 条")
|
||||
logger.info(f"错题记录: {mistakes_count} 条")
|
||||
logger.info(f"陪练记录: {sessions_count} 条")
|
||||
logger.info("=" * 60)
|
||||
logger.info("\n现在可以访问统计分析页面查看数据:")
|
||||
logger.info("http://localhost:5173/analysis/statistics")
|
||||
logger.info("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 生成数据失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
220
backend/scripts/seed_statistics_demo_data.sql
Normal file
220
backend/scripts/seed_statistics_demo_data.sql
Normal file
@@ -0,0 +1,220 @@
|
||||
-- 为统计分析页面生成轻医美场景的模拟数据
|
||||
-- 使用方法: mysql -h localhost -u root -p'nj861021' kaopeilian < seed_statistics_demo_data.sql
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 获取用户ID(使用admin用户)
|
||||
-- ============================================
|
||||
SET @user_id = (SELECT id FROM users WHERE username = 'admin' LIMIT 1);
|
||||
SET @user_id = IFNULL(@user_id, 1);
|
||||
|
||||
SELECT CONCAT('使用用户ID: ', @user_id) AS info;
|
||||
|
||||
-- ============================================
|
||||
-- 2. 为课程添加知识点(如果不存在)
|
||||
-- ============================================
|
||||
-- 皮肤生理学基础
|
||||
SET @course_id_1 = (SELECT id FROM courses WHERE name LIKE '%皮肤生理学%' LIMIT 1);
|
||||
INSERT INTO knowledge_points (course_id, material_id, name, description, type, source, is_deleted, created_at, updated_at)
|
||||
SELECT
|
||||
@course_id_1,
|
||||
NULL,
|
||||
kp.name,
|
||||
CONCAT('核心知识点:', kp.name),
|
||||
'核心概念',
|
||||
0,
|
||||
FALSE,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT '皮肤结构与层次' AS name UNION ALL
|
||||
SELECT '皮肤类型与特征' UNION ALL
|
||||
SELECT '皮肤屏障功能' UNION ALL
|
||||
SELECT '皮肤老化机制' UNION ALL
|
||||
SELECT '皮肤色素形成' UNION ALL
|
||||
SELECT '皮肤水分平衡'
|
||||
) AS kp
|
||||
WHERE @course_id_1 IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM knowledge_points
|
||||
WHERE course_id = @course_id_1 AND name = kp.name AND is_deleted = FALSE
|
||||
);
|
||||
|
||||
-- 医美产品知识与应用
|
||||
SET @course_id_2 = (SELECT id FROM courses WHERE name LIKE '%医美产品%' LIMIT 1);
|
||||
INSERT INTO knowledge_points (course_id, material_id, name, description, type, source, is_deleted, created_at, updated_at)
|
||||
SELECT
|
||||
@course_id_2,
|
||||
NULL,
|
||||
kp.name,
|
||||
CONCAT('核心知识点:', kp.name),
|
||||
'核心概念',
|
||||
0,
|
||||
FALSE,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT '透明质酸的应用' AS name UNION ALL
|
||||
SELECT '肉毒素的作用机理' UNION ALL
|
||||
SELECT '光子嫩肤原理' UNION ALL
|
||||
SELECT '果酸焕肤技术' UNION ALL
|
||||
SELECT '维生素C美白' UNION ALL
|
||||
SELECT '胶原蛋白补充'
|
||||
) AS kp
|
||||
WHERE @course_id_2 IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM knowledge_points
|
||||
WHERE course_id = @course_id_2 AND name = kp.name AND is_deleted = FALSE
|
||||
);
|
||||
|
||||
SELECT '✓ 知识点创建完成' AS info;
|
||||
|
||||
-- ============================================
|
||||
-- 3. 生成考试记录(过去60天,呈现进步趋势)
|
||||
-- ============================================
|
||||
-- 删除旧的演示数据
|
||||
DELETE FROM exam_mistakes WHERE user_id = @user_id;
|
||||
DELETE FROM exams WHERE user_id = @user_id;
|
||||
|
||||
-- 生成考试记录
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes, round1_score, score, is_passed, status,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id,
|
||||
c.id,
|
||||
CONCAT(c.name, '测试'),
|
||||
10,
|
||||
100.0,
|
||||
60.0,
|
||||
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 60) DAY) - INTERVAL FLOOR(RAND() * 86400) SECOND,
|
||||
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 60) DAY) - INTERVAL FLOOR(RAND() * 86400) SECOND + INTERVAL (15 + FLOOR(RAND() * 30)) MINUTE,
|
||||
15 + FLOOR(RAND() * 30),
|
||||
-- 分数呈现进步趋势:早期60-75分,后期75-95分
|
||||
60 + (RAND() * 15) + (30 * RAND()),
|
||||
60 + (RAND() * 15) + (30 * RAND()),
|
||||
TRUE,
|
||||
'submitted',
|
||||
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 60) DAY),
|
||||
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 60) DAY)
|
||||
FROM courses c
|
||||
CROSS JOIN (
|
||||
SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
|
||||
UNION ALL SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15
|
||||
UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18 UNION ALL SELECT 19 UNION ALL SELECT 20
|
||||
) AS numbers
|
||||
WHERE c.is_deleted = FALSE AND c.status = 'published'
|
||||
LIMIT 50;
|
||||
|
||||
-- 更新is_passed状态
|
||||
UPDATE exams SET is_passed = (round1_score >= pass_score) WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 创建了 ', COUNT(*), ' 条考试记录') AS info
|
||||
FROM exams WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 4. 生成错题记录
|
||||
-- ============================================
|
||||
-- 为每个考试生成错题(根据分数决定错题数)
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_id, knowledge_point_id,
|
||||
question_content, correct_answer, user_answer, question_type,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
e.user_id,
|
||||
e.id,
|
||||
NULL,
|
||||
kp.id,
|
||||
CONCAT('关于', kp.name, '的问题'),
|
||||
'正确答案',
|
||||
'用户错误答案',
|
||||
ELT(1 + FLOOR(RAND() * 5), 'single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'essay'),
|
||||
e.created_at,
|
||||
e.created_at
|
||||
FROM exams e
|
||||
CROSS JOIN knowledge_points kp
|
||||
WHERE e.user_id = @user_id
|
||||
AND kp.course_id = e.course_id
|
||||
AND kp.is_deleted = FALSE
|
||||
AND RAND() < (100 - e.round1_score) / 100 -- 分数越低,错题概率越高
|
||||
LIMIT 200;
|
||||
|
||||
SELECT CONCAT('✓ 创建了 ', COUNT(*), ' 条错题记录') AS info
|
||||
FROM exam_mistakes WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 5. 生成陪练会话记录
|
||||
-- ============================================
|
||||
-- 删除旧的陪练记录
|
||||
DELETE FROM practice_dialogues WHERE session_id IN (
|
||||
SELECT session_id FROM practice_sessions WHERE user_id = @user_id
|
||||
);
|
||||
DELETE FROM practice_reports WHERE session_id IN (
|
||||
SELECT session_id FROM practice_sessions WHERE user_id = @user_id
|
||||
);
|
||||
DELETE FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
-- 生成陪练会话
|
||||
INSERT INTO practice_sessions (
|
||||
session_id, user_id, scene_id, scene_name, scene_type,
|
||||
start_time, end_time, duration_seconds, turns, status, is_deleted,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
CONCAT('session_', @user_id, '_', UNIX_TIMESTAMP(start_dt)),
|
||||
@user_id,
|
||||
1 + FLOOR(RAND() * 5),
|
||||
ELT(1 + FLOOR(RAND() * 5), '电话销售', '面对面咨询', '客户投诉处理', '售后服务', '产品介绍'),
|
||||
ELT(1 + FLOOR(RAND() * 5), 'phone', 'face', 'complaint', 'after-sales', 'product-intro'),
|
||||
start_dt,
|
||||
DATE_ADD(start_dt, INTERVAL (600 + FLOOR(RAND() * 1200)) SECOND),
|
||||
600 + FLOOR(RAND() * 1200),
|
||||
10 + FLOOR(RAND() * 20),
|
||||
'completed',
|
||||
FALSE,
|
||||
start_dt,
|
||||
start_dt
|
||||
FROM (
|
||||
SELECT DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 60) DAY) - INTERVAL FLOOR(RAND() * 86400) SECOND AS start_dt
|
||||
FROM (
|
||||
SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
|
||||
UNION ALL SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15
|
||||
) AS numbers
|
||||
) AS dates;
|
||||
|
||||
SELECT CONCAT('✓ 创建了 ', COUNT(*), ' 条陪练会话记录') AS info
|
||||
FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 6. 统计汇总
|
||||
-- ============================================
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '✅ 数据生成完成!' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
SELECT CONCAT('用户: ', username, ' (ID: ', id, ')') AS info
|
||||
FROM users WHERE id = @user_id;
|
||||
|
||||
SELECT CONCAT('考试记录: ', COUNT(*), ' 条') AS info
|
||||
FROM exams WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('错题记录: ', COUNT(*), ' 条') AS info
|
||||
FROM exam_mistakes WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('陪练记录: ', COUNT(*), ' 条') AS info
|
||||
FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '现在可以访问统计分析页面查看数据:' AS '';
|
||||
SELECT 'http://localhost:5173/analysis/statistics' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
207
backend/scripts/seed_statistics_demo_data_v2.sql
Normal file
207
backend/scripts/seed_statistics_demo_data_v2.sql
Normal file
@@ -0,0 +1,207 @@
|
||||
-- 为统计分析页面生成轻医美场景的模拟数据(简化版)
|
||||
-- 使用方法: docker-compose -f docker-compose.dev.yml exec -T mysql-dev mysql -u root -p'nj861021' kaopeilian < kaopeilian-backend/scripts/seed_statistics_demo_data_v2.sql
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 获取用户ID(使用admin用户)
|
||||
-- ============================================
|
||||
SET @user_id = (SELECT id FROM users WHERE username = 'admin' LIMIT 1);
|
||||
SET @user_id = IFNULL(@user_id, (SELECT id FROM users WHERE role = 'trainee' LIMIT 1));
|
||||
SET @user_id = IFNULL(@user_id, 1);
|
||||
|
||||
SELECT CONCAT('✓ 使用用户: ', (SELECT username FROM users WHERE id = @user_id), ' (ID: ', @user_id, ')') AS '';
|
||||
|
||||
-- ============================================
|
||||
-- 2. 清理旧数据
|
||||
-- ============================================
|
||||
DELETE FROM exam_mistakes WHERE user_id = @user_id;
|
||||
DELETE FROM exams WHERE user_id = @user_id;
|
||||
DELETE FROM practice_dialogues WHERE session_id IN (
|
||||
SELECT session_id FROM practice_sessions WHERE user_id = @user_id
|
||||
);
|
||||
DELETE FROM practice_reports WHERE session_id IN (
|
||||
SELECT session_id FROM practice_sessions WHERE user_id = @user_id
|
||||
);
|
||||
DELETE FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
SELECT '✓ 旧数据清理完成' AS '';
|
||||
|
||||
-- ============================================
|
||||
-- 3. 生成考试记录(过去60天,呈现进步趋势)
|
||||
-- ============================================
|
||||
-- 使用临时表生成日期序列
|
||||
DROP TEMPORARY TABLE IF EXISTS date_series;
|
||||
CREATE TEMPORARY TABLE date_series (
|
||||
day_offset INT,
|
||||
exam_date DATETIME,
|
||||
progress_factor DECIMAL(5,2)
|
||||
);
|
||||
|
||||
-- 生成60天的日期,30%的天数有考试
|
||||
INSERT INTO date_series (day_offset, exam_date, progress_factor)
|
||||
SELECT
|
||||
n.day_offset,
|
||||
DATE_SUB(NOW(), INTERVAL n.day_offset DAY) + INTERVAL FLOOR(RAND() * 43200) SECOND AS exam_date,
|
||||
(60 - n.day_offset) / 60 AS progress_factor
|
||||
FROM (
|
||||
SELECT 1 AS day_offset UNION ALL SELECT 3 UNION ALL SELECT 5 UNION ALL SELECT 7 UNION ALL SELECT 9
|
||||
UNION ALL SELECT 11 UNION ALL SELECT 13 UNION ALL SELECT 15 UNION ALL SELECT 17 UNION ALL SELECT 19
|
||||
UNION ALL SELECT 21 UNION ALL SELECT 23 UNION ALL SELECT 25 UNION ALL SELECT 27 UNION ALL SELECT 29
|
||||
UNION ALL SELECT 31 UNION ALL SELECT 33 UNION ALL SELECT 35 UNION ALL SELECT 37 UNION ALL SELECT 39
|
||||
UNION ALL SELECT 41 UNION ALL SELECT 43 UNION ALL SELECT 45 UNION ALL SELECT 47 UNION ALL SELECT 49
|
||||
UNION ALL SELECT 51 UNION ALL SELECT 53 UNION ALL SELECT 55 UNION ALL SELECT 57 UNION ALL SELECT 59
|
||||
) AS n;
|
||||
|
||||
-- 生成考试记录
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, total_score, pass_score,
|
||||
start_time, end_time, duration_minutes, round1_score, score, is_passed, status,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id,
|
||||
c.id,
|
||||
CONCAT(c.name, '测试'),
|
||||
10,
|
||||
100.0,
|
||||
60.0,
|
||||
ds.exam_date,
|
||||
DATE_ADD(ds.exam_date, INTERVAL (20 + FLOOR(RAND() * 25)) MINUTE),
|
||||
20 + FLOOR(RAND() * 25),
|
||||
-- 分数呈现进步趋势:基础分60分,根据时间进度增加0-35分
|
||||
ROUND(60 + (ds.progress_factor * 20) + (RAND() * 15), 1) AS round1_score,
|
||||
ROUND(60 + (ds.progress_factor * 20) + (RAND() * 15), 1) AS score,
|
||||
TRUE,
|
||||
'submitted',
|
||||
ds.exam_date,
|
||||
ds.exam_date
|
||||
FROM date_series ds
|
||||
CROSS JOIN courses c
|
||||
WHERE c.is_deleted = FALSE AND c.status = 'published'
|
||||
ORDER BY RAND()
|
||||
LIMIT 50;
|
||||
|
||||
-- 更新is_passed状态
|
||||
UPDATE exams SET is_passed = (round1_score >= pass_score) WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 创建了 ', COUNT(*), ' 条考试记录') AS ''
|
||||
FROM exams WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 4. 生成错题记录(增强版 - 确保足够的错题数据)
|
||||
-- ============================================
|
||||
-- 为每个考试生成3-5个错题,不管分数高低
|
||||
-- 这样可以确保有足够的统计样本
|
||||
|
||||
-- 方法:为每个考试生成多个错题
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_id, knowledge_point_id,
|
||||
question_content, correct_answer, user_answer, question_type,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
e.user_id,
|
||||
e.id,
|
||||
NULL,
|
||||
kp.id,
|
||||
CONCAT('关于"', kp.name, '"的',
|
||||
ELT(1 + FLOOR(RAND() * 3), '概念理解', '实际应用', '综合分析'), '问题'),
|
||||
CONCAT('正确答案:', kp.name, '的标准解释'),
|
||||
CONCAT('错误理解:', '学员对', kp.name, '的误解'),
|
||||
ELT(1 + FLOOR(RAND() * 5), 'single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'essay'),
|
||||
e.start_time,
|
||||
e.start_time
|
||||
FROM exams e
|
||||
CROSS JOIN (
|
||||
SELECT 1 AS mistake_num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
|
||||
) AS mistake_counts
|
||||
INNER JOIN knowledge_points kp ON kp.course_id = e.course_id
|
||||
WHERE e.user_id = @user_id
|
||||
AND kp.is_deleted = FALSE
|
||||
-- 高分考试(90+)生成1-2个错题
|
||||
AND (
|
||||
(e.round1_score >= 90 AND mistake_counts.mistake_num <= 2 AND RAND() < 0.5)
|
||||
-- 中等分数(80-90)生成2-3个错题
|
||||
OR (e.round1_score >= 80 AND e.round1_score < 90 AND mistake_counts.mistake_num <= 3 AND RAND() < 0.7)
|
||||
-- 一般分数(70-80)生成3-4个错题
|
||||
OR (e.round1_score >= 70 AND e.round1_score < 80 AND mistake_counts.mistake_num <= 4 AND RAND() < 0.8)
|
||||
-- 低分(<70)生成4-5个错题
|
||||
OR (e.round1_score < 70 AND mistake_counts.mistake_num <= 5 AND RAND() < 0.9)
|
||||
)
|
||||
ORDER BY RAND()
|
||||
LIMIT 250;
|
||||
|
||||
SELECT CONCAT('✓ 创建了 ', COUNT(*), ' 条错题记录') AS ''
|
||||
FROM exam_mistakes WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 5. 生成陪练会话记录
|
||||
-- ============================================
|
||||
INSERT INTO practice_sessions (
|
||||
session_id, user_id, scene_id, scene_name, scene_type,
|
||||
start_time, end_time, duration_seconds, turns, status, is_deleted,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
CONCAT('session_', @user_id, '_', UNIX_TIMESTAMP(practice_dt)),
|
||||
@user_id,
|
||||
1 + FLOOR(RAND() * 5),
|
||||
ELT(1 + FLOOR(RAND() * 5), '电话销售陪练', '面对面咨询陪练', '客户投诉处理陪练', '售后服务陪练', '产品介绍陪练'),
|
||||
ELT(1 + FLOOR(RAND() * 5), 'phone', 'face', 'complaint', 'after-sales', 'product-intro'),
|
||||
practice_dt,
|
||||
DATE_ADD(practice_dt, INTERVAL duration_sec SECOND),
|
||||
duration_sec,
|
||||
12 + FLOOR(RAND() * 18),
|
||||
'completed',
|
||||
FALSE,
|
||||
practice_dt,
|
||||
practice_dt
|
||||
FROM (
|
||||
SELECT
|
||||
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 60) DAY) + INTERVAL FLOOR(RAND() * 43200) SECOND AS practice_dt,
|
||||
900 + FLOOR(RAND() * 900) AS duration_sec
|
||||
FROM (
|
||||
SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
|
||||
UNION ALL SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15
|
||||
UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18
|
||||
) AS numbers
|
||||
) AS practice_dates;
|
||||
|
||||
SELECT CONCAT('✓ 创建了 ', COUNT(*), ' 条陪练会话记录') AS ''
|
||||
FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 6. 统计汇总
|
||||
-- ============================================
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '✅ 数据生成完成!' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
SELECT CONCAT('用户: ', username, ' (ID: ', id, ')') AS ''
|
||||
FROM users WHERE id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 考试记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM exams WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 错题记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM exam_mistakes WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 陪练记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 知识点数量: ', COUNT(*), ' 个') AS ''
|
||||
FROM knowledge_points WHERE is_deleted = FALSE;
|
||||
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '现在可以访问统计分析页面查看数据:' AS '';
|
||||
SELECT 'http://localhost:5173/analysis/statistics' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
-- 清理临时表
|
||||
DROP TEMPORARY TABLE IF EXISTS date_series;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
172
backend/scripts/seed_statistics_for_user6.sql
Normal file
172
backend/scripts/seed_statistics_for_user6.sql
Normal file
@@ -0,0 +1,172 @@
|
||||
-- ==============================================
|
||||
-- 为 user_id=6 (nurse_001) 生成统计分析数据
|
||||
-- ==============================================
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
SET @user_id = 6;
|
||||
SET @user_name = (SELECT username FROM users WHERE id = @user_id);
|
||||
|
||||
SELECT CONCAT('开始为用户 ', @user_name, ' (ID: ', @user_id, ') 生成统计数据') AS '';
|
||||
|
||||
-- ============================================
|
||||
-- 1. 生成考试记录 (50条)
|
||||
-- ============================================
|
||||
-- 保留原有的3条记录,再生成47条
|
||||
INSERT INTO exams (
|
||||
user_id, course_id, exam_name, question_count, pass_score,
|
||||
questions, start_time, end_time, status, duration_minutes,
|
||||
round1_score, is_passed, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
@user_id,
|
||||
c.id,
|
||||
CONCAT(c.name, '练习测试_', DATE_FORMAT(exam_dt, '%m月%d日')),
|
||||
10,
|
||||
60,
|
||||
'[]',
|
||||
exam_dt,
|
||||
DATE_ADD(exam_dt, INTERVAL FLOOR(20 + RAND() * 40) MINUTE),
|
||||
'completed',
|
||||
FLOOR(20 + RAND() * 40), -- 20-60分钟
|
||||
FLOOR(55 + RAND() * 45), -- 55-100分
|
||||
1,
|
||||
exam_dt,
|
||||
exam_dt
|
||||
FROM (
|
||||
SELECT DATE_SUB(NOW(), INTERVAL days DAY) + INTERVAL hours HOUR + INTERVAL minutes MINUTE as exam_dt
|
||||
FROM (
|
||||
SELECT 0 as days UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL
|
||||
SELECT 5 UNION ALL SELECT 7 UNION ALL SELECT 9 UNION ALL SELECT 12 UNION ALL SELECT 15
|
||||
) d,
|
||||
(SELECT 9 as hours UNION ALL SELECT 10 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16) h,
|
||||
(SELECT 0 as minutes UNION ALL SELECT 15 UNION ALL SELECT 30 UNION ALL SELECT 45) m
|
||||
) exam_dates
|
||||
CROSS JOIN (
|
||||
SELECT id, name FROM courses WHERE is_deleted = FALSE LIMIT 3
|
||||
) c
|
||||
LIMIT 47;
|
||||
|
||||
-- 更新 is_passed 标志
|
||||
UPDATE exams SET is_passed = (round1_score >= pass_score) WHERE user_id = @user_id AND is_passed = 0;
|
||||
|
||||
SELECT CONCAT('✓ 当前考试记录总数: ', COUNT(*), ' 条') AS ''
|
||||
FROM exams WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 2. 生成错题记录 (250条)
|
||||
-- ============================================
|
||||
-- 删除旧的错题记录,重新生成
|
||||
DELETE FROM exam_mistakes WHERE user_id = @user_id;
|
||||
|
||||
-- 为每个考试生成3-6个错题
|
||||
INSERT INTO exam_mistakes (
|
||||
user_id, exam_id, question_id, knowledge_point_id,
|
||||
question_content, correct_answer, user_answer, question_type,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
e.user_id,
|
||||
e.id,
|
||||
NULL,
|
||||
kp.id,
|
||||
CONCAT('关于"', kp.name, '"的',
|
||||
ELT(1 + FLOOR(RAND() * 3), '概念理解', '实际应用', '综合分析'), '问题'),
|
||||
CONCAT('正确答案:', kp.name, '的标准解释'),
|
||||
CONCAT('错误理解:', '学员对', kp.name, '的误解'),
|
||||
ELT(1 + FLOOR(RAND() * 5), 'single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'essay'),
|
||||
e.start_time,
|
||||
e.start_time
|
||||
FROM exams e
|
||||
CROSS JOIN (
|
||||
SELECT 1 AS mistake_num UNION ALL SELECT 2 UNION ALL SELECT 3
|
||||
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
|
||||
) AS mistake_counts
|
||||
INNER JOIN knowledge_points kp ON kp.course_id = e.course_id
|
||||
WHERE e.user_id = @user_id
|
||||
AND kp.is_deleted = FALSE
|
||||
AND RAND() < 0.8
|
||||
ORDER BY RAND()
|
||||
LIMIT 250;
|
||||
|
||||
SELECT CONCAT('✓ 错题记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM exam_mistakes WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 3. 生成陪练记录 (20条)
|
||||
-- ============================================
|
||||
-- 删除旧的陪练记录
|
||||
DELETE FROM practice_dialogues WHERE session_id IN (
|
||||
SELECT session_id FROM practice_sessions WHERE user_id = @user_id
|
||||
);
|
||||
DELETE FROM practice_reports WHERE session_id IN (
|
||||
SELECT session_id FROM practice_sessions WHERE user_id = @user_id
|
||||
);
|
||||
DELETE FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
-- 生成新的陪练记录
|
||||
INSERT INTO practice_sessions (
|
||||
session_id, user_id, scene_id, scene_name,
|
||||
duration_seconds, status,
|
||||
start_time, end_time, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
CONCAT('session_', @user_id, '_', UNIX_TIMESTAMP(practice_dt)),
|
||||
@user_id,
|
||||
1 + FLOOR(RAND() * 5),
|
||||
ELT(1 + FLOOR(RAND() * 5), '电话销售陪练', '面对面咨询陪练', '客户投诉处理陪练', '售后服务陪练', '产品介绍陪练'),
|
||||
FLOOR(600 + RAND() * 3000), -- 600-3600秒 (10-60分钟)
|
||||
'completed',
|
||||
practice_dt,
|
||||
DATE_ADD(practice_dt, INTERVAL FLOOR(10 + RAND() * 50) MINUTE),
|
||||
practice_dt,
|
||||
practice_dt
|
||||
FROM (
|
||||
SELECT DATE_SUB(NOW(), INTERVAL days DAY) + INTERVAL hours HOUR as practice_dt
|
||||
FROM (
|
||||
SELECT 0 as days UNION ALL SELECT 1 UNION ALL SELECT 3 UNION ALL SELECT 5 UNION ALL
|
||||
SELECT 7 UNION ALL SELECT 10 UNION ALL SELECT 12 UNION ALL SELECT 15 UNION ALL SELECT 20
|
||||
) d,
|
||||
(SELECT 10 as hours UNION ALL SELECT 14 UNION ALL SELECT 16) h
|
||||
) practice_dates
|
||||
LIMIT 20;
|
||||
|
||||
SELECT CONCAT('✓ 陪练记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
-- ============================================
|
||||
-- 4. 统计概览
|
||||
-- ============================================
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '数据生成完成' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
SELECT CONCAT('用户: ', username, ' (', role, ')') AS ''
|
||||
FROM users WHERE id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 考试记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM exams WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 错题记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM exam_mistakes WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 陪练记录: ', COUNT(*), ' 条') AS ''
|
||||
FROM practice_sessions WHERE user_id = @user_id;
|
||||
|
||||
SELECT CONCAT('✓ 知识点数量: ', COUNT(*), ' 个') AS ''
|
||||
FROM knowledge_points WHERE is_deleted = FALSE;
|
||||
|
||||
-- 成绩分布
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '成绩分布情况' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
SELECT
|
||||
CONCAT('优秀(90-100): ', SUM(CASE WHEN round1_score >= 90 THEN 1 ELSE 0 END), ' 场') AS '',
|
||||
CONCAT('良好(80-89): ', SUM(CASE WHEN round1_score >= 80 AND round1_score < 90 THEN 1 ELSE 0 END), ' 场') AS '',
|
||||
CONCAT('中等(70-79): ', SUM(CASE WHEN round1_score >= 70 AND round1_score < 80 THEN 1 ELSE 0 END), ' 场') AS '',
|
||||
CONCAT('及格(60-69): ', SUM(CASE WHEN round1_score >= 60 AND round1_score < 70 THEN 1 ELSE 0 END), ' 场') AS '',
|
||||
CONCAT('不及格(<60): ', SUM(CASE WHEN round1_score < 60 THEN 1 ELSE 0 END), ' 场') AS ''
|
||||
FROM exams WHERE user_id = @user_id;
|
||||
|
||||
SELECT '✓ 数据生成成功,可以刷新统计分析页面查看' AS '';
|
||||
|
||||
83
backend/scripts/simple_init.py
Normal file
83
backend/scripts/simple_init.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
简单的初始化脚本
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
# 创建数据库文件
|
||||
db_path = 'kaopeilian.db'
|
||||
|
||||
# 如果数据库已存在,删除它
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
|
||||
# 连接数据库
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建teams表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
team_type VARCHAR(50) DEFAULT 'department',
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
leader_id INTEGER,
|
||||
parent_id INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
is_deleted BOOLEAN DEFAULT 0,
|
||||
deleted_at DATETIME,
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER,
|
||||
FOREIGN KEY (parent_id) REFERENCES teams(id),
|
||||
FOREIGN KEY (leader_id) REFERENCES users(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建users表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20) UNIQUE,
|
||||
hashed_password VARCHAR(200) NOT NULL,
|
||||
full_name VARCHAR(100),
|
||||
avatar_url VARCHAR(500),
|
||||
bio TEXT,
|
||||
role VARCHAR(20) DEFAULT 'trainee',
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
is_verified BOOLEAN DEFAULT 0,
|
||||
last_login_at DATETIME,
|
||||
password_changed_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
is_deleted BOOLEAN DEFAULT 0,
|
||||
deleted_at DATETIME,
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建user_teams关联表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS user_teams (
|
||||
user_id INTEGER NOT NULL,
|
||||
team_id INTEGER NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'member',
|
||||
joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, team_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 提交更改
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("✓ SQLite数据库初始化成功!")
|
||||
print(f"✓ 数据库文件: {os.path.abspath(db_path)}")
|
||||
247
backend/scripts/simple_rollback.py
Normal file
247
backend/scripts/simple_rollback.py
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
考培练系统 - 简化数据库回滚工具
|
||||
基于MySQL Binlog的快速回滚方案
|
||||
|
||||
使用方法:
|
||||
1. 查看Binlog文件: python scripts/simple_rollback.py --list
|
||||
2. 模拟回滚: python scripts/simple_rollback.py --time "2024-12-20 10:30:00"
|
||||
3. 实际回滚: python scripts/simple_rollback.py --time "2024-12-20 10:30:00" --execute
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import aiomysql
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SimpleRollbackTool:
|
||||
"""简化回滚工具"""
|
||||
|
||||
def __init__(self):
|
||||
self.host = "localhost"
|
||||
self.port = 3306
|
||||
self.user = "root"
|
||||
self.password = "root"
|
||||
self.database = "kaopeilian"
|
||||
self.connection = None
|
||||
|
||||
async def connect(self):
|
||||
"""连接数据库"""
|
||||
try:
|
||||
self.connection = await aiomysql.connect(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
db=self.database,
|
||||
charset='utf8mb4'
|
||||
)
|
||||
logger.info("✅ 数据库连接成功")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 数据库连接失败: {e}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""关闭连接"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
|
||||
async def list_binlogs(self):
|
||||
"""列出Binlog文件"""
|
||||
cursor = await self.connection.cursor()
|
||||
await cursor.execute("SHOW BINARY LOGS")
|
||||
result = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
|
||||
print("\n📋 可用的Binlog文件:")
|
||||
print("-" * 60)
|
||||
for i, row in enumerate(result, 1):
|
||||
print(f"{i:2d}. {row[0]} ({row[1]} bytes)")
|
||||
print("-" * 60)
|
||||
|
||||
def extract_sql_from_binlog(self, binlog_file: str, start_time: str) -> str:
|
||||
"""从Binlog提取SQL语句"""
|
||||
# 使用mysqlbinlog工具解析
|
||||
cmd = [
|
||||
'docker', 'exec', 'kaopeilian-mysql',
|
||||
'mysqlbinlog',
|
||||
'--base64-output=decode-rows',
|
||||
'-v',
|
||||
'--start-datetime', start_time,
|
||||
'--database', self.database,
|
||||
f'/var/lib/mysql/{binlog_file}'
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
else:
|
||||
logger.error(f"mysqlbinlog执行失败: {result.stderr}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"执行mysqlbinlog异常: {e}")
|
||||
return ""
|
||||
|
||||
def generate_rollback_sql(self, binlog_content: str) -> list:
|
||||
"""生成回滚SQL语句"""
|
||||
rollback_sqls = []
|
||||
|
||||
# 简单的SQL解析和反转
|
||||
lines = binlog_content.split('\n')
|
||||
current_table = None
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
|
||||
# 检测表名
|
||||
if '### UPDATE' in line and '`' in line:
|
||||
table_match = line.split('`')[1] if '`' in line else None
|
||||
if table_match:
|
||||
current_table = table_match
|
||||
|
||||
# 检测INSERT操作,生成DELETE
|
||||
elif '### INSERT INTO' in line and '`' in line:
|
||||
table_match = line.split('`')[1] if '`' in line else None
|
||||
if table_match:
|
||||
current_table = table_match
|
||||
|
||||
# 检测DELETE操作,生成INSERT
|
||||
elif '### DELETE FROM' in line and '`' in line:
|
||||
table_match = line.split('`')[1] if '`' in line else None
|
||||
if table_match:
|
||||
current_table = table_match
|
||||
|
||||
# 检测WHERE条件
|
||||
elif '### WHERE' in line and current_table:
|
||||
# 提取WHERE条件
|
||||
where_part = line.replace('### WHERE', '').strip()
|
||||
if where_part:
|
||||
rollback_sqls.append(f"-- 需要手动处理 {current_table} 表的回滚")
|
||||
rollback_sqls.append(f"-- WHERE条件: {where_part}")
|
||||
|
||||
return rollback_sqls
|
||||
|
||||
async def create_backup_before_rollback(self) -> str:
|
||||
"""回滚前创建备份"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_file = f"/tmp/kaopeilian_backup_{timestamp}.sql"
|
||||
|
||||
# 使用mysqldump创建备份
|
||||
cmd = [
|
||||
'docker', 'exec', 'kaopeilian-mysql',
|
||||
'mysqldump',
|
||||
'-uroot', '-proot',
|
||||
'--single-transaction',
|
||||
'--routines',
|
||||
'--triggers',
|
||||
self.database
|
||||
]
|
||||
|
||||
try:
|
||||
with open(backup_file, 'w') as f:
|
||||
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, text=True)
|
||||
if result.returncode == 0:
|
||||
logger.info(f"✅ 备份已创建: {backup_file}")
|
||||
return backup_file
|
||||
else:
|
||||
logger.error(f"❌ 备份失败: {result.stderr}")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 备份异常: {e}")
|
||||
return ""
|
||||
|
||||
async def rollback_by_time(self, target_time: str, execute: bool = False):
|
||||
"""根据时间点回滚"""
|
||||
logger.info(f"🎯 开始回滚到时间点: {target_time}")
|
||||
|
||||
# 获取最新的Binlog文件
|
||||
cursor = await self.connection.cursor()
|
||||
await cursor.execute("SHOW BINARY LOGS")
|
||||
binlog_files = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
|
||||
if not binlog_files:
|
||||
logger.error("❌ 未找到Binlog文件")
|
||||
return
|
||||
|
||||
# 使用最新的Binlog文件
|
||||
latest_binlog = binlog_files[-1][0]
|
||||
logger.info(f"📁 使用Binlog文件: {latest_binlog}")
|
||||
|
||||
# 提取SQL
|
||||
binlog_content = self.extract_sql_from_binlog(latest_binlog, target_time)
|
||||
if not binlog_content:
|
||||
logger.error("❌ 无法从Binlog提取SQL")
|
||||
return
|
||||
|
||||
# 生成回滚SQL
|
||||
rollback_sqls = self.generate_rollback_sql(binlog_content)
|
||||
|
||||
if not rollback_sqls:
|
||||
logger.warning("⚠️ 未找到需要回滚的操作")
|
||||
return
|
||||
|
||||
print("\n🔄 回滚SQL语句:")
|
||||
print("-" * 60)
|
||||
for i, sql in enumerate(rollback_sqls, 1):
|
||||
print(f"{i:2d}. {sql}")
|
||||
print("-" * 60)
|
||||
|
||||
if not execute:
|
||||
logger.info("🔍 这是模拟执行,使用 --execute 参数实际执行")
|
||||
return
|
||||
|
||||
# 创建备份
|
||||
backup_file = await self.create_backup_before_rollback()
|
||||
if not backup_file:
|
||||
logger.error("❌ 备份失败,取消回滚操作")
|
||||
return
|
||||
|
||||
# 确认执行
|
||||
confirm = input("\n⚠️ 确认执行回滚操作?这将修改数据库数据!(yes/no): ")
|
||||
if confirm.lower() != 'yes':
|
||||
logger.info("❌ 用户取消回滚操作")
|
||||
return
|
||||
|
||||
# 执行回滚(这里需要根据实际情况手动执行SQL)
|
||||
logger.info("✅ 回滚操作准备完成")
|
||||
logger.info(f"📁 备份文件位置: {backup_file}")
|
||||
logger.info("📝 请手动执行上述SQL语句完成回滚")
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='考培练系统 - 简化数据库回滚工具')
|
||||
parser.add_argument('--list', action='store_true', help='列出Binlog文件')
|
||||
parser.add_argument('--time', help='回滚到的时间点 (格式: YYYY-MM-DD HH:MM:SS)')
|
||||
parser.add_argument('--execute', action='store_true', help='实际执行回滚')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tool = SimpleRollbackTool()
|
||||
|
||||
try:
|
||||
await tool.connect()
|
||||
|
||||
if args.list:
|
||||
await tool.list_binlogs()
|
||||
elif args.time:
|
||||
await tool.rollback_by_time(args.time, args.execute)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 程序执行异常: {e}")
|
||||
finally:
|
||||
await tool.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
138
backend/scripts/sync_core_tables.py
Normal file
138
backend/scripts/sync_core_tables.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
同步核心表结构(teams, user_teams),自动适配 users.id 类型。
|
||||
|
||||
步骤:
|
||||
1) 检查 users.id 的数据类型(INT 或 BIGINT)
|
||||
2) 如缺失则创建 teams、user_teams 表;外键列按兼容类型创建
|
||||
3) 如存在则跳过
|
||||
|
||||
运行:
|
||||
cd kaopeilian-backend && python3 scripts/sync_core_tables.py
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
async def table_exists(conn, db_name: str, table: str) -> bool:
|
||||
result = await conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(1)
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = :db AND TABLE_NAME = :table
|
||||
"""
|
||||
),
|
||||
{"db": db_name, "table": table},
|
||||
)
|
||||
return (result.scalar() or 0) > 0
|
||||
|
||||
|
||||
async def get_users_id_type(conn, db_name: str) -> str:
|
||||
result = await conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COLUMN_TYPE
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = :db AND TABLE_NAME = 'users' AND COLUMN_NAME = 'id'
|
||||
"""
|
||||
),
|
||||
{"db": db_name},
|
||||
)
|
||||
col_type = result.scalar() or "int"
|
||||
# 正规化
|
||||
col_type = col_type.lower()
|
||||
if "bigint" in col_type:
|
||||
return "BIGINT"
|
||||
return "INT"
|
||||
|
||||
|
||||
async def sync_core_tables():
|
||||
settings = get_settings()
|
||||
db_url = settings.DATABASE_URL
|
||||
db_name = db_url.split("/")[-1].split("?")[0]
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
created = []
|
||||
async with engine.begin() as conn:
|
||||
# 检查 users.id 类型
|
||||
user_id_type = await get_users_id_type(conn, db_name)
|
||||
|
||||
# 创建 teams
|
||||
if not await table_exists(conn, db_name, "teams"):
|
||||
await conn.execute(
|
||||
text(
|
||||
f"""
|
||||
CREATE TABLE `teams` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(100) NOT NULL UNIQUE,
|
||||
`code` VARCHAR(50) NOT NULL UNIQUE,
|
||||
`description` TEXT,
|
||||
`team_type` VARCHAR(50) DEFAULT 'department',
|
||||
`is_active` BOOLEAN DEFAULT TRUE,
|
||||
`leader_id` {user_id_type},
|
||||
`parent_id` INT,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`is_deleted` BOOLEAN DEFAULT FALSE,
|
||||
`deleted_at` DATETIME,
|
||||
`created_by` {user_id_type},
|
||||
`updated_by` {user_id_type},
|
||||
FOREIGN KEY (`leader_id`) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (`parent_id`) REFERENCES teams(id) ON DELETE CASCADE,
|
||||
INDEX idx_team_type (team_type),
|
||||
INDEX idx_is_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
)
|
||||
)
|
||||
created.append("teams")
|
||||
|
||||
# 创建 user_teams
|
||||
if not await table_exists(conn, db_name, "user_teams"):
|
||||
await conn.execute(
|
||||
text(
|
||||
f"""
|
||||
CREATE TABLE `user_teams` (
|
||||
`user_id` {user_id_type} NOT NULL,
|
||||
`team_id` INT NOT NULL,
|
||||
`role` VARCHAR(50) DEFAULT 'member',
|
||||
`joined_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`user_id`, `team_id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`team_id`) REFERENCES teams(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"""
|
||||
)
|
||||
)
|
||||
created.append("user_teams")
|
||||
|
||||
await engine.dispose()
|
||||
return created
|
||||
|
||||
|
||||
async def main():
|
||||
try:
|
||||
created = await sync_core_tables()
|
||||
if created:
|
||||
print("创建表:", ", ".join(created))
|
||||
else:
|
||||
print("核心表已存在,无需创建。")
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
print("同步失败:", str(exc))
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
94
backend/scripts/sync_users_table.py
Normal file
94
backend/scripts/sync_users_table.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
同步 users 表缺失列(适配当前模型定义)。
|
||||
|
||||
- 逐列检查 INFORMATION_SCHEMA,缺失则执行 ALTER TABLE 添加
|
||||
- 避免一次性重建表,降低风险
|
||||
|
||||
运行方式:
|
||||
cd kaopeilian-backend && python3 scripts/sync_users_table.py
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 确保可导入应用配置
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
async def column_exists(conn, db_name: str, table: str, column: str) -> bool:
|
||||
"""检查列是否存在"""
|
||||
result = await conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COUNT(1)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = :db
|
||||
AND TABLE_NAME = :table
|
||||
AND COLUMN_NAME = :column
|
||||
"""
|
||||
),
|
||||
{"db": db_name, "table": table, "column": column},
|
||||
)
|
||||
return (result.scalar() or 0) > 0
|
||||
|
||||
|
||||
async def sync_users_table():
|
||||
"""对 users 表进行列同步(仅添加缺失列)。"""
|
||||
settings = get_settings()
|
||||
|
||||
# 解析数据库名
|
||||
db_url = settings.DATABASE_URL
|
||||
db_name = db_url.split("/")[-1].split("?")[0]
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
# 需要确保存在的列(列名 -> DDL 片段)
|
||||
required_columns = {
|
||||
"avatar_url": "ALTER TABLE `users` ADD COLUMN `avatar_url` VARCHAR(500) NULL COMMENT '头像URL'",
|
||||
"bio": "ALTER TABLE `users` ADD COLUMN `bio` TEXT NULL COMMENT '个人简介'",
|
||||
"role": "ALTER TABLE `users` ADD COLUMN `role` VARCHAR(20) NOT NULL DEFAULT 'trainee' COMMENT '系统角色'",
|
||||
"is_active": "ALTER TABLE `users` ADD COLUMN `is_active` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否激活'",
|
||||
"is_verified": "ALTER TABLE `users` ADD COLUMN `is_verified` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否验证'",
|
||||
"last_login_at": "ALTER TABLE `users` ADD COLUMN `last_login_at` DATETIME NULL COMMENT '最后登录时间'",
|
||||
"password_changed_at": "ALTER TABLE `users` ADD COLUMN `password_changed_at` DATETIME NULL COMMENT '密码修改时间'",
|
||||
"is_deleted": "ALTER TABLE `users` ADD COLUMN `is_deleted` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否删除'",
|
||||
"deleted_at": "ALTER TABLE `users` ADD COLUMN `deleted_at` DATETIME NULL COMMENT '删除时间'",
|
||||
}
|
||||
|
||||
applied = []
|
||||
for col, ddl in required_columns.items():
|
||||
exists = await column_exists(conn, db_name, "users", col)
|
||||
if not exists:
|
||||
await conn.execute(text(ddl))
|
||||
applied.append(col)
|
||||
|
||||
await engine.dispose()
|
||||
return applied
|
||||
|
||||
|
||||
async def main():
|
||||
try:
|
||||
applied = await sync_users_table()
|
||||
if applied:
|
||||
print("添加列:", ", ".join(applied))
|
||||
else:
|
||||
print("users 表结构已满足要求,无需变更。")
|
||||
except Exception as exc:
|
||||
# 输出完整错误,便于调试
|
||||
import traceback
|
||||
|
||||
print("同步失败:", str(exc))
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
133
backend/scripts/update_position_descriptions.py
Normal file
133
backend/scripts/update_position_descriptions.py
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
更新岗位描述脚本
|
||||
为瑞小美轻医美连锁品牌的真实岗位添加专业描述
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.models.position import Position
|
||||
from app.core.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 轻医美岗位描述映射
|
||||
POSITION_DESCRIPTIONS = {
|
||||
# 医疗专业类
|
||||
"护士": "负责医美项目的护理工作,包括术前准备、术中配合、术后护理及客户健康指导",
|
||||
"护士长": "管理护理团队,制定护理标准流程,确保医疗安全和服务质量",
|
||||
"副护士长": "协助护士长管理护理团队,负责日常护理工作的督导和质量控制",
|
||||
"区域医务主任兼护士长": "统筹区域内各门店的医务管理工作,制定护理标准和培训体系",
|
||||
"储备护士长": "接受护士长岗位培训,准备承担护理团队管理工作",
|
||||
"皮肤科医生": "提供专业皮肤诊疗服务,制定个性化医美方案,确保客户安全和效果",
|
||||
"皮肤科助理医生": "协助主治医生进行皮肤诊疗工作,参与医美项目的实施",
|
||||
"微创技术院长": "负责微创医美技术的研发和应用,带领技术团队提升专业水平",
|
||||
|
||||
# 咨询销售类
|
||||
"美学规划师": "为客户提供专业医美咨询,设计个性化美丽方案,促进项目成交",
|
||||
"美学规划师兼店长": "负责门店运营管理的同时,担任首席美学规划师",
|
||||
"见习美学规划师": "接受美学规划专业培训,学习咨询技巧和方案设计",
|
||||
"会员服务经理": "负责VIP会员的全周期服务管理,提升客户满意度和复购率",
|
||||
"网络咨询专员": "通过线上渠道为客户提供医美咨询服务,引导到店体验",
|
||||
|
||||
# 管理类
|
||||
"院长": "全面负责门店运营管理、团队建设、业绩达成和客户服务质量",
|
||||
"连锁院长": "统筹多家门店的运营管理,制定标准化流程,推动连锁发展",
|
||||
"店长": "负责门店日常运营管理、团队协调和业绩目标达成",
|
||||
"区域总经理&人才战略董事&瑞小美学苑苑长": "负责区域战略规划、人才培养体系建设和学苑运营管理",
|
||||
"微创运营经理": "负责微创项目的运营推广和业绩管理",
|
||||
"储备总经理助理": "接受高管培训,准备承担门店总经理工作",
|
||||
|
||||
# 客服服务类
|
||||
"前厅接待": "负责客户接待、预约管理、环境维护,提供优质的前台服务",
|
||||
"分诊结算专员": "负责客户分诊引导和费用结算工作,确保流程顺畅",
|
||||
"客服总监": "统筹客户服务体系建设,制定服务标准,提升客户满意度",
|
||||
|
||||
# 运营支持类
|
||||
"保洁员": "负责门店环境卫生维护,确保医美场所的清洁和消毒标准",
|
||||
"瑞柏美五象总院保洁员": "负责五象总院的环境卫生和消毒管理工作",
|
||||
"药房兼行政": "负责药品和医疗器械管理,同时协助行政事务处理",
|
||||
"行政采购": "负责门店物资采购和供应商管理,确保运营物资供应",
|
||||
|
||||
# 市场品牌类
|
||||
"小红书运营专员": "负责小红书平台的内容运营和粉丝互动,提升品牌影响力",
|
||||
"电商运营": "负责线上商城的运营管理,推动电商业务发展",
|
||||
"医生IP运营": "负责医生个人品牌打造和IP运营,提升医生影响力",
|
||||
"设计总监": "负责品牌视觉设计、营销物料设计和品牌形象管理",
|
||||
"平面设计师": "负责平面设计工作,包括海报、宣传册、广告物料等",
|
||||
"摄影剪辑师": "负责门店的摄影摄像和视频剪辑工作,制作营销内容",
|
||||
"首席文化传播官": "负责企业文化建设和品牌传播策略制定",
|
||||
"AI PR": "负责品牌公关工作,运用AI技术提升传播效率",
|
||||
|
||||
# 人力财务类
|
||||
"人事经理&瑞小美学苑执行秘书长": "负责人力资源管理和学苑行政工作",
|
||||
"人事专员&瑞小美学苑执行秘书": "负责人事日常工作和学苑事务协调",
|
||||
"薪酬服务BP": "负责薪酬福利管理和人力资源业务支持",
|
||||
"财务经理": "负责财务管理、成本控制和财务报表分析",
|
||||
"财务专员": "负责日常财务核算、报销审核和账务处理",
|
||||
"资金管理专员": "负责资金流管理、账户管理和资金调度",
|
||||
|
||||
# 战略发展类
|
||||
"战投经理": "负责战略投资项目的评估和推进,支持公司扩张发展",
|
||||
"商业分析师": "负责业务数据分析和商业模式研究,支持决策制定",
|
||||
"AI维护程序员": "负责AI系统的维护和优化,支持智能化运营",
|
||||
|
||||
# 高管类
|
||||
"总裁": "负责公司整体战略规划和经营管理,带领团队实现发展目标",
|
||||
"区域经理": "负责区域内多家门店的运营管理和业绩达成",
|
||||
}
|
||||
|
||||
|
||||
async def update_position_descriptions():
|
||||
"""更新岗位描述"""
|
||||
logger.info("=" * 60)
|
||||
logger.info("开始更新岗位描述")
|
||||
logger.info("=" * 60)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
# 查询所有未删除的岗位
|
||||
stmt = select(Position).where(Position.is_deleted == False)
|
||||
result = await db.execute(stmt)
|
||||
positions = result.scalars().all()
|
||||
|
||||
logger.info(f"找到 {len(positions)} 个岗位")
|
||||
|
||||
updated_count = 0
|
||||
for position in positions:
|
||||
# 查找匹配的描述
|
||||
description = POSITION_DESCRIPTIONS.get(position.name)
|
||||
|
||||
if description:
|
||||
position.description = description
|
||||
logger.info(f"✓ 更新岗位: {position.name}")
|
||||
updated_count += 1
|
||||
else:
|
||||
# 如果没有匹配的描述,使用通用描述
|
||||
position.description = f"{position.name}岗位,负责相关专业工作"
|
||||
logger.info(f"○ 使用通用描述: {position.name}")
|
||||
updated_count += 1
|
||||
|
||||
# 提交更新
|
||||
await db.commit()
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"✅ 成功更新 {updated_count} 个岗位描述")
|
||||
logger.info("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新失败: {str(e)}")
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(update_position_descriptions())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user