-
Notifications
You must be signed in to change notification settings - Fork 45
Events Feature
活动(Events)功能完整的接入 / 扩展 / 调试指南。对应 PR:
做的事:管理员在 /admin/events 自助维护社群活动(Coffee Chat / Mock Interview / Career Journey / Open.Onion 等),普通用户在 /events 浏览、/events/[id] 看详情和回放、登录后"感兴趣"。
不做的事(刻意推迟):
- Discord Scheduled Events 双向同步(user tester 不信任自动同步会坏掉)
- iCal / Google Calendar 订阅(UNSW 学生使用率低)
- Speaker 关联
user_accounts外键(先用 JSON 过 MVP) - Tag 过滤子页(按 tag 筛选到 P3 再做)
- 在个人主页展示参会记录 / Contributions 加分(病毒传播钩子,延后)
| 位置 | 路径 / 文件 | 说明 |
|---|---|---|
| 后端 schema | backend/src/main/resources/schema.sql |
events + event_interests 表 + 4 条种子数据 + 维护者 admin role |
| 后端 domain | backend/src/main/java/com/involutionhell/backend/events/model/Event.java |
Event + 内嵌 Speaker record |
| 后端 repo | backend/.../events/repository/JdbcEventRepository.java |
JDBC 实现,JSONB 序列化 |
| 后端 service | backend/.../events/service/EventService.java |
CRUD + 感兴趣业务服务 |
| 后端 controller(公开) | backend/.../events/controller/EventController.java |
GET /api/events, /api/events/{id}
|
| 后端 controller(登录) | backend/.../events/controller/EventInterestController.java |
POST/DELETE /api/events/{id}/interest |
| 后端 controller(admin) | backend/.../events/controller/EventAdminController.java |
/api/admin/events CRUD,@SaCheckRole("admin")
|
| SaToken 白名单 | backend/.../common/config/SaTokenConfigure.java |
.notMatch("/api/events", "/api/events/*") 放行公开读 |
| 前端公开 |
frontend/app/events/page.tsxfrontend/app/events/[id]/page.tsx
|
SSR,revalidate: 300
|
| 前端感兴趣按钮 | frontend/app/events/[id]/InterestButton.tsx |
Client Component,乐观更新 |
| 前端 admin |
frontend/app/admin/events/page.tsxfrontend/app/admin/events/new/page.tsxfrontend/app/admin/events/[id]/edit/page.tsx
|
Client Component + AdminGuard
|
| 前端 admin layout | frontend/app/admin/events/layout.tsx |
Header / Footer 在 Server Component 这一层(因为 Header 用 next-intl server API) |
| 前端 admin api client | frontend/app/admin/events/lib.ts |
listAdminEvents / createEvent / updateEvent / deleteEvent
|
| 前端主页拉数据 | frontend/lib/events-fetch.ts |
ActivityTicker + FloatWindow 用;后端失败 fallback 到 data/event.json
|
| 个人主页管理员入口 | frontend/app/u/[username]/AdminLinkIfOwnerAdmin.tsx |
三重条件才渲染:登录 + 是 owner + roles 含 admin |
| Next rewrites | frontend/next.config.mjs |
/api/events + /api/admin/events 两对(:path* 不匹配空路径,所以显式写两条) |
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL DEFAULT '',
cover_url VARCHAR(500),
start_time TIMESTAMPTZ, -- 可空,未排期
end_time TIMESTAMPTZ,
discord_link VARCHAR(500),
playback_url VARCHAR(500),
speakers JSONB NOT NULL DEFAULT '[]', -- [{name,avatarUrl,profileUrl}]
tags TEXT NOT NULL DEFAULT '', -- 逗号分隔
status VARCHAR(20) NOT NULL DEFAULT 'published',-- draft|published|archived|cancelled
organizer_id BIGINT REFERENCES user_accounts(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);字段约定:
-
status流:draft→published→archived(或cancelled撤销) -
draft不出现在公开接口,管理员才能看 -
speakersJSONB 数组:MVP 只用{name},avatarUrl/profileUrl前端表单暂没填入口,后端结构已留 -
tags用逗号分隔 TEXT:保持跟user_accounts.roles同风格。未来要按 tag 过滤 SQL,再ALTER COLUMN tags TYPE TEXT[]+ GIN 索引
CREATE TABLE event_interests (
event_id BIGINT NOT NULL REFERENCES events(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES user_accounts(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (event_id, user_id)
);比 RSVP 轻量:不承诺出席,只表态关注。点一下切换。
不造新轮子——用现有 user_accounts.roles 字段(逗号分隔 TEXT,已存在)。
- Sa-Token
@SaCheckRole("admin")做后端强校验 - 前端
useAuth().user.roles.includes("admin")做 UX 判定 - 种子:
longsizhuo / Mira190 / Crokily三个 username 在schema.sql里ON CONFLICT DO UPDATE升 admin role。生产 Neon 上这些账号是 GitHub OAuth 登录后由AuthService自动创建的,所以这里 UPDATE 不会把现有字段(email、avatar_url等)覆盖——只更新roles和permissions两列。
Browser → /events (Next SSR)
→ Next fetch BACKEND_URL/api/events (revalidate: 300)
→ Java EventController.list()
→ JdbcEventRepository.findPublic() (status IN published,archived)
→ 返回 EventView[]
Browser → /admin/events/new
(AdminGuard 前端预检 roles)
Form submit →
fetch /api/admin/events POST (+ satoken header)
→ Next rewrite → Java
→ SaInterceptor 校验登录
→ @SaCheckRole("admin") 校验角色,非 admin 403
→ EventAdminController.create(req)
→ EventService.create → JdbcEventRepository.insert
Browser → /events/4
用户点"感兴趣"按钮(InterestButton.tsx)
→ 乐观更新 UI
→ fetch /api/events/4/interest POST (+ satoken)
→ @SaCheckLogin 校验登录,未登录 401
→ EventInterestController.mark
→ EventInterestRepository.add (ON CONFLICT DO NOTHING 幂等)
→ 返回 {count, interested:true}
→ 用返回值覆盖乐观值
-
backend/src/main/resources/schema.sql加ALTER TABLE events ADD COLUMN IF NOT EXISTS venue VARCHAR(255); -
backend/src/test/resources/test-schema.sql同步加字段(H2 语法) -
Event.javarecord 加字段 -
JdbcEventRepository的rowMapper/insert/update三处 SQL 加字段 -
EventView.java/EventRequest.javaDTO 加字段 - 前端
types.ts加字段;/events/[id]/page.tsx渲染;EventForm.tsx加<Field>
-
EventAdminController.validate()的 status 枚举加pending -
EventController.detail()的"不对外公开"条件可能要扩展(目前只排draft) - 前端
types.ts的EventStatus加pending -
EventForm.tsx的STATUS_OPTIONS加
- 后端加
@SaCheckRole("admin")类级注解的 Controller - 前端参考
app/admin/events/结构:-
layout.tsx渲染 Header/Footer -
page.tsx纯 client,包一层<AdminGuard>
-
- 登录用户角色判定只需要
useAuth().user.roles.includes("admin")
- 方式 A(推荐):找已有 admin 登录
/admin/users升 role(这个界面还没做,先走方式 B) - 方式 B:直接 SQL
UPDATE user_accounts SET roles = 'admin,user' WHERE username = 'xxx'; - 方式 C:改
schema.sql的种子 INSERT,下次SPRING_SQL_INIT_MODE=always启动时自动升
症状:admin 页 500。原因:"use client" 的页面里直接引 <Header /> / <Footer />(它们内部用 next-intl/server.getTranslations)。
修:把 Header / Footer 提到 Server Component 的 layout.tsx 里(见 app/admin/events/layout.tsx),page.tsx 只渲染内容区。
症状:/events 页偶发 Application error: a server-side exception has occurred。
修:参照 /u/[username]/page.tsx 的重试策略——加 cache: "no-store" 重试 + 诊断日志。
见 PR #286。
症状:curl localhost:3010/api/events 返回 Next 404。
原因:Next :path* 模式不匹配空路径。/api/events 本身需要独立一条 rewrite。
修:next.config.mjs 要两对 rewrite:/api/events 和 /api/events/:path*。
原因:application.properties 的 spring.sql.init.mode=${SPRING_SQL_INIT_MODE:never},生产默认 never。
修:生产首次部署这个功能时,临时设 SPRING_SQL_INIT_MODE=always 让 schema.sql 重新跑一遍(里面所有 CREATE TABLE IF NOT EXISTS / INSERT ... ON CONFLICT DO NOTHING|UPDATE 都是幂等的,重跑无害)。跑完切回 never。
原因:BACKEND_URL 没配,fetchHomepageEvents fallback 到 data/event.json。
修:生产 Vercel 设 BACKEND_URL=https://api.involutionhell.com;dev 环境设 BACKEND_URL=http://localhost:8081。
- 后端:
tail -f backend/.logs/*.log看 SQL 错误;或在EventController加@Slf4j+log.info - 前端 SSR:
vercel logs --project involutionhell-github-io --filter error或本地pnpm dev看 console - DB:连 Neon
psql "$DATABASE_URL",SELECT * FROM events ORDER BY id DESC LIMIT 5; - Sa-Token:
StpUtil.getLoginId()只在拦截器后的 handler 里有值;/api/events/*在白名单里,要读当前用户先StpUtil.isLogin()短路