Compare commits

..

1 Commits

Author SHA1 Message Date
Tim
5d7ca3d29a feat: use runtime config for API and OAuth client IDs 2025-08-13 16:00:26 +08:00
15 changed files with 32 additions and 204 deletions

View File

@@ -1,29 +0,0 @@
package com.openisle.controller;
import com.openisle.dto.CommentDto;
import com.openisle.mapper.CommentMapper;
import com.openisle.service.CommentService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
/**
* Endpoints for administrators to manage comments.
*/
@RestController
@RequestMapping("/api/admin/comments")
@RequiredArgsConstructor
public class AdminCommentController {
private final CommentService commentService;
private final CommentMapper commentMapper;
@PostMapping("/{id}/pin")
public CommentDto pin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
}
@PostMapping("/{id}/unpin")
public CommentDto unpin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
}
}

View File

@@ -85,16 +85,4 @@ public class CommentController {
commentService.deleteComment(auth.getName(), id); commentService.deleteComment(auth.getName(), id);
log.debug("deleteComment completed for comment {}", id); log.debug("deleteComment completed for comment {}", id);
} }
@PostMapping("/comments/{id}/pin")
public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
}
@PostMapping("/comments/{id}/unpin")
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
}
} }

View File

@@ -13,7 +13,6 @@ public class CommentDto {
private Long id; private Long id;
private String content; private String content;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime pinnedAt;
private AuthorDto author; private AuthorDto author;
private List<CommentDto> replies; private List<CommentDto> replies;
private List<ReactionDto> reactions; private List<ReactionDto> reactions;

View File

@@ -24,7 +24,6 @@ public class CommentMapper {
dto.setId(comment.getId()); dto.setId(comment.getId());
dto.setContent(comment.getContent()); dto.setContent(comment.getContent());
dto.setCreatedAt(comment.getCreatedAt()); dto.setCreatedAt(comment.getCreatedAt());
dto.setPinnedAt(comment.getPinnedAt());
dto.setAuthor(userMapper.toAuthorDto(comment.getAuthor())); dto.setAuthor(userMapper.toAuthorDto(comment.getAuthor()));
dto.setReward(0); dto.setReward(0);
return dto; return dto;

View File

@@ -38,7 +38,4 @@ public class Comment {
@JoinColumn(name = "parent_id") @JoinColumn(name = "parent_id")
private Comment parent; private Comment parent;
@Column
private LocalDateTime pinnedAt;
} }

View File

@@ -32,8 +32,6 @@ public enum NotificationType {
REGISTER_REQUEST, REGISTER_REQUEST,
/** A user redeemed an activity reward */ /** A user redeemed an activity reward */
ACTIVITY_REDEEM, ACTIVITY_REDEEM,
/** You won a lottery post */
LOTTERY_WIN,
/** You were mentioned in a post or comment */ /** You were mentioned in a post or comment */
MENTION MENTION
} }

View File

@@ -23,7 +23,6 @@ import java.util.List;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -130,26 +129,13 @@ public class CommentService {
Post post = postRepository.findById(postId) Post post = postRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
List<Comment> list = commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post); List<Comment> list = commentRepository.findByPostAndParentIsNullOrderByCreatedAtAsc(post);
java.util.List<Comment> pinned = new java.util.ArrayList<>();
java.util.List<Comment> others = new java.util.ArrayList<>();
for (Comment c : list) {
if (c.getPinnedAt() != null) {
pinned.add(c);
} else {
others.add(c);
}
}
pinned.sort(java.util.Comparator.comparing(Comment::getPinnedAt).reversed());
if (sort == CommentSort.NEWEST) { if (sort == CommentSort.NEWEST) {
others.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed()); list.sort(java.util.Comparator.comparing(Comment::getCreatedAt).reversed());
} else if (sort == CommentSort.MOST_INTERACTIONS) { } else if (sort == CommentSort.MOST_INTERACTIONS) {
others.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a))); list.sort((a, b) -> Integer.compare(interactionCount(b), interactionCount(a)));
} }
java.util.List<Comment> result = new java.util.ArrayList<>(); log.debug("getCommentsForPost returning {} comments", list.size());
result.addAll(pinned); return list;
result.addAll(others);
log.debug("getCommentsForPost returning {} comments", result.size());
return result;
} }
public List<Comment> getReplies(Long parentId) { public List<Comment> getReplies(Long parentId) {
@@ -237,32 +223,6 @@ public class CommentService {
log.debug("deleteCommentCascade removed comment {}", comment.getId()); log.debug("deleteCommentCascade removed comment {}", comment.getId());
} }
@Transactional
public Comment pinComment(String username, Long id) {
Comment c = commentRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized");
}
c.setPinnedAt(LocalDateTime.now());
return commentRepository.save(c);
}
@Transactional
public Comment unpinComment(String username, Long id) {
Comment c = commentRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Comment not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
if (!user.getId().equals(c.getPost().getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized");
}
c.setPinnedAt(null);
return commentRepository.save(c);
}
private int interactionCount(Comment comment) { private int interactionCount(Comment comment) {
int reactions = reactionRepository.findByComment(comment).size(); int reactions = reactionRepository.findByComment(comment).size();
int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size(); int replies = commentRepository.findByParentOrderByCreatedAtAsc(comment).size();

View File

@@ -69,8 +69,6 @@ public class PostService {
private final EmailSender emailSender; private final EmailSender emailSender;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>(); private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
@Value("${app.website-url:https://www.open-isle.com}")
private String websiteUrl;
@org.springframework.beans.factory.annotation.Autowired @org.springframework.beans.factory.annotation.Autowired
public PostService(PostRepository postRepository, public PostService(PostRepository postRepository,
@@ -251,8 +249,6 @@ public class PostService {
if (w.getEmail() != null) { if (w.getEmail() != null) {
emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖"); emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖");
} }
notificationService.createNotification(w, NotificationType.LOTTERY_WIN, lp, null, null, lp.getAuthor(), null, null);
notificationService.sendCustomPush(w, "你中奖了", String.format("%s/posts/%d", websiteUrl, lp.getId()));
} }
}); });
} }

View File

@@ -1 +0,0 @@
ALTER TABLE comments ADD COLUMN pinned_at DATETIME(6) NULL;

5
frontend_nuxt/.env Normal file
View File

@@ -0,0 +1,5 @@
NUXT_PUBLIC_API_BASE_URL=https://www.open-isle.com
NUXT_PUBLIC_GOOGLE_CLIENT_ID=777830451304-xxx.apps.googleusercontent.com
NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ
NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779
NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ

View File

@@ -22,7 +22,6 @@
:to="`/users/${comment.userId}?tab=achievements`" :to="`/users/${comment.userId}?tab=achievements`"
>{{ getMedalTitle(comment.medal) }}</router-link >{{ getMedalTitle(comment.medal) }}</router-link
> >
<i v-if="comment.pinned" class="fas fa-thumbtack pin-icon"></i>
<span v-if="level >= 2"> <span v-if="level >= 2">
<i class="fas fa-reply reply-icon"></i> <i class="fas fa-reply reply-icon"></i>
<span class="user-name reply-user-name">{{ comment.parentUserName }}</span> <span class="user-name reply-user-name">{{ comment.parentUserName }}</span>
@@ -75,7 +74,6 @@
:comment="item" :comment="item"
:level="level + 1" :level="level + 1"
:default-show-replies="item.openReplies" :default-show-replies="item.openReplies"
:post-author-id="postAuthorId"
/> />
</template> </template>
</BaseTimeline> </BaseTimeline>
@@ -121,10 +119,6 @@ const CommentItem = {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
postAuthorId: {
type: [Number, String],
required: true,
},
}, },
setup(props, { emit }) { setup(props, { emit }) {
const router = useRouter() const router = useRouter()
@@ -177,22 +171,12 @@ const CommentItem = {
}) })
const isAuthor = computed(() => authState.username === props.comment.userName) const isAuthor = computed(() => authState.username === props.comment.userName)
const isPostAuthor = computed(() => Number(authState.userId) === Number(props.postAuthorId))
const isAdmin = computed(() => authState.role === 'ADMIN') const isAdmin = computed(() => authState.role === 'ADMIN')
const commentMenuItems = computed(() => { const commentMenuItems = computed(() =>
const items = [] isAuthor.value || isAdmin.value
if (isAuthor.value || isAdmin.value) { ? [{ text: '删除评论', color: 'red', onClick: () => deleteComment() }]
items.push({ text: '删除评论', color: 'red', onClick: () => deleteComment() }) : [],
} )
if (isAdmin.value || isPostAuthor.value) {
if (props.comment.pinned) {
items.push({ text: '取消置顶', onClick: () => unpinComment() })
} else {
items.push({ text: '置顶', onClick: () => pinComment() })
}
}
return items
})
const deleteComment = async () => { const deleteComment = async () => {
const token = getToken() const token = getToken()
if (!token) { if (!token) {
@@ -212,46 +196,6 @@ const CommentItem = {
toast.error('操作失败') toast.error('操作失败')
} }
} }
const pinComment = async () => {
const token = getToken()
if (!token) {
toast.error('请先登录')
return
}
const url = isAdmin.value
? `${API_BASE_URL}/api/admin/comments/${props.comment.id}/pin`
: `${API_BASE_URL}/api/comments/${props.comment.id}/pin`
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
if (res.ok) {
props.comment.pinned = true
toast.success('已置顶')
} else {
toast.error('操作失败')
}
}
const unpinComment = async () => {
const token = getToken()
if (!token) {
toast.error('请先登录')
return
}
const url = isAdmin.value
? `${API_BASE_URL}/api/admin/comments/${props.comment.id}/unpin`
: `${API_BASE_URL}/api/comments/${props.comment.id}/unpin`
const res = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
if (res.ok) {
props.comment.pinned = false
toast.success('已取消置顶')
} else {
toast.error('操作失败')
}
}
const submitReply = async (parentUserName, text, clear) => { const submitReply = async (parentUserName, text, clear) => {
if (!text.trim()) return if (!text.trim()) return
isWaitingForReply.value = true isWaitingForReply.value = true
@@ -340,9 +284,6 @@ const CommentItem = {
isWaitingForReply, isWaitingForReply,
commentMenuItems, commentMenuItems,
deleteComment, deleteComment,
pinComment,
unpinComment,
isPostAuthor,
lightboxVisible, lightboxVisible,
lightboxIndex, lightboxIndex,
lightboxImgs, lightboxImgs,
@@ -429,12 +370,6 @@ export default CommentItem
margin-left: 10px; margin-left: 10px;
} }
.pin-icon {
font-size: 12px;
margin-left: 10px;
opacity: 0.6;
}
@keyframes highlight { @keyframes highlight {
from { from {
background-color: yellow; background-color: yellow;

View File

@@ -1,11 +1,12 @@
export const API_BASE_URL = 'https://www.open-isle.com' import { useRuntimeConfig } from '#app'
// export const API_BASE_URL = 'http://127.0.0.1:8081'
// export const API_BASE_URL = 'http://30.211.97.238:8081' const config = useRuntimeConfig()
export const GOOGLE_CLIENT_ID =
'777830451304-nt8afkkap18gui4f9entcha99unal744.apps.googleusercontent.com' export const API_BASE_URL = config.public.apiBaseUrl
export const GITHUB_CLIENT_ID = 'Ov23liVkO1NPAX5JyWxJ' export const GOOGLE_CLIENT_ID = config.public.googleClientId
export const DISCORD_CLIENT_ID = '1394985417044000779' export const GITHUB_CLIENT_ID = config.public.githubClientId
export const TWITTER_CLIENT_ID = 'ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ' export const DISCORD_CLIENT_ID = config.public.discordClientId
export const TWITTER_CLIENT_ID = config.public.twitterClientId
// 重新导出 toast 功能,使用 composable 方式 // 重新导出 toast 功能,使用 composable 方式
export { toast } from './composables/useToast' export { toast } from './composables/useToast'

View File

@@ -2,6 +2,15 @@ import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({ export default defineNuxtConfig({
ssr: true, ssr: true,
runtimeConfig: {
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || '',
googleClientId: process.env.NUXT_PUBLIC_GOOGLE_CLIENT_ID || '',
githubClientId: process.env.NUXT_PUBLIC_GITHUB_CLIENT_ID || '',
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
},
},
// Ensure Vditor styles load before our overrides in global.css // Ensure Vditor styles load before our overrides in global.css
css: ['vditor/dist/index.css', '~/assets/global.css'], css: ['vditor/dist/index.css', '~/assets/global.css'],
app: { app: {

View File

@@ -185,19 +185,6 @@
</router-link> </router-link>
</NotificationContainer> </NotificationContainer>
</template> </template>
<template v-else-if="item.type === 'LOTTERY_WIN'">
<NotificationContainer :item="item" :markRead="markRead">
恭喜你在抽奖贴
<router-link
class="notif-content-text"
@click="markRead(item.id)"
:to="`/posts/${item.post.id}`"
>
{{ stripMarkdownLength(item.post.title, 100) }}
</router-link>
中获奖
</NotificationContainer>
</template>
<template v-else-if="item.type === 'POST_UPDATED'"> <template v-else-if="item.type === 'POST_UPDATED'">
<NotificationContainer :item="item" :markRead="markRead"> <NotificationContainer :item="item" :markRead="markRead">
您关注的帖子 您关注的帖子
@@ -578,7 +565,6 @@ export default {
POST_UNSUBSCRIBED: 'fas fa-bookmark', POST_UNSUBSCRIBED: 'fas fa-bookmark',
REGISTER_REQUEST: 'fas fa-user-clock', REGISTER_REQUEST: 'fas fa-user-clock',
ACTIVITY_REDEEM: 'fas fa-coffee', ACTIVITY_REDEEM: 'fas fa-coffee',
LOTTERY_WIN: 'fas fa-trophy',
MENTION: 'fas fa-at', MENTION: 'fas fa-at',
} }
@@ -636,17 +622,6 @@ export default {
} }
}, },
}) })
} else if (n.type === 'LOTTERY_WIN') {
notifications.value.push({
...n,
icon: iconMap[n.type],
iconClick: () => {
if (n.post) {
markRead(n.id)
router.push(`/posts/${n.post.id}`)
}
},
})
} else if (n.type === 'POST_UPDATED') { } else if (n.type === 'POST_UPDATED') {
notifications.value.push({ notifications.value.push({
...n, ...n,
@@ -816,8 +791,6 @@ export default {
return '有人申请注册' return '有人申请注册'
case 'ACTIVITY_REDEEM': case 'ACTIVITY_REDEEM':
return '有人申请兑换奶茶' return '有人申请兑换奶茶'
case 'LOTTERY_WIN':
return '抽奖中奖了'
default: default:
return t return t
} }

View File

@@ -195,7 +195,6 @@
:comment="item" :comment="item"
:level="0" :level="0"
:default-show-replies="item.openReplies" :default-show-replies="item.openReplies"
:post-author-id="author.id"
@deleted="onCommentDeleted" @deleted="onCommentDeleted"
/> />
</template> </template>
@@ -406,7 +405,6 @@ export default {
avatar: c.author.avatar, avatar: c.author.avatar,
text: c.content, text: c.content,
reactions: c.reactions || [], reactions: c.reactions || [],
pinned: !!c.pinnedAt,
reply: (c.replies || []).map((r) => mapComment(r, c.author.username, level + 1)), reply: (c.replies || []).map((r) => mapComment(r, c.author.username, level + 1)),
openReplies: level === 0, openReplies: level === 0,
src: c.author.avatar, src: c.author.avatar,