セクション D: batch-jobs (Go) をモノレポ配下へ取り込み#4
Merged
Merged
Conversation
Feature/insert faculty rooms
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 決定的 UUID + ON CONFLICT DO NOTHING で再実行安全な Upsert を追加 - 休講・補講・教室変更から前日 18:00 JST の通知を生成 - 通知対象は course_registrations から引く授業履修者 - 不要になった Notification の Create 層は削除 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
これまでは新しいtarget_userの追加のみで、対象外になったユーザーの行が残留していた。 uniqueIDsが空のときは全削除、そうでないときは新リストに含まれないユーザーを削除してから OnConflict DoNothingでinsertするよう変更し、冪等なupsertにした。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ListUpcomingの比較を date > ? から date >= ? に変更し、引数をfrom.Format("2006-01-02")で
日付文字列化。type:dateカラムと整合させ、意図(明日以降)を命名と条件の両方で表現した
- 呼び出し側をtodayStartからtomorrowに変更し、対象日が明日以降であることを明確化
- periodJaを(string, bool)に変更。未知のPeriod値は警告ログを出しつつ、時限抜きの文面に
フォールバックしてユーザー向け通知に内部enum文字列を漏らさないようにした
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
repository の ListUpcoming を ListByDate にリネームし、WHERE 句を date >= ? から date = ? に変更。翌カレンダー日(JST)に該当する 休講・補講・教室変更のみを通知テーブルに入れるようにする。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
通知ごとに is_notified を即時更新し、500件超バッチの部分失敗時も sent > 0 を成功扱いとして重複送信を防ぐ。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ications DB接続と通知書き込み層を追加(user-api 準拠)
これまでリポジトリ直下の classification_result.csv と
faculty_rooms_data/ 配下に散らばっていた静的データを
data/ 直下にフラット化し、参照箇所を併せて更新した。
- classification_result.csv と faculties_*.csv / rooms.csv を data/ 直下へ移動
- lesson_ids.py のデフォルトパスを data/classification_result.csv に変更
- scripts/insert_faculty_rooms.py の CSV_DIR を data/ に変更
- README のディレクトリ説明と取り込みスクリプトのパス記載を更新
- .dockerignore のパターンを data/**/*.{json,csv} に変更し、
移動後も Docker ビルドコンテキストから除外され続けるようにした
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: pyproject.toml と uv.lock を追加 requirements.txt の内容を pyproject.toml に移植し、uv で依存を厳密にロックする。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(mise): Python 管理を uv に委譲して uv 自体をピン留め Python のバージョンは pyproject.toml の requires-python と uv.lock で 管理するため、mise からは python を外して uv 0.11.8 を追加する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(docker): uv ベースイメージに移行 ghcr.io/astral-sh/uv:0.11.8-debian-slim をベースに、 依存と本体を 2 段階 COPY して uv sync --frozen でインストールする。 ビルドキャッシュが効きやすくなり、再現性も uv.lock に統一される。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: ローカル実行手順を uv 前提に更新し requirements.txt を削除 uv sync / uv run でローカル開発を完結させる手順に書き換え、 役目を終えた requirements.txt を削除する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * コードを src/dotto_batch_jobs/ 配下の単一パッケージに再配置 スクレイピングと教員居室取り込みの 2 ジョブを scrape_class_changes / insert_faculty_rooms サブパッケージに分け、共有 DB レイヤを db/ に集約。 旧パス(main.py, lesson_ids.py, db/, scrapers/, scripts/)を削除し、 モジュール内の相対 import を dotto_batch_jobs.* の絶対 import に統一する。 本コミットはコード再配置と import 書き換えに限定し、振る舞いは変更しない。 * pyproject を hatchling ベースの単一パッケージ化し console_scripts を追加 uv ワークスペース構成の検討から、依存と配布が単純な単一パッケージ構成に 方針を変更。pyproject.toml に hatchling を build-backend として設定し、 依存(requests / beautifulsoup4 / sqlalchemy / cloud-sql-python-connector / pg8000 / python-dotenv)をトップに集約する。 2 つのジョブのエントリポイントを [project.scripts] に登録し、 scrape-class-changes と insert-faculty-rooms コマンドで起動できるようにする。 uv.lock も新しい単一パッケージ構成に合わせて再生成する。 * Dockerfile を新しい console_script エントリポイントに合わせて更新 単一パッケージ化に伴い、ソースコピー後にプロジェクト本体も uv sync してインストールし、CMD を python main.py から scrape-class-changes コマンド起動に切り替える。 * README をパッケージ再配置と新コマンドに合わせて更新 エントリポイント表とディレクトリ構成、ローカル実行手順、運用フローを src/dotto_batch_jobs/ レイアウトと scrape-class-changes / insert-faculty-rooms コマンド呼び出しに更新する。 * リポジトリルート算出を src 配下の新パッケージ階層に合わせて修正 src/dotto_batch_jobs/ 配下への再配置でファイル深さが 1 段増えたが、 ROOT を Path(__file__).resolve().parents[4] のままにしていたため リポジトリ外(~/Developer 直下)を指してしまい、 data/classification_result.csv が見つからず lessonId 照合がスキップされていた。 parents[3] に修正してリポジトリルートを正しく解決する。 * 休講・補講スクレイプ結果の最新スナップショットを反映 scrape-class-changes 実行で取得した最新の休講・補講データを反映する。 休講に 4/30 ロボットの科学技術、5/1 情報機器概論、7/10 自律システムを追加し、 補講は 7/10 自律システム休講分(7/24)を最新内容に差し替えた。 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Notification ドメイン/DB スキーマを per-user 通知済み管理 + FCM 拡張ペイロードへ更新 通知済みフラグを Notification.IsNotified から NotificationTargetUser.NotifiedAt に移し、ユーザー単位での配信管理を可能にする。 あわせて Title/Body 以外の FCM オプション (ImageURL, AnalyticsLabel, APNs, Android, Webpush) をスキーマ・ドメインに追加し、Message を Body に改名。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Notification リポジトリを per-user notified_at 管理に切り替え MarkAsDispatched (通知単位の is_notified 更新) を MarkUsersAsNotified (target_users.notified_at 更新) に置き換える。 ListPendingNotifications は notified_at IS NULL のユーザーが残っている 通知だけを返し、未通知ユーザーのみを TargetUsers として詰めるように変更。 UpsertNotification は TargetUsers の差集合同期と既存 notified_at の保持を 両立させ (OnConflict DoNothing)、util に uniqueTargetUsers ヘルパーを追加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 通知配信サービスを per-user 管理 + FCM 拡張ペイロードに対応 dispatch-notifications を、成功した user だけ MarkUsersAsNotified で 通知済みにする per-user 配信に変更。FCM トークン未登録ユーザーや、 multicast 応答で個別失敗したユーザーを区別して扱うようにし、 全件一括の MarkAsDispatched 呼び出しを撤廃。 配信メッセージは Title/Body に加え APNs / Android / Webpush 等の 拡張オプションを反映できるようにし、enqueue 側では class change 通知に APNsSound=default をセット。dry-run のフラグ説明も notified_at 表記に更新。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 通知配信ループ内で都度 MarkUsersAsNotified を呼んでクラッシュ時の重複配信を抑止 ループ末尾でまとめてフラッシュしていたため、途中でプロセスが落ちると FCM 送信済みでも notified_at が未更新のまま残り、次回ディスパッチで 重複配信される恐れがあった。通知1件ごとに即時永続化するよう変更。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * TotalFCMSent をトークン成功送信数に統一 dry-run では len(tokens)、本実行では len(successUserIDs) を加算しており、 1ユーザー複数トークン時に値の意味が乖離していた。sendToTokens に 成功トークン数を返させて dry-run と意味を揃え、フィールド名 TotalFCMSent の示す内容と一致させる。部分失敗ログもトークン単位の内訳を併記。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Python 出力データ data/*.json を追跡対象から除外 Python プログラムが生成する成果物のため、リポジトリで管理する必要がない。 追跡済みの 6 ファイルを削除し、.gitignore に data/*.json を追加して 今後再び追跡されないようにした。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * insert-faculty-rooms の入力 CSV と年度を CLI 引数化 faculties CSV パスと対象年度をコード内にハードコードしていたため、 追加年度の取り込みや別パス指定のたびに編集が必要だった。 --faculties YEAR=PATH 形式の必須引数(複数指定可)に置き換え、 ヘルプに必須カラム (name, email, room_name) と CSV 形式も明記した。 README の実行例も新しい呼び出し方に更新している。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 入力 CSV (rooms / faculties_2025 / faculties_2026) をリポジトリから削除 insert-faculty-rooms が CSV パスを CLI 引数で受け取るようになり、 リポジトリ内に固定パスで CSV を抱える必要が無くなった。 rooms.csv も含めて入力データはリポジトリ外で管理する方針へ寄せ、 追跡対象から削除する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * lessonId 照合のソースを CSV から subjects テーブルへ移行 classification_result.csv で持っていた (syllabus_id, name) は subjects テーブルに同等情報があり、CSV を別管理する必要がない。 load_name_maps を Engine 受け取りに変更して DB から名前マップを構築し、 fill_lesson_ids_in_records は複数レコード呼び出しでもクエリを 1 回に 抑えられるよう、マップを引数で受け取る形にした。 main.py は DB エンジン取得後に lessonId 照合を行う流れへ変更し、 不要になった CSV パスヘルパと data/classification_result.csv を削除した。 README の data/ 説明も併せて更新している。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * JSON 出力先を data/ から output/ に変更し追跡対象から除外 入力データの置き場である data/ にスクレイピング結果の JSON を混在させて いたが、入力 CSV 群を整理した結果 data/ を使う必要がなくなったため、 出力専用の output/ ディレクトリへ分離する。 .gitignore は data/*.json を output/ に置き換え、ディレクトリごと 追跡対象外とした。 README の構成図と実行結果の説明も output/ に揃えている。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: 入力 CSV のパス例をプレースホルダ化 data/ 配下を前提とした例から <path-to-data-directory>/... に書き換え、 入力 CSV の置き場が任意であることを README とヘルプ文の双方で揃える。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scrape-class-changes): subjects 取得結果を syllabus_id, name で並べ替えて決定的にする ORDER BY が無いと DB の返却順に依存し、同一 name の重複がある場合に load_name_maps() の「先勝ち」結果が非決定になる。実行ごとに同じ マッピングへ収束するよう ORDER BY syllabus_id, name を付与する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(scrape-class-changes): fuzzy_pick_id の docstring を subjects 由来の表現に更新 CSV からの取り込みを廃止し subjects テーブルへ移行したのに合わせ、 「CSV 側キー」という残存表現を「subjects 由来のキー」に直す。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(insert-faculty-rooms): 必須カラムを email, room_name に統一しヘッダ検証を追加 実装で参照していない name を必須カラムから外し、README/ヘルプ/description を 実装と整合させる。あわせて CSV ヘッダ行に email / room_name が含まれていない 場合は INSERT 前に中断するよう存在チェックを追加し、ヘッダ欠落で全行未一致に なる事故を早期に検知する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(insert-faculty-rooms): BOM 付き CSV を扱えるよう utf-8-sig で開く * fix(scrape-class-changes): syllabus_id の int 変換失敗時に stderr に警告を出す * fix(scrape-class-changes): lessonId 照合の例外捕捉を SQLAlchemyError に絞り traceback を出力する * docs(readme): 構成表の insert-faculty-rooms 説明を CLI 引数指定に合わせる * fix(insert-faculty-rooms): CSV パスのオープン失敗時に stderr へメッセージを出して終了コード 1 で中断する * fix(insert-faculty-rooms): CSV を newline="" で開いて環境差による空行混入を防ぐ --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
git filter-repo で配置した internal/modules/batch-jobs/ 配下の import を 新リポジトリのパスに揃える (Notion 計画 §7-D)。 - github.com/fun-dotto/schedule-scripts/internal/<pkg> → github.com/fun-dotto/server/internal/modules/batch-jobs/<pkg> - github.com/fun-dotto/shared-go/db/model → github.com/fun-dotto/server/internal/shared/model cmd 側の wiring 変更や DB 接続の差し替えは別コミットで扱い、 本コミットは internal/modules/batch-jobs/ 内のパッケージ参照だけを 書き換える純粋な機械的置換に留める。
build-class-change-notifications-job / dispatch-notifications-job の DB 接続を internal/shared/db.ConnectWithConnectorIAMAuthN へ集約し、 shared 層をモノレポ全体の単一接続ソースとして扱う方針 (Notion 計画 §7-D) を batch-jobs 側にも適用する。 - パッケージ名 db との衝突回避のため変数名は conn に統一 - repository wiring の引数も conn に置換 - バイナリ名 /bin/build-class-change-notifications-job、 /bin/dispatch-notifications-job の対応を維持
batch-jobs (旧 fun-dotto/schedule-scripts) の repository / service が 要求する firebase.google.com/go/v4 とその間接依存を go.mod に追加し、 go mod tidy で整理した。go test ./... も通ることを確認した上で確定する。
Contributor
There was a problem hiding this comment.
Pull request overview
fun-dotto/batch-jobs(旧 schedule-scripts の Go 部分)をモノレポ配下へ履歴ごと取り込み、クラス変更通知の生成ジョブと通知配信ジョブ(FCM)を cmd/*-job と internal/modules/batch-jobs として動かせるようにする PR です。
Changes:
internal/modules/batch-jobs/に domain/database/repository/service を追加し、通知の enqueue と dispatch ロジックを実装cmd/build-class-change-notifications-job/cmd/dispatch-notifications-jobのエントリポイントを追加し、共通 DB 接続ヘルパ(internal/shared/db)へ接続を統一- Firebase Admin SDK 追加に伴う依存(
go.mod/go.sum)更新
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/modules/batch-jobs/service/notification_dispatch.go | 未配信通知を取得し、FCM multicast で配信・配信済み更新するサービスを追加 |
| internal/modules/batch-jobs/service/class_change_notification.go | 休講/補講/教室変更から通知を組み立てるサービスの骨組みを追加 |
| internal/modules/batch-jobs/service/class_change_notification_enqueue.go | 翌日分のクラス変更を通知テーブルへ enqueue する処理を追加 |
| internal/modules/batch-jobs/repository/util.go | 重複排除ユーティリティを追加 |
| internal/modules/batch-jobs/repository/room_change.go | RoomChangeRepository の器を追加 |
| internal/modules/batch-jobs/repository/room_change_list_by_date.go | 日付指定で教室変更を取得するクエリを追加 |
| internal/modules/batch-jobs/repository/notification.go | NotificationRepository の器を追加 |
| internal/modules/batch-jobs/repository/notification_upsert.go | 通知とターゲットユーザーの upsert/同期処理を追加 |
| internal/modules/batch-jobs/repository/notification_mark_notified.go | 配信済み(notified_at)更新処理を追加 |
| internal/modules/batch-jobs/repository/notification_list_pending.go | 配信ウィンドウ内かつ未配信ユーザーがいる通知の取得処理を追加 |
| internal/modules/batch-jobs/repository/makeup_class.go | MakeupClassRepository の器を追加 |
| internal/modules/batch-jobs/repository/makeup_class_list_by_date.go | 日付指定で補講を取得するクエリを追加 |
| internal/modules/batch-jobs/repository/fcm_token.go | FCM トークン一覧取得(フィルタ付き)を追加 |
| internal/modules/batch-jobs/repository/course_registration.go | CourseRegistrationRepository の器を追加 |
| internal/modules/batch-jobs/repository/course_registration_list_user_ids_by_subject.go | 科目IDから受講ユーザーID一覧を取得するクエリを追加 |
| internal/modules/batch-jobs/repository/cancelled_class.go | CancelledClassRepository の器を追加 |
| internal/modules/batch-jobs/repository/cancelled_class_list_by_date.go | 日付指定で休講を取得するクエリを追加 |
| internal/modules/batch-jobs/domain/subject.go | Subject ドメインモデルを追加 |
| internal/modules/batch-jobs/domain/room.go | Room ドメインモデルを追加 |
| internal/modules/batch-jobs/domain/room_change.go | RoomChange ドメインモデルを追加 |
| internal/modules/batch-jobs/domain/notification.go | Notification / NotificationTargetUser ドメインモデルを追加 |
| internal/modules/batch-jobs/domain/makeup_class.go | MakeupClass ドメインモデルを追加 |
| internal/modules/batch-jobs/domain/fcm_token.go | FCMToken とフィルタ型を追加 |
| internal/modules/batch-jobs/domain/cancelled_class.go | CancelledClass ドメインモデルを追加 |
| internal/modules/batch-jobs/database/subject.go | subjects テーブル相当の GORM モデルと ToDomain を追加 |
| internal/modules/batch-jobs/database/room.go | rooms テーブル相当の GORM モデルと ToDomain を追加 |
| internal/modules/batch-jobs/database/room_change.go | room_changes テーブル相当の GORM モデルと ToDomain を追加 |
| internal/modules/batch-jobs/database/notification.go | notifications テーブル相当の GORM モデルと From/ToDomain を追加 |
| internal/modules/batch-jobs/database/notification_target_user.go | notification_target_users テーブル相当の GORM モデルを追加 |
| internal/modules/batch-jobs/database/makeup_class.go | makeup_classes テーブル相当の GORM モデルと ToDomain を追加 |
| internal/modules/batch-jobs/database/fcm_token.go | fcm_tokens テーブル相当の GORM モデルと ToDomain を追加 |
| internal/modules/batch-jobs/database/course_registration.go | course_registrations テーブル相当の GORM モデルを追加 |
| internal/modules/batch-jobs/database/cancelled_class.go | cancelled_classes テーブル相当の GORM モデルと ToDomain を追加 |
| cmd/dispatch-notifications-job/main.go | 通知配信ジョブ(Firebase Messaging + DB)エントリポイントを追加 |
| cmd/build-class-change-notifications-job/main.go | クラス変更通知 enqueue ジョブ(DB)エントリポイントを追加 |
| go.mod | Firebase Admin SDK 追加に伴う依存関係を更新 |
| go.sum | 依存追加に伴うチェックサムを更新 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+35
to
+42
| firebaseApp, err := firebase.NewApp(ctx, nil) | ||
| if err != nil { | ||
| log.Fatalf("Failed to initialize Firebase app: %v", err) | ||
| } | ||
| messagingClient, err := firebaseApp.Messaging(ctx) | ||
| if err != nil { | ||
| log.Fatalf("Failed to initialize Firebase Messaging client: %v", err) | ||
| } |
Comment on lines
+22
to
+26
| // ID 衝突時は本文を更新しない (再通知・重複配信を防ぐため)。target_users の増減のみ下で同期する。 | ||
| if err := tx.Clauses(clause.OnConflict{ | ||
| Columns: []clause.Column{{Name: "id"}}, | ||
| DoNothing: true, | ||
| }).Create(&dbNotification).Error; err != nil { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
やったこと
Notion 計画 §7-D に沿って、
fun-dotto/batch-jobsの Go 部分 (旧fun-dotto/schedule-scriptsモジュール名) をcmd/{build-class-change-notifications-job,dispatch-notifications-job}/+internal/modules/batch-jobs/としてgit filter-repoで履歴ごと取り込む。Python スクレイパー (src/,Dockerfile.scraper,pyproject.toml,uv.lock) は対象外。git filter-repo --invert-pathsで Python 部分 (src/,pyproject.toml,uv.lock,Dockerfile.scraper) とterraform/を除外internal/database/database.go(shared-go と重複する Connect 実装)、go.mod/mise.toml/Dockerfile等の競合トップレベルファイル、.claude/.codex/.github/.dockerignore/README.md/.gitignoreも同時に除外--path-rename internal/→internal/modules/batch-jobs/、cmd/build-class-change-notifications/→cmd/build-class-change-notifications-job/、cmd/dispatch-notifications/→cmd/dispatch-notifications-job/で再配置git merge --allow-unrelated-historiesでブランチに合流 (.mcp.json は merge 中の競合回避のため一時退避)github.com/fun-dotto/schedule-scripts/internal/<pkg>→internal/modules/batch-jobs/<pkg>github.com/fun-dotto/shared-go/db/model→internal/shared/modelinternal/shared/db.ConnectWithConnectorIAMAuthNに切り替え (academic と同様)。変数名はdbパッケージとの衝突回避のためconnに統一repositorywiring の引数もconnに置換-dry-runフラグや CLI 引数の扱いは変更せず、Cloud Run Job 側の args は §F で受け止める前提を維持firebase.google.com/go/v4とその間接依存をgo.modに追加しgo mod tidy確認したこと
mise exec -- go build ./...がリポジトリ全体で成功mise exec -- go test ./...が成功 (academic / batch-jobs ともテスト Pass)cmd/{build-class-change-notifications-job,dispatch-notifications-job}/main.goから旧database.ConnectWithConnectorIAMAuthN呼び出しが消え、shared/db に切り替わっていることを確認メモ
internal/database/の処理は §C と同じ理由でdatabase.goのみ除外し、ToDomain等の変換ヘルパを含む model ファイル群はinternal/modules/batch-jobs/database/として保持。internal/shared/modelへの一本化は §E でatlas schema diffを踏まえて検討batch-jobsリポジトリの Python 部分削除は §H (cutover) のタスクとして別途実施feat/c-academic-api