参加承認・公開文書に関する歴史的な設計・準備コードの原文移管。 設計・コード断片・机上チェックをBot実装PASS、DB migration、Cloudflare Pages公開、Telegram実E2Eと扱わない。旧原本:‣。
対象:seigo-gace/GateApp-Bot master。既存の PostgreSQL/Drizzle/Telegraf を継続利用する。これは先行準備ソースであり未デプロイ・未テスト。
新規ファイル候補: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 に移植する。両方実行しない。
新規ファイル候補: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の照合、または申請の直列化を先行実装する。
target_id、本人のtg_user_id、審査試行attempt_id、版、日時を含める。