diff --git a/open-isle-cli/src/components/MenuComponent.vue b/open-isle-cli/src/components/MenuComponent.vue index b194a9fab..cfc5650f3 100644 --- a/open-isle-cli/src/components/MenuComponent.vue +++ b/open-isle-cli/src/components/MenuComponent.vue @@ -32,6 +32,15 @@ 关于 + + + 活动 + +
+

活动列表

+ +
+ + + + + diff --git a/src/main/java/com/openisle/config/ActivityInitializer.java b/src/main/java/com/openisle/config/ActivityInitializer.java new file mode 100644 index 000000000..000a800a3 --- /dev/null +++ b/src/main/java/com/openisle/config/ActivityInitializer.java @@ -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); + } + } +} diff --git a/src/main/java/com/openisle/config/SecurityConfig.java b/src/main/java/com/openisle/config/SecurityConfig.java index 30580eed5..44ef64d65 100644 --- a/src/main/java/com/openisle/config/SecurityConfig.java +++ b/src/main/java/com/openisle/config/SecurityConfig.java @@ -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); diff --git a/src/main/java/com/openisle/controller/ActivityController.java b/src/main/java/com/openisle/controller/ActivityController.java new file mode 100644 index 000000000..587b59847 --- /dev/null +++ b/src/main/java/com/openisle/controller/ActivityController.java @@ -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 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; + } +} diff --git a/src/main/java/com/openisle/model/Activity.java b/src/main/java/com/openisle/model/Activity.java new file mode 100644 index 000000000..e84c71c46 --- /dev/null +++ b/src/main/java/com/openisle/model/Activity.java @@ -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 participants = new HashSet<>(); + + @Column(nullable = false) + private boolean ended = false; +} diff --git a/src/main/java/com/openisle/model/ActivityType.java b/src/main/java/com/openisle/model/ActivityType.java new file mode 100644 index 000000000..8bc8504ae --- /dev/null +++ b/src/main/java/com/openisle/model/ActivityType.java @@ -0,0 +1,7 @@ +package com.openisle.model; + +/** Activity type enumeration. */ +public enum ActivityType { + NORMAL, + MILK_TEA +} diff --git a/src/main/java/com/openisle/model/NotificationType.java b/src/main/java/com/openisle/model/NotificationType.java index a08e3a286..fb5672af3 100644 --- a/src/main/java/com/openisle/model/NotificationType.java +++ b/src/main/java/com/openisle/model/NotificationType.java @@ -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 } diff --git a/src/main/java/com/openisle/repository/ActivityRepository.java b/src/main/java/com/openisle/repository/ActivityRepository.java new file mode 100644 index 000000000..9724d00ae --- /dev/null +++ b/src/main/java/com/openisle/repository/ActivityRepository.java @@ -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 findByType(com.openisle.model.ActivityType type); +} diff --git a/src/main/java/com/openisle/repository/UserRepository.java b/src/main/java/com/openisle/repository/UserRepository.java index a4145abb4..ddd25c661 100644 --- a/src/main/java/com/openisle/repository/UserRepository.java +++ b/src/main/java/com/openisle/repository/UserRepository.java @@ -9,4 +9,5 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); java.util.List findByUsernameContainingIgnoreCase(String keyword); java.util.List findByRole(com.openisle.model.Role role); + long countByExperienceGreaterThanEqual(int experience); } diff --git a/src/main/java/com/openisle/service/ActivityService.java b/src/main/java/com/openisle/service/ActivityService.java new file mode 100644 index 000000000..9ee87f891 --- /dev/null +++ b/src/main/java/com/openisle/service/ActivityService.java @@ -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 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); + } +}