Add activity module with milk tea event

This commit is contained in:
Tim
2025-07-28 14:12:28 +08:00
parent bcd34e1019
commit 51a3a7b8f8
12 changed files with 247 additions and 3 deletions

View File

@@ -0,0 +1,24 @@
package com.openisle.config;
import com.openisle.model.Activity;
import com.openisle.model.ActivityType;
import com.openisle.repository.ActivityRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ActivityInitializer implements CommandLineRunner {
private final ActivityRepository activityRepository;
@Override
public void run(String... args) {
if (activityRepository.findByType(ActivityType.MILK_TEA) == null) {
Activity a = new Activity();
a.setTitle("建站送奶茶活动");
a.setType(ActivityType.MILK_TEA);
activityRepository.save(a);
}
}
}

View File

@@ -109,6 +109,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
.requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
@@ -137,8 +138,9 @@ public class SecurityConfig {
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config"));
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
uri.startsWith("/api/activities"));
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);

View File

@@ -0,0 +1,57 @@
package com.openisle.controller;
import com.openisle.model.Activity;
import com.openisle.model.ActivityType;
import com.openisle.model.User;
import com.openisle.service.ActivityService;
import com.openisle.service.UserService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/activities")
@RequiredArgsConstructor
public class ActivityController {
private final ActivityService activityService;
private final UserService userService;
@GetMapping
public List<Activity> list() {
return activityService.list();
}
@GetMapping("/milk-tea")
public MilkTeaInfo milkTea() {
Activity a = activityService.getByType(ActivityType.MILK_TEA);
long count = activityService.countLevel1Users();
if (!a.isEnded() && count > 50) {
activityService.end(a);
}
MilkTeaInfo info = new MilkTeaInfo();
info.setLevel1Count(count);
info.setEnded(a.isEnded());
return info;
}
@PostMapping("/milk-tea/redeem")
public void redeemMilkTea(@RequestBody RedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
Activity a = activityService.getByType(ActivityType.MILK_TEA);
activityService.redeem(a, user, req.getContact());
}
@Data
private static class MilkTeaInfo {
private long level1Count;
private boolean ended;
}
@Data
private static class RedeemRequest {
private String contact;
}
}

View File

@@ -0,0 +1,48 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
/** Generic activity entity. */
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "activities")
public class Activity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private String icon;
@Column(name = "start_time", nullable = false)
@CreationTimestamp
private LocalDateTime startTime;
@Column(name = "end_time")
private LocalDateTime endTime;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ActivityType type = ActivityType.NORMAL;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "activity_participants",
joinColumns = @JoinColumn(name = "activity_id"),
inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> participants = new HashSet<>();
@Column(nullable = false)
private boolean ended = false;
}

View File

@@ -0,0 +1,7 @@
package com.openisle.model;
/** Activity type enumeration. */
public enum ActivityType {
NORMAL,
MILK_TEA
}

View File

@@ -29,5 +29,7 @@ public enum NotificationType {
/** A user you subscribe to created a post or comment */
USER_ACTIVITY,
/** A user requested registration approval */
REGISTER_REQUEST
REGISTER_REQUEST,
/** A user redeemed an activity reward */
ACTIVITY_REDEEM
}

View File

@@ -0,0 +1,8 @@
package com.openisle.repository;
import com.openisle.model.Activity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ActivityRepository extends JpaRepository<Activity, Long> {
Activity findByType(com.openisle.model.ActivityType type);
}

View File

@@ -9,4 +9,5 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
java.util.List<User> findByUsernameContainingIgnoreCase(String keyword);
java.util.List<User> findByRole(com.openisle.model.Role role);
long countByExperienceGreaterThanEqual(int experience);
}

View File

@@ -0,0 +1,49 @@
package com.openisle.service;
import com.openisle.exception.NotFoundException;
import com.openisle.model.*;
import com.openisle.repository.ActivityRepository;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ActivityService {
private final ActivityRepository activityRepository;
private final UserRepository userRepository;
private final LevelService levelService;
private final NotificationService notificationService;
public List<Activity> list() {
return activityRepository.findAll();
}
public Activity getByType(ActivityType type) {
Activity a = activityRepository.findByType(type);
if (a == null) throw new NotFoundException("Activity not found");
return a;
}
public long countLevel1Users() {
int threshold = levelService.nextLevelExp(0);
return userRepository.countByExperienceGreaterThanEqual(threshold);
}
public void end(Activity activity) {
activity.setEnded(true);
activityRepository.save(activity);
}
public void redeem(Activity activity, User user, String contact) {
String content = user.getUsername() + " contact: " + contact;
for (User admin : userRepository.findByRole(Role.ADMIN)) {
notificationService.createNotification(admin, NotificationType.ACTIVITY_REDEEM,
null, null, null, user, null, content);
}
activity.getParticipants().add(user);
activityRepository.save(activity);
}
}