refactor(server): 待办统计日期懒计算,aislop 豁免注释与文档纠错

This commit is contained in:
2026-08-08 16:03:05 +08:00
parent e50f660811
commit 6c11fd7e16
6 changed files with 39 additions and 31 deletions

View File

@@ -1,3 +1,4 @@
// aislop-ignore-file: duplicate-block -- 业务实体/工作流为声明式元数据,结构相似但语义不同
import type { import type {
BusinessEntity, BusinessEntity,
BusinessEntityField, BusinessEntityField,

View File

@@ -25,6 +25,8 @@ interface TaskDefinition {
sql: (scope: StudentAccessScope) => { sql: string; params: unknown[] }; sql: (scope: StudentAccessScope) => { sql: string; params: unknown[] };
} }
type SqlBuilder = (scope: StudentAccessScope) => { sql: string; params: unknown[] };
function can(context: AgentToolContext, permission: string): boolean { function can(context: AgentToolContext, permission: string): boolean {
return context.isSuperAdmin || context.permissions.includes(permission); return context.isSuperAdmin || context.permissions.includes(permission);
} }
@@ -42,6 +44,16 @@ function teacherParams(scope: StudentAccessScope): unknown[] {
return scope.type === 'teacher' ? [scope.userId] : []; return scope.type === 'teacher' ? [scope.userId] : [];
} }
/** 无作用域参数的计数查询:统一包装,避免各条目重复写 () => ({ sql, params }) 结构 */
function countSql(sql: string, params: unknown[] | (() => unknown[]) = []): SqlBuilder {
return () => ({ sql, params: typeof params === 'function' ? params() : params });
}
/** 教师作用域计数查询:按作用域注入班级过滤片段与 userId 参数 */
function teacherScopedCountSql(sql: (scope: StudentAccessScope) => string): SqlBuilder {
return (scope) => ({ sql: sql(scope), params: teacherParams(scope) });
}
function today(): string { function today(): string {
const now = new Date(); const now = new Date();
const year = now.getFullYear(); const year = now.getFullYear();
@@ -67,17 +79,14 @@ const TASKS: readonly TaskDefinition[] = [
permission: 'student:view', permission: 'student:view',
workflowKeys: ['student_teaching'], workflowKeys: ['student_teaching'],
teacherRestricted: true, teacherRestricted: true,
sql: () => ({ sql: countSql(`
sql: `
SELECT COUNT(*) AS cnt FROM students s SELECT COUNT(*) AS cnt FROM students s
WHERE s.status = 'active' WHERE s.status = 'active'
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM class_student cs SELECT 1 FROM class_student cs
WHERE cs.student_id = s.id AND cs.status = 'active' WHERE cs.student_id = s.id AND cs.status = 'active'
) )
`, `),
params: [],
}),
}, },
{ {
key: 'students_without_checkin', key: 'students_without_checkin',
@@ -85,8 +94,8 @@ const TASKS: readonly TaskDefinition[] = [
entity: 'occupancy', entity: 'occupancy',
permission: 'occupancy:view', permission: 'occupancy:view',
workflowKeys: ['dormitory_billing'], workflowKeys: ['dormitory_billing'],
sql: (scope) => ({ sql: teacherScopedCountSql(
sql: ` (scope) => `
SELECT COUNT(DISTINCT s.id) AS cnt FROM students s SELECT COUNT(DISTINCT s.id) AS cnt FROM students s
INNER JOIN class_student cs ON cs.student_id = s.id AND cs.status = 'active' INNER JOIN class_student cs ON cs.student_id = s.id AND cs.status = 'active'
LEFT JOIN occupancies o ON o.student_id = s.id AND o.status = 'active' LEFT JOIN occupancies o ON o.student_id = s.id AND o.status = 'active'
@@ -94,8 +103,7 @@ const TASKS: readonly TaskDefinition[] = [
${teacherScoped(scope) ? TEACHER_CLASS_FILTER_SQL : ''} ${teacherScoped(scope) ? TEACHER_CLASS_FILTER_SQL : ''}
AND o.id IS NULL AND o.id IS NULL
`, `,
params: teacherParams(scope), ),
}),
}, },
{ {
key: 'occupancies_without_bill', key: 'occupancies_without_bill',
@@ -103,8 +111,8 @@ const TASKS: readonly TaskDefinition[] = [
entity: 'bill', entity: 'bill',
permission: 'bill:view', permission: 'bill:view',
workflowKeys: ['dormitory_billing'], workflowKeys: ['dormitory_billing'],
sql: (scope) => ({ sql: teacherScopedCountSql(
sql: ` (scope) => `
SELECT COUNT(DISTINCT o.id) AS cnt FROM occupancies o SELECT COUNT(DISTINCT o.id) AS cnt FROM occupancies o
${teacherScoped(scope) ? "INNER JOIN class_student cs ON cs.student_id = o.student_id AND cs.status = 'active'" : ''} ${teacherScoped(scope) ? "INNER JOIN class_student cs ON cs.student_id = o.student_id AND cs.status = 'active'" : ''}
LEFT JOIN bills b ON b.student_id = o.student_id LEFT JOIN bills b ON b.student_id = o.student_id
@@ -112,8 +120,7 @@ const TASKS: readonly TaskDefinition[] = [
${teacherScoped(scope) ? TEACHER_CLASS_FILTER_SQL : ''} ${teacherScoped(scope) ? TEACHER_CLASS_FILTER_SQL : ''}
AND b.id IS NULL AND b.id IS NULL
`, `,
params: teacherParams(scope), ),
}),
}, },
{ {
key: 'rentals_without_contract', key: 'rentals_without_contract',
@@ -121,13 +128,10 @@ const TASKS: readonly TaskDefinition[] = [
entity: 'rental', entity: 'rental',
permission: 'rental:view', permission: 'rental:view',
workflowKeys: ['classroom_rental'], workflowKeys: ['classroom_rental'],
sql: () => ({ sql: countSql(`
sql: `
SELECT COUNT(*) AS cnt FROM classroom_rentals r SELECT COUNT(*) AS cnt FROM classroom_rentals r
WHERE r.status = 'active' AND r.contract_path IS NULL WHERE r.status = 'active' AND r.contract_path IS NULL
`, `),
params: [],
}),
}, },
{ {
key: 'rentals_ending_soon', key: 'rentals_ending_soon',
@@ -135,13 +139,13 @@ const TASKS: readonly TaskDefinition[] = [
entity: 'rental', entity: 'rental',
permission: 'rental:view', permission: 'rental:view',
workflowKeys: ['classroom_rental'], workflowKeys: ['classroom_rental'],
sql: () => ({ sql: countSql(
sql: ` `
SELECT COUNT(*) AS cnt FROM classroom_rentals r SELECT COUNT(*) AS cnt FROM classroom_rentals r
WHERE r.status = 'active' AND r.end_date BETWEEN ? AND ? WHERE r.status = 'active' AND r.end_date BETWEEN ? AND ?
`, `,
params: [today(), inDays(7)], () => [today(), inDays(7)],
}), ),
}, },
]; ];

View File

@@ -1,3 +1,4 @@
// aislop-ignore-file: thin-wrapper -- String() 薄包装:绕开 eslint no-base-to-string 对 unknown 收窄后的误报,避免在各调用点散落 disable
/** /**
* 安全字符串化 unknown。 * 安全字符串化 unknown。
* *

View File

@@ -1,3 +1,4 @@
// aislop-ignore-file: file-too-large -- 既有规模445 行),费用业务方法高度耦合仓储/DTO拆分作为独立重构任务跟踪
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, In, Repository } from 'typeorm'; import { DataSource, In, Repository } from 'typeorm';

View File

@@ -1,3 +1,4 @@
// aislop-ignore-file: file-too-large -- 既有规模477 行),费用控制器路由与文件导出逻辑集中于此,拆分作为独立重构任务跟踪
import { import {
Controller, Controller,
Get, Get,

View File

@@ -33,7 +33,7 @@ Agent 的工具全部通过 CASL 权限过滤 + 执行时二次鉴权,只暴
- `render_form` → 用户填写提交 → `create_student` / `update_students` - `render_form` → 用户填写提交 → `create_student` / `update_students`
- `start_import_wizard` → 解析上传 Excel 并生成批量导入向导students / rooms / transfers / checkins→ 用户按阶段确认 → 系统按依赖顺序入库:学生 → 宿舍 → 换宿 → 入住 - `start_import_wizard` → 解析上传 Excel 并生成批量导入向导students / rooms / transfers / checkins→ 用户按阶段确认 → 系统按依赖顺序入库:学生 → 宿舍 → 换宿 → 入住
- Excel 结构探查:`office_analyze`outline/get/query,不整表读取 - Office 附件上传时系统已自动提取附件文本Excel 为“工作表名 + tab 分隔行”),无需单独解析工具,不整表读取
## 四、当前执行约束(已有) ## 四、当前执行约束(已有)