feat: improve report and comment workflows

This commit is contained in:
2026-06-27 01:59:31 +08:00
parent 203f3b9ec1
commit df3b623144
26 changed files with 1068 additions and 560 deletions
+147 -15
View File
@@ -1,6 +1,8 @@
import 'reflect-metadata'
import { randomUUID } from 'crypto'
import { Brackets, type Repository } from 'typeorm'
import { BrowserWindow } from 'electron'
import {
StudentProfileEntitySchema,
@@ -10,6 +12,8 @@ import { ClassEntitySchema, type ClassEntity } from '../entities/ClassEntity'
import type {
ChildProfile,
ClassProfile,
CommentGenerationProgress,
GenerateStudentCommentsInput,
ListStudentProfilesInput,
LoadStudentDataInput
} from '../types/student'
@@ -118,11 +122,14 @@ function mapClassEntity(entity: ClassEntity): ClassProfile {
name: entity.name,
type: normalizeClassType(entity.type),
teacherNames: parseTeacherNames(entity.teacherNames || entity.teacherName || ''),
familyPhoto: entity.familyPhoto ?? undefined
familyPhoto: entity.familyPhoto || undefined
}
}
function mapChildEntity(entity: StudentProfileEntity, options: { includeImages?: boolean } = {}): ChildProfile {
function mapChildEntity(
entity: StudentProfileEntity,
options: { includeImages?: boolean } = {}
): ChildProfile {
return {
id: entity.id,
classId: entity.classId,
@@ -154,7 +161,8 @@ async function mapClassProfile(profile: ClassProfile): Promise<ClassEntity> {
type: profile.type,
teacherName: (profile.teacherNames ?? []).join(' ') || null,
teacherNames: stringifyTeacherNames(profile.teacherNames ?? []),
familyPhoto: (await storeImageValue('classes', profile.id, 'familyPhoto', profile.familyPhoto)) || null
familyPhoto:
(await storeImageValue('classes', profile.id, 'familyPhoto', profile.familyPhoto)) || null
}
}
@@ -205,6 +213,16 @@ function buildStudentCommentPrompt(profile: ChildProfile): string {
].join('\n')
}
function getStudentDisplayName(profile: ChildProfile | StudentProfileEntity): string {
return profile.name || profile.englishName || '未命名学生'
}
function sendCommentProgress(progress: CommentGenerationProgress): void {
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send('comments:generation-progress', progress)
}
}
async function getRepositories(): Promise<{
classRepository: Repository<ClassEntity>
childRepository: Repository<StudentProfileEntity>
@@ -268,8 +286,12 @@ export async function loadStudentData(input: LoadStudentDataInput = {}): Promise
}
}
const hydratedClasses = includeImages
? await Promise.all(normalizedClasses.map(hydrateClassImages))
: normalizedClasses
return {
classes: (await Promise.all(normalizedClasses.map(hydrateClassImages))).map(mapClassEntity),
classes: hydratedClasses.map(mapClassEntity),
profiles: hydratedProfiles.map((profile) => mapChildEntity(profile, { includeImages }))
}
}
@@ -369,15 +391,10 @@ export async function updateStudentProfile(profile: ChildProfile): Promise<Child
return mapChildEntity(await hydrateStudentImages(storedProfile))
}
export async function generateStudentComment(profileId: string): Promise<ChildProfile> {
const source = await getAppDataSource()
const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
const entity = await repository.findOneBy({ id: profileId })
if (!entity) {
throw new Error('学生信息不存在')
}
async function generateStudentCommentForEntity(
entity: StudentProfileEntity,
repository: Repository<StudentProfileEntity>
): Promise<ChildProfile> {
const profile = mapChildEntity(entity)
const settings = await loadSettings()
@@ -451,6 +468,117 @@ export async function generateStudentComment(profileId: string): Promise<ChildPr
return mapChildEntity(await hydrateStudentImages(storedProfile))
}
export async function generateStudentComment(profileId: string): Promise<ChildProfile> {
const source = await getAppDataSource()
const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
const entity = await repository.findOneBy({ id: profileId })
if (!entity) {
throw new Error('学生信息不存在')
}
return generateStudentCommentForEntity(entity, repository)
}
export async function generateStudentComments(input: GenerateStudentCommentsInput): Promise<{
profiles: ChildProfile[]
skipped: Array<{ studentName: string; reason: string }>
}> {
const source = await getAppDataSource()
const repository = source.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
const selectedIds = new Set(input.profileIds ?? [])
const allProfiles = await repository.find({
where: input.classId ? { classId: input.classId } : undefined,
order: {
name: 'ASC',
id: 'ASC'
}
})
const targetProfiles =
selectedIds.size > 0
? allProfiles.filter((profile) => selectedIds.has(profile.id))
: allProfiles
if (targetProfiles.length === 0) {
throw new Error('没有可生成评语的学生')
}
const batchId = randomUUID()
const className = targetProfiles[0]?.className
const profiles: ChildProfile[] = []
const skipped: Array<{ studentName: string; reason: string }> = []
sendCommentProgress({
batchId,
status: 'started',
classId: input.classId,
className,
current: 0,
total: targetProfiles.length
})
for (const [profileIndex, profile] of targetProfiles.entries()) {
const studentName = getStudentDisplayName(profile)
try {
sendCommentProgress({
batchId,
status: 'student-started',
classId: profile.classId,
className: profile.className,
studentId: profile.id,
studentName,
current: profileIndex + 1,
total: targetProfiles.length
})
const nextProfile = await generateStudentCommentForEntity(profile, repository)
profiles.push(nextProfile)
sendCommentProgress({
batchId,
status: 'student-finished',
classId: profile.classId,
className: profile.className,
studentId: profile.id,
studentName,
current: profileIndex + 1,
total: targetProfiles.length,
profile: nextProfile
})
} catch (error) {
const reason = error instanceof Error ? error.message : '生成失败'
skipped.push({
studentName,
reason
})
sendCommentProgress({
batchId,
status: 'student-failed',
classId: profile.classId,
className: profile.className,
studentId: profile.id,
studentName,
current: profileIndex + 1,
total: targetProfiles.length,
error: reason
})
}
}
sendCommentProgress({
batchId,
status: 'finished',
classId: input.classId,
className,
current: targetProfiles.length,
total: targetProfiles.length
})
return { profiles, skipped }
}
export async function saveClasses(classes: ClassProfile[]): Promise<void> {
const source = await getAppDataSource()
@@ -491,7 +619,9 @@ export async function migrateLocalStudentData(
}
await classRepository.save(await Promise.all(classes.map(mapClassProfile)))
await childRepository.save(await Promise.all(profiles.map((profile) => mapChildProfile(profile))))
await childRepository.save(
await Promise.all(profiles.map((profile) => mapChildProfile(profile)))
)
})
}
@@ -515,7 +645,9 @@ export async function deleteClassWithProfiles(classId: string): Promise<void> {
const source = await getAppDataSource()
await source.transaction(async (manager) => {
await manager.getRepository<StudentProfileEntity>(StudentProfileEntitySchema).delete({ classId })
await manager
.getRepository<StudentProfileEntity>(StudentProfileEntitySchema)
.delete({ classId })
await manager.getRepository<ClassEntity>(ClassEntitySchema).delete({ id: classId })
})
}