mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 22:21:09 +08:00
Merge pull request #262 from nagisa77/codex/add-activity-module-and-endpoints
Add activity module with milk tea event
This commit is contained in:
@@ -32,6 +32,15 @@
|
||||
<i class="menu-item-icon fas fa-info-circle"></i>
|
||||
<span class="menu-item-text">关于</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
class="menu-item"
|
||||
exact-active-class="selected"
|
||||
to="/activities"
|
||||
@click="handleItemClick"
|
||||
>
|
||||
<i class="menu-item-icon fas fa-gift"></i>
|
||||
<span class="menu-item-text">活动</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="shouldShowStats"
|
||||
class="menu-item"
|
||||
|
||||
@@ -3,6 +3,7 @@ import HomePageView from '../views/HomePageView.vue'
|
||||
import MessagePageView from '../views/MessagePageView.vue'
|
||||
import AboutPageView from '../views/AboutPageView.vue'
|
||||
import SiteStatsPageView from '../views/SiteStatsPageView.vue'
|
||||
import ActivityListPageView from '../views/ActivityListPageView.vue'
|
||||
import PostPageView from '../views/PostPageView.vue'
|
||||
import LoginPageView from '../views/LoginPageView.vue'
|
||||
import SignupPageView from '../views/SignupPageView.vue'
|
||||
@@ -38,6 +39,11 @@ const routes = [
|
||||
name: 'site-stats',
|
||||
component: SiteStatsPageView
|
||||
},
|
||||
{
|
||||
path: '/activities',
|
||||
name: 'activities',
|
||||
component: ActivityListPageView
|
||||
},
|
||||
{
|
||||
path: '/new-post',
|
||||
name: 'new-post',
|
||||
|
||||
31
open-isle-cli/src/views/ActivityListPageView.vue
Normal file
31
open-isle-cli/src/views/ActivityListPageView.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="activity-list-page">
|
||||
<h1>活动列表</h1>
|
||||
<ul>
|
||||
<li v-for="a in activities" :key="a.id">{{ a.title }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { API_BASE_URL } from '../main'
|
||||
|
||||
export default {
|
||||
name: 'ActivityListPageView',
|
||||
data() {
|
||||
return { activities: [] }
|
||||
},
|
||||
async mounted() {
|
||||
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
||||
if (res.ok) {
|
||||
this.activities = await res.json()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-list-page {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
24
src/main/java/com/openisle/config/ActivityInitializer.java
Normal file
24
src/main/java/com/openisle/config/ActivityInitializer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/openisle/model/Activity.java
Normal file
48
src/main/java/com/openisle/model/Activity.java
Normal 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;
|
||||
}
|
||||
7
src/main/java/com/openisle/model/ActivityType.java
Normal file
7
src/main/java/com/openisle/model/ActivityType.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.openisle.model;
|
||||
|
||||
/** Activity type enumeration. */
|
||||
public enum ActivityType {
|
||||
NORMAL,
|
||||
MILK_TEA
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
49
src/main/java/com/openisle/service/ActivityService.java
Normal file
49
src/main/java/com/openisle/service/ActivityService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user