Skip to content

trajectoryjp/cudi-oss

Repository files navigation

CUDI

プロジェクトの背景

本プロジェクトは、石川県加賀市・能美市が推進する 加賀・能美スマートサービス構築事業 において開発されたシステムのバックエンドです。

必要環境

ローカルで起動・開発するために必要なもの:

  • Go(go.modgo バージョンに準拠)
  • Docker / Docker Compose(ローカル DB、統合テスト、E2E テストで使用)
  • GNU Make(make ターゲット経由で各種コマンドを実行)

OpenAPI コードを再生成する場合は、追加で @redocly/cli を実行できる JavaScript ランタイム(Node.js + npx、bun + bunx 等)が必要です。

環境変数

  • デフォルトでは ./env/service.env を読み込みます。
  • 読み込み先は環境変数 ENV で差し替え可能です。

例:

ENV=./env/service.env go run .

主な設定は env/service.env を参照してください(DB 接続、API ベースパス、Cognito 設定など)。

OpenAPI コード生成

OpenAPI 定義は docs/openapi/cudi/openapi.yaml です。 OpenAPI 定義を分割した場合、参照($ref)を含んだままだと oapi-codegen で期待通りに生成されないケースがあるため、先に @redocly/cli で参照を解決した単一ファイル(bundled)を生成してから oapi-codegen を実行します。

  1. oapi-codegen のインストール(例)
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
  1. @redocly/cli の準備(いずれか)
# グローバルインストールする場合
npm install -g @redocly/cli

# もしくは、インストールせずに npx で実行する場合
# npx @redocly/cli --version
  1. bundled.yaml の生成(docs/openapi/cudi/openapi.yaml を起点に参照を解決)
# docs/openapi/cudi/openapi.yaml を起点に、すべての参照を解決して bundled.yaml を生成
redocly bundle docs/openapi/cudi/openapi.yaml --output docs/openapi/cudi/gen/bundled.yaml
  1. oapi-codegen 実行
oapi-codegen --config=configs/oapi-codegen.yaml docs/openapi/cudi/gen/bundled.yaml

出力先は configs/oapi-codegen.yamloutput を参照してください(現状は pkg/openapi/openapi.gen.go)。

ローカル起動

DB(Docker)

docker compose --env-file env/service.env up -d db

API サーバ

go run .

起動時に以下の順序でスキーマ同期が実行されます:

  1. pre/ SQL — enum 定義、レガシー制約の削除など(AutoMigrate の前提条件)
  2. GORM AutoMigrate — テーブル/カラム/インデックスの作成・調整
  3. post/ SQL — CHECK 制約、CASCADE FK、トリガー、VIEW、部分 UNIQUE インデックスなど(GORM で表現できない定義)

詳細は DB スキーマ管理 を参照してください。

API のベースパスは API_VERSION から組み立てられます。 デフォルト例: /api/v1

DB スキーマ管理

概要

本プロジェクトでは GORM AutoMigrate をベースに、GORM で表現できない PostgreSQL 固有の機能を SQL extras ファイルで補完しています。

アプリ起動
  │
  ├─ 1. pre/ SQL    ← enum 定義など AutoMigrate の前提条件
  ├─ 2. AutoMigrate ← テーブル/カラム/インデックス
  └─ 3. post/ SQL   ← 制約/トリガー/VIEW/部分インデックス

SQL extras の構成

SQL ファイルは dbextras/ に格納され、go:embed でバイナリに埋め込まれます。 目的別のサブディレクトリで整理されており、ディレクトリ名・ファイル名の辞書順で実行されます。 dbextras/GORM / AutoMigrate では表現できないが、最終状態として恒久的に必要なSQL だけを置きます。 一度だけ適用すればよい cleanup / backfill / legacy互換対応は docs/DB/migrations/ に置き、dbextras/ には置きません。

dbextras/
├── embed.go                          # go:embed + ApplySQLDir() ユーティリティ
├── embed_test.go                     # 順序保証・冪等性テスト
├── pre/                              # AutoMigrate 前に実行
│   └── 02_enums/
│       └── enum_types.sql
└── post/                             # AutoMigrate 後に実行
    ├── 01_constraints/               # CHECK・CASCADE 制約
    ├── 02_triggers/                  # トリガー関数・トリガー
    ├── 03_schema_fixes/              # GORM 型不整合の補正
    ├── 06_views/                     # VIEW 定義
    └── 07_indexes/                   # 部分 UNIQUE インデックス

命名規則: <番号>_<カテゴリ>/<説明>.sql — サブディレクトリ名 → ファイル名の辞書順で実行されます。

新しい SQL を追加する場合

  1. pre/ または post/ の適切なサブディレクトリに .sql ファイルを作成
  2. 新しいカテゴリが必要な場合は <番号>_<カテゴリ>/ ディレクトリを作成(番号で実行順序を制御)
  3. その SQL が one-shot migration なら dbextras/ ではなく docs/DB/migrations/ に追加する
  4. 冪等(idempotent)に書くIF NOT EXISTSCREATE OR REPLACEDO $$ ... EXCEPTION WHEN ... $$ 等を使用
  5. embed.go//go:embed pre post が再帰的に拾うため、embed.go の変更は不要
  6. テスト用 DB にも同じ SQL が適用される(testutil/migrations.goApplySQLDir を呼び出し)

GORM で管理する範囲 / SQL extras で管理する範囲

項目 管理方法
テーブル定義(カラム/型/NOT NULL) GORM モデルタグ → AutoMigrate
通常インデックス GORM index タグ → AutoMigrate
PostgreSQL カスタム enum pre/02_enums/
CHECK 制約 post/01_constraints/
CASCADE 外部キー post/01_constraints/
トリガー post/02_triggers/
GORM 型不整合の補正 post/03_schema_fixes/
VIEW post/06_views/
部分 UNIQUE インデックス (WHERE deleted_at IS NULL) post/07_indexes/ + GORM タグ(ヒント)

DBML / schema.sql について

  • DBML (docs/DB/SSDB.dbml): スキーマ設計のリファレンス
  • schema.sql (docs/DB/sql/schema.sql): DBML から make gen-schema で自動生成される SQL リファレンス
  • 日常開発では make db-sync は主経路ではありません。通常のローカル同期はアプリ起動時の同期処理に委ねます
  • 明示的に DB へ適用したい場合だけ make db-migrate / make migrate-* を使います
  • 部分 UNIQUE インデックスなど DBML で表現できない制約は NOTE で SQL extras への参照を記載しています

AutoMigrate の対象モデル

model.AllModels() が Single Source of Truth です(internal/infrastructure/persistence/model/models.go)。 新しいテーブルを追加する場合は、このスライスにモデルを追加してください。

開発コマンド

普段使うコマンドの一覧。詳細なターゲットは make help を参照してください。

Lint / Format

make lint   # golangci-lint + OpenAPI lint (vacuum) + yamlfmt --lint
make fmt    # gofmt + golangci-lint --fix + yamlfmt

make fmt は Go コードの整形に加え、auto-fix 可能な lint 違反と YAML(OpenAPI 定義含む)の整形まで一括で行います。

テスト

make test              # ユニットテスト(go test ./...)
make test-unit         # ユニットテスト + カバレッジ + mock 自動生成
make test-integration  # 統合テスト(要: Docker。testcontainers が PostgreSQL を起動)
make test-all          # ユニット + 統合

API E2E テスト

OpenAPI 仕様に対する契約テストは runn で記述しています(tests/api/**/*.yaml)。

# Docker でDB+APIを起動し、E2E テストを実行
make test-api-docker         # 既存イメージを使う
make test-api-docker-build   # イメージを再ビルドしてから実行

ローカルで API サーバーを起動済みの場合は make test-api でも実行できます(要: シードデータ投入)。

POS CSV 連携の差し替え

無人店舗の売上CSV取込・在庫CSV出力機能には、参考実装として internal/infrastructure/integration/poscsv/ を同梱しています。 この実装は接続先 POS システムを想定しないシンプルな UTF-8 CSV 形式を採用しており、実運用では自組織の POS フォーマットに合わせて差し替える想定です。

参考実装の仕様

売上取込 CSV:

sales_date,product_name,quantity,unit_price,tax_rate
2026-04-24,Apple,10,100,10
  • sales_date: YYYY-MM-DD(JST 解釈)
  • product_name: 商品マスタの名称と完全一致
  • quantity, unit_price: 整数
  • tax_rate: 0, 8, 10(%)
  • 1ファイルにつき1日分の集計として取り込まれ、external_order_id は POSCSV_YYYYMMDD

在庫出力 CSV:

product_name,category,quantity,price,tax_kind,tax_rate
Apple,Fruit,50,100,internal,10
  • tax_kind: internal / external / non_taxable
  • tax_rate: 8 or 10(%)

独自 POS フォーマットへの差し替え方

  1. internal/domain/port/csv/port.go の以下のインターフェースを実装した構造体を用意する:
    • SalesCSVParser — 売上CSVを []SalesImportData に変換
    • StockCSVBuilder — 在庫データを CSV として書き出す
  2. bootstrap.go 内で参考実装(poscsv.NewPosCsvSalesParser() / poscsv.NewPosCsvStockBuilder())の生成箇所を独自実装に差し替える
  3. 必要なら internal/domain/entity/sales.goSourceSystem enum と DB enum (dbextras/pre/02_enums/source_system_enum.sql) に独自の識別子を追加する

ビジネスロジック側(重複時の上書き判定・商品名マッチ失敗時のエラー整形)は internal/domain/service/csv_import_service.go にあります。差し替え時にこの挙動を変えたい場合は同ファイルを調整してください。

Smartlock 連携の差し替え

設備(Equipment)の解錠機能は internal/domain/gateway/smartlock_gateway.goSmartlockGatewayInterface を介して呼び出されます。 デフォルトでは internal/infrastructure/client/noop_smartlock_client.goNoopSmartlockClient が組み込まれており、Unlock 呼び出しは常に gateway.ErrSmartlockMisconfigured(HTTP 500 / SmartlockMisconfigured)を返します。

実運用では自組織が利用するスマートロック製品の API に合わせた client 実装を用意し、Noop を差し替えてください。

gateway 契約

type SmartlockGatewayInterface interface {
    Unlock(ctx context.Context, equipmentID uuid.UUID) *apperror.AppError
}

実装は失敗時に以下のいずれかの sentinel error でラップした *AppError を返してください。service 層が HTTP ステータスを付与して上位に伝搬させます。

sentinel 用途 HTTP
gateway.ErrSmartlockMisconfigured URL/認証/設定/デバイスID不備 500
gateway.ErrSmartlockGatewayUnavailable 外部API到達不可・異常応答 502

差し替え手順

  1. internal/infrastructure/client/<vendor>_smartlock_client.go を新規作成し、SmartlockGatewayInterface を実装する
  2. bootstrap.goclient.NewNoopSmartlockClient() を独自実装の生成に差し替える
  3. ベンダー固有の設定(API キー、デバイス ID 等)が必要なら pkg/config/env.go に環境変数を追加し、必要に応じて新しい DB テーブルとモデルを追加する
  4. 設備とベンダー固有設定を紐付けるための汎用マッピングテーブルとして smartlock_vendor_mappingsinternal/infrastructure/persistence/model/equipments_model.go)が用意されています。vendor_type + config_id のポリモーフィック参照で複数ベンダーをサポートできます

License

Apache License 2.0. 詳細は LICENSE を参照してください。

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages