Skip to content

Commit 03cc78d

Browse files
committed
user streak for backend
1 parent 1da953a commit 03cc78d

5 files changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.rk.postguard.controllers;
2+
3+
4+
import com.rk.postguard.dto.StreakResponse;
5+
import com.rk.postguard.service.StreakService;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
@RestController
14+
@RequestMapping("/api/v1/analytics")
15+
@RequiredArgsConstructor
16+
public class StreakController {
17+
private final StreakService streakService;
18+
19+
20+
21+
@GetMapping("/streak")
22+
public ResponseEntity<StreakResponse> getStreak(Authentication authentication){
23+
String username = authentication.getName();
24+
StreakResponse streak = streakService.getStreakStats(username);
25+
return ResponseEntity.ok(streak);
26+
}
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.rk.postguard.dto;
2+
3+
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.LocalDate;
10+
11+
@Data
12+
@Builder
13+
@AllArgsConstructor
14+
@NoArgsConstructor
15+
public class StreakResponse {
16+
private Integer currentStreak;
17+
private Integer longestStreak;
18+
private LocalDate lastActivateDate;
19+
20+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.rk.postguard.entity;
2+
3+
4+
import jakarta.persistence.*;
5+
import lombok.*;
6+
import org.hibernate.annotations.ColumnTransformers;
7+
8+
import java.time.LocalDate;
9+
import java.time.LocalDateTime;
10+
11+
@Entity
12+
@Table(name = "user_streaks")
13+
@Data
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
@Builder
17+
public class UserStreak {
18+
19+
@Id
20+
@GeneratedValue(strategy = GenerationType.IDENTITY)
21+
private Long id;
22+
23+
@Column(name = "user_id",unique = true,nullable = false)
24+
private Long userId;
25+
26+
@Column(name="current_streak",nullable = false)
27+
private Integer currentStreak =0;
28+
29+
@Column(name="longest_streak",nullable = false)
30+
private Integer longestStreak =0;
31+
32+
@Column(name = "last_activity_date")
33+
private LocalDate lastActivityDate;
34+
35+
@Column(name = "created_at")
36+
private LocalDateTime createdAt;
37+
38+
@Column(name = "updated_at")
39+
private LocalDateTime updatedAt;
40+
41+
@PrePersist
42+
protected void onCreate(){
43+
createdAt = LocalDateTime.now();
44+
updatedAt = LocalDateTime.now();
45+
}
46+
@PreUpdate
47+
protected void onUpdate(){
48+
updatedAt = LocalDateTime.now();
49+
}
50+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.rk.postguard.repositories;
2+
3+
import com.rk.postguard.entity.UserStreak;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
import java.util.Optional;
7+
8+
public interface UserStreakRepository extends JpaRepository<UserStreak,Long> {
9+
Optional<UserStreak> findByUserId(Long userId);
10+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.rk.postguard.service;
2+
3+
4+
import com.rk.postguard.dto.StreakResponse;
5+
import com.rk.postguard.entity.PostureEvent;
6+
import com.rk.postguard.entity.User;
7+
import com.rk.postguard.entity.UserStreak;
8+
import com.rk.postguard.repositories.PostureEventRepository;
9+
import com.rk.postguard.repositories.UserRepository;
10+
import com.rk.postguard.repositories.UserStreakRepository;
11+
import jakarta.transaction.Transactional;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.stereotype.Service;
14+
15+
import java.time.LocalDate;
16+
import java.time.LocalDateTime;
17+
import java.util.List;
18+
19+
@Service
20+
@RequiredArgsConstructor
21+
public class StreakService {
22+
23+
private final UserStreakRepository streakRepository;
24+
private final PostureEventRepository postureEventRepository;
25+
private final UserRepository userRepository;
26+
27+
/**
28+
* Calculate if user has good posture for a day
29+
* Definition: 70%+ good posture events
30+
*/
31+
private boolean hasGoodPostureDay(Long userId, LocalDate date){
32+
LocalDateTime startOfDay =date.atStartOfDay();
33+
LocalDateTime endOfDay = date.atTime(23,59,59);
34+
List<PostureEvent> events = postureEventRepository.findByUserIdAndTimestampBetweenOrderByTimestampDesc(userId,startOfDay,endOfDay);
35+
if(events.isEmpty()){
36+
return false;
37+
}
38+
long goodCount = events.stream().
39+
filter(e -> "good".equalsIgnoreCase(e.getPostureState()))
40+
.count();
41+
42+
double goodPercentage = (double) goodCount / events.size();
43+
return goodPercentage >= 0.70;
44+
45+
}
46+
47+
@Transactional
48+
public UserStreak updateStreak(Long userId) {
49+
UserStreak streak = streakRepository.findByUserId(userId)
50+
.orElse(UserStreak.builder()
51+
.userId(userId)
52+
.currentStreak(0)
53+
.longestStreak(0)
54+
.build());
55+
56+
LocalDate today = LocalDate.now();
57+
LocalDate yesterday = today.minusDays(1);
58+
59+
// First time or very old data
60+
if (streak.getLastActivityDate() == null) {
61+
if (hasGoodPostureDay(userId, today)) {
62+
streak.setCurrentStreak(1);
63+
streak.setLongestStreak(1);
64+
streak.setLastActivityDate(today);
65+
}
66+
return streakRepository.save(streak);
67+
}
68+
69+
LocalDate lastDate = streak.getLastActivityDate();
70+
71+
// Already updated today
72+
if (lastDate.equals(today)) {
73+
return streak;
74+
}
75+
76+
// Consecutive day (yesterday was last update)
77+
if (lastDate.equals(yesterday)) {
78+
if (hasGoodPostureDay(userId, today)) {
79+
streak.setCurrentStreak(streak.getCurrentStreak() + 1);
80+
streak.setLastActivityDate(today);
81+
82+
// Update longest streak if needed
83+
if (streak.getCurrentStreak() > streak.getLongestStreak()) {
84+
streak.setLongestStreak(streak.getCurrentStreak());
85+
}
86+
} else {
87+
// Today wasn't a good day - streak breaks
88+
streak.setCurrentStreak(0);
89+
streak.setLastActivityDate(today);
90+
}
91+
}
92+
// Streak was broken (missed days)
93+
else {
94+
if (hasGoodPostureDay(userId, today)) {
95+
streak.setCurrentStreak(1);
96+
streak.setLastActivityDate(today);
97+
} else {
98+
streak.setCurrentStreak(0);
99+
streak.setLastActivityDate(today);
100+
}
101+
}
102+
103+
return streakRepository.save(streak);
104+
}
105+
/**
106+
* Get current streak for user
107+
* */
108+
public int getCurrentStreak(Long userId){
109+
UserStreak streak = updateStreak(userId);
110+
return streak.getCurrentStreak();
111+
}
112+
113+
/**
114+
* Get Streak Stats
115+
* */
116+
public StreakResponse getStreakStats(String username){
117+
User user = userRepository.findByUsername(username)
118+
.orElseThrow(() -> new RuntimeException("User not found"));
119+
120+
UserStreak streak = updateStreak(user.getId());
121+
return StreakResponse.builder()
122+
.currentStreak(streak.getCurrentStreak())
123+
.longestStreak(streak.getLongestStreak())
124+
.lastActivateDate(streak.getLastActivityDate())
125+
.build();
126+
}
127+
128+
}

0 commit comments

Comments
 (0)