feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View File

View 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;

View 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;

View File

@@ -0,0 +1,290 @@
-- 模拟数据脚本:考试成绩和错题记录
-- 创建时间2025-10-12
-- 说明:为成绩报告和错题本页面添加演示数据
-- 场景:轻医美连锁品牌员工培训考试
USE kaopeilian;
-- ========================================
-- 一、插入考试记录(包含三轮得分)
-- ========================================
-- 用户5consultant_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;

View File

@@ -0,0 +1,78 @@
-- ============================================
-- 为 testuser 添加考试记录
-- ============================================
USE `kaopeilian`;
-- 设置 testuser 的 ID
SET @test_user_id = 1;
-- 获取第一个课程IDPython基础课程
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;

View 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", "高级", "并发"]');

View 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执行完成!")

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,10 @@
-- 修改users表email字段为可空
-- 用于支持员工同步功能,部分员工可能没有邮箱
-- 修改email字段为可空
ALTER TABLE users MODIFY COLUMN email VARCHAR(100) NULL COMMENT '邮箱';
-- 验证修改
DESCRIBE users;

View 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())

View 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 "$@"

View 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())

View 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 "$@"

View 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())

View 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())

View 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;

View 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');

View 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;

View 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()

View 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())

View 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())

View 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;

View 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
View 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
View 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 "祝开发顺利! 🚀"

View 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())

View 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()

View 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-51最简单
请以 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()

View 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;

View 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())

View 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())

View 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())

View 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())

View 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;

View 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())

View 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;

View 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;

View 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 '';

View 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)}")

View 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())

View 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())

View 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())

View 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())