File size: 10,288 Bytes
78c5cf9
67a8b0c
 
37ea041
67a8b0c
78c5cf9
 
 
 
 
 
 
 
 
 
37ea041
 
 
 
 
 
 
 
 
 
78c5cf9
67a8b0c
 
 
 
 
 
 
 
 
 
37ea041
 
 
 
 
 
 
 
67a8b0c
 
37ea041
 
 
 
 
 
 
 
 
 
 
67a8b0c
37ea041
67a8b0c
 
 
 
 
 
 
 
 
 
 
 
 
37ea041
67a8b0c
 
 
 
 
37ea041
 
 
 
 
 
67a8b0c
 
 
 
 
37ea041
67a8b0c
37ea041
67a8b0c
 
 
 
 
 
37ea041
67a8b0c
 
 
 
 
bdfccdc
67a8b0c
37ea041
bdfccdc
 
 
67a8b0c
bdfccdc
37ea041
bdfccdc
37ea041
bdfccdc
37ea041
 
 
 
bdfccdc
37ea041
bdfccdc
37ea041
 
 
 
 
67a8b0c
 
bdfccdc
67a8b0c
bdfccdc
37ea041
67a8b0c
 
37ea041
 
bdfccdc
 
37ea041
 
 
 
 
 
 
 
 
 
 
 
 
bdfccdc
37ea041
bdfccdc
37ea041
bdfccdc
37ea041
 
 
 
 
 
 
 
 
bdfccdc
37ea041
 
 
 
 
 
 
 
 
67a8b0c
37ea041
 
 
 
 
 
 
 
 
bdfccdc
37ea041
 
 
67a8b0c
 
 
37ea041
 
 
 
67a8b0c
37ea041
 
 
67a8b0c
 
37ea041
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67a8b0c
 
 
 
 
 
 
 
 
bdfccdc
67a8b0c
 
 
bdfccdc
78c5cf9
 
 
 
67a8b0c
78c5cf9
 
67a8b0c
78c5cf9
67a8b0c
 
 
78c5cf9
 
 
 
bdfccdc
 
 
67a8b0c
 
 
8d16fdf
 
 
 
 
 
 
78c5cf9
 
 
 
bdfccdc
78c5cf9
 
 
 
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
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272

const { 
    User, Student, Score, AttendanceModel, ClassModel, 
    Course, ConfigModel 
} = require('./models');

/**
 * 格式化当前日期
 */
const getCurrentDateInfo = () => {
    const now = new Date();
    const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
    return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}${days[now.getDay()]}`;
};

/**
 * 辅助函数:解析学年前缀
 * 例如 "2023-2024学年 第二学期" -> "2023-2024学年"
 */
const getSchoolYearPrefix = (semester) => {
    if (!semester) return null;
    const match = semester.match(/^(.+?学年)/);
    return match ? match[1] : null;
};

/**
 * 构建学生画像上下文 (学生视角)
 */
async function buildStudentContext(username, schoolId) {
    const student = await Student.findOne({ 
        $or: [{ studentNo: username }, { name: username }],
        schoolId 
    });

    if (!student) return "无法找到该学生的详细档案。";

    // 获取当前学期配置
    const config = await ConfigModel.findOne({ key: 'main' });
    const currentSemester = config ? config.semester : null;
    
    // 逻辑:尝试拉取本学年的所有数据 (第一学期 + 第二学期)
    const schoolYearPrefix = getSchoolYearPrefix(currentSemester);

    const scoreQuery = { 
        studentNo: student.studentNo, 
        schoolId 
    };
    
    if (schoolYearPrefix) {
        // 匹配该学年开头的所有学期
        scoreQuery.semester = { $regex: new RegExp('^' + schoolYearPrefix) };
    } else if (currentSemester) {
        scoreQuery.semester = currentSemester;
    }

    // 获取成绩 (稍微放宽限制以容纳整年数据)
    const recentScores = await Score.find(scoreQuery).sort({ semester: -1, _id: -1 }).limit(100);

    // 获取考勤概况
    const attendanceStats = await AttendanceModel.aggregate([
        { $match: { studentId: student._id.toString() } },
        { $group: { _id: "$status", count: { $sum: 1 } } }
    ]);
    const absentCount = attendanceStats.find(a => a._id === 'Absent')?.count || 0;
    const leaveCount = attendanceStats.find(a => a._id === 'Leave')?.count || 0;

    let prompt = `
### 当前用户身份:学生 (个人视图)
- **姓名**: ${student.name}
- **班级**: ${student.className}
- **学号**: ${student.studentNo}
- **积分(小红花)**: ${student.flowerBalance} 🌺
- **当前学期**: ${currentSemester || '全部'}

### 个人学习数据
`;

    if (recentScores.length > 0) {
        // 格式化输出,带上学期标识
        const scoreList = recentScores.map(s => {
            const semShort = s.semester ? s.semester.replace(schoolYearPrefix || '', '').trim() : '';
            return `${s.courseName}: ${s.score} (${semShort} ${s.examName||s.type})`;
        }).join('\n');
        prompt += `#### 成绩记录 (本学年):\n${scoreList}\n`;
    } else {
        prompt += `- **近期成绩**: 暂无记录\n`;
    }

    if (absentCount > 0 || leaveCount > 0) {
        prompt += `- **考勤**: 缺勤 ${absentCount} 次,请假 ${leaveCount} 次。\n`;
    } else {
        prompt += `- **考勤**: 全勤。\n`;
    }

    return prompt;
}

/**
 * 构建教师画像上下文 (严格权限版 + 全量数据)
 */
async function buildTeacherContext(username, schoolId) {
    const user = await User.findOne({ username, schoolId });
    if (!user) return "无法找到该教师档案。";

    const homeroomClass = user.homeroomClass; 
    
    // 1. 查找任教课程信息 (确定科任权限)
    const courses = await Course.find({ 
        schoolId, 
        $or: [{ teacherId: user._id }, { teacherName: user.trueName || user.username }] 
    });

    // 2. 确定有权限的班级列表
    const authorizedClasses = new Set();
    const teachingSubjectsMap = {}; // Map<ClassName, Set<SubjectName>>

    // 班主任权限:拥有该班级所有数据权限
    if (homeroomClass) {
        authorizedClasses.add(homeroomClass);
    }

    // 科任权限:拥有特定班级的特定科目权限
    courses.forEach(c => {
        if (c.className) {
            authorizedClasses.add(c.className);
            if (!teachingSubjectsMap[c.className]) teachingSubjectsMap[c.className] = new Set();
            teachingSubjectsMap[c.className].add(c.courseName);
        }
    });

    const classList = Array.from(authorizedClasses);

    if (classList.length === 0) {
        return `### 当前用户身份:教师 (${user.trueName})\n目前系统显示您未绑定任何班级。请告知用户去“班级管理”或“课程安排”进行绑定。`;
    }

    // 3. 全量拉取相关班级的学生
    const students = await Student.find({ 
        schoolId, 
        className: { $in: classList },
        status: 'Enrolled' 
    }).sort({ seatNo: 1, studentNo: 1 }); // 按座号排序

    if (students.length === 0) {
        return `### 当前用户身份:教师\n管理班级: [${classList.join(', ')}]\n但系统未在这些班级找到学生档案。`;
    }

    const studentNos = students.map(s => s.studentNo);

    // 4. 拉取这些学生的成绩 (限制为本学年:包含第一学期和第二学期)
    const config = await ConfigModel.findOne({ key: 'main' });
    const currentSemester = config ? config.semester : null;
    const schoolYearPrefix = getSchoolYearPrefix(currentSemester);

    const scoreQuery = { 
        schoolId, 
        studentNo: { $in: studentNos },
        status: 'Normal'
    };

    // 关键修复:使用正则匹配整个学年 (例如 "2024-2025学年")
    if (schoolYearPrefix) {
        scoreQuery.semester = { $regex: new RegExp('^' + schoolYearPrefix) };
    } else if (currentSemester) {
        // 如果无法解析年份,回退到当前学期
        scoreQuery.semester = currentSemester;
    }

    const allScores = await Score.find(scoreQuery);

    // 5. 构建 Prompt
    let prompt = `
### 当前用户身份:教师 (${user.trueName || username})
### 权限范围 (严格遵守)
你只能回答下列班级的数据。如果用户询问其他班级(例如用户只教一年级,却问四年级),请礼貌拒绝,说明权限不足。
管理班级: [${classList.join(', ')}]
数据范围: ${schoolYearPrefix ? schoolYearPrefix + " (全学年)" : (currentSemester || '所有历史')}

### 详细班级数据
`;

    for (const cls of classList) {
        const isClassHomeroom = cls === homeroomClass;
        const subjects = teachingSubjectsMap[cls] ? Array.from(teachingSubjectsMap[cls]) : [];
        const roleText = isClassHomeroom ? "班主任 (全科权限)" : `任课老师 (科目: ${subjects.join(', ')})`;
        
        prompt += `\n#### 🏫 ${cls} [${roleText}]\n`;
        
        const clsStudents = students.filter(s => s.className === cls);
        if (clsStudents.length === 0) {
            prompt += "(暂无学生)\n";
            continue;
        }

        prompt += `学生总数: ${clsStudents.length}人\n名单及成绩 (格式: [学期]科目:分数):\n`;

        for (const s of clsStudents) {
            let sScores = allScores.filter(sc => sc.studentNo === s.studentNo);
            
            // 如果不是班主任,仅展示自己教的科目成绩
            if (!isClassHomeroom && subjects.length > 0) {
                sScores = sScores.filter(sc => subjects.includes(sc.courseName));
            }

            // 格式: 张三(01号): [一]语文:90, [二]语文:85...
            const scoreStr = sScores.length > 0 
                ? sScores.map(sc => {
                    // 简化学期显示,例如 "第一学期" -> "一", "第二学期" -> "二"
                    let semLabel = "";
                    if (sc.semester && schoolYearPrefix) {
                        if (sc.semester.includes("第一")) semLabel = "[上]";
                        else if (sc.semester.includes("第二")) semLabel = "[下]";
                    }
                    return `${semLabel}${sc.courseName}:${sc.score}`;
                }).join(', ') 
                : (isClassHomeroom ? "暂无本学年成绩" : "无本科目成绩");

            prompt += `- ${s.name} (${s.seatNo ? s.seatNo+'号' : '无座号'}): ${scoreStr}\n`;
        }
    }

    return prompt;
}

/**
 * 构建管理员/校长画像上下文
 */
async function buildAdminContext(role, schoolId) {
    return `### 当前用户身份:${role === 'PRINCIPAL' ? '校长' : '超级管理员'}\n你拥有最高权限,但请注意保护隐私。`;
}

/**
 * 主入口
 */
async function buildUserContext(username, role, schoolId) {
    try {
        const dateStr = getCurrentDateInfo();
        let roleContext = "";

        if (role === 'STUDENT') {
            roleContext = await buildStudentContext(username, schoolId);
        } else if (role === 'TEACHER') {
            roleContext = await buildTeacherContext(username, schoolId);
        } else if (role === 'ADMIN' || role === 'PRINCIPAL') {
            roleContext = await buildAdminContext(role, schoolId);
        }

        return `
---
【系统注入上下文】
当前时间: ${dateStr}
以下是当前用户权限范围内的 **全量真实数据**。
${roleContext}

【AI 行为准则】
1. **角色设定**: 你是学校的AI智能助手。你的职责是协助查询校内信息,同时也乐于回答通用的百科/日常问题。
2. **数据查询规则**:
   - 当用户询问 **学校内部数据** (如: 学生成绩、考勤、班级名单、老师信息) 时,**必须且只能** 使用上述提供的【系统注入上下文】。
   - 如果用户询问的 **校内数据** 不在上下文中 (例如: 用户只教一年级,却问三年级的数据),请明确告知“无权限查看该数据”。
3. **通用问答规则**:
   - 当用户询问 **非校内数据** (如: 天气、历史、写代码、翻译、闲聊) 时,请忽略权限限制,利用你的通用知识库或联网搜索功能正常回答。不要因为上下文中没有天气数据就拒绝回答。
4. **联网搜索**: 如果用户开启了联网搜索,积极搜索最新信息回答通用问题。
---
`;
    } catch (e) {
        console.error("Context build failed:", e);
        return ""; 
    }
}

module.exports = { buildUserContext };