mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-08 11:07:34 +08:00
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>
|
<i class="menu-item-icon fas fa-info-circle"></i>
|
||||||
<span class="menu-item-text">关于</span>
|
<span class="menu-item-text">关于</span>
|
||||||
</router-link>
|
</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
|
<router-link
|
||||||
v-if="shouldShowStats"
|
v-if="shouldShowStats"
|
||||||
class="menu-item"
|
class="menu-item"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import HomePageView from '../views/HomePageView.vue'
|
|||||||
import MessagePageView from '../views/MessagePageView.vue'
|
import MessagePageView from '../views/MessagePageView.vue'
|
||||||
import AboutPageView from '../views/AboutPageView.vue'
|
import AboutPageView from '../views/AboutPageView.vue'
|
||||||
import SiteStatsPageView from '../views/SiteStatsPageView.vue'
|
import SiteStatsPageView from '../views/SiteStatsPageView.vue'
|
||||||
|
import ActivityListPageView from '../views/ActivityListPageView.vue'
|
||||||
import PostPageView from '../views/PostPageView.vue'
|
import PostPageView from '../views/PostPageView.vue'
|
||||||
import LoginPageView from '../views/LoginPageView.vue'
|
import LoginPageView from '../views/LoginPageView.vue'
|
||||||
import SignupPageView from '../views/SignupPageView.vue'
|
import SignupPageView from '../views/SignupPageView.vue'
|
||||||
@@ -38,6 +39,11 @@ const routes = [
|
|||||||
name: 'site-stats',
|
name: 'site-stats',
|
||||||
component: SiteStatsPageView
|
component: SiteStatsPageView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/activities',
|
||||||
|
name: 'activities',
|
||||||
|
component: ActivityListPageView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/new-post',
|
path: '/new-post',
|
||||||
name: '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/search/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/reaction-types").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/categories/**").hasAuthority("ADMIN")
|
||||||
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
|
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
|
||||||
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
||||||
@@ -137,8 +138,9 @@ public class SecurityConfig {
|
|||||||
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
|
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
|
||||||
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
|
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
|
||||||
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
|
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
|
||||||
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
||||||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config"));
|
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
||||||
|
uri.startsWith("/api/activities"));
|
||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
String token = authHeader.substring(7);
|
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 */
|
/** A user you subscribe to created a post or comment */
|
||||||
USER_ACTIVITY,
|
USER_ACTIVITY,
|
||||||
/** A user requested registration approval */
|
/** 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);
|
Optional<User> findByEmail(String email);
|
||||||
java.util.List<User> findByUsernameContainingIgnoreCase(String keyword);
|
java.util.List<User> findByUsernameContainingIgnoreCase(String keyword);
|
||||||
java.util.List<User> findByRole(com.openisle.model.Role role);
|
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