Skip to content

fix(events): 响应 PR #9 Copilot CR — N+1 / seed 幂等 / admin seed / 测试 schema#10

Merged
longsizhuo merged 2 commits intomainfrom
fix/events-cr-local
Apr 17, 2026
Merged

fix(events): 响应 PR #9 Copilot CR — N+1 / seed 幂等 / admin seed / 测试 schema#10
longsizhuo merged 2 commits intomainfrom
fix/events-cr-local

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

针对 #9 的 Copilot CR 全部修完。commit author 是 Claude。

修的 5 条

  1. N+1 查询消除:`EventInterestRepository.countByEventIds` 批量 GROUP BY 一次拿全,`EventController` / `EventAdminController` 列表接口改用批量 count,替换原先 per-event 的 `countInterest` 调用。
  2. EventRequest.joinTags 先 filter null 再 trim,避免 JSON 客户端传 `["x", null, "y"]` 时 NPE → 500。
  3. test-schema.sql 的 `event_interests` 补 FK + `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. 删除错误的 admin seed:原先按 GitHub handle(`longsizhuo`/`Mira190`/`Crokily`)插 user_accounts,但 `AuthService.loginByGithub` 实际创建的 username 是 `github_{githubId}` 格式,seed 出来的三个账号永远没人用。改用注释指引:首次 OAuth 登录后按 github_id 手动 UPDATE 升 roles。

⚠️ 合并后需要在生产 Neon 手动跑一次

```sql
UPDATE user_accounts
SET roles = 'admin,user',
permissions = 'user:profile:read,user:center:read,user:center:manage'
WHERE github_id IN (114939201, 163523387, 7187663);
-- longsizhuo, Mira190, Crokily
```

前提:这三位先用 GitHub OAuth 登录过站点一次,让 AuthService 创建各自的 `github_{id}` 账号。

验证

  • `mvn compile` 绿
  • `mvn test -Dtest='AuthServiceTests,UserCenterControllerIntegrationTests,JdbcUserAccountRepositoryTests'` 绿

…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,避免插错数据。
Copilot AI review requested due to automatic review settings April 17, 2026 03:36
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

本 PR 主要针对 events 模块在代码审查中提出的问题做修复:消除列表接口的 N+1 查询、增强入参健壮性、让测试 schema 与生产对齐以覆盖级联删除路径,并调整/移除不正确的 seed 逻辑以减少脏数据与误导。

Changes:

  • 将“兴趣人数”统计改为按 eventIds 批量 GROUP BY 查询,并在公开/管理员列表接口使用批量结果替代逐条 COUNT(消除 N+1)。
  • 加固 EventRequest.joinTags:先过滤 null 再 trim,避免 JSON 数组含 null 导致 NPE。
  • 对齐测试/生产 schema:测试表补齐 FK + ON DELETE CASCADE;生产 schema.sql 调整 events seed 的幂等策略并移除错误的 admin seed。

Reviewed changes

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

Show a summary per file
File Description
src/test/resources/test-schema.sql event_interests 补 FK + 级联删除,提升 H2 测试对关键路径覆盖能力
src/main/resources/schema.sql 移除不正确的 admin seed;调整 events seed 幂等插入逻辑
src/main/java/com/involutionhell/backend/events/service/EventService.java 新增批量兴趣数统计 service 方法供列表接口使用
src/main/java/com/involutionhell/backend/events/repository/EventInterestRepository.java 新增 countByEventIds 批量统计实现,避免 N+1
src/main/java/com/involutionhell/backend/events/dto/EventRequest.java joinTags 过滤 null,避免客户端传 null 导致 500
src/main/java/com/involutionhell/backend/events/controller/EventController.java 公开列表接口改为批量统计兴趣数
src/main/java/com/involutionhell/backend/events/controller/EventAdminController.java 管理员列表接口改为批量统计兴趣数

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

Comment thread src/main/resources/schema.sql Outdated
Comment on lines +79 to +108
-- 种子:原来 data/event.json 里的 4 条活动。幂等策略:
-- events 表主键是 BIGSERIAL id,INSERT 不带 id 所以不会冲突;原先用 ON CONFLICT
-- DO NOTHING 实际上不防重复。这里改用 WHERE NOT EXISTS(title) 做幂等——
-- 每次 schema 重跑时,title 已存在的就跳过。
-- startTime / endTime 先不填,管理员登录后在 /admin/events 里补时间再 publish。
INSERT INTO events (title, description, cover_url, discord_link, playback_url, tags, status)
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
);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

这里用 WHERE NOT EXISTS (SELECT 1 FROM events e WHERE e.title = seed.title) 做幂等,但 events.title 在 schema 里并没有唯一约束/唯一索引,因此该写法在并发初始化(两个实例同时跑 schema.sql)时仍可能插入重复行;同时它也不能防止 seed VALUES 内部出现重复 title 的情况。若需要“真正幂等”,建议为自然键(如 title)加唯一约束并用 ON CONFLICT,或给这 4 条 seed 指定固定 id(并同步 setval 序列)后用 ON CONFLICT (id)

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +78
/**
* 批量统计多场活动的兴趣人数,避免列表接口 N+1 查询。
*
* 一次 GROUP BY 查完返回 map;没出现在结果里的 event id(即兴趣人数为 0)调用方
* 自己 getOrDefault(id, 0L) 兜底。传入空集合直接返回空 map,不打 DB。
*/
public Map<Long, Long> countByEventIds(Collection<Long> eventIds) {
if (eventIds == null || eventIds.isEmpty()) return Map.of();
Map<Long, Long> result = new HashMap<>();
MapSqlParameterSource params = new MapSqlParameterSource("ids", eventIds);
namedJdbc.query(
"SELECT event_id, COUNT(*) AS cnt FROM event_interests "
+ "WHERE event_id IN (:ids) GROUP BY event_id",
params,
rs -> {
result.put(rs.getLong("event_id"), rs.getLong("cnt"));
});
return result;
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

新增的批量统计方法缺少回归测试覆盖(例如:多 event_id 计数是否正确、未出现在结果集中的 id 是否按 0 处理、传入空集合是否短路不打 DB)。仓库里已有 JdbcTemplate 仓储的集成测试模式(H2 + test-schema.sql),建议为该方法补一组类似的 repository 集成测试,避免后续改 SQL/方言时回归。

Copilot uses AI. Check for mistakes.
## PR #10 Copilot CR 修复

1) schema.sql 给 events.title 加 UNIQUE 约束,seed 改用真正原子幂等的
   ON CONFLICT (title) DO NOTHING(并发初始化 / 重跑都安全)。
2) 新增 EventInterestRepositoryTests(H2 + test-schema)覆盖:
   - add 幂等、remove 幂等
   - countByEventIds 多 id 聚合正确
   - 未出现的 id 由调用方 getOrDefault(0L) 兜底
   - 空集合不打 DB
   - cascadeDelete 事件删除级联清理 interest(test-schema FK 对齐生产)
3) 顺手把 EventInterestRepository.add 的 ON CONFLICT 去掉,改纯 INSERT +
   DuplicateKeyException 兜底:H2 在 PostgreSQL MODE 下不稳支持 ON CONFLICT
   语法,改写后测试 / 生产方言一致,跑 7 个测试全绿。

## 顶级权限(superadmin)功能

- 新增 AdminUserController,类级 @SaCheckRole("superadmin")
  - GET  /api/admin/users?q=xxx   列出所有用户(可按 username / displayName / email
    模糊搜,后端做一遍防止大用户表全拉到前端)
  - PUT  /api/admin/users/{id}/admin  {admin: true|false}  切换 admin 角色
- 新增 DTO:AdminUserView(外发快照,不含敏感字段)+ UpdateUserAdminRoleRequest
  (只收一个布尔动作,避免前端伪造未定义角色)
- 规则:
  - superadmin 角色永远不允许通过 API 授予 / 撤销(防误操作锁死后台)
  - 自己不能给自己撤销 admin(防唯一管理员把自己锁出来)
  - user 角色始终保留(OAuth 登录时自动挂)

## Seed 文档更新

schema.sql 顶部注释写清 superadmin 升级流程:首次 OAuth 登录 → 手动 UPDATE
一次 roles='superadmin,admin,user';之后所有 admin 授予 / 撤销都在
/admin/users 页面完成,不再碰 DB。
@longsizhuo longsizhuo merged commit 46e5c1b into main Apr 17, 2026
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.

3 participants