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

接入点速览

位置 路径 / 文件 说明
后端 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.tsx
frontend/app/events/[id]/page.tsx
SSR,revalidate: 300
前端感兴趣按钮 frontend/app/events/[id]/InterestButton.tsx Client Component,乐观更新
前端 admin frontend/app/admin/events/page.tsx
frontend/app/admin/events/new/page.tsx
frontend/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* 不匹配空路径,所以显式写两条)

数据模型

events

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

event_interests

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.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() 短路