已完成,学生端考勤记录现在按课程展示。

每门课程会独立显示:
课程名称、代码、教学班和任课教师。
课程总体出勤率。
出勤、迟到、缺勤、请假、免修次数。
按日期排列的历次点名记录。
缺勤或迟到记录仍可单独申诉。
This commit is contained in:
biss committed 2026-07-26 10:49:06 +08:00
1 parent 72297aa23f
commit 3b73292d93
12 files changed
+2172 -54

No files matched your search

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using ClosedXML.Excel; using ClosedXML.Excel;
using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
@@ -76,8 +77,12 @@ public sealed class AttendanceController(
x.Name, x.Name,
x.AttendanceDate, x.AttendanceDate,
x.Status, x.Status,
x.CheckInMethod,
x.CheckInStartsAt,
x.CheckInEndsAt,
x.Notes, x.Notes,
x.SubmittedAt, x.SubmittedAt,
CheckedInCount = x.Records.Count(r => r.CheckInAt != null),
PresentCount = x.Records.Count(r => r.Status == AttendanceStatus.Present), PresentCount = x.Records.Count(r => r.Status == AttendanceStatus.Present),
AbsentCount = x.Records.Count(r => r.Status == AttendanceStatus.Absent), AbsentCount = x.Records.Count(r => r.Status == AttendanceStatus.Absent),
LateCount = x.Records.Count(r => r.Status == AttendanceStatus.Late), LateCount = x.Records.Count(r => r.Status == AttendanceStatus.Late),
@@ -109,20 +114,71 @@ public sealed class AttendanceController(
if (studentIds.Count == 0) if (studentIds.Count == 0)
return ConflictProblem("该教学班没有有效选课学生。"); return ConflictProblem("该教学班没有有效选课学生。");
var checkInMethod = request.CheckInMethod ?? AttendanceCheckInMethod.Manual;
if (!Enum.IsDefined(checkInMethod))
return ConflictProblem("不支持该签到方式。");
var now = DateTime.UtcNow;
DateTime? checkInEndsAt = null;
string? checkInToken = null;
if (checkInMethod != AttendanceCheckInMethod.Manual)
{
if (request.CheckInDurationMinutes is < 1 or > 180)
return ConflictProblem("签到时长应为 1 至 180 分钟。");
checkInEndsAt = now.AddMinutes(request.CheckInDurationMinutes!.Value);
}
if (checkInMethod == AttendanceCheckInMethod.QrCode)
checkInToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(24));
if (checkInMethod == AttendanceCheckInMethod.Location)
{
if (request.TargetLatitude is < -90 or > 90 ||
request.TargetLongitude is < -180 or > 180 ||
request.TargetLatitude is null ||
request.TargetLongitude is null)
return ConflictProblem("请获取有效的签到位置。");
if (request.LocationRadiusMeters is < 20 or > 1000)
return ConflictProblem("定位签到范围应为 20 至 1000 米。");
}
var sheet = new AttendanceSheet var sheet = new AttendanceSheet
{ {
TeachingTaskId = request.TeachingTaskId, TeachingTaskId = request.TeachingTaskId,
Name = request.Name.Trim(), Name = request.Name.Trim(),
AttendanceDate = request.AttendanceDate, AttendanceDate = request.AttendanceDate,
CheckInMethod = checkInMethod,
CheckInToken = checkInToken,
CheckInStartsAt = checkInMethod == AttendanceCheckInMethod.Manual
? null
: now,
CheckInEndsAt = checkInEndsAt,
TargetLatitude = checkInMethod == AttendanceCheckInMethod.Location
? request.TargetLatitude
: null,
TargetLongitude = checkInMethod == AttendanceCheckInMethod.Location
? request.TargetLongitude
: null,
LocationRadiusMeters = checkInMethod == AttendanceCheckInMethod.Location
? request.LocationRadiusMeters
: null,
Notes = Normalize(request.Notes), Notes = Normalize(request.Notes),
Records = studentIds.Select(studentId => new AttendanceRecord Records = studentIds.Select(studentId => new AttendanceRecord
{ {
StudentId = studentId StudentId = studentId,
Status = checkInMethod == AttendanceCheckInMethod.Manual
? AttendanceStatus.Present
: AttendanceStatus.Absent
}).ToList() }).ToList()
}; };
db.AttendanceSheets.Add(sheet); db.AttendanceSheets.Add(sheet);
await db.SaveChangesAsync(cancellationToken); await db.SaveChangesAsync(cancellationToken);
return Created(string.Empty, new { sheet.Id }); return Created(string.Empty, new
{
sheet.Id,
sheet.CheckInMethod,
sheet.CheckInToken,
sheet.CheckInStartsAt,
sheet.CheckInEndsAt
});
} }
[HttpGet("sheets/{id:guid}")] [HttpGet("sheets/{id:guid}")]
@@ -138,6 +194,13 @@ public sealed class AttendanceController(
x.Name, x.Name,
x.AttendanceDate, x.AttendanceDate,
x.Status, x.Status,
x.CheckInMethod,
x.CheckInToken,
x.CheckInStartsAt,
x.CheckInEndsAt,
x.TargetLatitude,
x.TargetLongitude,
x.LocationRadiusMeters,
x.Notes, x.Notes,
x.SubmittedAt, x.SubmittedAt,
TaskNumber = x.TeachingTask!.TaskNumber, TaskNumber = x.TeachingTask!.TaskNumber,
@@ -153,7 +216,11 @@ public sealed class AttendanceController(
r.Student.Name, r.Student.Name,
ClassName = r.Student.AdministrativeClass!.Name, ClassName = r.Student.AdministrativeClass!.Name,
r.Status, r.Status,
r.Notes r.Notes,
r.CheckInAt,
r.CheckedInMethod,
r.CheckInAccuracyMeters,
r.CheckInDistanceMeters
}) })
}) })
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -170,10 +237,11 @@ public sealed class AttendanceController(
.Where(e => e.TeachingTaskId == sheet.TeachingTaskId && e.Status == ApprovalStatus.Approved) .Where(e => e.TeachingTaskId == sheet.TeachingTaskId && e.Status == ApprovalStatus.Approved)
.Select(e => e.StudentId).ToHashSetAsync(cancellationToken); .Select(e => e.StudentId).ToHashSetAsync(cancellationToken);
var canEdit = sheet.Status == AttendanceSheetStatus.Draft && var canManage = await CanManageTaskAsync(
await CanManageTaskAsync( sheet.TeachingTaskId,
sheet.TeachingTaskId, cancellationToken);
cancellationToken); var canEdit = sheet.Status == AttendanceSheetStatus.Draft && canManage;
var now = DateTime.UtcNow;
return Ok(new return Ok(new
{ {
Sheet = new Sheet = new
@@ -183,6 +251,20 @@ public sealed class AttendanceController(
sheet.Name, sheet.Name,
sheet.AttendanceDate, sheet.AttendanceDate,
sheet.Status, sheet.Status,
sheet.CheckInMethod,
CheckInToken = canManage ? sheet.CheckInToken : null,
sheet.CheckInStartsAt,
sheet.CheckInEndsAt,
sheet.TargetLatitude,
sheet.TargetLongitude,
sheet.LocationRadiusMeters,
IsCheckInOpen = IsCheckInOpen(
sheet.Status,
sheet.CheckInMethod,
sheet.CheckInStartsAt,
sheet.CheckInEndsAt,
now),
CheckedInCount = sheet.Records.Count(r => r.CheckInAt != null),
sheet.Notes, sheet.Notes,
sheet.SubmittedAt, sheet.SubmittedAt,
sheet.TaskNumber, sheet.TaskNumber,
@@ -197,6 +279,10 @@ public sealed class AttendanceController(
r.ClassName, r.ClassName,
r.Status, r.Status,
r.Notes, r.Notes,
r.CheckInAt,
r.CheckedInMethod,
r.CheckInAccuracyMeters,
r.CheckInDistanceMeters,
IsExempt = exemptStudentIds.Contains(r.StudentId), IsExempt = exemptStudentIds.Contains(r.StudentId),
IsDeferred = deferredStudentIds.Contains(r.StudentId) IsDeferred = deferredStudentIds.Contains(r.StudentId)
}) })
@@ -387,8 +473,231 @@ public sealed class AttendanceController(
return NoContent(); return NoContent();
} }
[HttpPost("sheets/{id:guid}/close-check-in")]
[Authorize(Roles = AttendanceRoles)]
public async Task<ActionResult> CloseCheckIn(
Guid id,
CancellationToken cancellationToken)
{
var sheet = await db.AttendanceSheets
.Include(x => x.TeachingTask)
.ThenInclude(x => x!.Teachers)
.ThenInclude(x => x.Teacher)
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
if (sheet is null) return NotFound();
if (!CanManageSheet(sheet)) return Forbid();
if (sheet.Status != AttendanceSheetStatus.Draft)
return ConflictProblem("考勤表已提交,签到活动已经结束。");
if (sheet.CheckInMethod == AttendanceCheckInMethod.Manual)
return ConflictProblem("普通点名没有在线签到活动。");
var now = DateTime.UtcNow;
if (sheet.CheckInEndsAt is null || sheet.CheckInEndsAt > now)
sheet.CheckInEndsAt = now;
await db.SaveChangesAsync(cancellationToken);
return NoContent();
}
// ═══════════════ Student endpoints ═══════════════ // ═══════════════ Student endpoints ═══════════════
[HttpGet("check-in-info")]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> GetCheckInInfo(
[FromQuery, MaxLength(64)] string token,
CancellationToken cancellationToken)
{
var studentId = await GetCurrentStudentIdAsync(cancellationToken);
if (!studentId.HasValue)
return ConflictProblem("当前账号未关联学生档案。");
if (string.IsNullOrWhiteSpace(token))
return NotFound();
var activity = await db.AttendanceRecords.AsNoTracking()
.Where(x =>
x.StudentId == studentId.Value &&
x.AttendanceSheet!.CheckInToken == token.Trim())
.Select(x => new
{
SheetId = x.AttendanceSheetId,
SheetName = x.AttendanceSheet!.Name,
x.AttendanceSheet.AttendanceDate,
x.AttendanceSheet.Status,
x.AttendanceSheet.CheckInMethod,
x.AttendanceSheet.CheckInStartsAt,
x.AttendanceSheet.CheckInEndsAt,
CourseCode = x.AttendanceSheet.TeachingTask!.Course!.Code,
CourseName = x.AttendanceSheet.TeachingTask.Course.Name,
TaskNumber = x.AttendanceSheet.TeachingTask.TaskNumber,
x.CheckInAt
})
.FirstOrDefaultAsync(cancellationToken);
if (activity is null) return NotFound();
var now = DateTime.UtcNow;
return Ok(new
{
activity.SheetId,
activity.SheetName,
activity.AttendanceDate,
activity.CheckInMethod,
activity.CheckInStartsAt,
activity.CheckInEndsAt,
activity.CourseCode,
activity.CourseName,
activity.TaskNumber,
activity.CheckInAt,
IsOpen = IsCheckInOpen(
activity.Status,
activity.CheckInMethod,
activity.CheckInStartsAt,
activity.CheckInEndsAt,
now)
});
}
[HttpGet("open-check-ins")]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> GetOpenCheckIns(
CancellationToken cancellationToken)
{
var studentId = await GetCurrentStudentIdAsync(cancellationToken);
if (!studentId.HasValue)
return ConflictProblem("当前账号未关联学生档案。");
var now = DateTime.UtcNow;
return Ok(await db.AttendanceRecords.AsNoTracking()
.Where(x =>
x.StudentId == studentId.Value &&
x.AttendanceSheet!.Status == AttendanceSheetStatus.Draft &&
x.AttendanceSheet.CheckInMethod == AttendanceCheckInMethod.Location &&
x.AttendanceSheet.CheckInStartsAt <= now &&
x.AttendanceSheet.CheckInEndsAt >= now)
.OrderBy(x => x.AttendanceSheet!.CheckInEndsAt)
.Select(x => new
{
SheetId = x.AttendanceSheetId,
SheetName = x.AttendanceSheet!.Name,
x.AttendanceSheet.AttendanceDate,
x.AttendanceSheet.CheckInMethod,
x.AttendanceSheet.CheckInStartsAt,
x.AttendanceSheet.CheckInEndsAt,
x.AttendanceSheet.LocationRadiusMeters,
CourseCode = x.AttendanceSheet.TeachingTask!.Course!.Code,
CourseName = x.AttendanceSheet.TeachingTask.Course.Name,
TaskNumber = x.AttendanceSheet.TeachingTask.TaskNumber,
x.CheckInAt
})
.ToListAsync(cancellationToken));
}
[HttpPost("check-in")]
[Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> CheckIn(
AttendanceCheckInRequest request,
CancellationToken cancellationToken)
{
var studentId = await GetCurrentStudentIdAsync(cancellationToken);
if (!studentId.HasValue)
return ConflictProblem("当前账号未关联学生档案。");
var source = db.AttendanceRecords
.Include(x => x.AttendanceSheet)
.Where(x => x.StudentId == studentId.Value);
if (!string.IsNullOrWhiteSpace(request.Token))
{
var token = request.Token.Trim();
source = source.Where(x => x.AttendanceSheet!.CheckInToken == token);
}
else if (request.AttendanceSheetId.HasValue)
{
source = source.Where(x =>
x.AttendanceSheetId == request.AttendanceSheetId.Value);
}
else
{
return ConflictProblem("缺少签到活动信息。");
}
var record = await source.FirstOrDefaultAsync(cancellationToken);
if (record?.AttendanceSheet is null) return NotFound();
var sheet = record.AttendanceSheet;
var now = DateTime.UtcNow;
if (record.CheckInAt.HasValue)
{
return Ok(new
{
AlreadyCheckedIn = true,
record.CheckInAt,
record.CheckInDistanceMeters
});
}
if (!IsCheckInOpen(
sheet.Status,
sheet.CheckInMethod,
sheet.CheckInStartsAt,
sheet.CheckInEndsAt,
now))
return ConflictProblem("签到尚未开始或已经结束。");
if (sheet.CheckInMethod == AttendanceCheckInMethod.Manual)
return ConflictProblem("该考勤表不支持学生在线签到。");
double? distanceMeters = null;
if (sheet.CheckInMethod == AttendanceCheckInMethod.QrCode)
{
if (string.IsNullOrWhiteSpace(request.Token) ||
!string.Equals(
sheet.CheckInToken,
request.Token.Trim(),
StringComparison.Ordinal))
return NotFound();
}
else if (sheet.CheckInMethod == AttendanceCheckInMethod.Location)
{
if (request.Latitude is < -90 or > 90 ||
request.Longitude is < -180 or > 180 ||
request.Latitude is null ||
request.Longitude is null)
return ConflictProblem("未获取到有效的当前位置。");
if (sheet.TargetLatitude is null ||
sheet.TargetLongitude is null ||
sheet.LocationRadiusMeters is null)
return ConflictProblem("签到活动没有配置有效的位置范围。");
distanceMeters = CalculateDistanceMeters(
(double)sheet.TargetLatitude.Value,
(double)sheet.TargetLongitude.Value,
(double)request.Latitude.Value,
(double)request.Longitude.Value);
if (distanceMeters > sheet.LocationRadiusMeters.Value)
{
return ConflictProblem(
$"当前位置距签到点约 {Math.Round(distanceMeters.Value)} 米,超出 {sheet.LocationRadiusMeters.Value} 米签到范围。");
}
}
record.Status = AttendanceStatus.Present;
record.CheckInAt = now;
record.CheckedInMethod = sheet.CheckInMethod;
record.CheckInLatitude = sheet.CheckInMethod == AttendanceCheckInMethod.Location
? request.Latitude
: null;
record.CheckInLongitude = sheet.CheckInMethod == AttendanceCheckInMethod.Location
? request.Longitude
: null;
record.CheckInAccuracyMeters = sheet.CheckInMethod == AttendanceCheckInMethod.Location
? request.AccuracyMeters
: null;
record.CheckInDistanceMeters = distanceMeters;
await db.SaveChangesAsync(cancellationToken);
return Ok(new
{
AlreadyCheckedIn = false,
record.CheckInAt,
record.CheckInDistanceMeters
});
}
[HttpGet("my-records")] [HttpGet("my-records")]
[Authorize(Roles = SystemRoles.Student)] [Authorize(Roles = SystemRoles.Student)]
public async Task<ActionResult> GetMyRecords( public async Task<ActionResult> GetMyRecords(
@@ -415,6 +724,7 @@ public sealed class AttendanceController(
.Select(r => new .Select(r => new
{ {
r.AttendanceSheetId, r.AttendanceSheetId,
r.AttendanceSheet!.TeachingTaskId,
SheetName = r.AttendanceSheet!.Name, SheetName = r.AttendanceSheet!.Name,
r.AttendanceSheet.AttendanceDate, r.AttendanceSheet.AttendanceDate,
TaskNumber = r.AttendanceSheet.TeachingTask!.TaskNumber, TaskNumber = r.AttendanceSheet.TeachingTask!.TaskNumber,
@@ -657,9 +967,54 @@ public sealed class AttendanceController(
.AnyAsync( .AnyAsync(
x => x.TeachingTaskId == teachingTaskId && x => x.TeachingTaskId == teachingTaskId &&
x.Teacher!.UserId == scope.UserId, x.Teacher!.UserId == scope.UserId,
cancellationToken); cancellationToken);
} }
private async Task<Guid?> GetCurrentStudentIdAsync(
CancellationToken cancellationToken)
{
var userId = currentUserDataScope.Current.UserId;
return await db.Students.AsNoTracking()
.Where(x => x.UserId == userId)
.Select(x => (Guid?)x.Id)
.FirstOrDefaultAsync(cancellationToken);
}
private static bool IsCheckInOpen(
AttendanceSheetStatus status,
AttendanceCheckInMethod method,
DateTime? startsAt,
DateTime? endsAt,
DateTime now) =>
status == AttendanceSheetStatus.Draft &&
method != AttendanceCheckInMethod.Manual &&
startsAt.HasValue &&
endsAt.HasValue &&
startsAt.Value <= now &&
endsAt.Value >= now;
internal static double CalculateDistanceMeters(
double latitude1,
double longitude1,
double latitude2,
double longitude2)
{
const double earthRadiusMeters = 6_371_000;
var latitudeDelta = DegreesToRadians(latitude2 - latitude1);
var longitudeDelta = DegreesToRadians(longitude2 - longitude1);
var startLatitude = DegreesToRadians(latitude1);
var endLatitude = DegreesToRadians(latitude2);
var haversine =
Math.Sin(latitudeDelta / 2) * Math.Sin(latitudeDelta / 2) +
Math.Cos(startLatitude) * Math.Cos(endLatitude) *
Math.Sin(longitudeDelta / 2) * Math.Sin(longitudeDelta / 2);
return earthRadiusMeters * 2 *
Math.Atan2(Math.Sqrt(haversine), Math.Sqrt(1 - haversine));
}
private static double DegreesToRadians(double degrees) =>
degrees * Math.PI / 180;
private async Task<AttendanceCourseStatistics?> LoadTaskStatisticsAsync( private async Task<AttendanceCourseStatistics?> LoadTaskStatisticsAsync(
Guid teachingTaskId, Guid teachingTaskId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -993,7 +1348,12 @@ public sealed record AttendanceSheetRequest(
Guid TeachingTaskId, Guid TeachingTaskId,
[MaxLength(120)] string Name, [MaxLength(120)] string Name,
DateTime AttendanceDate, DateTime AttendanceDate,
[MaxLength(500)] string? Notes); [MaxLength(500)] string? Notes,
AttendanceCheckInMethod? CheckInMethod,
[Range(1, 180)] int? CheckInDurationMinutes,
[Range(-90, 90)] decimal? TargetLatitude,
[Range(-180, 180)] decimal? TargetLongitude,
[Range(20, 1000)] int? LocationRadiusMeters);
public sealed record AttendanceRecordsRequest( public sealed record AttendanceRecordsRequest(
IReadOnlyCollection<AttendanceRecordRequest> Records); IReadOnlyCollection<AttendanceRecordRequest> Records);
@@ -1011,6 +1371,13 @@ public sealed record AttendanceAppealReviewRequest(
bool Approve, bool Approve,
[MaxLength(300)] string? Comment); [MaxLength(300)] string? Comment);
public sealed record AttendanceCheckInRequest(
Guid? AttendanceSheetId,
[MaxLength(64)] string? Token,
[Range(-90, 90)] decimal? Latitude,
[Range(-180, 180)] decimal? Longitude,
[Range(0, 5000)] double? AccuracyMeters);
public sealed record AttendanceCourseStatistics( public sealed record AttendanceCourseStatistics(
AttendanceStatisticsCourse Course, AttendanceStatisticsCourse Course,
AttendanceStatisticsSummary Summary, AttendanceStatisticsSummary Summary,
@@ -9,6 +9,14 @@ public sealed class AttendanceSheet : EntityBase
public required string Name { get; set; } public required string Name { get; set; }
public DateTime AttendanceDate { get; set; } public DateTime AttendanceDate { get; set; }
public AttendanceSheetStatus Status { get; set; } = AttendanceSheetStatus.Draft; public AttendanceSheetStatus Status { get; set; } = AttendanceSheetStatus.Draft;
public AttendanceCheckInMethod CheckInMethod { get; set; } =
AttendanceCheckInMethod.Manual;
public string? CheckInToken { get; set; }
public DateTime? CheckInStartsAt { get; set; }
public DateTime? CheckInEndsAt { get; set; }
public decimal? TargetLatitude { get; set; }
public decimal? TargetLongitude { get; set; }
public int? LocationRadiusMeters { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTime? SubmittedAt { get; set; } public DateTime? SubmittedAt { get; set; }
public ICollection<AttendanceRecord> Records { get; set; } = []; public ICollection<AttendanceRecord> Records { get; set; } = [];
@@ -22,6 +30,12 @@ public sealed class AttendanceRecord
public Student? Student { get; set; } public Student? Student { get; set; }
public AttendanceStatus Status { get; set; } = AttendanceStatus.Present; public AttendanceStatus Status { get; set; } = AttendanceStatus.Present;
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTime? CheckInAt { get; set; }
public AttendanceCheckInMethod? CheckedInMethod { get; set; }
public decimal? CheckInLatitude { get; set; }
public decimal? CheckInLongitude { get; set; }
public double? CheckInAccuracyMeters { get; set; }
public double? CheckInDistanceMeters { get; set; }
public AttendanceAppealStatus AppealStatus { get; set; } = AttendanceAppealStatus.None; public AttendanceAppealStatus AppealStatus { get; set; } = AttendanceAppealStatus.None;
public string? AppealReason { get; set; } public string? AppealReason { get; set; }
public DateTime? AppealSubmittedAt { get; set; } public DateTime? AppealSubmittedAt { get; set; }
@@ -44,6 +58,13 @@ public enum AttendanceStatus
Excused = 5 Excused = 5
} }
public enum AttendanceCheckInMethod
{
Manual = 1,
QrCode = 2,
Location = 3
}
public enum AttendanceAppealStatus public enum AttendanceAppealStatus
{ {
None = 0, None = 0,
@@ -561,8 +561,12 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
builder.Entity<AttendanceSheet>(entity => builder.Entity<AttendanceSheet>(entity =>
{ {
entity.Property(x => x.Name).HasMaxLength(120); entity.Property(x => x.Name).HasMaxLength(120);
entity.Property(x => x.CheckInToken).HasMaxLength(64);
entity.Property(x => x.TargetLatitude).HasPrecision(10, 7);
entity.Property(x => x.TargetLongitude).HasPrecision(10, 7);
entity.Property(x => x.Notes).HasMaxLength(500); entity.Property(x => x.Notes).HasMaxLength(500);
entity.HasIndex(x => new { x.TeachingTaskId, x.AttendanceDate }); entity.HasIndex(x => new { x.TeachingTaskId, x.AttendanceDate });
entity.HasIndex(x => x.CheckInToken).IsUnique();
entity.HasOne(x => x.TeachingTask) entity.HasOne(x => x.TeachingTask)
.WithMany() .WithMany()
.HasForeignKey(x => x.TeachingTaskId) .HasForeignKey(x => x.TeachingTaskId)
@@ -575,6 +579,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
entity.Property(x => x.Notes).HasMaxLength(300); entity.Property(x => x.Notes).HasMaxLength(300);
entity.Property(x => x.AppealReason).HasMaxLength(500); entity.Property(x => x.AppealReason).HasMaxLength(500);
entity.Property(x => x.AppealReviewComment).HasMaxLength(300); entity.Property(x => x.AppealReviewComment).HasMaxLength(300);
entity.Property(x => x.CheckInLatitude).HasPrecision(10, 7);
entity.Property(x => x.CheckInLongitude).HasPrecision(10, 7);
entity.HasIndex(x => x.AppealStatus); entity.HasIndex(x => x.AppealStatus);
entity.HasOne(x => x.AttendanceSheet) entity.HasOne(x => x.AttendanceSheet)
.WithMany(x => x.Records) .WithMany(x => x.Records)
Loaded 3 of 12 files, more files were not shown because too many files have changed in this diff. Show more