mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(referrals): 添加邀请返利和注册确认功能
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN privacy_policy_accepted_version VARCHAR(64),
|
||||
ADD COLUMN privacy_policy_accepted_at BIGINT;
|
||||
|
||||
ALTER TABLE announcements
|
||||
ADD COLUMN requires_ack BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_invite_codes (
|
||||
user_id VARCHAR(64) PRIMARY KEY,
|
||||
invite_code VARCHAR(64) NOT NULL UNIQUE,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
CONSTRAINT user_invite_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_referrals (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
inviter_user_id VARCHAR(64) NOT NULL,
|
||||
invitee_user_id VARCHAR(64) NOT NULL UNIQUE,
|
||||
invite_code_snapshot VARCHAR(64) NOT NULL,
|
||||
source_json TEXT,
|
||||
first_paid_order_id VARCHAR(64),
|
||||
first_paid_at BIGINT,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
KEY idx_user_referrals_inviter (inviter_user_id, created_at),
|
||||
KEY idx_user_referrals_invite_code (invite_code_snapshot),
|
||||
CONSTRAINT user_referrals_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT user_referrals_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT user_referrals_first_paid_order_fkey FOREIGN KEY (first_paid_order_id) REFERENCES payment_orders(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS referral_rewards (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
referral_id VARCHAR(64) NOT NULL,
|
||||
inviter_user_id VARCHAR(64) NOT NULL,
|
||||
invitee_user_id VARCHAR(64) NOT NULL,
|
||||
reward_type VARCHAR(32) NOT NULL,
|
||||
trigger_point VARCHAR(64) NOT NULL,
|
||||
source_order_id VARCHAR(64),
|
||||
idempotency_key VARCHAR(128) NOT NULL UNIQUE,
|
||||
amount_usd DOUBLE NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
wallet_transaction_id VARCHAR(64),
|
||||
reversed_amount_usd DOUBLE NOT NULL DEFAULT 0,
|
||||
pending_reversal_amount_usd DOUBLE NOT NULL DEFAULT 0,
|
||||
failure_reason TEXT,
|
||||
admin_operator_id VARCHAR(64),
|
||||
admin_note TEXT,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
KEY idx_referral_rewards_inviter_status (inviter_user_id, status, created_at),
|
||||
KEY idx_referral_rewards_source_order (source_order_id),
|
||||
CONSTRAINT referral_rewards_referral_id_fkey FOREIGN KEY (referral_id) REFERENCES user_referrals(id) ON DELETE CASCADE,
|
||||
CONSTRAINT referral_rewards_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT referral_rewards_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT referral_rewards_source_order_fkey FOREIGN KEY (source_order_id) REFERENCES payment_orders(id) ON DELETE SET NULL
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN privacy_policy_accepted_version character varying(64),
|
||||
ADD COLUMN privacy_policy_accepted_at timestamp with time zone;
|
||||
|
||||
ALTER TABLE public.announcements
|
||||
ADD COLUMN requires_ack boolean NOT NULL DEFAULT false;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_invite_codes (
|
||||
user_id character varying(64) PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
invite_code character varying(64) NOT NULL UNIQUE,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_referrals (
|
||||
id character varying(64) PRIMARY KEY,
|
||||
inviter_user_id character varying(64) NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
invitee_user_id character varying(64) NOT NULL UNIQUE REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
invite_code_snapshot character varying(64) NOT NULL,
|
||||
source_json jsonb,
|
||||
first_paid_order_id character varying(64) REFERENCES public.payment_orders(id) ON DELETE SET NULL,
|
||||
first_paid_at timestamp with time zone,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_referrals_inviter
|
||||
ON public.user_referrals USING btree (inviter_user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_referrals_invite_code
|
||||
ON public.user_referrals USING btree (invite_code_snapshot);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.referral_rewards (
|
||||
id character varying(64) PRIMARY KEY,
|
||||
referral_id character varying(64) NOT NULL REFERENCES public.user_referrals(id) ON DELETE CASCADE,
|
||||
inviter_user_id character varying(64) NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
invitee_user_id character varying(64) NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
reward_type character varying(32) NOT NULL,
|
||||
trigger_point character varying(64) NOT NULL,
|
||||
source_order_id character varying(64) REFERENCES public.payment_orders(id) ON DELETE SET NULL,
|
||||
idempotency_key character varying(128) NOT NULL UNIQUE,
|
||||
amount_usd numeric(20,8) NOT NULL,
|
||||
status character varying(32) NOT NULL DEFAULT 'pending',
|
||||
wallet_transaction_id character varying(64),
|
||||
reversed_amount_usd numeric(20,8) NOT NULL DEFAULT 0,
|
||||
pending_reversal_amount_usd numeric(20,8) NOT NULL DEFAULT 0,
|
||||
failure_reason text,
|
||||
admin_operator_id character varying(64),
|
||||
admin_note text,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_rewards_inviter_status
|
||||
ON public.referral_rewards USING btree (inviter_user_id, status, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_rewards_source_order
|
||||
ON public.referral_rewards USING btree (source_order_id);
|
||||
@@ -0,0 +1,63 @@
|
||||
ALTER TABLE users ADD COLUMN privacy_policy_accepted_version TEXT;
|
||||
ALTER TABLE users ADD COLUMN privacy_policy_accepted_at INTEGER;
|
||||
|
||||
ALTER TABLE announcements ADD COLUMN requires_ack INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_invite_codes (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
invite_code TEXT NOT NULL UNIQUE,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_referrals (
|
||||
id TEXT PRIMARY KEY,
|
||||
inviter_user_id TEXT NOT NULL,
|
||||
invitee_user_id TEXT NOT NULL UNIQUE,
|
||||
invite_code_snapshot TEXT NOT NULL,
|
||||
source_json TEXT,
|
||||
first_paid_order_id TEXT,
|
||||
first_paid_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(inviter_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(invitee_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(first_paid_order_id) REFERENCES payment_orders(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_referrals_inviter
|
||||
ON user_referrals (inviter_user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_referrals_invite_code
|
||||
ON user_referrals (invite_code_snapshot);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS referral_rewards (
|
||||
id TEXT PRIMARY KEY,
|
||||
referral_id TEXT NOT NULL,
|
||||
inviter_user_id TEXT NOT NULL,
|
||||
invitee_user_id TEXT NOT NULL,
|
||||
reward_type TEXT NOT NULL,
|
||||
trigger_point TEXT NOT NULL,
|
||||
source_order_id TEXT,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
amount_usd REAL NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
wallet_transaction_id TEXT,
|
||||
reversed_amount_usd REAL NOT NULL DEFAULT 0,
|
||||
pending_reversal_amount_usd REAL NOT NULL DEFAULT 0,
|
||||
failure_reason TEXT,
|
||||
admin_operator_id TEXT,
|
||||
admin_note TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(referral_id) REFERENCES user_referrals(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(inviter_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(invitee_user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(source_order_id) REFERENCES payment_orders(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_rewards_inviter_status
|
||||
ON referral_rewards (inviter_user_id, status, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_rewards_source_order
|
||||
ON referral_rewards (source_order_id);
|
||||
@@ -131,6 +131,7 @@ CREATE TABLE IF NOT EXISTS public.announcements (
|
||||
author_id character varying(36),
|
||||
is_active boolean DEFAULT true,
|
||||
is_pinned boolean DEFAULT false,
|
||||
requires_ack boolean DEFAULT false NOT NULL,
|
||||
start_time timestamp with time zone,
|
||||
end_time timestamp with time zone,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
@@ -1379,11 +1380,72 @@ CREATE TABLE IF NOT EXISTS public.users (
|
||||
email_verified boolean NOT NULL,
|
||||
rate_limit integer,
|
||||
rate_limit_mode text DEFAULT 'system'::text NOT NULL,
|
||||
privacy_policy_accepted_version character varying(64),
|
||||
privacy_policy_accepted_at timestamp with time zone,
|
||||
metadata json
|
||||
);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_invite_codes; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_invite_codes (
|
||||
user_id character varying(64) NOT NULL,
|
||||
invite_code character varying(64) NOT NULL,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_referrals; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_referrals (
|
||||
id character varying(64) NOT NULL,
|
||||
inviter_user_id character varying(64) NOT NULL,
|
||||
invitee_user_id character varying(64) NOT NULL,
|
||||
invite_code_snapshot character varying(64) NOT NULL,
|
||||
source_json jsonb,
|
||||
first_paid_order_id character varying(64),
|
||||
first_paid_at timestamp with time zone,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: referral_rewards; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.referral_rewards (
|
||||
id character varying(64) NOT NULL,
|
||||
referral_id character varying(64) NOT NULL,
|
||||
inviter_user_id character varying(64) NOT NULL,
|
||||
invitee_user_id character varying(64) NOT NULL,
|
||||
reward_type character varying(32) NOT NULL,
|
||||
trigger_point character varying(64) NOT NULL,
|
||||
source_order_id character varying(64),
|
||||
idempotency_key character varying(128) NOT NULL,
|
||||
amount_usd numeric(20,8) NOT NULL,
|
||||
status character varying(32) DEFAULT 'pending'::character varying NOT NULL,
|
||||
wallet_transaction_id character varying(64),
|
||||
reversed_amount_usd numeric(20,8) DEFAULT 0 NOT NULL,
|
||||
pending_reversal_amount_usd numeric(20,8) DEFAULT 0 NOT NULL,
|
||||
failure_reason text,
|
||||
admin_operator_id character varying(64),
|
||||
admin_note text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_groups; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -1227,6 +1227,96 @@ END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_invite_codes user_invite_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_invite_codes
|
||||
ADD CONSTRAINT user_invite_codes_pkey PRIMARY KEY (user_id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_invite_codes user_invite_codes_invite_code_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_invite_codes
|
||||
ADD CONSTRAINT user_invite_codes_invite_code_key UNIQUE (invite_code);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_referrals user_referrals_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_referrals
|
||||
ADD CONSTRAINT user_referrals_pkey PRIMARY KEY (id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_referrals user_referrals_invitee_user_id_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_referrals
|
||||
ADD CONSTRAINT user_referrals_invitee_user_id_key UNIQUE (invitee_user_id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: referral_rewards referral_rewards_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.referral_rewards
|
||||
ADD CONSTRAINT referral_rewards_pkey PRIMARY KEY (id);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: referral_rewards referral_rewards_idempotency_key_key; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.referral_rewards
|
||||
ADD CONSTRAINT referral_rewards_idempotency_key_key UNIQUE (idempotency_key);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_plan_entitlements user_plan_entitlements_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -581,6 +581,38 @@ CREATE INDEX IF NOT EXISTS idx_user_sessions_user_device ON public.user_sessions
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_user_referrals_inviter; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_referrals_inviter ON public.user_referrals USING btree (inviter_user_id, created_at DESC);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_user_referrals_invite_code; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_referrals_invite_code ON public.user_referrals USING btree (invite_code_snapshot);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_referral_rewards_inviter_status; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_rewards_inviter_status ON public.referral_rewards USING btree (inviter_user_id, status, created_at DESC);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_referral_rewards_source_order; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_rewards_source_order ON public.referral_rewards USING btree (source_order_id);
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: idx_video_tasks_external_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -792,6 +792,126 @@ END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_invite_codes user_invite_codes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_invite_codes
|
||||
ADD CONSTRAINT user_invite_codes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_referrals user_referrals_inviter_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_referrals
|
||||
ADD CONSTRAINT user_referrals_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_referrals user_referrals_invitee_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_referrals
|
||||
ADD CONSTRAINT user_referrals_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_referrals user_referrals_first_paid_order_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.user_referrals
|
||||
ADD CONSTRAINT user_referrals_first_paid_order_id_fkey FOREIGN KEY (first_paid_order_id) REFERENCES public.payment_orders(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: referral_rewards referral_rewards_referral_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.referral_rewards
|
||||
ADD CONSTRAINT referral_rewards_referral_id_fkey FOREIGN KEY (referral_id) REFERENCES public.user_referrals(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: referral_rewards referral_rewards_inviter_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.referral_rewards
|
||||
ADD CONSTRAINT referral_rewards_inviter_user_id_fkey FOREIGN KEY (inviter_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: referral_rewards referral_rewards_invitee_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.referral_rewards
|
||||
ADD CONSTRAINT referral_rewards_invitee_user_id_fkey FOREIGN KEY (invitee_user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: referral_rewards referral_rewards_source_order_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
DO $mig$ BEGIN
|
||||
ALTER TABLE ONLY public.referral_rewards
|
||||
ADD CONSTRAINT referral_rewards_source_order_id_fkey FOREIGN KEY (source_order_id) REFERENCES public.payment_orders(id) ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
WHEN duplicate_table THEN NULL;
|
||||
WHEN invalid_table_definition THEN NULL;
|
||||
END $mig$;
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Name: video_tasks video_tasks_api_key_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -7,7 +7,7 @@ use tracing::info;
|
||||
// Generated by build.rs from schema/bootstrap/postgres.
|
||||
pub(crate) static EMPTY_DATABASE_SNAPSHOT_SQL: &str =
|
||||
include_str!(concat!(env!("OUT_DIR"), "/empty_database_snapshot.sql"));
|
||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260515000000;
|
||||
pub(crate) const EMPTY_DATABASE_SNAPSHOT_CUTOFF_VERSION: i64 = 20260516000000;
|
||||
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
SELECT COUNT(*)::BIGINT
|
||||
|
||||
@@ -305,6 +305,7 @@ fn empty_database_snapshot_covers_current_cutoff_versions() {
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260515000000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -587,6 +588,7 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
||||
20260512000000,
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -603,6 +605,7 @@ fn mysql_and_sqlite_migrations_include_enabled_incrementals() {
|
||||
20260512000000,
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -1119,6 +1122,7 @@ fn pending_migrations_from_applied_skips_versions_already_applied() {
|
||||
20260512090000,
|
||||
20260512110000,
|
||||
20260515000000,
|
||||
20260516000000,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,6 +135,48 @@ impl AnnouncementReadRepository for InMemoryAnnouncementReadRepository {
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
async fn list_required_unread_active_announcements(
|
||||
&self,
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredAnnouncement>, DataLayerError> {
|
||||
let announcements = self
|
||||
.announcements
|
||||
.read()
|
||||
.expect("announcement repository lock");
|
||||
let reads = self
|
||||
.announcement_reads
|
||||
.read()
|
||||
.expect("announcement reads repository lock");
|
||||
|
||||
let mut items = announcements
|
||||
.iter()
|
||||
.filter(|announcement| {
|
||||
announcement.requires_ack
|
||||
&& announcement.is_active
|
||||
&& announcement
|
||||
.start_time_unix_secs
|
||||
.is_none_or(|value| value <= now_unix_secs)
|
||||
&& announcement
|
||||
.end_time_unix_secs
|
||||
.is_none_or(|value| value >= now_unix_secs)
|
||||
&& !reads.contains(&(user_id.to_string(), announcement.id.clone()))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.is_pinned
|
||||
.cmp(&left.is_pinned)
|
||||
.then_with(|| right.priority.cmp(&left.priority))
|
||||
.then_with(|| right.created_at_unix_ms.cmp(&left.created_at_unix_ms))
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
items.truncate(limit);
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -153,6 +195,7 @@ impl AnnouncementWriteRepository for InMemoryAnnouncementReadRepository {
|
||||
record.priority,
|
||||
true,
|
||||
record.is_pinned,
|
||||
record.requires_ack,
|
||||
Some(record.author_id),
|
||||
None,
|
||||
record.start_time_unix_secs.map(|value| value as i64),
|
||||
@@ -201,6 +244,9 @@ impl AnnouncementWriteRepository for InMemoryAnnouncementReadRepository {
|
||||
if let Some(is_pinned) = record.is_pinned {
|
||||
announcement.is_pinned = is_pinned;
|
||||
}
|
||||
if let Some(requires_ack) = record.requires_ack {
|
||||
announcement.requires_ack = requires_ack;
|
||||
}
|
||||
if let Some(start_time_unix_secs) = record.start_time_unix_secs {
|
||||
announcement.start_time_unix_secs = Some(start_time_unix_secs);
|
||||
}
|
||||
@@ -261,6 +307,7 @@ mod tests {
|
||||
10,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -291,6 +338,7 @@ mod tests {
|
||||
kind: "maintenance".to_string(),
|
||||
priority: 10,
|
||||
is_pinned: true,
|
||||
requires_ack: false,
|
||||
author_id: "admin-1".to_string(),
|
||||
start_time_unix_secs: None,
|
||||
end_time_unix_secs: None,
|
||||
@@ -308,6 +356,7 @@ mod tests {
|
||||
priority: Some(99),
|
||||
is_active: Some(false),
|
||||
is_pinned: Some(false),
|
||||
requires_ack: Some(true),
|
||||
start_time_unix_secs: None,
|
||||
end_time_unix_secs: None,
|
||||
})
|
||||
@@ -337,6 +386,7 @@ mod tests {
|
||||
10,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -353,6 +403,7 @@ mod tests {
|
||||
5,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
|
||||
@@ -18,6 +18,7 @@ SELECT
|
||||
a.priority,
|
||||
a.is_active,
|
||||
a.is_pinned,
|
||||
a.requires_ack,
|
||||
a.author_id,
|
||||
u.username AS author_username,
|
||||
a.start_time AS start_time_unix_secs,
|
||||
@@ -144,6 +145,39 @@ WHERE a.is_active = 1
|
||||
.map_sql_err()?;
|
||||
Ok(row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64)
|
||||
}
|
||||
|
||||
async fn list_required_unread_active_announcements(
|
||||
&self,
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredAnnouncement>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!(
|
||||
r#"
|
||||
{ANNOUNCEMENT_SELECT}
|
||||
WHERE a.is_active = 1
|
||||
AND a.requires_ack = 1
|
||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM announcement_reads r
|
||||
WHERE r.user_id = ?
|
||||
AND r.announcement_id = a.id
|
||||
)
|
||||
ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC
|
||||
LIMIT ?
|
||||
"#
|
||||
))
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(user_id)
|
||||
.bind(limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_announcement_row).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -159,9 +193,9 @@ impl AnnouncementWriteRepository for MysqlAnnouncementRepository {
|
||||
r#"
|
||||
INSERT INTO announcements (
|
||||
id, title, content, `type`, priority, author_id, is_active, is_pinned,
|
||||
start_time, end_time, created_at, updated_at
|
||||
requires_ack, start_time, end_time, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
@@ -171,6 +205,7 @@ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||
.bind(record.priority)
|
||||
.bind(record.author_id)
|
||||
.bind(record.is_pinned)
|
||||
.bind(record.requires_ack)
|
||||
.bind(optional_i64_from_u64(
|
||||
record.start_time_unix_secs,
|
||||
"announcements.start_time",
|
||||
@@ -204,6 +239,7 @@ SET title = COALESCE(?, title),
|
||||
priority = COALESCE(?, priority),
|
||||
is_active = COALESCE(?, is_active),
|
||||
is_pinned = COALESCE(?, is_pinned),
|
||||
requires_ack = COALESCE(?, requires_ack),
|
||||
start_time = COALESCE(?, start_time),
|
||||
end_time = COALESCE(?, end_time),
|
||||
updated_at = ?
|
||||
@@ -216,6 +252,7 @@ WHERE id = ?
|
||||
.bind(record.priority)
|
||||
.bind(record.is_active)
|
||||
.bind(record.is_pinned)
|
||||
.bind(record.requires_ack)
|
||||
.bind(optional_i64_from_u64(
|
||||
record.start_time_unix_secs,
|
||||
"announcements.start_time",
|
||||
@@ -303,6 +340,7 @@ fn map_announcement_row(row: &MySqlRow) -> Result<StoredAnnouncement, DataLayerE
|
||||
row.try_get("priority").map_sql_err()?,
|
||||
row.try_get("is_active").map_sql_err()?,
|
||||
row.try_get("is_pinned").map_sql_err()?,
|
||||
row.try_get("requires_ack").map_sql_err()?,
|
||||
row.try_get("author_id").map_sql_err()?,
|
||||
row.try_get("author_username").map_sql_err()?,
|
||||
row.try_get("start_time_unix_secs").map_sql_err()?,
|
||||
|
||||
@@ -18,6 +18,7 @@ SELECT
|
||||
a.priority,
|
||||
a.is_active,
|
||||
a.is_pinned,
|
||||
a.requires_ack,
|
||||
a.author_id,
|
||||
u.username AS author_username,
|
||||
EXTRACT(EPOCH FROM a.start_time)::bigint AS start_time_unix_secs,
|
||||
@@ -39,6 +40,7 @@ SELECT
|
||||
a.priority,
|
||||
a.is_active,
|
||||
a.is_pinned,
|
||||
a.requires_ack,
|
||||
a.author_id,
|
||||
u.username AS author_username,
|
||||
EXTRACT(EPOCH FROM a.start_time)::bigint AS start_time_unix_secs,
|
||||
@@ -85,6 +87,38 @@ WHERE a.is_active = TRUE
|
||||
)
|
||||
"#;
|
||||
|
||||
const LIST_REQUIRED_UNREAD_ACTIVE_ANNOUNCEMENTS_SQL: &str = r#"
|
||||
SELECT
|
||||
a.id,
|
||||
a.title,
|
||||
a.content,
|
||||
a.type,
|
||||
a.priority,
|
||||
a.is_active,
|
||||
a.is_pinned,
|
||||
a.requires_ack,
|
||||
a.author_id,
|
||||
u.username AS author_username,
|
||||
EXTRACT(EPOCH FROM a.start_time)::bigint AS start_time_unix_secs,
|
||||
EXTRACT(EPOCH FROM a.end_time)::bigint AS end_time_unix_secs,
|
||||
EXTRACT(EPOCH FROM a.created_at)::bigint AS created_at_unix_ms,
|
||||
EXTRACT(EPOCH FROM a.updated_at)::bigint AS updated_at_unix_secs
|
||||
FROM announcements a
|
||||
LEFT JOIN users u ON u.id = a.author_id
|
||||
WHERE a.is_active = TRUE
|
||||
AND a.requires_ack = TRUE
|
||||
AND (a.start_time IS NULL OR a.start_time <= TO_TIMESTAMP($2::double precision))
|
||||
AND (a.end_time IS NULL OR a.end_time >= TO_TIMESTAMP($2::double precision))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM announcement_reads r
|
||||
WHERE r.user_id = $1
|
||||
AND r.announcement_id = a.id
|
||||
)
|
||||
ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC
|
||||
LIMIT $3
|
||||
"#;
|
||||
|
||||
const CREATE_ANNOUNCEMENT_SQL: &str = r#"
|
||||
INSERT INTO announcements (
|
||||
id,
|
||||
@@ -95,6 +129,7 @@ INSERT INTO announcements (
|
||||
author_id,
|
||||
is_active,
|
||||
is_pinned,
|
||||
requires_ack,
|
||||
start_time,
|
||||
end_time,
|
||||
created_at,
|
||||
@@ -111,6 +146,7 @@ VALUES (
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
$10,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
@@ -122,6 +158,7 @@ RETURNING
|
||||
priority,
|
||||
is_active,
|
||||
is_pinned,
|
||||
requires_ack,
|
||||
author_id,
|
||||
(SELECT username FROM users WHERE id = announcements.author_id) AS author_username,
|
||||
EXTRACT(EPOCH FROM start_time)::bigint AS start_time_unix_secs,
|
||||
@@ -139,8 +176,9 @@ SET
|
||||
priority = COALESCE($5, priority),
|
||||
is_active = COALESCE($6, is_active),
|
||||
is_pinned = COALESCE($7, is_pinned),
|
||||
start_time = COALESCE($8, start_time),
|
||||
end_time = COALESCE($9, end_time),
|
||||
requires_ack = COALESCE($8, requires_ack),
|
||||
start_time = COALESCE($9, start_time),
|
||||
end_time = COALESCE($10, end_time),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
@@ -151,6 +189,7 @@ RETURNING
|
||||
priority,
|
||||
is_active,
|
||||
is_pinned,
|
||||
requires_ack,
|
||||
author_id,
|
||||
(SELECT username FROM users WHERE id = announcements.author_id) AS author_username,
|
||||
EXTRACT(EPOCH FROM start_time)::bigint AS start_time_unix_secs,
|
||||
@@ -252,6 +291,24 @@ impl AnnouncementReadRepository for SqlxAnnouncementReadRepository {
|
||||
.map_postgres_err()?;
|
||||
Ok(row.try_get::<i64, _>("total").map_postgres_err()?.max(0) as u64)
|
||||
}
|
||||
|
||||
async fn list_required_unread_active_announcements(
|
||||
&self,
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredAnnouncement>, DataLayerError> {
|
||||
let mut rows = sqlx::query(LIST_REQUIRED_UNREAD_ACTIVE_ANNOUNCEMENTS_SQL)
|
||||
.bind(user_id)
|
||||
.bind(now_unix_secs as f64)
|
||||
.bind(limit as i64)
|
||||
.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_announcement_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -269,6 +326,7 @@ impl AnnouncementWriteRepository for SqlxAnnouncementReadRepository {
|
||||
.bind(record.priority)
|
||||
.bind(record.author_id)
|
||||
.bind(record.is_pinned)
|
||||
.bind(record.requires_ack)
|
||||
.bind(optional_datetime(record.start_time_unix_secs))
|
||||
.bind(optional_datetime(record.end_time_unix_secs))
|
||||
.fetch_one(&self.pool)
|
||||
@@ -290,6 +348,7 @@ impl AnnouncementWriteRepository for SqlxAnnouncementReadRepository {
|
||||
.bind(record.priority)
|
||||
.bind(record.is_active)
|
||||
.bind(record.is_pinned)
|
||||
.bind(record.requires_ack)
|
||||
.bind(optional_datetime(record.start_time_unix_secs))
|
||||
.bind(optional_datetime(record.end_time_unix_secs))
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -356,6 +415,7 @@ fn map_announcement_row(row: &PgRow) -> Result<StoredAnnouncement, DataLayerErro
|
||||
row.try_get("priority").map_postgres_err()?,
|
||||
row.try_get("is_active").map_postgres_err()?,
|
||||
row.try_get("is_pinned").map_postgres_err()?,
|
||||
row.try_get("requires_ack").map_postgres_err()?,
|
||||
row.try_get("author_id").map_postgres_err()?,
|
||||
row.try_get("author_username").map_postgres_err()?,
|
||||
row.try_get("start_time_unix_secs").map_postgres_err()?,
|
||||
|
||||
@@ -18,6 +18,7 @@ SELECT
|
||||
a.priority,
|
||||
a.is_active,
|
||||
a.is_pinned,
|
||||
a.requires_ack,
|
||||
a.author_id,
|
||||
u.username AS author_username,
|
||||
a.start_time AS start_time_unix_secs,
|
||||
@@ -144,6 +145,39 @@ WHERE a.is_active = 1
|
||||
.map_sql_err()?;
|
||||
Ok(row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64)
|
||||
}
|
||||
|
||||
async fn list_required_unread_active_announcements(
|
||||
&self,
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredAnnouncement>, DataLayerError> {
|
||||
let rows = sqlx::query(&format!(
|
||||
r#"
|
||||
{ANNOUNCEMENT_SELECT}
|
||||
WHERE a.is_active = 1
|
||||
AND a.requires_ack = 1
|
||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM announcement_reads r
|
||||
WHERE r.user_id = ?
|
||||
AND r.announcement_id = a.id
|
||||
)
|
||||
ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC
|
||||
LIMIT ?
|
||||
"#
|
||||
))
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(user_id)
|
||||
.bind(limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_announcement_row).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -159,9 +193,9 @@ impl AnnouncementWriteRepository for SqliteAnnouncementRepository {
|
||||
r#"
|
||||
INSERT INTO announcements (
|
||||
id, title, content, type, priority, author_id, is_active, is_pinned,
|
||||
start_time, end_time, created_at, updated_at
|
||||
requires_ack, start_time, end_time, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
@@ -171,6 +205,7 @@ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
||||
.bind(record.priority)
|
||||
.bind(record.author_id)
|
||||
.bind(record.is_pinned)
|
||||
.bind(record.requires_ack)
|
||||
.bind(optional_i64_from_u64(
|
||||
record.start_time_unix_secs,
|
||||
"announcements.start_time",
|
||||
@@ -204,6 +239,7 @@ SET title = COALESCE(?, title),
|
||||
priority = COALESCE(?, priority),
|
||||
is_active = COALESCE(?, is_active),
|
||||
is_pinned = COALESCE(?, is_pinned),
|
||||
requires_ack = COALESCE(?, requires_ack),
|
||||
start_time = COALESCE(?, start_time),
|
||||
end_time = COALESCE(?, end_time),
|
||||
updated_at = ?
|
||||
@@ -216,6 +252,7 @@ WHERE id = ?
|
||||
.bind(record.priority)
|
||||
.bind(record.is_active)
|
||||
.bind(record.is_pinned)
|
||||
.bind(record.requires_ack)
|
||||
.bind(optional_i64_from_u64(
|
||||
record.start_time_unix_secs,
|
||||
"announcements.start_time",
|
||||
@@ -303,6 +340,7 @@ fn map_announcement_row(row: &SqliteRow) -> Result<StoredAnnouncement, DataLayer
|
||||
row.try_get("priority").map_sql_err()?,
|
||||
row.try_get("is_active").map_sql_err()?,
|
||||
row.try_get("is_pinned").map_sql_err()?,
|
||||
row.try_get("requires_ack").map_sql_err()?,
|
||||
row.try_get("author_id").map_sql_err()?,
|
||||
row.try_get("author_username").map_sql_err()?,
|
||||
row.try_get("start_time_unix_secs").map_sql_err()?,
|
||||
@@ -341,6 +379,7 @@ mod tests {
|
||||
kind: "info".to_string(),
|
||||
priority: 10,
|
||||
is_pinned: true,
|
||||
requires_ack: false,
|
||||
author_id: "user-1".to_string(),
|
||||
start_time_unix_secs: Some(100),
|
||||
end_time_unix_secs: Some(300),
|
||||
@@ -392,6 +431,7 @@ mod tests {
|
||||
priority: Some(20),
|
||||
is_active: Some(false),
|
||||
is_pinned: Some(false),
|
||||
requires_ack: Some(true),
|
||||
start_time_unix_secs: None,
|
||||
end_time_unix_secs: None,
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ pub struct StoredAnnouncement {
|
||||
pub priority: i32,
|
||||
pub is_active: bool,
|
||||
pub is_pinned: bool,
|
||||
pub requires_ack: bool,
|
||||
pub author_id: Option<String>,
|
||||
pub author_username: Option<String>,
|
||||
pub start_time_unix_secs: Option<u64>,
|
||||
@@ -27,6 +28,7 @@ impl StoredAnnouncement {
|
||||
priority: i32,
|
||||
is_active: bool,
|
||||
is_pinned: bool,
|
||||
requires_ack: bool,
|
||||
author_id: Option<String>,
|
||||
author_username: Option<String>,
|
||||
start_time_unix_secs: Option<i64>,
|
||||
@@ -63,6 +65,7 @@ impl StoredAnnouncement {
|
||||
priority,
|
||||
is_active,
|
||||
is_pinned,
|
||||
requires_ack,
|
||||
author_id,
|
||||
author_username,
|
||||
start_time_unix_secs: start_time_unix_secs
|
||||
@@ -117,6 +120,13 @@ pub trait AnnouncementReadRepository: Send + Sync {
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<u64, crate::DataLayerError>;
|
||||
|
||||
async fn list_required_unread_active_announcements(
|
||||
&self,
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredAnnouncement>, crate::DataLayerError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
@@ -126,6 +136,7 @@ pub struct CreateAnnouncementRecord {
|
||||
pub kind: String,
|
||||
pub priority: i32,
|
||||
pub is_pinned: bool,
|
||||
pub requires_ack: bool,
|
||||
pub author_id: String,
|
||||
pub start_time_unix_secs: Option<u64>,
|
||||
pub end_time_unix_secs: Option<u64>,
|
||||
@@ -166,6 +177,7 @@ pub struct UpdateAnnouncementRecord {
|
||||
pub priority: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
pub is_pinned: Option<bool>,
|
||||
pub requires_ack: Option<bool>,
|
||||
pub start_time_unix_secs: Option<u64>,
|
||||
pub end_time_unix_secs: Option<u64>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user