diff --git a/src/main/services/AutoScoreService.ts b/src/main/services/AutoScoreService.ts index 6570b35..c796668 100644 --- a/src/main/services/AutoScoreService.ts +++ b/src/main/services/AutoScoreService.ts @@ -9,7 +9,7 @@ interface AutoScoreRule { name: string studentNames: string[] lastExecuted?: Date - triggers?: { event: string; value?: string }[] + triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[] actions?: { event: string; value?: string; reason?: string }[] } @@ -19,7 +19,7 @@ interface AutoScoreRuleFileData { name: string studentNames: string[] lastExecuted?: string - triggers?: { event: string; value?: string }[] + triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[] actions?: { event: string; value?: string; reason?: string }[] } @@ -355,43 +355,134 @@ export class AutoScoreService extends Service { } } - for (const trigger of rule.triggers || []) { - const logic = getTriggerLogic(trigger.event) - if (logic?.check) { - const context = { - students: studentsToScore, - events: [], - rule: { - id: rule.id, - name: rule.name, - studentNames: rule.studentNames, - triggers: rule.triggers, - actions: rule.actions - }, - now: new Date() - } - const result = logic.check(context, trigger.value || '') - if (result.matchedStudents && result.matchedStudents.length > 0) { - studentsToScore = result.matchedStudents - } + // 使用AND/OR逻辑评估触发器 + const matchedStudents = this.evaluateTriggersWithLogic(rule, studentsToScore) + + if (matchedStudents.length > 0) { + for (const action of rule.actions || []) { + await this.executeAction(action, matchedStudents, rule.name) } } - for (const action of rule.actions || []) { - await this.executeAction(action, studentsToScore, rule.name) - } - rule.lastExecuted = new Date() await this.saveRulesToFile() this.logger.info( - `Auto score rule executed successfully for ${studentsToScore.length} students` + `Auto score rule executed successfully for ${matchedStudents.length} students` ) } catch (error) { this.logger.error(`Failed to execute auto score rule ${rule.name}:`, { error }) } } + private evaluateTriggersWithLogic(rule: AutoScoreRule, initialStudents: student[]): student[] { + if (!rule.triggers || rule.triggers.length === 0) { + return [] + } + + let resultStudents: student[] = [...initialStudents] + let currentGroup: student[] = [...initialStudents] + let currentRelation: 'AND' | 'OR' = 'AND' + + for (let i = 0; i < rule.triggers.length; i++) { + const trigger = rule.triggers[i] + const logic = getTriggerLogic(trigger.event) + + if (!logic?.check) { + continue + } + + const context = { + students: currentGroup, + events: [], + rule: { + id: rule.id, + name: rule.name, + studentNames: rule.studentNames, + triggers: rule.triggers, + actions: rule.actions + }, + now: new Date() + } + + const result = logic.check(context, trigger.value || '') + + if (!result.matchedStudents || result.matchedStudents.length === 0) { + // 当前触发器没有匹配的学生 + if (currentRelation === 'AND') { + // AND关系下,任何一个触发器不匹配,整个组就不匹配 + currentGroup = [] + } + // OR关系下,继续评估下一个触发器 + } else { + // 当前触发器有匹配的学生 + if (currentRelation === 'AND') { + // AND关系下,取交集 + currentGroup = currentGroup.filter(student => + result.matchedStudents!.some(matched => matched.id === student.id) + ) + } else { + // OR关系下,取并集 + const newStudents = result.matchedStudents.filter(matched => + !currentGroup.some(student => student.id === matched.id) + ) + currentGroup = [...currentGroup, ...newStudents] + } + } + + // 处理下一个关系(如果存在) + if (i < rule.triggers.length - 1) { + const nextRelation = rule.triggers[i + 1].relation || 'AND' + + if (nextRelation !== currentRelation) { + // 关系发生变化,处理当前组的结果 + if (currentRelation === 'AND') { + // AND组结束,如果当前组不为空,则合并到结果 + if (currentGroup.length > 0) { + resultStudents = resultStudents.filter(student => + currentGroup.some(groupStudent => groupStudent.id === student.id) + ) + } else { + // AND组为空,整个规则不匹配 + return [] + } + } else { + // OR组结束,合并当前组到结果 + const newStudents = currentGroup.filter(groupStudent => + !resultStudents.some(resultStudent => resultStudent.id === groupStudent.id) + ) + resultStudents = [...resultStudents, ...newStudents] + } + + // 重置当前组为所有学生,开始新的关系组 + currentGroup = [...initialStudents] + currentRelation = nextRelation + } + } + } + + // 处理最后一组的结果 + if (currentRelation === 'AND') { + // AND组结束,如果当前组不为空,则合并到结果 + if (currentGroup.length > 0) { + resultStudents = resultStudents.filter(student => + currentGroup.some(groupStudent => groupStudent.id === student.id) + ) + } else { + // AND组为空,整个规则不匹配 + return [] + } + } else { + // OR组结束,合并当前组到结果 + const newStudents = currentGroup.filter(groupStudent => + !resultStudents.some(resultStudent => resultStudent.id === groupStudent.id) + ) + resultStudents = [...resultStudents, ...newStudents] + } + + return resultStudents + } + private async executeAction( action: { event: string; value?: string; reason?: string }, students: student[], diff --git a/src/main/services/HttpServerService.ts b/src/main/services/HttpServerService.ts index 74e2552..2fa3477 100644 --- a/src/main/services/HttpServerService.ts +++ b/src/main/services/HttpServerService.ts @@ -337,8 +337,11 @@ export class HttpServerService extends Service { } }) + // ww + this.app.use + // 404处理 - this.app.use((_, res: Response) => { + this.app.use((_req: Request, res: Response) => { res.status(404).json({ success: false, message: 'Endpoint not found' diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 63b1bea..0ce3904 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -26,7 +26,7 @@ interface AutoScoreRule { name: string studentNames: string[] lastExecuted?: string - triggers?: { event: string; value?: string }[] + triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[] actions?: { event: string; value?: string; reason?: string }[] } @@ -108,7 +108,11 @@ export const AutoScoreManager: React.FC = () => { const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] - const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value })) + const triggersPayload = triggerList.map((t) => ({ + event: t.eventName, + value: t.value, + relation: t.relation + })) const actionsPayload = actionList.map((a) => ({ event: a.eventName, value: a.value, @@ -175,7 +179,8 @@ export const AutoScoreManager: React.FC = () => { const mapped = rule.triggers.map((t, idx) => ({ id: idx + 1, eventName: t.event, - value: t.value ?? '' + value: t.value ?? '', + relation: t.relation ?? 'AND' })) setTriggerList(mapped) } else { @@ -268,7 +273,8 @@ export const AutoScoreManager: React.FC = () => { { id: nextId, eventName: defaultTrigger.eventName, - value: '' + value: '', + relation: 'AND' } ]) } @@ -285,6 +291,10 @@ export const AutoScoreManager: React.FC = () => { setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value } : t))) } + const handleTriggerRelationChange = (id: number, relation: 'AND' | 'OR') => { + setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, relation } : t))) + } + const handleAddAction = () => { const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1 const defaultAction = allActions.list[0] @@ -447,6 +457,7 @@ export const AutoScoreManager: React.FC = () => { onDelete={handleDeleteTrigger} onChange={handleTriggerChange} onValueChange={handleTriggerValueChange} + onRelationChange={handleTriggerRelationChange} isFirst={idx === 0} /> )) @@ -566,7 +577,8 @@ export const AutoScoreManager: React.FC = () => { const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] const triggersPayload = triggerList.map((t) => ({ event: t.eventName, - value: t.value + value: t.value, + relation: t.relation })) const actionsPayload = actionList.map((a) => ({ event: a.eventName, diff --git a/src/renderer/src/components/com.automatically/TriggerItem.tsx b/src/renderer/src/components/com.automatically/TriggerItem.tsx index 10d3cad..841a538 100644 --- a/src/renderer/src/components/com.automatically/TriggerItem.tsx +++ b/src/renderer/src/components/com.automatically/TriggerItem.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react' import { Button, Select } from 'tdesign-react' import { Delete1Icon } from 'tdesign-icons-react' import { triggerRegistry, allTriggers } from './registry' @@ -9,6 +8,7 @@ interface TriggerItemProps { onDelete: (id: number) => void onChange: (id: number, eventName: string) => void onValueChange: (id: number, value: string) => void + onRelationChange?: (id: number, relation: 'AND' | 'OR') => void isFirst?: boolean } @@ -17,21 +17,22 @@ const TriggerItem: React.FC = ({ onDelete, onChange, onValueChange, + onRelationChange, isFirst = false }) => { const definition = triggerRegistry.get(item.eventName) const Component = definition?.component - const [andFilled, setAndFilled] = useState(false) + const relation = item.relation || 'AND' return (
{!isFirst && ( )}