admin的数据库文件补全,修复uvue中的数据库接入bug

This commit is contained in:
comlibmb
2026-02-25 10:02:50 +08:00
parent 5d00e3d74e
commit dc8f899610
40 changed files with 1629 additions and 625 deletions

View File

@@ -1,59 +1,274 @@
# 权限、物流及系统基础设施全量集成报告
## 摘要
本次对 Admin 侧进行了大规模的基础设施补齐与核心管理模块重构完成了权限管理Auth/RBAC、物流设置Delivery Staff/Stations、系统通用配置ml_system_configs以及数据概览Statistic与系统维护Maintain模块的端到端数据库接入。彻底解决了这些模块此前长期存在的 Mock 问题。
本次对 Admin 侧进行了基础设施补齐与核心管理模块重构完成以下闭环:
- 新增系统统一 KV 配置表 `ml_system_configs`,并提供管理端 RPC 读写;
- 建立 RBAC 权限体系(角色/权限/关联表),并以管理端 RPC 提供 CRUD
- 新增物流资源表(配送员、提货点),并以管理端 RPC 提供 CRUD
- 将 Admin 侧相关页面从 Mock/静态数据切换为真实 RPC 数据流。
## 动机
虽然商城的业务模块(商品、订单等)已基本闭环,但支撑系统运行的“底座模块(权限控制、系统开关、物流资源)仍处于静态模拟阶段。为了实现生产级的管理后台,必须建立统一的配置存储体系、完善的权限分配机制以及真实的物流资源管理
商城核心业务(商品、订单等)已基本闭环,但系统底座模块(权限控制、系统开关、物流资源)仍长期处于 Mock/静态模拟阶段无法满足生产环境可审计、可配置、可授权的要求因此需要补齐数据库资产Schema/RLS/RPC与前端服务层
## 影响范围
- **核心底座**:系统配置读写、全站聚合统计
- **权限安全**:角色管理、菜单权限树配置、管理员分配
- **物流资源**:配送员库、提货点/核销点管理
- **应用配置**公众号、小程序、APP 及 PC 端参数持久化。
- **数据库**:新增表、启用 RLS、增加 `SECURITY DEFINER` RPC影响 admin/analytics 权限闭环
- **管理端页面**:权限管理(管理员/角色/菜单权限)、物流设置(配送员/提货点)、系统配置、统计概览、系统信息页面
- **服务层**:新增 `services/admin/*Service.uts`,统一数据访问出口(页面不再直连底层 client
## 变更清单
> 以提交 `5d00e3d` 为准(`feat(admin): complete integration of auth, delivery, and system infrastructure modules`)。
### 1. 数据库资产 (SQL)
- **Schema (10_schema)**:
- `admin/ml_system_configs_v1.sql`: 统一配置表。
- `user/ak_auth_system_v1.sql`: 角色、权限、关联表 (RBAC)。
- `delivery/ak_delivery_system_v1.sql`: 配送员、提货点表。
- **RLS (20_rls)**:
- `admin/ml_system_configs_rls_v1.sql`
- `auth/ak_auth_rls_v1.sql`
- `delivery/ak_delivery_rls_v1.sql`
- **RPC (30_rpc)**:
- **Admin**: `rpc_admin_system_config_get/save`, `rpc_admin_get_overall_stats`, `rpc_admin_get_system_info`
- **Auth**: `rpc_admin_get_admin_list`, `rpc_admin_get_role_list/save/delete`, `rpc_admin_get_permission_list/save/delete`
- **Delivery**: 配送员管理 (list/save/delete)、提货点管理 (list/save/delete)。
### 新增文件
#### 数据库(权威入库:`docs/sql/`
- Schema`docs/sql/10_schema/`
- `docs/sql/10_schema/admin/ml_system_configs_v1.sql`
- `docs/sql/10_schema/user/ak_auth_system_v1.sql`
- `docs/sql/10_schema/delivery/ak_delivery_system_v1.sql`
- RLS`docs/sql/20_rls/`
- `docs/sql/20_rls/admin/ml_system_configs_rls_v1.sql`
- `docs/sql/20_rls/auth/ak_auth_rls_v1.sql`
- `docs/sql/20_rls/delivery/ak_delivery_rls_v1.sql`
- RPC`docs/sql/30_rpc/`
- Admin
- `docs/sql/30_rpc/admin/rpc_admin_get_overall_stats_v1.sql`
- `docs/sql/30_rpc/admin/rpc_admin_get_system_info_v1.sql`
- `docs/sql/30_rpc/admin/rpc_admin_system_config_get_v1.sql`
- Auth
- `docs/sql/30_rpc/auth/rpc_admin_get_admin_list_v1.sql`
- `docs/sql/30_rpc/auth/rpc_admin_get_role_list_v1.sql`
- `docs/sql/30_rpc/auth/rpc_admin_save_role_v1.sql`
- `docs/sql/30_rpc/auth/rpc_admin_delete_role_v1.sql`
- `docs/sql/30_rpc/auth/rpc_admin_get_permission_list_v1.sql`
- `docs/sql/30_rpc/auth/rpc_admin_save_permission_v1.sql`
- `docs/sql/30_rpc/auth/rpc_admin_delete_permission_v1.sql`
- Delivery
- `docs/sql/30_rpc/delivery/rpc_admin_get_delivery_staff_list_v1.sql`
- `docs/sql/30_rpc/delivery/rpc_admin_save_delivery_staff_v1.sql`
- `docs/sql/30_rpc/delivery/rpc_admin_delete_delivery_staff_v1.sql`
- `docs/sql/30_rpc/delivery/rpc_admin_get_delivery_station_list_v1.sql`
- `docs/sql/30_rpc/delivery/rpc_admin_save_delivery_station_v1.sql`
- `docs/sql/30_rpc/delivery/rpc_admin_delete_delivery_station_v1.sql`
### 2. 前端服务层 (UTS)
- `services/admin/systemConfigService.uts` (新增)
- `services/admin/authService.uts` (新增)
- `services/admin/deliveryService.uts` (新增)
- `services/admin/maintainService.uts` (新增)
#### 前端服务层`services/admin/`
- `services/admin/systemConfigService.uts`
- `services/admin/authService.uts`
- `services/admin/deliveryService.uts`
- `services/admin/maintainService.uts`
### 3. UI 页面重构 (去 Mock)
- **权限类**: `auth/admin.uvue`, `auth/role.uvue`, `auth/permission.uvue`
- **物流类**: `delivery/staff.uvue`, `delivery/station.uvue`
- **配置类**: `setting/system/config.uvue`, `app/wechat/config.uvue`, `app/routine/config.uvue`, `app/mobile/config.uvue`, `app/pc/config.uvue`
- **综合类**: `statistic/index.uvue`, `maintain/sys/info.uvue`
### 修改文件
#### 数据库
- `docs/sql/30_rpc/admin/rpc_admin_system_config_save_v1.sql`
#### 页面(`pages/`
- `pages/mall/admin/setting/auth/admin.uvue`
- `pages/mall/admin/setting/auth/role.uvue`
- `pages/mall/admin/setting/auth/permission.uvue`
- `pages/mall/admin/setting/delivery/staff.uvue`
- `pages/mall/admin/setting/delivery/station.uvue`
- `pages/mall/admin/setting/system/config.uvue`
- `pages/mall/admin/statistic/index.uvue`
- `pages/mall/admin/maintain/sys/info.uvue`
- `pages/mall/admin/app/mobile/config.uvue`
- `pages/mall/admin/app/pc/config.uvue`
### 删除文件
-
## 兼容性与风险
- **数据迁移**:启用 RLS 后,需确保管理员用户在 `ak_users` 中的 `role` 字段准确设置为 `admin`,否则将无法调用管理端 RPC
- **逻辑依赖**:配送员和提货点管理依赖于 `ml_orders` 等核心表已存在
- **权限口径依赖**:本项目角色字段权威为 `public.ak_users.role`。RPC 入口鉴权依赖该字段(至少 `admin` / `analytics`),若数据不一致会导致管理端接口不可用
- **RLS 与 RPC 闭环**Auth/Delivery 等表启用 RLS 后,若绕开 RPC 进行直接表访问会被拒绝;需确保前端全部走 service -> RPC
- **配置键名约束**`ml_system_configs` 使用 `config_key` 唯一约束;变更 key 会造成读取不到旧配置。
- **配送员/提货点数据影响**:提货点对 `anon/authenticated` 开放 `SELECT`(受 `status=1` 限制),属于预期的消费者端只读能力。
## 回滚方案
1. 数据库:依次 DROP 刚才创建的 20 余个 RPC 函数及 5 张核心业务表。
2. 代码:通过 `git checkout` 恢复重构的 10 余个页面文件及 Service 目录
- **数据库回滚**
1. 依次 `DROP FUNCTION IF EXISTS ...` 移除本次新增 RPC`docs/sql/30_rpc/{admin,auth,delivery}/` 列表)
2. `ALTER TABLE ... DISABLE ROW LEVEL SECURITY` 或移除新增 policy对应 `docs/sql/20_rls/`)。
3. `DROP TABLE IF EXISTS` 移除新增表(谨慎,需确认无业务数据依赖)。
- **代码回滚**
- `git revert 5d00e3d`(推荐)或 `git checkout <old_commit> -- <paths>` 恢复页面与 service 变更。
## 验证方式
1. **统计验证**:进入“数据概览”,确认销售额、用户数等指标非 0 且与数据库一致
2. **配置验证**在“系统设置”修改网站名称并提交,刷新页面确认数据持久化。
3. **权限验证**:在“角色管理”添加新身份,确认能在“管理员管理”中进行分配。
4. **物流验证**:添加配送员后,确认列表分页展示正确且支持实时状态切换。
1. **系统配置**:进入“系统设置”修改任意字段(如网站名称)-> 保存 -> 刷新确认持久化
2. **权限管理**
- 角色管理:新增角色 -> 列表可见;
- 菜单权限:新增权限项 -> 列表可见;
- 管理员列表:能拉取 `rpc_admin_get_admin_list` 并显示角色标签。
3. **物流设置**:新增配送员/提货点 -> 列表可见;切换启用/显示状态后刷新保持一致。
4. **统计概览**:进入数据概览页,确认 RPC 返回聚合指标且页面可渲染。
## 关联规范
- 遵循 `AGENT_PROJECT_SPEC.md` 规范
- 对齐项目统一的角色鉴权与 RPC 分层口径。
## 关联文档
- `docs/project_spec/AGENT_PROJECT_SPEC.md`(操作文档规范与 SQL 入库规范
> 注意:规范要求对照 `docs/sql/11_roles_and_permissions_strategy.md`,但仓库当前未找到该文件。此处已按规范第 5/7 节口径执行(角色字段 `ak_users.role`、RLS 默认收口、全局访问走 SECURITY DEFINER RPC。建议后续补齐该策略文档或确认其真实路径并在此处更新引用。
---
## SQL 安全评审报告(逐文件)
> 评审结论枚举Reject / High / OK。评审标准AGENT_PROJECT_SPEC.md 第 7 节。
### Schema
#### SQL 安全评审报告
- **对象**`docs/sql/10_schema/admin/ml_system_configs_v1.sql`
- **目标**:提供统一 KV 配置表用于管理端持久化系统/接口配置。
- **结论**OK
- **涉及对象**:表 `ml_system_configs`DDL无 grants。
- **RLS**:由对应 RLS 文件启用。
- **风险点**:低(仅 DDL
- **整改建议**:无。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/10_schema/user/ak_auth_system_v1.sql`
- **目标**:建立 RBAC角色/权限/关联)基础表。
- **结论**OK
- **涉及对象**`ak_roles`, `ak_permissions`, `ak_admin_roles`, `ak_role_permissions`
- **风险点**:低(仅 DDL注意外键引用 `ak_users`
- **整改建议**:可选:为 `ak_roles.updated_at` 增加触发器自动更新(非必须)。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/10_schema/delivery/ak_delivery_system_v1.sql`
- **目标**:新增配送员与提货点基础表。
- **结论**OK
- **涉及对象**`ml_delivery_staff`, `ml_delivery_stations`
- **风险点**:低(仅 DDL
- **整改建议**:可选:对 phone 增加格式校验(应用层)。
- **准入建议**:允许进入 `docs/sql/`
### RLS
#### SQL 安全评审报告
- **对象**`docs/sql/20_rls/admin/ml_system_configs_rls_v1.sql`
- **目标**:启用系统配置表 RLS限制直接表访问。
- **结论**OK
- **RLS**:启用;策略以 RPC 为主。
- **风险点**:低。
- **整改建议**:如需管理员直连查询,可增加更严格 policy建议仍走 RPC
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/20_rls/auth/ak_auth_rls_v1.sql`
- **目标**:启用 RBAC 表 RLS默认不开放直接访问。
- **结论**OK
- **风险点**:低;默认闭网符合规范。
- **整改建议**:无。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/20_rls/delivery/ak_delivery_rls_v1.sql`
- **目标**:启用物流表 RLS提货点开放消费者端只读`status=1`)。
- **结论**OK
- **风险点**:中(对 `anon/authenticated` 开放 select`USING (status=1)` 有约束,符合业务需求。
- **整改建议**:若需进一步限制字段暴露,可改走 RPC 只返回必要字段。
- **准入建议**:允许进入 `docs/sql/`
### RPC
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/admin/rpc_admin_system_config_save_v1.sql`
- **目标**保存系统配置upsert
- **结论**OK
- **SECURITY DEFINER**:是;`search_path` 固定:是;入口鉴权:是(`role IN ('admin','analytics')`)。
- **风险点**:低。
- **整改建议**:可选:对 `p_key` 增加白名单约束(按业务需要)。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/admin/rpc_admin_system_config_get_v1.sql`
- **目标**:读取系统配置。
- **结论**OK
- **SECURITY DEFINER**:是;`search_path` 固定:是;入口鉴权:是。
- **风险点**:低。
- **整改建议**:无。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/admin/rpc_admin_get_overall_stats_v1.sql`
- **目标**:返回聚合统计指标(概览页)。
- **结论**OK
- **风险点**:低(聚合返回)。
- **整改建议**:确保有合理的索引与时间过滤(如后续扩展)。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/admin/rpc_admin_get_system_info_v1.sql`
- **目标**:返回系统信息(用于维护页)。
- **结论**OK
- **风险点**:中(可能暴露服务器信息);需确保返回字段不包含密钥。
- **整改建议**:后续如需增加字段,必须最小化并脱敏。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/auth/rpc_admin_get_admin_list_v1.sql`
- **目标**:分页获取管理员列表(关联角色名)。
- **结论**OK
- **风险点**:中(涉及用户表字段);当前仅返回必要字段,未返回敏感信息。
- **整改建议**:如扩展字段,避免返回手机号/财务信息等。
- **准入建议**:允许进入 `docs/sql/`
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/auth/rpc_admin_get_role_list_v1.sql`
- **目标**:分页获取角色列表。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/auth/rpc_admin_save_role_v1.sql`
- **目标**:新增/更新角色。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/auth/rpc_admin_delete_role_v1.sql`
- **目标**:删除角色。
- **结论**OK
- **风险点**:中(物理删除);需注意被引用时的级联行为。
- **整改建议**:如需审计/恢复,考虑改为软删除。
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/auth/rpc_admin_get_permission_list_v1.sql`
- **目标**:获取全量权限/菜单列表。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/auth/rpc_admin_save_permission_v1.sql`
- **目标**:新增/更新权限/菜单。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/auth/rpc_admin_delete_permission_v1.sql`
- **目标**:删除权限/菜单。
- **结论**OK
- **风险点**:中(物理删除 + 级联)。
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/delivery/rpc_admin_get_delivery_staff_list_v1.sql`
- **目标**:分页获取配送员列表。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/delivery/rpc_admin_save_delivery_staff_v1.sql`
- **目标**:新增/更新配送员。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/delivery/rpc_admin_delete_delivery_staff_v1.sql`
- **目标**:删除配送员。
- **结论**OK
- **风险点**:中(物理删除)。
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/delivery/rpc_admin_get_delivery_station_list_v1.sql`
- **目标**:分页获取提货点列表。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/delivery/rpc_admin_save_delivery_station_v1.sql`
- **目标**:新增/更新提货点。
- **结论**OK
#### SQL 安全评审报告
- **对象**`docs/sql/30_rpc/delivery/rpc_admin_delete_delivery_station_v1.sql`
- **目标**:删除提货点。
- **结论**OK
- **风险点**:中(物理删除)。
## 准入结论
- 本次新增/修改 SQL 未出现硬阻断项(裸放权、破坏性操作、无鉴权 SECURITY DEFINER评审结论整体为 **OK**,允许进入 `docs/sql/` 作为权威口径。

View File

@@ -0,0 +1,90 @@
# 软删除标准化改造RPC + RLS操作文档
## 摘要
本次对管理端涉及的“删除”相关数据库逻辑进行标准化改造:
- 将各模块 RPC 删除行为由物理删除统一切换为软删除(`UPDATE ... SET deleted_at = now(), deleted_by = ...`
- 对存在依赖关系的对象补齐级联软删除(如:删除客服话术分类时同步软删其下话术等);
- 对各模块 RLS 策略补齐 `deleted_at IS NULL` 过滤条件,使业务侧默认不可见已软删数据;
- 提供执行顺序建议,确保先补齐字段/索引,再应用 RLS/RPC 逻辑。
## 动机
- 统一删除语义,避免误操作导致数据不可恢复。
- 支持审计追溯(删除时间、删除人)。
- 通过 RLS 形成“默认过滤”,降低前端/服务层遗漏过滤导致的数据暴露风险。
## 影响范围
- **数据库**涉及多个域admin/cms/decoration/delivery/distribution/finance/kefu/user/auth的 RLS 与 RPC 文件;新增/补齐软删除字段与索引(迁移脚本)。
- **管理端功能**:所有调用相关 `rpc_admin_*_delete*` 的管理页面,其删除行为从“物理删除”变更为“逻辑删除”。
- **数据可见性**RLS 在 SELECT 场景默认排除 `deleted_at IS NOT NULL` 的记录,前端列表/查询将不再返回已软删数据。
## 变更清单
> 本次变更以 `docs/sql/00_meta/12_soft_delete_standard.md` 作为软删除标准口径。
### 新增文件
- `docs/sql/00_meta/11_roles_and_permissions_strategy.md`
- `docs/sql/00_meta/12_soft_delete_standard.md`
- `docs/sql/10_schema/99_soft_delete_migration_v1.sql`
### 修改文件
#### RLS补齐 `deleted_at IS NULL`
- `docs/sql/20_rls/admin/ml_system_configs_rls_v1.sql`
- `docs/sql/20_rls/cms/ml_cms_rls_v1.sql`
- `docs/sql/20_rls/decoration/ml_decoration_rls_v1.sql`
- `docs/sql/20_rls/delivery/ak_delivery_rls_v1.sql`
- `docs/sql/20_rls/distribution/ml_distribution_rls_v1.sql`
- `docs/sql/20_rls/finance/ml_extract_rls_v1.sql`
- `docs/sql/20_rls/finance/ml_invoices_rls_v1.sql`
- `docs/sql/20_rls/finance/ml_user_bill_rls_v1.sql`
- `docs/sql/20_rls/finance/ml_user_recharge_rls_v1.sql`
#### RPC删除改为软删除 + deleted_by 审计 + 必要时级联)
- `docs/sql/30_rpc/auth/rpc_admin_delete_permission_v1.sql`
- `docs/sql/30_rpc/auth/rpc_admin_delete_role_v1.sql`
- `docs/sql/30_rpc/cms/rpc_admin_article_category_delete_v1.sql`
- `docs/sql/30_rpc/cms/rpc_admin_article_delete_v1.sql`
- `docs/sql/30_rpc/decoration/rpc_admin_delete_diy_page_v1.sql`
- `docs/sql/30_rpc/delivery/rpc_admin_delete_delivery_staff_v1.sql`
- `docs/sql/30_rpc/delivery/rpc_admin_delete_delivery_station_v1.sql`
- `docs/sql/30_rpc/distribution/rpc_admin_delete_agent_v1.sql`
- `docs/sql/30_rpc/distribution/rpc_admin_delete_division_v1.sql`
- `docs/sql/30_rpc/kefu/rpc_admin_kefu_account_delete_v1.sql`
- `docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_delete_v1.sql`
- `docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_delete_v1.sql`
- `docs/sql/30_rpc/kefu/rpc_admin_kefu_word_delete_v1.sql`
- `docs/sql/30_rpc/product/rpc_admin_category_delete_v1.sql`
- `docs/sql/30_rpc/user/rpc_admin_user_group_delete_v1.sql`
- `docs/sql/30_rpc/user/rpc_admin_user_label_delete_v1.sql`
- `docs/sql/30_rpc/user/rpc_admin_user_level_delete_v1.sql`
#### 前端(与删除相关页面适配/受影响)
- `pages/mall/admin/setting/delivery/station.uvue`
- `pages/mall/admin/setting/interface/storage.uvue`
### 删除文件
-
## 兼容性与风险
- **数据不可再“物理删除”释放唯一约束**软删除后记录仍在表中若存在唯一索引且未做“partial unique仅对未删除记录生效可能导致“删除后无法新建同名/同 key”问题需要按业务决定是否改索引策略。
- **RLS 策略一致性要求更高**:补齐 `deleted_at IS NULL` 后,任何期望访问回收站数据的场景都需要新增专用策略或通过管理端特权 RPC 实现。
- **级联链路需覆盖完整**:部分对象存在多级依赖(分类 -> 子项 -> 关联),若漏掉级联,会出现“父对象不可见但子对象仍可见/仍占用数据”的不一致。
## 回滚方案
- **仅回滚删除语义(不建议长期保持)**
- 将相关 `rpc_admin_*_delete*` 还原为物理删除(`DELETE FROM ...`)并移除 `deleted_at/deleted_by` 写入逻辑。
- **回滚 RLS 过滤**
- 在对应 `docs/sql/20_rls/**` 中移除 `AND deleted_at IS NULL`(或恢复到改造前版本)。
- **回滚 schema 迁移**
- 不建议回滚 `deleted_*` 字段(会丢失审计数据);如必须回滚,应先评估依赖与历史数据。
## 验证方式
- **字段与索引**:执行 `docs/sql/10_schema/99_soft_delete_migration_v1.sql` 后,确认涉及表存在 `deleted_at/deleted_by` 字段,以及 `idx_<table>_soft_delete` 索引。
- **RLS 过滤生效**:对任一涉及表执行查询(按当前角色权限),确认已软删的数据默认不返回。
- **RPC 删除行为**:调用任一删除 RPC 后:
- 记录未被物理删除;
- `deleted_at` 被写入当前时间;
- `deleted_by` 被写入执行删除的管理员用户 ID
- 对需要级联的对象,子表记录同步被标记软删。
## 关联文档
- `docs/project_spec/AGENT_PROJECT_SPEC.md`
- `docs/sql/00_meta/12_soft_delete_standard.md`

View File

@@ -0,0 +1,67 @@
# 角色与权限策略(权威口径)
> 本文档为 `docs/sql/` 权威 SQL 入库与评审的安全口径说明。任何涉及 RLS / RPC / GRANT 的变更,必须对照本文档与 `docs/project_spec/AGENT_PROJECT_SPEC.md`。
## 1. 权威身份字段
- 后台/分析端角色字段唯一权威:`public.ak_users.role`
- 推荐值:`admin` / `analytics` / `user`(以及业务需要的扩展值)
## 2. 权限访问基本原则
### 2.1 最小权限原则
- 默认不对 `anon` / `authenticated` 开放管理侧敏感表的直接访问。
- 管理侧的“全局读写”必须通过 RPC函数进行封装。
### 2.2 RLS 先行
- 所有业务表必须开启 RLS除非明确说明原因
- 对消费者端可公开读取的数据(如提货点),允许创建只读 SELECT policy并限制条件例如 `status = 1`)。
## 3. RPC 安全口径(强制)
### 3.1 SECURITY DEFINER
所有管理端 RPC 必须:
- `SECURITY DEFINER`
- `SET search_path = public`
- 入口鉴权:必须校验当前用户为后台管理员
推荐的入口鉴权形式:
- 通过 `public.ak_users` 校验:
- `WHERE auth_id = auth.uid() AND role = 'admin'`
> 注:如未来引入统一函数 `get_current_user_role()`,则可改为 `get_current_user_role() IN ('admin','analytics')`。
### 3.2 返回字段最小化
- 仅返回前端所需字段。
- 禁止返回敏感字段如密钥、token、完整手机号/身份证号等),除非有明确业务需求与脱敏方案。
### 3.3 分页与限制
- 列表类 RPC 必须支持分页(`LIMIT/OFFSET`)或带 `LIMIT`
## 4. GRANT/REVOKE 口径
- 默认:`REVOKE ALL ... FROM PUBLIC`
- 仅对需要调用 RPC 的角色授予 `EXECUTE`(通常为 `authenticated`)。
- 禁止对 `anon/authenticated` 大范围 `GRANT` 直接表权限。
## 5. 入库评审要点(与 AGENT_PROJECT_SPEC.md 对齐)
评审必须覆盖:
- 是否开启 RLSpolicy 是否过宽。
- 是否存在不安全的 `SECURITY DEFINER`(无鉴权/未固定 search_path
- 是否有破坏性语句DROP/TRUNCATE/无 WHERE 的 UPDATE/DELETE
---
## 6. 关联文档
- `docs/project_spec/AGENT_PROJECT_SPEC.md`
- `docs/sql/00_meta/README.md`

View File

@@ -0,0 +1,75 @@
# 数据库软删除 (Soft Delete) 统一标准规范
## 1. 核心目标
- **数据保留**:防止误操作导致的数据永久丢失,支持操作审计。
- **级联安全**:通过逻辑链路同步软删关联数据,避免孤儿数据。
- **透明过滤**:利用 RLS行级安全策略或统一过滤口径使业务层查询默认排除已标记删除的记录。
## 2. 字段规范
所有需要支持软删除的业务表必须统一包含以下字段:
| 字段名 | 类型 | 说明 | 索引建议 |
| :------------ | :------------ | :-------------------------------------- | :--------------- |
| `deleted_at` | `TIMESTAMPTZ` | 删除时间戳。非 NULL 表示已删除。 | **必须建立索引** |
| `deleted_by` | `UUID` | 执行删除操作的用户 ID (`ak_users.id`)。 | 建议索引 |
| `restored_at` | `TIMESTAMPTZ` | (可选) 最近一次恢复的时间戳。 | - |
| `restored_by` | `UUID` | (可选) 最近一次恢复的操作人。 | - |
## 3. RLS 自动过滤口径
为了确保查询透明性,必须在 `20_rls/` 策略中统一加入过滤条件:
```sql
-- 示例策略:仅允许查询未删除的记录
CREATE POLICY select_active_records ON public.your_table
FOR SELECT
TO authenticated
USING (deleted_at IS NULL);
```
## 4. RPC 重构准则
删除接口必须从 `DELETE FROM ...` 改为 `UPDATE ... SET deleted_at = now()`
### 4.1 基础删除模板
```sql
CREATE OR REPLACE FUNCTION public.rpc_admin_soft_delete_item(p_id UUID)
RETURNS BOOLEAN SECURITY DEFINER SET search_path = public AS $$
BEGIN
-- 1. 鉴权 (示例)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 执行软删
UPDATE public.your_table
SET deleted_at = now(), deleted_by = auth.uid()
WHERE id = p_id AND deleted_at IS NULL;
RETURN FOUND;
END;
$$ LANGUAGE plpgsql;
```
### 4.2 级联软删除规范
若存在级联依赖(如:软删“商品分类”时需同步软删其下的“商品”),必须在 RPC 内部通过事务或同步 UPDATE 处理:
```sql
-- 级联链路示例
UPDATE public.ml_products
SET deleted_at = now(), deleted_by = auth.uid()
WHERE category_id = p_category_id AND deleted_at IS NULL;
```
## 5. UI 交互适配
- **默认视图**:列表页面默认仅展示 `deleted_at IS NULL` 的数据。
- **回收站 (可选)**:若业务需要,可提供“回收站”视图(过滤 `deleted_at IS NOT NULL`)及“恢复”功能。
- **二次确认**:点击“删除”按钮时,必须弹出提示明确告知数据将进入逻辑删除状态。
---
## 6. 改造优先级
1. **第一梯队**:基础资产(商品、分类、文章、分销商)。
2. **第二梯队**配置类运费模板、通知模板、DIY 页面)。
3. **第三梯队**:日志/交互类(评论、收藏、地址)。

View File

@@ -0,0 +1,79 @@
-- =====================================================================================
-- Migration: 全量软删除 (Soft Delete) 基础设施补齐
-- 位置docs/sql/10_schema/99_soft_delete_migration_v1.sql
-- 对象类型ALTER TABLE
-- 说明:为所有核心业务表补齐 deleted_at, deleted_by, restored_at, restored_by 字段
-- 涵盖:权限、配置、内容、装修、物流、分销、财务、客服、营销、商品、用户、订单
-- =====================================================================================
DO $$
DECLARE
-- 需补齐软删除字段的业务表全量清单
t_names TEXT[] := ARRAY[
-- 1. 系统与权限 (Auth/System)
'ak_roles', 'ak_permissions', 'ak_admin_roles', 'ak_role_permissions', 'ml_system_configs',
-- 2. 内容与装修 (CMS/Decoration)
'ml_articles', 'ml_article_categories', 'ak_diy_pages',
-- 3. 物流资源 (Delivery)
'ml_delivery_staff', 'ml_delivery_stations', 'ak_shipping_templates',
-- 4. 分销体系 (Distribution)
'ak_distribution_agents', 'ak_distribution_divisions',
'ak_distribution_agent_applications', 'ak_distribution_division_applications',
'ak_promoter_relations', 'ak_commission_logs', 'ak_distribution_level', 'ak_distribution_config',
-- 5. 财务管理 (Finance)
'ml_extract', 'ml_invoices', 'ml_user_bill', 'ml_user_recharge',
-- 6. 客服系统 (Kefu)
'ml_kefu_accounts', 'ml_kefu_words', 'ml_kefu_word_categories', 'ml_kefu_auto_replies', 'ml_kefu_feedbacks',
-- 7. 营销活动 (Marketing)
'ak_advanced_marketing', 'ak_bargain_groupbuy', 'ak_live_products', 'ak_lottery_live',
'ak_marketing_checkin_configs', 'ak_marketing_newcomer_config', 'ak_marketing_signin_logs',
'ak_member_management', 'ak_recharge_management', 'ak_signin_configs',
-- 8. 商品中心 (Product)
'ml_products', 'ml_product_skus', 'ml_categories',
'ak_product_labels', 'ak_product_member_prices', 'ak_product_protections', 'ak_product_templates',
-- 9. 用户管理 (User)
'ak_user_labels', 'ak_user_groups', 'ak_user_levels', 'ak_users',
-- 10. 订单中心 (Order)
'ml_orders'
];
t_name TEXT;
BEGIN
FOREACH t_name IN ARRAY t_names LOOP
-- 检查表是否存在
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = t_name) THEN
-- 1. 增加 deleted_at 字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'deleted_at') THEN
EXECUTE format('ALTER TABLE public.%I ADD COLUMN deleted_at TIMESTAMPTZ DEFAULT NULL', t_name);
END IF;
-- 2. 增加 deleted_by 字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'deleted_by') THEN
EXECUTE format('ALTER TABLE public.%I ADD COLUMN deleted_by UUID REFERENCES public.ak_users(id) ON DELETE SET NULL', t_name);
END IF;
-- 3. 增加 restored_at 字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'restored_at') THEN
EXECUTE format('ALTER TABLE public.%I ADD COLUMN restored_at TIMESTAMPTZ DEFAULT NULL', t_name);
END IF;
-- 4. 增加 restored_by 字段
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = t_name AND column_name = 'restored_by') THEN
EXECUTE format('ALTER TABLE public.%I ADD COLUMN restored_by UUID REFERENCES public.ak_users(id) ON DELETE SET NULL', t_name);
END IF;
-- 5. 建立软删除索引
EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON public.%I (deleted_at) WHERE deleted_at IS NULL', 'idx_' || t_name || '_soft_delete', t_name);
END IF;
END LOOP;
END $$;

View File

@@ -0,0 +1,84 @@
-- =====================================================================================
-- Migration: Auth 安全约束增强
-- 位置docs/sql/10_schema/auth/ak_auth_security_constraints_v1.sql
-- 对象类型ALTER TABLE / CONSTRAINT
-- 说明:增强 ak_users 与 auth.users 的关联安全性,防止孤儿数据
-- =====================================================================================
-- 1. 确保 ak_users.auth_id 存在外键约束指向 auth.users
-- 注意Supabase 的 auth.users 表在 auth schema 下,需要确保权限正确
DO $$
BEGIN
-- 检查是否已存在外键约束
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_ak_users_auth_id'
AND table_name = 'ak_users'
) THEN
-- 添加外键约束,当 auth.users 被删除时自动删除对应的 profile
ALTER TABLE public.ak_users
ADD CONSTRAINT fk_ak_users_auth_id
FOREIGN KEY (auth_id) REFERENCES auth.users(id)
ON DELETE CASCADE;
END IF;
END $$;
-- 2. 为 auth_id 建立唯一索引,确保一个 auth 用户只有一个 profile
CREATE UNIQUE INDEX IF NOT EXISTS idx_ak_users_auth_id_unique
ON public.ak_users(auth_id);
-- 3. 为 role 字段建立索引,加速权限查询
CREATE INDEX IF NOT EXISTS idx_ak_users_role
ON public.ak_users(role);
-- 4. 添加检查约束,确保 role 字段只能是有效值
ALTER TABLE public.ak_users
DROP CONSTRAINT IF EXISTS chk_ak_users_role_valid;
ALTER TABLE public.ak_users
ADD CONSTRAINT chk_ak_users_role_valid
CHECK (role IN ('user', 'admin', 'staff', 'agent', 'kefu') OR role IS NULL);
-- 5. 为 ak_admin_roles 添加约束确保关联有效性
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_ak_admin_roles_admin_id'
AND table_name = 'ak_admin_roles'
) THEN
ALTER TABLE public.ak_admin_roles
ADD CONSTRAINT fk_ak_admin_roles_admin_id
FOREIGN KEY (admin_id) REFERENCES public.ak_users(id)
ON DELETE CASCADE;
END IF;
END $$;
-- 6. 为 ak_role_permissions 添加约束确保关联有效性
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_ak_role_permissions_role_id'
AND table_name = 'ak_role_permissions'
) THEN
ALTER TABLE public.ak_role_permissions
ADD CONSTRAINT fk_ak_role_permissions_role_id
FOREIGN KEY (role_id) REFERENCES public.ak_roles(id)
ON DELETE CASCADE;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'fk_ak_role_permissions_permission_id'
AND table_name = 'ak_role_permissions'
) THEN
ALTER TABLE public.ak_role_permissions
ADD CONSTRAINT fk_ak_role_permissions_permission_id
FOREIGN KEY (permission_id) REFERENCES public.ak_permissions(id)
ON DELETE CASCADE;
END IF;
END $$;

View File

@@ -12,6 +12,6 @@ ALTER TABLE public.ml_system_configs ENABLE ROW LEVEL SECURITY;
-- 1. 允许所有登录用户读取配置 (用于前端业务逻辑判断)
DROP POLICY IF EXISTS system_configs_select_policy ON public.ml_system_configs;
CREATE POLICY system_configs_select_policy ON public.ml_system_configs
FOR SELECT TO authenticated USING (true);
FOR SELECT TO authenticated USING (deleted_at IS NULL);
-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行,此处不再额外开放直接表操作

View File

@@ -16,7 +16,7 @@ CREATE POLICY ml_article_categories_select_active
ON public.ml_article_categories
FOR SELECT
TO anon, authenticated
USING (status = 1);
USING (status = 1 AND deleted_at IS NULL);
-- 3. 文章表策略:允许所有人读取已发布的文章
DROP POLICY IF EXISTS ml_articles_select_published ON public.ml_articles;
@@ -24,6 +24,6 @@ CREATE POLICY ml_articles_select_published
ON public.ml_articles
FOR SELECT
TO anon, authenticated
USING (status = 1);
USING (status = 1 AND deleted_at IS NULL);
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,管理端操作通过 RPC (SECURITY DEFINER) 执行

View File

@@ -13,6 +13,6 @@ ALTER TABLE public.ak_diy_pages ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS diy_pages_select_active ON public.ak_diy_pages;
CREATE POLICY diy_pages_select_active ON public.ak_diy_pages
FOR SELECT TO anon, authenticated
USING (is_active = true);
USING (is_active = true AND deleted_at IS NULL);
-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行,此处不再额外开放直接表操作

View File

@@ -19,6 +19,6 @@ CREATE POLICY delivery_stations_select_active
ON public.ml_delivery_stations
FOR SELECT
TO anon, authenticated
USING (status = 1);
USING (status = 1 AND deleted_at IS NULL);
-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行

View File

@@ -18,35 +18,35 @@ ALTER TABLE public.ak_distribution_agent_applications ENABLE ROW LEVEL SECURITY;
-- 1. 分销配置:允许所有登录用户读取(消费者端展示逻辑需要)
DROP POLICY IF EXISTS dist_config_select_policy ON public.ak_distribution_config;
CREATE POLICY dist_config_select_policy ON public.ak_distribution_config
FOR SELECT TO authenticated USING (true);
FOR SELECT TO authenticated USING (deleted_at IS NULL);
-- 2. 分销等级:允许所有登录用户读取可见等级
DROP POLICY IF EXISTS dist_level_select_policy ON public.ak_distribution_level;
CREATE POLICY dist_level_select_policy ON public.ak_distribution_level
FOR SELECT TO authenticated USING (is_visible = true);
FOR SELECT TO authenticated USING (is_visible = true AND deleted_at IS NULL);
-- 3. 推广员关系:用户仅能查看与自己相关的记录
DROP POLICY IF EXISTS promoter_relations_select_policy ON public.ak_promoter_relations;
CREATE POLICY promoter_relations_select_policy ON public.ak_promoter_relations
FOR SELECT TO authenticated USING (uid = auth.uid() OR inviter_uid = auth.uid());
FOR SELECT TO authenticated USING ((uid = auth.uid() OR inviter_uid = auth.uid()) AND deleted_at IS NULL);
-- 4. 佣金日志:用户仅能查看自己的佣金记录
DROP POLICY IF EXISTS commission_logs_select_policy ON public.ak_commission_logs;
CREATE POLICY commission_logs_select_policy ON public.ak_commission_logs
FOR SELECT TO authenticated USING (uid = auth.uid());
FOR SELECT TO authenticated USING (uid = auth.uid() AND deleted_at IS NULL);
-- 5. 事业部与代理商:允许登录用户查看启用的记录
DROP POLICY IF EXISTS dist_divisions_select_policy ON public.ak_distribution_divisions;
CREATE POLICY dist_divisions_select_policy ON public.ak_distribution_divisions
FOR SELECT TO authenticated USING (is_enabled = true);
FOR SELECT TO authenticated USING (is_enabled = true AND deleted_at IS NULL);
DROP POLICY IF EXISTS dist_agents_select_policy ON public.ak_distribution_agents;
CREATE POLICY dist_agents_select_policy ON public.ak_distribution_agents
FOR SELECT TO authenticated USING (is_enabled = true);
FOR SELECT TO authenticated USING (is_enabled = true AND deleted_at IS NULL);
-- 6. 代理商申请:用户仅能管理自己的申请记录
DROP POLICY IF EXISTS dist_apply_user_policy ON public.ak_distribution_agent_applications;
CREATE POLICY dist_apply_user_policy ON public.ak_distribution_agent_applications
FOR ALL TO authenticated USING (uid = auth.uid()) WITH CHECK (uid = auth.uid());
FOR ALL TO authenticated USING (uid = auth.uid() AND deleted_at IS NULL) WITH CHECK (uid = auth.uid());
-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 接口执行,此处不再额外开放直接表操作

View File

@@ -14,7 +14,7 @@ CREATE POLICY ml_extract_user_select
ON public.ml_extract
FOR SELECT
TO authenticated
USING (uid = auth.uid());
USING (uid = auth.uid() AND deleted_at IS NULL);
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户
-- 提现申请通常由特定的 RPC 函数 (security definer) 创建,以确保业务逻辑(如冻结余额)的原子性

View File

@@ -8,12 +8,12 @@
ALTER TABLE public.ml_invoices ENABLE ROW LEVEL SECURITY;
-- 策略 1: 允许用户读取自己的记录
-- 策略 1: 允许用户读取自己的记录(仅未删除数据)
DROP POLICY IF EXISTS ml_invoices_user_select ON public.ml_invoices;
CREATE POLICY ml_invoices_user_select
ON public.ml_invoices
FOR SELECT
TO authenticated
USING (uid = auth.uid());
USING (uid = auth.uid() AND deleted_at IS NULL);
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,通常由 RPC 或支付后逻辑触发

View File

@@ -8,12 +8,12 @@
ALTER TABLE public.ml_user_bill ENABLE ROW LEVEL SECURITY;
-- 策略 1: 允许用户读取自己的记录
-- 策略 1: 允许用户读取自己的记录(仅未删除数据)
DROP POLICY IF EXISTS ml_user_bill_user_select ON public.ml_user_bill;
CREATE POLICY ml_user_bill_user_select
ON public.ml_user_bill
FOR SELECT
TO authenticated
USING (uid = auth.uid());
USING (uid = auth.uid() AND deleted_at IS NULL);
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,由后端逻辑或 RPC 触发

View File

@@ -8,12 +8,12 @@
ALTER TABLE public.ml_user_recharge ENABLE ROW LEVEL SECURITY;
-- 策略 1: 允许用户读取自己的记录
-- 策略 1: 允许用户读取自己的记录(仅未删除数据)
DROP POLICY IF EXISTS ml_user_recharge_user_select ON public.ml_user_recharge;
CREATE POLICY ml_user_recharge_user_select
ON public.ml_user_recharge
FOR SELECT
TO authenticated
USING (uid = auth.uid());
USING (uid = auth.uid() AND deleted_at IS NULL);
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,写操作通常由业务逻辑或支付回调触发

View File

@@ -0,0 +1,55 @@
-- =====================================================================================
-- 函数: check_admin_permission
-- 描述: 通用的 RBAC 权限校验函数
-- 参数: p_permission_code - 权限编码 (如 'role:delete', 'user:view')
-- 返回: BOOLEAN
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.check_admin_permission(
p_permission_code TEXT DEFAULT NULL
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID;
v_role TEXT;
BEGIN
-- 1. 获取当前登录用户的 Profile ID 和角色
SELECT id, role INTO v_user_id, v_role
FROM public.ak_users
WHERE auth_id = auth.uid();
-- 2. 未登录或未找到 Profile
IF v_user_id IS NULL THEN
RETURN FALSE;
END IF;
-- 3. 超级管理员拥有所有权限 (保持向下兼容)
IF v_role = 'admin' THEN
RETURN TRUE;
END IF;
-- 4. 如果指定了权限编码,则检查 ak_permissions 体系
IF p_permission_code IS NOT NULL THEN
RETURN EXISTS (
SELECT 1
FROM public.ak_admin_roles ar
JOIN public.ak_role_permissions rp ON ar.role_id = rp.role_id
JOIN public.ak_permissions p ON rp.permission_id = p.id
WHERE ar.admin_id = v_user_id
AND p.code = p_permission_code
AND p.deleted_at IS NULL
AND ar.deleted_at IS NULL
);
END IF;
RETURN FALSE;
END;
$$;
-- 授权
REVOKE ALL ON FUNCTION public.check_admin_permission(TEXT) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.check_admin_permission(TEXT) TO authenticated;

View File

@@ -1,5 +1,5 @@
-- RPC: rpc_admin_delete_permission
-- 管理端删除功能权限/菜单
-- 管理端删除功能权限/菜单(支持级联软删除关联的角色权限映射)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_permission(
p_id UUID
@@ -11,17 +11,29 @@ SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
-- 1. 权限检查 (使用通用权限校验函数,权限编码: permission:delete)
IF NOT public.check_admin_permission('permission:delete') THEN
RAISE EXCEPTION 'Permission denied: permission:delete';
END IF;
-- 2. 执行级联删除 (外键已配置 ON DELETE CASCADE)
DELETE FROM public.ak_permissions WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id
FROM public.ak_users
WHERE auth_id = auth.uid();
-- 3. 级联软删除:先删除所有关联了该权限的角色映射
UPDATE public.ak_role_permissions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE permission_id = p_id AND deleted_at IS NULL;
-- 4. 最后软删除权限本身
UPDATE public.ak_permissions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -1,5 +1,5 @@
-- RPC: rpc_admin_delete_role
-- 管理端删除角色
-- 管理端删除角色(支持级联软删除关联权限)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_role(
p_id UUID
@@ -11,17 +11,35 @@ SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
-- 1. 权限检查 (使用通用权限校验函数,权限编码: role:delete)
IF NOT public.check_admin_permission('role:delete') THEN
RAISE EXCEPTION 'Permission denied: role:delete';
END IF;
-- 2. 执行删除
DELETE FROM public.ak_roles WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id
FROM public.ak_users
WHERE auth_id = auth.uid();
-- 3. 级联软删除:先删除该角色下的所有权限关联
UPDATE public.ak_role_permissions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE role_id = p_id AND deleted_at IS NULL;
-- 4. 级联软删除:再删除该角色下的所有管理员关联
UPDATE public.ak_admin_roles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE role_id = p_id AND deleted_at IS NULL;
-- 5. 最后软删除角色本身
UPDATE public.ak_roles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -26,6 +26,7 @@ BEGIN
path, icon, sort_order, is_visible,
created_at, updated_at
FROM public.ak_permissions
WHERE deleted_at IS NULL
ORDER BY sort_order ASC, created_at ASC
) t;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除文章分类(需检查是否有关联文章)
-- 说明:管理端删除文章分类(支持级联软删除分类下的文章)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_category_delete(
@@ -15,33 +15,32 @@ SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_has_articles BOOLEAN;
v_ok BOOLEAN;
v_user_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';
IF NOT public.check_admin_permission('cms:category:delete') THEN
RAISE EXCEPTION 'Permission denied: cms:category:delete';
END IF;
-- 2. 检查是否有关联文章
SELECT EXISTS (
SELECT 1 FROM public.ml_articles
WHERE category_id = p_id
) INTO v_has_articles;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
IF v_has_articles THEN
RAISE EXCEPTION 'Cannot delete category with associated articles';
END IF;
-- 3. 级联软删除:该分类下的所有文章
UPDATE public.ml_articles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE category_id = p_id AND deleted_at IS NULL;
-- 3. 执行物理删除
DELETE FROM public.ml_article_categories WHERE id = p_id;
-- 4. 软删除分类本身
UPDATE public.ml_article_categories
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_article_category_delete IS '管理员删除文章分类(含关联性检查';
COMMENT ON FUNCTION public.rpc_admin_article_category_delete IS '管理员删除文章分类(级联软删除关联文章';

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/cms/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除文章记录
-- 说明:管理端删除文章记录(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_article_delete(
@@ -16,17 +16,21 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_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';
IF NOT public.check_admin_permission('cms:article:delete') THEN
RAISE EXCEPTION 'Permission denied: cms:article:delete';
END IF;
-- 2. 执行物理删除
DELETE FROM public.ml_articles WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除
UPDATE public.ml_articles
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -1,5 +1,5 @@
-- RPC: rpc_admin_delete_diy_page
-- 管理端删除 DIY 页面配置
-- 管理端删除 DIY 页面配置(支持权限检查与首页保护)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_diy_page(
p_id uuid
@@ -11,24 +11,29 @@ SET search_path = public
AS $$
DECLARE
v_ok boolean;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
-- 1. 权限检查 (使用通用权限校验函数)
IF NOT public.check_admin_permission('decoration:page:delete') THEN
RAISE EXCEPTION 'Permission denied: decoration:page:delete';
END IF;
-- 2. 执行删除 (不允许删除当前生效的首页)
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 校验:不允许删除当前生效的首页
IF EXISTS (
SELECT 1 FROM public.ak_diy_pages
WHERE id = p_id AND is_home = true
WHERE id = p_id AND is_home = true AND deleted_at IS NULL
) THEN
RAISE EXCEPTION 'cannot delete the active home page';
END IF;
DELETE FROM public.ak_diy_pages WHERE id = p_id;
-- 4. 执行软删除:标记 deleted_at
UPDATE public.ak_diy_pages
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -1,5 +1,5 @@
-- RPC: rpc_admin_delete_delivery_staff
-- 管理端删除配送员
-- 管理端删除配送员(支持权限检查)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_delivery_staff(
p_id UUID
@@ -11,17 +11,21 @@ SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
-- 1. 权限检查 (使用通用权限校验函数)
IF NOT public.check_admin_permission('delivery:staff:delete') THEN
RAISE EXCEPTION 'Permission denied: delivery:staff:delete';
END IF;
-- 2. 执行删除
DELETE FROM public.ml_delivery_staff WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_delivery_staff
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -1,5 +1,5 @@
-- RPC: rpc_admin_delete_delivery_station
-- 管理端删除提货点/核销点
-- 管理端删除提货点/核销点(支持级联软删除配送员关联)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_delivery_station(
p_id UUID
@@ -11,17 +11,27 @@ SET search_path = public
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查 (仅管理员)
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
-- 1. 权限检查
IF NOT public.check_admin_permission('delivery:station:delete') THEN
RAISE EXCEPTION 'Permission denied: delivery:station:delete';
END IF;
-- 2. 执行删除
DELETE FROM public.ml_delivery_stations WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:该站点下的所有配送员
UPDATE public.ml_delivery_staff
SET deleted_at = now(),
deleted_by = v_user_id
WHERE station_id = p_id AND deleted_at IS NULL;
-- 4. 执行软删除站点本身
UPDATE public.ml_delivery_stations
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -1,5 +1,5 @@
-- RPC: rpc_admin_delete_agent
-- 管理端删除代理商
-- 管理端删除代理商(支持级联软删除代理申请记录)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_agent(
p_uid uuid
@@ -11,16 +11,27 @@ SET search_path = public
AS $$
DECLARE
v_ok boolean;
v_user_id UUID;
BEGIN
-- 仅管理员可操作
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
-- 1. 权限检查
IF NOT public.check_admin_permission('distribution:agent:delete') THEN
RAISE EXCEPTION 'Permission denied: distribution:agent:delete';
END IF;
DELETE FROM public.ak_distribution_agents WHERE uid = p_uid;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:该代理的所有申请记录
UPDATE public.ak_distribution_agent_applications
SET deleted_at = now(),
deleted_by = v_user_id
WHERE user_id = p_uid AND deleted_at IS NULL;
-- 4. 软删除代理商记录
UPDATE public.ak_distribution_agents
SET deleted_at = now(),
deleted_by = v_user_id
WHERE uid = p_uid AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -1,5 +1,5 @@
-- RPC: rpc_admin_delete_division
-- 管理端删除事业部
-- 管理端删除事业部(支持级联软删除关联代理)
CREATE OR REPLACE FUNCTION public.rpc_admin_delete_division(
p_uid uuid
@@ -11,23 +11,33 @@ SET search_path = public
AS $$
DECLARE
v_ok boolean;
v_user_id UUID;
BEGIN
-- 仅管理员可操作
IF NOT EXISTS (
SELECT 1 FROM public.ak_users u
WHERE u.id = auth.uid() AND u.role = 'admin'
) THEN
RAISE EXCEPTION 'permission denied';
-- 1. 权限检查
IF NOT public.check_admin_permission('distribution:division:delete') THEN
RAISE EXCEPTION 'Permission denied: distribution:division:delete';
END IF;
-- 检查是否有关联代理商
IF EXISTS (
SELECT 1 FROM public.ak_distribution_agents WHERE division_uid = p_uid
) THEN
RAISE EXCEPTION 'cannot delete division with associated agents';
END IF;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
DELETE FROM public.ak_distribution_divisions WHERE uid = p_uid;
-- 3. 级联软删除:该事业部下的所有代理商
UPDATE public.ak_distribution_agents
SET deleted_at = now(),
deleted_by = v_user_id
WHERE division_uid = p_uid AND deleted_at IS NULL;
-- 4. 级联软删除:该事业部的所有申请记录
UPDATE public.ak_distribution_division_applications
SET deleted_at = now(),
deleted_by = v_user_id
WHERE user_id = p_uid AND deleted_at IS NULL;
-- 5. 软删除事业部本身
UPDATE public.ak_distribution_divisions
SET deleted_at = now(),
deleted_by = v_user_id
WHERE uid = p_uid AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除客服账号
-- 说明:管理端删除客服账号(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_delete(
@@ -16,17 +16,21 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
IF NOT public.check_admin_permission('kefu:account:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:account:delete';
END IF;
-- 2. 执行删除
DELETE FROM public.ml_kefu_accounts WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_kefu_accounts
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除客服自动回复配置
-- 说明:管理端删除客服自动回复配置(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_delete(
@@ -16,17 +16,21 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
IF NOT public.check_admin_permission('kefu:auto_reply:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:auto_reply:delete';
END IF;
-- 2. 执行删除
DELETE FROM public.ml_kefu_auto_replies WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_kefu_auto_replies
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除话术分类
-- 说明:管理端删除话术分类(支持级联软删除话术)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_delete(
@@ -16,17 +16,27 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
IF NOT public.check_admin_permission('kefu:word:category:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:word:category:delete';
END IF;
-- 2. 执行删除 (ml_kefu_words 已设置 ON DELETE CASCADE)
DELETE FROM public.ml_kefu_word_categories WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 级联软删除:先标记该分类下的话术为删除
UPDATE public.ml_kefu_words
SET deleted_at = now(),
deleted_by = v_user_id
WHERE category_id = p_id AND deleted_at IS NULL;
-- 4. 执行软删除分类本身:标记 deleted_at
UPDATE public.ml_kefu_word_categories
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/kefu/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:管理端删除快捷话术
-- 说明:管理端删除快捷话术(使用通用权限校验)
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_delete(
@@ -16,17 +16,21 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
IF NOT public.check_admin_permission('kefu:word:delete') THEN
RAISE EXCEPTION 'Permission denied: kefu:word:delete';
END IF;
-- 2. 执行删除
DELETE FROM public.ml_kefu_words WHERE id = p_id;
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 执行软删除:标记 deleted_at
UPDATE public.ml_kefu_words
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;
RETURN v_ok;

View File

@@ -2,9 +2,8 @@
-- Admin 商品模块 - 删除分类 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 方案:方案 1有子项禁止删除
-- 版本v1
-- 依赖ml_categories, ak_users 表已存在
-- 版本v1支持级联软删除商品关联
-- 依赖ml_categories, ml_products, ak_users 表已存在
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_category_delete(
@@ -15,31 +14,36 @@ SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_user_id UUID;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
IF NOT public.check_admin_permission('product:category:delete') THEN
RAISE EXCEPTION 'Permission denied: product:category:delete';
END IF;
-- 2. 检查是否有子分类 (方案 1)
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 检查是否有子分类 (方案 1)
IF EXISTS (
SELECT 1 FROM public.ml_categories WHERE parent_id = p_id
SELECT 1 FROM public.ml_categories
WHERE parent_id = p_id AND deleted_at IS NULL
) THEN
RAISE EXCEPTION '请先删除该分类下的子分类';
END IF;
-- 3. 检查是否有商品关联 (可选,通常作为安全保障)
IF EXISTS (
SELECT 1 FROM public.ml_products WHERE category_id = p_id AND status != 4
) THEN
RAISE EXCEPTION '该分类下仍有商品,无法删除';
END IF;
-- 4. 级联软删除:该分类下的所有商品
UPDATE public.ml_products
SET deleted_at = now(),
deleted_by = v_user_id
WHERE category_id = p_id AND deleted_at IS NULL;
-- 4. 执行删除
DELETE FROM public.ml_categories WHERE id = p_id;
-- 5. 执行删除分类本身
UPDATE public.ml_categories
SET deleted_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
RETURN FOUND;
END;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户分组(设置 deleted_at
-- 说明:逻辑删除用户分组(使用通用权限校验
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_group_delete(
@@ -16,18 +16,21 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_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';
IF NOT public.check_admin_permission('user:group:delete') THEN
RAISE EXCEPTION 'Permission denied: user:group:delete';
END IF;
-- 2. 逻辑删除
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 逻辑删除
UPDATE public.ak_user_groups
SET deleted_at = now(), updated_at = now()
SET deleted_at = now(),
updated_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户标签(设置 deleted_at
-- 说明:逻辑删除用户标签(使用通用权限校验
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_label_delete(
@@ -16,18 +16,21 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_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';
IF NOT public.check_admin_permission('user:label:delete') THEN
RAISE EXCEPTION 'Permission denied: user:label:delete';
END IF;
-- 2. 逻辑删除
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 逻辑删除
UPDATE public.ak_user_labels
SET deleted_at = now(), updated_at = now()
SET deleted_at = now(),
updated_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;

View File

@@ -3,7 +3,7 @@
-- 位置docs/sql/30_rpc/user/
-- 对象类型RPC 函数 (SECURITY DEFINER)
-- 版本v1
-- 说明:逻辑删除用户等级(设置 deleted_at
-- 说明:逻辑删除用户等级(使用通用权限校验
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_level_delete(
@@ -16,18 +16,21 @@ LANGUAGE plpgsql
AS $$
DECLARE
v_ok BOOLEAN;
v_user_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';
IF NOT public.check_admin_permission('user:level:delete') THEN
RAISE EXCEPTION 'Permission denied: user:level:delete';
END IF;
-- 2. 逻辑删除
-- 2. 获取当前操作用户 ID
SELECT id INTO v_user_id FROM public.ak_users WHERE auth_id = auth.uid();
-- 3. 逻辑删除
UPDATE public.ak_user_levels
SET deleted_at = now(), updated_at = now()
SET deleted_at = now(),
updated_at = now(),
deleted_by = v_user_id
WHERE id = p_id AND deleted_at IS NULL;
GET DIAGNOSTICS v_ok = ROW_COUNT;

View File

@@ -0,0 +1,489 @@
# SQL 文件执行顺序文档(软删除标准化改造 + Admin 模块全量)
## 目标
为本次“软删除标准化改造Schema + RLS + RPC”提供**可直接执行**且**顺序准确**的数据库同步步骤,同时整理**全量 Admin 模块**对应的数据库文件,确保:
- 先补齐表字段与索引(避免 RPC/RLS 引用字段失败);
- 再应用 RLS确保查询默认过滤已软删数据
- 最后加载/替换 RPC确保删除行为变更与审计字段写入生效
## 前置约束
- 执行账号需具备:`ALTER TABLE``CREATE INDEX``ALTER POLICY/CREATE POLICY``CREATE OR REPLACE FUNCTION` 等权限。
- **禁止并发执行**:建议串行执行,尤其是 RLS/RPC 部分。
- 若生产环境已存在同名对象:本次以 `CREATE OR REPLACE FUNCTION` 与 policy 更新为主,属于“覆盖式更新”。
---
## 第一阶段Schema / Migration必须先执行
> 目的:补齐 `deleted_at/deleted_by/restored_at/restored_by` 字段与软删除索引,作为后续 RLS/RPC 的基础依赖。
| 序号 | 文件路径 | 说明 |
|:--:|:---|:---|
| 1 | [`10_schema/99_soft_delete_migration_v1.sql`](./10_schema/99_soft_delete_migration_v1.sql) | 软删除字段迁移(核心) |
### 其他 Schema 文件(按业务域分类)
#### Admin 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/admin/ml_system_configs_v1.sql`](./10_schema/admin/ml_system_configs_v1.sql) | 系统配置表 |
#### Auth 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/user/ak_auth_system_v1.sql`](./10_schema/user/ak_auth_system_v1.sql) | 认证系统表roles/permissions/admin_roles/role_permissions |
| [`10_schema/user/ak_users_add_phone_real_name_v1.sql`](./10_schema/user/ak_users_add_phone_real_name_v1.sql) | 用户扩展字段 |
| [`10_schema/user/ak_users_finance_fields_v1.sql`](./10_schema/user/ak_users_finance_fields_v1.sql) | 用户财务字段 |
| [`10_schema/user/ak_users_constraints_fix_v1.sql`](./10_schema/user/ak_users_constraints_fix_v1.sql) | 用户表约束修复 |
#### CMS 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/cms/ml_cms_tables_v1.sql`](./10_schema/cms/ml_cms_tables_v1.sql) | CMS 内容管理表 |
#### Decoration 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/decoration/ak_diy_pages_v1.sql`](./10_schema/decoration/ak_diy_pages_v1.sql) | DIY 页面表 |
#### Delivery 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/delivery/ak_delivery_system_v1.sql`](./10_schema/delivery/ak_delivery_system_v1.sql) | 配送系统表 |
#### Distribution 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/distribution/ak_distribution_agents_v1.sql`](./10_schema/distribution/ak_distribution_agents_v1.sql) | 分销代理表 |
| [`10_schema/distribution/ak_distribution_agent_applications_v1.sql`](./10_schema/distribution/ak_distribution_agent_applications_v1.sql) | 代理申请表 |
| [`10_schema/distribution/ak_distribution_divisions_v1.sql`](./10_schema/distribution/ak_distribution_divisions_v1.sql) | 事业部表 |
| [`10_schema/distribution/ak_distribution_division_applications_v1.sql`](./10_schema/distribution/ak_distribution_division_applications_v1.sql) | 事业部申请表 |
| [`10_schema/distribution/ak_promoter_relations_v1.sql`](./10_schema/distribution/ak_promoter_relations_v1.sql) | 推广员关系表 |
| [`10_schema/distribution/ak_commission_logs_v1.sql`](./10_schema/distribution/ak_commission_logs_v1.sql) | 佣金日志表 |
| [`10_schema/distribution/ak_distribution_level_v1.sql`](./10_schema/distribution/ak_distribution_level_v1.sql) | 分销等级表 |
| [`10_schema/distribution/ak_distribution_config_v1.sql`](./10_schema/distribution/ak_distribution_config_v1.sql) | 分销配置表 |
#### Finance 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/finance/ml_invoices_v1.sql`](./10_schema/finance/ml_invoices_v1.sql) | 发票表 |
| [`10_schema/finance/ml_extract_v1.sql`](./10_schema/finance/ml_extract_v1.sql) | 提现表 |
| [`10_schema/finance/ml_user_recharge_v1.sql`](./10_schema/finance/ml_user_recharge_v1.sql) | 用户充值表 |
| [`10_schema/finance/ml_user_bill_v1.sql`](./10_schema/finance/ml_user_bill_v1.sql) | 用户账单表 |
#### Kefu 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/kefu/ml_kefu_tables_v1.sql`](./10_schema/kefu/ml_kefu_tables_v1.sql) | 客服系统表 |
#### Marketing 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/marketing/ak_lottery_live_v1.sql`](./10_schema/marketing/ak_lottery_live_v1.sql) | 直播抽奖 |
| [`10_schema/marketing/ak_marketing_signin_logs_v1.sql`](./10_schema/marketing/ak_marketing_signin_logs_v1.sql) | 签到日志 |
| [`10_schema/marketing/ak_marketing_newcomer_config_v1.sql`](./10_schema/marketing/ak_marketing_newcomer_config_v1.sql) | 新人配置 |
| [`10_schema/marketing/ak_bargain_groupbuy_v1.sql`](./10_schema/marketing/ak_bargain_groupbuy_v1.sql) | 拼团砍价 |
| [`10_schema/marketing/ak_live_products_v1.sql`](./10_schema/marketing/ak_live_products_v1.sql) | 直播商品 |
| [`10_schema/marketing/ak_advanced_marketing_v1.sql`](./10_schema/marketing/ak_advanced_marketing_v1.sql) | 高级营销 |
| [`10_schema/marketing/ak_marketing_checkin_configs_v1.sql`](./10_schema/marketing/ak_marketing_checkin_configs_v1.sql) | 签到配置 |
| [`10_schema/marketing/ak_recharge_management_v1.sql`](./10_schema/marketing/ak_recharge_management_v1.sql) | 充值管理 |
| [`10_schema/marketing/ak_member_management_v1.sql`](./10_schema/marketing/ak_member_management_v1.sql) | 会员管理 |
| [`10_schema/marketing/ak_signin_configs_v1.sql`](./10_schema/marketing/ak_signin_configs_v1.sql) | 签到配置 |
#### Order 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/order/ml_orders_schema_update_v1.sql`](./10_schema/order/ml_orders_schema_update_v1.sql) | 订单表结构更新 |
#### Product 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/product/ml_products_ext_v1.sql`](./10_schema/product/ml_products_ext_v1.sql) | 商品扩展表 |
| [`10_schema/product/ak_shipping_templates_v1.sql`](./10_schema/product/ak_shipping_templates_v1.sql) | 运费模板 |
| [`10_schema/product/ak_product_member_prices_v1.sql`](./10_schema/product/ak_product_member_prices_v1.sql) | 会员价 |
| [`10_schema/product/ak_product_protections_v1.sql`](./10_schema/product/ak_product_protections_v1.sql) | 商品保障 |
| [`10_schema/product/ak_product_templates_v1.sql`](./10_schema/product/ak_product_templates_v1.sql) | 商品模板 |
| [`10_schema/product/ak_product_labels_v1.sql`](./10_schema/product/ak_product_labels_v1.sql) | 商品标签 |
#### User 域
| 文件路径 | 说明 |
|:---|:---|
| [`10_schema/user/ak_user_labels_v1.sql`](./10_schema/user/ak_user_labels_v1.sql) | 用户标签表 |
| [`10_schema/user/ak_user_groups_v1.sql`](./10_schema/user/ak_user_groups_v1.sql) | 用户分组表 |
| [`10_schema/user/ak_user_levels_v1.sql`](./10_schema/user/ak_user_levels_v1.sql) | 用户等级表 |
---
## 第二阶段RLS在 Schema 之后执行)
> 目的:将各模块策略统一补齐 `deleted_at IS NULL`,确保默认查询排除已软删记录。
按域执行(域之间无强依赖,可按任意顺序;但**必须在 RPC 之前**执行):
### Auth 域(新增)
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/auth/ak_auth_rls_v1.sql`](./20_rls/auth/ak_auth_rls_v1.sql) | 权限/角色表 RLS 策略(禁止直接访问) |
### Admin 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/admin/ml_system_configs_rls_v1.sql`](./20_rls/admin/ml_system_configs_rls_v1.sql) | 系统配置 RLS |
### CMS 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/cms/ml_cms_rls_v1.sql`](./20_rls/cms/ml_cms_rls_v1.sql) | CMS 内容 RLS |
### Decoration 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/decoration/ml_decoration_rls_v1.sql`](./20_rls/decoration/ml_decoration_rls_v1.sql) | 装修 RLS |
### Delivery 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/delivery/ak_delivery_rls_v1.sql`](./20_rls/delivery/ak_delivery_rls_v1.sql) | 配送 RLS |
### Distribution 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/distribution/ml_distribution_rls_v1.sql`](./20_rls/distribution/ml_distribution_rls_v1.sql) | 分销 RLS |
### Finance 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/finance/ml_extract_rls_v1.sql`](./20_rls/finance/ml_extract_rls_v1.sql) | 提现 RLS |
| [`20_rls/finance/ml_invoices_rls_v1.sql`](./20_rls/finance/ml_invoices_rls_v1.sql) | 发票 RLS |
| [`20_rls/finance/ml_user_bill_rls_v1.sql`](./20_rls/finance/ml_user_bill_rls_v1.sql) | 用户账单 RLS |
| [`20_rls/finance/ml_user_recharge_rls_v1.sql`](./20_rls/finance/ml_user_recharge_rls_v1.sql) | 用户充值 RLS |
### Kefu 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/kefu/ml_kefu_rls_v1.sql`](./20_rls/kefu/ml_kefu_rls_v1.sql) | 客服 RLS |
### Marketing 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/marketing/ml_marketing_others_rls_v1.sql`](./20_rls/marketing/ml_marketing_others_rls_v1.sql) | 其他营销 RLS |
| [`20_rls/marketing/ml_marketing_activities_rls_v1.sql`](./20_rls/marketing/ml_marketing_activities_rls_v1.sql) | 营销活动 RLS |
| [`20_rls/marketing/ml_coupon_templates_rls_v1.sql`](./20_rls/marketing/ml_coupon_templates_rls_v1.sql) | 优惠券模板 RLS |
### User 域
| 文件路径 | 说明 |
|:---|:---|
| [`20_rls/user/ak_user_labels_rls_v1.sql`](./20_rls/user/ak_user_labels_rls_v1.sql) | 用户标签 RLS |
| [`20_rls/user/ak_user_groups_rls_v1.sql`](./20_rls/user/ak_user_groups_rls_v1.sql) | 用户分组 RLS |
| [`20_rls/user/ak_user_levels_rls_v1.sql`](./20_rls/user/ak_user_levels_rls_v1.sql) | 用户等级 RLS |
---
## 第三阶段RPC最后执行
> 目的:将“删除”统一切换为软删除(写入 `deleted_at/deleted_by`),并在需要的函数中补齐级联软删。
按域执行(域之间一般无强依赖;但建议按下列顺序,先基础权限/用户,再业务模块):
---
### 3.1 Admin 域(系统管理)
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/admin/rpc_admin_get_system_info_v1.sql`](./30_rpc/admin/rpc_admin_get_system_info_v1.sql) | 获取系统信息 |
| [`30_rpc/admin/rpc_admin_get_overall_stats_v1.sql`](./30_rpc/admin/rpc_admin_get_overall_stats_v1.sql) | 获取整体统计 |
| [`30_rpc/admin/rpc_admin_system_config_get_v1.sql`](./30_rpc/admin/rpc_admin_system_config_get_v1.sql) | 获取系统配置 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/admin/rpc_admin_system_config_save_v1.sql`](./30_rpc/admin/rpc_admin_system_config_save_v1.sql) | 保存系统配置 |
---
### 3.2 Auth 域(权限认证)
#### 权限校验辅助函数(新增)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/auth/fn_check_admin_permission_v1.sql`](./30_rpc/auth/fn_check_admin_permission_v1.sql) | 通用 RBAC 权限校验函数 |
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/auth/rpc_admin_get_permission_list_v1.sql`](./30_rpc/auth/rpc_admin_get_permission_list_v1.sql) | 获取权限列表 |
| [`30_rpc/auth/rpc_admin_get_role_list_v1.sql`](./30_rpc/auth/rpc_admin_get_role_list_v1.sql) | 获取角色列表 |
| [`30_rpc/auth/rpc_admin_get_admin_list_v1.sql`](./30_rpc/auth/rpc_admin_get_admin_list_v1.sql) | 获取管理员列表 |
| [`30_rpc/auth/get_current_user_role_v1.sql`](./30_rpc/auth/get_current_user_role_v1.sql) | 获取当前用户角色 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/auth/rpc_admin_save_permission_v1.sql`](./30_rpc/auth/rpc_admin_save_permission_v1.sql) | 保存权限 |
| [`30_rpc/auth/rpc_admin_save_role_v1.sql`](./30_rpc/auth/rpc_admin_save_role_v1.sql) | 保存角色 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/auth/rpc_admin_delete_permission_v1.sql`](./30_rpc/auth/rpc_admin_delete_permission_v1.sql) | 删除权限(软删除) |
| [`30_rpc/auth/rpc_admin_delete_role_v1.sql`](./30_rpc/auth/rpc_admin_delete_role_v1.sql) | 删除角色(软删除) |
#### 用户处理类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/auth/handle_new_user_v3.sql`](./30_rpc/auth/handle_new_user_v3.sql) | 处理新用户 v3 |
| [`30_rpc/auth/handle_new_user_v2.sql`](./30_rpc/auth/handle_new_user_v2.sql) | 处理新用户 v2 |
---
### 3.3 CMS 域(内容管理)
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/cms/rpc_admin_article_list_v1.sql`](./30_rpc/cms/rpc_admin_article_list_v1.sql) | 获取文章列表 |
| [`30_rpc/cms/rpc_admin_article_get_detail_v1.sql`](./30_rpc/cms/rpc_admin_article_get_detail_v1.sql) | 获取文章详情 |
| [`30_rpc/cms/rpc_admin_article_category_list_v1.sql`](./30_rpc/cms/rpc_admin_article_category_list_v1.sql) | 获取文章分类列表 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/cms/rpc_admin_article_save_v1.sql`](./30_rpc/cms/rpc_admin_article_save_v1.sql) | 保存文章 |
| [`30_rpc/cms/rpc_admin_article_category_save_v1.sql`](./30_rpc/cms/rpc_admin_article_category_save_v1.sql) | 保存文章分类 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/cms/rpc_admin_article_delete_v1.sql`](./30_rpc/cms/rpc_admin_article_delete_v1.sql) | 删除文章(软删除) |
| [`30_rpc/cms/rpc_admin_article_category_delete_v1.sql`](./30_rpc/cms/rpc_admin_article_category_delete_v1.sql) | 删除文章分类(软删除) |
#### 状态管理类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/cms/rpc_admin_article_set_status_v1.sql`](./30_rpc/cms/rpc_admin_article_set_status_v1.sql) | 设置文章状态 |
| [`30_rpc/cms/rpc_admin_article_category_set_status_v1.sql`](./30_rpc/cms/rpc_admin_article_category_set_status_v1.sql) | 设置文章分类状态 |
---
### 3.4 Decoration 域(装修管理)
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/decoration/rpc_admin_get_diy_page_list_v1.sql`](./30_rpc/decoration/rpc_admin_get_diy_page_list_v1.sql) | 获取 DIY 页面列表 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/decoration/rpc_admin_save_diy_page_v1.sql`](./30_rpc/decoration/rpc_admin_save_diy_page_v1.sql) | 保存 DIY 页面 |
| [`30_rpc/decoration/rpc_admin_set_home_page_v1.sql`](./30_rpc/decoration/rpc_admin_set_home_page_v1.sql) | 设置首页 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/decoration/rpc_admin_delete_diy_page_v1.sql`](./30_rpc/decoration/rpc_admin_delete_diy_page_v1.sql) | 删除 DIY 页面(软删除) |
---
### 3.5 Delivery 域(配送管理)
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/delivery/rpc_admin_get_delivery_staff_list_v1.sql`](./30_rpc/delivery/rpc_admin_get_delivery_staff_list_v1.sql) | 获取配送员列表 |
| [`30_rpc/delivery/rpc_admin_get_delivery_station_list_v1.sql`](./30_rpc/delivery/rpc_admin_get_delivery_station_list_v1.sql) | 获取配送站点列表 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/delivery/rpc_admin_save_delivery_staff_v1.sql`](./30_rpc/delivery/rpc_admin_save_delivery_staff_v1.sql) | 保存配送员 |
| [`30_rpc/delivery/rpc_admin_save_delivery_station_v1.sql`](./30_rpc/delivery/rpc_admin_save_delivery_station_v1.sql) | 保存配送站点 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/delivery/rpc_admin_delete_delivery_staff_v1.sql`](./30_rpc/delivery/rpc_admin_delete_delivery_staff_v1.sql) | 删除配送员(软删除) |
| [`30_rpc/delivery/rpc_admin_delete_delivery_station_v1.sql`](./30_rpc/delivery/rpc_admin_delete_delivery_station_v1.sql) | 删除配送站点(软删除) |
---
### 3.6 Distribution 域(分销管理)
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/distribution/rpc_admin_get_agent_list_v1.sql`](./30_rpc/distribution/rpc_admin_get_agent_list_v1.sql) | 获取代理列表 |
| [`30_rpc/distribution/rpc_admin_get_agent_apply_list_v1.sql`](./30_rpc/distribution/rpc_admin_get_agent_apply_list_v1.sql) | 获取代理申请列表 |
| [`30_rpc/distribution/rpc_admin_get_division_list_v1.sql`](./30_rpc/distribution/rpc_admin_get_division_list_v1.sql) | 获取事业部列表 |
| [`30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql`](./30_rpc/distribution/rpc_admin_get_promoter_list_v1.sql) | 获取推广员列表 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/distribution/rpc_admin_save_agent_v1.sql`](./30_rpc/distribution/rpc_admin_save_agent_v1.sql) | 保存代理 |
| [`30_rpc/distribution/rpc_admin_save_division_v1.sql`](./30_rpc/distribution/rpc_admin_save_division_v1.sql) | 保存事业部 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/distribution/rpc_admin_delete_agent_v1.sql`](./30_rpc/distribution/rpc_admin_delete_agent_v1.sql) | 删除代理(软删除) |
| [`30_rpc/distribution/rpc_admin_delete_division_v1.sql`](./30_rpc/distribution/rpc_admin_delete_division_v1.sql) | 删除事业部(软删除) |
#### 处理/审核类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/distribution/rpc_admin_process_agent_apply_v1.sql`](./30_rpc/distribution/rpc_admin_process_agent_apply_v1.sql) | 处理代理申请 |
---
### 3.7 Finance 域(财务管理)
#### 查询/统计类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/finance/rpc_admin_balance_stats_v1.sql`](./30_rpc/finance/rpc_admin_balance_stats_v1.sql) | 余额统计 |
| [`30_rpc/finance/rpc_admin_balance_distribution_v1.sql`](./30_rpc/finance/rpc_admin_balance_distribution_v1.sql) | 余额分布 |
| [`30_rpc/finance/rpc_admin_balance_trend_v1.sql`](./30_rpc/finance/rpc_admin_balance_trend_v1.sql) | 余额趋势 |
| [`30_rpc/finance/rpc_admin_finance_bill_summary_v1.sql`](./30_rpc/finance/rpc_admin_finance_bill_summary_v1.sql) | 财务账单汇总 |
| [`30_rpc/finance/rpc_admin_finance_overview_v1.sql`](./30_rpc/finance/rpc_admin_finance_overview_v1.sql) | 财务概览 |
| [`30_rpc/finance/rpc_admin_invoice_list_v1.sql`](./30_rpc/finance/rpc_admin_invoice_list_v1.sql) | 发票列表 |
| [`30_rpc/finance/rpc_admin_user_bill_list_v1.sql`](./30_rpc/finance/rpc_admin_user_bill_list_v1.sql) | 用户账单列表 |
| [`30_rpc/finance/rpc_admin_recharge_list_v1.sql`](./30_rpc/finance/rpc_admin_recharge_list_v1.sql) | 充值列表 |
| [`30_rpc/finance/rpc_admin_extract_list_v1.sql`](./30_rpc/finance/rpc_admin_extract_list_v1.sql) | 提现列表 |
#### 处理/审核类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/finance/rpc_admin_invoice_process_v1.sql`](./30_rpc/finance/rpc_admin_invoice_process_v1.sql) | 处理发票 |
| [`30_rpc/finance/rpc_admin_recharge_audit_v1.sql`](./30_rpc/finance/rpc_admin_recharge_audit_v1.sql) | 审核充值 |
| [`30_rpc/finance/rpc_admin_extract_review_v1.sql`](./30_rpc/finance/rpc_admin_extract_review_v1.sql) | 审核提现 |
---
### 3.8 Kefu 域(客服管理)
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/kefu/rpc_admin_kefu_account_list_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_account_list_v1.sql) | 获取客服账号列表 |
| [`30_rpc/kefu/rpc_admin_kefu_auto_reply_list_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_auto_reply_list_v1.sql) | 获取自动回复列表 |
| [`30_rpc/kefu/rpc_admin_kefu_word_list_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_word_list_v1.sql) | 获取客服话术列表 |
| [`30_rpc/kefu/rpc_admin_kefu_word_category_list_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_word_category_list_v1.sql) | 获取话术分类列表 |
| [`30_rpc/kefu/rpc_admin_kefu_feedback_list_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_feedback_list_v1.sql) | 获取反馈列表 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/kefu/rpc_admin_kefu_account_save_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_account_save_v1.sql) | 保存客服账号 |
| [`30_rpc/kefu/rpc_admin_kefu_auto_reply_save_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_auto_reply_save_v1.sql) | 保存自动回复 |
| [`30_rpc/kefu/rpc_admin_kefu_word_save_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_word_save_v1.sql) | 保存客服话术 |
| [`30_rpc/kefu/rpc_admin_kefu_word_category_save_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_word_category_save_v1.sql) | 保存话术分类 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/kefu/rpc_admin_kefu_account_delete_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_account_delete_v1.sql) | 删除客服账号(软删除) |
| [`30_rpc/kefu/rpc_admin_kefu_auto_reply_delete_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_auto_reply_delete_v1.sql) | 删除自动回复(软删除) |
| [`30_rpc/kefu/rpc_admin_kefu_word_delete_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_word_delete_v1.sql) | 删除客服话术(软删除) |
| [`30_rpc/kefu/rpc_admin_kefu_word_category_delete_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_word_category_delete_v1.sql) | 删除话术分类(软删除) |
#### 状态/处理类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/kefu/rpc_admin_kefu_account_set_status_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_account_set_status_v1.sql) | 设置客服账号状态 |
| [`30_rpc/kefu/rpc_admin_kefu_auto_reply_set_status_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_auto_reply_set_status_v1.sql) | 设置自动回复状态 |
| [`30_rpc/kefu/rpc_admin_kefu_feedback_process_v1.sql`](./30_rpc/kefu/rpc_admin_kefu_feedback_process_v1.sql) | 处理客服反馈 |
---
### 3.9 Marketing 域(营销管理)
#### 查询/统计类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql`](./30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql) | 获取积分统计 |
---
### 3.10 Order 域(订单管理)
#### 查询/统计类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/order/rpc_admin_order_list_v1.sql`](./30_rpc/order/rpc_admin_order_list_v1.sql) | 获取订单列表 |
| [`30_rpc/order/rpc_admin_order_source_stats_v1.sql`](./30_rpc/order/rpc_admin_order_source_stats_v1.sql) | 订单来源统计 |
| [`30_rpc/order/rpc_admin_order_type_stats_v1.sql`](./30_rpc/order/rpc_admin_order_type_stats_v1.sql) | 订单类型统计 |
| [`30_rpc/order/rpc_admin_order_stats_v1.sql`](./30_rpc/order/rpc_admin_order_stats_v1.sql) | 订单统计 |
| [`30_rpc/order/rpc_admin_order_trend_v1.sql`](./30_rpc/order/rpc_admin_order_trend_v1.sql) | 订单趋势 |
| [`30_rpc/order/rpc_admin_write_off_record_list_v1.sql`](./30_rpc/order/rpc_admin_write_off_record_list_v1.sql) | 核销记录列表 |
| [`30_rpc/order/rpc_admin_refund_order_list_v1.sql`](./30_rpc/order/rpc_admin_refund_order_list_v1.sql) | 退款订单列表 |
| [`30_rpc/order/rpc_admin_cashier_order_list_v1.sql`](./30_rpc/order/rpc_admin_cashier_order_list_v1.sql) | 收银订单列表 |
---
### 3.11 Product 域(商品管理)
#### 查询/统计类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/product/rpc_admin_get_product_reviews_v1.sql`](./30_rpc/product/rpc_admin_get_product_reviews_v1.sql) | 获取商品评价 |
| [`30_rpc/product/rpc_admin_product_count_stats_v1.sql`](./30_rpc/product/rpc_admin_product_count_stats_v1.sql) | 商品数量统计 |
| [`30_rpc/product/rpc_admin_product_analytics_v1.sql`](./30_rpc/product/rpc_admin_product_analytics_v1.sql) | 商品分析 |
| [`30_rpc/product/rpc_admin_product_trend_v1.sql`](./30_rpc/product/rpc_admin_product_trend_v1.sql) | 商品趋势 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/product/rpc_admin_category_delete_v1.sql`](./30_rpc/product/rpc_admin_category_delete_v1.sql) | 删除商品分类(软删除) |
---
### 3.12 User 域(用户管理)
#### 查询类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/user/rpc_admin_user_label_list_v1.sql`](./30_rpc/user/rpc_admin_user_label_list_v1.sql) | 获取用户标签列表 |
| [`30_rpc/user/rpc_admin_user_group_list_v1.sql`](./30_rpc/user/rpc_admin_user_group_list_v1.sql) | 获取用户分组列表 |
| [`30_rpc/user/rpc_admin_user_level_list_v1.sql`](./30_rpc/user/rpc_admin_user_level_list_v1.sql) | 获取用户等级列表 |
#### 保存/更新类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/user/rpc_admin_user_label_save_v1.sql`](./30_rpc/user/rpc_admin_user_label_save_v1.sql) | 保存用户标签 |
| [`30_rpc/user/rpc_admin_user_group_save_v1.sql`](./30_rpc/user/rpc_admin_user_group_save_v1.sql) | 保存用户分组 |
| [`30_rpc/user/rpc_admin_user_level_save_v1.sql`](./30_rpc/user/rpc_admin_user_level_save_v1.sql) | 保存用户等级 |
#### 删除类(软删除)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/user/rpc_admin_user_label_delete_v1.sql`](./30_rpc/user/rpc_admin_user_label_delete_v1.sql) | 删除用户标签(软删除) |
| [`30_rpc/user/rpc_admin_user_group_delete_v1.sql`](./30_rpc/user/rpc_admin_user_group_delete_v1.sql) | 删除用户分组(软删除) |
| [`30_rpc/user/rpc_admin_user_level_delete_v1.sql`](./30_rpc/user/rpc_admin_user_level_delete_v1.sql) | 删除用户等级(软删除) |
#### 状态/可见性管理类
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/user/rpc_admin_user_label_set_status_v1.sql`](./30_rpc/user/rpc_admin_user_label_set_status_v1.sql) | 设置用户标签状态 |
| [`30_rpc/user/rpc_admin_user_group_set_status_v1.sql`](./30_rpc/user/rpc_admin_user_group_set_status_v1.sql) | 设置用户分组状态 |
| [`30_rpc/user/rpc_admin_user_level_set_status_v1.sql`](./30_rpc/user/rpc_admin_user_level_set_status_v1.sql) | 设置用户等级状态 |
| [`30_rpc/user/rpc_admin_user_level_set_visible_v1.sql`](./30_rpc/user/rpc_admin_user_level_set_visible_v1.sql) | 设置用户等级可见性 |
---
### 3.13 Analytics 域(数据分析)
| 文件路径 | 说明 |
|:---|:---|
| [`30_rpc/analytics/rpc_analytics_user_gender_distribution_v1.sql`](./30_rpc/analytics/rpc_analytics_user_gender_distribution_v1.sql) | 用户性别分布分析 |
---
## 执行后验证清单
- 字段存在性:抽查任一涉及表,确认 `deleted_at/deleted_by` 字段存在。
- 索引存在性:确认 `idx_<table>_soft_delete` 存在。
- RLS 生效:软删一条数据后,使用正常查询路径确认该条记录不可见。
- RPC 行为:调用任一删除 RPC确认
- 记录未被物理删除;
- `deleted_at` 写入;
- `deleted_by` 写入(管理员 ID
- 需要级联的对象其关联数据同步被软删。

View File

@@ -207,27 +207,42 @@ function handleEdit(item: ArticleCategory) {
async function toggleStatus(item: ArticleCategory) {
const targetStatus = item.status === 1 ? 0 : 1
const ok = await setArticleCategoryStatus(item.id, targetStatus)
if (ok) {
item.status = targetStatus
uni.showToast({ title: '状态已更新' })
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
try {
const resId = await saveArticleCategory(
item.id,
item.name,
item.icon,
item.sort,
targetStatus
)
if (resId != null) {
item.status = targetStatus
uni.showToast({ title: '状态已更新' })
}
} catch (e: any) {
const errMsg = e?.message || '操作失败'
uni.showToast({ title: errMsg, icon: 'none' })
}
}
async function handleDelete(item: ArticleCategory) {
uni.showModal({
title: '提示',
content: `确定要删除分类 "${item.name}" 吗?`,
title: '删除确认',
content: `确定要删除分类 "${item.name}" 吗?\n\n⚠ 警告:该操作将同时删除该分类下的所有文章!`,
confirmText: '确认删除',
confirmColor: '#ed4014',
success: async (res) => {
if (res.confirm) {
const ok = await deleteArticleCategory(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
} else {
uni.showToast({ title: '删除失败', icon: 'none' })
try {
const ok = await deleteArticleCategory(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
} catch (e: any) {
// 显示后端抛出的具体错误信息(如权限不足)
const errMsg = e?.message || '删除失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
}
}

View File

@@ -1,4 +1,4 @@
<template>
<template>
<view class="admin-main">
<!-- 头部搜索和操作 -->
<view class="search-card">
@@ -292,17 +292,24 @@ item.status = !item.status
}
async function deleteItem(item: CateItem) {
uni.showModal({
title: '提示',
content: '确定删除分类吗?',
uni.showModal({
title: '删除确认',
content: `确定删除分类 "${item.name}" 吗?\n\n⚠ 警告:该操作将同时删除该分类下的所有子分类及关联商品!`,
confirmText: '确认删除',
confirmColor: '#ff4d4f',
success: async (res) => {
if (res.confirm) {
await deleteAdminCategory(item.id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadList()
}
}
})
if (res.confirm) {
try {
await deleteAdminCategory(item.id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadList()
} catch (e: any) {
const errMsg = e?.message || '删除失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
}
}
})
}
</script>

View File

@@ -187,14 +187,21 @@ async function handleSave() {
async function onDelete(item : AdminRole) {
uni.showModal({
title: '提示',
content: `确定要删除角色 "${item.name}" 吗?`,
title: '删除确认',
content: `确定要删除角色 "${item.name}" 吗?\n\n⚠ 警告:该操作将同时解除所有关联管理员的身份并清空该角色的权限配置!`,
confirmText: '确认删除',
confirmColor: '#ff4d4f',
success: async (res) => {
if (res.confirm) {
const ok = await deleteRole(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
try {
const ok = await deleteRole(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
} catch (e: any) {
const errMsg = e?.message || '删除失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
}
}

View File

@@ -198,14 +198,21 @@ async function handleSave() {
async function onDelete(item : DeliveryStation) {
uni.showModal({
title: '提示',
content: `确定要删除提货点 "${item.name}" 吗?`,
title: '删除确认',
content: `确定要删除提货点 "${item.name}" 吗?\n\n⚠ 警告:该操作将同时删除该站点下的所有配送员关联!`,
confirmText: '确认删除',
confirmColor: '#ff4d4f',
success: async (res) => {
if (res.confirm) {
const ok = await deleteDeliveryStation(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
try {
const ok = await deleteDeliveryStation(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
} catch (e: any) {
const errMsg = e?.message || '删除失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
}
}

View File

@@ -28,16 +28,18 @@
</view>
<view class="tab-body">
<view v-if="isLoading" class="loading-state">
<text>配置加载中...</text>
</view>
<!-- Tab 0: 储存配置 -->
<view v-if="activeTab === 0" class="config-view">
<view v-else-if="activeTab === 0" class="config-view">
<view class="notice-box">
<view class="notice-content">
<text class="notice-line">上传图片时会生成缩略图</text>
<text class="notice-line">未设置按照系统默认生成系统默认大图800*800中图300*300小图150*150</text>
<text class="notice-line">水印只在上传图片时生成,原图,大中小缩略图都会按照比例存在。</text>
<text class="notice-line">若上传图片时未开启水印,则该图在开启水印之后依旧无水印效果。</text>
</view>
<text class="notice-close">×</text>
</view>
<view class="form-body">
@@ -68,69 +70,17 @@
</view>
<view class="form-footer mt-40">
<button class="save-btn" type="primary" @click="handleSave">保存</button>
<button class="save-btn" type="primary" :disabled="isSaving" @click="handleSave">
{{ isSaving ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
<!-- Tab 1-6: Cloud Storage -->
<!-- 其他云存储 Tab 逻辑保持现有结构,数据由 backend 动态驱动 -->
<view v-else class="cloud-view">
<view class="cloud-notice">
<view class="notice-title">
<text class="blue-title">{{ tabs[activeTab] }}开通方法:</text>
<text class="link-text">点击查看</text>
</view>
<text class="notice-step">第一步:添加【存储空间】(空间名称不能重复)</text>
<text class="notice-step">第二步:开启【使用状态】</text>
<text class="notice-step">第三步(可选):选择云存储空间列表上的修改【空间域名操作】</text>
<text class="notice-step">第四步可选选择云存储空间列表上的修改【CNAME配置】打开后复制记录值到对应的平台解析</text>
<text class="notice-close">×</text>
</view>
<view class="action-bar mt-20">
<view class="left-actions">
<button class="action-btn primary-btn">添加存储空间</button>
<button class="action-btn success-btn ml-10">同步存储空间</button>
</view>
<button class="action-btn outline-btn">修改配置信息</button>
</view>
<view class="table-container mt-20">
<view class="table-header">
<text class="th flex-2">储存空间名称</text>
<text class="th flex-1">区域</text>
<text class="th flex-3">空间域名</text>
<text class="th flex-1">使用状态</text>
<text class="th flex-2">创建时间</text>
<text class="th flex-2">更新时间</text>
<text class="th flex-2 center">操作</text>
</view>
<view class="table-body">
<view v-if="getCloudData().length > 0">
<view
class="table-row"
v-for="(row, rIndex) in getCloudData()"
:key="rIndex"
>
<text class="td flex-2">{{ row.name }}</text>
<text class="td flex-1">{{ row.region }}</text>
<text class="td flex-3 link">{{ row.domain }}</text>
<view class="td flex-1">
<switch :checked="row.status" scale="0.6" color="#2d8cf0" />
</view>
<text class="td flex-2 small-text muted">{{ row.createTime }}</text>
<text class="td flex-2 small-text muted">{{ row.updateTime }}</text>
<view class="td flex-2 center-row">
<text class="op-link">CNAME配置</text>
<text class="op-link ml-10">修改空间域名</text>
<text class="op-link danger ml-10">删除</text>
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无数据</text>
</view>
</view>
<view class="empty-state">
<text class="empty-text">{{ tabs[activeTab] }} 详细配置请在“修改配置信息”中设置</text>
</view>
</view>
</view>
@@ -139,10 +89,13 @@
</template>
<script setup lang="uts">
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { getSystemConfig, saveSystemConfig } from "@/services/admin/systemConfigService.uts"
const tabs = ['储存配置', '七牛云储存', '阿里云储存', '腾讯云储存', '京东云储存', '华为云储存', '天翼云储存']
const activeTab = ref(0)
const isLoading = ref(false)
const isSaving = ref(false)
const storageOptions = [
{ label: '本地存储', value: 'local' },
@@ -160,6 +113,22 @@ const form = reactive({
enableWatermark: false
})
onMounted(() => {
loadConfig()
})
async function loadConfig() {
isLoading.value = true
try {
const res = await getSystemConfig('storage_config')
if (res != null) {
Object.assign(form, res as any)
}
} finally {
isLoading.value = false
}
}
const onStorageTypeChange = (e : any) => {
form.storageType = e.detail.value as string
}
@@ -172,310 +141,46 @@ const onToggleWatermark = (e : any) => {
form.enableWatermark = e.detail.value as boolean
}
const handleSave = () => {
uni.showToast({
title: '保存成功',
icon: 'success'
})
}
// Simulated data for Aliyun Storage (based on screenshot)
const aliyunData = [
{ name: 'crmebdoc', region: '华北2 (北京)', domain: 'https://crmebdoc.oss-cn-beijing.aliyuncs.com', status: false, createTime: '2024-07-30 12:10:42', updateTime: '2025-01-22 10:58:20' },
{ name: 'crmebjavasingle', region: '华北2 (北京)', domain: 'https://crmebjavasingl...oss-cn-beijing.aliyunc...s.com', status: false, createTime: '2024-07-29 17:22:24', updateTime: '2025-01-22 10:58:20' },
{ name: 'crmebjavamer', region: '华北2 (北京)', domain: 'https://crmebjavamer.o...ss-cn-beijing.aliyuncs.c...om', status: false, createTime: '2024-07-22 14:43:42', updateTime: '2025-01-22 10:58:20' },
{ name: 'crmebmer', region: '华北2 (北京)', domain: 'https://crmebmer.oss-c...n-beijing.aliyuncs.com', status: false, createTime: '2024-07-22 14:42:53', updateTime: '2025-01-22 10:58:20' },
{ name: 'crmebmulti', region: '华北2 (北京)', domain: 'https://crmebmulti.oss-...cn-beijing.aliyuncs.com', status: false, createTime: '2024-07-22 14:42:08', updateTime: '2025-01-22 10:58:20' },
{ name: 'crmebpros', region: '华北2 (北京)', domain: 'https://crmebpros.oss-c...n-beijing.aliyuncs.com', status: false, createTime: '2024-07-22 14:41:17', updateTime: '2025-01-22 10:58:20' },
{ name: 'crmebbz', region: '华东1 (杭州)', domain: 'https://crmebbz.oss-cn-...hangzhou.aliyuncs.com', status: true, createTime: '2022-08-18 17:30:33', updateTime: '2025-01-22 10:58:20' }
]
const getCloudData = () : any[] => {
if (activeTab.value === 2) { // 阿里云
return aliyunData
const handleSave = async () => {
isSaving.value = true
try {
const ok = await saveSystemConfig('storage_config', form as UTSJSONObject, '系统存储配置')
if (ok) {
uni.showToast({ title: '保存成功', icon: 'success' })
}
} finally {
isSaving.value = false
}
return [] as any[]
}
</script>
<style scoped>
.admin-page {
min-height: 100vh;
background-color: #f5f7f9;
padding: 20px;
}
.breadcrumb {
display: flex;
flex-direction: row;
margin-bottom: 20px;
}
.bc-item {
font-size: 14px;
color: #999;
}
.bc-item.active {
color: #333;
}
.bc-sep {
margin: 0 8px;
color: #ccc;
}
.content-card {
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
}
.tabs-header {
border-bottom: 1px solid #f0f0f0;
}
.tabs-scroll {
white-space: nowrap;
width: 100%;
}
.tabs-list {
display: flex;
flex-direction: row;
padding: 0 15px;
}
.tab-item {
padding: 15px 15px;
cursor: pointer;
position: relative;
}
.tab-text {
font-size: 14px;
color: #666;
}
.tab-item.active .tab-text {
color: #2d8cf0;
font-weight: bold;
}
.tab-item.active::after {
content: "";
position: absolute;
bottom: 0;
left: 15px;
right: 15px;
height: 2px;
background-color: #2d8cf0;
}
.tab-body {
padding: 20px;
}
/* Notice Box Styles */
.notice-box {
background-color: #fffaf3;
border: 1px solid #ffebcc;
padding: 15px 20px;
border-radius: 4px;
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 30px;
}
.notice-line {
font-size: 13px;
color: #666;
line-height: 1.8;
display: block;
}
.notice-close {
color: #ccc;
font-size: 18px;
cursor: pointer;
}
/* Form Styles */
.form-body {
max-width: 800px;
}
.form-row {
display: flex;
flex-direction: row;
align-items: center;
}
.form-label {
width: 140px;
font-size: 13px;
color: #333;
}
.storage-radio-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.radio-label {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 20px;
margin-bottom: 10px;
}
.radio-text {
font-size: 13px;
margin-left: 4px;
color: #666;
}
.save-btn {
width: 80px;
height: 32px;
line-height: 32px;
background-color: #2d8cf0;
color: white;
font-size: 13px;
padding: 0;
border-radius: 4px;
}
.mt-20 { margin-top: 20px; }
.admin-page { min-height: 100vh; background-color: #f5f7f9; padding: 20px; }
.breadcrumb { display: flex; flex-direction: row; margin-bottom: 20px; }
.bc-item { font-size: 14px; color: #999; }
.bc-item.active { color: #333; }
.bc-sep { margin: 0 8px; color: #ccc; }
.content-card { background-color: #ffffff; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,21,41,.08); }
.tabs-header { border-bottom: 1px solid #f0f0f0; }
.tabs-scroll { white-space: nowrap; width: 100%; }
.tabs-list { display: flex; flex-direction: row; padding: 0 15px; }
.tab-item { padding: 15px 15px; cursor: pointer; position: relative; }
.tab-text { font-size: 14px; color: #666; }
.tab-item.active .tab-text { color: #2d8cf0; font-weight: bold; }
.tab-item.active::after { content: ""; position: absolute; bottom: 0; left: 15px; right: 15px; height: 2px; background-color: #2d8cf0; }
.tab-body { padding: 20px; }
.loading-state { padding: 60px; text-align: center; color: #999; }
.notice-box { background-color: #fffaf3; border: 1px solid #ffebcc; padding: 15px 20px; border-radius: 4px; margin-bottom: 30px; }
.notice-line { font-size: 13px; color: #666; line-height: 1.8; display: block; }
.form-body { max-width: 800px; }
.form-row { display: flex; flex-direction: row; align-items: center; }
.form-label { width: 140px; font-size: 13px; color: #333; }
.storage-radio-group { display: flex; flex-direction: row; flex-wrap: wrap; }
.radio-label { display: flex; flex-direction: row; align-items: center; margin-right: 20px; margin-bottom: 10px; }
.radio-text { font-size: 13px; margin-left: 4px; color: #666; }
.save-btn { width: 80px; height: 32px; line-height: 32px; background-color: #2d8cf0; color: white; font-size: 13px; border-radius: 4px; }
.mt-30 { margin-top: 30px; }
.mt-40 { margin-top: 40px; }
.ml-10 { margin-left: 10px; }
/* Cloud View Styles */
.cloud-notice {
background-color: #f0faff;
border: 1px solid #d5e8fc;
border-radius: 4px;
padding: 15px 20px;
position: relative;
}
.blue-title {
color: #2db7f5;
font-size: 14px;
font-weight: bold;
}
.link-text {
color: #2d8cf0;
font-size: 14px;
margin-left: 10px;
cursor: pointer;
text-decoration: underline;
}
.notice-step {
display: block;
font-size: 13px;
color: #666;
line-height: 1.8;
}
.action-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.left-actions {
display: flex;
flex-direction: row;
}
.action-btn {
font-size: 12px;
height: 30px;
line-height: 30px;
padding: 0 12px;
border-radius: 3px;
}
.primary-btn { background-color: #2d8cf0; color: white; border: none; }
.success-btn { background-color: #19be6b; color: white; border: none; }
.outline-btn { background-color: white; color: #666; border: 1px solid #dcdee2; }
/* Table Styles */
.table-container {
border: 1px solid #f0f0f0;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #f8f8f9;
border-bottom: 1px solid #f0f0f0;
}
.th {
padding: 12px 10px;
font-size: 13px;
font-weight: bold;
color: #333;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
align-items: center;
}
.td {
padding: 12px 10px;
font-size: 13px;
color: #666;
display: flex;
flex-direction: row;
align-items: center;
}
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
.flex-3 { flex: 3; }
.center { justify-content: center; }
.center-row { justify-content: center; }
.link {
color: #2d8cf0;
text-decoration: underline;
}
.small-text { font-size: 11px; }
.muted { color: #999; }
.op-link {
color: #2d8cf0;
font-size: 12px;
cursor: pointer;
}
.op-link.danger {
color: #ed4014;
}
.empty-state {
padding: 40px;
display: flex;
justify-content: center;
}
.empty-text {
color: #ccc;
font-size: 14px;
}
.cloud-view { padding: 40px; text-align: center; }
.empty-text { color: #ccc; font-size: 14px; }
</style>