Skip to content

feat(events): 活动管理后端 — events 表 + 公开读 API + 管理员 CRUD + 感兴趣#9

Merged
longsizhuo merged 2 commits intomainfrom
feat/events-management
Apr 17, 2026
Merged

feat(events): 活动管理后端 — events 表 + 公开读 API + 管理员 CRUD + 感兴趣#9
longsizhuo merged 2 commits intomainfrom
feat/events-management

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

配套前端 PR(同分支名 feat/events-management):https://github.com/InvolutionHell/involutionhell/tree/feat/events-management

背景

前端 `data/event.json` 里写死 4 条活动,新增 / 改时间都要改代码发版。用户(站点 owner)要求:管理员在自己个人主页点"管理员界面"进后台自助维护活动,不需要每次改代码。

Schema

```sql
events (id, title, description, cover_url, start_time, end_time,
discord_link, playback_url, speakers JSONB, tags TEXT,
status (draft|published|archived|cancelled),
organizer_id FK → user_accounts.id, created_at, updated_at)
event_interests (event_id FK CASCADE, user_id FK CASCADE, created_at, PK composite)
```

种子:把 `event.json` 4 条活动迁进来;longsizhuo / Mira190 / Crokily 三个维护者用 `ON CONFLICT DO UPDATE` 幂等升 admin role,不清洗已有用户其他字段。

API

公开(SaToken 白名单 `/api/events` + `/api/events/*`):

  • `GET /api/events` 列表(published + archived)
  • `GET /api/events/{id}` 详情(含 interestCount + 当前用户 interested 标记)

登录(`@SaCheckLogin`):

  • `POST /api/events/{id}/interest` 感兴趣(幂等)
  • `DELETE /api/events/{id}/interest` 取消(幂等)

管理员(`@SaCheckRole("admin")` 类级注解,匿名 401 / 非 admin 403):

  • `GET /api/admin/events` 全状态列表(含 draft)
  • `GET /api/admin/events/{id}`
  • `POST /api/admin/events`
  • `PUT /api/admin/events/{id}`
  • `DELETE /api/admin/events/{id}` ON DELETE CASCADE 清 event_interests

设计取舍

  • `speakers` 用 JSONB 而非独立关联表:嘉宾多数不是站点注册用户
  • `tags` 用逗号分隔 TEXT:和 `user_accounts.roles` 同风格;按 tag 过滤 P3 再升 TEXT[] + GIN
  • `startTime` / `endTime` 都可空:支持草稿 / 未排期
  • 不引入 `spring-boot-starter-validation`:手写 validate() 够用

刻意不做的

  • Discord Scheduled Events 双向同步(user tester 不信任会坏掉)
  • iCal 订阅(UNSW 学生使用率低)
  • speakers 关联 `user_accounts`(先 JSON 过 MVP)

本地 E2E

```
GET /api/events → 4 条 seeded 数据
admin 登录 → CRUD /api/admin/events → 全绿
alice(非 admin) → /api/admin/events → "拒绝访问: 缺少角色 [admin]"
alice → POST /api/events/4/interest → {count:1, interested:true}
```

前情:Next.js `/api/chat` 登录用户会代理到 Java `/openai/responses/stream`。
但 Java 的 OPENAI_API_KEY 之前塞的是已过期的 Intern-S1 key,URL 却指
api.openai.com,必然 401,每次都 fallback 回 Next.js 本地 GLM 推理。

改动:
- .env.example 默认 URL/Model 改为智谱 GLM(OpenAI-compatible 协议)
- docker-compose.yml 默认值同步更新

变量名仍叫 OPENAI_*:Java 代码里硬编码走 /chat/completions 规范协议,
URL+Key+Model 三件套指哪打哪。未来切真 OpenAI 付费 / Anthropic-compat /
自建 OpenAI-like 网关,只换这三个值即可,不用动代码。

部署:运行时 .env 需把 OPENAI_API_KEY 换成智谱 ZHIPU_API_KEY 的值,
URL / MODEL 已由 compose 默认值兜住(本机已验证重启后 healthy)。

Refs #297
目的:把前端 data/event.json 里写死的 4 条活动迁到 DB,让管理员在 /admin/events
自助维护,不用每次改代码。

## Schema
- events:title / description / cover_url / start_time / end_time / discord_link /
  playback_url / speakers (JSONB) / tags / status (draft|published|archived|cancelled) /
  organizer_id FK / timestamps
- event_interests:event_id + user_id 联合主键,记录用户"感兴趣"(比 RSVP 轻量,
  不承诺出席,门槛低 10 倍)
- 种子:原 event.json 4 条活动迁过来;longsizhuo / Mira190 / Crokily 三个维护者种
  admin role(ON CONFLICT DO UPDATE 幂等升级,不清洗已有用户)

## API
公开(SaToken 白名单 /api/events, /api/events/*):
- GET  /api/events                 列表(仅 published + archived)
- GET  /api/events/{id}            详情(含 interestCount + 当前用户 interested 标记)

登录(@SaCheckLogin):
- POST   /api/events/{id}/interest
- DELETE /api/events/{id}/interest

管理员(@SaCheckRole("admin"),类级注解,匿名 401 / 非 admin 403):
- GET    /api/admin/events         全状态列表(含 draft)
- GET    /api/admin/events/{id}
- POST   /api/admin/events
- PUT    /api/admin/events/{id}
- DELETE /api/admin/events/{id}    ON DELETE CASCADE 清 event_interests

## 设计取舍
- speakers 用 JSONB 而非关联表:嘉宾多数不是站点注册用户,没必要做外键
- tags 用逗号分隔 TEXT 而非 TEXT[]:和 user_accounts.roles 保持风格一致,
  P3 按 tag 过滤时再 ALTER 升 TEXT[] + GIN 索引
- startTime / endTime 都可空:草稿状态和未排期的 Open.Onion 场景
- 不引入 spring-boot-starter-validation:手写一个 validate() 够用,避免为这模块加新依赖

## 没做的事(刻意推迟到后续)
- Discord Scheduled Events 双向同步(user tester 明确表达不信任自动同步会出 bug,
  PM 也同意不是 P0)
- iCal 订阅(UNSW 学生 iCal 使用率低,不是杠杆点)
- speakers 关联 user_accounts(先用 JSON 的 {name} 过 MVP)
- 独立 /admin 路由首页(本次只给 /admin/events 一个入口)
@longsizhuo
Copy link
Copy Markdown
Member Author

开发文档同步在前端仓 Wiki:https://github.com/InvolutionHell/involutionhell/wiki/Events-Feature

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

为活动管理提供后端支持:新增 events / event_interests 表结构与种子数据,并提供公开读 API、管理员 CRUD,以及登录用户“感兴趣”开关接口,替代前端 data/event.json 硬编码维护方式。

Changes:

  • 新增数据库结构:eventsevent_interests(含索引)并在 schema.sql 写入 4 条活动种子
  • 新增活动模块后端实现:JDBC Repository + Service + 公共/管理/兴趣 Controller + DTO/Model
  • 更新 Sa-Token 白名单放行公开活动读接口;更新本地运行的 AI 默认配置文档与 compose 默认值

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/test/resources/test-schema.sql 为 H2 测试库补充 events 相关表结构
src/main/resources/schema.sql 生产库新增 events/event_interests 表、索引、维护者 seed、活动 seed
src/main/java/com/involutionhell/backend/events/service/EventService.java 新增 Event 业务薄封装,统一对外提供读写与兴趣统计
src/main/java/com/involutionhell/backend/events/repository/JdbcEventRepository.java 新增基于 JdbcTemplate 的 events CRUD 与 JSON speakers 序列化/反序列化
src/main/java/com/involutionhell/backend/events/repository/EventRepository.java 定义 events 数据访问接口(public/admin 读维度 + 写接口)
src/main/java/com/involutionhell/backend/events/repository/EventInterestRepository.java 新增 event_interests 的 add/remove/count/isInterested 等操作
src/main/java/com/involutionhell/backend/events/model/Event.java 新增 Event 领域模型与 ongoing/past 判定
src/main/java/com/involutionhell/backend/events/dto/EventView.java 新增对外返回 DTO(含 interestCount/ongoing/past 与 tags 拆分)
src/main/java/com/involutionhell/backend/events/dto/EventRequest.java 新增管理员创建/更新入参 DTO 与 tags join/字段清洗
src/main/java/com/involutionhell/backend/events/controller/EventInterestController.java 新增登录用户“感兴趣”开关 API(幂等)
src/main/java/com/involutionhell/backend/events/controller/EventController.java 新增公开 events 列表与详情接口(含 interested 标记)
src/main/java/com/involutionhell/backend/events/controller/EventAdminController.java 新增管理员 events CRUD 接口与手写校验
src/main/java/com/involutionhell/backend/common/config/SaTokenConfigure.java 白名单放行 /api/events/api/events/* 匿名访问
docker-compose.yml 调整默认 AI 模型服务地址/模型名与注释说明
.env.example 更新 AI 环境变量示例与说明(OpenAI-compatible)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +51
List<Event> events = eventService.listAllForAdmin();
List<EventView> views = events.stream()
.map(e -> EventView.from(e, eventService.countInterest(e.id())))
.toList();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

管理员列表同样对每个活动逐条 countInterest,会造成 N+1 查询;后台活动数量增长后会明显拖慢。建议改为一次性聚合 interestCount(JOIN/GROUP BY 或批量计数)再构造 EventView。

Copilot uses AI. Check for mistakes.

private static String joinTags(List<String> tags) {
if (tags == null || tags.isEmpty()) return "";
return String.join(",", tags.stream().map(String::trim).filter(s -> !s.isEmpty()).toList());
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

joinTags 对 tags 元素直接 String::trim;如果客户端传入的数组里包含 null(JSON 允许),这里会 NPE 导致 500。建议在 stream 中先过滤 null(或在 validate() 中拒绝 null/blank tag),确保健壮性。

Suggested change
return String.join(",", tags.stream().map(String::trim).filter(s -> !s.isEmpty()).toList());
return String.join(",", tags.stream()
.filter(tag -> tag != null)
.map(String::trim)
.filter(s -> !s.isEmpty())
.toList());

Copilot uses AI. Check for mistakes.
event_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (event_id, user_id)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试用 H2 schema 里 event_interests 没有外键(也就没有 ON DELETE CASCADE)。这会让测试环境无法覆盖“删 events 级联清理 interest”这一关键行为,且可能允许插入不存在的 event/user 产生脏数据。建议在 H2 schema 中补齐对 events(id) / user_accounts(id) 的外键并加 ON DELETE CASCADE,与生产 schema 行为对齐。

Suggested change
PRIMARY KEY (event_id, user_id)
PRIMARY KEY (event_id, user_id),
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES user_accounts(id) ON DELETE CASCADE

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +52
@GetMapping
public ApiResponse<List<EventView>> list() {
List<Event> events = eventService.listAllForAdmin();
List<EventView> views = events.stream()
.map(e -> EventView.from(e, eventService.countInterest(e.id())))
.toList();
return ApiResponse.ok(views);
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新增了一整套 Events 的公开读、管理员 CRUD 与 interest 开关,但当前测试目录里已有大量基于 MockMvc 的集成测试覆盖鉴权与业务错误分支;这里缺少对应的集成测试会让权限白名单(/api/events)、admin 角色校验(/api/admin/events)以及 interest 幂等行为缺乏回归保障。建议补充类似 UserCenterControllerIntegrationTests 的 MockMvc 测试用例。

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +99
VALUES
('Mock Interview', '模拟面试专场:匹配面试官 1v1,结束即反馈,积累真实面试体感。', '/event/mockInterview.webp',
'https://discord.gg/QHsjqezfC?event=1430500169299922965',
'https://involutionhell.com/docs/jobs/event-keynote/event-takeway',
'interview,mock', 'archived'),
('Coffee Chat', '邀请业界嘉宾小范围交流,聊 career path、求职反思、日常 dev 体感。', '/event/coffeeChat.webp',
'https://discord.com/invite/8AQZj7sa?event=1432010537402761348',
'https://involutionhell.com/docs/jobs/event-keynote/coffee-chat',
'career,chat', 'archived'),
('Career Journey', '资深从业者分享完整职业路径 + 关键决策点。', '/event/careerJourney.webp',
'https://discord.com/invite/8AQZj7sa?event=1432010537402761348',
'https://involutionhell.com/docs/jobs/event-keynote/event-takeway',
'career,sharing', 'archived'),
('Open.Onion', '持续进行中的开源 / 内部项目协作节奏,参与即获得 contributor 标签。', '/event/openOnion.webp',
'https://discord.gg/kJZFMr5chU?event=1477581193582088304',
NULL,
'project,open-source', 'published')
ON CONFLICT DO NOTHING;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的种子 INSERT 用了 ON CONFLICT DO NOTHING,但 events 表当前只有主键 id 且该 INSERT 没有显式写入 id,因此正常情况下不会产生 conflict——当 schema.sql 在本地用 SPRING_SQL_INIT_MODE=always 重复执行时会不断插入重复活动。建议改成真正幂等的写法(例如给 seed 写固定 id 并 ON CONFLICT (id) DO NOTHING,或基于 title 做 WHERE NOT EXISTS/唯一约束再 upsert)。

Suggested change
VALUES
('Mock Interview', '模拟面试专场:匹配面试官 1v1,结束即反馈,积累真实面试体感。', '/event/mockInterview.webp',
'https://discord.gg/QHsjqezfC?event=1430500169299922965',
'https://involutionhell.com/docs/jobs/event-keynote/event-takeway',
'interview,mock', 'archived'),
('Coffee Chat', '邀请业界嘉宾小范围交流,聊 career path、求职反思、日常 dev 体感。', '/event/coffeeChat.webp',
'https://discord.com/invite/8AQZj7sa?event=1432010537402761348',
'https://involutionhell.com/docs/jobs/event-keynote/coffee-chat',
'career,chat', 'archived'),
('Career Journey', '资深从业者分享完整职业路径 + 关键决策点。', '/event/careerJourney.webp',
'https://discord.com/invite/8AQZj7sa?event=1432010537402761348',
'https://involutionhell.com/docs/jobs/event-keynote/event-takeway',
'career,sharing', 'archived'),
('Open.Onion', '持续进行中的开源 / 内部项目协作节奏,参与即获得 contributor 标签。', '/event/openOnion.webp',
'https://discord.gg/kJZFMr5chU?event=1477581193582088304',
NULL,
'project,open-source', 'published')
ON CONFLICT DO NOTHING;
SELECT seed.title,
seed.description,
seed.cover_url,
seed.discord_link,
seed.playback_url,
seed.tags,
seed.status
FROM (
VALUES
('Mock Interview', '模拟面试专场:匹配面试官 1v1,结束即反馈,积累真实面试体感。', '/event/mockInterview.webp',
'https://discord.gg/QHsjqezfC?event=1430500169299922965',
'https://involutionhell.com/docs/jobs/event-keynote/event-takeway',
'interview,mock', 'archived'),
('Coffee Chat', '邀请业界嘉宾小范围交流,聊 career path、求职反思、日常 dev 体感。', '/event/coffeeChat.webp',
'https://discord.com/invite/8AQZj7sa?event=1432010537402761348',
'https://involutionhell.com/docs/jobs/event-keynote/coffee-chat',
'career,chat', 'archived'),
('Career Journey', '资深从业者分享完整职业路径 + 关键决策点。', '/event/careerJourney.webp',
'https://discord.com/invite/8AQZj7sa?event=1432010537402761348',
'https://involutionhell.com/docs/jobs/event-keynote/event-takeway',
'career,sharing', 'archived'),
('Open.Onion', '持续进行中的开源 / 内部项目协作节奏,参与即获得 contributor 标签。', '/event/openOnion.webp',
'https://discord.gg/kJZFMr5chU?event=1477581193582088304',
NULL,
'project,open-source', 'published')
) AS seed(title, description, cover_url, discord_link, playback_url, tags, status)
WHERE NOT EXISTS (
SELECT 1
FROM events e
WHERE e.title = seed.title
);

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +39
-- 站点维护者(GitHub OAuth 登录后由 sync 服务补 github_id;此处按 username 打 admin role)
-- 生产 Neon 上这些账号是 GitHub OAuth 登录后由 AuthService 自动创建的,所以用
-- ON CONFLICT DO UPDATE 幂等升级角色,避免漏 seed。
INSERT INTO user_accounts (username, password_hash, display_name, enabled, roles, permissions)
VALUES ('longsizhuo', '', 'Siz Long', TRUE, 'admin,user', 'user:profile:read,user:center:read,user:center:manage'),
('Mira190', '', 'Mira', TRUE, 'admin,user', 'user:profile:read,user:center:read,user:center:manage'),
('Crokily', '', 'Crokily', TRUE, 'admin,user', 'user:profile:read,user:center:read,user:center:manage')
ON CONFLICT (username) DO UPDATE
SET roles = 'admin,user',
permissions = 'user:profile:read,user:center:read,user:center:manage';
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 username='longsizhuo'/'Mira190'/'Crokily' 插入 admin 账号,但当前 AuthService 的 GitHub OAuth 用户名规则是 "github_{githubId}"(见 AuthService.loginByGithub),因此这些 seed 不会命中真实维护者账号,导致维护者登录后仍非 admin。建议改为按 github_id(或对应的 github_{id} username)做幂等角色升级,避免插入一批无法使用的本地 admin 账号。

Suggested change
-- 站点维护者(GitHub OAuth 登录后由 sync 服务补 github_id;此处按 username 打 admin role)
-- 生产 Neon 上这些账号是 GitHub OAuth 登录后由 AuthService 自动创建的,所以用
-- ON CONFLICT DO UPDATE 幂等升级角色,避免漏 seed。
INSERT INTO user_accounts (username, password_hash, display_name, enabled, roles, permissions)
VALUES ('longsizhuo', '', 'Siz Long', TRUE, 'admin,user', 'user:profile:read,user:center:read,user:center:manage'),
('Mira190', '', 'Mira', TRUE, 'admin,user', 'user:profile:read,user:center:read,user:center:manage'),
('Crokily', '', 'Crokily', TRUE, 'admin,user', 'user:profile:read,user:center:read,user:center:manage')
ON CONFLICT (username) DO UPDATE
SET roles = 'admin,user',
permissions = 'user:profile:read,user:center:read,user:center:manage';
-- 站点维护者:admin 权限不能按 GitHub handle 直接 seed 到 username。
-- 当前 AuthService.loginByGithub 创建的本地用户名规则是 "github_{githubId}",
-- 因此像 'longsizhuo' / 'Mira190' / 'Crokily' 这样的 username 不会命中真实 OAuth 账号,
-- 反而会插入一批无法被维护者实际使用的本地 admin 账号。
--
-- 正确做法应是在已知维护者 github_id 后,按 github_id(或对应的 github_{id} username)
-- 对真实 OAuth 用户做幂等角色升级;这里不再插入错误的本地维护者账号 seed。

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +43
List<EventView> views = events.stream()
.map(e -> EventView.from(e, eventService.countInterest(e.id())))
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

公开列表这里对每个活动单独调用 countInterest,会产生 N+1 次查询(活动越多越慢)。建议在 repository 层一次性聚合 interest_count(例如 LEFT JOIN + GROUP BY 或者批量 IN 查询后在内存合并),让列表接口保持单次/常数次查询。

Suggested change
List<EventView> views = events.stream()
.map(e -> EventView.from(e, eventService.countInterest(e.id())))
List<Long> eventIds = events.stream()
.map(Event::id)
.toList();
Map<Long, Long> interestCounts = eventService.countInterestByEventIds(eventIds);
List<EventView> views = events.stream()
.map(e -> EventView.from(e, interestCounts.getOrDefault(e.id(), 0L)))

Copilot uses AI. Check for mistakes.
@longsizhuo longsizhuo merged commit 2a4477b into main Apr 17, 2026
4 checks passed
longsizhuo pushed a commit that referenced this pull request Apr 17, 2026
…hema

修复 5 个 Copilot 提出的问题:

1) EventInterestRepository 新增 countByEventIds 批量聚合,EventService 暴露
   countInterestByEventIds;EventController 和 EventAdminController 列表接口
   都改用一次 GROUP BY 拿全量 interestCount,避免每条活动单独 COUNT 的 N+1。

2) EventRequest.joinTags 先过滤 null 元素再 trim,避免 JSON 客户端在 tags
   数组里放 null 时 NPE 导致 500。

3) test-schema.sql 的 event_interests 补齐对 events(id) / user_accounts(id) 的
   外键与 ON DELETE CASCADE,和生产 schema 行为对齐;否则 H2 测试无法覆盖
   "删 event 级联清 interest" 这条关键路径。

4) schema.sql 的 events seed 原先用 ON CONFLICT DO NOTHING 但 INSERT 不写 id,
   正常根本不会产生 conflict,等于非幂等。改成 SELECT ... WHERE NOT EXISTS(title)
   的 upsert 风格,重跑 schema 不会再插重复活动。

5) schema.sql 原先的 admin seed 按 GitHub handle(longsizhuo / Mira190 /
   Crokily)插 user_accounts,但 AuthService.loginByGithub 实际创建的 username
   是 "github_{githubId}" 格式,seed 出来的是三个永远没人用的死账号。删掉错误
   seed,改用注释指引:首次 OAuth 登录后按 github_id 执行一次 UPDATE 升
   roles,避免插错数据。
longsizhuo pushed a commit that referenced this pull request Apr 17, 2026
修两条独立但叠加的生产部署 bug:

1) docker-compose.yml 的 environment 缺 AUTH_URL 透传。
   application.properties 里 justauth redirect-uri 是
   ${AUTH_URL:http://localhost:3000}/api/auth/callback/github,
   compose 不显式透传时 .env 的 AUTH_URL=https://involutionhell.com
   进不到容器,生产 /oauth/render/github 302 的 redirect_uri
   变成 localhost,跳 GitHub 必报 redirect_uri_mismatch。
   docker inspect 查到运行中容器 AUTH_URL=http://localhost:3010
   是老容器手工 docker run 遗留。

2) deploy.yml 的 docker compose up -d 没加 --force-recreate。
   CI 的 docker build 会把 :latest 标签指向新 sha,但 compose 看
   image 名没变就不重建容器,:latest 指向新 sha 而运行中容器仍
   绑死老 sha —— PR #9 / #10 的 SaToken /api/events 白名单、
   events controller 全都躺在新镜像里没生效,/api/events 一直 401。

合起来就是"部署全 success 但生产没变化"的伪成功。

后续 merge 这个 PR 触发 deploy workflow 后,验证:
  curl -I https://api.involutionhell.com/oauth/render/github | grep -i location
  # → redirect_uri=https%3A%2F%2Finvolutionhell.com%2Fapi%2Fauth%2Fcallback%2Fgithub
  curl https://api.involutionhell.com/api/events
  # → {"success":true,"data":[...]}

⚠️ GitHub OAuth App 的 Authorization callback URLs 需要手动加
   https://involutionhell.com/api/auth/callback/github(若还没加)。
longsizhuo added a commit that referenced this pull request Apr 17, 2026
修两条独立但叠加的生产部署 bug:

1) docker-compose.yml 的 environment 缺 AUTH_URL 透传。
   application.properties 里 justauth redirect-uri 是
   ${AUTH_URL:http://localhost:3000}/api/auth/callback/github,
   compose 不显式透传时 .env 的 AUTH_URL=https://involutionhell.com
   进不到容器,生产 /oauth/render/github 302 的 redirect_uri
   变成 localhost,跳 GitHub 必报 redirect_uri_mismatch。
   docker inspect 查到运行中容器 AUTH_URL=http://localhost:3010
   是老容器手工 docker run 遗留。

2) deploy.yml 的 docker compose up -d 没加 --force-recreate。
   CI 的 docker build 会把 :latest 标签指向新 sha,但 compose 看
   image 名没变就不重建容器,:latest 指向新 sha 而运行中容器仍
   绑死老 sha —— PR #9 / #10 的 SaToken /api/events 白名单、
   events controller 全都躺在新镜像里没生效,/api/events 一直 401。

合起来就是"部署全 success 但生产没变化"的伪成功。

后续 merge 这个 PR 触发 deploy workflow 后,验证:
  curl -I https://api.involutionhell.com/oauth/render/github | grep -i location
  # → redirect_uri=https%3A%2F%2Finvolutionhell.com%2Fapi%2Fauth%2Fcallback%2Fgithub
  curl https://api.involutionhell.com/api/events
  # → {"success":true,"data":[...]}

⚠️ GitHub OAuth App 的 Authorization callback URLs 需要手动加
   https://involutionhell.com/api/auth/callback/github(若还没加)。
longsizhuo added a commit that referenced this pull request Apr 17, 2026
* fix(deploy): AUTH_URL 透传 + 强制重建容器

修两条独立但叠加的生产部署 bug:

1) docker-compose.yml 的 environment 缺 AUTH_URL 透传。
   application.properties 里 justauth redirect-uri 是
   ${AUTH_URL:http://localhost:3000}/api/auth/callback/github,
   compose 不显式透传时 .env 的 AUTH_URL=https://involutionhell.com
   进不到容器,生产 /oauth/render/github 302 的 redirect_uri
   变成 localhost,跳 GitHub 必报 redirect_uri_mismatch。
   docker inspect 查到运行中容器 AUTH_URL=http://localhost:3010
   是老容器手工 docker run 遗留。

2) deploy.yml 的 docker compose up -d 没加 --force-recreate。
   CI 的 docker build 会把 :latest 标签指向新 sha,但 compose 看
   image 名没变就不重建容器,:latest 指向新 sha 而运行中容器仍
   绑死老 sha —— PR #9 / #10 的 SaToken /api/events 白名单、
   events controller 全都躺在新镜像里没生效,/api/events 一直 401。

合起来就是"部署全 success 但生产没变化"的伪成功。

后续 merge 这个 PR 触发 deploy workflow 后,验证:
  curl -I https://api.involutionhell.com/oauth/render/github | grep -i location
  # → redirect_uri=https%3A%2F%2Finvolutionhell.com%2Fapi%2Fauth%2Fcallback%2Fgithub
  curl https://api.involutionhell.com/api/events
  # → {"success":true,"data":[...]}

⚠️ GitHub OAuth App 的 Authorization callback URLs 需要手动加
   https://involutionhell.com/api/auth/callback/github(若还没加)。

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(deploy): 回滚路径也加 --force-recreate + --no-deps

CR 指出:回滚分支把 rollback tag 覆盖回 latest 后 image 名依旧是 :latest,
compose 同样会判断"无需重建",导致回滚等于没回(容器还绑着失败那版的 sha)。

- --force-recreate 保证 rollback tag 切换后容器真的重建
- --no-deps 限定只重建 backend 服务,不碰 postgres 等依赖,
  避免部署失败同时顺带重启 DB 扩大爆炸半径

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants