Compare commits

...

717 Commits

Author SHA1 Message Date
Tim
fa2ffaa64a fix: viditor样式失效 #586 2025-08-18 11:27:18 +08:00
Tim
3037c856d0 fix: viditor样式失效 #586 2025-08-18 11:27:13 +08:00
Tim
239f1f8c84 Merge pull request #617 from CH-122/fix/mobile-invite-ui
fix: 优化邀请链接组件样式,增加文本换行支持;调整积分商城页面内边距
2025-08-18 10:55:40 +08:00
CH-122
ac303184c4 fix: 优化邀请链接组件样式,增加文本换行支持;调整积分商城页面内边距 2025-08-18 10:32:55 +08:00
Tim
7f16bbdb94 Merge pull request #607 from nagisa77/feature/coin_store
支持积分商城 & 邀请码
2025-08-18 02:20:59 +08:00
tim
f1c83b0f68 fix: 更新提示 2025-08-18 02:19:43 +08:00
tim
22c2b1564d feat: ui 优化+弹窗 2025-08-18 02:18:04 +08:00
tim
628d28c12d feat: 注册流程重构 2025-08-18 02:06:48 +08:00
Tim
2577992ee3 Merge pull request #613 from nagisa77/codex/implement-invitation-link-functionality
feat: add invite link generation and copy
2025-08-18 01:24:05 +08:00
Tim
5b837c9d7f feat: add invite link generation and copy 2025-08-18 01:23:33 +08:00
tim
017ad5bf54 feat: invite ui 2025-08-18 01:15:46 +08:00
Tim
f076b70e9b Merge pull request #612 from nagisa77/codex/add-invitejwt-for-generating-invitation-tokens
feat: add invite token support
2025-08-18 01:11:33 +08:00
Tim
62d12ad2a7 feat: track oauth new-user result 2025-08-18 01:11:16 +08:00
tim
923854bbc6 feat: 适配透传invite_code 2025-08-17 21:56:14 +08:00
tim
9ca5d7b167 feat: 各种登录方式传入invite_token 2025-08-17 12:45:58 +08:00
tim
9c3e1d17f0 Merge remote-tracking branch 'origin/main' into feature/coin_store 2025-08-17 12:09:26 +08:00
tim
7906062945 fix: 添加缺失route 2025-08-17 12:08:18 +08:00
tim
785c36d339 feat: 新增邀请页面ui 2025-08-17 11:51:16 +08:00
Tim
197cbca99c Merge pull request #609 from nagisa77/codex/add-invitation-code-points-event-3vhg3b
Add invite points activity
2025-08-17 11:38:34 +08:00
Tim
b1076d7256 Add invite points activity 2025-08-17 11:38:09 +08:00
tim
ce94cd7e73 feat: 积分禁止删除 2025-08-17 02:31:23 +08:00
Tim
90147d6cd9 Merge pull request #606 from nagisa77/codex/add-new-notification-type-for-points-exchange
feat: add point redeem notification type
2025-08-17 02:27:33 +08:00
Tim
2c187cf2cd feat: add point redeem notification 2025-08-17 02:27:19 +08:00
tim
0b6d4f9709 feat: 积分页面不足展示 2025-08-17 02:19:21 +08:00
Tim
cf3b6d8fc7 Merge pull request #605 from nagisa77/codex/update-points-mall-functionality
feat: add point mall redemption
2025-08-17 02:07:02 +08:00
Tim
8d98c876d2 feat: add point mall redemption 2025-08-17 02:06:47 +08:00
tim
df4df1933a feat: 积分页面ui 2025-08-17 01:57:42 +08:00
Tim
7507f1bb03 Merge pull request #604 from nagisa77/codex/add-hni7s1
feat: add point rules and products to points mall
2025-08-17 01:32:59 +08:00
Tim
9b4c36c76a feat: add point rules and products 2025-08-17 01:32:26 +08:00
Tim
edfc81aeb0 Merge pull request #603 from nagisa77/codex/add
feat: add point mall module
2025-08-17 01:24:06 +08:00
Tim
7bd1225b27 feat: add point mall module 2025-08-17 01:23:47 +08:00
Tim
2dd56e27af Merge pull request #599 from nagisa77/feature/daily_bugfix_0816
Feature/daily bugfix 0816
2025-08-17 01:13:39 +08:00
tim
c3ecef3609 feat: tooltip修改 2025-08-17 01:06:21 +08:00
Tim
efc74d0f77 Merge pull request #601 from nagisa77/codex/save-user-tab-selection-in-localstorage-4dcpd4
feat: remember home tab selection
2025-08-16 18:13:49 +08:00
Tim
f27cb5c703 feat: remember home tab selection 2025-08-16 18:13:37 +08:00
tim
a756c2fab3 feat: add 毛玻璃效果 + 开关 2025-08-16 18:11:56 +08:00
Tim
4e2171a8a6 Merge pull request #600 from nagisa77/codex/add-switch-for-frosted-glass-effect
Add frosted glass effect toggle
2025-08-16 17:58:01 +08:00
Tim
bcbdff8768 feat: initialize frosted glass setting 2025-08-16 17:57:42 +08:00
Tim
b976a1f46f Merge pull request #598 from nagisa77/codex/add-sub-tabs-to-personal-homepage-timeline
feat: add timeline filters on profile page
2025-08-16 16:21:57 +08:00
Tim
b9fd9711de feat: add timeline filters on profile page 2025-08-16 16:21:45 +08:00
tim
642a527dcf Revert "feat: persist home tab selection"
This reverts commit 2c5462cd97.
2025-08-16 16:20:52 +08:00
Tim
88afcc5a8e Merge pull request #597 from nagisa77/codex/save-user-tab-selection-in-localstorage-9dskt8
feat: persist home tab selection
2025-08-16 16:19:58 +08:00
Tim
2c5462cd97 feat: persist home tab selection 2025-08-16 16:19:44 +08:00
tim
2f29946b11 Revert "feat: remember selected tab"
This reverts commit 2322b2da15.
2025-08-16 16:19:23 +08:00
Tim
e27aa34cfd Merge pull request #596 from nagisa77/codex/save-user-tab-selection-in-localstorage
feat: persist home tab selection
2025-08-16 16:10:49 +08:00
Tim
2322b2da15 feat: remember selected tab 2025-08-16 16:10:37 +08:00
tim
79261054f9 feat: ci & cd 2025-08-16 15:24:32 +08:00
tim
86633e1f21 feat: ci & cd 2025-08-16 15:23:54 +08:00
tim
784598a6f0 feat: ci & cd 2025-08-16 15:23:05 +08:00
tim
fdad0e5d34 feat: cd & cd 2025-08-16 15:21:53 +08:00
tim
ebf63c4072 feat: test commit 2025-08-16 15:20:46 +08:00
tim
354d6bdaf9 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-16 15:19:21 +08:00
tim
d9aebdebdc feat: 预发环境 2025-08-16 15:19:10 +08:00
Tim
d6f6495b35 Merge pull request #595 from AnNingUI/main
fix: 修复我的信息界面中的header无法粘性布局的bug以及解决了一些-webkit样式警告
2025-08-16 15:02:48 +08:00
AnNingUI
300f8705ef fix: 修复我的信息界面中的header无法粘性布局的bug以及解决了一些-webkit样式警告
fixed: #588
2025-08-16 13:49:24 +08:00
tim
1f74a29dce fix: 修复header 显示异常 2025-08-16 11:34:03 +08:00
Tim
27ef792b11 Merge pull request #594 from immortal521/feat/user-menu-animation
feat: add transition effects for page and dropdown
2025-08-16 11:25:45 +08:00
Tim
8dd2d59617 Merge pull request #593 from immortal521/fix/mobile-theme-toggle-position
fix: incorrect animation start position on mobile theme toggle
2025-08-16 11:25:06 +08:00
Tim
077ba448d7 Merge pull request #592 from immortal521/fix/unlogin-cant-change-theme
fix: allow theme toggle without requiring user login
2025-08-16 11:22:36 +08:00
immortal521
9ce85f2769 fix: fix incorrect animation start position on mobile theme toggle
- Unified coordinate handling for mouse and touch events to ensure the
animation start point accurately follows the finger position on mobile
devices.
2025-08-16 01:45:57 +08:00
immortal521
f5557cbf08 feat: add transition effects for page and dropdown
- Add page transition CSS with opacity and blur effects

- Wrap dropdown in Transition component with slide effect

- Configure Nuxt pageTransition in config
2025-08-16 01:22:56 +08:00
immortal521
e042c499e1 fix: allow theme toggle without requiring user login 2025-08-16 01:11:30 +08:00
tim
e01afb168c Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-16 01:11:14 +08:00
tim
c1d81eb1d1 fix: update staging base url 2025-08-16 01:11:02 +08:00
Tim
2b0b429866 Merge pull request #589 from AnNingUI/main
fix: 添加对非startViewTransition支持的浏览器添加一个回退的主题切换动画
2025-08-16 00:34:08 +08:00
AnNingUI
8ea85d78ee fix: 解决menu-background-color变量被firefox的userChrome.css覆盖问题 2025-08-15 22:54:49 +08:00
AnNingUI
3b506fe8a8 Merge branch 'main' of github.com:AnNingUI/OpenIsle 2025-08-15 22:25:40 +08:00
AnNingUI
3cc7a4c01a fix: 添加对非startViewTransition支持的浏览器添加一个回退的主题切换动画
fixes: #583
2025-08-15 22:25:02 +08:00
tim
2e749a5672 fix: update 后端端口 2025-08-15 18:16:10 +08:00
tim
7d553d7750 fix: add staging example file 2025-08-15 17:50:20 +08:00
Tim
16105cef54 Merge pull request #584 from CH-122/fix/mobile-theme-mode
fix: 手机状态栏暗黑模式背景颜色显示不正确
2025-08-15 15:48:09 +08:00
CH-122
2b824d94f2 fix: 更新新增帖子图标类名并调整样式作用域 2025-08-15 15:47:24 +08:00
CH-122
00d3c563e2 feat: 移动端 header 中添加主题切换图标, 菜单中隐藏 2025-08-15 15:36:25 +08:00
CH-122
b26891261c fix: 适配 ios safari 浏览器暗黑模式 2025-08-15 15:23:49 +08:00
CH-122
c1d19b854b fix: 手机状态栏暗黑模式背景颜色显示不正确 2025-08-15 14:52:30 +08:00
Tim
72e7ccf262 Merge pull request #581 from immortal521/feat/theme-toggle-transition
feat: implement theme transition animations and dark mode improvements
2025-08-15 13:27:44 +08:00
tim
84ca6fd28c feat: add refresh home 2025-08-15 13:24:00 +08:00
immortal521
d1c148c5c4 Merge branch 'main' into feat/theme-toggle-transition 2025-08-15 13:20:37 +08:00
immortal521
ef58630dae feat: implement theme transition animations and dark mode improvements
- Add view transition API for theme switching

- Update cycleTheme to handle animation circle

- Refactor CSS with consistent quoting and indentation

- Improve theme variable handling and no-op optimizations

- Pass event to cycleTheme in MenuComponent
2025-08-15 13:12:27 +08:00
Tim
f025e82e7c Merge pull request #580 from nagisa77/codex/resolve-chunk-size-warning-issue
chore: split large vite chunks
2025-08-15 13:11:00 +08:00
Tim
4380a988f7 chore: split large vite chunks 2025-08-15 13:10:47 +08:00
tim
2899f7af48 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-15 13:04:48 +08:00
tim
d4b05256a3 fix: update package-lock 2025-08-15 13:03:43 +08:00
Tim
57a26e375d Merge pull request #579 from palmcivet/docs/update-readme
feat: 更新 README “开发”章节
2025-08-15 12:57:05 +08:00
Palm Civet
8a202c4fba feat: 更新 README 2025-08-15 12:39:51 +08:00
Tim
089b2a3f5f Merge pull request #578 from AnNingUI/main
feat: Add Messages Update
2025-08-15 12:26:40 +08:00
AnNingUI
0b3d7a21d5 fix: 迁移markAllRead函数 2025-08-15 11:59:29 +08:00
AnNingUI
fe8a705a28 Merge branch 'main' of github.com:AnNingUI/OpenIsle 2025-08-15 11:44:19 +08:00
AnNingUI
974c7ba83e feat: Add Message Update 2025-08-15 11:42:39 +08:00
Tim
f2937d735d Merge pull request #576 from nagisa77/feature/ui_fix_v0
fix: 移动端才显示
2025-08-15 11:40:21 +08:00
Tim
423248c574 fix: 移动端才显示 2025-08-15 11:39:47 +08:00
Tim
5126cfda8c Merge pull request #575 from nagisa77/feature/ui_fix_v0
fix: 仅仅在主页显示
2025-08-15 11:38:07 +08:00
Tim
e009875797 fix: 仅仅在主页显示 2025-08-15 11:37:30 +08:00
Tim
04ff17f796 Merge pull request #574 from nagisa77/feature/ui_fix_v0
fix: ui fix
2025-08-15 11:25:45 +08:00
Tim
e9c9fbd742 fix: ui fix 2025-08-15 11:24:01 +08:00
Tim
b385945c2d Merge pull request #572 from CH-122/refactor/ui
refactor: 在 header 组件中添加发帖功能,移动端添加发帖悬浮按钮,优化首页搜索标题样式 ,
2025-08-15 11:16:31 +08:00
CH-122
24cbed2eda feat: 移动端添加发帖悬浮按钮 2025-08-15 10:59:29 +08:00
CH-122
ba073b71a6 feat: 在头部组件和菜单组件中添加发帖功能,并优化首页搜索标题样式 2025-08-15 10:37:51 +08:00
CH-122
5ff098ea21 feat: 添加 Tooltip 组件 2025-08-15 10:31:53 +08:00
Tim
f6713b956e Merge pull request #569 from immortal521/fix/564-theme-toggle-btn-position 2025-08-15 09:27:55 +08:00
Tim
b8ea12646f Merge pull request #568 from immortal521/fix/about-page-link-color-#566 2025-08-15 09:27:14 +08:00
immortal521
e573e54c2b fix: correct theme toggle button position (#564) 2025-08-15 03:00:57 +08:00
immortal521
8ec005d392 fix(about): fix link color issue on about page (#566)
Questions:
- Why are markdown styles split into `about-content` and
`info-content-text`?
- Why is `about-content` defined both globally and inside the Vue
component?
2025-08-15 02:42:04 +08:00
tim
b1f92f61a6 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-15 01:37:01 +08:00
tim
824b4dd8aa feat: ui update 2025-08-15 01:36:50 +08:00
Tim
6b08db7e58 Merge pull request #565 from nagisa77/feature/daily_bugfix_0814
fix: revert vditor change
2025-08-15 00:51:09 +08:00
tim
6f3830b3f7 fix: revert vditor change 2025-08-15 00:50:44 +08:00
Tim
d70dad723f Merge pull request #563 from nagisa77/feature/daily_bugfix_0814
若干问题修复,见评论
2025-08-15 00:31:46 +08:00
tim
2cf89e4802 fix: ssr 水合采用useAsyncData 2025-08-15 00:12:06 +08:00
tim
1fc6460ae0 fix: 修复vditor移动端贴顶的问题 2025-08-15 00:01:18 +08:00
Tim
a04e5c2f6f Merge pull request #560 from CH-122/feat/password-recovery-hint
feat: 忘记密码页面添加提示 & 修复缺少定义导致的报错 #535
2025-08-14 23:43:26 +08:00
Tim
77b26937f5 Merge pull request #562 from CH-122/fix/mobile-header-search
fix: 移动端 header 点击搜索图标功能异常
2025-08-14 23:39:19 +08:00
Tim
a1134b9d4b Merge pull request #559 from AnNingUI/main 2025-08-14 21:42:32 +08:00
AnNingUI
600f6ac1d1 fix: 修复代码高亮背景与抽奖背景色公用的问题 2025-08-14 21:39:39 +08:00
CH_122
9ad50b35c9 fix: 移动端 header 点击搜索图标功能异常 2025-08-14 21:35:57 +08:00
CH_122
867ee3907b feat: 忘记密码添加提示 & 修复缺少定义导致的报错 2025-08-14 21:21:34 +08:00
CH_122
58fcd42745 style: add cursor pointer to dropdown items for better UX 2025-08-14 21:20:23 +08:00
AnNingUI
0ee62a3a04 fix: 让代码展示背景的样式更加现代化,修复分类选择框仅有一个当前分类的问题
Fixes #558
2025-08-14 21:05:08 +08:00
Tim
f0bc7a22a0 fix: google login 问题修复 2025-08-14 20:34:21 +08:00
Tim
f6c0c8e226 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-14 20:25:33 +08:00
Tim
8f3c0d6710 fix: google login 问题修复 2025-08-14 20:25:09 +08:00
Tim
4f738778db Merge pull request #557 from nagisa77/feature/code_buauty
fix: 代码风格设置
2025-08-14 20:17:23 +08:00
Tim
84b45f785d fix: 代码风格设置 2025-08-14 19:55:53 +08:00
tim
df56d7e885 Revert "optimize(backend): optimize /api/posts/latest-reply"
This reverts commit 1e87e9252d.
2025-08-14 18:54:12 +08:00
tim
76176e135c Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-14 18:27:25 +08:00
tim
ab87e0e51c fix: fix missing setup 2025-08-14 18:27:12 +08:00
Tim
5346a063bf Merge pull request #555 from netcaty/main
优化主页列表接口/api/posts/latest-reply
2025-08-14 18:19:19 +08:00
netcaty
e53f2130b8 Merge branch 'nagisa77:main' into main 2025-08-14 17:54:08 +08:00
netcat
1e87e9252d optimize(backend): optimize /api/posts/latest-reply
resolves #554
2025-08-14 17:53:01 +08:00
tim
3fc4d29dce Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-14 17:27:42 +08:00
tim
bcdac9d9b2 fix: delete hook update 2025-08-14 17:27:30 +08:00
Tim
ea9710d16f Merge pull request #553 from nagisa77/codex/fix-missing-comment-pinning-feature
fix: restore comment pin handling
2025-08-14 17:21:26 +08:00
Tim
47134cadc2 fix: handle pinned comments from backend 2025-08-14 17:21:08 +08:00
tim
1a1b20b9cf fix: update css import 2025-08-14 17:20:02 +08:00
Tim
b63ebb8fae Merge pull request #552 from immortal521/feat/code-block-line-number
feat: add code block line number display
2025-08-14 16:47:46 +08:00
immortal521
e0f7299a86 feat: add code block line number display
- Added Maple Mono font
- Changed code block font to Maple Mono
- Increased mobile line height from 1.1 to 1.5
2025-08-14 15:40:14 +08:00
Tim
1f9ae8d057 Merge pull request #550 from nagisa77/feature/fix_db_error
fix: fix reward db error
2025-08-14 15:21:31 +08:00
Tim
da1ad73cf6 fix: fix reward db error 2025-08-14 15:19:21 +08:00
Tim
53c603f33a Merge pull request #546 from netcaty/main
optimize(backend): batch query for /api/categories && /api/tags
2025-08-14 14:30:14 +08:00
Tim
06f86f2b21 Merge pull request #545 from nagisa77/feature/first_screen
Feature/first screen
2025-08-14 14:26:17 +08:00
Tim
22693bfdd9 fix: 首屏ssr优化 2025-08-14 14:25:38 +08:00
netcat
0058f20b1e optimize(backend): batch query for /api/categories && /api/tags 2025-08-14 14:19:04 +08:00
Tim
304d941d68 Revert "fix: use home path"
This reverts commit 2efe4e733a.
2025-08-14 13:50:58 +08:00
Tim
3dbcd2ac4d Merge pull request #543 from nagisa77/feature/first_screen
fix: use home path
2025-08-14 13:46:48 +08:00
Tim
2efe4e733a fix: use home path 2025-08-14 13:45:29 +08:00
Tim
08239a16b8 Merge pull request #542 from nagisa77/feature/first_screen
fix: 首屏ssr优化
2025-08-14 13:40:07 +08:00
Tim
cb49dc9b73 fix: 首屏ssr优化 2025-08-14 13:39:25 +08:00
Tim
43d4c9be43 Merge pull request #541 from nagisa77/feature/first_screen
fix: 首屏ssr优化
2025-08-14 13:24:17 +08:00
Tim
1dc13698ad fix: 首屏ssr优化 2025-08-14 13:22:53 +08:00
Tim
d58432dcd9 Merge pull request #540 from nagisa77/codex/fix-logo-click-triggering-window.reload 2025-08-14 12:47:43 +08:00
Tim
e7ff73c7f9 fix: prevent header logo from triggering page reload 2025-08-14 12:47:26 +08:00
Tim
4ee9532d5f Merge pull request #539 from nagisa77/codex/fix-logo-click-reload-issue 2025-08-14 12:38:11 +08:00
Tim
80c3fd8ea2 fix: prevent homepage reload on logo click 2025-08-14 12:37:54 +08:00
Tim
7e277d06d5 Merge pull request #538 from nagisa77/feature/first_screen
fix: 首屏幕ssr优化
2025-08-14 12:29:58 +08:00
Tim
d2b68119bd fix: 首屏幕ssr优化 2025-08-14 12:29:08 +08:00
Tim
f7b0d7edd5 Merge pull request #537 from nagisa77/feature/first_screen
fix: 首屏幕ssr优化
2025-08-14 11:56:26 +08:00
Tim
cdea1ab911 fix: 首屏幕ssr优化 2025-08-14 11:55:39 +08:00
Tim
ada6bfb5cf Merge pull request #536 from nagisa77/codex/add-logo-click-to-refresh-homepage
feat: refresh home when clicking header logo
2025-08-14 11:00:37 +08:00
Tim
928dbd73b5 feat: allow logo to refresh home page 2025-08-14 11:00:17 +08:00
Tim
8c1a7afc6e Merge pull request #530 from nagisa77/feature/env
fix: 前后端代码域名hardcode调整(for预发环境做准备)
2025-08-14 10:38:49 +08:00
Tim
87453f7198 fix: add .env.example 2025-08-14 10:36:02 +08:00
Tim
48e3593ef9 Merge remote-tracking branch 'origin/main' into feature/env 2025-08-14 10:34:10 +08:00
Tim
655e8f2a65 fix: setup 迁移完成 v1 2025-08-14 10:27:01 +08:00
Tim
7a0afedc7c Merge pull request #533 from CH-122/feat/link 2025-08-13 18:12:34 +08:00
Tim
902fce5174 fix: setup 迁移完成 2025-08-13 17:59:38 +08:00
Tim
0034839e8d fix: 迁移部分页面为setup 2025-08-13 17:49:51 +08:00
CH-122
148fd36fd1 Merge branch 'main' into feat/link 2025-08-13 17:48:23 +08:00
Tim
06cd663eaf Merge pull request #532 from nagisa77/codex/add-comment-pinning-feature
feat: support comment pinning
2025-08-13 16:31:12 +08:00
Tim
0edbeabac2 feat: allow post authors to pin comments 2025-08-13 16:30:48 +08:00
Tim
65cc3ee58b Merge pull request #531 from nagisa77/codex/add-post-lottery-notification-to-author 2025-08-13 16:20:09 +08:00
Tim
6965fcfb7f feat: notify lottery author 2025-08-13 16:19:53 +08:00
Tim
40520c30ec Merge pull request #529 from nagisa77/codex/refactor-to-use-environment-variables
feat: move API and OAuth IDs to runtime config
2025-08-13 16:01:07 +08:00
Tim
5d7ca3d29a feat: use runtime config for API and OAuth client IDs 2025-08-13 16:00:26 +08:00
Tim
a3aec1133b Merge pull request #528 from nagisa77/codex/add-new-prize-notification-type
feat: add lottery win notification
2025-08-13 15:58:33 +08:00
Tim
8fa715477b feat: add lottery win notification 2025-08-13 15:57:59 +08:00
CH-122
9209ebea4c feat: 添加链接插件以支持外部链接在新窗口打开 2025-08-13 15:40:40 +08:00
Tim
47a9ce5843 fix: 后端取消网址hardcode 2025-08-13 14:02:32 +08:00
Tim
dfef13e2be Merge pull request #520 from AnNingUI/main
fix: 清理掉了大部分warn,优化了在移动端侧边栏的逻辑问题
2025-08-12 21:45:46 +08:00
AnNingUI
2f4d6e68da fix: 用传递menuBtn的ref代替手动查询dom的方式 2025-08-12 21:26:24 +08:00
AnNingUI
414872f61e fix: 解决tag与类别切换需要reload整个页面的bug 2025-08-12 20:42:31 +08:00
AnNingUI
82475f71db fix: 清理掉了所有warn,优化了在移动端侧边栏的逻辑问题 2025-08-12 20:36:00 +08:00
Tim
a6874e9be3 Merge pull request #512 from nagisa77/feature/message_control
feat: message control
2025-08-12 17:45:17 +08:00
Tim
720031770d Merge branch 'main' into feature/message_control 2025-08-12 17:43:36 +08:00
Tim
eb7a25434f fix: global popup 2025-08-12 17:34:13 +08:00
Tim
bda4b24cf0 Revert "Disable post viewed and user activity notifications by default"
This reverts commit aea4f59af7.
2025-08-12 17:31:01 +08:00
Tim
4dedb70d54 Merge pull request #519 from nagisa77/codex/enable-user-notification-filtering
Disable post-viewed and user-activity notifications by default
2025-08-12 17:18:15 +08:00
Tim
aea4f59af7 Disable post viewed and user activity notifications by default 2025-08-12 17:16:42 +08:00
Tim
84ed778dc0 Merge pull request #518 from nagisa77/codex/add-notification-settings-pop-up
feat: add notification settings popup
2025-08-12 16:41:13 +08:00
Tim
6ca1862034 feat: allow navigating to notification settings 2025-08-12 16:29:04 +08:00
Tim
b3ea41ad1e Merge pull request #513 from AnNingUI/main
fix: 统一使用绝对路径别名“~”并加入jsconfig方便编辑器跳转
2025-08-12 15:19:34 +08:00
Tim
210d3dfa6f fix: add type 2025-08-12 15:01:03 +08:00
AnNingUI
80ecb1620d fix: 统一使用绝对路径别名“~”并加入jsconfig方便编辑器跳转
Fixes #510
2025-08-12 14:45:55 +08:00
Tim
b094f2f287 Merge pull request #511 from nagisa77/codex/support-disabling-message-notification-types
feat: support notification type preferences
2025-08-12 14:29:30 +08:00
Tim
02076e24e5 feat: allow updating notification prefs 2025-08-12 14:28:34 +08:00
Tim
d195d2f624 fix: basic ui 2025-08-12 14:07:57 +08:00
Tim
8b12402e89 fix: 右上角头像有显示问题, 点击后恢复 #508 2025-08-12 12:46:01 +08:00
Tim
d72709ca4d Merge pull request #507 from nagisa77/feature/stats
fix: stat problems
2025-08-12 10:21:08 +08:00
Tim
9878c12e33 fix: stat problems 2025-08-12 10:19:40 +08:00
Tim
84c1833923 Merge pull request #506 from nagisa77/codex/add-three-new-reports 2025-08-12 09:31:42 +08:00
Tim
08a2678bd5 feat: add stat service 2025-08-12 09:31:27 +08:00
Tim
ae46cbf216 Merge pull request #505 from nagisa77/codex/adapt-mobile-menu-for-click-outside 2025-08-12 09:27:11 +08:00
Tim
5fee90dfae feat: close mobile menu on outside tap 2025-08-12 09:25:41 +08:00
Tim
5a5d5add23 Merge pull request #503 from nagisa77/feature/nuxt_opt_v2
fix: 前端水合前ui优化. 可以考虑评论框不出
2025-08-12 01:30:20 +08:00
tim
cf4c427335 Merge branch 'feature/nuxt_opt_v2' of github.com:nagisa77/OpenIsle into feature/nuxt_opt_v2 2025-08-12 01:30:02 +08:00
tim
85fb1b8a27 fix: search button issue 2025-08-12 01:29:50 +08:00
Tim
72282b1a2f Merge pull request #504 from nagisa77/codex/return-dto-in-activitycontroller-list-method
Use DTO for activity list
2025-08-12 01:22:40 +08:00
Tim
499488c22a Use DTO for activity list 2025-08-12 01:22:26 +08:00
tim
e2d812246a fix: 移动端勋章展示有点异常 #485 2025-08-12 01:21:39 +08:00
tim
c7d99885dc fix: 30DAYS 2025-08-12 01:17:03 +08:00
tim
f977f96407 fix: 抽奖ui优化 #494 2025-08-12 01:07:50 +08:00
tim
fb7d134b27 fix: SSR 迁移后 pwa 图标显示问题 #499 2025-08-12 00:37:33 +08:00
tim
98d85a0573 fix: 前端水合前ui优化. 可以考虑评论框不出 2025-08-12 00:20:07 +08:00
Tim
6c2a7f7957 Merge pull request #498 from nagisa77/codex/add-badge-for-first-1000-users
feat: add pioneer medal for first 1000 users
2025-08-11 20:17:26 +08:00
Tim
2ebccb40f5 feat: add pioneer medal dto 2025-08-11 20:15:49 +08:00
Tim
6342b8f3a6 Merge pull request #497 from nagisa77/codex/add-title-and-metadata-for-seo
feat: enhance SEO titles and descriptions
2025-08-11 19:02:49 +08:00
Tim
20585201dd feat: add SSR titles and metadata 2025-08-11 19:02:35 +08:00
Tim
1c4df40f12 fix: 全局格式化 2025-08-11 18:16:13 +08:00
Tim
31cff70f63 fix: test commit 2025-08-11 18:12:18 +08:00
Tim
678626a3d7 Merge pull request #495 from nagisa77/codex/add-gift-icon-for-lottery-posts
feat: add lottery icon to post list
2025-08-11 15:44:50 +08:00
Tim
05bf33dacd feat: add lottery icon to post list 2025-08-11 15:44:34 +08:00
tim
4cf7f1dacf fix: utc 2025-08-11 11:25:23 +08:00
tim
5871d74c4f fix: utc 2025-08-11 11:18:10 +08:00
Tim
1e3e1a78a5 Merge pull request #483 from nagisa77/zrjuux-codex/fix-circular-dependency-in-beans
Avoid PostService self-dependency at startup
2025-08-11 11:11:22 +08:00
Tim
4b987f894d Handle self-invocation in PostService 2025-08-11 11:11:11 +08:00
Tim
3ab1e92a37 Merge pull request #482 from nagisa77/revert-481-codex/fix-circular-dependency-in-beans
Revert "refactor: remove circular dependency in PostService"
2025-08-11 11:10:55 +08:00
Tim
2c334a26b6 Revert "refactor: remove circular dependency in PostService" 2025-08-11 11:10:45 +08:00
Tim
2a25c6edfa Merge pull request #481 from nagisa77/codex/fix-circular-dependency-in-beans
refactor: remove circular dependency in PostService
2025-08-11 11:09:35 +08:00
Tim
3c6553d7f8 refactor: remove circular dependency in PostService 2025-08-11 11:09:23 +08:00
tim
908df079e0 fix: UTC 时间 2025-08-11 11:03:00 +08:00
Tim
a3f30e9444 Merge pull request #480 from nagisa77/codex/fix-finalizelottery-execution-issue
Fix lottery finalization scheduling
2025-08-11 10:59:20 +08:00
Tim
aff3997687 Merge branch 'main' into codex/fix-finalizelottery-execution-issue 2025-08-11 10:59:06 +08:00
Tim
5fa0bd792b Ensure lottery finalization runs transactionally 2025-08-11 10:57:55 +08:00
tim
2280a16a83 Reapply "fix: UTC 时间"
This reverts commit 86ef6f9ce7.
2025-08-11 10:55:37 +08:00
tim
86ef6f9ce7 Revert "fix: UTC 时间"
This reverts commit bcd499b4bc.
2025-08-11 10:50:36 +08:00
tim
bcd499b4bc fix: UTC 时间 2025-08-11 10:43:27 +08:00
tim
dcc7c3ebcc fix: 取消时区计算 2025-08-11 10:28:40 +08:00
tim
fd2676ef04 fix: add log 2025-08-11 10:24:13 +08:00
Tim
ca49407bf9 Merge pull request #479 from nagisa77/codex/refactor-lottery-task-scheduling
Reschedule lottery finalization on startup
2025-08-11 10:21:01 +08:00
Tim
ff64f13765 Reschedule lottery finalization on startup 2025-08-11 10:20:50 +08:00
Tim
29e061c885 Merge pull request #478 from nagisa77/codex/fix-postservice-argument-length-error
test: update PostService tests for new signature
2025-08-11 10:07:10 +08:00
Tim
36bc86da7f test: update PostService tests for new signature 2025-08-11 10:06:53 +08:00
Tim
2811a89e3b Merge pull request #477 from nagisa77/codex/remove-schedule-when-deleting-lottery-post
Cancel scheduled lottery finalizations when deleting posts
2025-08-11 10:06:07 +08:00
Tim
a774c9cc97 Cancel lottery schedule on post deletion 2025-08-11 10:05:37 +08:00
tim
e27f57fab2 fix: website 2025-08-11 10:01:36 +08:00
Tim
692fc6d1d0 Merge pull request #470 from nagisa77/codex/add-lottery-post-type-and-api
feat: add lottery post type with participation API
2025-08-11 09:56:45 +08:00
tim
5360529327 feat: 抽奖ui 2025-08-11 09:56:15 +08:00
Tim
e7d0f7fb0e Merge pull request #476 from nagisa77/codex/implement-logic-in-posts-page
feat: add lottery section logic
2025-08-11 02:35:25 +08:00
Tim
6b1aeb82c1 feat: add lottery section logic 2025-08-11 02:34:54 +08:00
tim
8320a84ba0 feat: 抽奖ui 2025-08-11 02:22:38 +08:00
Tim
7aef126181 Merge pull request #475 from nagisa77/codex/add-post-type-selection-and-lottery-options
feat: add lottery post type options
2025-08-11 01:45:38 +08:00
Tim
e0291868bc feat: add lottery post type options 2025-08-11 01:45:23 +08:00
Tim
0cf1bf187a Merge pull request #474 from nagisa77/revert-473-f9h526-codex/add-post-creation-enhancements
Revert "feat: add lottery post fields"
2025-08-11 01:33:12 +08:00
Tim
6fb16e91dc Revert "feat: add lottery post fields" 2025-08-11 01:32:50 +08:00
Tim
6d40e6e5e8 Merge pull request #473 from nagisa77/f9h526-codex/add-post-creation-enhancements
feat: add lottery post fields
2025-08-11 01:32:26 +08:00
Tim
398226b9bc feat: add lottery post fields 2025-08-11 01:32:12 +08:00
Tim
c1ad6b499f Merge pull request #472 from nagisa77/revert-471-codex/add-post-creation-enhancements
Revert "feat: add lottery post options"
2025-08-11 01:31:00 +08:00
Tim
71e0b1379c Revert "feat: add lottery post options" 2025-08-11 01:30:36 +08:00
Tim
594d8bf994 Merge pull request #471 from nagisa77/codex/add-post-creation-enhancements
feat: add lottery post options
2025-08-11 01:29:42 +08:00
Tim
7616a2d0e0 feat: add lottery post options 2025-08-11 01:29:22 +08:00
tim
c4ca1465ee Merge remote-tracking branch 'origin/main' into codex/add-lottery-post-type-and-api 2025-08-11 01:18:50 +08:00
Tim
eb32e4bad7 feat: implement lottery post type 2025-08-11 01:17:55 +08:00
tim
ae1a8daa22 Revert "Reapply "feat: reuse server data on home page""
This reverts commit dbeaefe9ba.
2025-08-11 01:14:51 +08:00
tim
0fdb4c234a feat: fix menu show 2025-08-11 01:08:44 +08:00
tim
23815fbd0a feat: fix ssr 2025-08-11 00:55:58 +08:00
tim
dbeaefe9ba Reapply "feat: reuse server data on home page"
This reverts commit 6d277b5809.
2025-08-11 00:41:34 +08:00
tim
582873e505 Reapply "fix: retain scroll position after hydration"
This reverts commit ff767970a1.
2025-08-11 00:41:08 +08:00
Tim
6b35b43fd6 Merge pull request #469 from nagisa77/feature/nuxt_opt_v1 2025-08-10 22:12:02 +08:00
Tim
06fd7e893e Merge pull request #468 from nagisa77/codex/fix-avatar-misalignment-on-refresh 2025-08-10 22:09:00 +08:00
Tim
ef2bf7f32b fix: ensure unique avatar keys on home page 2025-08-10 22:01:56 +08:00
tim
f312cf7d1c Revert "fix: correct SSR mobile detection"
This reverts commit 113cec1705.
2025-08-10 17:41:20 +08:00
Tim
351b33bb3c Merge pull request #465 from nagisa77/codex/ssrismobile
fix: correct SSR mobile detection
2025-08-10 17:14:42 +08:00
Tim
113cec1705 fix: correct SSR mobile detection 2025-08-10 17:14:02 +08:00
tim
454f2b4a0b Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-10 13:19:02 +08:00
tim
6ab4879968 fix: 解决帖子评论问题 2025-08-10 13:18:37 +08:00
tim
57acb37e84 fix: 解决帖子评论问题 2025-08-10 12:46:20 +08:00
Tim
4441c697b3 Merge pull request #460 from nagisa77/feature/nuxt_opt_v1
Feature/nuxt opt
2025-08-10 12:30:22 +08:00
tim
bda2b54ce9 fix: magic delay 2025-08-10 12:30:09 +08:00
Tim
f6f4f198b0 Merge pull request #461 from nagisa77/codex/improve-comment-box-scroll-behavior
fix comment reply editor visibility
2025-08-10 12:27:45 +08:00
Tim
f50541bff7 feat: scroll to comment editor when opened 2025-08-10 12:27:32 +08:00
tim
2a5bff1086 feat: 本地回复数据fixed 2025-08-10 12:26:22 +08:00
Tim
8c1386a2d0 Merge pull request #459 from nagisa77/codex/reset-login-state-on-401-error
Reset auth on token expiry
2025-08-10 11:59:39 +08:00
Tim
ce5bcb9dca Reset auth on 401 2025-08-10 11:59:23 +08:00
tim
1177a778ee feat: fix 注册失败和登录失败 2025-08-10 11:50:21 +08:00
tim
1a8e216aa9 feat: 新增贡献者前端标识 2025-08-10 02:24:26 +08:00
tim
7189977a2f feat: 防止重新弄错代码行数 2025-08-10 02:14:43 +08:00
Tim
d5f52529f7 Merge pull request #457 from nagisa77/codex/fix-compilation-issue-in-medalservicetest
test: update MedalService tests for contributor medal
2025-08-10 02:08:47 +08:00
Tim
1e85e489a7 test: update MedalService tests for contributor medal 2025-08-10 02:08:28 +08:00
Tim
5ec85519c7 Merge pull request #447 from nagisa77/codex/add-new-medallions-and-related-api
feat: implement medal API
2025-08-10 02:03:00 +08:00
tim
9462c284d6 feat: 新增贡献者勋章 2025-08-10 02:02:36 +08:00
Tim
c6e88792a3 Merge pull request #456 from nagisa77/codex/add-contributor-achievement-feature
feat: add contributor achievement
2025-08-10 01:30:03 +08:00
Tim
2dfbf0d904 feat: track contributor lines 2025-08-10 01:29:41 +08:00
Tim
68c7b12cb0 Merge pull request #455 from nagisa77/codex/update-backend-to-set-selected-badge
feat: auto select medal for user in mappers
2025-08-10 01:15:21 +08:00
Tim
33b2734ba5 feat: auto select medal for user in mappers 2025-08-10 01:15:02 +08:00
Tim
b58a5d975c Merge pull request #454 from nagisa77/codex/update-medal-selection-logic-in-backend
feat: auto select medals and improve navigation
2025-08-10 00:59:51 +08:00
Tim
d0df698aa9 feat: auto select medals and improve navigation 2025-08-10 00:59:34 +08:00
tim
6b80f2386b Revert "feat: auto select medals and make badges interactive"
This reverts commit 4516f77727.
2025-08-10 00:32:07 +08:00
Tim
8e475103f8 Merge pull request #452 from nagisa77/codex/add-badge-selection-for-users
feat: auto select medals and make badges interactive
2025-08-10 00:26:54 +08:00
Tim
4516f77727 feat: auto select medals and make badges interactive 2025-08-10 00:25:13 +08:00
Tim
594068bd5e Merge pull request #451 from nagisa77/codex/add-user-badge-selection-feature
feat: enable user medal selection and display
2025-08-10 00:14:31 +08:00
Tim
041496cf98 feat: add user medal selection and display 2025-08-10 00:13:54 +08:00
tim
6aedec7a9b feat: achievement select to show 2025-08-09 22:26:46 +08:00
Tim
6d08d10f19 Merge pull request #450 from nagisa77/codex/add-achievement-popup-to-globalpopups.vue
feat: add medal popup
2025-08-09 17:18:46 +08:00
Tim
cdc35878a2 feat: add medal popup 2025-08-09 17:18:31 +08:00
tim
57c0aa5899 feat: 完成的排序在前面 2025-08-09 17:08:48 +08:00
Tim
b196be59a2 Merge pull request #449 from nagisa77/codex/tabloading
feat: optimize achievements tab loading
2025-08-09 16:53:40 +08:00
Tim
760bc4fc4b feat: optimize achievements tab loading 2025-08-09 16:53:27 +08:00
tim
6b5b6b8c81 feat: fix some code 2025-08-09 16:47:40 +08:00
Tim
411d24194b Merge pull request #448 from nagisa77/codex/integrate-real-data-for-achievementlist
feat: wire achievements to backend
2025-08-09 14:29:42 +08:00
Tim
2560bf45a7 feat: wire achievements to backend 2025-08-09 14:29:29 +08:00
tim
4207886dce feat: achivement 2025-08-09 14:15:54 +08:00
Tim
987fe0d885 feat: implement medal feature 2025-08-09 02:08:02 +08:00
tim
9c1cedd172 feature: delete vue3 CSR 2025-08-09 01:16:37 +08:00
tim
ff767970a1 Revert "fix: retain scroll position after hydration"
This reverts commit 637f5316e8.
2025-08-08 18:45:53 +08:00
tim
6d277b5809 Revert "feat: reuse server data on home page"
This reverts commit 4df96f8aa2.

# Conflicts:
#	frontend_nuxt/pages/index.vue
2025-08-08 18:45:34 +08:00
Tim
fddfd07836 Merge pull request #446 from nagisa77/codex/fix-ismobile-check-for-initial-data 2025-08-08 18:12:56 +08:00
Tim
27a2591904 refactor: per-request mobile detection 2025-08-08 18:12:40 +08:00
Tim
70eece7a83 Merge pull request #445 from nagisa77/codex/fix-scroll-issue-on-homepage 2025-08-08 18:06:10 +08:00
Tim
637f5316e8 fix: retain scroll position after hydration 2025-08-08 18:05:36 +08:00
Tim
25a64d7666 Merge pull request #443 from nagisa77/codex/use-server-fetch-data-in-homepage
feat: reuse server data on home page
2025-08-08 17:58:33 +08:00
Tim
4df96f8aa2 feat: reuse server data on home page 2025-08-08 17:53:44 +08:00
Tim
3e4834f0fd fix: profile提速 2025-08-08 17:37:12 +08:00
Tim
10a63d3659 fix: 夜间模式修复 2025-08-08 17:36:39 +08:00
Tim
cc1a414df4 feat: support keepalive 2025-08-08 17:24:43 +08:00
Tim
8ceae90962 Revert "feat: enable page keepalive"
This reverts commit d9099c7281.
2025-08-08 17:21:59 +08:00
Tim
12b7c68cae Merge pull request #442 from nagisa77/codex/add-keep-alive-feature-for-navigation
feat: enable page keepalive
2025-08-08 17:10:33 +08:00
Tim
d9099c7281 feat: enable page keepalive 2025-08-08 17:10:17 +08:00
Tim
7d793ede6e Merge pull request #441 from nagisa77/codex/fix-vditor-styles-not-applying-in-ssr
fix: apply global Vditor styles in SSR
2025-08-08 17:05:17 +08:00
Tim
6233508442 fix: apply global Vditor styles in SSR 2025-08-08 17:05:00 +08:00
Tim
91298c6922 Merge pull request #440 from nagisa77/codex/fix-flash-of-white-on-night-mode
fix: apply theme before render to avoid flash
2025-08-08 17:02:58 +08:00
Tim
8dcb61dfed fix: apply theme before render to avoid flash 2025-08-08 17:02:43 +08:00
Tim
2b33253182 Merge pull request #439 from nagisa77/codex/fix-homepage-redirection-issue
fix: support user profile trailing slash
2025-08-08 17:01:13 +08:00
Tim
30919212f3 fix: support user profile trailing slash 2025-08-08 17:00:58 +08:00
Tim
1a496dc0ee Merge pull request #438 from nagisa77/codex/fix-server-side-ismobile-detection-for-ssr
fix: handle mobile detection during SSR
2025-08-08 17:00:03 +08:00
Tim
7aafe30b46 feat: improve SSR mobile detection 2025-08-08 16:59:45 +08:00
Tim
688d29f445 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-08 15:53:40 +08:00
Tim
14dd5a8fc2 feat: add global css 2025-08-08 15:53:08 +08:00
Tim
0ec5db5d91 Merge pull request #437 from nagisa77/codex/fix-night-mode-reset-on-refresh
fix: persist theme mode across refresh
2025-08-08 15:41:17 +08:00
Tim
26622a4ad2 fix: persist theme mode across refresh 2025-08-08 15:41:02 +08:00
Tim
f36532d45f Merge pull request #436 from nagisa77/codex/add-global-loading-progress-bar
feat(frontend_nuxt): add global loading progress bar
2025-08-08 15:17:44 +08:00
Tim
af2cf99041 feat(frontend_nuxt): wire up nprogress plugin 2025-08-08 15:05:33 +08:00
Tim
af9028190d feat: 域名修改 2025-08-08 14:26:49 +08:00
Tim
9120ea511b Merge pull request #432 from nagisa77/feature/nuxt_optimization
Feature: nuxt optimization
2025-08-08 14:22:53 +08:00
Tim
37e0f9dbc5 feat: 移动端服务器渲染判断 2025-08-08 14:21:58 +08:00
Tim
98bcbf52ba feat: 首刷返回 2025-08-08 14:15:42 +08:00
Tim
3a23cd8e19 Merge pull request #435 from nagisa77/codex/fix-composable-usage-outside-of-valid-context 2025-08-08 13:57:52 +08:00
Tim
e1a1bc8a69 fix: ensure router composable initialized before async 2025-08-08 13:56:56 +08:00
Tim
44c3091951 fix: 解决冲突 2025-08-08 13:50:15 +08:00
Tim
1840284b48 Merge remote-tracking branch 'origin/codex/modify-menu-component-for-server-content' into feature/nuxt_optimization 2025-08-08 13:40:20 +08:00
Tim
9de5f4ce8d feat(frontend_nuxt): fetch menu data server-side 2025-08-08 13:38:17 +08:00
Tim
a794872ab6 Merge remote-tracking branch 'origin/main' into feature/nuxt_optimization 2025-08-08 13:35:37 +08:00
Tim
963f2167eb Merge pull request #433 from nagisa77/codex/fix-editing-article-navigation-issue
Fix post edit navigation in Nuxt frontend
2025-08-08 13:33:53 +08:00
Tim
35bdf58cd6 Fix post edit navigation by restructuring routes 2025-08-08 13:33:34 +08:00
Tim
65ae660486 feat: 修改为服务端渲染、解决跳转问题 2025-08-08 13:22:42 +08:00
Tim
6554e66a4e Merge pull request #430 from nagisa77/codex/add-server-side-rendering-for-index.vue
Enable SSR initial render for home and post pages
2025-08-08 12:58:05 +08:00
Tim
5e839be3af Enable SSR initial fetch for posts and home 2025-08-08 12:57:41 +08:00
Tim
44daa255c8 feat: 处理nuxt部分样式问题 & 跳转问题 2025-08-08 11:24:52 +08:00
Tim
2b1958a603 feat: 处理编译问题 2025-08-07 22:35:43 +08:00
Tim
51e958799d feat: 处理编译问题 2025-08-07 22:28:39 +08:00
Tim
676e959d4b feat: toast 问题修改 2025-08-07 22:20:59 +08:00
Tim
f9a89ae9ef Merge pull request #426 from nagisa77/codex/migrate-frontend-to-nuxt4-with-ssr
feat: add initial Nuxt frontend with SSR
2025-08-07 21:35:21 +08:00
Tim
af85e7eee4 feat: update base url 2025-08-07 21:34:18 +08:00
Tim
a9d104735c Merge pull request #428 from nagisa77/codex/fix-element-retrieval-error-on-refresh
fix: stable editor id on SSR
2025-08-07 21:28:34 +08:00
Tim
752d288e3b fix: stable editor id on SSR 2025-08-07 21:28:17 +08:00
Tim
9c59277023 feat: 处理页面报错 2025-08-07 21:18:21 +08:00
Tim
d19cfc0797 Merge pull request #427 from nagisa77/codex/assist-migration-of-other-pages-to-nuxt
feat: migrate legacy Vue pages to Nuxt
2025-08-07 20:22:02 +08:00
Tim
565678f79a chore: migrate legacy pages and utilities to nuxt 2025-08-07 20:21:22 +08:00
Tim
73b9dcf0cd fix: 操作ldrs 2025-08-07 20:07:37 +08:00
Tim
a65e051af8 Merge pull request #423 from WilliamColton/main
增加积分系统
2025-08-07 20:04:03 +08:00
WilliamColton
f2a034f299 Merge remote-tracking branch 'origin/main' 2025-08-07 19:53:52 +08:00
WilliamColton
b42cdcf640 增加积分系统 2025-08-07 19:53:25 +08:00
Tim
cfdd257b9a feat: add initial Nuxt frontend with SSR 2025-08-07 19:18:42 +08:00
WilliamColton
b4ac496b55 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	frontend/src/views/NewPostPageView.vue
#	frontend/src/views/PostPageView.vue
2025-08-07 16:29:14 +08:00
WilliamColton
105f7781b3 增加积分系统 2025-08-07 16:24:40 +08:00
Tim
925973b134 fix 2025-08-06 20:31:58 +08:00
Tim
4a88685e81 fix: 移动端表情面板fix 2025-08-06 20:26:01 +08:00
Tim
d121bb08b9 Merge pull request #413 from nagisa77/codex/add-support-for-tieba-emojis-in-vditor
feat: add tieba emoji support
2025-08-06 20:11:07 +08:00
tim
b4f85989d0 feat: 移动端 vditor 支持 2025-08-06 20:10:25 +08:00
Tim
21d8984bfb Merge pull request #414 from WoJiaoFuXiaoYun/main
fix: Click outside the drop-down box to not hide
2025-08-06 19:52:03 +08:00
tim
3de6b89cc4 fix: 修复vditor高度问题 2025-08-06 19:47:51 +08:00
Tim
9621efd282 Merge pull request #416 from nagisa77/codex/add-limited-toolbar-options-for-mobile
feat: show compact vditor toolbar on mobile
2025-08-06 19:33:09 +08:00
Tim
fbaa05f146 feat: show compact vditor toolbar on mobile 2025-08-06 19:32:56 +08:00
Tim
05089761b6 Merge pull request #415 from nagisa77/codex/prevent-clearing-comments-on-error
fix: preserve comment text on failure
2025-08-06 19:29:10 +08:00
Tim
fdf51be5f5 Preserve comment text on submission errors 2025-08-06 19:28:57 +08:00
tim
05dbeccdd7 fix: emoji fix 2025-08-06 19:26:35 +08:00
浮小云
25b8ac97d7 Merge branch 'nagisa77:main' into main 2025-08-06 19:16:00 +08:00
WangHe
c2fe5649e2 Merge branch 'main' of https://github.com/WoJiaoFuXiaoYun/OpenIsle 2025-08-06 19:14:24 +08:00
WangHe
2235612070 fix: Click outside the drop-down box to not hide 2025-08-06 19:14:19 +08:00
Tim
6a1b71de0f feat: add tieba emoji support 2025-08-06 18:59:08 +08:00
Tim
b9819252d3 Merge pull request #412 from nagisa77/codex/iconmap
refactor: centralize reaction emoji map
2025-08-06 18:58:17 +08:00
Tim
5709b0d6fd refactor: reuse shared reaction emoji map 2025-08-06 18:58:01 +08:00
Tim
5ef104df46 Merge pull request #411 from nagisa77/codex/modify-ui-to-update-on-read-actions
feat: improve notification read UX
2025-08-06 18:55:09 +08:00
Tim
c838caf9e1 feat: update notification read UI instantly 2025-08-06 18:52:02 +08:00
Tim
597f682b75 fix: message page layout fix 2025-08-06 18:37:51 +08:00
Tim
2a72345943 Merge pull request #408 from nagisa77/codex/vditor
fix: offset vditor toolbar when pinned
2025-08-06 17:26:12 +08:00
Tim
73066522e3 Merge pull request #410 from WoJiaoFuXiaoYun/main
feat: vditor add loading
2025-08-06 17:25:58 +08:00
Tim
f5a3206f36 feat: sticky 优化 2025-08-06 17:23:19 +08:00
浮小云
6a6d743b96 Merge branch 'nagisa77:main' into main 2025-08-06 17:11:29 +08:00
WangHe
2241cfc9da feat: vditor add loading 2025-08-06 17:10:36 +08:00
Tim
597bc09c57 fix: offset vditor toolbar when pinned 2025-08-06 16:15:22 +08:00
Tim
fd024cf65d Merge pull request #406 from WoJiaoFuXiaoYun/main
fix: npm ci "highlight.js" build fail
2025-08-06 15:40:18 +08:00
WangHe
393e60c6e9 fix: npm ci "highlight.js" build fail 2025-08-06 15:20:33 +08:00
Tim
9a36b7651b Merge pull request #405 from WoJiaoFuXiaoYun/main
fix: add missing highlight.js
2025-08-06 14:37:26 +08:00
WangHe
5a2ef02ce7 fix: add missing highlight.js 2025-08-06 14:29:10 +08:00
Tim
227269c639 Merge pull request #404 from nagisa77/codex/update-application-approval-logic 2025-08-06 13:12:33 +08:00
Tim
beb1bf70bf Test admin register request notification handling 2025-08-06 13:12:14 +08:00
Tim
3167aad6d8 更新 index.js 2025-08-06 12:36:17 +08:00
Tim
79ccc45c95 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-06 11:34:46 +08:00
Tim
48ee45a560 feat: md 格式优化 2025-08-06 11:27:09 +08:00
tim
5f75f34289 fix: callback 2025-08-06 02:07:09 +08:00
tim
f0d1caf5f3 feat: 去除多线程操作 规避400问题 2025-08-06 01:45:46 +08:00
tim
004924815b feat: 偶现400错误,线程处理 2025-08-06 00:09:12 +08:00
tim
472db0174b feat: 偶现400错误,线程处理 2025-08-05 23:59:15 +08:00
tim
26ed082f93 Revert "revert: 通知取消异步,并且采用事务,看看能否解决 400"
This reverts commit 4e0dda3a24.
2025-08-05 23:54:40 +08:00
tim
e491a00c57 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-05 23:26:45 +08:00
tim
4e0dda3a24 revert: 通知取消异步,并且采用事务,看看能否解决 400 2025-08-05 23:26:15 +08:00
Tim
91862713e7 Merge remote-tracking branch 'origin/main' into codex/fix-sticky-header-and-jumptohashcomment 2025-08-05 19:55:05 +08:00
Tim
009d139549 fix: magic bug with magic wait 2025-08-05 19:54:43 +08:00
Tim
74fa970902 Merge pull request #394 from nagisa77/codex/fix-sticky-header-and-jumptohashcomment
fix: sticky scroller and hash comment navigation
2025-08-05 19:45:32 +08:00
Tim
eb933b8f78 fix: scroller fix! 2025-08-05 19:44:59 +08:00
Tim
7fe37d0131 fix: sync post scroller with window 2025-08-05 19:39:13 +08:00
Tim
82c0aa240a fix: make post page scroller sticky and restore comment hash navigation 2025-08-05 19:27:02 +08:00
Tim
a543a7bcf2 fix: header use fixed 2025-08-05 19:23:44 +08:00
Tim
f0caf930c5 Merge pull request #393 from nagisa77/feature/site_fix
Feature/site fix
2025-08-05 18:37:31 +08:00
Tim
05e28123ed fix: 页面使用最外层滚动 2025-08-05 18:35:51 +08:00
Tim
7292834700 Merge pull request #392 from nagisa77/codex/adapt-load-more-feature-in-homepageview
feat: unify scroll-based loading for outer scroll
2025-08-05 18:25:09 +08:00
Tim
3e4b94f1f2 feat: 通用滚动加载工具 2025-08-05 18:24:45 +08:00
Tim
0957a5c132 feat: 设置为外层滚动 2025-08-05 18:13:01 +08:00
Tim
1edaa50732 Revert "Reapply "feat: handle ios safari keyboard""
This reverts commit fb3eb2646d.
2025-08-05 15:46:31 +08:00
Tim
fb3eb2646d Reapply "feat: handle ios safari keyboard"
This reverts commit dc73a74e1c.
2025-08-05 15:31:30 +08:00
Tim
1ed76f7687 feat: fix async request 2025-08-05 15:21:14 +08:00
Tim
dc73a74e1c Revert "feat: handle ios safari keyboard"
This reverts commit 9b31df28aa.
2025-08-05 15:20:57 +08:00
Tim
2c0d39a6b8 Merge pull request #391 from nagisa77/356s63-codex/fix-header-overlap-issue-in-ios-safari
fix: handle iOS Safari keyboard scroll offset
2025-08-05 15:16:28 +08:00
Tim
9b31df28aa feat: handle ios safari keyboard 2025-08-05 15:15:57 +08:00
tim
caba1c6658 fix: add error code 2025-08-05 13:34:28 +08:00
Tim
dd0beb1955 Merge pull request #388 from nagisa77/codex/0
Fix homepage reply count display
2025-08-05 12:45:13 +08:00
Tim
c65dfbcbf9 feat: show correct reply counts 2025-08-05 12:44:50 +08:00
Tim
2e8bc012fa feat: delete -webkit-backdrop-filter 2025-08-05 11:01:10 +08:00
Tim
8ac1794853 fix: 互动先取消通知 2025-08-05 10:38:36 +08:00
Tim
d047e3d17a Revert "fix: support iOS safe area for header"
This reverts commit ea1588d9e9.
2025-08-05 10:17:57 +08:00
Tim
3673f5b904 Merge pull request #387 from nagisa77/codex/fix-header-overlap-issue-in-ios-safari 2025-08-05 09:39:02 +08:00
Tim
ea1588d9e9 fix: support iOS safe area for header 2025-08-05 09:38:48 +08:00
tim
2b9a4d35b6 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-05 01:09:52 +08:00
tim
0cfcd5588f fix: markdown渲染的 code 间距调整 2025-08-05 01:09:22 +08:00
Tim
043b369695 Merge pull request #381 from nagisa77/codex/add-navigation-without-page-reload
Preserve home page state when navigating back
2025-08-05 01:04:30 +08:00
tim
730d5b1d10 feat: 主页缓存 2025-08-05 01:04:01 +08:00
Tim
42c3ef3377 Track home page scroll position 2025-08-05 01:00:19 +08:00
Tim
eedde0fd28 Restore scroll position on cached home page 2025-08-05 00:46:08 +08:00
Tim
ae054f76de cache home page view 2025-08-05 00:34:13 +08:00
Tim
e5c746fe27 fix: test cases 2025-08-04 21:57:52 +08:00
Tim
cce0560cb3 Merge pull request #377 from nagisa77/jfcb2y-codex/fix-applicationcontext-loading-error
Handle missing push notification keys
2025-08-04 21:48:12 +08:00
Tim
7368fa77ae Allow missing push notification keys 2025-08-04 21:47:59 +08:00
Tim
90c26c5e7f Merge pull request #376 from nagisa77/codex/fix-usercontrollertest-json-path-assertions
test: mock user mapper for user endpoints
2025-08-04 21:47:22 +08:00
Tim
ed7319d61b test: mock user mapper for user endpoints 2025-08-04 21:47:09 +08:00
Tim
232ce26246 Merge pull request #374 from nagisa77/codex/fix-nullpointerexception-in-authcontrollertest
test: fix auth controller tests
2025-08-04 21:39:39 +08:00
Tim
14b4f67bcb Merge pull request #372 from nagisa77/codex/fix-usercontrollertest-bean-resolution-issue
test: mock user controller dependencies
2025-08-04 21:39:22 +08:00
Tim
c62c4b2942 Merge pull request #371 from nagisa77/codex/fix-applicationcontext-loading-error
Add default website URL for tests
2025-08-04 21:39:10 +08:00
Tim
7ab247f58d Merge pull request #370 from nagisa77/codex/fix-application-context-startup-error
Add website URL property for tests
2025-08-04 21:38:55 +08:00
Tim
33e1e92af3 test: fix auth controller tests 2025-08-04 21:38:30 +08:00
Tim
ac6bafb708 test: mock user controller dependencies 2025-08-04 21:38:26 +08:00
Tim
cfe7a44b98 Add default website URL for tests 2025-08-04 21:38:24 +08:00
Tim
49ae2c1fa2 Add website-url property for tests 2025-08-04 21:38:22 +08:00
Tim
3b107ffbd2 Merge pull request #366 from nagisa77/codex/fix-userrepository-bean-not-found-error
Fix TagControllerTest dependency setup
2025-08-04 21:32:06 +08:00
Tim
eac582ee74 Merge pull request #365 from nagisa77/codex/fix-commentcontrollertest-bean-loading-issue
test: mock CommentMapper in controller tests
2025-08-04 21:32:00 +08:00
Tim
ee5e0f38dc Merge pull request #369 from nagisa77/codex/fix-unsatisfied-dependency-for-postmapper
test: mock PostMapper in CategoryControllerTest
2025-08-04 21:31:54 +08:00
Tim
88c3933540 Merge pull request #364 from nagisa77/codex/fix-missing-usermapper-bean-for-searchcontroller
test: mock mapper dependencies in SearchControllerTest
2025-08-04 21:31:43 +08:00
Tim
6f32de633a Merge pull request #363 from nagisa77/codex/fix-notification-repository-test-assertions
Fix notification handling and email content
2025-08-04 21:31:28 +08:00
Tim
58bed963c5 Merge pull request #362 from nagisa77/codex/fix-missing-githubauthservice-bean
Add missing mocks for AuthControllerTest
2025-08-04 21:31:15 +08:00
Tim
62c6699adc Merge pull request #361 from nagisa77/codex/fix-unsatisfied-dependency-for-jwtservice
test: add missing JWT secrets for tests
2025-08-04 21:31:08 +08:00
Tim
4d78877849 Merge pull request #368 from nagisa77/codex/fix-email-not-sent-in-tests
Send email and push notification every five reactions
2025-08-04 21:30:55 +08:00
Tim
63a30127b7 Merge pull request #367 from nagisa77/codex/fix-unsatisfied-dependency-in-reactioncontroller
Ensure ReactionMapper bean in controller tests
2025-08-04 21:30:18 +08:00
Tim
a6a302760a Merge pull request #360 from nagisa77/codex/fix-application-context-loading-error
test: provide jwt secrets for tests
2025-08-04 21:30:06 +08:00
Tim
93a3dd421a Merge pull request #359 from nagisa77/codex/fix-missing-notificationmapper-bean
test: mock NotificationMapper in NotificationControllerTest
2025-08-04 21:29:58 +08:00
Tim
1a1a610b29 Merge pull request #358 from nagisa77/codex/fix-missing-commentmapper-bean
test: include mapper deps in PostControllerTest
2025-08-04 21:29:50 +08:00
Tim
053c05ea26 test: mock PostMapper in CategoryControllerTest 2025-08-04 21:29:45 +08:00
Tim
f04c4b27d9 Send email and push every five reactions 2025-08-04 21:29:33 +08:00
Tim
e73479feec Load ReactionMapper in controller tests 2025-08-04 21:29:31 +08:00
Tim
c99839d75b Mock repository and mapper dependencies for TagControllerTest 2025-08-04 21:29:03 +08:00
Tim
8452ffbd03 test: mock CommentMapper in controller tests 2025-08-04 21:29:02 +08:00
Tim
9985884534 test: mock dependencies in SearchControllerTest 2025-08-04 21:29:00 +08:00
Tim
c795bdad28 Fix notification service 2025-08-04 21:28:59 +08:00
Tim
5171f89182 Mock additional services in AuthControllerTest 2025-08-04 21:28:59 +08:00
Tim
ccc87f3f10 test: add missing JWT secrets for tests 2025-08-04 21:28:58 +08:00
Tim
7f06964eb2 test: provide jwt secrets for tests 2025-08-04 21:28:52 +08:00
Tim
e0fcc0d1f7 test: mock NotificationMapper in NotificationControllerTest 2025-08-04 21:28:51 +08:00
Tim
e3964c1b3d test: include mapper deps in PostControllerTest 2025-08-04 21:28:04 +08:00
Tim
0f5d5653f5 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-04 21:16:59 +08:00
Tim
e886142b39 feat: fix compile 2025-08-04 21:15:20 +08:00
Tim
d11403209d Merge pull request #357 from nagisa77/codex/refactor-dto-structure-in-controller
refactor: extract post dtos
2025-08-04 21:13:34 +08:00
Tim
e2e5942941 feat: add dedicated mappers 2025-08-04 21:10:09 +08:00
Tim
2db998a9d9 feat: relocate remaining dtos 2025-08-04 20:51:33 +08:00
Tim
a22967fc0c test: verify post dtos 2025-08-04 20:37:43 +08:00
Tim
b41835d9c8 refactor: extract post dtos 2025-08-04 20:37:20 +08:00
Tim
93d3023276 Merge pull request #356 from WilliamColton/main
修复申请原因可能为空的bug
2025-08-04 19:26:19 +08:00
WilliamColton
3b82a4aba0 修复申请原因可能为空的bug 2025-08-04 18:36:38 +08:00
Tim
6fa5978613 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-04 17:05:03 +08:00
Tim
0584cdb42c fix 2025-08-04 17:04:49 +08:00
Tim
147598275a Merge pull request #351 from WilliamColton/feature/last-comment-time
添加显示最后评论时间的功能
2025-08-04 16:36:42 +08:00
WilliamColton
f6d7020165 添加显示最后评论时间的功能 2025-08-04 16:29:21 +08:00
Tim
44e262a636 Merge pull request #342 from nagisa77/g2s0r1-codex/fix-occasional-errors-in-application
Handle null exception messages in global handler
2025-08-04 14:26:27 +08:00
Tim
3ae27e6216 Handle missing exception messages in global handler 2025-08-04 14:26:12 +08:00
tim
4da16cd071 Revert "fix: guard against null exception messages"
This reverts commit d50e8c0863.
2025-08-04 14:25:43 +08:00
Tim
7eb6ffde1d Merge pull request #341 from nagisa77/codex/fix-occasional-errors-in-application
fix: guard against null exception messages
2025-08-04 14:23:41 +08:00
Tim
d50e8c0863 fix: guard against null exception messages 2025-08-04 14:22:55 +08:00
Tim
797c39b1e4 Merge pull request #340 from nagisa77/codex/add-hourly-log-saving
chore: add hourly log rotation
2025-08-04 13:19:30 +08:00
Tim
a5899565b1 feat: add log save 2025-08-04 13:18:55 +08:00
Tim
b30939dd7d chore: add hourly log rotation 2025-08-04 13:15:01 +08:00
Tim
a1b5597944 Merge pull request #339 from nagisa77/codex/add-user-defined-avatar-cropping
feat: add avatar cropping
2025-08-04 13:12:19 +08:00
Tim
9f1080eeb0 feat: cropper 2025-08-04 13:11:55 +08:00
Tim
a8187b7d38 feat: add avatar cropping 2025-08-04 12:59:43 +08:00
Tim
11b839b21a Revert "feat: add hourly rolling logs"
This reverts commit afe01bc6ad.
2025-08-04 12:53:43 +08:00
Tim
957a07309a Merge pull request #338 from nagisa77/codex/check-for-log-rotation-and-retention
feat: add hourly rolling logs
2025-08-04 12:51:45 +08:00
Tim
afe01bc6ad feat: add hourly rolling logs 2025-08-04 12:51:16 +08:00
Tim
ee0aceeab7 Merge pull request #337 from nagisa77/codex/add-debug-logging-to
chore: add debug logging for comments
2025-08-04 12:07:24 +08:00
Tim
8a2adc5632 chore: add debug logging for comments 2025-08-04 12:06:51 +08:00
Tim
9b840ca769 fix: 403 bugfix 2025-08-04 11:03:14 +08:00
Tim
8bd27af592 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-04 10:24:06 +08:00
tim
f5f09cddcc Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-04 02:06:34 +08:00
tim
c154967564 feat: 评论嵌套规则修复 2025-08-04 02:06:24 +08:00
tim
ba8babd68a feat: 评论嵌套规则修复 2025-08-04 02:04:31 +08:00
Tim
3c7915e672 Merge pull request #331 from nagisa77/codex/refactor-callbackpage-into-components
refactor: reuse callback page component
2025-08-04 01:23:11 +08:00
Tim
f78a3ae149 refactor: reuse callback page component 2025-08-04 01:22:56 +08:00
Tim
b8632acebd Merge pull request #330 from nagisa77/codex/fix-google-login-popup-issue
Open Google login in new window
2025-08-04 01:09:08 +08:00
tim
365bcb86dd feat: fix with google login 2025-08-04 01:08:33 +08:00
tim
089e38f577 feat: fix with google login 2025-08-04 01:07:45 +08:00
Tim
1b206af28c Open Google auth in new window 2025-08-04 00:58:45 +08:00
Tim
dc5fdf4857 Merge pull request #329 from WilliamColton/bug/countLikesReceived 2025-08-04 00:43:11 +08:00
WilliamColton
d7b0aca4e6 修复收到的点赞显示异常问题 2025-08-03 22:36:29 +08:00
tim
354cc7cd17 feat: 处理添加表情偶现失败的问题 2025-08-03 20:12:44 +08:00
tim
d70433ff93 feat: 处理添加评论偶现失败的问题 2025-08-03 20:10:14 +08:00
tim
3a0cea8cd4 Revert "Add popup-based Google login function"
This reverts commit 1b39289dad.
2025-08-03 20:07:59 +08:00
tim
5aeedb07fe fix: header avatar object-fit method 2025-08-03 19:57:44 +08:00
Tim
61e2122ca9 Merge pull request #321 from nagisa77/codex/add-loginwithgooglewithpop-function
Add popup-based Google login option
2025-08-03 19:28:31 +08:00
Tim
1b39289dad Add popup-based Google login function 2025-08-03 19:17:00 +08:00
Tim
d58d5014da Merge pull request #320 from nagisa77/codex/add-cache-cleanup-for-vditor-editor
chore: clear stale vditor caches
2025-08-03 17:34:30 +08:00
Tim
c3e377ca3c chore: add helper to clear vditor cache 2025-08-03 17:33:48 +08:00
tim
67910317e8 fix: 修复消息页面有时候消息过长的问题 2025-08-03 16:45:38 +08:00
tim
ff09df03e3 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-08-03 16:26:34 +08:00
tim
a34e213647 feat: ios PWA 2025-08-03 16:26:28 +08:00
tim
b393eaadea feat: ios PWA 2025-08-03 16:25:58 +08:00
Tim
80998c71b0 Merge pull request #319 from nagisa77/codex/add-new-reaction-types-to-frontend-and-backend 2025-08-03 15:39:43 +08:00
Tim
9fd67c6c71 feat: add new reaction types 2025-08-03 15:30:11 +08:00
tim
4d2b616aed bugfix: menu 高度调整 2025-08-03 14:35:23 +08:00
tim
41f603e349 bugfix: 规避后台偶现失败的问题 2025-08-03 14:28:51 +08:00
Tim
ca9b2cb14d Merge pull request #310 from WilliamColton/refactor/directory-structure
feature: 优化目录结构
2025-08-03 02:15:14 +08:00
WilliamColton
c08723574d 优化目录结构 2025-08-03 01:27:28 +08:00
Tim
d63081955e Merge pull request #309 from nagisa77/codex/add-immediate-reaction-feedback-in-ui
feat: add optimistic reaction update
2025-08-03 01:17:26 +08:00
Tim
561a622c26 feat: add optimistic reaction update 2025-08-03 01:16:06 +08:00
Tim
50fcdbd12f Merge pull request #308 from nagisa77/codex/update-reply-count-to-include-subcomments
feat: count nested replies in post lists and comments
2025-08-03 01:15:29 +08:00
Tim
06eb9e2c7e feat: count nested replies 2025-08-03 01:15:14 +08:00
Tim
2ad3275753 Merge pull request #307 from nagisa77/codex/fix-sidebar-scroll-syncing-issues
Fix post scroller sync and top time
2025-08-03 00:56:25 +08:00
tim
a128bfa960 feat: 时序问题修正 2025-08-03 00:56:05 +08:00
Tim
454c7fb8f3 Fix post scroller sync and top time 2025-08-03 00:33:34 +08:00
tim
ac08fc96ce bugfix: 首页列表划到底部最后一条数据会被地址栏挡住部分 2025-08-03 00:14:56 +08:00
tim
6642a248fd feat: 1.添加内网调试地址, 2. Spaceship最新域名优惠码 这条数据横向超出了 挡住发帖人 2025-08-03 00:10:03 +08:00
Tim
9df8a9d123 Merge pull request #306 from nagisa77/codex/display-read-message-status-for-roles
Adjust message for mark-all-read
2025-08-01 16:04:46 +08:00
Tim
486787084a Show detailed mark-all-read message only for admins 2025-08-01 16:04:34 +08:00
Tim
49f5f36630 Merge pull request #305 from nagisa77/fm7fu9-codex
fix comment replies expansion
2025-08-01 12:57:47 +08:00
Tim
624729ae9e fix: expand top-level comment replies 2025-08-01 12:57:35 +08:00
Tim
5b8f3a1284 Merge pull request #304 from nagisa77/codex/update-commentdto-for-multiple-reactions
Add reactions to comment listing
2025-08-01 12:30:28 +08:00
Tim
d056bc9120 Include reactions in comment API 2025-08-01 12:30:15 +08:00
Tim
f73f0cd45a Merge pull request #303 from nagisa77/codex/update-user-profile-like-statistics
Fix profile like counts
2025-08-01 12:16:54 +08:00
Tim
d9d4597e13 fix like counts 2025-08-01 12:16:42 +08:00
Tim
873db304d1 feat: 搜索点击tag、分类需要重入 2025-08-01 12:15:36 +08:00
Tim
a239e29c32 Merge pull request #302 from nagisa77/codex/add-default-display-for-level-1-comments
Expand first-level comments by default
2025-08-01 12:09:27 +08:00
Tim
847426a507 feat(frontend): expand first-level comments by default 2025-08-01 12:09:10 +08:00
Tim
d4d8245671 Merge pull request #301 from nagisa77/tl0f6z-codex
Fix duplicate post view notifications
2025-08-01 12:02:25 +08:00
Tim
14bd9d86c0 feat: 处理重复阅读问题 2025-08-01 12:01:44 +08:00
Tim
db31b5d6c1 Deduplicate post view notifications 2025-08-01 11:55:17 +08:00
Tim
e8e77b6467 Revert "Deduplicate post view notifications"
This reverts commit 2fc22e7580.
2025-08-01 11:49:40 +08:00
Tim
ba1432d945 Merge pull request #300 from nagisa77/codex/remove-duplicate-notifications-for-posts
Implement deduplication for post view notifications
2025-08-01 11:48:05 +08:00
Tim
2fc22e7580 Deduplicate post view notifications 2025-08-01 11:47:43 +08:00
Tim
ba2299e882 feat: 避免消息页面出错 2025-08-01 11:38:46 +08:00
Tim
cd020c7e49 Merge pull request #299 from nagisa77/codex/improve-menu-loading-behavior
Implement menu caching
2025-08-01 11:25:16 +08:00
tim
caa255b882 feat: 修复menu每次都刷新的问题 2025-08-01 11:24:45 +08:00
Tim
f900a45c81 feat: cache menu categories and tags 2025-08-01 11:18:06 +08:00
tim
ce1a94dbaf feat: 【界面优化】在看别人帖子的时候发现代码块的内容间距有点紧,尤其在有中文注释的时候更明显 2025-08-01 11:10:53 +08:00
Tim
6a3644bca1 Merge pull request #294 from nagisa77/codex/modify-image-upload-to-frontend-direct-transfer
Enable presigned URL uploads
2025-08-01 10:42:10 +08:00
tim
67db6579e9 feat: vditor 修改 2025-08-01 10:41:16 +08:00
tim
ebc39a6388 Revert "Fix paste upload handler"
This reverts commit 97118e7bc8.
2025-08-01 02:12:22 +08:00
Tim
a97ca0cec9 Merge pull request #298 from nagisa77/codex/fix-paste-event-for-image-upload
Fix image paste event
2025-08-01 02:10:05 +08:00
Tim
97118e7bc8 Fix paste upload handler 2025-08-01 02:09:50 +08:00
Tim
22c2b41ac5 Merge pull request #297 from nagisa77/codex/fix-image-upload-with-command-+-v
Fix paste image upload in Vditor
2025-08-01 01:06:38 +08:00
Tim
3cd49c64f2 fix paste upload 2025-08-01 01:06:16 +08:00
tim
cc371fad85 fix: enable tips 2025-08-01 00:55:10 +08:00
Tim
a182b0b8da Merge pull request #296 from nagisa77/codex/fix-image-upload-and-loading-issues
Fix paste upload in custom Vditor handler
2025-08-01 00:51:32 +08:00
Tim
913ffa8a5a fix: enable paste upload with progress 2025-08-01 00:51:11 +08:00
Tim
296b0a737f Merge pull request #295 from nagisa77/codex/fix-vditor-upload-text-output
Fix vditor upload insertion
2025-08-01 00:42:09 +08:00
Tim
69b5e96695 fix: insert uploaded file into vditor 2025-08-01 00:41:52 +08:00
tim
c6a133978b fix: old version upload 2025-08-01 00:35:42 +08:00
Tim
ea17240cf1 feat: support presigned url upload 2025-07-31 22:34:27 +08:00
Tim
68e3daa736 Merge pull request #292 from nagisa77/n67uug-codex/add-user-mention-feature
Add mention notification feature
2025-07-31 20:24:23 +08:00
Tim
8d3bc728c5 feat: 通知自测通过 2025-07-31 20:22:44 +08:00
Tim
5486b9a6fe Merge pull request #293 from nagisa77/codex/add-special-hyperlink-for-user-mentions
Add user mention hyperlink
2025-07-31 20:08:20 +08:00
Tim
d4c1ad54fc Add mention link rendering in Markdown 2025-07-31 20:03:23 +08:00
Tim
4fcc47aa40 feat: 修改 @ 样式 2025-07-31 19:53:21 +08:00
Tim
954eb9afc4 Merge pull request #291 from nagisa77/codex/add-x-n-support-for-categories
Add counts for categories in menu
2025-07-31 18:06:13 +08:00
Tim
c94aeb9984 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-31 17:58:33 +08:00
Tim
aa70fc3273 feat: 修改通知 2025-07-31 17:58:15 +08:00
Tim
c541306494 Add mention suggestions and admin list 2025-07-31 17:57:25 +08:00
Tim
ef2aa43f03 feat(menu): show post count for categories 2025-07-31 17:48:28 +08:00
Tim
a02129b8f9 Merge pull request #290 from nagisa77/codex/fix-searchservice-constructor-argument-mismatch
Fix SearchServiceTest constructor
2025-07-31 14:08:44 +08:00
Tim
c4ecb80524 Fix SearchServiceTest to match new constructor 2025-07-31 14:08:32 +08:00
Tim
3ff94d0a5c bugfix: 修复首页可能会加载相同文章的问题 2025-07-31 13:51:20 +08:00
Tim
d385ce8c54 Merge pull request #289 from nagisa77/codex/refine-user_activity-notification-type 2025-07-31 13:15:07 +08:00
Tim
e2053c0428 feat: refine USER_ACTIVITY notifications 2025-07-31 13:13:45 +08:00
Tim
62096ba34d Merge pull request #288 from nagisa77/codex/add-keyword-matching-for-categories-and-tags
Add categories/tags to search
2025-07-31 12:52:39 +08:00
Tim
d4bfef36f7 feat: include categories and tags in global search 2025-07-31 12:52:25 +08:00
Tim
c242c89690 Merge pull request #287 from nagisa77/codex/adjust-latest-and-latest-reply-direction
Adjust default topic view to latest reply
2025-07-31 12:50:37 +08:00
Tim
a322d94f42 feat(frontend): default to latest reply view 2025-07-31 12:50:23 +08:00
Tim
aa38c2e5e1 Merge pull request #286 from nagisa77/codex/fix-sitemap-invalid-date-errors
Fix sitemap lastmod format
2025-07-30 20:52:21 +08:00
Tim
c16709a36d fix sitemap lastmod format 2025-07-30 20:51:59 +08:00
Tim
c52ac92549 feat: update robot.txt 2025-07-30 20:47:39 +08:00
Tim
33432a10f4 Merge pull request #285 from nagisa77/codex/fix-sitemap.xml-redirection-issue
Fix sitemap access without auth
2025-07-30 20:46:58 +08:00
Tim
cb5411c091 feat: make a sitemap api 2025-07-30 20:44:30 +08:00
Tim
dc4159b308 fix: allow public access to sitemap 2025-07-30 20:32:36 +08:00
Tim
86aff7aeb2 Merge pull request #284 from nagisa77/codex/generate-dynamic-sitemap.xml-for-posts
Add sitemap.xml endpoint
2025-07-30 20:19:29 +08:00
Tim
43c1f67b33 feat: add dynamic sitemap endpoint 2025-07-30 20:19:05 +08:00
Tim
2a7727e446 Merge pull request #283 from nagisa77/codex/fix-robots.txt-format-error
Add robots.txt file
2025-07-30 20:12:29 +08:00
Tim
b65bf7cc82 feat: add robot.txt 2025-07-30 20:11:35 +08:00
Tim
cab4b5c57d Add basic sitemap 2025-07-30 20:06:41 +08:00
Tim
1834636e01 Merge pull request #282 from nagisa77/codex/fix-missing-alt-attributes-for-images
Fix alt attributes for SEO
2025-07-30 20:01:04 +08:00
Tim
1aa39b8f91 Add missing alt attributes 2025-07-30 20:00:43 +08:00
Tim
a80ffbf85b Merge pull request #281 from nagisa77/codex/update-milk-tea-redemption-logic
Count real milk tea redemptions
2025-07-30 17:09:58 +08:00
Tim
18f3e31251 feat: track milk tea redemption count 2025-07-30 17:09:44 +08:00
Tim
7129514a9a Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-30 16:55:03 +08:00
Tim
8711237cf8 feat: update toolbar layer 2025-07-30 16:54:51 +08:00
Tim
980e2e6d1e Merge pull request #280 from nagisa77/codex/fix-compilation-errors-in-notificationservicetest
Fix duplicate test mocks in NotificationServiceTest
2025-07-30 16:24:28 +08:00
Tim
1b98f09115 Fix duplicate ReactionRepository variables 2025-07-30 16:24:09 +08:00
Tim
e27c821d80 feat: email fix & default avatar 2025-07-30 14:47:10 +08:00
Tim
8efdd5a50d Merge pull request #279 from nagisa77/codex/remove-github-default-avatar
Switch to DiceBear default avatars
2025-07-30 14:25:03 +08:00
Tim
1fcdff6203 feat: use DiceBear for default avatars 2025-07-30 14:24:49 +08:00
Tim
b2779759c0 feat: mobile 修改 2025-07-30 13:05:20 +08:00
Tim
36652d3a39 Merge pull request #278 from nagisa77/codex/use-teleport-for-dropdown-mobile-page
Add Teleport for mobile dropdown menu
2025-07-30 13:02:33 +08:00
Tim
23ce2f7d1f use teleport for mobile dropdown 2025-07-30 13:01:43 +08:00
Tim
18c52c18e3 Merge pull request #277 from nagisa77/codex/refactor-notification-handling-to-async-thread
Enable asynchronous notification sending
2025-07-30 12:56:34 +08:00
Tim
03704b62a7 Merge branch 'main' into codex/refactor-notification-handling-to-async-thread 2025-07-30 12:56:27 +08:00
Tim
2a64e3ed14 Make ResendEmailSender asynchronous 2025-07-30 12:54:59 +08:00
Tim
3b69b1b3ee Merge pull request #276 from nagisa77/codex/fix-test-case-compilation-issues
Fix NotificationService tests
2025-07-30 12:47:51 +08:00
Tim
c7685f9b92 Fix NotificationServiceTest constructor 2025-07-30 12:47:39 +08:00
Tim
9347d423e2 Merge pull request #273 from nagisa77/codex/integrate-browser-notifications-for-website
Add web push notification support
2025-07-30 12:17:24 +08:00
Tim
854401ca8d feat: delete useless code 2025-07-30 12:16:20 +08:00
Tim
995cfdf87e feat: 修改邮件格式 2025-07-30 12:14:11 +08:00
Tim
748f7f9709 feat: 邮件、讯息通知 2025-07-30 12:06:23 +08:00
Tim
fb010607f1 Merge pull request #275 from nagisa77/codex/update-notification-format-for-replies-and-reactions
Improve push notifications
2025-07-30 11:53:56 +08:00
Tim
40c919348f feat: improve push notifications 2025-07-30 11:53:43 +08:00
Tim
fe79a5481a Merge remote-tracking branch 'origin/main' into codex/integrate-browser-notifications-for-website 2025-07-30 11:39:58 +08:00
Tim
02b0628bfc Merge pull request #274 from nagisa77/codex/add-email-notifications-for-user-replies
Add email notifications for replies and reactions
2025-07-30 11:37:55 +08:00
Tim
3464137511 feat: email notifications for replies and reactions 2025-07-30 11:37:40 +08:00
Tim
aa138afe61 feat: 推送链路调整 2025-07-30 11:35:28 +08:00
Tim
dccf8f9d0c feat: add browser push notifications 2025-07-30 10:48:02 +08:00
Tim
69f5745fe8 feat: 邮件发送,修改为中文 2025-07-30 10:42:06 +08:00
Tim
df8c5376f6 feat: delete sleep 1 s 2025-07-29 21:54:11 +08:00
Tim
2ec13ed2c9 feat: 评论排序支持 2025-07-29 21:50:11 +08:00
Tim
75a5be2d3c Merge pull request #272 from nagisa77/codex/add-comment-section-sorting-options
Add comment sorting by date and interactions
2025-07-29 21:33:21 +08:00
Tim
143ceebc00 feat: add comment sorting 2025-07-29 21:33:05 +08:00
Tim
24a46384b0 feat: sort by 2025-07-29 21:21:03 +08:00
Tim
d248be21e2 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-29 21:04:25 +08:00
Tim
27ec900780 fix: 2025-07-29 21:04:14 +08:00
Tim
393128b326 feat: blur 2025-07-29 21:01:51 +08:00
Tim
80a86cec3b Merge pull request #271 from nagisa77/codex/fix-test-case-compilation-error
Fix PostService test import
2025-07-29 19:52:48 +08:00
Tim
d6a1e53646 Fix PostService test compile error 2025-07-29 19:52:37 +08:00
Tim
275360983f feat: update avatar upload 2025-07-29 19:49:57 +08:00
Tim
89241ced04 feat: 搜索层级调整 2025-07-29 19:41:32 +08:00
Tim
3dbeb25a09 feat: 设置页面修改用户名,需要重新发放token 2025-07-29 19:12:39 +08:00
Tim
9aec1afcf5 feat: 活动新增🔥(低成本修改) 2025-07-29 18:41:03 +08:00
Tim
1f18c2b000 feat: 发布rate 明确指引 2025-07-29 18:39:23 +08:00
Tim
74433b507d Merge pull request #270 from nagisa77/codex/add-post-and-comment-rate-limiting
Implement comment and post rate limits
2025-07-29 18:33:43 +08:00
Tim
045693db21 Add rate limit for posts and comments 2025-07-29 18:33:30 +08:00
Tim
b84cc19ee5 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-29 18:32:44 +08:00
Tim
846cb4a2de Merge pull request #269 from nagisa77/codex/refactor-activity-popup-to-separate-file
Add global popups component
2025-07-29 18:32:32 +08:00
Tim
20b0eb0df5 refactor: move milk tea popup logic 2025-07-29 18:32:15 +08:00
Tim
08c78c8666 fix: router 页面宽度设置 2025-07-29 18:31:30 +08:00
Tim
23dc6a971c feat: 文章title 修改 2025-07-29 17:20:20 +08:00
Tim
9ed47c727b Merge pull request #268 from nagisa77/codex/optimize-site-for-seo-visibility
Implement dynamic metadata for post pages
2025-07-29 17:18:08 +08:00
Tim
e4dd121bb6 feat: dynamic meta for post page 2025-07-29 17:17:40 +08:00
Tim
92b7099a44 feat: 修复网站metaData 2025-07-29 17:13:58 +08:00
Tim
9589b10e2e Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-29 17:04:08 +08:00
Tim
2226577b68 feat: 修复网站metaData 2025-07-29 17:03:43 +08:00
Tim
3bc530cbde Merge pull request #267 from nagisa77/codex/fix-unread-indicator-not-disappearing
Fix menu unread indicator
2025-07-29 14:53:49 +08:00
Tim
2a1c19697a fix: update unread notification handling 2025-07-29 14:53:21 +08:00
Tim
1ed65ff184 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-29 14:12:33 +08:00
Tim
2bc64ecc87 Merge pull request #266 from nagisa77/codex/add-popup-for-new-user-activity-guidance
Add milk-tea activity popup for new users
2025-07-29 14:12:21 +08:00
Tim
cdeed1fcb0 fix: 活动页增加loading/个人等级增加loading 2025-07-29 14:11:55 +08:00
Tim
816eacde42 feat: add activity popup for milk tea 2025-07-29 13:50:26 +08:00
Tim
8f76ee80f4 fix: 在 xxx 下回复了 格式修复 2025-07-29 13:49:38 +08:00
Tim
9f3e5df9ca Revert "chore: fix component name for lint"
This reverts commit b900e7c620.
2025-07-29 11:39:54 +08:00
Tim
9255b37533 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-29 11:36:32 +08:00
Tim
b48114a34c Merge pull request #265 from nagisa77/codex/add-popup-for-activity-page
Add milk tea activity popup
2025-07-29 11:36:04 +08:00
Tim
b900e7c620 chore: fix component name for lint 2025-07-29 11:35:49 +08:00
Tim
03164bc87a fix: 【openisle】就是选了类别或者tag之后,想要回到无筛选状态只能从筛选框点击无类别才能回去。点首页无法回到最初状态,是不是不太符合通用逻辑 2025-07-29 10:30:29 +08:00
Tim
cbc3819697 feat: activity page mobile 2025-07-28 16:21:11 +08:00
Tim
5193df6289 feat: redeem ui 2025-07-28 16:07:37 +08:00
Tim
8d6a118e97 Merge pull request #264 from nagisa77/codex/update-dialog-for-milk-tea-exchange
Refine milk tea redeem flow
2025-07-28 15:59:25 +08:00
Tim
de7a2c5b80 Update milk tea redeem dialog and notifications 2025-07-28 15:59:09 +08:00
Tim
54bf778977 feat: coffe redeem 2025-07-28 15:47:25 +08:00
Tim
96827afd41 Merge pull request #263 from nagisa77/codex/enhance-event-page-for-tea-exchange
Add milk tea event detail page
2025-07-28 15:11:43 +08:00
Tim
29c584f059 feat: add milk tea activity page 2025-07-28 15:11:25 +08:00
Tim
9a01a5daf6 feat: 活动页面基础ui 2025-07-28 14:52:26 +08:00
Tim
5a3456b878 Merge pull request #262 from nagisa77/codex/add-activity-module-and-endpoints
Add activity module with milk tea event
2025-07-28 14:12:43 +08:00
Tim
51a3a7b8f8 Add activity module with milk tea event 2025-07-28 14:12:28 +08:00
Tim
bcd34e1019 Merge pull request #261 from nagisa77/codex/add-info-icon-for-experience-module
Add experience info tooltip
2025-07-28 13:56:07 +08:00
Tim
69dbcd2572 Merge branch 'main' into codex/add-info-icon-for-experience-module 2025-07-28 13:55:45 +08:00
Tim
b953b4c8fe feat(profile): add exp info tooltip 2025-07-28 13:55:15 +08:00
Tim
62299d17f0 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-28 13:54:33 +08:00
Tim
38405f89b2 Merge pull request #260 from nagisa77/codex/fix-experience-points-exploitation-issue
Prevent XP farming via deletion
2025-07-28 13:53:06 +08:00
Tim
10080e6d74 fix: track daily experience to prevent abuse 2025-07-28 13:52:50 +08:00
Tim
46531b5461 feat: level 模块 2025-07-28 13:35:59 +08:00
Tim
56c89c3228 Merge branch 'main' of github.com:nagisa77/OpenIsle 2025-07-28 13:11:08 +08:00
Tim
55b545c058 Merge pull request #259 from nagisa77/codex/add-progress-bar-to-profile-page
Add level display on profile page
2025-07-28 13:10:56 +08:00
Tim
2f41da9486 feat: show level progress on profile 2025-07-28 13:10:41 +08:00
Tim
d1843fc58d feat: fotgot password 2025-07-28 13:09:10 +08:00
Tim
14008292d9 Merge pull request #258 from nagisa77/codex/add-password-recovery-feature
Add password recovery
2025-07-28 13:02:39 +08:00
Tim
0c784dc5cc feat: add password recovery 2025-07-28 13:02:02 +08:00
Tim
ef6ad46d12 Merge pull request #257 from nagisa77/codex/add-leveling-system-with-experience-rewards
Implement user leveling and rewards
2025-07-28 12:40:50 +08:00
Tim
1c2751422d feat: add user leveling and experience system 2025-07-28 12:34:45 +08:00
tim
a5900aa60d feat: ui 调整 2025-07-26 18:08:03 +08:00
tim
0028a501fb feat: 优化google登录提醒 2025-07-26 17:09:44 +08:00
417 changed files with 31868 additions and 21727 deletions

23
.github/workflows/deploy-staging.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Staging CI & CD
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: Deploy
steps:
- uses: actions/checkout@v4
- name: Deploy to Server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: root
key: ${{ secrets.SSH_KEY }}
script: bash /opt/openisle/deploy-staging.sh

View File

@@ -1,9 +1,9 @@
name: CI & CD
on:
push:
branches: [main]
workflow_dispatch:
schedule:
- cron: "0 19 * * *" # 每天 UTC 19:00相当于北京时间凌晨3点
jobs:
build-and-deploy:
@@ -11,29 +11,12 @@ jobs:
environment: Deploy
steps:
- uses: actions/checkout@v4
# - uses: actions/setup-java@v4
# with:
# java-version: '17'
# distribution: 'temurin'
# - run: mvn -B clean package -DskipTests
# - uses: actions/setup-node@v4
# with:
# node-version: '20'
# - run: |
# cd open-isle-cli
# npm ci
# npm run build
- name: Deploy to Server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: root
key: ${{ secrets.SSH_KEY }}
script: bash /opt/openisle/deploy.sh
- uses: actions/checkout@v4
- name: Deploy to Server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: root
key: ${{ secrets.SSH_KEY }}
script: bash /opt/openisle/deploy.sh

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@
target
openisle.iml
node_modules
dist
dist
open-isle.env
logs

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@@ -10,19 +10,39 @@
OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台,提供用户注册、登录、贴文发布、评论交互等完整功能,可用于项目社区或直接打造自主社区站点。
## 🚀 部署
## 🚧 开发
### 后端
1. 确保安装 JDK 17 及 Maven
2. 信息配置修改 `src/main/resources/application.properties`,或通过环境变量设置数据库等参数
3. 执行 `mvn clean package` 生成包,之后使用 `java -jar target/openisle-0.0.1-SNAPSHOT.jar`启动,或在开发时直接使用 `mvn spring-boot:run`
### 前端
1. `cd open-isle-cli`
2. 执行 `npm install`
3. `npm run serve`可在本地启动开发服务,产品环境使用 `npm run build`生成 `dist/` 文件,配合线上网站方式部署
1. 进入前端目录
```bash
cd frontend_nuxt
```
2. 安装依赖
```bash
npm install
```
3. 启动开发服务
```bash
npm run dev
```
生产版本使用如下命令编译:
```bash
npm run build
```
会在 `.output` 目录生成文件,配合线上网站方式部署
## ✨ 项目特点
- JWT 认证以及 Google、GitHub、Discord、Twitter 等多种 OAuth 登录
- 支持分类、标签的贴文管理以及草稿保存功能
- 嵌套评论、指定贴文或评论的点赞/抖弹系统
@@ -31,14 +51,18 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台
- 集成 OpenAI 提供的 Markdown 格式化功能
- 通过环境变量可调整密码强度、登录方式、保护码等多种配置
- 支持图片上传,默认使用腾讯云 COS 扩展
- 默认头像使用 DiceBear Avatars可通过 `AVATAR_STYLE` 和 `AVATAR_SIZE` 环境变量自定义主题和大小
- 浏览器推送通知,离开网站也能及时收到提醒
## 🌟 项目优势
- 全面开源,便于二次开发和自定义扩展
- Spring Boot + Vue 3 成熟技术栈,学习起点低,社区资源丰富
- 支持多种登录方式和角色权限,容易展展到不同场景
- 模块化设计,代码结构清晰,维护成本低
- REST API 可接入任意前端框架,兼容多端平台
- 配置简单,通过环境变量快速调整和部署
- 如需推送通知,请设置 `WEBPUSH_PUBLIC_KEY` 和 `WEBPUSH_PRIVATE_KEY` 环境变量
## 🏘️ 社区
@@ -49,6 +73,7 @@ OpenIsle 是一个使用 Spring Boot 和 Vue 3 构建的全栈开源社区平台
本项目以 MIT License 发布,欢迎自由使用与修改。
## 🙏 鼓赞
- [Spring Boot](https://spring.io/projects/spring-boot)
- [JJWT](https://github.com/jwtk/jjwt)
- [Lombok](https://github.com/projectlombok/lombok)

View File

@@ -0,0 +1,39 @@
# === Database ===
MYSQL_URL=jdbc:mysql://<数据库地址>:<端口>/<数据库名>?useUnicode=yes&characterEncoding=UTF-8&useInformationSchema=true&useSSL=false&serverTimezone=UTC
MYSQL_USER=<数据库用户名>
MYSQL_PASSWORD=<数据库密码>
# === JWT ===
JWT_SECRET=<jwt secret>
JWT_REASON_SECRET=<jwt reason secret>
JWT_RESET_SECRET=<jwt reset secret>
JWT_INVITE_SECRET=<jwt invite secret>
JWT_EXPIRATION=2592000000
# === Resend ===
RESEND_API_KEY=<你的resend-api-key>
# === COS ===
# COS_BASE_URL=https://<你的cos>.cos.ap-guangzhou.myqcloud.com
COS_BASE_URL=https://<你的cos>.cos.accelerate.myqcloud.com
COS_SECRET_ID=<你的cos-secret-id>
COS_SECRET_KEY=<你的cos-secret-key>
COS_BUCKET_NAME=<你的cos-bucket-name>
# === OAuth ===
GOOGLE_CLIENT_ID=<你的google-client-id>
GITHUB_CLIENT_ID=<你的github-client-id>
GITHUB_CLIENT_SECRET=<你的github-client-secret>
TWITTER_CLIENT_ID=<你的twitter-client-id>
TWITTER_CLIENT_SECRET=<你的-twitter-client-secret>
DISCORD_CLIENT_ID=<你的discord-client-id>
DISCORD_CLIENT_SECRET=<你的discord-client-secret>
# === OPENAI ===
OPENAI_API_KEY=<你的openai-api-key>
# === Webpush ===
WEBPUSH_PUBLIC_KEY=<你的webpush-public-key>
WEBPUSH_PRIVATE_KEY=<你的webpush-private-key>
# LOG_LEVEL=DEBUG

View File

@@ -90,6 +90,16 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>nl.martijndwars</groupId>
<artifactId>web-push</artifactId>
<version>5.1.1</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
</dependencies>
<build>

View File

@@ -2,8 +2,10 @@ package com.openisle;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class OpenIsleApplication {
public static void main(String[] args) {
SpringApplication.run(OpenIsleApplication.class, args);

View File

@@ -0,0 +1,39 @@
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;
import java.time.LocalDate;
import java.time.LocalDateTime;
@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);
a.setIcon("https://icons.veryicon.com/png/o/food--drinks/delicious-food-1/coffee-36.png");
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);
}
}
}

View File

@@ -0,0 +1,23 @@
package com.openisle.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "notificationExecutor")
public Executor notificationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("notification-");
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,31 @@
package com.openisle.config;
import com.openisle.model.PointGood;
import com.openisle.repository.PointGoodRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/** Initialize default point mall goods. */
@Component
@RequiredArgsConstructor
public class PointGoodInitializer implements CommandLineRunner {
private final PointGoodRepository pointGoodRepository;
@Override
public void run(String... args) {
if (pointGoodRepository.count() == 0) {
PointGood g1 = new PointGood();
g1.setName("GPT Plus 1 个月");
g1.setCost(20000);
g1.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/chatgpt.png");
pointGoodRepository.save(g1);
PointGood g2 = new PointGood();
g2.setName("奶茶");
g2.setCost(5000);
g2.setImage("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/icons/coffee.png");
pointGoodRepository.save(g2);
}
}
}

View File

@@ -0,0 +1,20 @@
package com.openisle.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.TaskScheduler;
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(2);
scheduler.setThreadNamePrefix("lottery-");
scheduler.initialize();
return scheduler;
}
}

View File

@@ -74,11 +74,17 @@ public class SecurityConfig {
CorsConfiguration cfg = new CorsConfiguration();
cfg.setAllowedOrigins(List.of(
"http://127.0.0.1:8080",
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
"http://127.0.0.1",
"http://localhost:8080",
"http://localhost:3000",
"http://localhost:3001",
"http://localhost",
"http://30.211.97.254:8080",
"http://30.211.97.254",
"http://30.211.97.238:3000",
"http://30.211.97.238",
"http://192.168.7.98",
"http://192.168.7.98:3000",
websiteUrl,
websiteUrl.replace("://www.", "://")
));
@@ -108,11 +114,18 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.POST,"/api/auth/reason").permitAll()
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/medals/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/push/public-key").permitAll()
.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")
.requestMatchers(HttpMethod.DELETE, "/api/tags/**").hasAuthority("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/stats/**").hasAuthority("ADMIN")
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated()
)
@@ -137,8 +150,11 @@ 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") || 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 ")) {
String token = authHeader.substring(7);

View File

@@ -0,0 +1,57 @@
package com.openisle.controller;
import com.openisle.dto.ActivityDto;
import com.openisle.dto.MilkTeaInfoDto;
import com.openisle.dto.MilkTeaRedeemRequest;
import com.openisle.mapper.ActivityMapper;
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.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/activities")
@RequiredArgsConstructor
public class ActivityController {
private final ActivityService activityService;
private final UserService userService;
private final ActivityMapper activityMapper;
@GetMapping
public List<ActivityDto> list() {
return activityService.list().stream()
.map(activityMapper::toDto)
.collect(Collectors.toList());
}
@GetMapping("/milk-tea")
public MilkTeaInfoDto milkTea() {
Activity a = activityService.getByType(ActivityType.MILK_TEA);
long count = activityService.countParticipants(a);
if (!a.isEnded() && count >= 50) {
activityService.end(a);
}
MilkTeaInfoDto info = new MilkTeaInfoDto();
info.setRedeemCount(count);
info.setEnded(a.isEnded());
return info;
}
@PostMapping("/milk-tea/redeem")
public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
Activity a = activityService.getByType(ActivityType.MILK_TEA);
boolean first = activityService.redeem(a, user, req.getContact());
if (first) {
return java.util.Map.of("message", "redeemed");
}
return java.util.Map.of("message", "updated");
}
}

View File

@@ -0,0 +1,29 @@
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

@@ -1,13 +1,10 @@
package com.openisle.controller;
import com.openisle.model.PasswordStrength;
import com.openisle.model.PublishMode;
import com.openisle.dto.ConfigDto;
import com.openisle.service.AiUsageService;
import com.openisle.service.PasswordValidator;
import com.openisle.service.PostService;
import com.openisle.service.AiUsageService;
import com.openisle.service.RegisterModeService;
import com.openisle.model.RegisterMode;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -47,11 +44,4 @@ public class AdminConfigController {
return getConfig();
}
@Data
public static class ConfigDto {
private PublishMode publishMode;
private PasswordStrength passwordStrength;
private Integer aiFormatLimit;
private RegisterMode registerMode;
}
}

View File

@@ -0,0 +1,48 @@
package com.openisle.controller;
import com.openisle.dto.PostSummaryDto;
import com.openisle.mapper.PostMapper;
import com.openisle.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* Endpoints for administrators to manage posts.
*/
@RestController
@RequestMapping("/api/admin/posts")
@RequiredArgsConstructor
public class AdminPostController {
private final PostService postService;
private final PostMapper postMapper;
@GetMapping("/pending")
public List<PostSummaryDto> pendingPosts() {
return postService.listPendingPosts().stream()
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
}
@PostMapping("/{id}/approve")
public PostSummaryDto approve(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.approvePost(id));
}
@PostMapping("/{id}/reject")
public PostSummaryDto reject(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.rejectPost(id));
}
@PostMapping("/{id}/pin")
public PostSummaryDto pin(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.pinPost(id));
}
@PostMapping("/{id}/unpin")
public PostSummaryDto unpin(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.unpinPost(id));
}
}

View File

@@ -1,9 +1,10 @@
package com.openisle.controller;
import com.openisle.dto.TagDto;
import com.openisle.mapper.TagMapper;
import com.openisle.model.Tag;
import com.openisle.service.TagService;
import com.openisle.service.PostService;
import lombok.Data;
import com.openisle.service.TagService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -16,11 +17,12 @@ import java.util.stream.Collectors;
public class AdminTagController {
private final TagService tagService;
private final PostService postService;
private final TagMapper tagMapper;
@GetMapping("/pending")
public List<TagDto> pendingTags() {
return tagService.listPendingTags().stream()
.map(t -> toDto(t, postService.countPostsByTag(t.getId())))
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
.collect(Collectors.toList());
}
@@ -28,27 +30,6 @@ public class AdminTagController {
public TagDto approve(@PathVariable Long id) {
Tag tag = tagService.approveTag(id);
long count = postService.countPostsByTag(tag.getId());
return toDto(tag, count);
}
private TagDto toDto(Tag tag, long count) {
TagDto dto = new TagDto();
dto.setId(tag.getId());
dto.setName(tag.getName());
dto.setDescription(tag.getDescription());
dto.setIcon(tag.getIcon());
dto.setSmallIcon(tag.getSmallIcon());
dto.setCount(count);
return dto;
}
@Data
private static class TagDto {
private Long id;
private String name;
private String description;
private String icon;
private String smallIcon;
private Long count;
return tagMapper.toDto(tag, count);
}
}

View File

@@ -1,7 +1,10 @@
package com.openisle.controller;
import com.openisle.model.Notification;
import com.openisle.model.NotificationType;
import com.openisle.model.User;
import com.openisle.service.EmailSender;
import com.openisle.repository.NotificationRepository;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
@@ -13,6 +16,7 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
public class AdminUserController {
private final UserRepository userRepository;
private final NotificationRepository notificationRepository;
private final EmailSender emailSender;
@Value("${app.website-url}")
private String websiteUrl;
@@ -22,8 +26,9 @@ public class AdminUserController {
User user = userRepository.findById(id).orElseThrow();
user.setApproved(true);
userRepository.save(user);
emailSender.sendEmail(user.getEmail(), "Registration Approved",
"Your account has been approved. Visit: " + websiteUrl);
markRegisterRequestNotificationsRead(user);
emailSender.sendEmail(user.getEmail(), "您的注册已审核通过",
"🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl);
return ResponseEntity.ok().build();
}
@@ -32,8 +37,18 @@ public class AdminUserController {
User user = userRepository.findById(id).orElseThrow();
user.setApproved(false);
userRepository.save(user);
emailSender.sendEmail(user.getEmail(), "Registration Rejected",
"Your account request was rejected. Visit: " + websiteUrl);
markRegisterRequestNotificationsRead(user);
emailSender.sendEmail(user.getEmail(), "您的注册已被管理员拒绝",
"您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl);
return ResponseEntity.ok().build();
}
private void markRegisterRequestNotificationsRead(User applicant) {
java.util.List<Notification> notifs =
notificationRepository.findByTypeAndFromUser(NotificationType.REGISTER_REQUEST, applicant);
for (Notification n : notifs) {
n.setRead(true);
}
notificationRepository.saveAll(notifs);
}
}

View File

@@ -1,24 +1,16 @@
package com.openisle.controller;
import com.openisle.model.User;
import com.openisle.service.EmailSender;
import com.openisle.service.JwtService;
import com.openisle.service.UserService;
import com.openisle.service.CaptchaService;
import com.openisle.service.GoogleAuthService;
import com.openisle.service.GithubAuthService;
import com.openisle.service.DiscordAuthService;
import com.openisle.service.TwitterAuthService;
import com.openisle.service.RegisterModeService;
import com.openisle.service.NotificationService;
import com.openisle.dto.*;
import com.openisle.exception.FieldException;
import com.openisle.model.RegisterMode;
import com.openisle.model.User;
import com.openisle.repository.UserRepository;
import lombok.Data;
import com.openisle.service.*;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Optional;
@@ -37,6 +29,8 @@ public class AuthController {
private final RegisterModeService registerModeService;
private final NotificationService notificationService;
private final UserRepository userRepository;
private final InviteService inviteService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@@ -52,9 +46,29 @@ public class AuthController {
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
}
if (req.getInviteToken() != null && !req.getInviteToken().isEmpty()) {
if (!inviteService.validate(req.getInviteToken())) {
return ResponseEntity.badRequest().body(Map.of("error", "邀请码使用次数过多"));
}
try {
User user = userService.registerWithInvite(
req.getUsername(), req.getEmail(), req.getPassword());
inviteService.consume(req.getInviteToken());
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(user.getUsername()),
"reason_code", "INVITE_APPROVED"
));
} catch (FieldException e) {
return ResponseEntity.badRequest().body(Map.of(
"field", e.getField(),
"error", e.getMessage()
));
}
}
User user = userService.register(
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode());
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
if (!user.isApproved()) {
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
}
@@ -65,10 +79,26 @@ public class AuthController {
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
if (ok) {
return ResponseEntity.ok(Map.of(
"message", "Verified",
"token", jwtService.generateReasonToken(req.getUsername())
));
Optional<User> userOpt = userService.findByUsername(req.getUsername());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
}
User user = userOpt.get();
if (user.isApproved()) {
return ResponseEntity.ok(Map.of(
"message", "Verified and isApproved",
"reason_code", "VERIFIED_AND_APPROVED",
"token", jwtService.generateToken(req.getUsername())
));
} else {
return ResponseEntity.ok(Map.of(
"message", "Verified",
"reason_code", "VERIFIED",
"token", jwtService.generateReasonToken(req.getUsername())
));
}
}
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
}
@@ -90,7 +120,7 @@ public class AuthController {
User user = userOpt.get();
if (!user.isVerified()) {
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode());
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
return ResponseEntity.badRequest().body(Map.of(
"error", "User not verified",
"reason_code", "NOT_VERIFIED",
@@ -113,27 +143,42 @@ public class AuthController {
@PostMapping("/google")
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
Optional<User> user = googleAuthService.authenticate(req.getIdToken(), registerModeService.getRegisterMode());
if (user.isPresent()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
}
Optional<AuthResult> resultOpt = googleAuthService.authenticate(
req.getIdToken(),
registerModeService.getRegisterMode(),
viaInvite);
if (resultOpt.isPresent()) {
AuthResult result = resultOpt.get();
if (viaInvite && result.isNewUser()) {
inviteService.consume(req.getInviteToken());
return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(result.getUser().getUsername()),
"reason_code", "INVITE_APPROVED"
));
}
if (!user.get().isApproved()) {
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
if (!result.getUser().isApproved()) {
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "IS_APPROVING",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "NOT_APPROVED",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Invalid google token",
@@ -153,8 +198,8 @@ public class AuthController {
));
}
if (req.reason == null || req.reason.length() <= 20) {
return ResponseEntity.badRequest().body(Map.of(
if (req.getReason() == null || req.getReason().trim().length() <= 20) {
return ResponseEntity.badRequest().body(Map.of(
"error", "Reason's length must longer than 20",
"reason_code", "INVALID_CREDENTIALS"
));
@@ -172,28 +217,44 @@ public class AuthController {
@PostMapping("/github")
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
Optional<User> user = githubAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
if (user.isPresent()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
}
Optional<AuthResult> resultOpt = githubAuthService.authenticate(
req.getCode(),
registerModeService.getRegisterMode(),
req.getRedirectUri(),
viaInvite);
if (resultOpt.isPresent()) {
AuthResult result = resultOpt.get();
if (viaInvite && result.isNewUser()) {
inviteService.consume(req.getInviteToken());
return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(result.getUser().getUsername()),
"reason_code", "INVITE_APPROVED"
));
}
if (!user.get().isApproved()) {
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
if (!result.getUser().isApproved()) {
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
// 已填写注册理由
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "IS_APPROVING",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "NOT_APPROVED",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Invalid github code",
@@ -203,27 +264,43 @@ public class AuthController {
@PostMapping("/discord")
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
Optional<User> user = discordAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
if (user.isPresent()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
}
Optional<AuthResult> resultOpt = discordAuthService.authenticate(
req.getCode(),
registerModeService.getRegisterMode(),
req.getRedirectUri(),
viaInvite);
if (resultOpt.isPresent()) {
AuthResult result = resultOpt.get();
if (viaInvite && result.isNewUser()) {
inviteService.consume(req.getInviteToken());
return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(result.getUser().getUsername()),
"reason_code", "INVITE_APPROVED"
));
}
if (!user.get().isApproved()) {
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
if (!result.getUser().isApproved()) {
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "IS_APPROVING",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "NOT_APPROVED",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Invalid discord code",
@@ -233,31 +310,44 @@ public class AuthController {
@PostMapping("/twitter")
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
Optional<User> user = twitterAuthService.authenticate(
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
}
Optional<AuthResult> resultOpt = twitterAuthService.authenticate(
req.getCode(),
req.getCodeVerifier(),
registerModeService.getRegisterMode(),
req.getRedirectUri());
if (user.isPresent()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
req.getRedirectUri(),
viaInvite);
if (resultOpt.isPresent()) {
AuthResult result = resultOpt.get();
if (viaInvite && result.isNewUser()) {
inviteService.consume(req.getInviteToken());
return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(result.getUser().getUsername()),
"reason_code", "INVITE_APPROVED"
));
}
if (!user.get().isApproved()) {
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
if (!result.getUser().isApproved()) {
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "IS_APPROVING",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Account awaiting approval",
"reason_code", "NOT_APPROVED",
"token", jwtService.generateReasonToken(user.get().getUsername())
"token", jwtService.generateReasonToken(result.getUser().getUsername())
));
}
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
}
return ResponseEntity.badRequest().body(Map.of(
"error", "Invalid twitter code",
@@ -270,54 +360,40 @@ public class AuthController {
return ResponseEntity.ok(Map.of("valid", true));
}
@Data
private static class RegisterRequest {
private String username;
private String email;
private String password;
private String captcha;
@PostMapping("/forgot/send")
public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
Optional<User> userOpt = userService.findByEmail(req.getEmail());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
}
String code = userService.generatePasswordResetCode(req.getEmail());
emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code);
return ResponseEntity.ok(Map.of("message", "Verification code sent"));
}
@Data
private static class LoginRequest {
private String username;
private String password;
private String captcha;
@PostMapping("/forgot/verify")
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode());
if (ok) {
String username = userService.findByEmail(req.getEmail()).get().getUsername();
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
}
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
}
@Data
private static class GoogleLoginRequest {
private String idToken;
@PostMapping("/forgot/reset")
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
String username = jwtService.validateAndGetSubjectForReset(req.getToken());
try {
userService.updatePassword(username, req.getPassword());
return ResponseEntity.ok(Map.of("message", "Password updated"));
} catch (FieldException e) {
return ResponseEntity.badRequest().body(Map.of(
"field", e.getField(),
"error", e.getMessage()
));
}
}
@Data
private static class GithubLoginRequest {
private String code;
private String redirectUri;
}
@Data
private static class DiscordLoginRequest {
private String code;
private String redirectUri;
}
@Data
private static class TwitterLoginRequest {
private String code;
private String redirectUri;
private String codeVerifier;
}
@Data
private static class VerifyRequest {
private String username;
private String code;
}
@Data
private static class MakeReasonRequest {
private String token;
private String reason;
}
// DTO classes moved to com.openisle.dto package
}

View File

@@ -1,13 +1,18 @@
package com.openisle.controller;
import com.openisle.dto.CategoryDto;
import com.openisle.dto.CategoryRequest;
import com.openisle.dto.PostSummaryDto;
import com.openisle.mapper.CategoryMapper;
import com.openisle.mapper.PostMapper;
import com.openisle.model.Category;
import com.openisle.service.CategoryService;
import com.openisle.service.PostService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@@ -16,19 +21,21 @@ import java.util.stream.Collectors;
public class CategoryController {
private final CategoryService categoryService;
private final PostService postService;
private final PostMapper postMapper;
private final CategoryMapper categoryMapper;
@PostMapping
public CategoryDto create(@RequestBody CategoryRequest req) {
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId());
return toDto(c, count);
return categoryMapper.toDto(c, count);
}
@PutMapping("/{id}")
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId());
return toDto(c, count);
return categoryMapper.toDto(c, count);
}
@DeleteMapping("/{id}")
@@ -38,8 +45,11 @@ public class CategoryController {
@GetMapping
public List<CategoryDto> list() {
return categoryService.listCategories().stream()
.map(c -> toDto(c, postService.countPostsByCategory(c.getId())))
List<Category> all = categoryService.listCategories();
List<Long> ids = all.stream().map(Category::getId).toList();
Map<Long, Long> postsCntByCategoryIds = postService.countPostsByCategoryIds(ids);
return all.stream()
.map(c -> categoryMapper.toDto(c, postsCntByCategoryIds.getOrDefault(c.getId(), 0L)))
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
.collect(Collectors.toList());
}
@@ -48,7 +58,7 @@ public class CategoryController {
public CategoryDto get(@PathVariable Long id) {
Category c = categoryService.getCategory(id);
long count = postService.countPostsByCategory(c.getId());
return toDto(c, count);
return categoryMapper.toDto(c, count);
}
@GetMapping("/{id}/posts")
@@ -57,47 +67,7 @@ public class CategoryController {
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
return postService.listPostsByCategories(java.util.List.of(id), page, pageSize)
.stream()
.map(p -> {
PostSummaryDto dto = new PostSummaryDto();
dto.setId(p.getId());
dto.setTitle(p.getTitle());
return dto;
})
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
}
private CategoryDto toDto(Category c, long count) {
CategoryDto dto = new CategoryDto();
dto.setId(c.getId());
dto.setName(c.getName());
dto.setIcon(c.getIcon());
dto.setSmallIcon(c.getSmallIcon());
dto.setDescription(c.getDescription());
dto.setCount(count);
return dto;
}
@Data
private static class CategoryRequest {
private String name;
private String description;
private String icon;
private String smallIcon;
}
@Data
private static class CategoryDto {
private Long id;
private String name;
private String description;
private String icon;
private String smallIcon;
private Long count;
}
@Data
private static class PostSummaryDto {
private Long id;
private String title;
}
}

View File

@@ -0,0 +1,100 @@
package com.openisle.controller;
import com.openisle.model.Comment;
import com.openisle.dto.CommentDto;
import com.openisle.dto.CommentRequest;
import com.openisle.mapper.CommentMapper;
import com.openisle.service.CaptchaService;
import com.openisle.service.CommentService;
import com.openisle.service.LevelService;
import com.openisle.service.PointService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class CommentController {
private final CommentService commentService;
private final LevelService levelService;
private final CaptchaService captchaService;
private final CommentMapper commentMapper;
private final PointService pointService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@Value("${app.captcha.comment-enabled:false}")
private boolean commentCaptchaEnabled;
@PostMapping("/posts/{postId}/comments")
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
@RequestBody CommentRequest req,
Authentication auth) {
log.debug("createComment called by user {} for post {}", auth.getName(), postId);
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
log.debug("Captcha verification failed for user {} on post {}", auth.getName(), postId);
return ResponseEntity.badRequest().build();
}
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
CommentDto dto = commentMapper.toDto(comment);
dto.setReward(levelService.awardForComment(auth.getName()));
dto.setPointReward(pointService.awardForComment(auth.getName(),postId));
log.debug("createComment succeeded for comment {}", comment.getId());
return ResponseEntity.ok(dto);
}
@PostMapping("/comments/{commentId}/replies")
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
@RequestBody CommentRequest req,
Authentication auth) {
log.debug("replyComment called by user {} for comment {}", auth.getName(), commentId);
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
log.debug("Captcha verification failed for user {} on comment {}", auth.getName(), commentId);
return ResponseEntity.badRequest().build();
}
Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent());
CommentDto dto = commentMapper.toDto(comment);
dto.setReward(levelService.awardForComment(auth.getName()));
log.debug("replyComment succeeded for comment {}", comment.getId());
return ResponseEntity.ok(dto);
}
@GetMapping("/posts/{postId}/comments")
public List<CommentDto> listComments(@PathVariable Long postId,
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) {
log.debug("listComments called for post {} with sort {}", postId, sort);
List<CommentDto> list = commentService.getCommentsForPost(postId, sort).stream()
.map(commentMapper::toDtoWithReplies)
.collect(Collectors.toList());
log.debug("listComments returning {} comments", list.size());
return list;
}
@DeleteMapping("/comments/{id}")
public void deleteComment(@PathVariable Long id, Authentication auth) {
log.debug("deleteComment called by user {} for comment {}", auth.getName(), id);
commentService.deleteComment(auth.getName(), 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

@@ -1,9 +1,8 @@
package com.openisle.controller;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import com.openisle.dto.SiteConfigDto;
import com.openisle.service.RegisterModeService;
import com.openisle.model.RegisterMode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -34,8 +33,8 @@ public class ConfigController {
private final RegisterModeService registerModeService;
@GetMapping("/config")
public ConfigResponse getConfig() {
ConfigResponse resp = new ConfigResponse();
public SiteConfigDto getConfig() {
SiteConfigDto resp = new SiteConfigDto();
resp.setCaptchaEnabled(captchaEnabled);
resp.setRegisterCaptchaEnabled(registerCaptchaEnabled);
resp.setLoginCaptchaEnabled(loginCaptchaEnabled);
@@ -45,15 +44,4 @@ public class ConfigController {
resp.setRegisterMode(registerModeService.getRegisterMode());
return resp;
}
@Data
private static class ConfigResponse {
private boolean captchaEnabled;
private boolean registerCaptchaEnabled;
private boolean loginCaptchaEnabled;
private boolean postCaptchaEnabled;
private boolean commentCaptchaEnabled;
private int aiFormatLimit;
private RegisterMode registerMode;
}
}

View File

@@ -1,32 +1,32 @@
package com.openisle.controller;
import com.openisle.dto.DraftDto;
import com.openisle.dto.DraftRequest;
import com.openisle.mapper.DraftMapper;
import com.openisle.model.Draft;
import com.openisle.service.DraftService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/drafts")
@RequiredArgsConstructor
public class DraftController {
private final DraftService draftService;
private final DraftMapper draftMapper;
@PostMapping
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
return ResponseEntity.ok(toDto(draft));
return ResponseEntity.ok(draftMapper.toDto(draft));
}
@GetMapping("/me")
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
return draftService.getDraft(auth.getName())
.map(d -> ResponseEntity.ok(toDto(d)))
.map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
.orElseGet(() -> ResponseEntity.noContent().build());
}
@@ -35,33 +35,4 @@ public class DraftController {
draftService.deleteDraft(auth.getName());
return ResponseEntity.ok().build();
}
private DraftDto toDto(Draft draft) {
DraftDto dto = new DraftDto();
dto.setId(draft.getId());
dto.setTitle(draft.getTitle());
dto.setContent(draft.getContent());
if (draft.getCategory() != null) {
dto.setCategoryId(draft.getCategory().getId());
}
dto.setTagIds(draft.getTags().stream().map(com.openisle.model.Tag::getId).collect(Collectors.toList()));
return dto;
}
@Data
private static class DraftRequest {
private String title;
private String content;
private Long categoryId;
private List<Long> tagIds;
}
@Data
private static class DraftDto {
private Long id;
private String title;
private String content;
private Long categoryId;
private List<Long> tagIds;
}
}

View File

@@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.openisle.exception.FieldException;
import com.openisle.exception.NotFoundException;
import com.openisle.exception.RateLimitException;
import java.util.Map;
@@ -22,9 +23,18 @@ public class GlobalExceptionHandler {
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
}
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<?> handleRateLimitException(RateLimitException ex) {
return ResponseEntity.status(429).body(Map.of("error", ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
String message = ex.getMessage();
if (message == null) {
message = ex.getClass().getSimpleName();
}
return ResponseEntity.badRequest().body(Map.of("error", message));
}
}

View File

@@ -0,0 +1,23 @@
package com.openisle.controller;
import com.openisle.service.InviteService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/invite")
@RequiredArgsConstructor
public class InviteController {
private final InviteService inviteService;
@PostMapping("/generate")
public Map<String, String> generate(Authentication auth) {
String token = inviteService.generate(auth.getName());
return Map.of("token", token);
}
}

View File

@@ -0,0 +1,33 @@
package com.openisle.controller;
import com.openisle.dto.MedalDto;
import com.openisle.dto.MedalSelectRequest;
import com.openisle.service.MedalService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/medals")
@RequiredArgsConstructor
public class MedalController {
private final MedalService medalService;
@GetMapping
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
return medalService.getMedals(userId);
}
@PostMapping("/select")
public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) {
try {
medalService.selectMedal(auth.getName(), req.getType());
return ResponseEntity.ok().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
}

View File

@@ -0,0 +1,55 @@
package com.openisle.controller;
import com.openisle.dto.NotificationDto;
import com.openisle.dto.NotificationMarkReadRequest;
import com.openisle.dto.NotificationUnreadCountDto;
import com.openisle.dto.NotificationPreferenceDto;
import com.openisle.dto.NotificationPreferenceUpdateRequest;
import com.openisle.mapper.NotificationMapper;
import com.openisle.service.NotificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/** Endpoints for user notifications. */
@RestController
@RequestMapping("/api/notifications")
@RequiredArgsConstructor
public class NotificationController {
private final NotificationService notificationService;
private final NotificationMapper notificationMapper;
@GetMapping
public List<NotificationDto> list(@RequestParam(value = "read", required = false) Boolean read,
Authentication auth) {
return notificationService.listNotifications(auth.getName(), read).stream()
.map(notificationMapper::toDto)
.collect(Collectors.toList());
}
@GetMapping("/unread-count")
public NotificationUnreadCountDto unreadCount(Authentication auth) {
long count = notificationService.countUnread(auth.getName());
NotificationUnreadCountDto uc = new NotificationUnreadCountDto();
uc.setCount(count);
return uc;
}
@PostMapping("/read")
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
notificationService.markRead(auth.getName(), req.getIds());
}
@GetMapping("/prefs")
public List<NotificationPreferenceDto> prefs(Authentication auth) {
return notificationService.listPreferences(auth.getName());
}
@PostMapping("/prefs")
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
}
}

View File

@@ -0,0 +1,39 @@
package com.openisle.controller;
import com.openisle.dto.PointGoodDto;
import com.openisle.dto.PointRedeemRequest;
import com.openisle.mapper.PointGoodMapper;
import com.openisle.model.User;
import com.openisle.service.PointMallService;
import com.openisle.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** REST controller for point mall. */
@RestController
@RequestMapping("/api/point-goods")
@RequiredArgsConstructor
public class PointMallController {
private final PointMallService pointMallService;
private final UserService userService;
private final PointGoodMapper pointGoodMapper;
@GetMapping
public List<PointGoodDto> list() {
return pointMallService.listGoods().stream()
.map(pointGoodMapper::toDto)
.collect(Collectors.toList());
}
@PostMapping("/redeem")
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
int point = pointMallService.redeem(user, req.getGoodId(), req.getContact());
return Map.of("point", point);
}
}

View File

@@ -0,0 +1,164 @@
package com.openisle.controller;
import com.openisle.dto.PostDetailDto;
import com.openisle.dto.PostRequest;
import com.openisle.dto.PostSummaryDto;
import com.openisle.mapper.PostMapper;
import com.openisle.model.Post;
import com.openisle.service.*;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
private final LevelService levelService;
private final CaptchaService captchaService;
private final DraftService draftService;
private final UserVisitService userVisitService;
private final PostMapper postMapper;
private final PointService pointService;
@Value("${app.captcha.enabled:false}")
private boolean captchaEnabled;
@Value("${app.captcha.post-enabled:false}")
private boolean postCaptchaEnabled;
@PostMapping
public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) {
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().build();
}
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
req.getTitle(), req.getContent(), req.getTagIds(),
req.getType(), req.getPrizeDescription(), req.getPrizeIcon(),
req.getPrizeCount(), req.getStartTime(), req.getEndTime());
draftService.deleteDraft(auth.getName());
PostDetailDto dto = postMapper.toDetailDto(post, auth.getName());
dto.setReward(levelService.awardForPost(auth.getName()));
dto.setPointReward(pointService.awardForPost(auth.getName()));
return ResponseEntity.ok(dto);
}
@PutMapping("/{id}")
public ResponseEntity<PostDetailDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req,
Authentication auth) {
Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(),
req.getTitle(), req.getContent(), req.getTagIds());
return ResponseEntity.ok(postMapper.toDetailDto(post, auth.getName()));
}
@DeleteMapping("/{id}")
public void deletePost(@PathVariable Long id, Authentication auth) {
postService.deletePost(id, auth.getName());
}
@GetMapping("/{id}")
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
String viewer = auth != null ? auth.getName() : null;
Post post = postService.viewPost(id, viewer);
return ResponseEntity.ok(postMapper.toDetailDto(post, viewer));
}
@PostMapping("/{id}/lottery/join")
public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
postService.joinLottery(id, auth.getName());
return ResponseEntity.ok().build();
}
@GetMapping
public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId,
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize,
Authentication auth) {
List<Long> ids = categoryIds;
if (categoryId != null) {
ids = java.util.List.of(categoryId);
}
List<Long> tids = tagIds;
if (tagId != null) {
tids = java.util.List.of(tagId);
}
if (auth != null) {
userVisitService.recordVisit(auth.getName());
}
boolean hasCategories = ids != null && !ids.isEmpty();
boolean hasTags = tids != null && !tids.isEmpty();
if (hasCategories && hasTags) {
return postService.listPostsByCategoriesAndTags(ids, tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
}
if (hasTags) {
return postService.listPostsByTags(tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
}
return postService.listPostsByCategories(ids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
}
@GetMapping("/ranking")
public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId,
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize,
Authentication auth) {
List<Long> ids = categoryIds;
if (categoryId != null) {
ids = java.util.List.of(categoryId);
}
List<Long> tids = tagIds;
if (tagId != null) {
tids = java.util.List.of(tagId);
}
if (auth != null) {
userVisitService.recordVisit(auth.getName());
}
return postService.listPostsByViews(ids, tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
}
@GetMapping("/latest-reply")
public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId,
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize,
Authentication auth) {
List<Long> ids = categoryIds;
if (categoryId != null) {
ids = java.util.List.of(categoryId);
}
List<Long> tids = tagIds;
if (tagId != null) {
tids = java.util.List.of(tagId);
}
if (auth != null) {
userVisitService.recordVisit(auth.getName());
}
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
.stream().map(postMapper::toSummaryDto).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,30 @@
package com.openisle.controller;
import com.openisle.dto.PushPublicKeyDto;
import com.openisle.dto.PushSubscriptionRequest;
import com.openisle.service.PushSubscriptionService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/push")
@RequiredArgsConstructor
public class PushSubscriptionController {
private final PushSubscriptionService pushSubscriptionService;
@Value("${app.webpush.public-key}")
private String publicKey;
@GetMapping("/public-key")
public PushPublicKeyDto getPublicKey() {
PushPublicKeyDto r = new PushPublicKeyDto();
r.setKey(publicKey);
return r;
}
@PostMapping("/subscribe")
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
}
}

View File

@@ -1,9 +1,13 @@
package com.openisle.controller;
import com.openisle.dto.ReactionDto;
import com.openisle.dto.ReactionRequest;
import com.openisle.mapper.ReactionMapper;
import com.openisle.model.Reaction;
import com.openisle.model.ReactionType;
import com.openisle.service.LevelService;
import com.openisle.service.PointService;
import com.openisle.service.ReactionService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
@@ -14,6 +18,9 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
public class ReactionController {
private final ReactionService reactionService;
private final LevelService levelService;
private final ReactionMapper reactionMapper;
private final PointService pointService;
/**
* Get all available reaction types.
@@ -25,51 +32,29 @@ public class ReactionController {
@PostMapping("/posts/{postId}/reactions")
public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
@RequestBody ReactionRequest req,
Authentication auth) {
@RequestBody ReactionRequest req,
Authentication auth) {
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
if (reaction == null) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.ok(toDto(reaction));
ReactionDto dto = reactionMapper.toDto(reaction);
dto.setReward(levelService.awardForReaction(auth.getName()));
pointService.awardForReactionOfPost(auth.getName(), postId);
return ResponseEntity.ok(dto);
}
@PostMapping("/comments/{commentId}/reactions")
public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
@RequestBody ReactionRequest req,
Authentication auth) {
@RequestBody ReactionRequest req,
Authentication auth) {
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
if (reaction == null) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.ok(toDto(reaction));
}
private ReactionDto toDto(Reaction reaction) {
ReactionDto dto = new ReactionDto();
dto.setId(reaction.getId());
dto.setType(reaction.getType());
dto.setUser(reaction.getUser().getUsername());
if (reaction.getPost() != null) {
dto.setPostId(reaction.getPost().getId());
}
if (reaction.getComment() != null) {
dto.setCommentId(reaction.getComment().getId());
}
return dto;
}
@Data
private static class ReactionRequest {
private ReactionType type;
}
@Data
private static class ReactionDto {
private Long id;
private ReactionType type;
private String user;
private Long postId;
private Long commentId;
ReactionDto dto = reactionMapper.toDto(reaction);
dto.setReward(levelService.awardForReaction(auth.getName()));
pointService.awardForReactionOfComment(auth.getName(), commentId);
return ResponseEntity.ok(dto);
}
}

View File

@@ -1,10 +1,11 @@
package com.openisle.controller;
import com.openisle.model.Post;
import com.openisle.model.Comment;
import com.openisle.model.User;
import com.openisle.dto.PostSummaryDto;
import com.openisle.dto.SearchResultDto;
import com.openisle.dto.UserDto;
import com.openisle.mapper.PostMapper;
import com.openisle.mapper.UserMapper;
import com.openisle.service.SearchService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,32 +20,34 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class SearchController {
private final SearchService searchService;
private final UserMapper userMapper;
private final PostMapper postMapper;
@GetMapping("/users")
public List<UserDto> searchUsers(@RequestParam String keyword) {
return searchService.searchUsers(keyword).stream()
.map(this::toUserDto)
.map(userMapper::toDto)
.collect(Collectors.toList());
}
@GetMapping("/posts")
public List<PostDto> searchPosts(@RequestParam String keyword) {
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
return searchService.searchPosts(keyword).stream()
.map(this::toPostDto)
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
}
@GetMapping("/posts/content")
public List<PostDto> searchPostsByContent(@RequestParam String keyword) {
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
return searchService.searchPostsByContent(keyword).stream()
.map(this::toPostDto)
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
}
@GetMapping("/posts/title")
public List<PostDto> searchPostsByTitle(@RequestParam String keyword) {
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
return searchService.searchPostsByTitle(keyword).stream()
.map(this::toPostDto)
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
}
@@ -63,40 +66,4 @@ public class SearchController {
})
.collect(Collectors.toList());
}
private UserDto toUserDto(User user) {
UserDto dto = new UserDto();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
return dto;
}
private PostDto toPostDto(Post post) {
PostDto dto = new PostDto();
dto.setId(post.getId());
dto.setTitle(post.getTitle());
return dto;
}
@Data
private static class UserDto {
private Long id;
private String username;
}
@Data
private static class PostDto {
private Long id;
private String title;
}
@Data
private static class SearchResultDto {
private String type;
private Long id;
private String text;
private String subText;
private String extra;
private Long postId;
}
}

View File

@@ -0,0 +1,69 @@
package com.openisle.controller;
import com.openisle.model.Post;
import com.openisle.model.PostStatus;
import com.openisle.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* Controller for dynamic sitemap generation.
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class SitemapController {
private final PostRepository postRepository;
@Value("${app.website-url}")
private String websiteUrl;
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<String> sitemap() {
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);
StringBuilder body = new StringBuilder();
body.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
body.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
List<String> staticRoutes = List.of(
"/",
"/about",
"/activities",
"/login",
"/signup"
);
for (String path : staticRoutes) {
body.append(" <url><loc>")
.append(websiteUrl)
.append(path)
.append("</loc></url>\n");
}
for (Post p : posts) {
body.append(" <url>\n")
.append(" <loc>")
.append(websiteUrl)
.append("/posts/")
.append(p.getId())
.append("</loc>\n")
.append(" <lastmod>")
.append(p.getCreatedAt().toLocalDate())
.append("</lastmod>\n")
.append(" </url>\n");
}
body.append("</urlset>");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_XML)
.body(body.toString());
}
}

View File

@@ -0,0 +1,85 @@
package com.openisle.controller;
import com.openisle.service.UserVisitService;
import com.openisle.service.StatService;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/stats")
@RequiredArgsConstructor
public class StatController {
private final UserVisitService userVisitService;
private final StatService statService;
@GetMapping("/dau")
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
long count = userVisitService.countDau(date);
return Map.of("dau", count);
}
@GetMapping("/dau-range")
public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(days - 1L);
var data = userVisitService.countDauRange(start, end);
return data.entrySet().stream()
.map(e -> Map.<String,Object>of(
"date", e.getKey().toString(),
"value", e.getValue()
))
.toList();
}
@GetMapping("/new-users-range")
public List<Map<String, Object>> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(days - 1L);
var data = statService.countNewUsersRange(start, end);
return data.entrySet().stream()
.map(e -> Map.<String,Object>of(
"date", e.getKey().toString(),
"value", e.getValue()
))
.toList();
}
@GetMapping("/posts-range")
public List<Map<String, Object>> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(days - 1L);
var data = statService.countPostsRange(start, end);
return data.entrySet().stream()
.map(e -> Map.<String,Object>of(
"date", e.getKey().toString(),
"value", e.getValue()
))
.toList();
}
@GetMapping("/comments-range")
public List<Map<String, Object>> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(days - 1L);
var data = statService.countCommentsRange(start, end);
return data.entrySet().stream()
.map(e -> Map.<String,Object>of(
"date", e.getKey().toString(),
"value", e.getValue()
))
.toList();
}
}

View File

@@ -1,16 +1,21 @@
package com.openisle.controller;
import com.openisle.model.Tag;
import com.openisle.service.TagService;
import com.openisle.service.PostService;
import com.openisle.repository.UserRepository;
import com.openisle.dto.PostSummaryDto;
import com.openisle.dto.TagDto;
import com.openisle.dto.TagRequest;
import com.openisle.mapper.PostMapper;
import com.openisle.mapper.TagMapper;
import com.openisle.model.PublishMode;
import com.openisle.model.Role;
import lombok.Data;
import com.openisle.model.Tag;
import com.openisle.repository.UserRepository;
import com.openisle.service.PostService;
import com.openisle.service.TagService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@@ -20,6 +25,8 @@ public class TagController {
private final TagService tagService;
private final PostService postService;
private final UserRepository userRepository;
private final PostMapper postMapper;
private final TagMapper tagMapper;
@PostMapping
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
@@ -38,14 +45,14 @@ public class TagController {
approved,
auth != null ? auth.getName() : null);
long count = postService.countPostsByTag(tag.getId());
return toDto(tag, count);
return tagMapper.toDto(tag, count);
}
@PutMapping("/{id}")
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByTag(tag.getId());
return toDto(tag, count);
return tagMapper.toDto(tag, count);
}
@DeleteMapping("/{id}")
@@ -56,8 +63,11 @@ public class TagController {
@GetMapping
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "limit", required = false) Integer limit) {
List<TagDto> dtos = tagService.searchTags(keyword).stream()
.map(t -> toDto(t, postService.countPostsByTag(t.getId())))
List<Tag> tags = tagService.searchTags(keyword);
List<Long> tagIds = tags.stream().map(Tag::getId).toList();
Map<Long, Long> postCntByTagIds = postService.countPostsByTagIds(tagIds);
List<TagDto> dtos = tags.stream()
.map(t -> tagMapper.toDto(t, postCntByTagIds.getOrDefault(t.getId(), 0L)))
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
.collect(Collectors.toList());
if (limit != null && limit > 0 && dtos.size() > limit) {
@@ -70,7 +80,7 @@ public class TagController {
public TagDto get(@PathVariable Long id) {
Tag tag = tagService.getTag(id);
long count = postService.countPostsByTag(tag.getId());
return toDto(tag, count);
return tagMapper.toDto(tag, count);
}
@GetMapping("/{id}/posts")
@@ -79,47 +89,7 @@ public class TagController {
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
return postService.listPostsByTags(java.util.List.of(id), page, pageSize)
.stream()
.map(p -> {
PostSummaryDto dto = new PostSummaryDto();
dto.setId(p.getId());
dto.setTitle(p.getTitle());
return dto;
})
.map(postMapper::toSummaryDto)
.collect(Collectors.toList());
}
private TagDto toDto(Tag tag, long count) {
TagDto dto = new TagDto();
dto.setId(tag.getId());
dto.setName(tag.getName());
dto.setIcon(tag.getIcon());
dto.setSmallIcon(tag.getSmallIcon());
dto.setDescription(tag.getDescription());
dto.setCount(count);
return dto;
}
@Data
private static class TagRequest {
private String name;
private String description;
private String icon;
private String smallIcon;
}
@Data
private static class TagDto {
private Long id;
private String name;
private String description;
private String icon;
private String smallIcon;
private Long count;
}
@Data
private static class PostSummaryDto {
private Long id;
private String title;
}
}

View File

@@ -74,4 +74,9 @@ public class UploadController {
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
}
}
@GetMapping("/presign")
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
return imageUploader.presignUpload(filename);
}
}

View File

@@ -1,11 +1,13 @@
package com.openisle.controller;
import com.openisle.dto.*;
import com.openisle.exception.NotFoundException;
import com.openisle.mapper.TagMapper;
import com.openisle.mapper.UserMapper;
import com.openisle.model.User;
import com.openisle.service.*;
import org.springframework.beans.factory.annotation.Value;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@@ -25,8 +27,10 @@ public class UserController {
private final ReactionService reactionService;
private final TagService tagService;
private final SubscriptionService subscriptionService;
private final PostReadService postReadService;
private final UserVisitService userVisitService;
private final LevelService levelService;
private final JwtService jwtService;
private final UserMapper userMapper;
private final TagMapper tagMapper;
@Value("${app.upload.check-type:true}")
private boolean checkImageType;
@@ -43,13 +47,10 @@ public class UserController {
@Value("${app.user.tags-limit:50}")
private int defaultTagsLimit;
@Value("${app.snippet-length:50}")
private int snippetLength;
@GetMapping("/me")
public ResponseEntity<UserDto> me(Authentication auth) {
User user = userService.findByUsername(auth.getName()).orElseThrow();
return ResponseEntity.ok(toDto(user, auth));
return ResponseEntity.ok(userMapper.toDto(user, auth));
}
@PostMapping("/me/avatar")
@@ -72,17 +73,26 @@ public class UserController {
}
@PutMapping("/me")
public ResponseEntity<UserDto> updateProfile(@RequestBody UpdateProfileDto dto,
Authentication auth) {
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
Authentication auth) {
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
return ResponseEntity.ok(toDto(user, auth));
return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(user.getUsername()),
"user", userMapper.toDto(user, auth)
));
}
@PostMapping("/me/signin")
public Map<String, Integer> signIn(Authentication auth) {
int reward = levelService.awardForSignin(auth.getName());
return Map.of("reward", reward);
}
@GetMapping("/{identifier}")
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
Authentication auth) {
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
return ResponseEntity.ok(toDto(user, auth));
return ResponseEntity.ok(userMapper.toDto(user, auth));
}
@GetMapping("/{identifier}/posts")
@@ -91,7 +101,7 @@ public class UserController {
int l = limit != null ? limit : defaultPostsLimit;
User user = userService.findByIdentifier(identifier).orElseThrow();
return postService.getRecentPostsByUser(user.getUsername(), l).stream()
.map(this::toMetaDto)
.map(userMapper::toMetaDto)
.collect(java.util.stream.Collectors.toList());
}
@@ -101,7 +111,7 @@ public class UserController {
int l = limit != null ? limit : defaultRepliesLimit;
User user = userService.findByIdentifier(identifier).orElseThrow();
return commentService.getRecentCommentsByUser(user.getUsername(), l).stream()
.map(this::toCommentInfoDto)
.map(userMapper::toCommentInfoDto)
.collect(java.util.stream.Collectors.toList());
}
@@ -112,7 +122,7 @@ public class UserController {
User user = userService.findByIdentifier(identifier).orElseThrow();
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
return postService.getPostsByIds(ids).stream()
.map(this::toMetaDto)
.map(userMapper::toMetaDto)
.collect(java.util.stream.Collectors.toList());
}
@@ -123,49 +133,29 @@ public class UserController {
User user = userService.findByIdentifier(identifier).orElseThrow();
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
return commentService.getCommentsByIds(ids).stream()
.map(this::toCommentInfoDto)
.map(userMapper::toCommentInfoDto)
.collect(java.util.stream.Collectors.toList());
}
@GetMapping("/{identifier}/hot-tags")
public java.util.List<TagInfoDto> hotTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
public java.util.List<TagDto> hotTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10;
User user = userService.findByIdentifier(identifier).orElseThrow();
return tagService.getTagsByUser(user.getUsername()).stream()
.map(t -> {
TagInfoDto dto = new TagInfoDto();
dto.setId(t.getId());
dto.setName(t.getName());
dto.setDescription(t.getDescription());
dto.setIcon(t.getIcon());
dto.setSmallIcon(t.getSmallIcon());
dto.setCreatedAt(t.getCreatedAt());
dto.setCount(postService.countPostsByTag(t.getId()));
return dto;
})
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
.limit(l)
.collect(java.util.stream.Collectors.toList());
}
@GetMapping("/{identifier}/tags")
public java.util.List<TagInfoDto> userTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
public java.util.List<TagDto> userTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultTagsLimit;
User user = userService.findByIdentifier(identifier).orElseThrow();
return tagService.getRecentTagsByUser(user.getUsername(), l).stream()
.map(t -> {
TagInfoDto dto = new TagInfoDto();
dto.setId(t.getId());
dto.setName(t.getName());
dto.setDescription(t.getDescription());
dto.setIcon(t.getIcon());
dto.setSmallIcon(t.getSmallIcon());
dto.setCreatedAt(t.getCreatedAt());
dto.setCount(postService.countPostsByTag(t.getId()));
return dto;
})
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
.collect(java.util.stream.Collectors.toList());
}
@@ -173,7 +163,7 @@ public class UserController {
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
.map(this::toDto)
.map(userMapper::toDto)
.collect(java.util.stream.Collectors.toList());
}
@@ -181,7 +171,14 @@ public class UserController {
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribers(user.getUsername()).stream()
.map(this::toDto)
.map(userMapper::toDto)
.collect(java.util.stream.Collectors.toList());
}
@GetMapping("/admins")
public java.util.List<UserDto> admins() {
return userService.getAdmins().stream()
.map(userMapper::toDto)
.collect(java.util.stream.Collectors.toList());
}
@@ -194,149 +191,15 @@ public class UserController {
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
java.util.List<PostMetaDto> posts = postService.getRecentPostsByUser(user.getUsername(), pLimit).stream()
.map(this::toMetaDto)
.map(userMapper::toMetaDto)
.collect(java.util.stream.Collectors.toList());
java.util.List<CommentInfoDto> replies = commentService.getRecentCommentsByUser(user.getUsername(), rLimit).stream()
.map(this::toCommentInfoDto)
.map(userMapper::toCommentInfoDto)
.collect(java.util.stream.Collectors.toList());
UserAggregateDto dto = new UserAggregateDto();
dto.setUser(toDto(user, auth));
dto.setUser(userMapper.toDto(user, auth));
dto.setPosts(posts);
dto.setReplies(replies);
return ResponseEntity.ok(dto);
}
private UserDto toDto(User user, Authentication viewer) {
UserDto dto = new UserDto();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setEmail(user.getEmail());
dto.setAvatar(user.getAvatar());
dto.setRole(user.getRole().name());
dto.setIntroduction(user.getIntroduction());
dto.setFollowers(subscriptionService.countSubscribers(user.getUsername()));
dto.setFollowing(subscriptionService.countSubscribed(user.getUsername()));
dto.setCreatedAt(user.getCreatedAt());
dto.setLastPostTime(postService.getLastPostTime(user.getUsername()));
dto.setTotalViews(postService.getTotalViews(user.getUsername()));
dto.setVisitedDays(userVisitService.countVisits(user.getUsername()));
dto.setReadPosts(postReadService.countReads(user.getUsername()));
dto.setLikesSent(reactionService.countLikesSent(user.getUsername()));
dto.setLikesReceived(reactionService.countLikesReceived(user.getUsername()));
if (viewer != null) {
dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername()));
} else {
dto.setSubscribed(false);
}
return dto;
}
private UserDto toDto(User user) {
return toDto(user, null);
}
private PostMetaDto toMetaDto(com.openisle.model.Post post) {
PostMetaDto dto = new PostMetaDto();
dto.setId(post.getId());
dto.setTitle(post.getTitle());
String content = post.getContent();
if (content == null) {
content = "";
}
if (snippetLength >= 0) {
dto.setSnippet(content.length() > snippetLength ? content.substring(0, snippetLength) : content);
} else {
dto.setSnippet(content);
}
dto.setCreatedAt(post.getCreatedAt());
dto.setCategory(post.getCategory().getName());
dto.setViews(post.getViews());
return dto;
}
private CommentInfoDto toCommentInfoDto(com.openisle.model.Comment comment) {
CommentInfoDto dto = new CommentInfoDto();
dto.setId(comment.getId());
dto.setContent(comment.getContent());
dto.setCreatedAt(comment.getCreatedAt());
dto.setPost(toMetaDto(comment.getPost()));
if (comment.getParent() != null) {
ParentCommentDto pc = new ParentCommentDto();
pc.setId(comment.getParent().getId());
pc.setAuthor(comment.getParent().getAuthor().getUsername());
pc.setContent(comment.getParent().getContent());
dto.setParentComment(pc);
}
return dto;
}
@Data
private static class UserDto {
private Long id;
private String username;
private String email;
private String avatar;
private String role;
private String introduction;
private long followers;
private long following;
private java.time.LocalDateTime createdAt;
private java.time.LocalDateTime lastPostTime;
private long totalViews;
private long visitedDays;
private long readPosts;
private long likesSent;
private long likesReceived;
private boolean subscribed;
}
@Data
private static class PostMetaDto {
private Long id;
private String title;
private String snippet;
private java.time.LocalDateTime createdAt;
private String category;
private long views;
}
@Data
private static class CommentInfoDto {
private Long id;
private String content;
private java.time.LocalDateTime createdAt;
private PostMetaDto post;
private ParentCommentDto parentComment;
}
@Data
private static class TagInfoDto {
private Long id;
private String name;
private String description;
private String icon;
private String smallIcon;
private java.time.LocalDateTime createdAt;
private Long count;
}
@Data
private static class ParentCommentDto {
private Long id;
private String author;
private String content;
}
@Data
private static class UpdateProfileDto {
private String username;
private String introduction;
}
@Data
private static class UserAggregateDto {
private UserDto user;
private java.util.List<PostMetaDto> posts;
private java.util.List<CommentInfoDto> replies;
}
}

View File

@@ -0,0 +1,21 @@
package com.openisle.dto;
import com.openisle.model.ActivityType;
import lombok.Data;
import java.time.LocalDateTime;
/**
* DTO representing an activity without participant details.
*/
@Data
public class ActivityDto {
private Long id;
private String title;
private String icon;
private String content;
private LocalDateTime startTime;
private LocalDateTime endTime;
private ActivityType type;
private boolean ended;
}

View File

@@ -0,0 +1,16 @@
package com.openisle.dto;
import lombok.Data;
import com.openisle.model.MedalType;
/**
* DTO representing a post or comment author.
*/
@Data
public class AuthorDto {
private Long id;
private String username;
private String avatar;
private MedalType displayMedal;
}

View File

@@ -0,0 +1,17 @@
package com.openisle.dto;
import lombok.Data;
/**
* DTO representing a post category.
*/
@Data
public class CategoryDto {
private Long id;
private String name;
private String description;
private String icon;
private String smallIcon;
private Long count;
}

View File

@@ -0,0 +1,12 @@
package com.openisle.dto;
import lombok.Data;
/** Request body for creating or updating a category. */
@Data
public class CategoryRequest {
private String name;
private String description;
private String icon;
private String smallIcon;
}

View File

@@ -0,0 +1,23 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* DTO representing a comment and its nested replies.
*/
@Data
public class CommentDto {
private Long id;
private String content;
private LocalDateTime createdAt;
private LocalDateTime pinnedAt;
private AuthorDto author;
private List<CommentDto> replies;
private List<ReactionDto> reactions;
private int reward;
private int pointReward;
}

View File

@@ -0,0 +1,15 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
/** DTO for comment information in user profiles. */
@Data
public class CommentInfoDto {
private Long id;
private String content;
private LocalDateTime createdAt;
private PostMetaDto post;
private ParentCommentDto parentComment;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class CommentMedalDto extends MedalDto {
private long currentCommentCount;
private long targetCommentCount;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request body for creating or replying to a comment. */
@Data
public class CommentRequest {
private String content;
private String captcha;
}

View File

@@ -0,0 +1,15 @@
package com.openisle.dto;
import com.openisle.model.PasswordStrength;
import com.openisle.model.PublishMode;
import com.openisle.model.RegisterMode;
import lombok.Data;
/** DTO for site configuration. */
@Data
public class ConfigDto {
private PublishMode publishMode;
private PasswordStrength passwordStrength;
private Integer aiFormatLimit;
private RegisterMode registerMode;
}

View File

@@ -0,0 +1,12 @@
package com.openisle.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ContributorMedalDto extends MedalDto {
private long currentContributionLines;
private long targetContributionLines;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
/** Request for Discord OAuth login. */
@Data
public class DiscordLoginRequest {
private String code;
private String redirectUri;
private String inviteToken;
}

View File

@@ -0,0 +1,15 @@
package com.openisle.dto;
import lombok.Data;
import java.util.List;
/** DTO representing a saved draft. */
@Data
public class DraftDto {
private Long id;
private String title;
private String content;
private Long categoryId;
private List<Long> tagIds;
}

View File

@@ -0,0 +1,14 @@
package com.openisle.dto;
import lombok.Data;
import java.util.List;
/** Request body for saving a draft. */
@Data
public class DraftRequest {
private String title;
private String content;
private Long categoryId;
private List<Long> tagIds;
}

View File

@@ -0,0 +1,9 @@
package com.openisle.dto;
import lombok.Data;
/** Request to trigger a forgot password email. */
@Data
public class ForgotPasswordRequest {
private String email;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
/** Request for GitHub OAuth login. */
@Data
public class GithubLoginRequest {
private String code;
private String redirectUri;
private String inviteToken;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request for Google OAuth login. */
@Data
public class GoogleLoginRequest {
private String idToken;
private String inviteToken;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
/** Request to login. */
@Data
public class LoginRequest {
private String username;
private String password;
private String captcha;
}

View File

@@ -0,0 +1,17 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/** Metadata for lottery posts. */
@Data
public class LotteryDto {
private String prizeDescription;
private String prizeIcon;
private int prizeCount;
private LocalDateTime startTime;
private LocalDateTime endTime;
private List<AuthorDto> participants;
private List<AuthorDto> winners;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request to submit a reason (e.g., for moderation). */
@Data
public class MakeReasonRequest {
private String token;
private String reason;
}

View File

@@ -0,0 +1,14 @@
package com.openisle.dto;
import com.openisle.model.MedalType;
import lombok.Data;
@Data
public class MedalDto {
private String icon;
private String title;
private String description;
private MedalType type;
private boolean completed;
private boolean selected;
}

View File

@@ -0,0 +1,9 @@
package com.openisle.dto;
import com.openisle.model.MedalType;
import lombok.Data;
@Data
public class MedalSelectRequest {
private MedalType type;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Info about the milk tea activity. */
@Data
public class MilkTeaInfoDto {
private long redeemCount;
private boolean ended;
}

View File

@@ -0,0 +1,9 @@
package com.openisle.dto;
import lombok.Data;
/** Request to redeem the milk tea activity. */
@Data
public class MilkTeaRedeemRequest {
private String contact;
}

View File

@@ -0,0 +1,23 @@
package com.openisle.dto;
import com.openisle.model.NotificationType;
import com.openisle.model.ReactionType;
import lombok.Data;
import java.time.LocalDateTime;
/** DTO representing a user notification. */
@Data
public class NotificationDto {
private Long id;
private NotificationType type;
private PostSummaryDto post;
private CommentDto comment;
private CommentDto parentComment;
private AuthorDto fromUser;
private ReactionType reactionType;
private String content;
private Boolean approved;
private boolean read;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
import java.util.List;
/** Request to mark notifications as read. */
@Data
public class NotificationMarkReadRequest {
private List<Long> ids;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import com.openisle.model.NotificationType;
import lombok.Data;
/** User notification preference DTO. */
@Data
public class NotificationPreferenceDto {
private NotificationType type;
private boolean enabled;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import com.openisle.model.NotificationType;
import lombok.Data;
/** Request to update a single notification preference. */
@Data
public class NotificationPreferenceUpdateRequest {
private NotificationType type;
private boolean enabled;
}

View File

@@ -0,0 +1,9 @@
package com.openisle.dto;
import lombok.Data;
/** DTO representing unread notification count. */
@Data
public class NotificationUnreadCountDto {
private long count;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
/** DTO representing a parent comment. */
@Data
public class ParentCommentDto {
private Long id;
private String author;
private String content;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class PioneerMedalDto extends MedalDto {
private long rank;
}

View File

@@ -0,0 +1,12 @@
package com.openisle.dto;
import lombok.Data;
/** Point mall good info. */
@Data
public class PointGoodDto {
private Long id;
private String name;
private int cost;
private String image;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request to redeem a point mall good. */
@Data
public class PointRedeemRequest {
private Long goodId;
private String contact;
}

View File

@@ -0,0 +1,16 @@
package com.openisle.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* Detailed DTO for a post, including comments.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PostDetailDto extends PostSummaryDto {
private List<CommentDto> comments;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class PostMedalDto extends MedalDto {
private long currentPostCount;
private long targetPostCount;
}

View File

@@ -0,0 +1,16 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
/** Lightweight post metadata used in user profile lists. */
@Data
public class PostMetaDto {
private Long id;
private String title;
private String snippet;
private LocalDateTime createdAt;
private String category;
private long views;
}

View File

@@ -0,0 +1,29 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import com.openisle.model.PostType;
/**
* Request body for creating or updating a post.
*/
@Data
public class PostRequest {
private Long categoryId;
private String title;
private String content;
private List<Long> tagIds;
private String captcha;
// optional for lottery posts
private PostType type;
private String prizeDescription;
private String prizeIcon;
private Integer prizeCount;
private LocalDateTime startTime;
private LocalDateTime endTime;
}

View File

@@ -0,0 +1,35 @@
package com.openisle.dto;
import com.openisle.model.PostStatus;
import com.openisle.model.PostType;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* Lightweight DTO for listing posts without comments.
*/
@Data
public class PostSummaryDto {
private Long id;
private String title;
private String content;
private LocalDateTime createdAt;
private AuthorDto author;
private CategoryDto category;
private List<TagDto> tags;
private long views;
private long commentCount;
private PostStatus status;
private LocalDateTime pinnedAt;
private LocalDateTime lastReplyAt;
private List<ReactionDto> reactions;
private List<AuthorDto> participants;
private boolean subscribed;
private int reward;
private int pointReward;
private PostType type;
private LotteryDto lottery;
}

View File

@@ -0,0 +1,9 @@
package com.openisle.dto;
import lombok.Data;
/** Public key response for web push. */
@Data
public class PushPublicKeyDto {
private String key;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import lombok.Data;
/** Request body for saving a push subscription. */
@Data
public class PushSubscriptionRequest {
private String endpoint;
private String p256dh;
private String auth;
}

View File

@@ -0,0 +1,18 @@
package com.openisle.dto;
import com.openisle.model.ReactionType;
import lombok.Data;
/**
* DTO representing a reaction on a post or comment.
*/
@Data
public class ReactionDto {
private Long id;
private ReactionType type;
private String user;
private Long postId;
private Long commentId;
private int reward;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import com.openisle.model.ReactionType;
import lombok.Data;
/** Request for reacting to a post or comment. */
@Data
public class ReactionRequest {
private ReactionType type;
}

View File

@@ -0,0 +1,13 @@
package com.openisle.dto;
import lombok.Data;
/** Request to register a new user. */
@Data
public class RegisterRequest {
private String username;
private String email;
private String password;
private String captcha;
private String inviteToken;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request to reset password. */
@Data
public class ResetPasswordRequest {
private String token;
private String password;
}

View File

@@ -0,0 +1,14 @@
package com.openisle.dto;
import lombok.Data;
/** DTO representing a search result entry. */
@Data
public class SearchResultDto {
private String type;
private Long id;
private String text;
private String subText;
private String extra;
private Long postId;
}

View File

@@ -0,0 +1,11 @@
package com.openisle.dto;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class SeedUserMedalDto extends MedalDto {
private LocalDateTime registerDate;
}

View File

@@ -0,0 +1,16 @@
package com.openisle.dto;
import com.openisle.model.RegisterMode;
import lombok.Data;
/** Public site configuration values. */
@Data
public class SiteConfigDto {
private boolean captchaEnabled;
private boolean registerCaptchaEnabled;
private boolean loginCaptchaEnabled;
private boolean postCaptchaEnabled;
private boolean commentCaptchaEnabled;
private int aiFormatLimit;
private RegisterMode registerMode;
}

View File

@@ -0,0 +1,20 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* DTO representing a tag.
*/
@Data
public class TagDto {
private Long id;
private String name;
private String description;
private String icon;
private String smallIcon;
private LocalDateTime createdAt;
private Long count;
}

View File

@@ -0,0 +1,12 @@
package com.openisle.dto;
import lombok.Data;
/** Request body for creating or updating a tag. */
@Data
public class TagRequest {
private String name;
private String description;
private String icon;
private String smallIcon;
}

View File

@@ -0,0 +1,12 @@
package com.openisle.dto;
import lombok.Data;
/** Request for Twitter OAuth login. */
@Data
public class TwitterLoginRequest {
private String code;
private String redirectUri;
private String codeVerifier;
private String inviteToken;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request body for updating user profile. */
@Data
public class UpdateProfileDto {
private String username;
private String introduction;
}

View File

@@ -0,0 +1,13 @@
package com.openisle.dto;
import lombok.Data;
import java.util.List;
/** Aggregated user data including posts and replies. */
@Data
public class UserAggregateDto {
private UserDto user;
private List<PostMetaDto> posts;
private List<CommentInfoDto> replies;
}

View File

@@ -0,0 +1,31 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
/** Detailed user information. */
@Data
public class UserDto {
private Long id;
private String username;
private String email;
private String avatar;
private String role;
private String introduction;
private long followers;
private long following;
private LocalDateTime createdAt;
private LocalDateTime lastPostTime;
private LocalDateTime lastCommentTime;
private long totalViews;
private long visitedDays;
private long readPosts;
private long likesSent;
private long likesReceived;
private boolean subscribed;
private int experience;
private int point;
private int currentLevel;
private int nextLevelExp;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request to verify a forgot password code. */
@Data
public class VerifyForgotRequest {
private String email;
private String code;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
/** Request to verify a user registration. */
@Data
public class VerifyRequest {
private String username;
private String code;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.exception;
/**
* Exception thrown when a user exceeds allowed action rate.
*/
public class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}
}

Some files were not shown because too many files have changed in this diff Show More