Skip to content

Commit 5837a2a

Browse files
committed
重构AutoScoreManager 更方便添加actions和triggers
1 parent db3f05c commit 5837a2a

23 files changed

+1063
-413
lines changed

src/main/services/AutoScoreService.ts

Lines changed: 107 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Service } from '../../shared/kernel'
22
import { MainContext } from '../context'
33
import { student } from '../repos/StudentRepository'
4+
import { getTriggerLogic } from '../../shared/triggers'
45

56
interface AutoScoreRule {
67
id: number
@@ -129,7 +130,6 @@ export class AutoScoreService extends Service {
129130
const data = await fs.readJsonFile<AutoScoreRulesFile>(RULES_FILE_NAME, 'automatic')
130131
if (data && data.rules) {
131132
this.rules = data.rules.map((rule: any) => {
132-
// 数据迁移:将旧格式转换为新格式
133133
const migratedRule = this.migrateRule(rule)
134134
return {
135135
...migratedRule,
@@ -138,7 +138,6 @@ export class AutoScoreService extends Service {
138138
: undefined
139139
}
140140
})
141-
// 如果有数据迁移,保存新格式
142141
if (
143142
data.rules.some(
144143
(rule: any) => rule.intervalMinutes !== undefined || rule.scoreValue !== undefined
@@ -159,12 +158,10 @@ export class AutoScoreService extends Service {
159158
}
160159

161160
private migrateRule(rule: any): AutoScoreRule {
162-
// 如果已经是新格式,直接返回
163161
if (!rule.intervalMinutes && !rule.scoreValue) {
164162
return rule
165163
}
166164

167-
// 迁移旧格式到新格式
168165
const migratedRule: AutoScoreRule = {
169166
id: rule.id,
170167
enabled: rule.enabled,
@@ -175,7 +172,6 @@ export class AutoScoreService extends Service {
175172
actions: rule.actions || []
176173
}
177174

178-
// 将intervalMinutes迁移到triggers
179175
if (
180176
rule.intervalMinutes &&
181177
!migratedRule.triggers?.find((t) => t.event === 'interval_time_passed')
@@ -187,7 +183,6 @@ export class AutoScoreService extends Service {
187183
})
188184
}
189185

190-
// 将scoreValue和reason迁移到actions
191186
if (
192187
rule.scoreValue !== undefined &&
193188
!migratedRule.actions?.find((a) => a.event === 'add_score')
@@ -280,25 +275,28 @@ export class AutoScoreService extends Service {
280275
this.timers.delete(rule.id)
281276
}
282277

283-
// 从triggers中读取间隔时间
284-
const intervalTrigger = rule.triggers?.find((t) => t.event === 'interval_time_passed')
285-
const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0
278+
const now = new Date()
279+
let delayMs = 0
280+
let primaryTrigger: { event: string; value?: string } | undefined
281+
282+
for (const trigger of rule.triggers || []) {
283+
const logic = getTriggerLogic(trigger.event)
284+
if (logic?.calculateNextTime) {
285+
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
286+
if (delayMs === 0 || result.delayMs < delayMs) {
287+
delayMs = result.delayMs
288+
primaryTrigger = trigger
289+
}
290+
}
291+
}
286292

287-
if (!intervalMinutes || intervalMinutes <= 0) {
288-
this.logger.warn(`Rule ${rule.name} has no valid interval time, skipping timer`)
293+
if (!primaryTrigger) {
294+
this.logger.warn(`Rule ${rule.name} has no valid triggers with timing logic, skipping`)
289295
return
290296
}
291297

292-
const now = new Date()
293-
const intervalMs = intervalMinutes * 60 * 1000
294-
295-
let delayMs = intervalMs
296-
if (rule.lastExecuted) {
297-
const timeSinceLastExecution = now.getTime() - rule.lastExecuted.getTime()
298-
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
299-
if (timeSinceLastExecution >= intervalMs) {
300-
delayMs = 0
301-
}
298+
if (delayMs < 0) {
299+
delayMs = 0
302300
}
303301

304302
const timer = setTimeout(() => {
@@ -307,21 +305,32 @@ export class AutoScoreService extends Service {
307305
}, delayMs)
308306

309307
this.timers.set(rule.id, timer)
308+
this.logger.info(`Rule ${rule.name} scheduled to execute in ${delayMs}ms`)
310309
}
311310

312311
private setRuleInterval(rule: AutoScoreRule) {
313-
// 从triggers中读取间隔时间
314-
const intervalTrigger = rule.triggers?.find((t) => t.event === 'interval_time_passed')
315-
const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0
312+
const now = new Date()
313+
let minDelayMs = Infinity
314+
let primaryTrigger: { event: string; value?: string } | undefined
315+
316+
for (const trigger of rule.triggers || []) {
317+
const logic = getTriggerLogic(trigger.event)
318+
if (logic?.calculateNextTime) {
319+
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
320+
if (result.delayMs < minDelayMs) {
321+
minDelayMs = result.delayMs
322+
primaryTrigger = trigger
323+
}
324+
}
325+
}
316326

317-
if (!intervalMinutes || intervalMinutes <= 0) {
327+
if (!primaryTrigger || minDelayMs === Infinity) {
318328
return
319329
}
320330

321-
const intervalMs = intervalMinutes * 60 * 1000
322331
const timer = setInterval(() => {
323332
this.executeRule(rule)
324-
}, intervalMs)
333+
}, minDelayMs)
325334

326335
this.timers.set(rule.id, timer)
327336
}
@@ -331,7 +340,6 @@ export class AutoScoreService extends Service {
331340
this.logger.info(`Executing auto score rule: ${rule.name}`)
332341

333342
const studentRepo = this.mainCtx.students
334-
const eventRepo = this.mainCtx.events
335343

336344
let studentsToScore: student[] = []
337345
if (rule.studentNames.length === 0) {
@@ -347,17 +355,30 @@ export class AutoScoreService extends Service {
347355
}
348356
}
349357

350-
// 从actions中读取分数和理由
351-
const scoreAction = rule.actions?.find((a) => a.event === 'add_score')
352-
const scoreValue = scoreAction?.value ? parseInt(scoreAction.value, 10) : 0
353-
const reason = scoreAction?.reason || `自动化加分 - ${rule.name}`
358+
for (const trigger of rule.triggers || []) {
359+
const logic = getTriggerLogic(trigger.event)
360+
if (logic?.check) {
361+
const context = {
362+
students: studentsToScore,
363+
events: [],
364+
rule: {
365+
id: rule.id,
366+
name: rule.name,
367+
studentNames: rule.studentNames,
368+
triggers: rule.triggers,
369+
actions: rule.actions
370+
},
371+
now: new Date()
372+
}
373+
const result = logic.check(context, trigger.value || '')
374+
if (result.matchedStudents && result.matchedStudents.length > 0) {
375+
studentsToScore = result.matchedStudents
376+
}
377+
}
378+
}
354379

355-
for (const student of studentsToScore) {
356-
await eventRepo.create({
357-
student_name: student.name,
358-
reason_content: reason,
359-
delta: scoreValue
360-
})
380+
for (const action of rule.actions || []) {
381+
await this.executeAction(action, studentsToScore, rule.name)
361382
}
362383

363384
rule.lastExecuted = new Date()
@@ -371,6 +392,54 @@ export class AutoScoreService extends Service {
371392
}
372393
}
373394

395+
private async executeAction(
396+
action: { event: string; value?: string; reason?: string },
397+
students: student[],
398+
ruleName: string
399+
) {
400+
const eventRepo = this.mainCtx.events
401+
402+
switch (action.event) {
403+
case 'add_score': {
404+
const scoreValue = action.value ? parseInt(action.value, 10) : 0
405+
const reason = action.reason || `自动化加分 - ${ruleName}`
406+
for (const student of students) {
407+
await eventRepo.create({
408+
student_name: student.name,
409+
reason_content: reason,
410+
delta: scoreValue
411+
})
412+
}
413+
break
414+
}
415+
case 'add_tag': {
416+
const tagName = action.value
417+
if (tagName) {
418+
const studentRepo = this.mainCtx.students
419+
for (const student of students) {
420+
const currentTags = student.tags || []
421+
if (!currentTags.includes(tagName)) {
422+
await studentRepo.update(student.id, {
423+
tags: [...currentTags, tagName]
424+
})
425+
}
426+
}
427+
}
428+
break
429+
}
430+
case 'send_notification': {
431+
this.logger.info(`Notification action: ${action.value}`)
432+
break
433+
}
434+
case 'set_student_status': {
435+
this.logger.info(`Set student status action: ${action.value} (not implemented - student type has no status field)`)
436+
break
437+
}
438+
default:
439+
this.logger.warn(`Unknown action event: ${action.event}`)
440+
}
441+
}
442+
374443
private stopRules() {
375444
for (const [timer] of this.timers) {
376445
clearTimeout(timer)

0 commit comments

Comments
 (0)