Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.doubleo.passservice.domain.log.domain.BuildingEnterLog;
import com.doubleo.passservice.domain.log.domain.Direction;
import com.doubleo.passservice.domain.log.repository.BuildingEnterLogRepository;
import com.doubleo.passservice.domain.pass.enums.VisitCategory;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
Expand Down Expand Up @@ -60,7 +61,9 @@ public void consumeMessages() {
Long.parseLong((String) data.get("memberId")),
(String) data.get("memberName"),
Long.parseLong((String) data.get("passId")),
Direction.valueOf(((String) data.get("direction")).toUpperCase()));
Direction.valueOf(((String) data.get("direction")).toUpperCase()),
VisitCategory.valueOf(
((String) data.get("visitCategory")).toUpperCase()));
buildingEnterLogRepository.save(buildingEnterLog);
redisTemplate.opsForStream().acknowledge(GROUP, msg);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.doubleo.passservice.domain.log.consumer;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class RetainedCountConsumer {

private final RedisTemplate<String, String> redisTemplate;

private static final String STREAM_KEY = "building:enter:stream";
private static final String GROUP = "retained-counter-group";
private static final String CONSUMER = "retained-counter-consumer";

@PostConstruct
public void createGroup() {
try {
redisTemplate.opsForStream().createGroup(STREAM_KEY, ReadOffset.latest(), GROUP);
} catch (Exception e) {
if (!e.getMessage().contains("BUSYGROUP")) {
log.error("Failed to create Redis Stream Group", e);
}
}
}

@Scheduled(fixedDelay = 300000)
public void consume() {
List<MapRecord<String, Object, Object>> records =
redisTemplate
.opsForStream()
.read(
Consumer.from(GROUP, CONSUMER),
StreamReadOptions.empty().count(10).block(Duration.ofSeconds(1)),
StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed()));

if (records == null) return;

for (MapRecord<String, Object, Object> record : records) {
try {
Map<String, String> map =
record.getValue().entrySet().stream()
.collect(
Collectors.toMap(
e -> e.getKey().toString(),
e -> e.getValue().toString()));

String tenantId = map.get("tenantId");
String direction = map.get("direction");
String visitCategory = map.get("visitCategory");
String timestamp = map.get("timestamp");

String date = timestamp.substring(0, 10);
String redisKey =
String.format(
"visit:count:%s:%s:%s:%s",
tenantId, date, visitCategory, direction);

redisTemplate.opsForValue().increment(redisKey);

redisTemplate.expire(redisKey, Duration.ofDays(1));

redisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP, record.getId());

} catch (Exception e) {
log.error("Error processing stream message", e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.doubleo.passservice.domain.log.domain;

import com.doubleo.passservice.domain.common.model.BaseEntity;
import com.doubleo.passservice.domain.pass.enums.VisitCategory;
import jakarta.persistence.*;
import lombok.*;

Expand Down Expand Up @@ -32,20 +33,26 @@ public class BuildingEnterLog extends BaseEntity {
@Column(name = "direction", nullable = false, length = 10)
private Direction direction;

@Column(name = "visit_category")
@Enumerated(EnumType.STRING)
private VisitCategory visitCategory;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4: ๋ฐฉ๋ฌธ์ž ๊ตฌ๋ฌธ์„ ์œ„ํ•œ enum ์ด๋ผ๋ฉด visitorCategory๋„ ๊ดœ์ฐฎ์•„๋ณด์ž…๋‹ˆ๋‹ค!


@Builder(access = AccessLevel.PRIVATE)
private BuildingEnterLog(
String tenantId,
Long buildingId,
Long memberId,
String memberName,
Long passId,
Direction direction) {
Direction direction,
VisitCategory visitCategory) {
this.tenantId = tenantId;
this.buildingId = buildingId;
this.memberId = memberId;
this.memberName = memberName;
this.passId = passId;
this.direction = direction;
this.visitCategory = visitCategory;
}

public static BuildingEnterLog createBuildingEnterLog(
Expand All @@ -54,14 +61,16 @@ public static BuildingEnterLog createBuildingEnterLog(
Long memberId,
String memberName,
Long passId,
Direction direction) {
Direction direction,
VisitCategory visitCategory) {
return BuildingEnterLog.builder()
.tenantId(tenantId)
.buildingId(buildingId)
.memberId(memberId)
.memberName(memberName)
.passId(passId)
.direction(direction)
.visitCategory(visitCategory)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.doubleo.passservice.domain.log.dto.request;

import com.doubleo.passservice.domain.log.domain.Direction;
import com.doubleo.passservice.domain.pass.enums.VisitCategory;

public record CreateBuildingEnterLogRequest(
String tenantId,
Long buildingId,
Long memberId,
String memberName,
Long passId,
Direction direction) {}
Direction direction,
VisitCategory visitCategory) {}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public void sendBuildingEnterLogToStream(CreateBuildingEnterLogRequest request)
message.put("memberName", request.memberName());
message.put("passId", request.passId().toString());
message.put("direction", request.direction().name());
message.put("visitCategory", request.visitCategory().name());
message.put("timestamp", LocalDateTime.now().toString());

redisTemplate.opsForStream().add("building:enter:stream", message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,40 @@ Long countEnteredBetween(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("tenantId") String tenantId);

@Query(
"""
SELECT COUNT(b)
FROM BuildingEnterLog b
JOIN Pass p ON b.passId = p.id
WHERE b.direction = 'IN'
AND b.createdDt >= :start
AND b.createdDt < :end
AND b.tenantId = :tenantId
AND p.visitCategory = :visitCategory
""")
int countEnteredByCategory(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("tenantId") String tenantId,
@Param("visitCategory")
com.doubleo.passservice.domain.pass.enums.VisitCategory visitCategory);

@Query(
"""
SELECT COUNT(b)
FROM BuildingEnterLog b
JOIN Pass p ON b.passId = p.id
WHERE b.direction = 'OUT'
AND b.createdDt >= :start
AND b.createdDt < :end
AND b.tenantId = :tenantId
AND p.visitCategory = :visitCategory
""")
int countExitedByCategory(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("tenantId") String tenantId,
@Param("visitCategory")
com.doubleo.passservice.domain.pass.enums.VisitCategory visitCategory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@

public enum VisitCategory {
PATIENT,
GUARDIAN,
TEMPORARY
GUARDIAN
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ public List<LastWeekCategoryStatsInfoListResponse> lastWeekCategoryStatsListGet(
}

@GetMapping("/building")
public List<LastWeekBuildingStatsInfoListResponse> lastWeekBuilidngStatsListGet() {
public List<LastWeekBuildingStatsInfoListResponse> lastWeekBuildingStatsListGet() {
return statsService.getLastWeekBuildingStats();
}

@GetMapping("/dashboard-summary")
public List<RetainedStatusInfoResponse> currentRetainedStatusGet() {
return statsService.getCurrentRetainedStatus();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.doubleo.passservice.domain.stats.domain;

import com.doubleo.passservice.domain.common.model.BaseEntity;
import com.doubleo.passservice.domain.pass.enums.VisitCategory;
import jakarta.persistence.*;
import java.time.LocalDate;
import lombok.*;

@Entity
@Table(
name = "daily_retained_snapshot",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"snapshot_date", "tenant_id", "visit_category"})
})
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class DailyRetainedSnapshot extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "snapshot_id")
private Long id;

@Column(name = "snapshot_date", nullable = false)
private LocalDate snapshotDate;

@Enumerated(EnumType.STRING)
@Column(name = "visit_category", nullable = false)
private VisitCategory visitCategory;

@Column(name = "retained_count", nullable = false)
private int retainedCount;

public DailyRetainedSnapshot(
String tenantId,
LocalDate snapshotDate,
VisitCategory visitCategory,
int retainedCount) {
this.tenantId = tenantId;
this.snapshotDate = snapshotDate;
this.visitCategory = visitCategory;
this.retainedCount = retainedCount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.doubleo.passservice.domain.stats.dto.response;

import com.doubleo.passservice.domain.pass.enums.VisitCategory;

public record RetainedStatusInfoResponse(
VisitCategory category, int entered, int exited, int remaining) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.doubleo.passservice.domain.stats.repository;

import com.doubleo.passservice.domain.pass.enums.VisitCategory;
import com.doubleo.passservice.domain.stats.domain.DailyRetainedSnapshot;
import java.time.LocalDate;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface DailyRetainedSnapshotRepository
extends JpaRepository<DailyRetainedSnapshot, Long> {
Optional<DailyRetainedSnapshot> findBySnapshotDateAndTenantIdAndVisitCategory(
LocalDate snapshotDate, String tenantId, VisitCategory visitCategory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ public void runMonthlyStatsUpdate() {
statsBatchService.updateMonthlyStats();
log.info("[EntryStatsScheduler] Completed monthly entry stats aggregation");
}

@Scheduled(cron = "0 5 0 * * *")
public void runDailyRetainedSnapshotSave() {
log.info("[EntryStatsScheduler] Starting daily retained snapshot save");
statsBatchService.saveDailyRetainedSnapshot();
log.info("[EntryStatsScheduler] Completed daily retained snapshot save");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public interface StatsBatchService {
void updateWeeklyStats();

void updateMonthlyStats();

void saveDailyRetainedSnapshot();
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.doubleo.passservice.domain.stats.service;

import com.doubleo.passservice.domain.log.repository.BuildingEnterLogRepository;
import com.doubleo.passservice.domain.pass.enums.VisitCategory;
import com.doubleo.passservice.domain.stats.domain.DailyRetainedSnapshot;
import com.doubleo.passservice.domain.stats.domain.EntryStatsDaily;
import com.doubleo.passservice.domain.stats.domain.EntryStatsMonthly;
import com.doubleo.passservice.domain.stats.domain.EntryStatsWeekly;
import com.doubleo.passservice.domain.stats.dto.request.UpdateDailyEntryStatsRequest;
import com.doubleo.passservice.domain.stats.repository.DailyRetainedSnapshotRepository;
import com.doubleo.passservice.domain.stats.repository.EntryStatsDailyRepository;
import com.doubleo.passservice.domain.stats.repository.EntryStatsMonthlyRepository;
import com.doubleo.passservice.domain.stats.repository.EntryStatsWeeklyRepository;
Expand All @@ -28,6 +31,7 @@ public class StatsBatchServiceImpl implements StatsBatchService {
private final EntryStatsDailyRepository entryStatsDailyRepository;
private final EntryStatsWeeklyRepository entryStatsWeeklyRepository;
private final EntryStatsMonthlyRepository entryStatsMonthlyRepository;
private final DailyRetainedSnapshotRepository dailyRetainedSnapshotRepository;
private final TenantValidator tenantValidator;

public void updateDailyStats() {
Expand Down Expand Up @@ -95,4 +99,35 @@ public void updateMonthlyStats() {

entryStatsMonthlyRepository.save(monthly);
}

@Override
public void saveDailyRetainedSnapshot() {
String tenantId = tenantValidator.getTenantId();

LocalDate snapshotDate = LocalDate.now().minusDays(1);
LocalDateTime start = snapshotDate.atStartOfDay();
LocalDateTime end = start.plusDays(1);

for (VisitCategory category : VisitCategory.values()) {
int base =
dailyRetainedSnapshotRepository
.findBySnapshotDateAndTenantIdAndVisitCategory(
snapshotDate.minusDays(1), tenantId, category)
.map(DailyRetainedSnapshot::getRetainedCount)
.orElse(0);

int inCount =
buildingEnterLogRepository.countEnteredByCategory(
start, end, tenantId, category);
int outCount =
buildingEnterLogRepository.countExitedByCategory(
start, end, tenantId, category);
int retained = base + inCount - outCount;

DailyRetainedSnapshot snapshot =
new DailyRetainedSnapshot(tenantId, snapshotDate, category, retained);

dailyRetainedSnapshotRepository.save(snapshot);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface StatsService {
List<LastWeekCategoryStatsInfoListResponse> getLastWeekCategoryStats();

List<LastWeekBuildingStatsInfoListResponse> getLastWeekBuildingStats();

List<RetainedStatusInfoResponse> getCurrentRetainedStatus();
}
Loading