mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-03 08:27:35 +08:00
Merge pull request #730 from nagisa77/codex/add-point-system-for-lottery-participation
feat: integrate points with lottery participation
This commit is contained in:
@@ -41,7 +41,8 @@ public class PostController {
|
|||||||
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
||||||
req.getTitle(), req.getContent(), req.getTagIds(),
|
req.getTitle(), req.getContent(), req.getTagIds(),
|
||||||
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
|
||||||
req.getPrizeCount(), req.getStartTime(), req.getEndTime());
|
req.getPrizeCount(), req.getPointCost(),
|
||||||
|
req.getStartTime(), req.getEndTime());
|
||||||
draftService.deleteDraft(auth.getName());
|
draftService.deleteDraft(auth.getName());
|
||||||
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
|
||||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class LotteryDto {
|
|||||||
private String prizeDescription;
|
private String prizeDescription;
|
||||||
private String prizeIcon;
|
private String prizeIcon;
|
||||||
private int prizeCount;
|
private int prizeCount;
|
||||||
|
private int pointCost;
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
private List<AuthorDto> participants;
|
private List<AuthorDto> participants;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public class PostRequest {
|
|||||||
private String prizeDescription;
|
private String prizeDescription;
|
||||||
private String prizeIcon;
|
private String prizeIcon;
|
||||||
private Integer prizeCount;
|
private Integer prizeCount;
|
||||||
|
private Integer pointCost;
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public class PostMapper {
|
|||||||
l.setPrizeDescription(lp.getPrizeDescription());
|
l.setPrizeDescription(lp.getPrizeDescription());
|
||||||
l.setPrizeIcon(lp.getPrizeIcon());
|
l.setPrizeIcon(lp.getPrizeIcon());
|
||||||
l.setPrizeCount(lp.getPrizeCount());
|
l.setPrizeCount(lp.getPrizeCount());
|
||||||
|
l.setPointCost(lp.getPointCost());
|
||||||
l.setStartTime(lp.getStartTime());
|
l.setStartTime(lp.getStartTime());
|
||||||
l.setEndTime(lp.getEndTime());
|
l.setEndTime(lp.getEndTime());
|
||||||
l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
l.setParticipants(lp.getParticipants().stream().map(userMapper::toAuthorDto).collect(Collectors.toList()));
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ public class LotteryPost extends Post {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private int prizeCount;
|
private int prizeCount;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int pointCost;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ public enum PointHistoryType {
|
|||||||
INVITE,
|
INVITE,
|
||||||
FEATURE,
|
FEATURE,
|
||||||
SYSTEM_ONLINE,
|
SYSTEM_ONLINE,
|
||||||
REDEEM
|
REDEEM,
|
||||||
|
LOTTERY_JOIN,
|
||||||
|
LOTTERY_REWARD
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.openisle.service;
|
|||||||
|
|
||||||
import com.openisle.model.*;
|
import com.openisle.model.*;
|
||||||
import com.openisle.repository.*;
|
import com.openisle.repository.*;
|
||||||
|
import com.openisle.exception.FieldException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -39,6 +40,17 @@ public class PointService {
|
|||||||
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
return addPoint(user, 500, PointHistoryType.FEATURE, post, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void processLotteryJoin(User participant, LotteryPost post) {
|
||||||
|
int cost = post.getPointCost();
|
||||||
|
if (cost > 0) {
|
||||||
|
if (participant.getPoint() < cost) {
|
||||||
|
throw new FieldException("point", "积分不足");
|
||||||
|
}
|
||||||
|
addPoint(participant, -cost, PointHistoryType.LOTTERY_JOIN, post, null, post.getAuthor());
|
||||||
|
addPoint(post.getAuthor(), cost, PointHistoryType.LOTTERY_REWARD, post, null, participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PointLog getTodayLog(User user) {
|
private PointLog getTodayLog(User user) {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
return pointLogRepository.findByUserAndLogDate(user, today)
|
return pointLogRepository.findByUserAndLogDate(user, today)
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ public class PostService {
|
|||||||
String prizeDescription,
|
String prizeDescription,
|
||||||
String prizeIcon,
|
String prizeIcon,
|
||||||
Integer prizeCount,
|
Integer prizeCount,
|
||||||
|
Integer pointCost,
|
||||||
LocalDateTime startTime,
|
LocalDateTime startTime,
|
||||||
LocalDateTime endTime) {
|
LocalDateTime endTime) {
|
||||||
long recent = postRepository.countByAuthorAfter(username,
|
long recent = postRepository.countByAuthorAfter(username,
|
||||||
@@ -188,10 +189,14 @@ public class PostService {
|
|||||||
PostType actualType = type != null ? type : PostType.NORMAL;
|
PostType actualType = type != null ? type : PostType.NORMAL;
|
||||||
Post post;
|
Post post;
|
||||||
if (actualType == PostType.LOTTERY) {
|
if (actualType == PostType.LOTTERY) {
|
||||||
|
if (pointCost != null && (pointCost < 0 || pointCost > 100)) {
|
||||||
|
throw new IllegalArgumentException("pointCost must be between 0 and 100");
|
||||||
|
}
|
||||||
LotteryPost lp = new LotteryPost();
|
LotteryPost lp = new LotteryPost();
|
||||||
lp.setPrizeDescription(prizeDescription);
|
lp.setPrizeDescription(prizeDescription);
|
||||||
lp.setPrizeIcon(prizeIcon);
|
lp.setPrizeIcon(prizeIcon);
|
||||||
lp.setPrizeCount(prizeCount != null ? prizeCount : 0);
|
lp.setPrizeCount(prizeCount != null ? prizeCount : 0);
|
||||||
|
lp.setPointCost(pointCost != null ? pointCost : 0);
|
||||||
lp.setStartTime(startTime);
|
lp.setStartTime(startTime);
|
||||||
lp.setEndTime(endTime);
|
lp.setEndTime(endTime);
|
||||||
post = lp;
|
post = lp;
|
||||||
@@ -250,8 +255,10 @@ public class PostService {
|
|||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
|
||||||
User user = userRepository.findByUsername(username)
|
User user = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
|
||||||
post.getParticipants().add(user);
|
if (post.getParticipants().add(user)) {
|
||||||
lotteryPostRepository.save(post);
|
pointService.processLotteryJoin(user, post);
|
||||||
|
lotteryPostRepository.save(post);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE lottery_posts ADD COLUMN point_cost INT NOT NULL DEFAULT 0;
|
||||||
@@ -76,7 +76,7 @@ class PostControllerTest {
|
|||||||
post.setTags(Set.of(tag));
|
post.setTags(Set.of(tag));
|
||||||
|
|
||||||
when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)),
|
when(postService.createPost(eq("alice"), eq(1L), eq("t"), eq("c"), eq(List.of(1L)),
|
||||||
isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post);
|
isNull(), isNull(), isNull(), isNull(), isNull(), isNull(), isNull())).thenReturn(post);
|
||||||
when(postService.viewPost(eq(1L), any())).thenReturn(post);
|
when(postService.viewPost(eq(1L), any())).thenReturn(post);
|
||||||
when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of());
|
when(commentService.getCommentsForPost(eq(1L), any())).thenReturn(List.of());
|
||||||
when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of());
|
when(commentService.getParticipants(anyLong(), anyInt())).thenReturn(List.of());
|
||||||
@@ -187,7 +187,7 @@ class PostControllerTest {
|
|||||||
.andExpect(status().isBadRequest());
|
.andExpect(status().isBadRequest());
|
||||||
|
|
||||||
verify(postService, never()).createPost(any(), any(), any(), any(), any(),
|
verify(postService, never()).createPost(any(), any(), any(), any(), any(),
|
||||||
any(), any(), any(), any(), any(), any());
|
any(), any(), any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class PostServiceTest {
|
|||||||
|
|
||||||
assertThrows(RateLimitException.class,
|
assertThrows(RateLimitException.class,
|
||||||
() -> service.createPost("alice", 1L, "t", "c", List.of(1L),
|
() -> service.createPost("alice", 1L, "t", "c", List.of(1L),
|
||||||
null, null, null, null, null, null));
|
null, null, null, null, null, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -66,6 +66,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="prize-point-row">
|
||||||
|
<span class="prize-row-title">参与所需积分</span>
|
||||||
|
<div class="prize-count-input">
|
||||||
|
<input
|
||||||
|
class="prize-count-input-field"
|
||||||
|
type="number"
|
||||||
|
v-model.number="pointCost"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="prize-time-row">
|
<div class="prize-time-row">
|
||||||
<span class="prize-row-title">抽奖结束时间</span>
|
<span class="prize-row-title">抽奖结束时间</span>
|
||||||
<client-only>
|
<client-only>
|
||||||
@@ -105,6 +117,7 @@ const showPrizeCropper = ref(false)
|
|||||||
const prizeName = ref('')
|
const prizeName = ref('')
|
||||||
const prizeCount = ref(1)
|
const prizeCount = ref(1)
|
||||||
const prizeDescription = ref('')
|
const prizeDescription = ref('')
|
||||||
|
const pointCost = ref(0)
|
||||||
const endTime = ref(null)
|
const endTime = ref(null)
|
||||||
const startTime = ref(null)
|
const startTime = ref(null)
|
||||||
const dateConfig = { enableTime: true, time_24hr: true, dateFormat: 'Y-m-d H:i' }
|
const dateConfig = { enableTime: true, time_24hr: true, dateFormat: 'Y-m-d H:i' }
|
||||||
@@ -133,6 +146,11 @@ watch(prizeCount, (val) => {
|
|||||||
if (!val || val < 1) prizeCount.value = 1
|
if (!val || val < 1) prizeCount.value = 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(pointCost, (val) => {
|
||||||
|
if (val === undefined || val === null || val < 0) pointCost.value = 0
|
||||||
|
if (val > 100) pointCost.value = 100
|
||||||
|
})
|
||||||
|
|
||||||
const loadDraft = async () => {
|
const loadDraft = async () => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (!token) return
|
if (!token) return
|
||||||
@@ -168,6 +186,7 @@ const clearPost = async () => {
|
|||||||
showPrizeCropper.value = false
|
showPrizeCropper.value = false
|
||||||
prizeDescription.value = ''
|
prizeDescription.value = ''
|
||||||
prizeCount.value = 1
|
prizeCount.value = 1
|
||||||
|
pointCost.value = 0
|
||||||
endTime.value = null
|
endTime.value = null
|
||||||
startTime.value = null
|
startTime.value = null
|
||||||
|
|
||||||
@@ -315,6 +334,10 @@ const submitPost = async () => {
|
|||||||
toast.error('请选择抽奖结束时间')
|
toast.error('请选择抽奖结束时间')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (pointCost.value < 0 || pointCost.value > 100) {
|
||||||
|
toast.error('参与积分需在0到100之间')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
@@ -354,6 +377,7 @@ const submitPost = async () => {
|
|||||||
prizeDescription: postType.value === 'LOTTERY' ? prizeDescription.value : undefined,
|
prizeDescription: postType.value === 'LOTTERY' ? prizeDescription.value : undefined,
|
||||||
startTime:
|
startTime:
|
||||||
postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined,
|
postType.value === 'LOTTERY' ? new Date(startTime.value).toISOString() : undefined,
|
||||||
|
pointCost: postType.value === 'LOTTERY' ? pointCost.value : undefined,
|
||||||
// 将时间转换为 UTC+8.5 时区 todo: 需要优化
|
// 将时间转换为 UTC+8.5 时区 todo: 需要优化
|
||||||
endTime:
|
endTime:
|
||||||
postType.value === 'LOTTERY'
|
postType.value === 'LOTTERY'
|
||||||
|
|||||||
@@ -146,6 +146,24 @@
|
|||||||
<template v-else-if="item.type === 'REDEEM'">
|
<template v-else-if="item.type === 'REDEEM'">
|
||||||
兑换商品,消耗 {{ -item.amount }} 积分
|
兑换商品,消耗 {{ -item.amount }} 积分
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="item.type === 'LOTTERY_JOIN'">
|
||||||
|
参与抽奖帖
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
,消耗 {{ -item.amount }} 积分
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'LOTTERY_REWARD'">
|
||||||
|
你的抽奖帖
|
||||||
|
<NuxtLink :to="`/posts/${item.postId}`" class="timeline-link">{{
|
||||||
|
item.postTitle
|
||||||
|
}}</NuxtLink>
|
||||||
|
被
|
||||||
|
<NuxtLink :to="`/users/${item.fromUserId}`" class="timeline-link">{{
|
||||||
|
item.fromUserName
|
||||||
|
}}</NuxtLink>
|
||||||
|
参与,获得 {{ item.amount }} 积分
|
||||||
|
</template>
|
||||||
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
<template v-else-if="item.type === 'SYSTEM_ONLINE'"> 积分历史系统上线 </template>
|
||||||
<i class="fas fa-coins"></i> 你目前的积分是 {{ item.balance }}
|
<i class="fas fa-coins"></i> 你目前的积分是 {{ item.balance }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
class="join-prize-button"
|
class="join-prize-button"
|
||||||
@click="joinLottery"
|
@click="joinLottery"
|
||||||
>
|
>
|
||||||
<div class="join-prize-button-text">参与抽奖</div>
|
<div class="join-prize-button-text">参与抽奖({{ lottery.pointCost }}积分)</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
||||||
<div class="join-prize-button-text">已参与</div>
|
<div class="join-prize-button-text">已参与</div>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
class="join-prize-button"
|
class="join-prize-button"
|
||||||
@click="joinLottery"
|
@click="joinLottery"
|
||||||
>
|
>
|
||||||
<div class="join-prize-button-text">参与抽奖</div>
|
<div class="join-prize-button-text">参与抽奖({{ lottery.pointCost }}积分)</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
<div v-else-if="hasJoined" class="join-prize-button-disabled">
|
||||||
<div class="join-prize-button-text">已参与</div>
|
<div class="join-prize-button-text">已参与</div>
|
||||||
@@ -810,11 +810,12 @@ const joinLottery = async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success('已参与抽奖')
|
toast.success('已参与抽奖')
|
||||||
await refreshPost()
|
await refreshPost()
|
||||||
} else {
|
} else {
|
||||||
toast.error('操作失败')
|
toast.error(data.error || '操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user