Merge pull request 'main' (#50) from xiongyuxing/gongxue-base:main into main

This commit is contained in:
2026-07-24 08:28:07 +00:00
11 changed files with 1845 additions and 57 deletions

View File

@@ -21,7 +21,6 @@ import {
ApiOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
KeyOutlined,
WarningOutlined,
ReloadOutlined,
CloudServerOutlined,

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Card, Descriptions, Empty, Space, Spin, Table, Tooltip } from 'antd';
import { Alert, Button, Card, Descriptions, Empty, Space, Spin, Table, Tag, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
@@ -67,11 +67,14 @@ const ExamDetailPage: React.FC = () => {
useEffect(() => {
void load();
}, [load]);
const saveScore = async (row: ScoreRow, value: number | undefined) => {
const saveScore = useCallback(
async (row: ScoreRow, value: number | undefined) => {
await api.put(`/exams/${id}/scores/${row.id}`, { score: value ?? null });
message.success('成绩已保存');
await load();
};
},
[id, load],
);
const columns = useMemo<ColumnsType<ScoreRow>>(() => {
if (!detail) return [];
@@ -93,6 +96,9 @@ const ExamDetailPage: React.FC = () => {
dataIndex: 'score',
width: 100,
render: (value: number | null, row: ScoreRow) => (
detail.status === 'archived' ? (
value ?? '-'
) : (
<EditableCell<number | undefined>
value={value ?? undefined}
editor="money"
@@ -103,6 +109,7 @@ const ExamDetailPage: React.FC = () => {
>
{value ?? '-'}
</EditableCell>
)
),
},
{
@@ -120,7 +127,7 @@ const ExamDetailPage: React.FC = () => {
{ title: '考试日期', width: 110, render: () => detail.examDate },
{ title: '关联报读(班级名)', width: 180, render: () => detail.className },
];
}, [detail]);
}, [detail, saveScore]);
if (loading && !detail)
return (
@@ -139,8 +146,12 @@ const ExamDetailPage: React.FC = () => {
</Button>
<h2>{detail.examName}</h2>
{detail.status === 'archived' ? <Tag></Tag> : null}
</Space>
</div>
{detail.status === 'archived' ? (
<Alert type="info" showIcon message="该考试已归档,成绩仅供查看。如需继续录入,请先在考试列表中恢复。" />
) : null}
<Card className="exam-summary">
<Descriptions column={{ xs: 1, sm: 2, lg: 5 }}>
<Descriptions.Item label="考试类型">{detail.examType}</Descriptions.Item>

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Card, Col, Empty, Form, Input, Progress, Row, Select, Space, Tag } from 'antd';
import { CalendarOutlined, PlusOutlined, SearchOutlined, TeamOutlined } from '@ant-design/icons';
import { Button, Card, Col, Empty, Form, Input, Popconfirm, Progress, Row, Select, Space, Switch, Tag } from 'antd';
import { CalendarOutlined, InboxOutlined, PlusOutlined, SearchOutlined, TeamOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router-dom';
import api from '../../api';
@@ -21,6 +21,7 @@ const ExamsPage: React.FC = () => {
const [keyword, setKeyword] = useState('');
const [examType, setExamType] = useState<string>();
const [classId, setClassId] = useState<number>();
const [showArchived, setShowArchived] = useState(false);
const loadClasses = useCallback(async () => {
const result = await api.get<ClassOption[]>('/classes');
@@ -34,6 +35,7 @@ const ExamsPage: React.FC = () => {
if (keyword.trim()) params.set('keyword', keyword.trim());
if (examType) params.set('examType', examType);
if (classId) params.set('classId', String(classId));
params.set('isArchived', String(showArchived));
const result = await api.get<ExamItem[]>(`/exams?${params.toString()}`);
setData(result ?? []);
} catch (error) {
@@ -41,7 +43,7 @@ const ExamsPage: React.FC = () => {
} finally {
setLoading(false);
}
}, [classId, examType, keyword]);
}, [classId, examType, keyword, showArchived]);
useEffect(() => {
void loadClasses().catch((error: { message?: string }) => message.error(error?.message || '加载班级失败'));
@@ -80,6 +82,16 @@ const ExamsPage: React.FC = () => {
}
};
const changeArchiveStatus = async (exam: ExamItem, archive: boolean) => {
try {
await api.put(`/exams/${exam.id}/${archive ? 'archive' : 'restore'}`);
message.success(archive ? '考试已归档' : '考试已恢复');
await loadExams();
} catch (error) {
message.error((error as { message?: string })?.message || '操作失败');
}
};
return (
<div className="exam-page">
<div className="exam-toolbar">
@@ -89,6 +101,11 @@ const ExamsPage: React.FC = () => {
<Select value={classId} onChange={setClassId} options={classOptions} placeholder="考试班级" allowClear showSearch optionFilterProp="label" style={{ width: 180 }} />
</Space>
<Space wrap>
<span className="exam-archive-toggle">
<InboxOutlined />
<Switch size="small" checked={showArchived} onChange={setShowArchived} />
</span>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}></Button>
</Space>
</div>
@@ -105,9 +122,27 @@ const ExamsPage: React.FC = () => {
className="exam-card"
loading={loading}
title={<Space><Tag color="blue">{exam.examType}</Tag><span>{exam.examName}</span></Space>}
extra={<Tag color="green"></Tag>}
extra={<Tag color={exam.status === 'archived' ? 'default' : 'green'}>{exam.status === 'archived' ? '已归档' : '成绩录入'}</Tag>}
actions={[
<span key="detail" onClick={() => navigate(`/exams/${exam.id}`)}></span>,
exam.status === 'archived' ? (
<Popconfirm
key="restore"
title="确认恢复该考试?"
onConfirm={() => changeArchiveStatus(exam, false)}
>
<span></span>
</Popconfirm>
) : (
<Popconfirm
key="archive"
title="确认归档该考试?"
description={`当前已录入 ${exam.enteredScores}/${exam.totalStudents} 人,归档后成绩将变为只读。`}
onConfirm={() => changeArchiveStatus(exam, true)}
>
<span></span>
</Popconfirm>
),
]}
>
<div className="exam-meta"><span></span><strong>{exam.subject}</strong></div>

View File

@@ -68,6 +68,13 @@
border-radius: 8px;
}
.exam-archive-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
@media (max-width: 575px) {
.exam-toolbar > .ant-space,
.exam-toolbar .ant-input-affix-wrapper,

View File

@@ -4,33 +4,6 @@ import react from '@vitejs/plugin-react';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
build: {
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: 'react',
test: /node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
priority: 30,
},
{
name: 'echarts',
test: /node_modules[\\/](echarts|zrender)[\\/]/,
priority: 20,
maxSize: 600_000,
},
{
name: 'antd',
test: /node_modules[\\/](@ant-design|antd|rc-[^\\/]+)[\\/]/,
priority: 10,
maxSize: 600_000,
},
],
},
},
},
},
server: {
port: 3002,
proxy: {

View File

@@ -1,5 +1,6 @@
import { Type } from 'class-transformer';
import { Transform, Type } from 'class-transformer';
import {
IsBoolean,
IsDateString,
IsInt,
IsNotEmpty,
@@ -22,6 +23,16 @@ export class QueryExamDto {
@IsOptional() @IsString() keyword?: string;
@IsOptional() @IsString() examType?: string;
@IsOptional() @Type(() => Number) @IsInt() @Min(1) classId?: number;
@IsOptional()
@Transform(({ value }) => {
if (typeof value === 'boolean') return value;
if (value === 'true' || value === '1') return true;
if (value === 'false' || value === '0') return false;
return value;
})
@IsBoolean()
isArchived?: boolean;
}
export class UpdateExamScoreValueDto {

View File

@@ -71,6 +71,48 @@ export class ExamsController {
return result;
}
@Put(':id/archive')
@RequirePermission('exam:view')
async archive(
@Param('id', ParseIntPipe) id: number,
@Request() req: AuthenticatedRequest,
) {
const result = await this.service.archive(id, req.user.id, this.canManageAll(req));
const { ipAddress, userAgent } = extractRequestInfo(req);
await this.logService.log({
userId: req.user.id,
username: req.user.username,
module: '考试管理',
action: '归档考试',
targetId: id,
targetType: 'exam',
ipAddress,
userAgent,
});
return result;
}
@Put(':id/restore')
@RequirePermission('exam:view')
async restore(
@Param('id', ParseIntPipe) id: number,
@Request() req: AuthenticatedRequest,
) {
const result = await this.service.restore(id, req.user.id, this.canManageAll(req));
const { ipAddress, userAgent } = extractRequestInfo(req);
await this.logService.log({
userId: req.user.id,
username: req.user.username,
module: '考试管理',
action: '恢复考试',
targetId: id,
targetType: 'exam',
ipAddress,
userAgent,
});
return result;
}
@Put(':examId/scores/:scoreId')
@RequirePermission('exam:view')
async updateScore(
@@ -100,5 +142,4 @@ export class ExamsController {
});
return result;
}
}

View File

@@ -1,14 +1,24 @@
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, ForbiddenException, ValidationPipe } from '@nestjs/common';
import { ExamScore } from '../entities';
import { QueryExamDto } from './dto/exam.dto';
import { ExamsService } from './exams.service';
function createService(transaction: (run: (manager: any) => Promise<unknown>) => Promise<unknown>) {
function createService(
transaction: (run: (manager: any) => Promise<unknown>) => Promise<unknown>,
repositories: {
examRepo?: Record<string, jest.Mock>;
scoreRepo?: Record<string, jest.Mock>;
classTeacherRepo?: Record<string, jest.Mock>;
} = {},
) {
return new ExamsService(
(repositories.examRepo ?? {}) as never,
(repositories.scoreRepo ?? {}) as never,
{} as never,
{} as never,
{} as never,
{} as never,
{ findOne: jest.fn().mockResolvedValue({ id: 1 }) } as never,
(repositories.classTeacherRepo ?? {
findOne: jest.fn().mockResolvedValue({ id: 1 }),
}) as never,
{ transaction } as never,
);
}
@@ -124,4 +134,122 @@ describe('ExamsService', () => {
expect.objectContaining({ score: 70, classAvg: 70, rank: 1 }),
]);
});
it('lists active exams by default and archived exams on request', async () => {
const examRepo = {
find: jest.fn().mockResolvedValue([]),
};
const service = createService(async () => undefined, { examRepo });
await service.findAll({});
await service.findAll({ isArchived: true });
expect(examRepo.find).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ where: expect.objectContaining({ status: 'active' }) }),
);
expect(examRepo.find).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ where: expect.objectContaining({ status: 'archived' }) }),
);
});
it('preserves the accessible class filter for archived exams', async () => {
const examRepo = {
find: jest.fn().mockResolvedValue([]),
};
const service = createService(async () => undefined, { examRepo });
await service.findAll({ isArchived: true }, [3, 5]);
expect(examRepo.find).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ status: 'archived', classId: expect.anything() }),
}),
);
});
it('archives an exam without changing score rows', async () => {
const examRepo = {
findOne: jest.fn().mockResolvedValue({ id: 8, classId: 3, status: 'active' }),
update: jest.fn().mockResolvedValue({ affected: 1 }),
};
const scoreRepo = { update: jest.fn(), save: jest.fn() };
const service = createService(async () => undefined, { examRepo, scoreRepo });
await expect(service.archive(8, 1, true)).resolves.toEqual({ success: true });
expect(examRepo.update).toHaveBeenCalledWith(8, { status: 'archived' });
expect(scoreRepo.update).not.toHaveBeenCalled();
expect(scoreRepo.save).not.toHaveBeenCalled();
});
it('rejects archiving an archived exam', async () => {
const examRepo = {
findOne: jest.fn().mockResolvedValue({ id: 8, classId: 3, status: 'archived' }),
update: jest.fn(),
};
const service = createService(async () => undefined, { examRepo });
await expect(service.archive(8, 1, true)).rejects.toBeInstanceOf(BadRequestException);
expect(examRepo.update).not.toHaveBeenCalled();
});
it('restores an archived exam and rejects restoring an active exam', async () => {
const examRepo = {
findOne: jest
.fn()
.mockResolvedValueOnce({ id: 8, classId: 3, status: 'archived' })
.mockResolvedValueOnce({ id: 9, classId: 3, status: 'active' }),
update: jest.fn().mockResolvedValue({ affected: 1 }),
};
const service = createService(async () => undefined, { examRepo });
await expect(service.restore(8, 1, true)).resolves.toEqual({ success: true });
await expect(service.restore(9, 1, true)).rejects.toBeInstanceOf(BadRequestException);
expect(examRepo.update).toHaveBeenCalledTimes(1);
expect(examRepo.update).toHaveBeenCalledWith(8, { status: 'active' });
});
it('requires class access before archiving an exam', async () => {
const examRepo = {
findOne: jest.fn().mockResolvedValue({ id: 8, classId: 3, status: 'active' }),
update: jest.fn(),
};
const classTeacherRepo = { findOne: jest.fn().mockResolvedValue(null) };
const service = createService(async () => undefined, { examRepo, classTeacherRepo });
await expect(service.archive(8, 21, false)).rejects.toBeInstanceOf(ForbiddenException);
expect(classTeacherRepo.findOne).toHaveBeenCalledWith({ where: { userId: 21, classId: 3 } });
expect(examRepo.update).not.toHaveBeenCalled();
});
it('rejects score updates for archived exams', async () => {
const manager = {
findOne: jest.fn().mockResolvedValue({ id: 8, classId: 3, status: 'archived' }),
};
const service = createService(async (run) => run(manager));
await expect(service.updateScore(8, 1, 90, 1, true)).rejects.toBeInstanceOf(
BadRequestException,
);
});
});
describe('QueryExamDto - query transformation', () => {
const pipe = new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true });
it.each([
['false', false],
['0', false],
['true', true],
['1', true],
])('transforms isArchived=%s to %s', async (input, expected) => {
await expect(
pipe.transform(
{ isArchived: input },
{ type: 'query', metatype: QueryExamDto, data: undefined },
),
).resolves.toEqual({ isArchived: expected });
});
});

View File

@@ -29,7 +29,7 @@ export class ExamsService {
async findAll(query: QueryExamDto, accessibleClassIds?: number[]) {
const where: Record<string, unknown> = {
status: 'active',
status: query.isArchived ? 'archived' : 'active',
};
if (query.keyword) where.examName = Like(`%${query.keyword}%`);
if (query.examType) where.examType = query.examType;
@@ -131,6 +131,24 @@ export class ExamsService {
});
}
async archive(id: number, userId: number, canManageAll: boolean) {
const exam = await this.examRepo.findOne({ where: { id } });
if (!exam) throw new NotFoundException('考试不存在');
await this.assertClassAccess(userId, exam.classId, canManageAll);
if (exam.status === 'archived') throw new BadRequestException('考试已归档');
await this.examRepo.update(id, { status: 'archived' });
return { success: true };
}
async restore(id: number, userId: number, canManageAll: boolean) {
const exam = await this.examRepo.findOne({ where: { id } });
if (!exam) throw new NotFoundException('考试不存在');
await this.assertClassAccess(userId, exam.classId, canManageAll);
if (exam.status !== 'archived') throw new BadRequestException('考试未归档');
await this.examRepo.update(id, { status: 'active' });
return { success: true };
}
private async createScoreRows(manager: EntityManager, exam: Exam, members: ClassStudent[]) {
const rows = members.map((member) =>
manager.create(ExamScore, {

1264
data.sql Normal file

File diff suppressed because it is too large Load Diff

301
init.sql Normal file
View File

@@ -0,0 +1,301 @@
-- gongxue-base MySQL full rebuild script
-- Generated from the current TypeORM migrations on 2026-07-24.
-- WARNING: this script intentionally drops every table in dorm_billing_v2 and rebuilds the schema.
-- The database itself is preserved for environments without DROP/CREATE DATABASE privileges.
SET NAMES utf8mb4;
USE `dorm_billing_v2`;
SET FOREIGN_KEY_CHECKS = 0;
DROP PROCEDURE IF EXISTS `drop_all_tables`;
DELIMITER $$
CREATE PROCEDURE `drop_all_tables`()
BEGIN
DECLARE finished INTEGER DEFAULT 0;
DECLARE current_table VARCHAR(255);
DECLARE table_cursor CURSOR FOR
SELECT `table_name`
FROM `information_schema`.`tables`
WHERE `table_schema` = DATABASE()
AND `table_type` = 'BASE TABLE';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
OPEN table_cursor;
drop_loop: LOOP
FETCH table_cursor INTO current_table;
IF finished = 1 THEN
LEAVE drop_loop;
END IF;
SET @drop_table_sql = CONCAT(
'DROP TABLE IF EXISTS `',
REPLACE(current_table, '`', '``'),
'`'
);
PREPARE drop_statement FROM @drop_table_sql;
EXECUTE drop_statement;
DEALLOCATE PREPARE drop_statement;
END LOOP;
CLOSE table_cursor;
END$$
DELIMITER ;
CALL `drop_all_tables`();
DROP PROCEDURE IF EXISTS `drop_all_tables`;
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `wallet_transactions` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `bill_id` int NULL, `operation_id` varchar(64) NULL, `type` varchar(30) NOT NULL, `amount` decimal(12,2) NOT NULL, `balance_after` decimal(12,2) NOT NULL, `description` varchar(300) NULL, `recorded_by` int NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), INDEX `IDX_680bbd0275ac5e06c179f9b84c` (`student_id`, `created_at`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `permissions` (`id` int NOT NULL AUTO_INCREMENT, `code` varchar(50) NOT NULL, `name` varchar(50) NOT NULL, `group` varchar(30) NOT NULL, `description` varchar(200) NULL, UNIQUE INDEX `IDX_8dad765629e83229da6feda1c1` (`code`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `roles` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL, `code` varchar(30) NULL, `description` varchar(200) NULL, `is_system` tinyint NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT '1', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_648e3f5447f725579d7d4ffdfb` (`name`), UNIQUE INDEX `IDX_f6d54f95c31b73fb1bdd8e91d0` (`code`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `users` (`id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password_hash` varchar(255) NOT NULL, `name` varchar(50) NULL, `is_active` tinyint NOT NULL DEFAULT 1, `last_login_at` datetime NULL, `is_archived` tinyint NOT NULL DEFAULT 0, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `profile` text NULL, UNIQUE INDEX `IDX_fe0bb3f6520ee0469504521e71` (`username`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `sync_state` (`platform` varchar(20) NOT NULL, `last_sync_at` datetime NULL, `run_id` varchar(64) NULL, `running_since` datetime NULL, PRIMARY KEY (`platform`)) ENGINE=InnoDB;
CREATE TABLE `sync_logs` (`id` int NOT NULL AUTO_INCREMENT, `platform` varchar(20) NOT NULL, `sync_type` varchar(20) NOT NULL, `status` varchar(20) NOT NULL, `records_count` int NOT NULL DEFAULT '0', `error_message` text NULL, `started_at` datetime NULL, `finished_at` datetime NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `room_expenses` (`id` int NOT NULL AUTO_INCREMENT, `room_id` int NOT NULL, `expense_type` varchar(20) NOT NULL, `amount` decimal(10,2) NOT NULL, `period_start` date NOT NULL, `period_end` date NOT NULL, `description` text NULL, `recorded_by` int NULL, `import_key` varchar(120) NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_050485162d4fe47bd3cfd7dedb` (`import_key`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `rooms` (`id` int NOT NULL AUTO_INCREMENT, `room_number` varchar(20) NOT NULL, `building` varchar(50) NULL, `floor` int NULL, `capacity` int NOT NULL, `status` varchar(20) NOT NULL DEFAULT 'available', `room_type` varchar(20) NULL, `rental_category` varchar(10) NOT NULL DEFAULT 'short', `monthly_rate` decimal(10,2) NOT NULL DEFAULT '0.00', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_8f7c6fa4c469bab1a06fe3e49f` (`room_number`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `organizations` (`id` int NOT NULL AUTO_INCREMENT, `public_id` varchar(36) NOT NULL, `code` varchar(50) NOT NULL, `name` varchar(100) NOT NULL, `is_host` tinyint NOT NULL DEFAULT 0, `contact_name` varchar(50) NULL, `phone` varchar(30) NULL, `color` varchar(20) NULL, `notes` text NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_0db5eda192bf60a02bd41931f8` (`public_id`), UNIQUE INDEX `IDX_7e27c3b62c681fbe3e2322535f` (`code`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `beds` (`id` int NOT NULL AUTO_INCREMENT, `room_id` int NOT NULL, `bed_number` varchar(20) NOT NULL, `status` varchar(20) NOT NULL DEFAULT 'available', `notes` text NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_95e9ba0a907346ef7b0d5ca488` (`room_id`, `bed_number`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `lockers` (`id` int NOT NULL AUTO_INCREMENT, `room_id` int NOT NULL, `locker_number` varchar(20) NOT NULL, `status` varchar(20) NOT NULL DEFAULT 'available', `notes` text NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_8bc984d80d58c6909f738d8282` (`room_id`, `locker_number`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `occupancies` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `room_id` int NOT NULL, `check_in_date` date NOT NULL, `check_out_date` date NULL, `billing_start_date` date NOT NULL, `billing_end_date` date NULL, `check_out_reason` varchar(100) NULL, `notes` text NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `bed_id` int NULL, `locker_id` int NULL, `stay_type` varchar(10) NOT NULL DEFAULT 'short', `responsible_organization_id` int NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `personal_expenses` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `room_id` int NULL, `expense_type` varchar(20) NOT NULL, `amount` decimal(10,2) NOT NULL, `expense_date` date NOT NULL, `description` text NULL, `recorded_by` int NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `bill_id` int NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `bill_items` (`id` int NOT NULL AUTO_INCREMENT, `bill_id` int NOT NULL, `room_expense_id` int NULL, `personal_expense_id` int NULL, `room_id` int NULL, `expense_type` varchar(20) NULL, `description` varchar(200) NULL, `days` int NULL, `total_room_days` int NULL, `room_total_amount` decimal(10,2) NULL, `student_amount` decimal(10,2) NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `bills` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `period_start` date NOT NULL, `period_end` date NOT NULL, `shared_amount` decimal(10,2) NOT NULL DEFAULT '0.00', `personal_amount` decimal(10,2) NOT NULL DEFAULT '0.00', `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00', `source` varchar(30) NOT NULL DEFAULT 'batch', `paid_amount` decimal(10,2) NOT NULL DEFAULT '0.00', `outstanding_amount` decimal(10,2) NOT NULL DEFAULT '0.00', `status` varchar(20) NOT NULL DEFAULT 'unpaid', `cancelled_at` datetime NULL, `cancel_reason` varchar(300) NULL, `generated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `students` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `student_no` varchar(30) NULL, `phone` varchar(20) NULL, `id_number` varchar(30) NULL, `gender` varchar(10) NULL, `ethnicity` varchar(20) NULL, `emergency_contact` varchar(50) NULL, `emergency_phone` varchar(20) NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `supervisor` varchar(50) NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `user_id` int NULL, `organization_id` int NULL, UNIQUE INDEX `IDX_fb3eff90b11bddf7285f9b4e28` (`user_id`), UNIQUE INDEX `REL_fb3eff90b11bddf7285f9b4e28` (`user_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `student_wallets` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `balance` decimal(12,2) NOT NULL DEFAULT '0.00', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_07a434ad1a960d506386754d59` (`student_id`), UNIQUE INDEX `REL_07a434ad1a960d506386754d59` (`student_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `student_profiles` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `target_college` varchar(100) NULL, `target_major` varchar(100) NULL, `college_school` varchar(100) NULL, `college_major` varchar(100) NULL, `subject_direction` varchar(50) NULL, `grade` varchar(20) NULL, `profile_date` date NULL, `notes` text NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_4cedc08d3dc1f2c2da8a12f7a8` (`student_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `student_enrollments` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `course_category` varchar(50) NULL, `class_type` varchar(50) NULL, `class_name` varchar(100) NULL, `head_teacher` varchar(50) NULL, `subject_teacher` varchar(50) NULL, `start_date` date NULL, `end_date` date NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `student_ding_mapping` (`id` int NOT NULL AUTO_INCREMENT, `ding_user_id` varchar(100) NOT NULL, `student_id` int NOT NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_0d1ec47e2f901d37e3b6e56331` (`ding_user_id`), UNIQUE INDEX `IDX_f9ba15ff04de8ffbd8679ae9db` (`student_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `result_archives` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `culture_final_score` decimal(5,2) NULL, `professional_final_score` decimal(5,2) NULL, `admission_status` varchar(50) NULL, `admitted_college` varchar(100) NULL, `admitted_major` varchar(100) NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_377bba8eb6a027eecd9737d4ed` (`student_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `operation_logs` (`id` int NOT NULL AUTO_INCREMENT, `user_id` int NULL, `username` varchar(50) NULL, `module` varchar(50) NOT NULL, `action` varchar(50) NOT NULL, `target_id` int NULL, `target_type` varchar(50) NULL, `detail` text NULL, `ip_address` varchar(50) NULL, `user_agent` varchar(500) NULL, `status` varchar(20) NULL DEFAULT 'success', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `notifications` (`id` int NOT NULL AUTO_INCREMENT, `recipient_id` int NOT NULL, `type` varchar(30) NOT NULL, `title` varchar(200) NOT NULL, `content` text NULL, `link` varchar(500) NULL, `is_read` tinyint NOT NULL DEFAULT 0, `read_at` datetime NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `learning_records` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `record_date` date NULL, `record_type` varchar(50) NULL, `content` text NULL, `follow_up_method` varchar(50) NULL, `next_step` text NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `financial_operations` (`id` int NOT NULL AUTO_INCREMENT, `operation_id` varchar(64) NOT NULL, `type` varchar(64) NOT NULL, `status` varchar(20) NOT NULL DEFAULT 'running', `result_json` text NULL, `error_message` varchar(500) NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_f15aa4c89aa57366fdba8a84db` (`operation_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `expense_types` (`id` int NOT NULL AUTO_INCREMENT, `code` varchar(30) NOT NULL, `name` varchar(30) NOT NULL, `category` varchar(20) NOT NULL DEFAULT 'room', `sort_order` int NOT NULL DEFAULT '0', `enabled` tinyint NOT NULL DEFAULT 1, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_36eda3eb0f6740ecf2ba906012` (`code`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `exam_scores` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `enrollment_id` int NULL, `exam_type` varchar(50) NULL, `exam_name` varchar(100) NULL, `subject` varchar(50) NULL, `score` decimal(5,2) NULL, `class_avg` decimal(5,2) NULL, `rank` int NULL, `exam_date` date NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `ding_attendance_raw` (`id` int NOT NULL AUTO_INCREMENT, `ding_user_id` varchar(100) NOT NULL, `user_name` varchar(100) NOT NULL, `attendance_date` date NOT NULL, `ding_id` varchar(100) NOT NULL, `check_in_time` datetime NULL, `check_out_time` datetime NULL, `attendance_type` varchar(20) NOT NULL, `time_result` varchar(20) NOT NULL, `location_result` varchar(20) NULL, `punch_source` varchar(40) NULL, `punch_device_name` varchar(100) NULL, `punch_device_id` varchar(100) NULL, `match_status` varchar(20) NOT NULL DEFAULT 'unmatched', `matched_student_id` int NULL, `raw_data` text NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), INDEX `IDX_d6680d5150f0bfa47dbc1fe96f` (`match_status`), INDEX `IDX_088de070f537b15ab7da255e5b` (`attendance_date`), UNIQUE INDEX `IDX_997849bb04ff69149fcf00a8d3` (`ding_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `deposit_installments` (`id` int NOT NULL AUTO_INCREMENT, `deposit_id` int NOT NULL, `amount` decimal(10,2) NOT NULL, `due_date` date NOT NULL, `paid_date` date NULL, `status` varchar(20) NOT NULL DEFAULT 'pending', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `deposits` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `amount` decimal(10,2) NOT NULL DEFAULT '500.00', `status` varchar(20) NOT NULL DEFAULT 'paid', `paid_date` date NOT NULL, `refund_date` date NULL, `refund_amount` decimal(10,2) NULL, `deduction_amount` decimal(10,2) NOT NULL DEFAULT '0.00', `deduction_reason` text NULL, `notes` text NULL, `recorded_by` int NULL, `refunded_by` int NULL, `refunded_at` datetime NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `classrooms` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `building` varchar(50) NULL, `floor` int NULL, `capacity` int NOT NULL DEFAULT '30', `room_type` varchar(20) NOT NULL DEFAULT '', `status` varchar(20) NOT NULL DEFAULT 'available', `notes` text NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `classroom_rentals` (`id` int NOT NULL AUTO_INCREMENT, `classroom_id` int NOT NULL, `lessor_organization_id` int NULL, `lessee_organization_id` int NULL, `start_date` date NOT NULL, `end_date` date NOT NULL, `contract_path` varchar(255) NULL, `contract_original_name` varchar(255) NULL, `daily_rate` decimal(10,2) NULL, `total_amount` decimal(10,2) NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `notes` text NULL, `created_by` int NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), INDEX `IDX_b748a951d00b3f0c2090d10397` (`classroom_id`, `start_date`, `end_date`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `classes` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `code` varchar(50) NOT NULL, `class_type` varchar(20) NOT NULL, `start_date` date NULL, `end_date` date NULL, `status` varchar(20) NOT NULL DEFAULT 'enrolling', `head_teacher_id` int NULL, `life_teacher_id` int NULL, `academic_teacher_id` int NULL, `max_students` int NOT NULL DEFAULT '0', `notes` text NULL, `is_archived` tinyint NOT NULL DEFAULT 0, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_cf7491878e0fca859943862998` (`code`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `class_teacher` (`id` int NOT NULL AUTO_INCREMENT, `class_id` int NOT NULL, `user_id` int NOT NULL, `role_type` varchar(30) NOT NULL, `subject` varchar(50) NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_0117e9171e1533ff7b5c63f5d6` (`class_id`, `user_id`, `role_type`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `class_student` (`id` int NOT NULL AUTO_INCREMENT, `class_id` int NOT NULL, `student_id` int NOT NULL, `join_date` date NULL, `leave_date` date NULL, `status` varchar(10) NOT NULL DEFAULT 'active', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_b0ed786e05e93dd9bf77189af1` (`class_id`, `student_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `class_schedule` (`id` int NOT NULL AUTO_INCREMENT, `class_id` int NULL, `classroom_id` int NOT NULL, `week_day` int NOT NULL, `start_time` varchar(5) NOT NULL, `end_time` varchar(5) NOT NULL, `attendance_advance_minutes` int NOT NULL DEFAULT '30', `start_date` date NOT NULL, `end_date` date NOT NULL, `subject` varchar(50) NOT NULL, `teacher_id` int NULL, `schedule_type` varchar(20) NOT NULL DEFAULT 'INTERNAL', `rental_id` int NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `notes` text NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `attendance_records` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `class_id` int NULL, `schedule_id` int NULL, `attendance_session_id` int NULL, `attendance_date` date NOT NULL, `session` varchar(20) NOT NULL, `status` varchar(20) NOT NULL, `remark` varchar(200) NULL, `source` varchar(20) NOT NULL DEFAULT 'manual', `punch_time` datetime NULL, `punch_source` varchar(40) NULL, `punch_device_name` varchar(100) NULL, `punch_device_id` varchar(100) NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_43ac61c4ffa4117b738dd3a1e4` (`attendance_session_id`, `student_id`), INDEX `IDX_6b8f69a76bea22962e54bd21f5` (`student_id`, `attendance_date`), INDEX `IDX_a8eee7b8a0e0af27e79cb27ccc` (`class_id`, `attendance_date`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `attendance_sessions` (`id` int NOT NULL AUTO_INCREMENT, `schedule_id` int NOT NULL, `class_id` int NOT NULL, `lesson_date` date NOT NULL, `status` varchar(20) NOT NULL DEFAULT 'in_progress', `started_by` int NULL, `started_at` datetime NULL, `completed_by` int NULL, `completed_at` datetime NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `IDX_d4288982a2baaa6085cf872990` (`schedule_id`, `lesson_date`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `attendance_period_configs` (`id` int NOT NULL AUTO_INCREMENT, `period_key` varchar(40) NOT NULL, `label` varchar(40) NOT NULL, `start_time` varchar(5) NOT NULL, `end_time` varchar(5) NOT NULL, `sort_order` int NOT NULL DEFAULT '0', `enabled` tinyint NOT NULL DEFAULT 1, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), INDEX `IDX_02b29b9018684af3e3837408e0` (`sort_order`), UNIQUE INDEX `IDX_1cd8e45776b2dd7efb84ad994e` (`period_key`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `attendance_devices` (`id` int NOT NULL AUTO_INCREMENT, `device_sn` varchar(100) NOT NULL, `device_name` varchar(100) NOT NULL, `classroom_id` int NOT NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `location` varchar(200) NULL, `notes` text NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), INDEX `IDX_2d2806b8906e201230afb3376a` (`classroom_id`), UNIQUE INDEX `IDX_c6835138ea258d1259246dd736` (`device_sn`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `archive_attachments` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `category` varchar(50) NULL, `file_name` varchar(255) NULL, `file_path` varchar(500) NULL, `file_size` int NULL, `mime_type` varchar(100) NULL, `status` varchar(20) NOT NULL DEFAULT 'active', `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `ai_config` (`id` int NOT NULL AUTO_INCREMENT, `singleton_key` varchar(20) NOT NULL DEFAULT 'GLOBAL', `provider` varchar(50) NOT NULL DEFAULT 'OPENAI', `base_url` varchar(500) NULL, `encrypted_api_key` text NULL, `api_key_iv` varchar(50) NULL, `api_key_auth_tag` varchar(50) NULL, `key_last4` varchar(4) NULL, `default_model` varchar(100) NULL, `enabled` tinyint NOT NULL DEFAULT 0, `timeout_ms` int NOT NULL DEFAULT '30000', `verified` tinyint NOT NULL DEFAULT 0, `last_tested_at` datetime NULL, `last_test_latency_ms` int NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX `uq_ai_config_singleton` (`singleton_key`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `integration_config` (`id` int NOT NULL AUTO_INCREMENT, `type` varchar(50) NOT NULL, `sync_resource` varchar(50) NULL, `is_sync` tinyint NOT NULL DEFAULT 0, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `integration_config_detail` (`id` int NOT NULL AUTO_INCREMENT, `config_id` int NOT NULL, `name` varchar(100) NULL, `type` varchar(50) NOT NULL, `content` text NULL, `enable` tinyint NOT NULL DEFAULT 0, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), INDEX `IDX_a58106c4b876c86a5e085e4f42` (`config_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `role_permissions` (`role_id` int NOT NULL, `permission_id` int NOT NULL, INDEX `IDX_178199805b901ccd220ab7740e` (`role_id`), INDEX `IDX_17022daf3f885f7d35423e9971` (`permission_id`), PRIMARY KEY (`role_id`, `permission_id`)) ENGINE=InnoDB;
CREATE TABLE `user_roles` (`user_id` int NOT NULL, `role_id` int NOT NULL, INDEX `IDX_87b8888186ca9769c960e92687` (`user_id`), INDEX `IDX_b23c65e50a758245a33ee35fda` (`role_id`), PRIMARY KEY (`user_id`, `role_id`)) ENGINE=InnoDB;
ALTER TABLE `room_expenses` ADD CONSTRAINT `FK_7bb1ca73f8161af27e538917a55` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `beds` ADD CONSTRAINT `FK_fd7413faee42749f1b2e8270394` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `lockers` ADD CONSTRAINT `FK_344ae29b9cacbee1e24da7c07ea` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `occupancies` ADD CONSTRAINT `FK_bed301f5a4e2135dd14b55fec0e` FOREIGN KEY (`bed_id`) REFERENCES `beds`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `occupancies` ADD CONSTRAINT `FK_ea80244bd95c8c04334931e39a2` FOREIGN KEY (`locker_id`) REFERENCES `lockers`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `occupancies` ADD CONSTRAINT `FK_3c80232a56303a75ffd544b982f` FOREIGN KEY (`responsible_organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `occupancies` ADD CONSTRAINT `FK_dc062720596049470b56ce06973` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `occupancies` ADD CONSTRAINT `FK_9f7acc7567e42dda281ead9a75a` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `personal_expenses` ADD CONSTRAINT `FK_5e5bcfb70705c2c349c74b70912` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `bill_items` ADD CONSTRAINT `FK_b424156152a3230b034bdb51db4` FOREIGN KEY (`bill_id`) REFERENCES `bills`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `bills` ADD CONSTRAINT `FK_65a6cb602ebcbac1bf7a995e0c9` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `students` ADD CONSTRAINT `FK_fb3eff90b11bddf7285f9b4e281` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;
ALTER TABLE `students` ADD CONSTRAINT `FK_9571384818ecf499779d3a9d141` FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `student_wallets` ADD CONSTRAINT `FK_07a434ad1a960d506386754d592` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `student_profiles` ADD CONSTRAINT `FK_4cedc08d3dc1f2c2da8a12f7a88` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `student_enrollments` ADD CONSTRAINT `FK_08caafd8a026a19ecf54db0e958` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `student_ding_mapping` ADD CONSTRAINT `FK_f9ba15ff04de8ffbd8679ae9dbb` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `result_archives` ADD CONSTRAINT `FK_377bba8eb6a027eecd9737d4ed6` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `notifications` ADD CONSTRAINT `FK_5332a4daa46fd3f4e6625dd275d` FOREIGN KEY (`recipient_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `learning_records` ADD CONSTRAINT `FK_61f4664f02d86245e1231493951` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `exam_scores` ADD CONSTRAINT `FK_925e4f79518c947512cb600b452` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `exam_scores` ADD CONSTRAINT `FK_b0d812b639f32939ea57b81a89f` FOREIGN KEY (`enrollment_id`) REFERENCES `student_enrollments`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `ding_attendance_raw` ADD CONSTRAINT `FK_2b06491a70540db69e78e57ecf2` FOREIGN KEY (`matched_student_id`) REFERENCES `students`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;
ALTER TABLE `deposit_installments` ADD CONSTRAINT `FK_fba3c52b86d51fff34593877ff5` FOREIGN KEY (`deposit_id`) REFERENCES `deposits`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `deposits` ADD CONSTRAINT `FK_ec1b340d2963ed907421696fc33` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `classroom_rentals` ADD CONSTRAINT `FK_6a0af46fe476070c4c660307641` FOREIGN KEY (`classroom_id`) REFERENCES `classrooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `classroom_rentals` ADD CONSTRAINT `FK_40a46e95470ff454ffdcbb1e902` FOREIGN KEY (`lessor_organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `classroom_rentals` ADD CONSTRAINT `FK_f60533a91f16a331c9388308418` FOREIGN KEY (`lessee_organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `class_teacher` ADD CONSTRAINT `FK_9b647c8d8894a661dc1dac40361` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `class_teacher` ADD CONSTRAINT `FK_a9f0e77d193b015b6d5ca6637f5` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `class_student` ADD CONSTRAINT `FK_60674dc4f23b794de0a3560e200` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `class_student` ADD CONSTRAINT `FK_451334295b9cf221d55aca7cac1` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `class_schedule` ADD CONSTRAINT `FK_0aa74ee427967d4c82f298511bc` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `class_schedule` ADD CONSTRAINT `FK_c1d5d50c871fdf563be13d6e93a` FOREIGN KEY (`classroom_id`) REFERENCES `classrooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `class_schedule` ADD CONSTRAINT `FK_f1e761cac59f2d0a3d7949468c5` FOREIGN KEY (`teacher_id`) REFERENCES `users`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `attendance_records` ADD CONSTRAINT `FK_dbace05c012526710663f8d8911` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `attendance_records` ADD CONSTRAINT `FK_cc48ccbd03396874857ab06ac3b` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;
ALTER TABLE `attendance_records` ADD CONSTRAINT `FK_0dfe6d219c4a7a162ca0f84243c` FOREIGN KEY (`schedule_id`) REFERENCES `class_schedule`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;
ALTER TABLE `attendance_records` ADD CONSTRAINT `FK_32a57ba853da6939ceaccf8cfc8` FOREIGN KEY (`attendance_session_id`) REFERENCES `attendance_sessions`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION;
ALTER TABLE `attendance_sessions` ADD CONSTRAINT `FK_107206b64415a644d571d597ca3` FOREIGN KEY (`schedule_id`) REFERENCES `class_schedule`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION;
ALTER TABLE `attendance_sessions` ADD CONSTRAINT `FK_6fc552e07b31ac92445cd3e21fa` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION;
ALTER TABLE `attendance_devices` ADD CONSTRAINT `FK_2d2806b8906e201230afb3376af` FOREIGN KEY (`classroom_id`) REFERENCES `classrooms`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
ALTER TABLE `archive_attachments` ADD CONSTRAINT `FK_0bc7365c2629d0c6dae8052fc25` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `integration_config_detail` ADD CONSTRAINT `FK_a58106c4b876c86a5e085e4f42b` FOREIGN KEY (`config_id`) REFERENCES `integration_config`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE `role_permissions` ADD CONSTRAINT `FK_178199805b901ccd220ab7740ec` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `role_permissions` ADD CONSTRAINT `FK_17022daf3f885f7d35423e9971e` FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_roles` ADD CONSTRAINT `FK_87b8888186ca9769c960e926870` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `user_roles` ADD CONSTRAINT `FK_b23c65e50a758245a33ee35fda1` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
CREATE TABLE `exams` (
`id` int NOT NULL AUTO_INCREMENT,
`exam_type` varchar(50) NOT NULL,
`exam_name` varchar(100) NOT NULL,
`subject` varchar(50) NOT NULL,
`exam_date` date NOT NULL,
`class_id` int NOT NULL,
`status` varchar(20) NOT NULL DEFAULT 'active',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_exams_class_status` (`class_id`, `status`),
CONSTRAINT `fk_exams_class` FOREIGN KEY (`class_id`) REFERENCES `classes` (`id`),
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
ALTER TABLE `exam_scores` ADD COLUMN `exam_id` int NULL;
CREATE INDEX `idx_exam_scores_exam_id` ON `exam_scores` (`exam_id`);
ALTER TABLE `exam_scores` ADD CONSTRAINT `fk_exam_scores_exam` FOREIGN KEY (`exam_id`) REFERENCES `exams` (`id`) ON DELETE CASCADE;
CREATE TABLE `room_inspections` (
`id` int NOT NULL AUTO_INCREMENT, `inspection_date` date NOT NULL, `room_id` int NOT NULL,
`inspector_id` int NULL, `inspector_name` varchar(50) NOT NULL, `source` varchar(20) NOT NULL DEFAULT 'manual',
`submitted_at` datetime NOT NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
UNIQUE INDEX `uq_room_inspections_date_room` (`inspection_date`, `room_id`),
INDEX `idx_room_inspections_inspector_id` (`inspector_id`), PRIMARY KEY (`id`),
CONSTRAINT `fk_room_inspections_room` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_room_inspections_inspector` FOREIGN KEY (`inspector_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE `room_inspection_details` (
`id` int NOT NULL AUTO_INCREMENT, `inspection_id` int NOT NULL, `occupancy_id` int NOT NULL, `student_id` int NOT NULL,
`bed_id` int NULL, `status` varchar(20) NOT NULL, `student_name_snapshot` varchar(100) NOT NULL,
`bed_number_snapshot` varchar(20) NULL, UNIQUE INDEX `uq_room_inspection_details_occupancy` (`inspection_id`, `occupancy_id`),
INDEX `idx_room_inspection_details_occupancy_id` (`occupancy_id`), INDEX `idx_room_inspection_details_student_id` (`student_id`),
INDEX `idx_room_inspection_details_bed_id` (`bed_id`), PRIMARY KEY (`id`),
CONSTRAINT `fk_room_inspection_details_inspection` FOREIGN KEY (`inspection_id`) REFERENCES `room_inspections` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_room_inspection_details_occupancy` FOREIGN KEY (`occupancy_id`) REFERENCES `occupancies` (`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_room_inspection_details_student` FOREIGN KEY (`student_id`) REFERENCES `students` (`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_room_inspection_details_bed` FOREIGN KEY (`bed_id`) REFERENCES `beds` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE `jinshuju_match_rules` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `form_token` varchar(64) NOT NULL, `mappings` text NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB;
CREATE TABLE `ai_conversations` (`id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `title` varchar(100) NOT NULL DEFAULT '新对话', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `last_message_at` datetime NULL, INDEX `idx_ai_conversations_user_last_message` (`user_id`, `last_message_at`), PRIMARY KEY (`id`), CONSTRAINT `fk_ai_conversations_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE) ENGINE=InnoDB;
CREATE TABLE `ai_messages` (`id` int NOT NULL AUTO_INCREMENT, `conversation_id` int NOT NULL, `role` varchar(20) NOT NULL, `content` text NOT NULL, `reasoning_content` text NULL, `status` varchar(20) NOT NULL DEFAULT 'completed', `error_code` varchar(50) NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX `idx_ai_messages_conversation_created` (`conversation_id`, `created_at`), PRIMARY KEY (`id`), CONSTRAINT `fk_ai_messages_conversation` FOREIGN KEY (`conversation_id`) REFERENCES `ai_conversations` (`id`) ON DELETE CASCADE) ENGINE=InnoDB;
CREATE TABLE `ai_tool_runs` (`id` int NOT NULL AUTO_INCREMENT, `message_id` int NOT NULL, `tool_call_id` varchar(100) NOT NULL, `tool_name` varchar(64) NOT NULL, `arguments_summary` text NULL, `result_summary` text NULL, `status` varchar(20) NOT NULL, `duration_ms` int NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX `idx_ai_tool_runs_message` (`message_id`), PRIMARY KEY (`id`), CONSTRAINT `fk_ai_tool_runs_message` FOREIGN KEY (`message_id`) REFERENCES `ai_messages` (`id`) ON DELETE CASCADE) ENGINE=InnoDB;
CREATE TABLE `evening_study_duties` (
`id` int NOT NULL AUTO_INCREMENT, `duty_date` date NOT NULL, `teacher_id` int NULL, `teacher_name` varchar(50) NOT NULL,
`source` varchar(100) NOT NULL DEFAULT '暑期班课表 更新-2.xlsx', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX `uq_evening_study_duty_date` (`duty_date`), PRIMARY KEY (`id`),
CONSTRAINT `fk_evening_study_duty_teacher` FOREIGN KEY (`teacher_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB;
CREATE TABLE `migrations` (`id` int NOT NULL AUTO_INCREMENT, `timestamp` bigint NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
INSERT INTO `migrations` (`timestamp`, `name`) VALUES
(1784520727860, 'InitialSchema1784520727860'),
(1784600000000, 'AddExamManagement1784600000000'),
(1784680000000, 'AddRoomInspections1784680000000'),
(1784700000000, 'AddJinshujuMatchRules1784700000000'),
(1784780000000, 'AddAiChat1784780000000');