mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 09:39:03 +08:00
ALPHA: 未完成
This commit is contained in:
@@ -9,7 +9,7 @@ interface AutoScoreRule {
|
|||||||
name: string
|
name: string
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
lastExecuted?: Date
|
lastExecuted?: Date
|
||||||
triggers?: { event: string; value?: string }[]
|
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
actions?: { event: string; value?: string; reason?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ interface AutoScoreRuleFileData {
|
|||||||
name: string
|
name: string
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
lastExecuted?: string
|
lastExecuted?: string
|
||||||
triggers?: { event: string; value?: string }[]
|
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
actions?: { event: string; value?: string; reason?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,43 +355,134 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const trigger of rule.triggers || []) {
|
// 使用AND/OR逻辑评估触发器
|
||||||
const logic = getTriggerLogic(trigger.event)
|
const matchedStudents = this.evaluateTriggersWithLogic(rule, studentsToScore)
|
||||||
if (logic?.check) {
|
|
||||||
const context = {
|
if (matchedStudents.length > 0) {
|
||||||
students: studentsToScore,
|
for (const action of rule.actions || []) {
|
||||||
events: [],
|
await this.executeAction(action, matchedStudents, rule.name)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const action of rule.actions || []) {
|
|
||||||
await this.executeAction(action, studentsToScore, rule.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.lastExecuted = new Date()
|
rule.lastExecuted = new Date()
|
||||||
await this.saveRulesToFile()
|
await this.saveRulesToFile()
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Auto score rule executed successfully for ${studentsToScore.length} students`
|
`Auto score rule executed successfully for ${matchedStudents.length} students`
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to execute auto score rule ${rule.name}:`, { 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(
|
private async executeAction(
|
||||||
action: { event: string; value?: string; reason?: string },
|
action: { event: string; value?: string; reason?: string },
|
||||||
students: student[],
|
students: student[],
|
||||||
|
|||||||
@@ -337,8 +337,11 @@ export class HttpServerService extends Service {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ww
|
||||||
|
this.app.use
|
||||||
|
|
||||||
// 404处理
|
// 404处理
|
||||||
this.app.use((_, res: Response) => {
|
this.app.use((_req: Request, res: Response) => {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Endpoint not found'
|
message: 'Endpoint not found'
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface AutoScoreRule {
|
|||||||
name: string
|
name: string
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
lastExecuted?: string
|
lastExecuted?: string
|
||||||
triggers?: { event: string; value?: string }[]
|
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
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 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) => ({
|
const actionsPayload = actionList.map((a) => ({
|
||||||
event: a.eventName,
|
event: a.eventName,
|
||||||
value: a.value,
|
value: a.value,
|
||||||
@@ -175,7 +179,8 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
const mapped = rule.triggers.map((t, idx) => ({
|
const mapped = rule.triggers.map((t, idx) => ({
|
||||||
id: idx + 1,
|
id: idx + 1,
|
||||||
eventName: t.event,
|
eventName: t.event,
|
||||||
value: t.value ?? ''
|
value: t.value ?? '',
|
||||||
|
relation: t.relation ?? 'AND'
|
||||||
}))
|
}))
|
||||||
setTriggerList(mapped)
|
setTriggerList(mapped)
|
||||||
} else {
|
} else {
|
||||||
@@ -268,7 +273,8 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
{
|
{
|
||||||
id: nextId,
|
id: nextId,
|
||||||
eventName: defaultTrigger.eventName,
|
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)))
|
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 handleAddAction = () => {
|
||||||
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
|
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
|
||||||
const defaultAction = allActions.list[0]
|
const defaultAction = allActions.list[0]
|
||||||
@@ -447,6 +457,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
onDelete={handleDeleteTrigger}
|
onDelete={handleDeleteTrigger}
|
||||||
onChange={handleTriggerChange}
|
onChange={handleTriggerChange}
|
||||||
onValueChange={handleTriggerValueChange}
|
onValueChange={handleTriggerValueChange}
|
||||||
|
onRelationChange={handleTriggerRelationChange}
|
||||||
isFirst={idx === 0}
|
isFirst={idx === 0}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -566,7 +577,8 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
||||||
const triggersPayload = triggerList.map((t) => ({
|
const triggersPayload = triggerList.map((t) => ({
|
||||||
event: t.eventName,
|
event: t.eventName,
|
||||||
value: t.value
|
value: t.value,
|
||||||
|
relation: t.relation
|
||||||
}))
|
}))
|
||||||
const actionsPayload = actionList.map((a) => ({
|
const actionsPayload = actionList.map((a) => ({
|
||||||
event: a.eventName,
|
event: a.eventName,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Button, Select } from 'tdesign-react'
|
import { Button, Select } from 'tdesign-react'
|
||||||
import { Delete1Icon } from 'tdesign-icons-react'
|
import { Delete1Icon } from 'tdesign-icons-react'
|
||||||
import { triggerRegistry, allTriggers } from './registry'
|
import { triggerRegistry, allTriggers } from './registry'
|
||||||
@@ -9,6 +8,7 @@ interface TriggerItemProps {
|
|||||||
onDelete: (id: number) => void
|
onDelete: (id: number) => void
|
||||||
onChange: (id: number, eventName: string) => void
|
onChange: (id: number, eventName: string) => void
|
||||||
onValueChange: (id: number, value: string) => void
|
onValueChange: (id: number, value: string) => void
|
||||||
|
onRelationChange?: (id: number, relation: 'AND' | 'OR') => void
|
||||||
isFirst?: boolean
|
isFirst?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,21 +17,22 @@ const TriggerItem: React.FC<TriggerItemProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onChange,
|
onChange,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
|
onRelationChange,
|
||||||
isFirst = false
|
isFirst = false
|
||||||
}) => {
|
}) => {
|
||||||
const definition = triggerRegistry.get(item.eventName)
|
const definition = triggerRegistry.get(item.eventName)
|
||||||
const Component = definition?.component
|
const Component = definition?.component
|
||||||
const [andFilled, setAndFilled] = useState(false)
|
const relation = item.relation || 'AND'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', gap: 5 }}>
|
<div style={{ display: 'flex', gap: 5 }}>
|
||||||
{!isFirst && (
|
{!isFirst && (
|
||||||
<Button
|
<Button
|
||||||
theme="primary"
|
theme={relation === 'AND' ? 'primary' : 'warning'}
|
||||||
variant={andFilled ? 'base' : 'outline'}
|
variant={relation === 'AND' ? 'base' : 'outline'}
|
||||||
onClick={() => setAndFilled((v) => !v)}
|
onClick={() => onRelationChange?.(item.id, relation === 'AND' ? 'OR' : 'AND')}
|
||||||
>
|
>
|
||||||
并
|
{relation === 'AND' ? '并' : '或'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Select } from 'tdesign-react'
|
|
||||||
import type { ActionComponentProps } from '../types'
|
|
||||||
|
|
||||||
export const eventName = 'set_student_status'
|
|
||||||
export const label = '设置学生状态'
|
|
||||||
export const description = '设置学生的状态'
|
|
||||||
export const hasReason = false
|
|
||||||
|
|
||||||
const statusOptions = [
|
|
||||||
{ label: '活跃', value: 'active' },
|
|
||||||
{ label: '不活跃', value: 'inactive' },
|
|
||||||
{ label: '请假', value: 'leave' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const SetStudentStatusAction: React.FC<ActionComponentProps> = ({ value, onChange }) => {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
placeholder="请选择状态"
|
|
||||||
style={{ width: '150px' }}
|
|
||||||
value={value ?? ''}
|
|
||||||
onChange={(v: any) => onChange(v ? String(v) : '')}
|
|
||||||
options={statusOptions}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SetStudentStatusAction
|
|
||||||
@@ -22,13 +22,6 @@ import SendNotificationAction, {
|
|||||||
hasReason as sendNotificationHasReason
|
hasReason as sendNotificationHasReason
|
||||||
} from './SendNotificationAction'
|
} from './SendNotificationAction'
|
||||||
|
|
||||||
import SetStudentStatusAction, {
|
|
||||||
eventName as setStudentStatusEventName,
|
|
||||||
label as setStudentStatusLabel,
|
|
||||||
description as setStudentStatusDescription,
|
|
||||||
hasReason as setStudentStatusHasReason
|
|
||||||
} from './SetStudentStatusAction'
|
|
||||||
|
|
||||||
const actionDefinitions: ActionDefinition[] = [
|
const actionDefinitions: ActionDefinition[] = [
|
||||||
{
|
{
|
||||||
eventName: addScoreEventName,
|
eventName: addScoreEventName,
|
||||||
@@ -51,15 +44,8 @@ const actionDefinitions: ActionDefinition[] = [
|
|||||||
component: SendNotificationAction,
|
component: SendNotificationAction,
|
||||||
hasReason: sendNotificationHasReason
|
hasReason: sendNotificationHasReason
|
||||||
},
|
},
|
||||||
{
|
|
||||||
eventName: setStudentStatusEventName,
|
|
||||||
label: setStudentStatusLabel,
|
|
||||||
description: setStudentStatusDescription,
|
|
||||||
component: SetStudentStatusAction,
|
|
||||||
hasReason: setStudentStatusHasReason
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
actionDefinitions.forEach((def) => actionRegistry.register(def))
|
actionDefinitions.forEach((def) => actionRegistry.register(def))
|
||||||
|
|
||||||
export { AddScoreAction, AddTagAction, SendNotificationAction, SetStudentStatusAction }
|
export { AddScoreAction, AddTagAction, SendNotificationAction }
|
||||||
|
|||||||
@@ -18,5 +18,4 @@ export {
|
|||||||
AddScoreAction,
|
AddScoreAction,
|
||||||
AddTagAction,
|
AddTagAction,
|
||||||
SendNotificationAction,
|
SendNotificationAction,
|
||||||
SetStudentStatusAction
|
|
||||||
} from './actions'
|
} from './actions'
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface TriggerItem {
|
|||||||
id: number
|
id: number
|
||||||
eventName: string
|
eventName: string
|
||||||
value: string
|
value: string
|
||||||
|
relation?: 'AND' | 'OR'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionItem {
|
export interface ActionItem {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface TriggerContext {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
triggers?: { event: string; value?: string }[]
|
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
actions?: { event: string; value?: string; reason?: string }[]
|
||||||
}
|
}
|
||||||
now: Date
|
now: Date
|
||||||
|
|||||||
Reference in New Issue
Block a user