forked from wangziqi/gongxue-base
Merge pull request '添加考勤设备 SN 教室绑定' (#16)
This commit is contained in:
@@ -8,11 +8,15 @@ MYSQL_ROOT_PASSWORD=change-me-to-a-strong-password
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_DATABASE=gongxue
|
||||
DB_DATABASE=dorm_billing_v2
|
||||
DB_SYNCHRONIZE=false
|
||||
JWT_SECRET=change-me-to-a-random-string-at-least-32-chars
|
||||
JWT_EXPIRES_IN=24h
|
||||
PORT=3000
|
||||
|
||||
# 初始管理员 admin 密码(仅首次创建 admin 用户时生效)
|
||||
ADMIN_PASSWORD=change-me-admin-password
|
||||
|
||||
PORT=3002
|
||||
|
||||
# ---- AI 模型配置 ----
|
||||
# AES-256-GCM 加密主密钥,用于加密存储 API Key
|
||||
|
||||
@@ -31,6 +31,7 @@ const SchedulesPage = lazy(() => import('./pages/Schedules'));
|
||||
const RolesPage = lazy(() => import('./pages/Roles'));
|
||||
const PermissionsPage = lazy(() => import('./pages/Permissions'));
|
||||
const AttendancePage = lazy(() => import('./pages/Attendance'));
|
||||
const AttendanceDevicesPage = lazy(() => import('./pages/AttendanceDevices'));
|
||||
const TeacherWorkspacePage = lazy(() => import('./pages/TeacherWorkspace'));
|
||||
const NotificationsPage = lazy(() => import('./pages/Notifications'));
|
||||
const IntegrationConfigPage = lazy(() => import('./pages/IntegrationConfig'));
|
||||
@@ -240,6 +241,16 @@ const App: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
<Route
|
||||
path="attendance-devices"
|
||||
element={
|
||||
<PermissionRoute permission="classroom:view">
|
||||
<AttendanceDevicesPage />
|
||||
</PermissionRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="attendance"
|
||||
element={
|
||||
|
||||
@@ -85,6 +85,7 @@ const SECTIONS: MenuSection[] = [
|
||||
children: [
|
||||
{ key: '/classroom-schedule', label: '教室排期', icon: 'calendar', permission: 'rental:view' },
|
||||
{ key: '/classrooms', label: '教室管理', icon: 'classroom', permission: 'classroom:view' },
|
||||
{ key: '/attendance-devices', label: '考勤机绑定', icon: 'attendance', permission: 'classroom:view' },
|
||||
{ key: '/classroom-rentals', label: '租赁订单', icon: 'rental', permission: 'rental:view' },
|
||||
{ key: '/organizations', label: '机构管理', icon: 'organization', permission: 'organization:view' },
|
||||
],
|
||||
|
||||
@@ -20,6 +20,7 @@ export const PERMISSION_PAGES: readonly PermissionPage[] = [
|
||||
{ path: '/schedules', permission: 'schedule:view' },
|
||||
{ path: '/classroom-schedule', permission: 'rental:view' },
|
||||
{ path: '/classrooms', permission: 'classroom:view' },
|
||||
{ path: '/attendance-devices', permission: 'classroom:view' },
|
||||
{ path: '/classroom-rentals', permission: 'rental:view' },
|
||||
{ path: '/organizations', permission: 'organization:view' },
|
||||
{ path: '/expenses', permission: 'expense:view' },
|
||||
|
||||
215
apps/admin/src/pages/AttendanceDevices.tsx
Normal file
215
apps/admin/src/pages/AttendanceDevices.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Empty, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import api from '../api';
|
||||
import PermissionButton from '../components/PermissionButton';
|
||||
import { message } from '../ui/app-message';
|
||||
|
||||
interface ClassroomOption {
|
||||
id: number;
|
||||
name: string;
|
||||
building?: string | null;
|
||||
}
|
||||
|
||||
interface AttendanceDeviceRow {
|
||||
id: number;
|
||||
deviceSn: string;
|
||||
deviceName: string;
|
||||
classroomId: number;
|
||||
classroom?: ClassroomOption | null;
|
||||
status: 'active' | 'disabled';
|
||||
location?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
const statusMeta = {
|
||||
active: { text: '启用', color: 'green' },
|
||||
disabled: { text: '停用', color: 'default' },
|
||||
} as const;
|
||||
|
||||
const AttendanceDevicesPage: React.FC = () => {
|
||||
const [data, setData] = useState<AttendanceDeviceRow[]>([]);
|
||||
const [classrooms, setClassrooms] = useState<ClassroomOption[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<AttendanceDeviceRow | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [devices, classroomList] = await Promise.all([
|
||||
api.get<AttendanceDeviceRow[]>('/attendance-devices'),
|
||||
api.get<ClassroomOption[]>('/classrooms'),
|
||||
]);
|
||||
setData(devices);
|
||||
setClassrooms(classroomList.filter((item: any) => item.status !== 'archived'));
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载考勤机绑定失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, []);
|
||||
|
||||
const classroomOptions = useMemo(
|
||||
() => classrooms.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.building ? `${item.name}(${item.building})` : item.name,
|
||||
})),
|
||||
[classrooms],
|
||||
);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
const text = keyword.trim().toLocaleLowerCase('zh-CN');
|
||||
if (!text) return data;
|
||||
return data.filter((item) => [
|
||||
item.deviceSn,
|
||||
item.deviceName,
|
||||
item.classroom?.name,
|
||||
item.location,
|
||||
].some((value) => (value || '').toLocaleLowerCase('zh-CN').includes(text)));
|
||||
}, [data, keyword]);
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ status: 'active' });
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (record: AttendanceDeviceRow) => {
|
||||
setEditing(record);
|
||||
form.setFieldsValue({
|
||||
deviceSn: record.deviceSn,
|
||||
deviceName: record.deviceName,
|
||||
classroomId: record.classroomId,
|
||||
status: record.status,
|
||||
location: record.location,
|
||||
notes: record.notes,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await api.put(`/attendance-devices/${editing.id}`, values);
|
||||
message.success('考勤机绑定已更新');
|
||||
} else {
|
||||
await api.post('/attendance-devices', values);
|
||||
message.success('考勤机绑定已创建');
|
||||
}
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
form.resetFields();
|
||||
await loadData();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await api.delete(`/attendance-devices/${id}`);
|
||||
message.success('已删除绑定');
|
||||
await loadData();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<AttendanceDeviceRow> = [
|
||||
{ title: '设备名称', dataIndex: 'deviceName', width: 180 },
|
||||
{ title: 'SN 码', dataIndex: 'deviceSn', width: 220, render: (value) => <span style={{ fontFamily: 'monospace' }}>{value}</span> },
|
||||
{ title: '绑定教室', dataIndex: ['classroom', 'name'], width: 160, render: (_value, record) => record.classroom?.name || `教室 ${record.classroomId}` },
|
||||
{ title: '位置', dataIndex: 'location', render: (value) => value || <span style={{ color: '#999' }}>—</span> },
|
||||
{ title: '状态', dataIndex: 'status', width: 90, render: (value: keyof typeof statusMeta) => <Tag color={statusMeta[value]?.color}>{statusMeta[value]?.text || value}</Tag> },
|
||||
{ title: '备注', dataIndex: 'notes', ellipsis: true, render: (value) => value || <span style={{ color: '#999' }}>—</span> },
|
||||
{
|
||||
title: '操作',
|
||||
width: 150,
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<PermissionButton permission="classroom:edit" size="small" type="link" onClick={() => openEdit(record)}>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
<Popconfirm title="确定删除此考勤机绑定?" onConfirm={() => handleDelete(record.id)}>
|
||||
<PermissionButton permission="classroom:edit" size="small" danger>
|
||||
删除
|
||||
</PermissionButton>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder="搜索设备/SN/教室"
|
||||
style={{ width: 260 }}
|
||||
value={keyword}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
/>
|
||||
<PermissionButton permission="classroom:edit" type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
添加考勤机
|
||||
</PermissionButton>
|
||||
</div>
|
||||
<Table<AttendanceDeviceRow>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={filteredData}
|
||||
loading={loading}
|
||||
locale={{ emptyText: <Empty description="暂无考勤机绑定" /> }}
|
||||
pagination={{ defaultPageSize: 20, showSizeChanger: true }}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? '编辑考勤机绑定' : '添加考勤机绑定'}
|
||||
open={modalOpen}
|
||||
onOk={handleSave}
|
||||
onCancel={() => {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
}}
|
||||
confirmLoading={saving}
|
||||
okText="保存"
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="deviceName" label="设备名称" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Input placeholder="如:彼岸游境_N1604" />
|
||||
</Form.Item>
|
||||
<Form.Item name="deviceSn" label="SN 码" rules={[{ required: true, message: '请输入钉钉返回的 deviceSN' }]}>
|
||||
<Input placeholder="如:300419260325WN1604" />
|
||||
</Form.Item>
|
||||
<Form.Item name="classroomId" label="绑定教室" rules={[{ required: true, message: '请选择绑定教室' }]}>
|
||||
<Select showSearch optionFilterProp="label" options={classroomOptions} placeholder="选择教室" />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" initialValue="active">
|
||||
<Select options={[{ value: 'active', label: '启用' }, { value: 'disabled', label: '停用' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="location" label="位置">
|
||||
<Input placeholder="如:教学楼一楼东侧" />
|
||||
</Form.Item>
|
||||
<Form.Item name="notes" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttendanceDevicesPage;
|
||||
@@ -27,7 +27,10 @@ const WalletsPage: React.FC = () => {
|
||||
const [transactions, setTransactions] = useState<any[]>([]);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [batchForm] = Form.useForm();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchModalOpen, setBatchModalOpen] = useState(false);
|
||||
|
||||
const fetchRows = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -46,6 +49,11 @@ const WalletsPage: React.FC = () => {
|
||||
form.setFieldsValue({ type: 'recharge', amount: undefined, description: '' });
|
||||
};
|
||||
|
||||
const openBatchChange = () => {
|
||||
batchForm.setFieldsValue({ type: 'recharge', amount: undefined, description: '' });
|
||||
setBatchModalOpen(true);
|
||||
};
|
||||
|
||||
const submitChange = async () => {
|
||||
if (!selected) return;
|
||||
const values = await form.validateFields();
|
||||
@@ -60,6 +68,30 @@ const WalletsPage: React.FC = () => {
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const submitBatchChange = async () => {
|
||||
const values = await batchForm.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
const result: any = await api.post('/wallets/batch-change-balance', {
|
||||
studentIds: selectedRowKeys,
|
||||
...values,
|
||||
});
|
||||
const paid = (result.results || []).reduce((sum: number, item: any) => {
|
||||
return sum + (item.payments || []).reduce((paymentSum: number, bill: any) => paymentSum + Number(bill.paidAmount || 0), 0);
|
||||
}, 0);
|
||||
message.success(
|
||||
paid > 0
|
||||
? `已批量更新 ${selectedRowKeys.length} 名学生余额,并自动补扣历史账单`
|
||||
: `已批量更新 ${selectedRowKeys.length} 名学生余额`,
|
||||
);
|
||||
setBatchModalOpen(false);
|
||||
setSelectedRowKeys([]);
|
||||
batchForm.resetFields();
|
||||
await fetchRows();
|
||||
} catch (error: any) { message.error(error?.message || '批量余额操作失败'); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const showTransactions = async (row: WalletRow) => {
|
||||
setSelected(row); setDrawerOpen(true);
|
||||
try { setTransactions(await api.get('/wallets/transactions', { params: { studentId: row.studentId } }) as any[]); }
|
||||
@@ -76,9 +108,19 @@ const WalletsPage: React.FC = () => {
|
||||
return <div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<Space wrap><Input.Search allowClear placeholder="搜索姓名或学号" style={{ width: 240 }} onSearch={setKeyword} onChange={(event) => !event.target.value && setKeyword('')} /><span>仅看欠费</span><Switch checked={debtOnly} onChange={setDebtOnly} /></Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchRows}>刷新</Button>
|
||||
<Space wrap>
|
||||
<PermissionButton permission="wallet:edit" type="primary" icon={<PlusOutlined />} disabled={selectedRowKeys.length === 0} onClick={openBatchChange}>批量充值/调账</PermissionButton>
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchRows}>刷新</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table rowKey="studentId" loading={loading} dataSource={rows} columns={columns} pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 人` }} />
|
||||
<Table
|
||||
rowKey="studentId"
|
||||
loading={loading}
|
||||
dataSource={rows}
|
||||
columns={columns}
|
||||
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
||||
pagination={{ pageSize: 15, showTotal: (total) => `共 ${total} 人` }}
|
||||
/>
|
||||
<Modal title={`${selected?.studentName || ''} - 余额操作`} open={!!selected && !drawerOpen} onCancel={() => setSelected(null)} onOk={submitChange} confirmLoading={saving} okText="确认">
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="type" label="操作类型" rules={[{ required: true }]}><Radio.Group options={[{ label: '充值', value: 'recharge' }, { label: '调账', value: 'adjustment' }]} /></Form.Item>
|
||||
@@ -86,6 +128,27 @@ const WalletsPage: React.FC = () => {
|
||||
<Form.Item name="description" label="备注"><Input.TextArea maxLength={300} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={`批量余额操作(${selectedRowKeys.length} 人)`}
|
||||
open={batchModalOpen}
|
||||
onCancel={() => setBatchModalOpen(false)}
|
||||
onOk={submitBatchChange}
|
||||
confirmLoading={saving}
|
||||
okText="确认批量修改"
|
||||
>
|
||||
<Form form={batchForm} layout="vertical">
|
||||
<div style={{ marginBottom: 16, padding: 12, background: '#f5f5f5', borderRadius: 8 }}>
|
||||
已选择 <strong>{selectedRowKeys.length}</strong> 名学生,将按相同金额批量修改水电费余额。
|
||||
</div>
|
||||
<Form.Item name="type" label="操作类型" rules={[{ required: true }]}>
|
||||
<Radio.Group options={[{ label: '充值', value: 'recharge' }, { label: '调账', value: 'adjustment' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="amount" label="变动金额(元/人)" extra="充值填正数;调减余额时填写负数。充值后会按最早账单优先自动补扣。" rules={[{ required: true, message: '请输入金额' }]}>
|
||||
<InputNumber precision={2} style={{ width: '100%' }} addonBefore="¥" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="备注"><Input.TextArea maxLength={300} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Drawer title={`${selected?.studentName || ''} - 余额流水`} width={680} open={drawerOpen} onClose={() => { setDrawerOpen(false); setSelected(null); }}>
|
||||
<Table rowKey="id" dataSource={transactions} pagination={{ pageSize: 10 }} columns={[
|
||||
{ title: '时间', dataIndex: 'createdAt', render: (value: string) => dayjs(value).format('YYYY-MM-DD HH:mm') },
|
||||
|
||||
@@ -22,7 +22,7 @@ JWT_EXPIRES_IN=24h
|
||||
ADMIN_PASSWORD=请替换为强密码
|
||||
|
||||
# ---- 服务端口 ----
|
||||
PORT=3000
|
||||
PORT=3002
|
||||
|
||||
# ---- 文件上传 ----
|
||||
# 合同 PDF 存储根目录(相对或绝对)
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
ClassSchedule,
|
||||
AttendanceRecord,
|
||||
AttendanceSession,
|
||||
AttendanceDevice,
|
||||
DingAttendanceRaw,
|
||||
SyncLog,
|
||||
SyncState,
|
||||
@@ -64,6 +65,7 @@ import { ClassroomsModule } from './classrooms/classrooms.module';
|
||||
import { ClassesModule } from './classes/classes.module';
|
||||
import { OrganizationsModule } from './organizations/organizations.module';
|
||||
import { AttendanceModule } from './attendance/attendance.module';
|
||||
import { AttendanceDevicesModule } from './attendance-devices/attendance-devices.module';
|
||||
import { SchedulesModule } from './schedules/schedules.module';
|
||||
import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.module';
|
||||
import { SyncModule } from './sync/sync.module';
|
||||
@@ -123,6 +125,7 @@ import { IntegrationConfigModule } from './integration/config/config.module';
|
||||
ClassSchedule,
|
||||
AttendanceRecord,
|
||||
AttendanceSession,
|
||||
AttendanceDevice,
|
||||
DingAttendanceRaw,
|
||||
Notification,
|
||||
StudentProfile,
|
||||
@@ -176,6 +179,7 @@ import { IntegrationConfigModule } from './integration/config/config.module';
|
||||
WalletsModule,
|
||||
ClassroomsModule,
|
||||
AttendanceModule,
|
||||
AttendanceDevicesModule,
|
||||
ClassesModule,
|
||||
OrganizationsModule,
|
||||
SchedulesModule,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import { extractRequestInfo } from '../common/request-utils';
|
||||
import { AttendanceDevicesService } from './attendance-devices.service';
|
||||
import { CreateAttendanceDeviceDto, UpdateAttendanceDeviceDto } from './dto/attendance-device.dto';
|
||||
import { AttendanceDeviceStatus } from '../entities';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('attendance-devices')
|
||||
export class AttendanceDevicesController {
|
||||
constructor(
|
||||
private readonly service: AttendanceDevicesService,
|
||||
private readonly logService: OperationLogsService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('classroom:view')
|
||||
findAll(
|
||||
@Query('classroomId') classroomId?: string,
|
||||
@Query('status') status?: AttendanceDeviceStatus | 'active' | 'disabled',
|
||||
) {
|
||||
return this.service.findAll({
|
||||
classroomId: classroomId ? Number(classroomId) : undefined,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('classroom:edit')
|
||||
async create(@Body() dto: CreateAttendanceDeviceDto, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.create(dto);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '考勤机',
|
||||
action: '新增考勤机绑定',
|
||||
targetId: result.id,
|
||||
targetType: 'attendanceDevice',
|
||||
detail: `${result.deviceSn} -> ${result.classroom?.name || result.classroomId}`,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermission('classroom:edit')
|
||||
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateAttendanceDeviceDto, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.update(id, dto);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '考勤机',
|
||||
action: '编辑考勤机绑定',
|
||||
targetId: id,
|
||||
targetType: 'attendanceDevice',
|
||||
detail: JSON.stringify(dto),
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('classroom:edit')
|
||||
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.remove(id);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '考勤机',
|
||||
action: '删除考勤机绑定',
|
||||
targetId: id,
|
||||
targetType: 'attendanceDevice',
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AttendanceDevice, Classroom } from '../entities';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { AttendanceDevicesController } from './attendance-devices.controller';
|
||||
import { AttendanceDevicesService } from './attendance-devices.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AttendanceDevice, Classroom]), OperationLogsModule],
|
||||
controllers: [AttendanceDevicesController],
|
||||
providers: [AttendanceDevicesService],
|
||||
exports: [AttendanceDevicesService],
|
||||
})
|
||||
export class AttendanceDevicesModule {}
|
||||
105
apps/server/src/attendance-devices/attendance-devices.service.ts
Normal file
105
apps/server/src/attendance-devices/attendance-devices.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { AttendanceDevice, AttendanceDeviceStatus, Classroom } from '../entities';
|
||||
import { CreateAttendanceDeviceDto, UpdateAttendanceDeviceDto } from './dto/attendance-device.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AttendanceDevicesService {
|
||||
constructor(
|
||||
@InjectRepository(AttendanceDevice)
|
||||
private readonly repo: Repository<AttendanceDevice>,
|
||||
@InjectRepository(Classroom)
|
||||
private readonly classroomRepo: Repository<Classroom>,
|
||||
) {}
|
||||
|
||||
private normalizeSn(sn: string): string {
|
||||
return sn.trim();
|
||||
}
|
||||
|
||||
private async assertClassroomExists(classroomId: number): Promise<void> {
|
||||
const exists = await this.classroomRepo.exist({ where: { id: classroomId } });
|
||||
if (!exists) throw new BadRequestException('绑定教室不存在');
|
||||
}
|
||||
|
||||
async findAll(query?: { classroomId?: number; status?: AttendanceDeviceStatus | 'active' | 'disabled' }) {
|
||||
const where: Record<string, unknown> = {};
|
||||
if (query?.classroomId) where.classroomId = query.classroomId;
|
||||
if (query?.status) where.status = query.status;
|
||||
return this.repo.find({
|
||||
where,
|
||||
relations: ['classroom'],
|
||||
order: { classroomId: 'ASC', deviceName: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const device = await this.repo.findOne({ where: { id }, relations: ['classroom'] });
|
||||
if (!device) throw new NotFoundException('考勤机不存在');
|
||||
return device;
|
||||
}
|
||||
|
||||
async create(dto: CreateAttendanceDeviceDto) {
|
||||
const deviceSn = this.normalizeSn(dto.deviceSn);
|
||||
await this.assertClassroomExists(dto.classroomId);
|
||||
const exists = await this.repo.findOne({ where: { deviceSn } });
|
||||
if (exists) throw new BadRequestException(`SN ${deviceSn} 已绑定教室`);
|
||||
const saved = await this.repo.save(
|
||||
this.repo.create({
|
||||
...dto,
|
||||
deviceSn,
|
||||
deviceName: dto.deviceName.trim(),
|
||||
status: dto.status ?? AttendanceDeviceStatus.ACTIVE,
|
||||
}),
|
||||
);
|
||||
return this.findOne(saved.id);
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateAttendanceDeviceDto) {
|
||||
const device = await this.repo.findOne({ where: { id } });
|
||||
if (!device) throw new NotFoundException('考勤机不存在');
|
||||
const patch: Partial<AttendanceDevice> = { ...dto };
|
||||
if (dto.classroomId != null) await this.assertClassroomExists(dto.classroomId);
|
||||
if (dto.deviceSn != null) {
|
||||
const deviceSn = this.normalizeSn(dto.deviceSn);
|
||||
const exists = await this.repo.findOne({ where: { deviceSn } });
|
||||
if (exists && exists.id !== id) throw new BadRequestException(`SN ${deviceSn} 已绑定教室`);
|
||||
patch.deviceSn = deviceSn;
|
||||
}
|
||||
if (dto.deviceName != null) patch.deviceName = dto.deviceName.trim();
|
||||
await this.repo.update(id, patch);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const device = await this.repo.findOne({ where: { id } });
|
||||
if (!device) throw new NotFoundException('考勤机不存在');
|
||||
await this.repo.delete(id);
|
||||
return { message: '已删除' };
|
||||
}
|
||||
|
||||
async findActiveBySn(deviceSns: string[]) {
|
||||
const sns = [...new Set(deviceSns.map((sn) => this.normalizeSn(sn)).filter(Boolean))];
|
||||
if (sns.length === 0) return new Map<string, AttendanceDevice>();
|
||||
const devices = await this.repo.find({
|
||||
where: { deviceSn: In(sns), status: AttendanceDeviceStatus.ACTIVE },
|
||||
relations: ['classroom'],
|
||||
});
|
||||
return new Map(devices.map((device) => [device.deviceSn, device]));
|
||||
}
|
||||
|
||||
async findActiveByClassroomIds(classroomIds: number[]) {
|
||||
const ids = [...new Set(classroomIds.filter((id) => Number.isFinite(id)))];
|
||||
if (ids.length === 0) return new Map<number, AttendanceDevice>();
|
||||
const devices = await this.repo.find({
|
||||
where: { classroomId: In(ids), status: AttendanceDeviceStatus.ACTIVE },
|
||||
relations: ['classroom'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const result = new Map<number, AttendanceDevice>();
|
||||
for (const device of devices) {
|
||||
if (!result.has(device.classroomId)) result.set(device.classroomId, device);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { AttendanceDeviceStatus } from '../../entities/attendance-device.entity';
|
||||
|
||||
export class CreateAttendanceDeviceDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceSn: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceName: string;
|
||||
|
||||
@IsInt()
|
||||
classroomId: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AttendanceDeviceStatus)
|
||||
status?: AttendanceDeviceStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UpdateAttendanceDeviceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceSn?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
classroomId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AttendanceDeviceStatus)
|
||||
status?: AttendanceDeviceStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AttendanceRecord, AttendanceSession, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, ClassTeacher, StudentDingMapping } from '../entities';
|
||||
import { AttendanceRecord, AttendanceSession, AttendanceDevice, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, ClassTeacher, StudentDingMapping } from '../entities';
|
||||
import { AttendanceService } from './attendance.service';
|
||||
import { AttendanceImportService } from './attendance-import.service';
|
||||
import { AttendanceSettlementService } from './attendance-settlement.service';
|
||||
@@ -10,7 +10,7 @@ import { IntegrationModule } from '../integration/integration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AttendanceRecord, AttendanceSession, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, ClassTeacher, StudentDingMapping]),
|
||||
TypeOrmModule.forFeature([AttendanceRecord, AttendanceSession, AttendanceDevice, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, ClassTeacher, StudentDingMapping]),
|
||||
OperationLogsModule,
|
||||
IntegrationModule,
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Repository, In, Between, LessThanOrEqual, MoreThanOrEqual, DataSource }
|
||||
import {
|
||||
AttendanceRecord,
|
||||
AttendanceSession,
|
||||
AttendanceDevice,
|
||||
DingAttendanceRaw,
|
||||
Class,
|
||||
Student,
|
||||
@@ -66,11 +67,71 @@ export class AttendanceService {
|
||||
private classTeacherRepo: Repository<ClassTeacher>,
|
||||
@InjectRepository(AttendanceSession)
|
||||
private attendanceSessionRepo: Repository<AttendanceSession>,
|
||||
@InjectRepository(AttendanceDevice)
|
||||
private attendanceDeviceRepo: Repository<AttendanceDevice>,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
private sessionMutex = new SessionMutex();
|
||||
|
||||
private formatDeviceDetail(device: AttendanceDevice): string {
|
||||
const classroomName = device.classroom?.name;
|
||||
return classroomName ? `${device.deviceName} · ${classroomName}` : device.deviceName;
|
||||
}
|
||||
|
||||
private async attachAttendanceDeviceMappings<T extends AttendanceRecord>(
|
||||
records: T[],
|
||||
classroomId?: number | null,
|
||||
): Promise<T[]> {
|
||||
if (records.length === 0) return records;
|
||||
const sns = [...new Set(records.map((record) => record.punchDeviceId?.trim()).filter(Boolean) as string[])];
|
||||
const devicesBySn = new Map<string, AttendanceDevice>();
|
||||
if (sns.length > 0) {
|
||||
const devices = await this.attendanceDeviceRepo.find({
|
||||
where: { deviceSn: In(sns), status: 'active' },
|
||||
relations: ['classroom'],
|
||||
});
|
||||
for (const device of devices) devicesBySn.set(device.deviceSn, device);
|
||||
}
|
||||
|
||||
const classroomIds = [...new Set([
|
||||
...records.map((record) => record.classId).filter((id): id is number => id != null),
|
||||
...(classroomId != null ? [classroomId] : []),
|
||||
])];
|
||||
const devicesByClassroom = new Map<number, AttendanceDevice>();
|
||||
if (classroomIds.length > 0) {
|
||||
const devices = await this.attendanceDeviceRepo.find({
|
||||
where: { classroomId: In(classroomIds), status: 'active' },
|
||||
relations: ['classroom'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
for (const device of devices) {
|
||||
if (!devicesByClassroom.has(device.classroomId)) devicesByClassroom.set(device.classroomId, device);
|
||||
}
|
||||
}
|
||||
|
||||
for (const record of records) {
|
||||
const sn = record.punchDeviceId?.trim();
|
||||
const mappedBySn = sn ? devicesBySn.get(sn) : undefined;
|
||||
if (mappedBySn) {
|
||||
record.punchDeviceName = this.formatDeviceDetail(mappedBySn);
|
||||
record.punchDeviceId = mappedBySn.deviceSn;
|
||||
continue;
|
||||
}
|
||||
const source = (record.punchSource || '').trim().toUpperCase();
|
||||
const isMachine = ['ATM', 'ATTENDANCE_MACHINE', 'MACHINE', 'DEVICE'].some(
|
||||
(value) => source === value || source.includes(value),
|
||||
);
|
||||
const fallbackClassroomId = record.classId ?? classroomId ?? undefined;
|
||||
const mappedByClassroom = fallbackClassroomId ? devicesByClassroom.get(fallbackClassroomId) : undefined;
|
||||
if (isMachine && mappedByClassroom && !record.punchDeviceName) {
|
||||
record.punchDeviceName = this.formatDeviceDetail(mappedByClassroom);
|
||||
record.punchDeviceId = record.punchDeviceId || mappedByClassroom.deviceSn;
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
async getAccessibleClassIds(userId: number, canManageAll = false): Promise<number[] | undefined> {
|
||||
if (canManageAll) return undefined;
|
||||
const assignments = await this.classTeacherRepo.find({ where: { userId } });
|
||||
@@ -172,7 +233,7 @@ export class AttendanceService {
|
||||
order: { studentId: 'ASC' },
|
||||
})
|
||||
: [];
|
||||
return { schedule, session, records };
|
||||
return { schedule, session, records: await this.attachAttendanceDeviceMappings(records, schedule.classId) };
|
||||
}
|
||||
|
||||
private getLessonAttendanceWindow(
|
||||
@@ -299,7 +360,7 @@ export class AttendanceService {
|
||||
relations: ['student'],
|
||||
order: { studentId: 'ASC' },
|
||||
});
|
||||
return { schedule, session: existing, records };
|
||||
return { schedule, session: existing, records: await this.attachAttendanceDeviceMappings(records, schedule.classId) };
|
||||
}
|
||||
if (existing.status !== 'in_progress' && !(finalize && existing.status === 'settling')) {
|
||||
throw new BadRequestException('课程考勤正在结算');
|
||||
@@ -385,7 +446,7 @@ export class AttendanceService {
|
||||
existing.completedAt = new Date();
|
||||
await sessionRepo.save(existing);
|
||||
}
|
||||
return { schedule, session: existing, records: saved };
|
||||
return { schedule, session: existing, records: await this.attachAttendanceDeviceMappings(saved, schedule.classId) };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -428,7 +489,7 @@ export class AttendanceService {
|
||||
relations: ['student'],
|
||||
order: { studentId: 'ASC' },
|
||||
});
|
||||
return { schedule, session, records: existingRecords };
|
||||
return { schedule, session, records: await this.attachAttendanceDeviceMappings(existingRecords, schedule.classId) };
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
@@ -469,7 +530,7 @@ export class AttendanceService {
|
||||
session.completedAt = new Date();
|
||||
session = await sessionRepo.save(session);
|
||||
}
|
||||
return { schedule, session, records: saved };
|
||||
return { schedule, session, records: await this.attachAttendanceDeviceMappings(saved, schedule.classId) };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -516,7 +577,7 @@ export class AttendanceService {
|
||||
relations: ['student'],
|
||||
order: { studentId: 'ASC' },
|
||||
});
|
||||
return { session, records };
|
||||
return { session, records: await this.attachAttendanceDeviceMappings(records, session.classId) };
|
||||
}
|
||||
|
||||
const pendingRecords = await recordRepo.count({
|
||||
@@ -535,7 +596,7 @@ export class AttendanceService {
|
||||
relations: ['student'],
|
||||
order: { studentId: 'ASC' },
|
||||
});
|
||||
return { session: savedSession, records };
|
||||
return { session: savedSession, records: await this.attachAttendanceDeviceMappings(records, session.classId) };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
await this.ensureAiConfigTable();
|
||||
await this.ensureCourseAttendanceSchema();
|
||||
await this.ensureAttendanceDevicesSchema();
|
||||
await this.ensureStudentWalletSchema();
|
||||
await this.backfillOrganizations();
|
||||
await this.normalizeClassDates();
|
||||
@@ -22,6 +23,64 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
|
||||
await this.normalizeClassroomStatuses();
|
||||
}
|
||||
|
||||
private async ensureAttendanceDevicesSchema(): Promise<void> {
|
||||
const runner = this.dataSource.createQueryRunner();
|
||||
await runner.connect();
|
||||
try {
|
||||
const isMySQL = this.dataSource.options.type === 'mysql';
|
||||
const pk = isMySQL ? 'INTEGER PRIMARY KEY AUTO_INCREMENT' : 'INTEGER PRIMARY KEY AUTOINCREMENT';
|
||||
await runner.query(`CREATE TABLE IF NOT EXISTS attendance_devices (
|
||||
id ${pk},
|
||||
device_sn VARCHAR(100) NOT NULL,
|
||||
device_name VARCHAR(100) NOT NULL,
|
||||
classroom_id INTEGER NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
location VARCHAR(200),
|
||||
notes TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
const table = await runner.getTable('attendance_devices');
|
||||
const columnNames = new Set(table?.columns.map((column) => column.name) ?? []);
|
||||
const additions: Array<[string, string]> = [
|
||||
['device_sn', 'VARCHAR(100) NOT NULL DEFAULT \'\''],
|
||||
['device_name', 'VARCHAR(100) NOT NULL DEFAULT \'\''],
|
||||
['classroom_id', 'INTEGER NOT NULL DEFAULT 0'],
|
||||
['status', "VARCHAR(20) NOT NULL DEFAULT 'active'"],
|
||||
['location', 'VARCHAR(200)'],
|
||||
['notes', 'TEXT'],
|
||||
['created_at', 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'],
|
||||
['updated_at', 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'],
|
||||
];
|
||||
for (const [name, definition] of additions) {
|
||||
if (!columnNames.has(name)) await runner.query(`ALTER TABLE attendance_devices ADD COLUMN ${name} ${definition}`);
|
||||
}
|
||||
const refreshed = await runner.getTable('attendance_devices');
|
||||
const createIndex = async (sql: string) => {
|
||||
try {
|
||||
await runner.query(sql);
|
||||
} catch {
|
||||
// Existing MySQL indexes cannot use IF NOT EXISTS; startup must stay idempotent.
|
||||
}
|
||||
};
|
||||
const uniqueSn = refreshed?.indices.some((index) => index.columnNames.length === 1 && index.columnNames[0] === 'device_sn' && index.isUnique);
|
||||
if (!uniqueSn) {
|
||||
await createIndex(
|
||||
isMySQL
|
||||
? 'CREATE UNIQUE INDEX idx_attendance_devices_device_sn ON attendance_devices (device_sn)'
|
||||
: 'CREATE UNIQUE INDEX IF NOT EXISTS idx_attendance_devices_device_sn ON attendance_devices (device_sn)',
|
||||
);
|
||||
}
|
||||
await createIndex(
|
||||
isMySQL
|
||||
? 'CREATE INDEX idx_attendance_devices_classroom_id ON attendance_devices (classroom_id)'
|
||||
: 'CREATE INDEX IF NOT EXISTS idx_attendance_devices_classroom_id ON attendance_devices (classroom_id)',
|
||||
);
|
||||
} finally {
|
||||
await runner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureStudentWalletSchema(): Promise<void> {
|
||||
const runner = this.dataSource.createQueryRunner();
|
||||
await runner.connect();
|
||||
@@ -456,25 +515,57 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
|
||||
|
||||
private async normalizeClassDates(): Promise<void> {
|
||||
const driver = this.dataSource.options.type;
|
||||
const dateExpression = (column: string) =>
|
||||
driver === 'mysql' ? `DATE(${column})` : `substr(${column}, 1, 10)`;
|
||||
let columns: Array<'start_date' | 'end_date'> = ['start_date', 'end_date'];
|
||||
|
||||
const runner = this.dataSource.createQueryRunner();
|
||||
await runner.connect();
|
||||
try {
|
||||
const table = await runner.getTable('classes');
|
||||
if (!table) return;
|
||||
|
||||
// Fresh MySQL schemas created by TypeORM already use native DATE columns.
|
||||
// This cleanup is only for legacy schemas that stored dates as strings;
|
||||
// comparing a native DATE column with '' raises ER_TRUNCATED_WRONG_VALUE
|
||||
// in strict SQL mode.
|
||||
if (driver === 'mysql') {
|
||||
columns = columns.filter((columnName) => {
|
||||
const column = table.columns.find((item) => item.name === columnName);
|
||||
const type = String(column?.type ?? '').toLowerCase();
|
||||
return !['date', 'datetime', 'timestamp'].includes(type);
|
||||
});
|
||||
if (columns.length === 0) return;
|
||||
}
|
||||
} finally {
|
||||
await runner.release();
|
||||
}
|
||||
|
||||
const columnText = (column: string) =>
|
||||
driver === 'mysql' ? `CAST(${column} AS CHAR)` : column;
|
||||
const firstTenChars = (column: string) =>
|
||||
driver === 'mysql'
|
||||
? `NULLIF(LEFT(${columnText(column)}, 10), '')`
|
||||
: `NULLIF(substr(${column}, 1, 10), '')`;
|
||||
const normalizedDate = (column: string) => `CASE
|
||||
WHEN ${column} IS NULL THEN NULL
|
||||
ELSE ${firstTenChars(column)}
|
||||
END`;
|
||||
const lengthFunction = driver === 'mysql' ? 'CHAR_LENGTH' : 'length';
|
||||
const needsNormalization = (column: string) => `(
|
||||
${column} IS NOT NULL
|
||||
AND (${columnText(column)} = '' OR ${lengthFunction}(${columnText(column)}) > 10)
|
||||
)`;
|
||||
|
||||
const assignments = columns
|
||||
.map((column) => `${column} = ${normalizedDate(column)}`)
|
||||
.join(',\n ');
|
||||
const predicates = columns.map((column) => needsNormalization(column)).join('\n OR ');
|
||||
const result = await this.dataSource.transaction((manager) =>
|
||||
manager.query(`
|
||||
UPDATE classes
|
||||
SET
|
||||
start_date = CASE
|
||||
WHEN start_date IS NULL OR start_date = '' THEN start_date
|
||||
ELSE ${dateExpression('start_date')}
|
||||
END,
|
||||
end_date = CASE
|
||||
WHEN end_date IS NULL OR end_date = '' THEN end_date
|
||||
ELSE ${dateExpression('end_date')}
|
||||
END
|
||||
${assignments}
|
||||
WHERE
|
||||
(start_date IS NOT NULL AND ${lengthFunction}(start_date) > 10)
|
||||
OR (end_date IS NOT NULL AND ${lengthFunction}(end_date) > 10)
|
||||
${predicates}
|
||||
`),
|
||||
);
|
||||
|
||||
|
||||
52
apps/server/src/entities/attendance-device.entity.ts
Normal file
52
apps/server/src/entities/attendance-device.entity.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Classroom } from './classroom.entity';
|
||||
|
||||
export enum AttendanceDeviceStatus {
|
||||
ACTIVE = 'active',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
@Entity('attendance_devices')
|
||||
@Index(['deviceSn'], { unique: true })
|
||||
@Index(['classroomId'])
|
||||
export class AttendanceDevice {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'device_sn', type: 'varchar', length: 100 })
|
||||
deviceSn: string;
|
||||
|
||||
@Column({ name: 'device_name', type: 'varchar', length: 100 })
|
||||
deviceName: string;
|
||||
|
||||
@Column({ name: 'classroom_id', type: 'integer' })
|
||||
classroomId: number;
|
||||
|
||||
@ManyToOne(() => Classroom, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'classroom_id' })
|
||||
classroom: Classroom;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: AttendanceDeviceStatus.ACTIVE })
|
||||
status: AttendanceDeviceStatus | 'active' | 'disabled';
|
||||
|
||||
@Column({ type: 'varchar', length: 200, nullable: true })
|
||||
location: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export class ExpenseType {
|
||||
@Column({ length: 20, default: 'room' })
|
||||
category: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
@Column({ name: 'sort_order', type: 'integer', default: 0 })
|
||||
sortOrder: number;
|
||||
|
||||
@Column({ default: true })
|
||||
|
||||
@@ -22,6 +22,7 @@ export { ClassTeacher, TeacherRoleType } from './class-teacher.entity';
|
||||
export { ClassSchedule, ScheduleType } from './class-schedule.entity';
|
||||
export { AttendanceRecord } from './attendance-record.entity';
|
||||
export { AttendanceSession } from './attendance-session.entity';
|
||||
export { AttendanceDevice, AttendanceDeviceStatus } from './attendance-device.entity';
|
||||
export { DingAttendanceRaw } from './ding-attendance-raw.entity';
|
||||
export { SyncLog } from './sync-log.entity';
|
||||
export { SyncState } from './sync-state.entity';
|
||||
|
||||
@@ -515,7 +515,7 @@ export class DingTalkService {
|
||||
checkType?: string; timeResult?: string;
|
||||
locationResult?: string; locationMethod?: string;
|
||||
userAddress?: string; userLongitude?: number; userLatitude?: number;
|
||||
deviceName?: string; deviceId?: string | number;
|
||||
deviceName?: string; deviceId?: string | number; deviceSN?: string | number;
|
||||
attendanceMachineName?: string; attendanceMachineId?: string | number;
|
||||
}>;
|
||||
};
|
||||
@@ -535,7 +535,7 @@ export class DingTalkService {
|
||||
checkType: r.checkType ?? '',
|
||||
sourceType: r.sourceType ?? '',
|
||||
deviceName: r.deviceName ?? r.attendanceMachineName,
|
||||
deviceId: String(r.deviceId ?? r.attendanceMachineId ?? '') || undefined,
|
||||
deviceId: String(r.deviceId ?? r.attendanceMachineId ?? r.deviceSN ?? '') || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsIn, IsInt, IsNumber, IsOptional, IsString, MaxLength, NotEquals } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsIn, IsInt, IsNumber, IsOptional, IsString, MaxLength, NotEquals } from 'class-validator';
|
||||
|
||||
export class ChangeWalletBalanceDto {
|
||||
@IsInt()
|
||||
@@ -16,3 +17,24 @@ export class ChangeWalletBalanceDto {
|
||||
@MaxLength(300)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
|
||||
export class BatchChangeWalletBalanceDto {
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@Type(() => Number)
|
||||
studentIds: number[];
|
||||
|
||||
@IsNumber({ maxDecimalPlaces: 2 })
|
||||
@NotEquals(0)
|
||||
amount: number;
|
||||
|
||||
@IsIn(['recharge', 'adjustment'])
|
||||
type: 'recharge' | 'adjustment';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(300)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import { extractRequestInfo } from '../common/request-utils';
|
||||
import { ChangeWalletBalanceDto } from './dto/wallet.dto';
|
||||
import { BatchChangeWalletBalanceDto, ChangeWalletBalanceDto } from './dto/wallet.dto';
|
||||
import { WalletsService } from './wallets.service';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -41,4 +41,24 @@ export class WalletsController {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Post('batch-change-balance')
|
||||
@RequirePermission('wallet:edit')
|
||||
async batchChangeBalance(@Body() dto: BatchChangeWalletBalanceDto, @Request() req: any) {
|
||||
const result = await this.service.batchChangeBalance(dto, req.user?.id);
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '学生余额',
|
||||
action: dto.type === 'recharge' ? '批量余额充值' : '批量余额调账',
|
||||
targetId: undefined,
|
||||
targetType: 'student_wallet',
|
||||
detail: `学生${result.count}人,金额 ¥${dto.amount}${dto.description ? `,${dto.description}` : ''}`,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Student } from '../entities/student.entity';
|
||||
import { StudentWallet } from '../entities/student-wallet.entity';
|
||||
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||
import { In } from 'typeorm';
|
||||
import { ChangeWalletBalanceDto } from './dto/wallet.dto';
|
||||
import { BatchChangeWalletBalanceDto, ChangeWalletBalanceDto } from './dto/wallet.dto';
|
||||
|
||||
const money = (value: number | string | null | undefined) => Number(Number(value || 0).toFixed(2));
|
||||
|
||||
@@ -92,6 +92,20 @@ export class WalletsService {
|
||||
});
|
||||
}
|
||||
|
||||
async batchChangeBalance(dto: BatchChangeWalletBalanceDto, recordedBy?: number) {
|
||||
const uniqueStudentIds = Array.from(new Set(dto.studentIds));
|
||||
const results: Awaited<ReturnType<WalletsService['changeBalance']>>[] = [];
|
||||
for (const studentId of uniqueStudentIds) {
|
||||
results.push(await this.changeBalance({
|
||||
studentId,
|
||||
amount: dto.amount,
|
||||
type: dto.type,
|
||||
description: dto.description,
|
||||
}, recordedBy));
|
||||
}
|
||||
return { count: uniqueStudentIds.length, results };
|
||||
}
|
||||
|
||||
async debitBill(manager: EntityManager, bill: Bill, recordedBy?: number) {
|
||||
if (bill.status === 'cancelled') return bill;
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-gongxue_2024}
|
||||
MYSQL_DATABASE: gongxue
|
||||
MYSQL_DATABASE: dorm_billing_v2
|
||||
MYSQL_CHARSET: utf8mb4
|
||||
MYSQL_COLLATION: utf8mb4_unicode_ci
|
||||
ports:
|
||||
- "127.0.0.1:3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
|
||||
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
@@ -32,8 +33,8 @@ services:
|
||||
DB_PORT: 3306
|
||||
DB_USERNAME: root
|
||||
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD:-gongxue_2024}
|
||||
DB_DATABASE: gongxue
|
||||
DB_SYNCHRONIZE: "true"
|
||||
DB_DATABASE: dorm_billing_v2
|
||||
DB_SYNCHRONIZE: "false"
|
||||
JWT_SECRET: ${JWT_SECRET:-gongxue-jwt-prod-2026-k3y}
|
||||
JWT_EXPIRES_IN: 24h
|
||||
PORT: 3000
|
||||
|
||||
598
docker/mysql/init.sql
Normal file
598
docker/mysql/init.sql
Normal file
@@ -0,0 +1,598 @@
|
||||
-- 恭学教育 MySQL 初始化脚本
|
||||
-- 用途:在 DB_SYNCHRONIZE=false 时初始化空 MySQL 8.x 数据库,使当前项目可直接运行。
|
||||
-- 默认管理员:admin / admin123(首次登录后请立即修改密码)。
|
||||
-- 如果用 docker-entrypoint-initdb.d 自动初始化,可直接挂载本文件。
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `dorm_billing_v2` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE `dorm_billing_v2`;
|
||||
|
||||
-- ---------- 重置旧表 ----------
|
||||
DROP TABLE IF EXISTS `archive_attachments`;
|
||||
DROP TABLE IF EXISTS `result_archives`;
|
||||
DROP TABLE IF EXISTS `learning_records`;
|
||||
DROP TABLE IF EXISTS `exam_scores`;
|
||||
DROP TABLE IF EXISTS `student_enrollments`;
|
||||
DROP TABLE IF EXISTS `student_profiles`;
|
||||
DROP TABLE IF EXISTS `notifications`;
|
||||
DROP TABLE IF EXISTS `ding_attendance_raw`;
|
||||
DROP TABLE IF EXISTS `attendance_devices`;
|
||||
DROP TABLE IF EXISTS `attendance_records`;
|
||||
DROP TABLE IF EXISTS `attendance_sessions`;
|
||||
DROP TABLE IF EXISTS `class_schedule`;
|
||||
DROP TABLE IF EXISTS `class_teacher`;
|
||||
DROP TABLE IF EXISTS `class_student`;
|
||||
DROP TABLE IF EXISTS `classes`;
|
||||
DROP TABLE IF EXISTS `classroom_rentals`;
|
||||
DROP TABLE IF EXISTS `classrooms`;
|
||||
DROP TABLE IF EXISTS `deposit_installments`;
|
||||
DROP TABLE IF EXISTS `deposits`;
|
||||
DROP TABLE IF EXISTS `operation_logs`;
|
||||
DROP TABLE IF EXISTS `student_wallets`;
|
||||
DROP TABLE IF EXISTS `wallet_transactions`;
|
||||
DROP TABLE IF EXISTS `student_ding_mapping`;
|
||||
DROP TABLE IF EXISTS `students`;
|
||||
DROP TABLE IF EXISTS `user_roles`;
|
||||
DROP TABLE IF EXISTS `role_permissions`;
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
DROP TABLE IF EXISTS `roles`;
|
||||
DROP TABLE IF EXISTS `permissions`;
|
||||
DROP TABLE IF EXISTS `bills`;
|
||||
DROP TABLE IF EXISTS `bill_items`;
|
||||
DROP TABLE IF EXISTS `personal_expenses`;
|
||||
DROP TABLE IF EXISTS `room_expenses`;
|
||||
DROP TABLE IF EXISTS `occupancies`;
|
||||
DROP TABLE IF EXISTS `beds`;
|
||||
DROP TABLE IF EXISTS `lockers`;
|
||||
DROP TABLE IF EXISTS `rooms`;
|
||||
DROP TABLE IF EXISTS `organizations`;
|
||||
DROP TABLE IF EXISTS `sync_state`;
|
||||
DROP TABLE IF EXISTS `sync_logs`;
|
||||
DROP TABLE IF EXISTS `expense_types`;
|
||||
DROP TABLE IF EXISTS `ai_config`;
|
||||
DROP TABLE IF EXISTS `integration_config_detail`;
|
||||
DROP TABLE IF EXISTS `integration_config`;
|
||||
|
||||
-- ---------- 表结构 ----------
|
||||
CREATE TABLE IF NOT EXISTS `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, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
|
||||
CREATE TABLE IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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, `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 IF NOT EXISTS `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, `bill_id` int NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`)) ENGINE=InnoDB;
|
||||
CREATE TABLE IF NOT EXISTS `bill_items` (`id` int NOT NULL AUTO_INCREMENT, `bill_id` int NOT 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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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), UNIQUE INDEX `IDX_attendance_devices_device_sn` (`device_sn`), INDEX `IDX_attendance_devices_classroom_id` (`classroom_id`), PRIMARY KEY (`id`)) ENGINE=InnoDB;
|
||||
CREATE TABLE IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `sync_state` (`platform` varchar(20) NOT NULL, `last_sync_at` datetime NULL, PRIMARY KEY (`platform`)) ENGINE=InnoDB;
|
||||
CREATE TABLE IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `student_profiles` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `target_college` varchar(100) NULL, `target_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 IF NOT EXISTS `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 IF NOT EXISTS `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, `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 IF NOT EXISTS `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, `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 IF NOT EXISTS `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 IF NOT EXISTS `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, `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `wallet_transactions` (`id` int NOT NULL AUTO_INCREMENT, `student_id` int NOT NULL, `bill_id` int 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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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 IF NOT EXISTS `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;
|
||||
|
||||
-- ---------- 外键 ----------
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_178199805b901ccd220ab7740ec' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `role_permissions` ADD CONSTRAINT `FK_178199805b901ccd220ab7740ec` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE CASCADE ON UPDATE CASCADE', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_17022daf3f885f7d35423e9971e' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `role_permissions` ADD CONSTRAINT `FK_17022daf3f885f7d35423e9971e` FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_87b8888186ca9769c960e926870' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `user_roles` ADD CONSTRAINT `FK_87b8888186ca9769c960e926870` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_b23c65e50a758245a33ee35fda1' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `user_roles` ADD CONSTRAINT `FK_b23c65e50a758245a33ee35fda1` FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_7bb1ca73f8161af27e538917a55' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `room_expenses` ADD CONSTRAINT `FK_7bb1ca73f8161af27e538917a55` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_fd7413faee42749f1b2e8270394' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `beds` ADD CONSTRAINT `FK_fd7413faee42749f1b2e8270394` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_344ae29b9cacbee1e24da7c07ea' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `lockers` ADD CONSTRAINT `FK_344ae29b9cacbee1e24da7c07ea` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_bed301f5a4e2135dd14b55fec0e' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `occupancies` ADD CONSTRAINT `FK_bed301f5a4e2135dd14b55fec0e` FOREIGN KEY (`bed_id`) REFERENCES `beds`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_ea80244bd95c8c04334931e39a2' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `occupancies` ADD CONSTRAINT `FK_ea80244bd95c8c04334931e39a2` FOREIGN KEY (`locker_id`) REFERENCES `lockers`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_3c80232a56303a75ffd544b982f' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `occupancies` ADD CONSTRAINT `FK_3c80232a56303a75ffd544b982f` FOREIGN KEY (`responsible_organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_dc062720596049470b56ce06973' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `occupancies` ADD CONSTRAINT `FK_dc062720596049470b56ce06973` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_9f7acc7567e42dda281ead9a75a' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `occupancies` ADD CONSTRAINT `FK_9f7acc7567e42dda281ead9a75a` FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_5e5bcfb70705c2c349c74b70912' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `personal_expenses` ADD CONSTRAINT `FK_5e5bcfb70705c2c349c74b70912` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_b424156152a3230b034bdb51db4' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `bill_items` ADD CONSTRAINT `FK_b424156152a3230b034bdb51db4` FOREIGN KEY (`bill_id`) REFERENCES `bills`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_65a6cb602ebcbac1bf7a995e0c9' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `bills` ADD CONSTRAINT `FK_65a6cb602ebcbac1bf7a995e0c9` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_fb3eff90b11bddf7285f9b4e281' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `students` ADD CONSTRAINT `FK_fb3eff90b11bddf7285f9b4e281` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_9571384818ecf499779d3a9d141' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `students` ADD CONSTRAINT `FK_9571384818ecf499779d3a9d141` FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_fba3c52b86d51fff34593877ff5' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `deposit_installments` ADD CONSTRAINT `FK_fba3c52b86d51fff34593877ff5` FOREIGN KEY (`deposit_id`) REFERENCES `deposits`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_ec1b340d2963ed907421696fc33' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `deposits` ADD CONSTRAINT `FK_ec1b340d2963ed907421696fc33` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_6a0af46fe476070c4c660307641' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `classroom_rentals` ADD CONSTRAINT `FK_6a0af46fe476070c4c660307641` FOREIGN KEY (`classroom_id`) REFERENCES `classrooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_40a46e95470ff454ffdcbb1e902' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `classroom_rentals` ADD CONSTRAINT `FK_40a46e95470ff454ffdcbb1e902` FOREIGN KEY (`lessor_organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_f60533a91f16a331c9388308418' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `classroom_rentals` ADD CONSTRAINT `FK_f60533a91f16a331c9388308418` FOREIGN KEY (`lessee_organization_id`) REFERENCES `organizations`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_60674dc4f23b794de0a3560e200' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `class_student` ADD CONSTRAINT `FK_60674dc4f23b794de0a3560e200` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_451334295b9cf221d55aca7cac1' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `class_student` ADD CONSTRAINT `FK_451334295b9cf221d55aca7cac1` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_9b647c8d8894a661dc1dac40361' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `class_teacher` ADD CONSTRAINT `FK_9b647c8d8894a661dc1dac40361` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_a9f0e77d193b015b6d5ca6637f5' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `class_teacher` ADD CONSTRAINT `FK_a9f0e77d193b015b6d5ca6637f5` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_attendance_devices_classroom' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `attendance_devices` ADD CONSTRAINT `FK_attendance_devices_classroom` FOREIGN KEY (`classroom_id`) REFERENCES `classrooms`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_0aa74ee427967d4c82f298511bc' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `class_schedule` ADD CONSTRAINT `FK_0aa74ee427967d4c82f298511bc` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_c1d5d50c871fdf563be13d6e93a' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `class_schedule` ADD CONSTRAINT `FK_c1d5d50c871fdf563be13d6e93a` FOREIGN KEY (`classroom_id`) REFERENCES `classrooms`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_f1e761cac59f2d0a3d7949468c5' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `class_schedule` ADD CONSTRAINT `FK_f1e761cac59f2d0a3d7949468c5` FOREIGN KEY (`teacher_id`) REFERENCES `users`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_107206b64415a644d571d597ca3' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `attendance_sessions` ADD CONSTRAINT `FK_107206b64415a644d571d597ca3` FOREIGN KEY (`schedule_id`) REFERENCES `class_schedule`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_6fc552e07b31ac92445cd3e21fa' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `attendance_sessions` ADD CONSTRAINT `FK_6fc552e07b31ac92445cd3e21fa` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_dbace05c012526710663f8d8911' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `attendance_records` ADD CONSTRAINT `FK_dbace05c012526710663f8d8911` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_cc48ccbd03396874857ab06ac3b' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `attendance_records` ADD CONSTRAINT `FK_cc48ccbd03396874857ab06ac3b` FOREIGN KEY (`class_id`) REFERENCES `classes`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_0dfe6d219c4a7a162ca0f84243c' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `attendance_records` ADD CONSTRAINT `FK_0dfe6d219c4a7a162ca0f84243c` FOREIGN KEY (`schedule_id`) REFERENCES `class_schedule`(`id`) ON DELETE SET NULL ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_32a57ba853da6939ceaccf8cfc8' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, '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', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_2b06491a70540db69e78e57ecf2' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, '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', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_5332a4daa46fd3f4e6625dd275d' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `notifications` ADD CONSTRAINT `FK_5332a4daa46fd3f4e6625dd275d` FOREIGN KEY (`recipient_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_4cedc08d3dc1f2c2da8a12f7a88' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `student_profiles` ADD CONSTRAINT `FK_4cedc08d3dc1f2c2da8a12f7a88` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_08caafd8a026a19ecf54db0e958' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `student_enrollments` ADD CONSTRAINT `FK_08caafd8a026a19ecf54db0e958` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_925e4f79518c947512cb600b452' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `exam_scores` ADD CONSTRAINT `FK_925e4f79518c947512cb600b452` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_b0d812b639f32939ea57b81a89f' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `exam_scores` ADD CONSTRAINT `FK_b0d812b639f32939ea57b81a89f` FOREIGN KEY (`enrollment_id`) REFERENCES `student_enrollments`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_61f4664f02d86245e1231493951' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `learning_records` ADD CONSTRAINT `FK_61f4664f02d86245e1231493951` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_377bba8eb6a027eecd9737d4ed6' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `result_archives` ADD CONSTRAINT `FK_377bba8eb6a027eecd9737d4ed6` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_0bc7365c2629d0c6dae8052fc25' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `archive_attachments` ADD CONSTRAINT `FK_0bc7365c2629d0c6dae8052fc25` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE NO ACTION ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_f9ba15ff04de8ffbd8679ae9dbb' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `student_ding_mapping` ADD CONSTRAINT `FK_f9ba15ff04de8ffbd8679ae9dbb` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_07a434ad1a960d506386754d592' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, 'ALTER TABLE `student_wallets` ADD CONSTRAINT `FK_07a434ad1a960d506386754d592` FOREIGN KEY (`student_id`) REFERENCES `students`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @fk_exists := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'FK_a58106c4b876c86a5e085e4f42b' AND CONSTRAINT_TYPE = 'FOREIGN KEY');
|
||||
SET @sql := IF(@fk_exists = 0, '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', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ---------- 初始化数据 ----------
|
||||
|
||||
INSERT INTO `permissions` (`id`, `code`, `name`, `group`) VALUES
|
||||
('1', 'dashboard:view', '查看数据面板', 'dashboard'),
|
||||
('2', 'profile:view', '查看个人资料', 'profile'),
|
||||
('3', 'notification:view', '查看通知', 'notification'),
|
||||
('4', 'student:view', '查看学生管理', 'student'),
|
||||
('5', 'student:basic-view', '查看学生基础信息', 'student-scope'),
|
||||
('6', 'teacher-workspace:view', '查看教师工作台', 'teacher-workspace'),
|
||||
('7', 'teacher:view', '查看教师', 'teacher'),
|
||||
('8', 'teacher:edit', '编辑教师', 'teacher'),
|
||||
('9', 'student:create', '新增学生', 'student'),
|
||||
('10', 'student:edit', '编辑学生', 'student'),
|
||||
('11', 'student:delete', '删除学生', 'student'),
|
||||
('12', 'student:import', '导入学生', 'student'),
|
||||
('13', 'student:export', '导出学生', 'student'),
|
||||
('14', 'room:view', '查看宿舍', 'room'),
|
||||
('15', 'room:create', '新增宿舍', 'room'),
|
||||
('16', 'room:edit', '编辑宿舍', 'room'),
|
||||
('17', 'room:delete', '删除宿舍', 'room'),
|
||||
('18', 'occupancy:view', '查看入住', 'occupancy'),
|
||||
('19', 'occupancy:checkin', '办理入住', 'occupancy'),
|
||||
('20', 'occupancy:checkout', '办理退宿', 'occupancy'),
|
||||
('21', 'occupancy:transfer', '调换宿舍', 'occupancy'),
|
||||
('22', 'occupancy:delete', '删除入住记录', 'occupancy'),
|
||||
('23', 'expense:view', '查看费用', 'expense'),
|
||||
('24', 'expense:create', '录入费用', 'expense'),
|
||||
('25', 'expense:edit', '编辑费用', 'expense'),
|
||||
('26', 'expense:delete', '删除费用', 'expense'),
|
||||
('27', 'bill:view', '查看账单', 'bill'),
|
||||
('28', 'bill:generate', '生成账单', 'bill'),
|
||||
('29', 'bill:confirm', '确认账单', 'bill'),
|
||||
('30', 'bill:delete', '删除账单', 'bill'),
|
||||
('31', 'bill:export-excel', '导出 Excel', 'bill'),
|
||||
('32', 'bill:export-pdf', '导出 PDF', 'bill'),
|
||||
('33', 'deposit:view', '查看押金', 'deposit'),
|
||||
('34', 'deposit:create', '新增押金', 'deposit'),
|
||||
('35', 'deposit:edit', '编辑押金', 'deposit'),
|
||||
('36', 'deposit:delete', '删除押金', 'deposit'),
|
||||
('37', 'deposit:refund', '直接退还押金', 'deposit'),
|
||||
('38', 'wallet:view', '查看学生余额', 'wallet'),
|
||||
('39', 'wallet:edit', '充值和调账', 'wallet'),
|
||||
('40', 'classroom:view', '查看教室', 'classroom'),
|
||||
('41', 'classroom:create', '新增教室', 'classroom'),
|
||||
('42', 'classroom:edit', '编辑教室', 'classroom'),
|
||||
('43', 'classroom:delete', '删除教室', 'classroom'),
|
||||
('44', 'organization:view', '查看机构', 'organization'),
|
||||
('45', 'organization:create', '新增机构', 'organization'),
|
||||
('46', 'organization:edit', '编辑机构', 'organization'),
|
||||
('47', 'organization:delete', '归档机构', 'organization'),
|
||||
('48', 'rental:view', '查看租赁订单', 'rental'),
|
||||
('49', 'rental:create', '新增租赁订单', 'rental'),
|
||||
('50', 'rental:edit', '编辑租赁订单', 'rental'),
|
||||
('51', 'rental:delete', '删除租赁订单', 'rental'),
|
||||
('52', 'log:view', '查看操作日志', 'log'),
|
||||
('53', 'log:create', '写入操作日志', 'log'),
|
||||
('54', 'user:view', '查看用户', 'user'),
|
||||
('55', 'user:create', '创建用户', 'user'),
|
||||
('56', 'user:edit', '编辑用户', 'user'),
|
||||
('57', 'user:delete', '删除用户', 'user'),
|
||||
('58', 'user:reset-password', '重置密码', 'user'),
|
||||
('59', 'role:view', '查看角色', 'role'),
|
||||
('60', 'role:create', '创建角色', 'role'),
|
||||
('61', 'role:edit', '编辑角色', 'role'),
|
||||
('62', 'role:delete', '删除角色', 'role'),
|
||||
('63', 'class:view', '查看班级', 'class'),
|
||||
('64', 'class:create', '创建班级', 'class'),
|
||||
('65', 'class:edit', '编辑班级', 'class'),
|
||||
('66', 'class:delete', '删除班级', 'class'),
|
||||
('67', 'schedule:view', '查看排课', 'schedule'),
|
||||
('68', 'schedule:create', '创建排课', 'schedule'),
|
||||
('69', 'schedule:edit', '编辑排课', 'schedule'),
|
||||
('70', 'schedule:delete', '删除排课', 'schedule'),
|
||||
('71', 'attendance:view', '查看考勤', 'attendance'),
|
||||
('72', 'attendance:create', '新增考勤', 'attendance'),
|
||||
('73', 'attendance:edit', '编辑全部考勤', 'attendance'),
|
||||
('74', 'attendance:self-edit', '编辑任教班级考勤', 'attendance-scope'),
|
||||
('75', 'attendance:export', '导出考勤', 'attendance'),
|
||||
('76', 'attendance:generate', '按课表生成考勤', 'attendance'),
|
||||
('77', 'learning:create', '创建学习任务', 'learning'),
|
||||
('78', 'learning:edit', '编辑学习任务', 'learning'),
|
||||
('79', 'learning:delete', '删除学习任务', 'learning'),
|
||||
('80', 'exam:create', '创建考试', 'exam'),
|
||||
('81', 'exam:edit', '编辑考试', 'exam'),
|
||||
('82', 'exam:delete', '删除考试', 'exam'),
|
||||
('83', 'sync:trigger', '触发数据同步', 'sync'),
|
||||
('84', 'sync:read', '查看同步状态', 'sync'),
|
||||
('85', 'integration:trigger', '触发集成', 'integration'),
|
||||
('86', 'integration:read', '查看集成状态', 'integration'),
|
||||
('87', 'department:view', '查看部门', 'department'),
|
||||
('88', 'department:edit', '编辑部门', 'department'),
|
||||
('89', 'department:delete', '删除部门', 'department'),
|
||||
('90', 'ai:config:read', '查看 AI 配置', 'ai'),
|
||||
('91', 'ai:config:write', '修改 AI 配置', 'ai'),
|
||||
('92', 'ai:config:test', '测试 AI 连接', 'ai')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `group` = VALUES(`group`);
|
||||
|
||||
INSERT INTO `roles` (`id`, `name`, `code`, `description`, `is_system`, `status`) VALUES
|
||||
('1', '超级管理员', 'super_admin', '系统初始化、应急维护和全局权限处理', '1', '1'),
|
||||
('2', '任课老师', 'teacher', '查看自己的排课、今日课程和任教班级考勤', '1', '1'),
|
||||
('3', '教务管理员', 'academic', '管理学生、班级、教师、全局排课和历史考勤', '1', '1'),
|
||||
('4', '住宿运营管理员', 'accommodation_operations', '管理宿舍、入住、住宿费用、账单、押金和退宿结算', '1', '1'),
|
||||
('5', '教室运营管理员', 'classroom_operations', '管理教室、教室排期、外部机构和租赁订单', '1', '1'),
|
||||
('6', '系统管理员', 'system_admin', '管理账号、角色、日志、同步和系统配置', '1', '1')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `description` = VALUES(`description`), `is_system` = VALUES(`is_system`), `status` = VALUES(`status`);
|
||||
|
||||
INSERT IGNORE INTO `role_permissions` (`role_id`, `permission_id`) VALUES
|
||||
('1', '1'),
|
||||
('1', '2'),
|
||||
('1', '3'),
|
||||
('1', '4'),
|
||||
('1', '5'),
|
||||
('1', '6'),
|
||||
('1', '7'),
|
||||
('1', '8'),
|
||||
('1', '9'),
|
||||
('1', '10'),
|
||||
('1', '11'),
|
||||
('1', '12'),
|
||||
('1', '13'),
|
||||
('1', '14'),
|
||||
('1', '15'),
|
||||
('1', '16'),
|
||||
('1', '17'),
|
||||
('1', '18'),
|
||||
('1', '19'),
|
||||
('1', '20'),
|
||||
('1', '21'),
|
||||
('1', '22'),
|
||||
('1', '23'),
|
||||
('1', '24'),
|
||||
('1', '25'),
|
||||
('1', '26'),
|
||||
('1', '27'),
|
||||
('1', '28'),
|
||||
('1', '29'),
|
||||
('1', '30'),
|
||||
('1', '31'),
|
||||
('1', '32'),
|
||||
('1', '33'),
|
||||
('1', '34'),
|
||||
('1', '35'),
|
||||
('1', '36'),
|
||||
('1', '37'),
|
||||
('1', '38'),
|
||||
('1', '39'),
|
||||
('1', '40'),
|
||||
('1', '41'),
|
||||
('1', '42'),
|
||||
('1', '43'),
|
||||
('1', '44'),
|
||||
('1', '45'),
|
||||
('1', '46'),
|
||||
('1', '47'),
|
||||
('1', '48'),
|
||||
('1', '49'),
|
||||
('1', '50'),
|
||||
('1', '51'),
|
||||
('1', '52'),
|
||||
('1', '53'),
|
||||
('1', '54'),
|
||||
('1', '55'),
|
||||
('1', '56'),
|
||||
('1', '57'),
|
||||
('1', '58'),
|
||||
('1', '59'),
|
||||
('1', '60'),
|
||||
('1', '61'),
|
||||
('1', '62'),
|
||||
('1', '63'),
|
||||
('1', '64'),
|
||||
('1', '65'),
|
||||
('1', '66'),
|
||||
('1', '67'),
|
||||
('1', '68'),
|
||||
('1', '69'),
|
||||
('1', '70'),
|
||||
('1', '71'),
|
||||
('1', '72'),
|
||||
('1', '73'),
|
||||
('1', '74'),
|
||||
('1', '75'),
|
||||
('1', '76'),
|
||||
('1', '77'),
|
||||
('1', '78'),
|
||||
('1', '79'),
|
||||
('1', '80'),
|
||||
('1', '81'),
|
||||
('1', '82'),
|
||||
('1', '83'),
|
||||
('1', '84'),
|
||||
('1', '85'),
|
||||
('1', '86'),
|
||||
('1', '87'),
|
||||
('1', '88'),
|
||||
('1', '89'),
|
||||
('1', '90'),
|
||||
('1', '91'),
|
||||
('1', '92'),
|
||||
('2', '6'),
|
||||
('2', '67'),
|
||||
('2', '71'),
|
||||
('2', '72'),
|
||||
('2', '74'),
|
||||
('2', '2'),
|
||||
('2', '3'),
|
||||
('3', '6'),
|
||||
('3', '7'),
|
||||
('3', '8'),
|
||||
('3', '84'),
|
||||
('3', '83'),
|
||||
('3', '1'),
|
||||
('3', '2'),
|
||||
('3', '3'),
|
||||
('3', '4'),
|
||||
('3', '9'),
|
||||
('3', '10'),
|
||||
('3', '11'),
|
||||
('3', '12'),
|
||||
('3', '13'),
|
||||
('3', '40'),
|
||||
('3', '41'),
|
||||
('3', '42'),
|
||||
('3', '43'),
|
||||
('3', '63'),
|
||||
('3', '64'),
|
||||
('3', '65'),
|
||||
('3', '66'),
|
||||
('3', '67'),
|
||||
('3', '68'),
|
||||
('3', '69'),
|
||||
('3', '70'),
|
||||
('3', '71'),
|
||||
('3', '72'),
|
||||
('3', '73'),
|
||||
('3', '75'),
|
||||
('3', '76'),
|
||||
('3', '77'),
|
||||
('3', '78'),
|
||||
('3', '79'),
|
||||
('3', '80'),
|
||||
('3', '81'),
|
||||
('3', '82'),
|
||||
('4', '5'),
|
||||
('4', '1'),
|
||||
('4', '2'),
|
||||
('4', '3'),
|
||||
('4', '14'),
|
||||
('4', '15'),
|
||||
('4', '16'),
|
||||
('4', '17'),
|
||||
('4', '18'),
|
||||
('4', '19'),
|
||||
('4', '20'),
|
||||
('4', '21'),
|
||||
('4', '22'),
|
||||
('4', '23'),
|
||||
('4', '24'),
|
||||
('4', '25'),
|
||||
('4', '26'),
|
||||
('4', '27'),
|
||||
('4', '28'),
|
||||
('4', '29'),
|
||||
('4', '30'),
|
||||
('4', '31'),
|
||||
('4', '32'),
|
||||
('4', '33'),
|
||||
('4', '34'),
|
||||
('4', '35'),
|
||||
('4', '36'),
|
||||
('4', '37'),
|
||||
('4', '38'),
|
||||
('4', '39'),
|
||||
('5', '2'),
|
||||
('5', '3'),
|
||||
('5', '40'),
|
||||
('5', '41'),
|
||||
('5', '42'),
|
||||
('5', '43'),
|
||||
('5', '44'),
|
||||
('5', '45'),
|
||||
('5', '46'),
|
||||
('5', '47'),
|
||||
('5', '48'),
|
||||
('5', '49'),
|
||||
('5', '50'),
|
||||
('5', '51'),
|
||||
('6', '2'),
|
||||
('6', '3'),
|
||||
('6', '52'),
|
||||
('6', '53'),
|
||||
('6', '54'),
|
||||
('6', '55'),
|
||||
('6', '56'),
|
||||
('6', '57'),
|
||||
('6', '58'),
|
||||
('6', '59'),
|
||||
('6', '60'),
|
||||
('6', '61'),
|
||||
('6', '62'),
|
||||
('6', '83'),
|
||||
('6', '84'),
|
||||
('6', '85'),
|
||||
('6', '86'),
|
||||
('6', '87'),
|
||||
('6', '88'),
|
||||
('6', '89'),
|
||||
('6', '90'),
|
||||
('6', '91'),
|
||||
('6', '92');
|
||||
|
||||
INSERT INTO `users` (`id`, `username`, `password_hash`, `name`, `is_active`, `is_archived`) VALUES
|
||||
('1', 'admin', '$2b$10$vu8/qt4UCb6QnbmgQdbUtexPZWieuok030ZF0bI6xfpcf49MEML62', '管理员', '1', '0')
|
||||
ON DUPLICATE KEY UPDATE `username` = `username`;
|
||||
INSERT IGNORE INTO `user_roles` (`user_id`, `role_id`) VALUES (1, 1);
|
||||
|
||||
INSERT INTO `organizations` (`id`, `public_id`, `code`, `name`, `is_host`, `color`, `notes`, `status`) VALUES
|
||||
('1', '01900000-0000-7000-8000-000000000001', 'HOST', '本机构', '1', '#1677ff', '系统默认运营主体', 'active')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `is_host` = VALUES(`is_host`), `status` = VALUES(`status`);
|
||||
|
||||
INSERT INTO `expense_types` (`code`, `name`, `category`, `sort_order`, `enabled`) VALUES
|
||||
('water', '水费', 'room', '1', '1'),
|
||||
('electricity', '电费', 'room', '2', '1'),
|
||||
('cleaning', '保洁费', 'room', '3', '1'),
|
||||
('damage', '损坏赔偿', 'both', '4', '1'),
|
||||
('penalty', '罚款', 'personal', '5', '1'),
|
||||
('key', '钥匙费', 'personal', '6', '1'),
|
||||
('remote', '空调遥控器', 'personal', '7', '1'),
|
||||
('deposit_deduction', '押金扣除', 'personal', '8', '1'),
|
||||
('other', '其他', 'both', '99', '1')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `category` = VALUES(`category`), `sort_order` = VALUES(`sort_order`), `enabled` = VALUES(`enabled`);
|
||||
|
||||
INSERT INTO `ai_config` (`singleton_key`, `provider`, `enabled`, `timeout_ms`, `verified`) VALUES
|
||||
('GLOBAL', 'OPENAI', '0', '30000', '0')
|
||||
ON DUPLICATE KEY UPDATE `singleton_key` = `singleton_key`;
|
||||
|
||||
INSERT INTO `integration_config` (`id`, `type`, `is_sync`) VALUES
|
||||
('1', 'THIRD', '0')
|
||||
ON DUPLICATE KEY UPDATE `type` = VALUES(`type`);
|
||||
|
||||
-- 让后续 AUTO_INCREMENT 从安全位置继续。
|
||||
ALTER TABLE `permissions` AUTO_INCREMENT = 1000;
|
||||
ALTER TABLE `roles` AUTO_INCREMENT = 100;
|
||||
ALTER TABLE `users` AUTO_INCREMENT = 100;
|
||||
ALTER TABLE `organizations` AUTO_INCREMENT = 100;
|
||||
ALTER TABLE `attendance_devices` AUTO_INCREMENT = 100;
|
||||
@@ -1,10 +1,10 @@
|
||||
// PM2 进程配置 — 恭学教育
|
||||
// 用法: pm2 start ecosystem.config.cjs
|
||||
// 前提: 项目部署在 /opt/gongxue(deploy.sh 默认路径)
|
||||
// .env 文件在 /opt/gongxue/.env
|
||||
// docker compose up -d 已启动 MySQL
|
||||
// 用法: pm2 startOrReload ecosystem.config.cjs --update-env
|
||||
// 前提: 在项目根目录执行;也可通过 DEPLOY_DIR 指定项目目录
|
||||
// 生产环境配置写在项目根目录 .env,由 Nest ConfigModule 读取
|
||||
// 前端构建产物 apps/admin/dist 交给 Nginx 托管,不再用 PM2 启动前端
|
||||
|
||||
const DEPLOY_DIR = process.env.DEPLOY_DIR || '/opt/gongxue';
|
||||
const DEPLOY_DIR = process.env.DEPLOY_DIR || __dirname;
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
@@ -16,19 +16,6 @@ module.exports = {
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: '3000',
|
||||
DB_TYPE: 'mysql',
|
||||
DB_HOST: '127.0.0.1',
|
||||
DB_PORT: '3306',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'gongxue_2024',
|
||||
DB_DATABASE: 'gongxue',
|
||||
DB_SYNCHRONIZE: 'true',
|
||||
JWT_SECRET: 'gongxue-jwt-prod-2026-k3y',
|
||||
JWT_EXPIRES_IN: '24h',
|
||||
UPLOAD_DIR: './uploads',
|
||||
DINGTALK_APP_KEY: 'dingvpcg2i6p25ftxw5c',
|
||||
DINGTALK_APP_SECRET: 'ovAEql1r5Lu9FvBve6bHwBZxXAZuEQwSNpG3DkRJk4DMhQwJ_raKDQc6emdlJisU',
|
||||
},
|
||||
// 内存限制
|
||||
max_memory_restart: '512M',
|
||||
@@ -41,13 +28,5 @@ module.exports = {
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'gongxue-frontend',
|
||||
cwd: DEPLOY_DIR,
|
||||
script: 'serve-proxy.js',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user