From 811e7ce826b8b7cf0686d879b7f090e4b4d75764 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Tue, 14 Jul 2026 11:20:27 +0800 Subject: [PATCH 1/3] feat: show DingTalk punch device details --- .../attendance-workspace.integration.test.ts | 32 +++++++ .../pages/Attendance/attendance-workspace.ts | 53 ++++++++++++ .../admin/src/pages/Attendance/attendance.css | 22 +++++ apps/admin/src/pages/Attendance/index.tsx | 23 ++++- .../attendance-import.service.spec.ts | 84 ++++++++++++++++++- .../attendance/attendance-import.service.ts | 47 +++++++++-- .../attendance.lesson-session.spec.ts | 13 ++- .../src/attendance/attendance.service.ts | 70 ++++++++++++++++ .../dingtalk-attendance.service.spec.ts | 13 ++- .../src/entities/attendance-record.entity.ts | 12 +++ .../entities/ding-attendance-raw.entity.ts | 9 ++ .../src/integration/dingtalk.service.ts | 12 ++- 12 files changed, 377 insertions(+), 13 deletions(-) diff --git a/apps/admin/src/pages/Attendance/attendance-workspace.integration.test.ts b/apps/admin/src/pages/Attendance/attendance-workspace.integration.test.ts index eb42879..a8dd74e 100644 --- a/apps/admin/src/pages/Attendance/attendance-workspace.integration.test.ts +++ b/apps/admin/src/pages/Attendance/attendance-workspace.integration.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { canPullAttendance, getAttendanceExperience, + getPunchDisplayInfo, getSchedulePhase, summarizeAttendance, summarizeLessonCheckins, @@ -65,3 +66,34 @@ describe('lesson check-in summary', () => { ).toEqual({ total: 4, checkedIn: 2, notCheckedIn: 2 }); }); }); + + +describe('lesson punch device display', () => { + it('labels attendance machine punches with the machine name and id', () => { + expect( + getPunchDisplayInfo({ + status: 'present', + source: 'dingtalk', + punchSource: 'ATM', + punchDeviceName: '东门考勤机', + punchDeviceId: 'ATM-01', + punchTime: '2026-07-11T00:55:00.000Z', + }), + ).toEqual({ + label: '考勤机打卡', + machine: true, + detail: '东门考勤机(ATM-01)', + time: '2026-07-11T00:55:00.000Z', + }); + }); + + it('distinguishes mobile punches and manual teacher markings', () => { + expect( + getPunchDisplayInfo({ status: 'present', source: 'dingtalk', punchSource: 'USER' }), + ).toEqual({ label: '手机打卡', machine: false, detail: undefined, time: undefined }); + expect(getPunchDisplayInfo({ status: 'present', source: 'manual' })).toEqual({ + label: '老师手动标记', + machine: false, + }); + }); +}); diff --git a/apps/admin/src/pages/Attendance/attendance-workspace.ts b/apps/admin/src/pages/Attendance/attendance-workspace.ts index 42a6059..a78250d 100644 --- a/apps/admin/src/pages/Attendance/attendance-workspace.ts +++ b/apps/admin/src/pages/Attendance/attendance-workspace.ts @@ -82,3 +82,56 @@ export function summarizeLessonCheckins( notCheckedIn: records.length - checkedIn, }; } + + +export interface PunchDisplayRecord { + status: string; + source?: string; + punchTime?: string | null; + punchSource?: string | null; + punchDeviceName?: string | null; + punchDeviceId?: string | null; +} + +export interface PunchDisplayInfo { + label: string; + machine: boolean; + detail?: string; + time?: string; +} + +export function getPunchDisplayInfo(record: PunchDisplayRecord): PunchDisplayInfo | null { + if (record.status !== 'present' && record.status !== 'late') return null; + if (record.source === 'manual') return { label: '老师手动标记', machine: false }; + + const source = (record.punchSource || '').trim().toUpperCase(); + const machine = ['ATM', 'ATTENDANCE_MACHINE', 'MACHINE', 'DEVICE'].some( + (value) => source === value || source.includes(value), + ); + const label = machine + ? '考勤机打卡' + : source === 'USER' + ? '手机打卡' + : source.includes('BEACON') || source.includes('BLE') + ? '蓝牙打卡' + : source.includes('WIFI') + ? 'Wi-Fi 打卡' + : source.includes('APPROVE') + ? '审批补卡' + : source + ? `其他打卡(${record.punchSource})` + : '打卡来源未知'; + const device = record.punchDeviceName?.trim(); + const deviceId = record.punchDeviceId?.trim(); + const detail = device + ? deviceId && deviceId !== device + ? `${device}(${deviceId})` + : device + : deviceId || undefined; + return { + label, + machine, + detail, + time: record.punchTime || undefined, + }; +} diff --git a/apps/admin/src/pages/Attendance/attendance.css b/apps/admin/src/pages/Attendance/attendance.css index 77714dd..c9745e4 100644 --- a/apps/admin/src/pages/Attendance/attendance.css +++ b/apps/admin/src/pages/Attendance/attendance.css @@ -502,3 +502,25 @@ .attendance-marking-actions .ant-btn { min-width: 54px; } + + +.punch-device-cell { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} + +.punch-device-cell .ant-tag { + margin-inline-end: 0; +} + +.punch-device-cell strong { + color: #1f2937; + font-size: 13px; +} + +.punch-device-cell span { + color: #8c8c8c; + font-size: 12px; +} diff --git a/apps/admin/src/pages/Attendance/index.tsx b/apps/admin/src/pages/Attendance/index.tsx index 7af49c3..4c3a063 100644 --- a/apps/admin/src/pages/Attendance/index.tsx +++ b/apps/admin/src/pages/Attendance/index.tsx @@ -41,6 +41,7 @@ import { message } from '../../ui/app-message'; import { canPullAttendance, getAttendanceExperience, + getPunchDisplayInfo, getSchedulePhase, summarizeLessonCheckins, type AttendanceSummary, @@ -96,6 +97,10 @@ interface AttendanceRecordItem { class: { id: number; name: string } | null; scheduleId?: number | null; attendanceSessionId?: number | null; + punchTime?: string | null; + punchSource?: string | null; + punchDeviceName?: string | null; + punchDeviceId?: string | null; } interface AssignedClass { @@ -288,6 +293,7 @@ const TeacherAttendanceWorkspace: React.FC = () => { `/attendance-lessons/schedules/${schedule.id}/pull`, { date: today }, ); + setSelectedSchedule(data.schedule); setLessonSession(data.session); setLessonRecords(data.records); message.success('钉钉打卡已更新;课程截止后系统将自动结算'); @@ -386,7 +392,7 @@ const TeacherAttendanceWorkspace: React.FC = () => { )} - setDrawerOpen(false)} width={820} title={null} className="attendance-drawer"> + setDrawerOpen(false)} width={960} title={null} className="attendance-drawer">
LESSON ATTENDANCE

{selectedSchedule?.subject || '课程考勤'}

@@ -438,6 +444,21 @@ const TeacherAttendanceWorkspace: React.FC = () => { }, }, { title: '当前状态', dataIndex: 'status', width: 105, render: (value: string) => }, + { + title: '打卡设备', + width: 220, + render: (_: unknown, record: AttendanceRecordItem) => { + const info = getPunchDisplayInfo(record); + if (!info) return ; + return ( +
+ {info.label} + {info.detail && {info.detail}} + {info.time && {dayjs(info.time).format('HH:mm:ss')}} +
+ ); + }, + }, { title: '备注', dataIndex: 'remark', render: (value: string | null) => value || }, ]} /> diff --git a/apps/server/src/attendance/attendance-import.service.spec.ts b/apps/server/src/attendance/attendance-import.service.spec.ts index 1ac9065..f238993 100644 --- a/apps/server/src/attendance/attendance-import.service.spec.ts +++ b/apps/server/src/attendance/attendance-import.service.spec.ts @@ -93,6 +93,31 @@ describe('AttendanceImportService', () => { expect(entity.userName).toBe('张三'); }); + it('stores DingTalk punch source and attendance machine metadata', async () => { + const entity = await (service as any).mapToEntity({ + userId: 'ding-1', + userName: '张三', + workDate: '2026-07-01', + timeResult: 'Normal', + locationResult: '', + planCheckTime: '', + actualCheckTime: '2026-07-01T08:00:00.000Z', + checkId: 'check-1', + checkType: 'OnDuty', + sourceType: 'ATM', + deviceName: '东门考勤机', + deviceId: 'ATM-01', + }); + + expect(entity).toEqual( + expect.objectContaining({ + punchSource: 'ATM', + punchDeviceName: '东门考勤机', + punchDeviceId: 'ATM-01', + }), + ); + }); + it('fills the student name from the DingTalk mapping when saving an imported record', async () => { dingTalkService.fetchAttendanceResults.mockResolvedValue([ { @@ -138,9 +163,19 @@ describe('AttendanceImportService', () => { actualCheckTime: '2026-07-01T08:00:00.000Z', checkId: 'check-1', checkType: 'OnDuty', + sourceType: 'ATM', + deviceName: '东门考勤机', + deviceId: 'ATM-01', }, ]); - dingRawRepo.find.mockResolvedValue([{ dingId: 'check-1' }]); + dingRawRepo.find.mockResolvedValue([{ + dingId: 'check-1', + punchSource: null, + punchDeviceName: null, + punchDeviceId: null, + rawData: '', + }]); + dingRawRepo.save.mockImplementation(async (entities) => entities); attendanceService.autoMatchDingRecords.mockResolvedValue({ matched: 1, total: 1 }); const result = await service.importFromDingTalk({ @@ -150,10 +185,57 @@ describe('AttendanceImportService', () => { autoMatch: true, }); + expect(dingRawRepo.save).toHaveBeenCalledWith( + [expect.objectContaining({ + dingId: 'check-1', + punchSource: 'ATM', + punchDeviceName: '东门考勤机', + punchDeviceId: 'ATM-01', + })], + { chunk: 50 }, + ); expect(attendanceService.autoMatchDingRecords).toHaveBeenCalled(); expect(result.matched).toBe(1); }); + it('preserves existing device metadata when a duplicate response omits it', async () => { + const existing = { + dingId: 'check-keep-device', + punchSource: 'ATM', + punchDeviceName: '东门考勤机', + punchDeviceId: 'ATM-01', + rawData: '{}', + }; + dingTalkService.fetchAttendanceResults.mockResolvedValue([{ + userId: 'ding-1', + userName: '张三', + workDate: '2026-07-01', + timeResult: 'Normal', + locationResult: '', + planCheckTime: '', + actualCheckTime: '2026-07-01T08:00:00.000Z', + checkId: 'check-keep-device', + checkType: 'OnDuty', + sourceType: '', + }]); + dingRawRepo.find.mockResolvedValue([existing]); + attendanceService.autoMatchDingRecords.mockResolvedValue({ matched: 0, total: 1 }); + + await service.importFromDingTalk({ + startDate: '2026-07-01', + endDate: '2026-07-01', + userIds: ['ding-1'], + autoMatch: true, + }); + + expect(existing).toEqual(expect.objectContaining({ + punchSource: 'ATM', + punchDeviceName: '东门考勤机', + punchDeviceId: 'ATM-01', + })); + expect(dingRawRepo.save).not.toHaveBeenCalled(); + }); + it('scopes SSE progress events to the importing user', async () => { dingTalkService.fetchAttendanceResults.mockResolvedValue([ { diff --git a/apps/server/src/attendance/attendance-import.service.ts b/apps/server/src/attendance/attendance-import.service.ts index ecd4957..f133074 100644 --- a/apps/server/src/attendance/attendance-import.service.ts +++ b/apps/server/src/attendance/attendance-import.service.ts @@ -104,8 +104,10 @@ export class AttendanceImportService { // Stage 2: Parse & deduplicate this.emit('parsing', 0, total, `Parsing ${total} records...`); - const existingDingIds = await this.getExistingDingIds(rawResults); - const newRecords = rawResults.filter((r) => !existingDingIds.has(r.checkId)); + const existingByDingId = await this.getExistingRecordsByDingId(rawResults); + const newRecords = rawResults.filter((r) => !existingByDingId.has(r.checkId)); + const duplicateRecords = rawResults.filter((r) => existingByDingId.has(r.checkId)); + await this.refreshDuplicatePunchMetadata(duplicateRecords, existingByDingId); skipped = rawResults.length - newRecords.length; this.emit('parsing', newRecords.length, total, `${newRecords.length} new records, ${skipped} duplicates skipped`); @@ -246,17 +248,45 @@ export class AttendanceImportService { /** * Query which dingIds already exist to skip duplicates. */ - private async getExistingDingIds( + private async getExistingRecordsByDingId( results: DingTalkAttendanceResult[], - ): Promise> { + ): Promise> { const dingIds = results.map((r) => r.checkId).filter(Boolean); - if (dingIds.length === 0) return new Set(); + if (dingIds.length === 0) return new Map(); const existing = await this.dingRawRepo.find({ where: { dingId: In(dingIds) }, - select: ['dingId'], }); - return new Set(existing.map((e) => e.dingId)); + return new Map(existing.map((entity) => [entity.dingId, entity])); + } + + private async refreshDuplicatePunchMetadata( + results: DingTalkAttendanceResult[], + existingByDingId: Map, + ): Promise { + const changed: DingAttendanceRaw[] = []; + for (const result of results) { + const entity = existingByDingId.get(result.checkId); + if (!entity) continue; + const punchSource = result.sourceType || entity.punchSource || null; + const punchDeviceName = result.deviceName || entity.punchDeviceName || null; + const punchDeviceId = result.deviceId || entity.punchDeviceId || null; + if ( + entity.punchSource === punchSource && + entity.punchDeviceName === punchDeviceName && + entity.punchDeviceId === punchDeviceId + ) { + continue; + } + entity.punchSource = punchSource; + entity.punchDeviceName = punchDeviceName; + entity.punchDeviceId = punchDeviceId; + entity.rawData = JSON.stringify(result); + changed.push(entity); + } + if (changed.length > 0) { + await this.dingRawRepo.save(changed, { chunk: 50 }); + } } /** @@ -271,6 +301,9 @@ export class AttendanceImportService { entity.attendanceType = r.checkType || 'OnDuty'; entity.timeResult = r.timeResult; entity.locationResult = r.locationResult || ''; + entity.punchSource = r.sourceType || null; + entity.punchDeviceName = r.deviceName || null; + entity.punchDeviceId = r.deviceId || null; // Parse check-in/out times if (r.actualCheckTime) { diff --git a/apps/server/src/attendance/attendance.lesson-session.spec.ts b/apps/server/src/attendance/attendance.lesson-session.spec.ts index b4a2c94..140ccda 100644 --- a/apps/server/src/attendance/attendance.lesson-session.spec.ts +++ b/apps/server/src/attendance/attendance.lesson-session.spec.ts @@ -79,6 +79,9 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => { attendanceType: 'OnDuty', timeResult: 'Normal', checkInTime: new Date('2026-07-11T08:55:00+08:00'), + punchSource: 'ATM', + punchDeviceName: '东门考勤机', + punchDeviceId: 'ATM-01', }, { matchedStudentId: 2, @@ -100,7 +103,15 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => { }), ); expect(attendanceRepo.save).toHaveBeenCalledWith([ - expect.objectContaining({ studentId: 1, status: 'present', source: 'dingtalk' }), + expect.objectContaining({ + studentId: 1, + status: 'present', + source: 'dingtalk', + punchSource: 'ATM', + punchDeviceName: '东门考勤机', + punchDeviceId: 'ATM-01', + punchTime: new Date('2026-07-11T08:55:00+08:00'), + }), expect.objectContaining({ studentId: 2, status: 'present', source: 'dingtalk' }), expect.objectContaining({ studentId: 3, status: 'pending', source: 'dingtalk' }), expect.objectContaining({ studentId: 4, status: 'pending', source: 'dingtalk' }), diff --git a/apps/server/src/attendance/attendance.service.ts b/apps/server/src/attendance/attendance.service.ts index 8a8142f..ae833f3 100644 --- a/apps/server/src/attendance/attendance.service.ts +++ b/apps/server/src/attendance/attendance.service.ts @@ -200,6 +200,53 @@ export class AttendanceService { if (hasPunch) return 'present'; return finalize ? 'absent' : 'pending'; } + + private getLessonPunchMetadata( + records: DingAttendanceRaw[], + lessonDate: string, + startTime: string, + ): Pick { + const punches = records + .map((record) => ({ record, time: record.checkInTime ?? record.checkOutTime })) + .filter((item): item is { record: DingAttendanceRaw; time: Date } => !!item.time); + if (punches.length === 0) { + return { + punchTime: null, + punchSource: null, + punchDeviceName: null, + punchDeviceId: null, + }; + } + + const lessonStart = new Date(`${lessonDate}T${startTime}:00+08:00`).getTime(); + punches.sort( + (left, right) => + Math.abs(left.time.getTime() - lessonStart) - Math.abs(right.time.getTime() - lessonStart), + ); + const primary = punches[0]; + const metadataRecord = [...punches] + .filter(({ record }) => + !!(record.punchSource || record.punchDeviceName || record.punchDeviceId) || + !['OnDuty', 'OffDuty'].includes(record.attendanceType), + ) + .sort( + (left, right) => + Math.abs(left.time.getTime() - primary.time.getTime()) - + Math.abs(right.time.getTime() - primary.time.getTime()), + )[0]?.record; + const source = + metadataRecord?.punchSource || + (metadataRecord && !['OnDuty', 'OffDuty'].includes(metadataRecord.attendanceType) + ? metadataRecord.attendanceType + : primary.record.punchSource); + + return { + punchTime: primary.time, + punchSource: source || null, + punchDeviceName: metadataRecord?.punchDeviceName || primary.record.punchDeviceName || null, + punchDeviceId: metadataRecord?.punchDeviceId || primary.record.punchDeviceId || null, + }; + } async createLessonAttendanceFromDingTalk( scheduleId: number, lessonDate: string, @@ -270,6 +317,11 @@ export class AttendanceService { schedule.endTime, ); record.status = this.mapDingTalkStatus(raw, finalize); + Object.assign(record, this.getLessonPunchMetadata( + raw, + lessonDate, + schedule.startTime, + )); record.remark = raw.some((item) => item.checkInTime || item.checkOutTime) ? null : finalize @@ -296,6 +348,11 @@ export class AttendanceService { session: this.mapScheduleTimeToSession(schedule.startTime), status: this.mapDingTalkStatus(raw, finalize), source: 'dingtalk', + ...this.getLessonPunchMetadata( + raw, + lessonDate, + schedule.startTime, + ), remark: raw.some((item) => item.checkInTime || item.checkOutTime) ? undefined : finalize @@ -378,6 +435,11 @@ export class AttendanceService { session: this.mapScheduleTimeToSession(schedule.startTime), status: this.mapDingTalkStatus(raw, finalize), source: 'dingtalk', + ...this.getLessonPunchMetadata( + raw, + lessonDate, + schedule.startTime, + ), remark: raw.some((item) => item.checkInTime || item.checkOutTime) ? undefined : finalize @@ -908,6 +970,10 @@ export class AttendanceService { if (dto.status !== undefined) { record.status = dto.status; record.source = 'manual'; + record.punchTime = null; + record.punchSource = null; + record.punchDeviceName = null; + record.punchDeviceId = null; } if (dto.remark !== undefined) { record.remark = dto.remark; @@ -933,6 +999,10 @@ export class AttendanceService { if (dto.status !== undefined) { freshRecord.status = dto.status; freshRecord.source = 'manual'; + freshRecord.punchTime = null; + freshRecord.punchSource = null; + freshRecord.punchDeviceName = null; + freshRecord.punchDeviceId = null; } if (dto.remark !== undefined) { freshRecord.remark = dto.remark; diff --git a/apps/server/src/attendance/dingtalk-attendance.service.spec.ts b/apps/server/src/attendance/dingtalk-attendance.service.spec.ts index e3f4135..e8e79b0 100644 --- a/apps/server/src/attendance/dingtalk-attendance.service.spec.ts +++ b/apps/server/src/attendance/dingtalk-attendance.service.spec.ts @@ -68,8 +68,10 @@ describe('DingTalkService — attendance records', () => { userId: 'ding-1', workDate: Date.parse('2026-07-12T00:00:00+08:00'), userCheckTime: Date.parse('2026-07-12T21:05:00+08:00'), - sourceType: 'USER', + sourceType: 'ATM', checkType: 'OnDuty', + deviceName: '东门考勤机', + deviceId: 'ATM-01', timeResult: 'Normal', }, ], @@ -83,6 +85,13 @@ describe('DingTalkService — attendance records', () => { }); expect(record.workDate).toBe('2026-07-12'); + expect(record).toEqual( + expect.objectContaining({ + checkType: 'OnDuty', + sourceType: 'ATM', + deviceName: '东门考勤机', + deviceId: 'ATM-01', + }), + ); }); }); - diff --git a/apps/server/src/entities/attendance-record.entity.ts b/apps/server/src/entities/attendance-record.entity.ts index bbe5aa5..68bd36e 100644 --- a/apps/server/src/entities/attendance-record.entity.ts +++ b/apps/server/src/entities/attendance-record.entity.ts @@ -67,6 +67,18 @@ export class AttendanceRecord { @Column({ name: 'source', length: 20, default: 'manual' }) source: string; + @Column({ name: 'punch_time', type: 'datetime', nullable: true }) + punchTime: Date | null; + + @Column({ name: 'punch_source', type: 'varchar', length: 40, nullable: true }) + punchSource: string | null; + + @Column({ name: 'punch_device_name', type: 'varchar', length: 100, nullable: true }) + punchDeviceName: string | null; + + @Column({ name: 'punch_device_id', type: 'varchar', length: 100, nullable: true }) + punchDeviceId: string | null; + @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/ding-attendance-raw.entity.ts b/apps/server/src/entities/ding-attendance-raw.entity.ts index 49537de..f97958a 100644 --- a/apps/server/src/entities/ding-attendance-raw.entity.ts +++ b/apps/server/src/entities/ding-attendance-raw.entity.ts @@ -44,6 +44,15 @@ export class DingAttendanceRaw { @Column({ name: 'location_result', length: 20, nullable: true }) locationResult: string; + @Column({ name: 'punch_source', type: 'varchar', length: 40, nullable: true }) + punchSource: string | null; + + @Column({ name: 'punch_device_name', type: 'varchar', length: 100, nullable: true }) + punchDeviceName: string | null; + + @Column({ name: 'punch_device_id', type: 'varchar', length: 100, nullable: true }) + punchDeviceId: string | null; + @Column({ name: 'match_status', length: 20, default: 'unmatched' }) matchStatus: string; diff --git a/apps/server/src/integration/dingtalk.service.ts b/apps/server/src/integration/dingtalk.service.ts index a08cf4e..91c6506 100644 --- a/apps/server/src/integration/dingtalk.service.ts +++ b/apps/server/src/integration/dingtalk.service.ts @@ -51,6 +51,11 @@ export interface DingTalkAttendanceResult { actualCheckTime: string; checkId: string; checkType: string; + /** 钉钉返回的打卡来源,例如 ATM / USER / BEACON。 */ + sourceType: string; + /** 部分钉钉租户会额外返回考勤机名称或编号。 */ + deviceName?: string; + deviceId?: string; } // ── 组织架构 API 类型 ── @@ -505,6 +510,8 @@ export class DingTalkService { checkType?: string; timeResult?: string; locationResult?: string; locationMethod?: string; userAddress?: string; userLongitude?: number; userLatitude?: number; + deviceName?: string; deviceId?: string | number; + attendanceMachineName?: string; attendanceMachineId?: string | number; }>; }; if (data.errcode !== 0) throw new Error(`钉钉考勤获取失败: ${data.errmsg}`); @@ -520,7 +527,10 @@ export class DingTalkService { planCheckTime: '', actualCheckTime: new Date(r.userCheckTime).toISOString(), checkId: String(r.id), - checkType: r.checkType ?? r.sourceType ?? '', + checkType: r.checkType ?? '', + sourceType: r.sourceType ?? '', + deviceName: r.deviceName ?? r.attendanceMachineName, + deviceId: String(r.deviceId ?? r.attendanceMachineId ?? '') || undefined, })); } From 5cf6aede1ee27e2c43f608e0be4c73a233f65ea7 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Tue, 14 Jul 2026 11:20:27 +0800 Subject: [PATCH 2/3] feat: improve occupancy import template --- apps/admin/src/pages/Occupancies/index.tsx | 2 +- .../src/occupancies/occupancies.controller.ts | 109 +-------- .../occupancies/occupancies.service.spec.ts | 65 +++++ .../src/occupancies/occupancies.service.ts | 57 ++++- .../occupancy-import-template.spec.ts | 62 +++++ .../occupancies/occupancy-import-template.ts | 224 ++++++++++++++++++ apps/server/src/rbac/rbac.permissions.spec.ts | 4 +- apps/server/src/rbac/rbac.service.ts | 2 +- 8 files changed, 415 insertions(+), 110 deletions(-) create mode 100644 apps/server/src/occupancies/occupancy-import-template.spec.ts create mode 100644 apps/server/src/occupancies/occupancy-import-template.ts diff --git a/apps/admin/src/pages/Occupancies/index.tsx b/apps/admin/src/pages/Occupancies/index.tsx index b59066a..1ce8cdf 100644 --- a/apps/admin/src/pages/Occupancies/index.tsx +++ b/apps/admin/src/pages/Occupancies/index.tsx @@ -598,7 +598,7 @@ const OccupanciesPage: React.FC = () => { placeholder="默认为短租" /> - +