diff --git a/docs/ops/2026-02-10__admin__user-group-db-and-ui.md b/docs/ops/2026-02-10__admin__user-group-db-and-ui.md new file mode 100644 index 00000000..76a6a54c --- /dev/null +++ b/docs/ops/2026-02-10__admin__user-group-db-and-ui.md @@ -0,0 +1,31 @@ +# 用户分组数据库设计与 RPC (v1) + +## 摘要 +为“用户分组”功能新增数据库表 `public.ak_user_groups`(支持逻辑删除),并提供管理端 RPC(分页列表/保存/删除/状态切换)。同时启用 RLS,默认仅允许管理端通过 RPC 访问。 + +## 动机 +- 完善用户精细化运营体系,支持对用户进行分组分类。 +- 替换前端 `pages/mall/admin/user/group.uvue` 中的硬编码 Mock 数据,实现真实数据持久化与完整交互。 + +## 影响范围 +- 数据库:新增 `ak_user_groups` 表及相关索引、RLS 策略、4 个管理端 RPC。 +- 前端:新增 `userGroupService.uts`,重构 `group.uvue` 页面。 + +## 变更清单 +- 数据库 SQL: + - `docs/sql/10_schema/user/ak_user_groups_v1.sql` + - `docs/sql/20_rls/user/ak_user_groups_rls_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_group_list_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_group_save_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_group_delete_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_group_set_status_v1.sql` +- 前端代码: + - `services/admin/userGroupService.uts` + - `pages/mall/admin/user/group.uvue` + +## 兼容性与风险 +- 逻辑删除:采用 `deleted_at` 字段,确保历史关联数据不被物理破坏。 +- 安全性:所有管理操作均通过 `SECURITY DEFINER` RPC 并校验 `admin/analytics` 角色。 + +## 验证方式 +- 执行 SQL 后,通过 Admin 端的“用户分组”菜单进行增删改查操作,观察数据库及界面响应。 diff --git a/docs/ops/2026-02-10__admin__user-label-db-and-ui.md b/docs/ops/2026-02-10__admin__user-label-db-and-ui.md new file mode 100644 index 00000000..d6b7042c --- /dev/null +++ b/docs/ops/2026-02-10__admin__user-label-db-and-ui.md @@ -0,0 +1,31 @@ +# 用户标签数据库设计与 RPC (v1) + +## 摘要 +为“用户标签”功能新增数据库表 `public.ak_user_labels`(支持逻辑删除),并提供管理端 RPC(分页列表/保存/删除/状态切换)。同时启用 RLS,默认仅允许管理端通过 RPC 访问。 + +## 动机 +- 完善用户画像体系,支持对用户进行行为特征标记。 +- 替换前端 `pages/mall/admin/user/label.uvue` 中的硬编码 Mock 数据,实现真实数据持久化。 + +## 影响范围 +- 数据库:新增 `ak_user_labels` 表及相关索引、RLS 策略、4 个管理端 RPC。 +- 前端:新增 `userLabelService.uts`,重构 `label.uvue` 页面。 + +## 变更清单 +- 数据库 SQL: + - `docs/sql/10_schema/user/ak_user_labels_v1.sql` + - `docs/sql/20_rls/user/ak_user_labels_rls_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_label_list_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_label_save_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_label_delete_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_label_set_status_v1.sql` +- 前端代码: + - `services/admin/userLabelService.uts` + - `pages/mall/admin/user/label.uvue` + +## 兼容性与风险 +- 逻辑删除:采用 `deleted_at` 字段,确保历史关联数据不被物理破坏。 +- 安全性:所有管理操作均通过 `SECURITY DEFINER` RPC 并校验 `admin/analytics` 角色。 + +## 验证方式 +- 执行 SQL 后,通过 Admin 端的“用户标签”菜单进行增删改查操作,观察数据库及界面响应。 diff --git a/docs/ops/2026-02-10__admin__user-level-db-design.md b/docs/ops/2026-02-10__admin__user-level-db-design.md new file mode 100644 index 00000000..8fd328a1 --- /dev/null +++ b/docs/ops/2026-02-10__admin__user-level-db-design.md @@ -0,0 +1,46 @@ +# 用户等级数据库设计与 RPC(v1) + +## 摘要 +为“用户等级”功能新增数据库表 `public.ak_user_levels`(支持逻辑删除),并提供 admin 侧管理 RPC(分页列表/保存/删除/状态切换/展示切换)。同时启用 RLS:消费者端仅可读取“启用且展示且未删除”的等级。 + +## 动机 +- 当前 admin 用户等级页面存在硬编码 mock 数据,需要接入数据库。 +- 统一等级数据口径,为消费者端展示等级提供可靠数据源。 + +## 影响范围 +- 数据库:新增表、索引;启用 RLS 并新增 select policy;新增 5 个 admin RPC。 +- 前端:后续可通过 `services/` 接入新 RPC,移除 `pages/mall/admin/user/level.uvue` 中 mock。 + +## 变更清单 +- 新增文件: + - `docs/sql/10_schema/user/ak_user_levels_v1.sql` + - `docs/sql/20_rls/user/ak_user_levels_rls_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_level_list_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_level_save_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_level_delete_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_level_set_visible_v1.sql` + - `docs/sql/30_rpc/user/rpc_admin_user_level_set_status_v1.sql` + +## 兼容性与风险 +- consumer 端读取:RLS 仅开放 `anon/authenticated` 对可见/启用/未删除等级的 SELECT;不会暴露删除或禁用的数据。 +- admin 端管理:通过 `SECURITY DEFINER` RPC 并进行角色鉴权,避免直通表的全局权限。 +- 逻辑删除:通过 `deleted_at` 字段实现,避免历史引用破坏。 + +## 回滚方案 +- 回滚 schema:删除表 `ak_user_levels`(需人工确认,不在本次脚本中提供)。 +- 回滚 rpc:删除对应函数(需人工确认,不在本次脚本中提供)。 +- 回滚 rls:移除 policy 并禁用 RLS(需人工确认)。 + +## 验证方式 +- 在 SQL 控制台依次执行:schema -> rls -> rpc。 +- 使用 admin 账号调用: + - `rpc_admin_user_level_save` 创建数据 + - `rpc_admin_user_level_list` 验证分页返回 + - `rpc_admin_user_level_set_visible/status` 验证更新 + - `rpc_admin_user_level_delete` 验证逻辑删除 +- 使用普通账号/匿名访问验证仅能看到启用且展示的数据。 + +## 关联文档 +- `docs/project_spec/AGENT_PROJECT_SPEC.md` +- `docs/sql/30_rpc/auth/get_current_user_role_v1.sql` +- `docs/sql/11_roles_and_permissions_strategy.md` diff --git a/docs/sql/10_schema/user/ak_user_groups_v1.sql b/docs/sql/10_schema/user/ak_user_groups_v1.sql new file mode 100644 index 00000000..a4bc33f1 --- /dev/null +++ b/docs/sql/10_schema/user/ak_user_groups_v1.sql @@ -0,0 +1,30 @@ +-- ===================================================================================== +-- Schema: 用户分组表 +-- 位置:docs/sql/10_schema/user/ak_user_groups_v1.sql +-- 对象类型:Schema (DDL) +-- 版本:v1 +-- 说明:用户分组定义,支持逻辑删除和状态管理 +-- ===================================================================================== + +CREATE TABLE IF NOT EXISTS public.ak_user_groups ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + name TEXT NOT NULL, + remark TEXT NULL, + status INT NOT NULL DEFAULT 1, -- 1:启用, 0:禁用 + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ NULL, + + CONSTRAINT ak_user_groups_name_length CHECK (char_length(name) >= 1) +); + +-- 唯一性约束(仅对未删除记录生效) +CREATE UNIQUE INDEX IF NOT EXISTS ak_user_groups_name_uniq_active + ON public.ak_user_groups (name) + WHERE deleted_at IS NULL; + +-- 常用查询索引 +CREATE INDEX IF NOT EXISTS ak_user_groups_status_idx ON public.ak_user_groups (status) WHERE deleted_at IS NULL; +CREATE INDEX IF NOT EXISTS ak_user_groups_created_at_idx ON public.ak_user_groups (created_at DESC); diff --git a/docs/sql/10_schema/user/ak_user_labels_v1.sql b/docs/sql/10_schema/user/ak_user_labels_v1.sql new file mode 100644 index 00000000..18513a91 --- /dev/null +++ b/docs/sql/10_schema/user/ak_user_labels_v1.sql @@ -0,0 +1,33 @@ +-- ===================================================================================== +-- Schema: 用户标签表 +-- 位置:docs/sql/10_schema/user/ak_user_labels_v1.sql +-- 对象类型:Schema (DDL) +-- 版本:v1 +-- 说明:用户标签定义,支持逻辑删除与状态管理 +-- ===================================================================================== + +CREATE TABLE IF NOT EXISTS public.ak_user_labels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + name TEXT NOT NULL, + color TEXT NULL, + remark TEXT NULL, + status INT NOT NULL DEFAULT 1, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ NULL, + + CONSTRAINT ak_user_labels_name_length CHECK (char_length(name) >= 1) +); + +CREATE UNIQUE INDEX IF NOT EXISTS ak_user_labels_name_uniq_active + ON public.ak_user_labels (name) + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS ak_user_labels_status_idx + ON public.ak_user_labels (status) + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS ak_user_labels_created_at_idx + ON public.ak_user_labels (created_at DESC); diff --git a/docs/sql/10_schema/user/ak_user_levels_v1.sql b/docs/sql/10_schema/user/ak_user_levels_v1.sql new file mode 100644 index 00000000..4c0a1109 --- /dev/null +++ b/docs/sql/10_schema/user/ak_user_levels_v1.sql @@ -0,0 +1,55 @@ +-- ===================================================================================== +-- Schema: 用户等级表 +-- 位置:docs/sql/10_schema/user/ +-- 对象类型:Schema (DDL) +-- 版本:v1 +-- 说明:用户等级(经验值/折扣/展示/状态),支持逻辑删除 +-- ===================================================================================== + +CREATE TABLE IF NOT EXISTS public.ak_user_levels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + name TEXT NOT NULL, + level_weight INT NOT NULL, + min_experience INT NOT NULL DEFAULT 0, + discount_percent INT NOT NULL DEFAULT 100, + + is_visible BOOLEAN NOT NULL DEFAULT TRUE, + status INT NOT NULL DEFAULT 1, + + icon_url TEXT NULL, + bg_image_url TEXT NULL, + bg_style_json JSONB NULL, + + remark TEXT NULL, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ NULL, + + CONSTRAINT ak_user_levels_level_weight_nonnegative CHECK (level_weight >= 0), + CONSTRAINT ak_user_levels_min_experience_nonnegative CHECK (min_experience >= 0), + CONSTRAINT ak_user_levels_discount_percent_range CHECK (discount_percent BETWEEN 1 AND 100) +); + +-- 唯一性(仅对未删除记录生效) +CREATE UNIQUE INDEX IF NOT EXISTS ak_user_levels_name_uniq_active + ON public.ak_user_levels (name) + WHERE deleted_at IS NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS ak_user_levels_level_weight_uniq_active + ON public.ak_user_levels (level_weight) + WHERE deleted_at IS NULL; + +-- 常用查询索引 +CREATE INDEX IF NOT EXISTS ak_user_levels_active_filter_idx + ON public.ak_user_levels (status, is_visible) + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS ak_user_levels_min_experience_idx + ON public.ak_user_levels (min_experience) + WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS ak_user_levels_level_weight_desc_idx + ON public.ak_user_levels (level_weight DESC) + WHERE deleted_at IS NULL; diff --git a/docs/sql/20_rls/user/ak_user_groups_rls_v1.sql b/docs/sql/20_rls/user/ak_user_groups_rls_v1.sql new file mode 100644 index 00000000..8698205d --- /dev/null +++ b/docs/sql/20_rls/user/ak_user_groups_rls_v1.sql @@ -0,0 +1,12 @@ +-- ===================================================================================== +-- RLS: 用户分组表 +-- 位置:docs/sql/20_rls/user/ak_user_groups_rls_v1.sql +-- 对象类型:RLS 策略 +-- 版本:v1 +-- 说明:管理端全量访问通过 RPC 完成;消费者端默认不开放直接访问 +-- ===================================================================================== + +ALTER TABLE public.ak_user_groups ENABLE ROW LEVEL SECURITY; + +-- 如果未来消费者端需要展示所在分组,可以在此添加对应的 SELECT 策略 +-- 目前默认不向普通用户开放任何直接 SQL 读写权限 diff --git a/docs/sql/20_rls/user/ak_user_labels_rls_v1.sql b/docs/sql/20_rls/user/ak_user_labels_rls_v1.sql new file mode 100644 index 00000000..5df42930 --- /dev/null +++ b/docs/sql/20_rls/user/ak_user_labels_rls_v1.sql @@ -0,0 +1,12 @@ +-- ===================================================================================== +-- RLS: 用户标签表 +-- 位置:docs/sql/20_rls/user/ak_user_labels_rls_v1.sql +-- 对象类型:RLS 策略 +-- 版本:v1 +-- 说明:管理端全量访问通过 RPC 完成;消费者端默认不开放直接访问 +-- ===================================================================================== + +ALTER TABLE public.ak_user_labels ENABLE ROW LEVEL SECURITY; + +-- 若后续消费者端需要展示标签,可在此添加 SELECT 策略 +-- 当前默认不向普通用户开放任何直接 SQL 读写权限 diff --git a/docs/sql/20_rls/user/ak_user_levels_rls_v1.sql b/docs/sql/20_rls/user/ak_user_levels_rls_v1.sql new file mode 100644 index 00000000..9171be0c --- /dev/null +++ b/docs/sql/20_rls/user/ak_user_levels_rls_v1.sql @@ -0,0 +1,23 @@ +-- ===================================================================================== +-- RLS: 用户等级表 +-- 位置:docs/sql/20_rls/user/ +-- 对象类型:RLS 策略 +-- 版本:v1 +-- 说明:消费者端可读(仅可见/启用/未删除);管理端全量访问通过 RPC 完成 +-- ===================================================================================== + +ALTER TABLE public.ak_user_levels ENABLE ROW LEVEL SECURITY; + +-- 消费者端:允许读取可见且启用的等级(未删除) +DROP POLICY IF EXISTS ak_user_levels_public_select_visible_active ON public.ak_user_levels; +CREATE POLICY ak_user_levels_public_select_visible_active + ON public.ak_user_levels + FOR SELECT + TO anon, authenticated + USING ( + deleted_at IS NULL + AND status = 1 + AND is_visible = TRUE + ); + +-- 默认不开放写权限(INSERT/UPDATE/DELETE)给 anon/authenticated diff --git a/docs/sql/30_rpc/user/rpc_admin_user_group_delete_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_group_delete_v1.sql new file mode 100644 index 00000000..5fd60697 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_group_delete_v1.sql @@ -0,0 +1,36 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_group_delete +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:逻辑删除用户分组(设置 deleted_at) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_delete( + p_id UUID +) +RETURNS BOOLEAN +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_ok BOOLEAN; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 逻辑删除 + UPDATE public.ak_user_groups + SET deleted_at = now(), updated_at = now() + WHERE id = p_id AND deleted_at IS NULL; + + GET DIAGNOSTICS v_ok = ROW_COUNT; + RETURN v_ok; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_group_list_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_group_list_v1.sql new file mode 100644 index 00000000..686502c6 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_group_list_v1.sql @@ -0,0 +1,60 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_group_list +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:管理端分页获取用户分组列表,支持搜索、状态筛选及逻辑删除过滤 +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_list( + p_page INT, + p_page_size INT, + p_search TEXT DEFAULT NULL, + p_status INT DEFAULT NULL, + p_include_deleted BOOLEAN DEFAULT FALSE +) +RETURNS JSONB +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_total INT; + v_items JSONB; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 获取总数 + SELECT COUNT(*) INTO v_total + FROM public.ak_user_groups + WHERE (p_include_deleted OR deleted_at IS NULL) + AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%') + AND (p_status IS NULL OR status = p_status); + + -- 3. 分页获取数据 + SELECT jsonb_agg(t) INTO v_items + FROM ( + SELECT + id, name, remark, status, + created_at, updated_at, deleted_at + FROM public.ak_user_groups + WHERE (p_include_deleted OR deleted_at IS NULL) + AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%') + AND (p_status IS NULL OR status = p_status) + ORDER BY created_at DESC + LIMIT p_page_size + OFFSET (p_page - 1) * p_page_size + ) t; + + RETURN jsonb_build_object( + 'total', v_total, + 'items', COALESCE(v_items, '[]'::jsonb) + ); +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_group_save_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_group_save_v1.sql new file mode 100644 index 00000000..8cae1bde --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_group_save_v1.sql @@ -0,0 +1,66 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_group_save +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:新增/更新用户分组(逻辑删除记录默认不允许更新) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_save( + p_id UUID DEFAULT NULL, + p_name TEXT, + p_remark TEXT DEFAULT NULL, + p_status INT DEFAULT 1 +) +RETURNS UUID +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_id UUID; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 参数校验 + IF p_name IS NULL OR length(trim(p_name)) = 0 THEN + RAISE EXCEPTION 'Invalid name'; + END IF; + + -- 3. 新增 + IF p_id IS NULL THEN + INSERT INTO public.ak_user_groups( + name, remark, status, + created_at, updated_at, deleted_at + ) VALUES ( + p_name, p_remark, COALESCE(p_status, 1), + now(), now(), NULL + ) + RETURNING id INTO v_id; + + RETURN v_id; + END IF; + + -- 4. 更新(不允许更新已删除记录) + UPDATE public.ak_user_groups + SET + name = p_name, + remark = p_remark, + status = COALESCE(p_status, status), + updated_at = now() + WHERE id = p_id AND deleted_at IS NULL + RETURNING id INTO v_id; + + IF v_id IS NULL THEN + RAISE EXCEPTION 'Not found or deleted'; + END IF; + + RETURN v_id; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_group_set_status_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_group_set_status_v1.sql new file mode 100644 index 00000000..c3ae3897 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_group_set_status_v1.sql @@ -0,0 +1,37 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_group_set_status +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:设置用户分组状态(启用/禁用) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_set_status( + p_id UUID, + p_status INT +) +RETURNS BOOLEAN +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_ok BOOLEAN; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + UPDATE public.ak_user_groups + SET status = p_status, + updated_at = now() + WHERE id = p_id AND deleted_at IS NULL; + + GET DIAGNOSTICS v_ok = ROW_COUNT; + RETURN v_ok; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_label_delete_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_label_delete_v1.sql new file mode 100644 index 00000000..04b397cf --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_label_delete_v1.sql @@ -0,0 +1,36 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_label_delete +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:逻辑删除用户标签(设置 deleted_at) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_delete( + p_id UUID +) +RETURNS BOOLEAN +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_ok BOOLEAN; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 逻辑删除 + UPDATE public.ak_user_labels + SET deleted_at = now(), updated_at = now() + WHERE id = p_id AND deleted_at IS NULL; + + GET DIAGNOSTICS v_ok = ROW_COUNT; + RETURN v_ok; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_label_list_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_label_list_v1.sql new file mode 100644 index 00000000..2a555b36 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_label_list_v1.sql @@ -0,0 +1,60 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_label_list +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:管理端分页获取用户标签列表,支持搜索、状态筛选及逻辑删除过滤 +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_list( + p_page INT, + p_page_size INT, + p_search TEXT DEFAULT NULL, + p_status INT DEFAULT NULL, + p_include_deleted BOOLEAN DEFAULT FALSE +) +RETURNS JSONB +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_total INT; + v_items JSONB; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 获取总数 + SELECT COUNT(*) INTO v_total + FROM public.ak_user_labels + WHERE (p_include_deleted OR deleted_at IS NULL) + AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%') + AND (p_status IS NULL OR status = p_status); + + -- 3. 分页获取数据 + SELECT jsonb_agg(t) INTO v_items + FROM ( + SELECT + id, name, color, remark, status, + created_at, updated_at, deleted_at + FROM public.ak_user_labels + WHERE (p_include_deleted OR deleted_at IS NULL) + AND (p_search IS NULL OR name ILIKE '%' || p_search || '%' OR remark ILIKE '%' || p_search || '%') + AND (p_status IS NULL OR status = p_status) + ORDER BY created_at DESC + LIMIT p_page_size + OFFSET (p_page - 1) * p_page_size + ) t; + + RETURN jsonb_build_object( + 'total', v_total, + 'items', COALESCE(v_items, '[]'::jsonb) + ); +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_label_save_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_label_save_v1.sql new file mode 100644 index 00000000..47d12ed4 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_label_save_v1.sql @@ -0,0 +1,68 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_label_save +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:新增/更新用户标签(逻辑删除记录默认不允许更新) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_save( + p_id UUID DEFAULT NULL, + p_name TEXT, + p_color TEXT DEFAULT NULL, + p_remark TEXT DEFAULT NULL, + p_status INT DEFAULT 1 +) +RETURNS UUID +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_id UUID; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 参数校验 + IF p_name IS NULL OR length(trim(p_name)) = 0 THEN + RAISE EXCEPTION 'Invalid name'; + END IF; + + -- 3. 新增 + IF p_id IS NULL THEN + INSERT INTO public.ak_user_labels( + name, color, remark, status, + created_at, updated_at, deleted_at + ) VALUES ( + p_name, p_color, p_remark, COALESCE(p_status, 1), + now(), now(), NULL + ) + RETURNING id INTO v_id; + + RETURN v_id; + END IF; + + -- 4. 更新(不允许更新已删除记录) + UPDATE public.ak_user_labels + SET + name = p_name, + color = p_color, + remark = p_remark, + status = COALESCE(p_status, status), + updated_at = now() + WHERE id = p_id AND deleted_at IS NULL + RETURNING id INTO v_id; + + IF v_id IS NULL THEN + RAISE EXCEPTION 'Not found or deleted'; + END IF; + + RETURN v_id; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_label_set_status_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_label_set_status_v1.sql new file mode 100644 index 00000000..ffd0f4dd --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_label_set_status_v1.sql @@ -0,0 +1,37 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_label_set_status +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:设置用户标签状态(启用/禁用) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_set_status( + p_id UUID, + p_status INT +) +RETURNS BOOLEAN +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_ok BOOLEAN; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + UPDATE public.ak_user_labels + SET status = p_status, + updated_at = now() + WHERE id = p_id AND deleted_at IS NULL; + + GET DIAGNOSTICS v_ok = ROW_COUNT; + RETURN v_ok; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_level_delete_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_level_delete_v1.sql new file mode 100644 index 00000000..aed3d544 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_level_delete_v1.sql @@ -0,0 +1,36 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_level_delete +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:逻辑删除用户等级(设置 deleted_at) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_delete( + p_id UUID +) +RETURNS BOOLEAN +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_ok BOOLEAN; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 逻辑删除 + UPDATE public.ak_user_levels + SET deleted_at = now(), updated_at = now() + WHERE id = p_id AND deleted_at IS NULL; + + GET DIAGNOSTICS v_ok = ROW_COUNT; + RETURN v_ok; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_level_list_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_level_list_v1.sql new file mode 100644 index 00000000..d83b6fed --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_level_list_v1.sql @@ -0,0 +1,64 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_level_list +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:管理端分页获取用户等级列表,支持搜索、状态筛选及逻辑删除过滤 +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_list( + p_page INT, + p_page_size INT, + p_search TEXT DEFAULT NULL, + p_status INT DEFAULT NULL, + p_is_visible BOOLEAN DEFAULT NULL, + p_include_deleted BOOLEAN DEFAULT FALSE +) +RETURNS JSONB +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_total INT; + v_items JSONB; +BEGIN + -- 1. 权限检查 (依赖 public.get_current_user_role()) + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 获取总数 + SELECT COUNT(*) INTO v_total + FROM public.ak_user_levels + WHERE (p_include_deleted OR deleted_at IS NULL) + AND (p_search IS NULL OR name ILIKE '%' || p_search || '%') + AND (p_status IS NULL OR status = p_status) + AND (p_is_visible IS NULL OR is_visible = p_is_visible); + + -- 3. 分页获取数据 + SELECT jsonb_agg(t) INTO v_items + FROM ( + SELECT + id, name, level_weight, min_experience, discount_percent, + is_visible, status, icon_url, bg_image_url, bg_style_json, + remark, created_at, updated_at, deleted_at + FROM public.ak_user_levels + WHERE (p_include_deleted OR deleted_at IS NULL) + AND (p_search IS NULL OR name ILIKE '%' || p_search || '%') + AND (p_status IS NULL OR status = p_status) + AND (p_is_visible IS NULL OR is_visible = p_is_visible) + ORDER BY level_weight ASC + LIMIT p_page_size + OFFSET (p_page - 1) * p_page_size + ) t; + + RETURN jsonb_build_object( + 'total', v_total, + 'items', COALESCE(v_items, '[]'::jsonb) + ); +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_level_save_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_level_save_v1.sql new file mode 100644 index 00000000..bf92d230 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_level_save_v1.sql @@ -0,0 +1,94 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_level_save +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:新增/更新用户等级(逻辑删除记录默认不允许更新) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_save( + p_id UUID DEFAULT NULL, + p_name TEXT, + p_level_weight INT, + p_min_experience INT, + p_discount_percent INT, + p_is_visible BOOLEAN, + p_status INT, + p_icon_url TEXT DEFAULT NULL, + p_bg_image_url TEXT DEFAULT NULL, + p_bg_style_json JSONB DEFAULT NULL, + p_remark TEXT DEFAULT NULL +) +RETURNS UUID +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_id UUID; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 参数校验(最小化) + IF p_name IS NULL OR length(trim(p_name)) = 0 THEN + RAISE EXCEPTION 'Invalid name'; + END IF; + + IF p_level_weight < 0 OR p_min_experience < 0 THEN + RAISE EXCEPTION 'Invalid level_weight or min_experience'; + END IF; + + IF p_discount_percent < 1 OR p_discount_percent > 100 THEN + RAISE EXCEPTION 'Invalid discount_percent'; + END IF; + + -- 3. 新增 + IF p_id IS NULL THEN + INSERT INTO public.ak_user_levels( + name, level_weight, min_experience, discount_percent, + is_visible, status, + icon_url, bg_image_url, bg_style_json, + remark, + created_at, updated_at, deleted_at + ) VALUES ( + p_name, p_level_weight, p_min_experience, p_discount_percent, + p_is_visible, p_status, + p_icon_url, p_bg_image_url, p_bg_style_json, + p_remark, + now(), now(), NULL + ) + RETURNING id INTO v_id; + + RETURN v_id; + END IF; + + -- 4. 更新(不允许更新已删除记录) + UPDATE public.ak_user_levels + SET + name = p_name, + level_weight = p_level_weight, + min_experience = p_min_experience, + discount_percent = p_discount_percent, + is_visible = p_is_visible, + status = p_status, + icon_url = p_icon_url, + bg_image_url = p_bg_image_url, + bg_style_json = p_bg_style_json, + remark = p_remark, + updated_at = now() + WHERE id = p_id AND deleted_at IS NULL + RETURNING id INTO v_id; + + IF v_id IS NULL THEN + RAISE EXCEPTION 'Not found or deleted'; + END IF; + + RETURN v_id; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_level_set_status_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_level_set_status_v1.sql new file mode 100644 index 00000000..70604a68 --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_level_set_status_v1.sql @@ -0,0 +1,37 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_level_set_status +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:设置用户等级状态(启用/禁用) +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_set_status( + p_id UUID, + p_status INT +) +RETURNS BOOLEAN +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_ok BOOLEAN; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + UPDATE public.ak_user_levels + SET status = p_status, + updated_at = now() + WHERE id = p_id AND deleted_at IS NULL; + + GET DIAGNOSTICS v_ok = ROW_COUNT; + RETURN v_ok; +END; +$$; diff --git a/docs/sql/30_rpc/user/rpc_admin_user_level_set_visible_v1.sql b/docs/sql/30_rpc/user/rpc_admin_user_level_set_visible_v1.sql new file mode 100644 index 00000000..f111341f --- /dev/null +++ b/docs/sql/30_rpc/user/rpc_admin_user_level_set_visible_v1.sql @@ -0,0 +1,37 @@ +-- ===================================================================================== +-- RPC: rpc_admin_user_level_set_visible +-- 位置:docs/sql/30_rpc/user/ +-- 对象类型:RPC 函数 (SECURITY DEFINER) +-- 版本:v1 +-- 说明:设置用户等级是否展示 +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_set_visible( + p_id UUID, + p_is_visible BOOLEAN +) +RETURNS BOOLEAN +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_ok BOOLEAN; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + UPDATE public.ak_user_levels + SET is_visible = p_is_visible, + updated_at = now() + WHERE id = p_id AND deleted_at IS NULL; + + GET DIAGNOSTICS v_ok = ROW_COUNT; + RETURN v_ok; +END; +$$; diff --git a/pages/mall/admin/user/group.uvue b/pages/mall/admin/user/group.uvue index 3ac2abab..7cce44ac 100644 --- a/pages/mall/admin/user/group.uvue +++ b/pages/mall/admin/user/group.uvue @@ -3,7 +3,12 @@ + + + + + @@ -11,19 +16,29 @@ ID - 分组 + 分组名称 + 备注 + 状态 操作 - + 加载中... + 暂无数据 + {{ group.id }} {{ group.name }} + {{ group.remark || '-' }} + + + + + 修改 | - 删除 + 删除 @@ -31,19 +46,15 @@ - 共 {{ groupList.length }} 条 - - 15条/页 - - + 共 {{ total }} 条 - - 1 - + + {{ page }} + 前往 - + @@ -66,9 +77,26 @@ class="form-input" v-model="formData.name" placeholder="请输入分组名称" - autofocus /> + + + 备注说明: + +