参加承認・公開文書に関する歴史的な設計・準備コードの原文移管。 設計・コード断片・机上チェックをBot実装PASS、DB migration、Cloudflare Pages公開、Telegram実E2Eと扱わない。旧原本:‣。

原文本文

対象:seigo-gace/GateApp-Bot master。既存の PostgreSQL/Drizzle/Telegraf を継続利用する。これは先行準備ソースであり未デプロイ・未テスト。

追加 migration SQL

新規ファイル候補:artifacts/bot/src/db/migrations/20261028_gate_consent.sql

CREATE TABLE IF NOT EXISTS gate_consents (
  id BIGSERIAL PRIMARY KEY,
  target_id INTEGER NOT NULL,
  tg_user_id BIGINT NOT NULL,
  attempt_id TEXT NOT NULL,
  stage TEXT NOT NULL CHECK (stage IN ('question1','question2','rules')),
  document_version TEXT NOT NULL,
  accepted BOOLEAN NOT NULL,
  accepted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (target_id, tg_user_id, attempt_id, stage)
);

CREATE INDEX IF NOT EXISTS gate_consents_target_user_idx
ON gate_consents (target_id, tg_user_id, accepted_at DESC);

ALTER TABLE gate_sessions ADD COLUMN IF NOT EXISTS attempt_id TEXT;
ALTER TABLE gate_sessions ADD COLUMN IF NOT EXISTS document_version TEXT;

既存runMigrations()は大きな複合SQLを実行しているので、新規SQLファイルを実行対象に登録するか、最小限の追加SQLを既存の client.query に移植する。両方実行しない。

追加Drizzleスキーマ

新規ファイル候補:artifacts/bot/src/db/schema/gate_consents.ts

import { pgTable, bigserial, bigint, integer, text, boolean, timestamp, uniqueIndex, index } from "drizzle-orm/pg-core";

export const gate_consents = pgTable("gate_consents", {
  id: bigserial("id", { mode: "number" }).primaryKey(),
  target_id: integer("target_id").notNull(),
  tg_user_id: bigint("tg_user_id", { mode: "number" }).notNull(),
  attempt_id: text("attempt_id").notNull(),
  stage: text("stage").notNull(),
  document_version: text("document_version").notNull(),
  accepted: boolean("accepted").notNull(),
  accepted_at: timestamp("accepted_at", { withTimezone: true }).notNull().defaultNow(),
}, (t) => [
  uniqueIndex("gate_consents_attempt_stage_uidx").on(t.target_id,t.tg_user_id,t.attempt_id,t.stage),
  index("gate_consents_target_user_idx").on(t.target_id,t.tg_user_id,t.accepted_at),
]);

artifacts/bot/src/db/schema/index.tsへexport * from "./gate_consents";を追加。

artifacts/bot/src/db/schema/sessions.tsの gate_sessions へ次の2列を追加:

attempt_id: text("attempt_id"),
document_version: text("document_version"),

同意記録リポジトリ(追加)

artifacts/bot/src/repositories/consentRepo.ts

import { and,eq } from "drizzle-orm";
import { db, gate_consents } from "../db/index.js";

export type ConsentStage = "question1" | "question2" | "rules";

export async function recordConsent(input: {
 targetId:number; userId:number; attemptId:string;
 stage:ConsentStage; version:string; accepted:boolean;
}) {
  const values = {
    target_id:input.targetId,
    tg_user_id:input.userId,
    attempt_id:input.attemptId,
    stage:input.stage,
    document_version:input.version,
    accepted:input.accepted,
  };
  await db.insert(gate_consents).values(values)
    .onConflictDoNothing();
}

export async function hasAllConsents(input:{
 targetId:number; userId:number; attemptId:string; version:string;
}):Promise<boolean> {
  const rows = await db.select({
    stage: gate_consents.stage,
    version: gate_consents.document_version,
    accepted: gate_consents.accepted
  }).from(gate_consents).where(and(
    eq(gate_consents.target_id,input.targetId),
    eq(gate_consents.tg_user_id,input.userId),
    eq(gate_consents.attempt_id,input.attemptId),
  ));
  return (["question1","question2","rules"] as const).every(stage =>
    rows.some(r=>r.stage===stage && r.version===input.version && r.accepted)
  );
}

注意:gate_sessionsはtg_user_id主キーで現状1ユーザー1セッション。別対象グループと並行する審査は競合する。既存構造を無条件に変更せず、attempt_idとtarget_idの照合、または申請の直列化を先行実装する。

仕様上の条件