Skip to content

Events Feature

longsizhuo edited this page Apr 16, 2026 · 1 revision

Events 模块开发文档

活动(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 加分(病毒传播钩子,延后)

接入点速览

位置路径 / 文件说明
后端 schemabackend/src/main/resources/schema.sqlevents + event_interests 表 + 4 条种子数据 + 维护者 admin role
后端 domainbackend/src/main/java/com/involutionhell/backend/events/model/Event.javaEvent + 内嵌 Speaker record
后端 repobackend/.../events/repository/JdbcEventRepository.javaJDBC 实现,JSONB 序列化
后端 servicebackend/.../events/service/EventService.javaCRUD + 感兴趣业务服务
后端 controller(公开)backend/.../events/controller/EventController.javaGET /api/events, /api/events/{id}
后端 controller(登录)backend/.../events/controller/EventInterestController.javaPOST/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.tsx
frontend/app/events/[id]/page.tsx
SSR,revalidate: 300
前端感兴趣按钮frontend/app/events/[id]/InterestButton.tsxClient Component,乐观更新
前端 adminfrontend/app/admin/events/page.tsx
frontend/app/admin/events/new/page.tsx
frontend/app/admin/events/[id]/edit/page.tsx
Client Component + AdminGuard
前端 admin layoutfrontend/app/admin/events/layout.tsxHeader / Footer 在 Server Component 这一层(因为 Header 用 next-intl server API)
前端 admin api clientfrontend/app/admin/events/lib.tslistAdminEvents / createEvent / updateEvent / deleteEvent
前端主页拉数据frontend/lib/events-fetch.tsActivityTicker + FloatWindow 用;后端失败 fallback 到 data/event.json
个人主页管理员入口frontend/app/u/[username]/AdminLinkIfOwnerAdmin.tsx三重条件才渲染:登录 + 是 owner + roles 含 admin
Next rewritesfrontend/next.config.mjs/api/events + /api/admin/events 两对(:path* 不匹配空路径,所以显式写两条)

数据模型

events

CREATETABLEevents (
id BIGSERIALPRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXTNOT 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 TEXTNOT NULL DEFAULT '', -- 逗号分隔
status VARCHAR(20) NOT NULL DEFAULT 'published',-- draft|published|archived|cancelled
organizer_id BIGINTREFERENCES user_accounts(id) ON DELETESETNULL,
created_at TIMESTAMPTZNOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZNOT NULL DEFAULT NOW()
);

字段约定:

  • status 流:draftpublishedarchived(或 cancelled 撤销)
  • draft不出现在公开接口,管理员才能看
  • speakers JSONB 数组:MVP 只用 {name}avatarUrl / profileUrl 前端表单暂没填入口,后端结构已留
  • tags 用逗号分隔 TEXT:保持跟 user_accounts.roles 同风格。未来要按 tag 过滤 SQL,再 ALTER COLUMN tags TYPE TEXT[] + GIN 索引

event_interests

CREATETABLEevent_interests (
event_id BIGINTNOT NULLREFERENCES events(id) ON DELETE CASCADE,
user_id BIGINTNOT NULLREFERENCES user_accounts(id) ON DELETE CASCADE,
created_at TIMESTAMPTZNOT 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.sqlON CONFLICT DO UPDATE 升 admin role。生产 Neon 上这些账号是 GitHub OAuth 登录后由 AuthService 自动创建的,所以这里 UPDATE 不会把现有字段(emailavatar_url 等)覆盖——只更新 rolespermissions 两列

调用流程(典型)

普通访客浏览

Browser → /events (Next SSR)
→ Next fetch BACKEND_URL/api/events (revalidate: 300)
→ Java EventController.list()
→ JdbcEventRepository.findPublic() (status IN published,archived)
→ 返回 EventView[]

Admin 创建活动

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}
→ 用返回值覆盖乐观值

扩展指引

新增字段(比如 venue

  1. backend/src/main/resources/schema.sqlALTER TABLE events ADD COLUMN IF NOT EXISTS venue VARCHAR(255);
  2. backend/src/test/resources/test-schema.sql 同步加字段(H2 语法)
  3. Event.java record 加字段
  4. JdbcEventRepositoryrowMapper / insert / update 三处 SQL 加字段
  5. EventView.java / EventRequest.java DTO 加字段
  6. 前端 types.ts 加字段;/events/[id]/page.tsx 渲染;EventForm.tsx<Field>

新增一种状态(比如 pending

  1. EventAdminController.validate() 的 status 枚举加 pending
  2. EventController.detail() 的"不对外公开"条件可能要扩展(目前只排 draft
  3. 前端 types.tsEventStatuspending
  4. EventForm.tsxSTATUS_OPTIONS

新增 admin 模块(比如 /admin/contributors

  1. 后端加 @SaCheckRole("admin") 类级注解的 Controller
  2. 前端参考 app/admin/events/ 结构:
    • layout.tsx 渲染 Header/Footer
    • page.tsx 纯 client,包一层 <AdminGuard>
  3. 登录用户角色判定只需要 useAuth().user.roles.includes("admin")

新增 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 启动时自动升

常见坑

1. getTranslations is not supported in Client Components

症状:admin 页 500。原因:"use client" 的页面里直接引 <Header /> / <Footer />(它们内部用 next-intl/server.getTranslations)。 修:把 Header / Footer 提到 Server Component 的 layout.tsx 里(见 app/admin/events/layout.tsx),page.tsx 只渲染内容区。

2. Vercel SSR 收到后端 403(CF 拦截)

症状:/events 页偶发 Application error: a server-side exception has occurred。 修:参照 /u/[username]/page.tsx 的重试策略——加 cache: "no-store" 重试 + 诊断日志。 见 PR #286。

3. /api/events rewrite 不工作

症状:curl localhost:3010/api/events 返回 Next 404。 原因:Next :path* 模式不匹配空路径。/api/events 本身需要独立一条 rewrite。 修:next.config.mjs 要两对 rewrite:/api/events/api/events/:path*

4. 数据库 schema 没更新到生产

原因:application.propertiesspring.sql.init.mode=${SPRING_SQL_INIT_MODE:never},生产默认 never。 修:生产首次部署这个功能时,临时设 SPRING_SQL_INIT_MODE=alwaysschema.sql 重新跑一遍(里面所有 CREATE TABLE IF NOT EXISTS / INSERT ... ON CONFLICT DO NOTHING|UPDATE 都是幂等的,重跑无害)。跑完切回 never

5. ActivityTicker / FloatWindow 一直显示 JSON 老数据

原因:BACKEND_URL 没配,fetchHomepageEvents fallback 到 data/event.json。 修:生产 Vercel 设 BACKEND_URL=https://api.involutionhell.com;dev 环境设 BACKEND_URL=http://localhost:8081

Debug 入口

  • 后端: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() 短路