定制商品方案end
投票功能
- 先发布定制产品,然后,接受投票,然后预定,
- 不需要收藏
- 朋友帮忙支付分享链接
-- =========================================================-- 004_phase1_mvp_feature_tables.sql-- Phase-1 feature tables (incremental)-- Depends on: 001_base_schema.sql + 002_approval_extension.sql + 003_simple_supplier_sync.sql-- =========================================================
create extension if not exists pgcrypto;
-- =========================-- 1) Enums-- =========================do $$begin if not exists (select 1 from pg_type where typname = 'admin_role') then create type admin_role as enum ('admin', 'operator', 'viewer'); end if;end$$;comment on type admin_role is '后台角色:admin=全权限,operator=运营,viewer=只读';
do $$begin if not exists (select 1 from pg_type where typname = 'template_channel') then create type template_channel as enum ('email', 'sms', 'push'); end if;end$$;comment on type template_channel is '消息模板渠道类型';
do $$begin if not exists (select 1 from pg_type where typname = 'message_send_status') then create type message_send_status as enum ('queued', 'sending', 'sent', 'failed', 'cancelled'); end if;end$$;comment on type message_send_status is '消息发送状态';
do $$begin if not exists (select 1 from pg_type where typname = 'refund_status') then create type refund_status as enum ('pending', 'processing', 'succeeded', 'failed', 'cancelled'); end if;end$$;comment on type refund_status is '退款状态';
do $$begin if not exists (select 1 from pg_type where typname = 'customization_draft_status') then create type customization_draft_status as enum ('draft', 'validated', 'submitted', 'archived'); end if;end$$;comment on type customization_draft_status is '定制草稿状态';
-- =========================-- 2) Admin user / role / audit-- =========================create table if not exists admin_users ( id uuid primary key default gen_random_uuid(), user_id uuid not null unique references auth.users(id) on delete cascade, admin_email text not null, role admin_role not null default 'operator', is_active boolean not null default true, last_login_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now());comment on table admin_users is '后台管理员账户映射(基于 Supabase auth.users 扩展)';comment on column admin_users.id is '主键ID';comment on column admin_users.user_id is '关联 auth.users.id';comment on column admin_users.admin_email is '后台登录邮箱';comment on column admin_users.role is '管理员角色';comment on column admin_users.is_active is '是否启用';comment on column admin_users.last_login_at is '最近登录时间';comment on column admin_users.created_at is '创建时间';comment on column admin_users.updated_at is '更新时间';
create unique index if not exists uq_admin_users_email on admin_users(lower(admin_email));
create table if not exists admin_permissions ( id uuid primary key default gen_random_uuid(), role admin_role not null, resource text not null, action text not null, allowed boolean not null default true, created_at timestamptz not null default now(), unique(role, resource, action));comment on table admin_permissions is '后台角色权限矩阵(菜单/按钮/接口粒度)';comment on column admin_permissions.role is '角色';comment on column admin_permissions.resource is '资源标识(如 orders、mail_templates)';comment on column admin_permissions.action is '动作标识(如 read/create/approve)';comment on column admin_permissions.allowed is '是否允许';
create table if not exists admin_login_events ( id uuid primary key default gen_random_uuid(), admin_user_id uuid references admin_users(id) on delete set null, login_identifier text, ip inet, user_agent text, device_info jsonb not null default '{}'::jsonb, success boolean not null, failure_reason text, created_at timestamptz not null default now());comment on table admin_login_events is '管理员登录审计日志(用于安全审计与异常排查)';comment on column admin_login_events.admin_user_id is '关联管理员ID';comment on column admin_login_events.login_identifier is '登录标识(邮箱或账号)';comment on column admin_login_events.ip is '登录IP';comment on column admin_login_events.user_agent is '浏览器UA';comment on column admin_login_events.device_info is '设备信息快照';comment on column admin_login_events.success is '是否登录成功';comment on column admin_login_events.failure_reason is '失败原因';comment on column admin_login_events.created_at is '事件时间';
create index if not exists idx_admin_login_events_admin_created on admin_login_events(admin_user_id, created_at desc);
create index if not exists idx_admin_login_events_success_created on admin_login_events(success, created_at desc);
-- =========================-- 3) C-end account/address-- =========================create table if not exists user_addresses ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, receiver_name text not null, receiver_phone text, country_code text not null references countries(code), state text, city text, district text, address_line1 text not null, address_line2 text, postal_code text, label text, is_default boolean not null default false, created_at timestamptz not null default now(), updated_at timestamptz not null default now());comment on table user_addresses is '用户地址簿(对应 C 端地址管理)';comment on column user_addresses.user_id is '地址归属用户';comment on column user_addresses.receiver_name is '收件人姓名';comment on column user_addresses.receiver_phone is '收件人电话';comment on column user_addresses.country_code is '国家代码';comment on column user_addresses.state is '省/州';comment on column user_addresses.city is '城市';comment on column user_addresses.district is '区/县';comment on column user_addresses.address_line1 is '地址主行';comment on column user_addresses.address_line2 is '地址补充行';comment on column user_addresses.postal_code is '邮编';comment on column user_addresses.label is '地址标签(如 Home/Office)';comment on column user_addresses.is_default is '是否默认地址';
create index if not exists idx_user_addresses_user_created on user_addresses(user_id, created_at desc);
create unique index if not exists uq_user_addresses_default on user_addresses(user_id) where is_default = true;
-- =========================-- 4) Product customization template/draft-- =========================create table if not exists product_custom_templates ( id uuid primary key default gen_random_uuid(), product_id uuid not null unique references products(id) on delete cascade, template_key text not null, template_path text not null, config_path text, schema_version int not null default 1, preview_image text, is_active boolean not null default true, created_at timestamptz not null default now(), updated_at timestamptz not null default now());comment on table product_custom_templates is '商品定制模板绑定(对应 custom-pages 与 custom-config 路由)';comment on column product_custom_templates.product_id is '本地商品ID';comment on column product_custom_templates.template_key is '模板唯一键(业务可读)';comment on column product_custom_templates.template_path is '模板文件路径(如 src/custom-pages/xxx.html)';comment on column product_custom_templates.config_path is '配置文件路径(如 src/custom-config/xxx.ts)';comment on column product_custom_templates.schema_version is '模板参数版本号';comment on column product_custom_templates.preview_image is '模板预览图';comment on column product_custom_templates.is_active is '是否启用';
create unique index if not exists uq_product_custom_templates_key on product_custom_templates(template_key);
create table if not exists customization_drafts ( id uuid primary key default gen_random_uuid(), draft_key text not null unique, user_id uuid references auth.users(id) on delete set null, session_id text, product_id uuid not null references products(id) on delete cascade, template_id uuid references product_custom_templates(id) on delete set null, options_payload jsonb not null default '{}'::jsonb, render_meta jsonb not null default '{}'::jsonb, validation_result jsonb not null default '{}'::jsonb, status customization_draft_status not null default 'draft', submitted_order_item_id uuid references order_items(id) on delete set null, expires_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now());comment on table customization_drafts is '定制草稿表(支持 validate/draft 接口与跨端续作)';comment on column customization_drafts.draft_key is '草稿幂等键(前端可复用)';comment on column customization_drafts.user_id is '登录用户ID(游客为空)';comment on column customization_drafts.session_id is '游客会话ID';comment on column customization_drafts.product_id is '商品ID';comment on column customization_drafts.template_id is '模板ID';comment on column customization_drafts.options_payload is '定制参数快照';comment on column customization_drafts.render_meta is '渲染元数据(图层/缩放/资源引用)';comment on column customization_drafts.validation_result is '校验结果快照';comment on column customization_drafts.status is '草稿状态';comment on column customization_drafts.submitted_order_item_id is '提交后关联订单项ID';comment on column customization_drafts.expires_at is '草稿过期时间';
create index if not exists idx_customization_drafts_user_updated on customization_drafts(user_id, updated_at desc);
create index if not exists idx_customization_drafts_session_updated on customization_drafts(session_id, updated_at desc);
create index if not exists idx_customization_drafts_product_status on customization_drafts(product_id, status);
-- =========================-- 5) Order note / refund / payment webhook-- =========================create table if not exists order_notes ( id uuid primary key default gen_random_uuid(), order_id uuid not null references orders(id) on delete cascade, note_type text not null default 'customer_service', note text not null, is_private boolean not null default true, actor_admin_id uuid references admin_users(id) on delete set null, actor_user_id uuid references auth.users(id) on delete set null, created_at timestamptz not null default now());comment on table order_notes is '订单备注表(客服备注/内部沟通)';comment on column order_notes.order_id is '订单ID';comment on column order_notes.note_type is '备注类型';comment on column order_notes.note is '备注内容';comment on column order_notes.is_private is '是否仅后台可见';comment on column order_notes.actor_admin_id is '操作管理员';comment on column order_notes.actor_user_id is '操作用户(用户留言场景)';
create index if not exists idx_order_notes_order_created on order_notes(order_id, created_at desc);
create table if not exists order_refunds ( id uuid primary key default gen_random_uuid(), order_id uuid not null references orders(id) on delete cascade, payment_id uuid references payments(id) on delete set null, idempotency_key text, provider text not null, provider_refund_id text, reason text, amount numeric(12,2) not null check (amount > 0), currency text not null, status refund_status not null default 'pending', raw_payload jsonb not null default '{}'::jsonb, requested_by_admin_id uuid references admin_users(id) on delete set null, requested_at timestamptz not null default now(), settled_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now());comment on table order_refunds is '订单退款记录(支持幂等重试与状态追踪)';comment on column order_refunds.idempotency_key is '退款请求幂等键';comment on column order_refunds.provider is '支付通道';comment on column order_refunds.provider_refund_id is '支付侧退款ID';comment on column order_refunds.reason is '退款原因';comment on column order_refunds.amount is '退款金额';comment on column order_refunds.currency is '币种';comment on column order_refunds.status is '退款状态';comment on column order_refunds.raw_payload is '支付侧原始响应';comment on column order_refunds.requested_by_admin_id is '发起退款管理员';comment on column order_refunds.requested_at is '发起时间';comment on column order_refunds.settled_at is '退款完成时间';
create unique index if not exists uq_order_refunds_idempotency_key on order_refunds(idempotency_key) where idempotency_key is not null;
create index if not exists idx_order_refunds_order_created on order_refunds(order_id, created_at desc);
create table if not exists payment_webhook_events ( id uuid primary key default gen_random_uuid(), provider text not null, event_id text not null, event_type text not null, signature text, payload jsonb not null, verified boolean not null default false, processed boolean not null default false, processed_at timestamptz, created_at timestamptz not null default now(), unique(provider, event_id));comment on table payment_webhook_events is '支付 webhook 事件去重表(验签与幂等入口)';comment on column payment_webhook_events.provider is '支付通道';comment on column payment_webhook_events.event_id is '通道事件ID(幂等键)';comment on column payment_webhook_events.event_type is '事件类型';comment on column payment_webhook_events.signature is '回调签名原文或摘要';comment on column payment_webhook_events.payload is '回调负载';comment on column payment_webhook_events.verified is '是否通过验签';comment on column payment_webhook_events.processed is '是否完成业务处理';comment on column payment_webhook_events.processed_at is '处理完成时间';
create index if not exists idx_payment_webhook_events_processed_created on payment_webhook_events(processed, created_at desc);
-- =========================-- 6) Abandoned checkout + mail templates-- =========================create table if not exists abandoned_checkout_events ( id uuid primary key default gen_random_uuid(), checkout_draft_id uuid not null references checkout_drafts(id) on delete cascade, user_id uuid references auth.users(id) on delete set null, contact_email text, contact_phone text, event_type text not null default 'detected', reason text, recovered_order_id uuid references orders(id) on delete set null, created_at timestamptz not null default now());comment on table abandoned_checkout_events is '弃单事件记录(识别、提醒、恢复)';comment on column abandoned_checkout_events.checkout_draft_id is '结算草稿ID';comment on column abandoned_checkout_events.user_id is '归属用户';comment on column abandoned_checkout_events.contact_email is '提醒邮箱';comment on column abandoned_checkout_events.contact_phone is '提醒手机号';comment on column abandoned_checkout_events.event_type is '事件类型(detected/reminded/recovered)';comment on column abandoned_checkout_events.reason is '原因说明';comment on column abandoned_checkout_events.recovered_order_id is '恢复后订单ID';
create index if not exists idx_abandoned_checkout_events_draft_created on abandoned_checkout_events(checkout_draft_id, created_at desc);
create table if not exists mail_templates ( id uuid primary key default gen_random_uuid(), code text not null unique, name text not null, channel template_channel not null default 'email', subject_template text, body_html text, body_text text, variables jsonb not null default '[]'::jsonb, is_active boolean not null default true, version int not null default 1, created_by uuid references admin_users(id) on delete set null, updated_by uuid references admin_users(id) on delete set null, created_at timestamptz not null default now(), updated_at timestamptz not null default now());comment on table mail_templates is '消息模板表(弃单提醒/审批通知/订单状态通知)';comment on column mail_templates.code is '模板编码(业务唯一)';comment on column mail_templates.name is '模板名称';comment on column mail_templates.channel is '模板渠道';comment on column mail_templates.subject_template is '标题模板';comment on column mail_templates.body_html is 'HTML 模板体';comment on column mail_templates.body_text is '纯文本模板体';comment on column mail_templates.variables is '变量定义列表';comment on column mail_templates.is_active is '是否启用';comment on column mail_templates.version is '模板版本号';comment on column mail_templates.created_by is '创建人';comment on column mail_templates.updated_by is '更新人';
create table if not exists message_send_logs ( id uuid primary key default gen_random_uuid(), template_id uuid references mail_templates(id) on delete set null, channel template_channel not null default 'email', biz_type text not null, biz_id uuid, recipient text not null, send_status message_send_status not null default 'queued', provider text, provider_message_id text, variables_payload jsonb not null default '{}'::jsonb, provider_response jsonb not null default '{}'::jsonb, error_message text, created_at timestamptz not null default now(), updated_at timestamptz not null default now());comment on table message_send_logs is '消息发送日志(用于模板测试、弃单提醒、通知追踪)';comment on column message_send_logs.template_id is '关联模板ID';comment on column message_send_logs.channel is '发送渠道';comment on column message_send_logs.biz_type is '业务类型(abandoned_checkout/order_status 等)';comment on column message_send_logs.biz_id is '业务对象ID';comment on column message_send_logs.recipient is '接收方(邮箱/手机号/推送标识)';comment on column message_send_logs.send_status is '发送状态';comment on column message_send_logs.provider is '发送服务商';comment on column message_send_logs.provider_message_id is '服务商消息ID';comment on column message_send_logs.variables_payload is '渲染变量快照';comment on column message_send_logs.provider_response is '发送响应原文';comment on column message_send_logs.error_message is '失败信息';
create index if not exists idx_message_send_logs_biz_created on message_send_logs(biz_type, biz_id, created_at desc);
create index if not exists idx_message_send_logs_status_created on message_send_logs(send_status, created_at desc);
-- =========================-- 7) updated_at triggers-- =========================create or replace function set_updated_at_common()returns triggerlanguage plpgsqlas $$begin new.updated_at = now(); return new;end;$$;
drop trigger if exists tr_admin_users_set_updated_at on admin_users;create trigger tr_admin_users_set_updated_atbefore update on admin_usersfor each row execute function set_updated_at_common();
drop trigger if exists tr_user_addresses_set_updated_at on user_addresses;create trigger tr_user_addresses_set_updated_atbefore update on user_addressesfor each row execute function set_updated_at_common();
drop trigger if exists tr_product_custom_templates_set_updated_at on product_custom_templates;create trigger tr_product_custom_templates_set_updated_atbefore update on product_custom_templatesfor each row execute function set_updated_at_common();
drop trigger if exists tr_customization_drafts_set_updated_at on customization_drafts;create trigger tr_customization_drafts_set_updated_atbefore update on customization_draftsfor each row execute function set_updated_at_common();
drop trigger if exists tr_order_refunds_set_updated_at on order_refunds;create trigger tr_order_refunds_set_updated_atbefore update on order_refundsfor each row execute function set_updated_at_common();
drop trigger if exists tr_mail_templates_set_updated_at on mail_templates;create trigger tr_mail_templates_set_updated_atbefore update on mail_templatesfor each row execute function set_updated_at_common();
drop trigger if exists tr_message_send_logs_set_updated_at on message_send_logs;create trigger tr_message_send_logs_set_updated_atbefore update on message_send_logsfor each row execute function set_updated_at_common();