refactor(settings): 统一语言设置,合并 interface_language 和 ai_language

- 后端 Schema 将 interface_language 和 ai_language 合并为 language
- 前端设置界面只保留一个语言选项
- AI 回复语言统一使用 language 设置
- 更新协议文档
- 新增数据库迁移脚本
This commit is contained in:
ZL-Q
2026-04-28 17:19:47 +08:00
parent 940c67e642
commit b9617ae152
20 changed files with 740 additions and 176 deletions
@@ -0,0 +1,201 @@
"""update profile settings schema in trigger
Revision ID: 20260428_0002
Revises: 20260428_0001
Create Date: 2026-04-28
"""
from alembic import op
revision = "20260428_0002"
down_revision = "20260428_0001"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"""
CREATE OR REPLACE FUNCTION public.initialize_profile_and_invite_code_on_signup()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_username text;
v_invite_code text;
v_referrer_id uuid;
v_attempts int := 0;
invite_code_value text;
BEGIN
v_username := 'user_' || substring(md5(new.id::text || clock_timestamp()::text || random()::text) from 1 for 6);
INSERT INTO public.profiles (id, username, avatar_url, bio, settings)
VALUES (
new.id,
v_username,
null,
null,
jsonb_build_object(
'version', 1,
'preferences', jsonb_build_object(
'language', 'zh-CN',
'timezone', 'Asia/Shanghai',
'country', 'US'
),
'privacy', jsonb_build_object(
'can_sell', false,
'profile_visibility', 'public'
),
'notification', jsonb_build_object(
'allow_notifications', true,
'allow_vibration', true
),
'divination_tutorial', jsonb_build_object(
'divination_entry_shown', false,
'auto_divination_shown', false,
'manual_divination_shown', false
)
)
)
ON CONFLICT (id) DO NOTHING;
LOOP
BEGIN
v_invite_code := public.generate_invite_code();
INSERT INTO public.invite_codes (code, owner_id, status, used_count, max_uses, expires_at, reward_config)
VALUES (
v_invite_code,
new.id,
'active',
0,
NULL,
NULL,
'{}'::jsonb
);
EXIT;
EXCEPTION WHEN unique_violation THEN
v_attempts := v_attempts + 1;
IF v_attempts >= 100 THEN
RAISE EXCEPTION 'Failed to generate unique invite code after 100 attempts';
END IF;
END;
END LOOP;
invite_code_value := new.raw_user_meta_data ->> 'invite_code';
IF invite_code_value IS NOT NULL AND length(invite_code_value) = 6 THEN
invite_code_value := upper(invite_code_value);
IF invite_code_value ~ '^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{6}$' THEN
UPDATE public.invite_codes
SET used_count = used_count + 1
WHERE code = invite_code_value
AND status = 'active'
AND (max_uses IS NULL OR used_count < max_uses)
AND (expires_at IS NULL OR expires_at > NOW())
RETURNING owner_id INTO v_referrer_id;
IF v_referrer_id IS NOT NULL THEN
UPDATE public.profiles
SET referred_by = v_referrer_id
WHERE id = new.id;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$$;
"""
)
def downgrade() -> None:
op.execute(
"""
CREATE OR REPLACE FUNCTION public.initialize_profile_and_invite_code_on_signup()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_username text;
v_invite_code text;
v_referrer_id uuid;
v_attempts int := 0;
invite_code_value text;
BEGIN
v_username := 'user_' || substring(md5(new.id::text || clock_timestamp()::text || random()::text) from 1 for 6);
INSERT INTO public.profiles (id, username, avatar_url, bio, settings)
VALUES (
new.id,
v_username,
null,
null,
jsonb_build_object(
'version', 1,
'preferences', jsonb_build_object(
'interface_language', 'zh-CN',
'ai_language', 'zh-CN',
'timezone', 'Asia/Shanghai',
'country', 'CN'
),
'privacy', jsonb_build_object('profile_visibility', 'public'),
'notification', jsonb_build_object(
'allow_notifications', true,
'allow_vibration', true
)
)
)
ON CONFLICT (id) DO NOTHING;
LOOP
BEGIN
v_invite_code := public.generate_invite_code();
INSERT INTO public.invite_codes (code, owner_id, status, used_count, max_uses, expires_at, reward_config)
VALUES (
v_invite_code,
new.id,
'active',
0,
NULL,
NULL,
'{}'::jsonb
);
EXIT;
EXCEPTION WHEN unique_violation THEN
v_attempts := v_attempts + 1;
IF v_attempts >= 100 THEN
RAISE EXCEPTION 'Failed to generate unique invite code after 100 attempts';
END IF;
END;
END LOOP;
invite_code_value := new.raw_user_meta_data ->> 'invite_code';
IF invite_code_value IS NOT NULL AND length(invite_code_value) = 6 THEN
invite_code_value := upper(invite_code_value);
IF invite_code_value ~ '^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{6}$' THEN
UPDATE public.invite_codes
SET used_count = used_count + 1
WHERE code = invite_code_value
AND status = 'active'
AND (max_uses IS NULL OR used_count < max_uses)
AND (expires_at IS NULL OR expires_at > NOW())
RETURNING owner_id INTO v_referrer_id;
IF v_referrer_id IS NOT NULL THEN
UPDATE public.profiles
SET referred_by = v_referrer_id
WHERE id = new.id;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$$;
"""
)
@@ -0,0 +1,52 @@
"""migrate existing profile settings to new schema
Revision ID: 20260428_0003
Revises: 20260428_0002
Create Date: 2026-04-28
"""
from alembic import op
revision = "20260428_0003"
down_revision = "20260428_0002"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"""
UPDATE profiles
SET settings = jsonb_build_object(
'version', 1,
'preferences', jsonb_build_object(
'language', COALESCE(settings->'preferences'->>'language', 'zh-CN'),
'timezone', COALESCE(settings->'preferences'->>'timezone', 'Asia/Shanghai'),
'country', COALESCE(settings->'preferences'->>'country', 'US')
),
'privacy', jsonb_build_object(
'can_sell', COALESCE((settings->'privacy'->>'can_sell')::boolean, false),
'profile_visibility', COALESCE(settings->'privacy'->>'profile_visibility', 'public')
),
'notification', jsonb_build_object(
'allow_notifications', COALESCE((settings->'notification'->>'allow_notifications')::boolean, true),
'allow_vibration', COALESCE((settings->'notification'->>'allow_vibration')::boolean, true)
),
'divination_tutorial', jsonb_build_object(
'divination_entry_shown', COALESCE((settings->'divination_tutorial'->>'divination_entry_shown')::boolean, false),
'auto_divination_shown', COALESCE((settings->'divination_tutorial'->>'auto_divination_shown')::boolean, false),
'manual_divination_shown', COALESCE((settings->'divination_tutorial'->>'manual_divination_shown')::boolean, false)
)
)
WHERE settings IS NOT NULL;
"""
)
def downgrade() -> None:
raise RuntimeError(
"20260428_0003 is a destructive JSON data-shape migration and cannot be "
"downgraded automatically. Restore profile settings from backup if rollback "
"to the previous schema is required."
)