:::info Phạm vi Thời gian: 3 tháng | Nền tảng: iOS & Android (React Native / Flutter) Ra mắt App Di động với các tính năng đặc thù mobile. :::

Tổng quan các bảng

#Tên bảngMô tả
1t_huca_event_attendancesĐiểm danh sự kiện (QR/GPS)
2t_huca_user_locationsVị trí người dùng (Tìm CSV lân cận)
3t_huca_device_tokensToken thiết bị (Push Notification)

1. Điểm danh sự kiện

t_huca_event_attendances

Ghi nhận điểm danh với đầy đủ thông tin chống gian lận.
CREATE TABLE t_huca_event_attendances (
    Id              BIGINT IDENTITY(1,1) PRIMARY KEY,
    EventId         BIGINT NOT NULL,    -- FK -> t_huca_events
    UserId          BIGINT NOT NULL,    -- FK -> t_huca_users
    -- Thông tin điểm danh
    CheckInTime     DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    CheckOutTime    DATETIME2,
    Method          NVARCHAR(20) NOT NULL,      -- QR, GPS, Manual
    -- Vị trí GPS
    Latitude        DECIMAL(10,7),
    Longitude       DECIMAL(10,7),
    IsWithinGeofence BIT,              -- So sánh với Events.GeoFenceRadius
    -- Thiết bị (chống gian lận)
    DeviceInfo      NVARCHAR(200),
    DeviceId        NVARCHAR(100),
    -- Trạng thái xác thực
    Status          NVARCHAR(20) NOT NULL DEFAULT 'Valid',
    Note            NVARCHAR(500)
);

Luồng điểm danh QR

CSV mở App → Nhấn nút Điểm danh
    → Quét mã QR sự kiện (Events.EventQrCode)
    → App ghi nhận GPS tại thời điểm quét
    → So sánh GPS với tọa độ sự kiện + GeoFenceRadius
    → IsWithinGeofence = true/false
    → Lưu vào t_huca_event_attendances
    → Cập nhật EventRegistrations.Status = 'Attended'

Luồng điểm danh GPS

CSV mở App tại địa điểm sự kiện
    → App tự động phát hiện khi vào GeoFence
    → Ghi nhận điểm danh tự động
    → Method = 'GPS', IsWithinGeofence = true

Logic chống gian lận

Kiểm traMô tảKết quả
GeoFenceSo sánh GPS với bán kính sự kiệnIsWithinGeofence
DeviceIdPhát hiện 1 thiết bị điểm danh nhiều ngườiStatus = 'Suspicious'
CheckInTimeĐiểm danh ngoài giờ sự kiệnStatus = 'Invalid'
DuplicateĐiểm danh 2 lần cùng sự kiệnTừ chối ở tầng API
Nhật ký điểm danh bao gồm:
  • Thời gian chính xác (CheckInTime)
  • Tọa độ GPS (Latitude, Longitude)
  • Thông tin thiết bị (DeviceInfo, DeviceId)
  • Phương thức điểm danh (Method)

2. Tìm CSV lân cận (Radar)

t_huca_user_locations

CREATE TABLE t_huca_user_locations (
    Id              BIGINT IDENTITY(1,1) PRIMARY KEY,
    UserId          BIGINT NOT NULL UNIQUE,    -- 1 bản ghi/người dùng
    Latitude        DECIMAL(10,7) NOT NULL,
    Longitude       DECIMAL(10,7) NOT NULL,
    -- Vị trí xấp xỉ (bảo vệ riêng tư)
    ApproxDistrict  NVARCHAR(100),
    ApproxProvince  NVARCHAR(100),
    -- Cài đặt chia sẻ
    SharingMode     NVARCHAR(20) NOT NULL DEFAULT 'Never',
    ShowExactLocation BIT NOT NULL DEFAULT 0,
    LastUpdatedAt   DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);

Chế độ chia sẻ vị trí

SharingModeMô tả
AlwaysLuôn luôn chia sẻ vị trí
WhenActiveChỉ khi đang mở ứng dụng
Never(Mặc định) Không bao giờ chia sẻ
:::caution Bảo mật vị trí Khi ShowExactLocation = false (mặc định), hệ thống chỉ hiển thị vị trí xấp xỉ ở cấp quận/huyện (ApproxDistrict), không lộ tọa độ chính xác cho người dùng khác. :::

Tìm CSV theo bán kính

-- Truy vấn tìm CSV trong bán kính R km (dùng Haversine formula)
-- Hoặc bổ sung SQL Server Spatial Index cho hiệu năng cao hơn

SELECT u.Id, u.FullName, u.AvatarUrl, u.AcademicYearId,
       -- Khoảng cách (km)
       6371 * ACOS(
           COS(RADIANS(@UserLat)) * COS(RADIANS(l.Latitude))
           * COS(RADIANS(l.Longitude) - RADIANS(@UserLng))
           + SIN(RADIANS(@UserLat)) * SIN(RADIANS(l.Latitude))
       ) AS DistanceKm,
       -- Hiển thị theo setting riêng tư
       CASE WHEN l.ShowExactLocation = 1 THEN l.Latitude ELSE NULL END AS Latitude,
       CASE WHEN l.ShowExactLocation = 1 THEN l.Longitude ELSE NULL END AS Longitude,
       l.ApproxDistrict,
       l.ApproxProvince
FROM t_huca_user_locations l
JOIN t_huca_users u ON u.Id = l.UserId
WHERE l.SharingMode != 'Never'
  AND l.LastUpdatedAt > DATEADD(HOUR, -1, GETUTCDATE())   -- Vị trí cập nhật trong 1h
  AND 6371 * ACOS(...) <= @RadiusKm                        -- Trong bán kính R km
ORDER BY DistanceKm;
:::tip Spatial Index (Khuyến nghị) Khi số người dùng lớn, bổ sung SQL Server Spatial Index trên cột geography để tăng tốc truy vấn tìm kiếm theo bán kính.
ALTER TABLE t_huca_user_locations
    ADD LocationGeo AS geography::Point(Latitude, Longitude, 4326) PERSISTED;
CREATE SPATIAL INDEX SIX_user_locations ON t_huca_user_locations(LocationGeo);
:::

3. Push Notification

t_huca_device_tokens

CREATE TABLE t_huca_device_tokens (
    Id              BIGINT IDENTITY(1,1) PRIMARY KEY,
    UserId          BIGINT NOT NULL,
    DeviceToken     NVARCHAR(500) NOT NULL,    -- FCM token (Android) / APNs token (iOS)
    Platform        NVARCHAR(10) NOT NULL,      -- iOS, Android
    DeviceModel     NVARCHAR(100),
    AppVersion      NVARCHAR(20),
    IsActive        BIT NOT NULL DEFAULT 1,
    CreatedAt       DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    UpdatedAt       DATETIME2
);

Các loại thông báo đẩy

LoạiKhi nàoNguồn dữ liệu
Sự kiện mớiKhi event được publishedt_huca_events
Nhắc sự kiện24h/1h trước khi bắt đầut_huca_events
Tin nhắn mớiKhi có message mớit_huca_messages
Hoạt động nhómKhi có post mới trong nhómt_huca_posts
Phê duyệt tài khoảnKhi admin approvet_huca_users

Luồng gửi Push

Trigger (Event/Message/...)
    → Insert t_huca_notifications (Channel = 'Push')
    → Query t_huca_device_tokens (UserId, IsActive = 1)
    → Gửi qua FCM (Android) / APNs (iOS)
    → Cập nhật notifications.ReadAt khi người dùng tap

Chức năng kế thừa từ Web

App Di động kế thừa toàn bộ tính năng từ Giai đoạn 1, bao gồm:
  • ✅ Xem danh bạ CSV, Tìm kiếm nâng cao
  • ✅ Xem/Đăng ký sự kiện
  • ✅ Xem tin tức, Nhóm và thảo luận
  • ✅ Nhắn tin 1-1 và nhóm
  • ✅ Gây quỹ và đóng góp
  • ✅ Tích hợp hệ thống HUCE (Một cửa, Việc làm)
Không cần bảng mới — dữ liệu tái sử dụng từ Giai đoạn 1, chỉ bổ sung tầng API endpoint và UI mobile.