mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-31 00:47:30 +08:00
Compare commits
6 Commits
codex/upda
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1076d7256 | ||
|
|
ce94cd7e73 | ||
|
|
90147d6cd9 | ||
|
|
2c187cf2cd | ||
|
|
0b6d4f9709 | ||
|
|
cf3b6d8fc7 |
@@ -6,6 +6,8 @@ import com.openisle.repository.ActivityRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@@ -22,5 +24,16 @@ public class ActivityInitializer implements CommandLineRunner {
|
||||
a.setContent("为了有利于建站推广以及激励发布内容,我们推出了建站送奶茶的活动,前50名达到level 1的用户,可以联系站长获取奶茶/咖啡一杯");
|
||||
activityRepository.save(a);
|
||||
}
|
||||
|
||||
if (activityRepository.findByType(ActivityType.INVITE_POINTS) == null) {
|
||||
Activity a = new Activity();
|
||||
a.setTitle("🎁邀请码送积分活动");
|
||||
a.setType(ActivityType.INVITE_POINTS);
|
||||
a.setIcon("https://img.icons8.com/color/96/gift.png");
|
||||
a.setContent("使用邀请码注册或邀请好友即可获得积分奖励,快来参与吧!");
|
||||
a.setStartTime(LocalDateTime.now());
|
||||
a.setEndTime(LocalDate.of(LocalDate.now().getYear(), 10, 1).atStartOfDay());
|
||||
activityRepository.save(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,8 @@ public class SecurityConfig {
|
||||
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
||||
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
|
||||
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
||||
@@ -151,6 +153,7 @@ public class SecurityConfig {
|
||||
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
||||
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
||||
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
|
||||
uri.startsWith("/api/point-goods") ||
|
||||
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals"));
|
||||
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
|
||||
@@ -3,5 +3,6 @@ package com.openisle.model;
|
||||
/** Activity type enumeration. */
|
||||
public enum ActivityType {
|
||||
NORMAL,
|
||||
MILK_TEA
|
||||
MILK_TEA,
|
||||
INVITE_POINTS
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ public enum NotificationType {
|
||||
REGISTER_REQUEST,
|
||||
/** A user redeemed an activity reward */
|
||||
ACTIVITY_REDEEM,
|
||||
/** A user redeemed a point good */
|
||||
POINT_REDEEM,
|
||||
/** You won a lottery post */
|
||||
LOTTERY_WIN,
|
||||
/** Your lottery post was drawn */
|
||||
|
||||
@@ -141,6 +141,19 @@ public class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notifications for all admins when a user redeems a point good.
|
||||
* Old redeem notifications from the same user are removed first.
|
||||
*/
|
||||
@org.springframework.transaction.annotation.Transactional
|
||||
public void createPointRedeemNotifications(User user, String content) {
|
||||
// notificationRepository.deleteByTypeAndFromUser(NotificationType.POINT_REDEEM, user);
|
||||
for (User admin : userRepository.findByRole(Role.ADMIN)) {
|
||||
createNotification(admin, NotificationType.POINT_REDEEM, null, null,
|
||||
null, user, null, content);
|
||||
}
|
||||
}
|
||||
|
||||
public List<NotificationPreferenceDto> listPreferences(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||
|
||||
@@ -31,7 +31,7 @@ public class PointMallService {
|
||||
}
|
||||
user.setPoint(user.getPoint() - good.getCost());
|
||||
userRepository.save(user);
|
||||
notificationService.createActivityRedeemNotifications(user, good.getName() + ": " + contact);
|
||||
notificationService.createPointRedeemNotifications(user, good.getName() + ": " + contact);
|
||||
return user.getPoint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,30 @@ class NotificationServiceTest {
|
||||
verify(nRepo).save(any(Notification.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPointRedeemNotificationsDeletesOldOnes() {
|
||||
NotificationRepository nRepo = mock(NotificationRepository.class);
|
||||
UserRepository uRepo = mock(UserRepository.class);
|
||||
ReactionRepository rRepo = mock(ReactionRepository.class);
|
||||
EmailSender email = mock(EmailSender.class);
|
||||
PushNotificationService push = mock(PushNotificationService.class);
|
||||
Executor executor = Runnable::run;
|
||||
NotificationService service = new NotificationService(nRepo, uRepo, email, push, rRepo, executor);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "websiteUrl", "https://ex.com");
|
||||
|
||||
User admin = new User();
|
||||
admin.setId(10L);
|
||||
User user = new User();
|
||||
user.setId(20L);
|
||||
|
||||
when(uRepo.findByRole(Role.ADMIN)).thenReturn(List.of(admin));
|
||||
|
||||
service.createPointRedeemNotifications(user, "contact");
|
||||
|
||||
verify(nRepo).deleteByTypeAndFromUser(NotificationType.POINT_REDEEM, user);
|
||||
verify(nRepo).save(any(Notification.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createNotificationSendsEmailForCommentReply() {
|
||||
NotificationRepository nRepo = mock(NotificationRepository.class);
|
||||
|
||||
@@ -130,6 +130,12 @@
|
||||
申请进行奶茶兑换,联系方式是:{{ item.content }}
|
||||
</NotificationContainer>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'POINT_REDEEM' && !item.parentComment">
|
||||
<NotificationContainer :item="item" :markRead="markRead">
|
||||
<span class="notif-user">{{ item.fromUser.username }} </span>
|
||||
申请积分兑换,联系方式是:{{ item.content }}
|
||||
</NotificationContainer>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'REACTION' && item.post && !item.comment">
|
||||
<NotificationContainer :item="item" :markRead="markRead">
|
||||
<span class="notif-user">{{ item.fromUser.username }} </span> 对我的文章
|
||||
@@ -610,6 +616,8 @@ const formatType = (t) => {
|
||||
return '有人申请注册'
|
||||
case 'ACTIVITY_REDEEM':
|
||||
return '有人申请兑换奶茶'
|
||||
case 'POINT_REDEEM':
|
||||
return '有人申请积分兑换'
|
||||
case 'LOTTERY_WIN':
|
||||
return '抽奖中奖了'
|
||||
case 'LOTTERY_DRAW':
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="loading-points-container" v-if="isLoading">
|
||||
<l-hatch size="28" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||
</div>
|
||||
|
||||
<div class="point-info">
|
||||
<p v-if="authState.loggedIn && point !== null">
|
||||
<span><i class="fas fa-coins coin-icon"></i></span>我的积分:<span class="point-value">{{
|
||||
@@ -23,7 +27,13 @@
|
||||
<i class="fas fa-coins"></i>
|
||||
{{ good.cost }} 积分
|
||||
</div>
|
||||
<div class="goods-item-button" @click="openRedeem(good)">兑换</div>
|
||||
<div
|
||||
class="goods-item-button"
|
||||
:class="{ disabled: !authState.loggedIn || point === null || point < good.cost }"
|
||||
@click="openRedeem(good)"
|
||||
>
|
||||
兑换
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<RedeemPopup
|
||||
@@ -46,6 +56,7 @@ const config = useRuntimeConfig()
|
||||
const API_BASE_URL = config.public.apiBaseUrl
|
||||
|
||||
const point = ref(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const pointRules = [
|
||||
'发帖:每天前两次,每次 30 积分',
|
||||
@@ -61,11 +72,13 @@ const loading = ref(false)
|
||||
const selectedGood = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
isLoading.value = true
|
||||
if (authState.loggedIn) {
|
||||
const user = await fetchCurrentUser()
|
||||
point.value = user ? user.point : null
|
||||
}
|
||||
await loadGoods()
|
||||
isLoading.value = false
|
||||
})
|
||||
|
||||
const loadGoods = async () => {
|
||||
@@ -76,6 +89,10 @@ const loadGoods = async () => {
|
||||
}
|
||||
|
||||
const openRedeem = (good) => {
|
||||
if (!authState.loggedIn || point.value === null || point.value < good.cost) {
|
||||
toast.error('积分不足')
|
||||
return
|
||||
}
|
||||
selectedGood.value = good
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -117,6 +134,13 @@ const submitRedeem = async () => {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading-points-container {
|
||||
margin-top: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.point-info {
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -184,6 +208,12 @@ const submitRedeem = async () => {
|
||||
background-color: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.goods-item-button.disabled,
|
||||
.goods-item-button.disabled:hover {
|
||||
background-color: var(--primary-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -22,6 +22,7 @@ const iconMap = {
|
||||
POST_UNSUBSCRIBED: 'fas fa-bookmark',
|
||||
REGISTER_REQUEST: 'fas fa-user-clock',
|
||||
ACTIVITY_REDEEM: 'fas fa-coffee',
|
||||
POINT_REDEEM: 'fas fa-gift',
|
||||
LOTTERY_WIN: 'fas fa-trophy',
|
||||
LOTTERY_DRAW: 'fas fa-bullhorn',
|
||||
MENTION: 'fas fa-at',
|
||||
|
||||
Reference in New Issue
Block a user