1.0.3
parent
3c16839fa5
commit
898f207302
|
|
@ -1,177 +0,0 @@
|
||||||
# 生产环境数据库升级指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
此升级脚本包含以下变更:
|
|
||||||
1. 在 `celestial_bodies` 表增加 `short_name` 字段
|
|
||||||
2. 完整导入 `menus` 和 `role_menus` 表
|
|
||||||
3. 清空 `celestial_events` 表(将由定时任务重新生成)
|
|
||||||
4. 完整导入 `scheduled_jobs` 表
|
|
||||||
5. 导入/更新 `system_settings` 表
|
|
||||||
6. 保留 `user_follows` 表的现有数据
|
|
||||||
|
|
||||||
## 升级前准备
|
|
||||||
|
|
||||||
### 1. 备份数据库
|
|
||||||
```bash
|
|
||||||
# 在生产服务器上执行
|
|
||||||
pg_dump -U postgres -d cosmo_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 测试升级脚本(推荐)
|
|
||||||
```bash
|
|
||||||
# 在测试环境先运行
|
|
||||||
psql -U postgres -d cosmo_db_test < upgrade_production.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 执行升级
|
|
||||||
|
|
||||||
### 方式1:直接执行SQL文件
|
|
||||||
```bash
|
|
||||||
psql -U postgres -d cosmo_db < upgrade_production.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方式2:通过Docker容器执行
|
|
||||||
```bash
|
|
||||||
docker cp upgrade_production.sql <container_name>:/tmp/
|
|
||||||
docker exec -it <container_name> psql -U postgres -d cosmo_db -f /tmp/upgrade_production.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方式3:交互式执行(推荐,便于观察)
|
|
||||||
```bash
|
|
||||||
psql -U postgres -d cosmo_db
|
|
||||||
\i upgrade_production.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## 升级后验证
|
|
||||||
|
|
||||||
脚本会自动输出验证信息,检查以下内容:
|
|
||||||
|
|
||||||
1. **celestial_bodies.short_name 字段**:应该存在
|
|
||||||
2. **menus 数量**:应该是 14 条
|
|
||||||
3. **role_menus 数量**:应该是 16 条
|
|
||||||
4. **scheduled_jobs 数量**:应该是 2 条
|
|
||||||
5. **system_settings 数量**:应该至少 3 条
|
|
||||||
|
|
||||||
### 手动验证命令
|
|
||||||
```sql
|
|
||||||
-- 检查 short_name 字段
|
|
||||||
\d celestial_bodies
|
|
||||||
|
|
||||||
-- 检查菜单数据
|
|
||||||
SELECT id, name, title, path FROM menus ORDER BY parent_id NULLS FIRST, sort_order;
|
|
||||||
|
|
||||||
-- 检查角色菜单关联
|
|
||||||
SELECT r.name as role, m.title as menu
|
|
||||||
FROM role_menus rm
|
|
||||||
JOIN roles r ON rm.role_id = r.id
|
|
||||||
JOIN menus m ON rm.menu_id = m.id
|
|
||||||
ORDER BY r.name, m.sort_order;
|
|
||||||
|
|
||||||
-- 检查定时任务
|
|
||||||
SELECT id, name, is_active, predefined_function FROM scheduled_jobs;
|
|
||||||
|
|
||||||
-- 检查系统设置
|
|
||||||
SELECT key, value, value_type FROM system_settings;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 升级详情
|
|
||||||
|
|
||||||
### 1. celestial_bodies 表升级
|
|
||||||
- 增加 `short_name VARCHAR(50)` 字段
|
|
||||||
- 如果字段已存在,则跳过
|
|
||||||
|
|
||||||
### 2. menus 和 role_menus 导入
|
|
||||||
- **重要**:会清空现有菜单数据
|
|
||||||
- 导入 14 条菜单记录
|
|
||||||
- 导入 16 条角色-菜单关联记录
|
|
||||||
- 管理员可访问所有菜单
|
|
||||||
- 普通用户只能访问:个人资料、我的天体
|
|
||||||
|
|
||||||
### 3. celestial_events 清空
|
|
||||||
- 清空所有现有天体事件
|
|
||||||
- 数据会由定时任务 `calculate_planetary_events` 自动重新生成
|
|
||||||
|
|
||||||
### 4. scheduled_jobs 导入
|
|
||||||
导入2个定时任务:
|
|
||||||
- **每日更新天体位置数据**(已禁用)
|
|
||||||
- Cron: `0 2 * * *`(每天凌晨2点)
|
|
||||||
- 可通过后台管理界面手动执行
|
|
||||||
|
|
||||||
- **获取主要天体事件**(已启用)
|
|
||||||
- Cron: `0 3 1 * *`(每月1日凌晨3点)
|
|
||||||
- 自动计算未来一年的天文事件
|
|
||||||
|
|
||||||
### 5. system_settings 导入
|
|
||||||
导入3个系统设置:
|
|
||||||
- `view_mode`: solar(默认视图模式)
|
|
||||||
- `nasa_api_timeout`: 120(NASA API超时时间)
|
|
||||||
- `auto_download_positions`: False(自动下载位置数据开关)
|
|
||||||
|
|
||||||
使用 `ON CONFLICT` 策略,如果键已存在则更新值。
|
|
||||||
|
|
||||||
### 6. user_follows 保留
|
|
||||||
- **不会修改此表**
|
|
||||||
- 保留所有用户关注数据
|
|
||||||
|
|
||||||
## 回滚方案
|
|
||||||
|
|
||||||
如果升级失败,使用备份恢复:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 方式1:完整恢复
|
|
||||||
psql -U postgres -d cosmo_db < backup_YYYYMMDD_HHMMSS.sql
|
|
||||||
|
|
||||||
# 方式2:选择性回滚
|
|
||||||
# 如果只是某些表有问题,可以只恢复特定表
|
|
||||||
pg_restore -U postgres -d cosmo_db -t menus -t role_menus backup.dump
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **事务安全**:整个脚本在一个事务中执行,失败会自动回滚
|
|
||||||
2. **外键约束**:menus 表有自引用外键,脚本已处理
|
|
||||||
3. **数据清空**:menus、role_menus、celestial_events、scheduled_jobs 会被清空
|
|
||||||
4. **用户数据**:user_follows 不会被修改
|
|
||||||
5. **定时任务**:位置数据下载任务默认禁用,需要手动执行或启用
|
|
||||||
|
|
||||||
## 升级后操作
|
|
||||||
|
|
||||||
1. **重启应用服务**
|
|
||||||
```bash
|
|
||||||
# 重启后端服务
|
|
||||||
systemctl restart cosmo-backend
|
|
||||||
# 或 docker restart cosmo-backend
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **手动执行位置数据下载**(如需要)
|
|
||||||
- 登录后台管理系统
|
|
||||||
- 进入"定时任务设置"
|
|
||||||
- 找到"每日更新天体位置数据"
|
|
||||||
- 点击"立即执行"
|
|
||||||
|
|
||||||
3. **验证前端功能**
|
|
||||||
- 登录系统
|
|
||||||
- 检查菜单是否正确显示
|
|
||||||
- 测试个人资料页面
|
|
||||||
- 测试我的天体页面
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q: 升级过程中断怎么办?
|
|
||||||
A: 由于使用了事务,中断会自动回滚。使用备份重新开始。
|
|
||||||
|
|
||||||
### Q: 如何只导入某个表?
|
|
||||||
A: 从脚本中复制对应表的部分,单独执行。
|
|
||||||
|
|
||||||
### Q: 线上已有自定义菜单怎么办?
|
|
||||||
A: 脚本会清空菜单,请在升级前导出自定义菜单,升级后手动添加。
|
|
||||||
|
|
||||||
### Q: 定时任务什么时候开始执行?
|
|
||||||
A: 天体事件任务会在下个月1日凌晨3点执行。位置数据任务需手动启用或执行。
|
|
||||||
|
|
||||||
## 联系支持
|
|
||||||
|
|
||||||
如遇问题,请检查:
|
|
||||||
1. 数据库日志
|
|
||||||
2. 应用程序日志
|
|
||||||
3. 脚本执行输出
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
# 生产数据库升级指南 (最终版)
|
||||||
|
|
||||||
|
## 重要改进
|
||||||
|
|
||||||
|
本方案使用 PostgreSQL 的 `session_replication_role` 特性,**彻底解决外键约束问题**:
|
||||||
|
|
||||||
|
### 优势
|
||||||
|
- ✅ **无需关心插入顺序** - 可以任意顺序导入数据
|
||||||
|
- ✅ **大幅提升速度** - 跳过外键检查,导入速度提升数倍
|
||||||
|
- ✅ **事务安全** - 失败自动回滚,数据一致性有保障
|
||||||
|
- ✅ **自动验证** - 完成后自动检查数据完整性
|
||||||
|
|
||||||
|
### 原理
|
||||||
|
```sql
|
||||||
|
-- 1. 开启"上帝模式":暂时忽略外键和触发器
|
||||||
|
SET session_replication_role = 'replica';
|
||||||
|
|
||||||
|
-- 2. 执行所有数据操作(不受外键限制)
|
||||||
|
BEGIN;
|
||||||
|
-- 随意导入数据
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- 3. 恢复正常模式
|
||||||
|
SET session_replication_role = 'origin';
|
||||||
|
|
||||||
|
-- 4. 验证数据一致性
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 方式1:一键自动升级(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/jiliu/WorkSpace/cosmo/backend/scripts
|
||||||
|
./upgrade_final.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式2:手动执行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 备份数据库
|
||||||
|
docker exec cosmo_postgres pg_dump -U postgres -d cosmo_db > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||||
|
|
||||||
|
# 2. 执行升级(Navicat 或命令行均可)
|
||||||
|
cat upgrade_production_final.sql | docker exec -i cosmo_postgres psql -U postgres -d cosmo_db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关于 display_name 字段
|
||||||
|
|
||||||
|
脚本已适配 `roles` 表的 `display_name` 字段。如果你的生产环境没有这个字段,请:
|
||||||
|
|
||||||
|
1. 打开 `upgrade_production_final.sql`
|
||||||
|
2. 找到第 20-27 行(带 display_name 的版本)
|
||||||
|
3. 注释掉它们
|
||||||
|
4. 取消注释第 29-36 行(不带 display_name 的版本)
|
||||||
|
|
||||||
|
## 权限要求
|
||||||
|
|
||||||
|
**需要 superuser 权限**才能使用 `session_replication_role`。
|
||||||
|
|
||||||
|
如果你的数据库用户不是 superuser:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 由 superuser 执行
|
||||||
|
ALTER USER your_user WITH SUPERUSER;
|
||||||
|
|
||||||
|
-- 升级完成后可以撤销
|
||||||
|
ALTER USER your_user WITH NOSUPERUSER;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 升级内容
|
||||||
|
|
||||||
|
1. ✅ 创建/更新 roles(admin、user)
|
||||||
|
2. ✅ 添加 celestial_bodies.short_name 字段
|
||||||
|
3. ✅ 完整导入 14 条 menus 记录
|
||||||
|
4. ✅ 完整导入 16 条 role_menus 记录
|
||||||
|
5. ✅ 清空 celestial_events(由定时任务重新生成)
|
||||||
|
6. ✅ 导入 2 条 scheduled_jobs
|
||||||
|
7. ✅ 导入/更新 3 条 system_settings
|
||||||
|
8. ✅ 为现有用户分配角色
|
||||||
|
9. ✅ 自动验证数据完整性
|
||||||
|
|
||||||
|
## 数据完整性保障
|
||||||
|
|
||||||
|
脚本在恢复约束后会自动执行以下验证:
|
||||||
|
|
||||||
|
- ✅ 检查 role_menus 的 role_id 引用
|
||||||
|
- ✅ 检查 role_menus 的 menu_id 引用
|
||||||
|
- ✅ 检查 menus 的 parent_id 引用
|
||||||
|
|
||||||
|
如果发现无效引用,会输出 WARNING 信息。
|
||||||
|
|
||||||
|
## 回滚方案
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 如果升级失败,使用备份恢复
|
||||||
|
cat backup_YYYYMMDD_HHMMSS.sql | docker exec -i cosmo_postgres psql -U postgres -d cosmo_db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 为什么不迁移到 MySQL?
|
||||||
|
|
||||||
|
PostgreSQL 的优势:
|
||||||
|
- 更强的 JSON 支持(你的系统用到 JSONB)
|
||||||
|
- 更好的 GIS 扩展(PostGIS,适合天文坐标)
|
||||||
|
- 更完善的窗口函数和 CTE
|
||||||
|
- 更严格的数据完整性保障
|
||||||
|
|
||||||
|
使用 `session_replication_role` 后,外键约束不再是问题!
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么需要 superuser 权限?
|
||||||
|
A: `session_replication_role` 是为数据库复制设计的特性,PostgreSQL 限制只有 superuser 可以修改它,以防止普通用户破坏数据一致性。
|
||||||
|
|
||||||
|
### Q: 会影响生产环境吗?
|
||||||
|
A: 不会。这个设置只影响当前会话,不影响其他连接。而且在事务中执行,失败自动回滚。
|
||||||
|
|
||||||
|
### Q: 如果我没有 superuser 权限怎么办?
|
||||||
|
A:
|
||||||
|
1. 联系 DBA 临时授予权限
|
||||||
|
2. 或者使用之前的 `upgrade_production_smart.sql`(会慢一些)
|
||||||
|
|
||||||
|
### Q: 数据一致性如何保证?
|
||||||
|
A: 脚本在恢复约束后会自动验证所有外键引用。如果有问题会立即报告。
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
| 方案 | 导入速度 | 复杂度 | 推荐度 |
|
||||||
|
|------|---------|--------|--------|
|
||||||
|
| 传统方案(严格顺序) | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
|
||||||
|
| DISABLE TRIGGER | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
|
||||||
|
| **session_replication_role** | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||||
|
|
||||||
|
## 升级后操作
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 重启后端服务
|
||||||
|
docker restart cosmo-backend
|
||||||
|
|
||||||
|
# 2. 验证前端功能
|
||||||
|
# - 登录系统
|
||||||
|
# - 检查菜单显示
|
||||||
|
# - 测试用户功能
|
||||||
|
|
||||||
|
# 3. 手动执行定时任务(可选)
|
||||||
|
# 在后台管理 -> 定时任务设置 -> 立即执行
|
||||||
|
```
|
||||||
|
|
||||||
|
## 联系支持
|
||||||
|
|
||||||
|
如有问题,请检查:
|
||||||
|
1. PostgreSQL 日志: `docker logs cosmo_postgres --tail 100`
|
||||||
|
2. 用户权限: `SELECT current_user, usesuper FROM pg_user WHERE usename = current_user;`
|
||||||
|
3. 数据一致性验证输出
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- 检查 roles 表
|
||||||
|
SELECT * FROM roles ORDER BY id;
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# 生产数据库终极升级脚本
|
||||||
|
# ============================================================
|
||||||
|
# 使用 session_replication_role 绕过外键约束
|
||||||
|
# 大幅提升升级速度和成功率
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
CONTAINER="cosmo_postgres"
|
||||||
|
DB_NAME="cosmo_db"
|
||||||
|
DB_USER="postgres"
|
||||||
|
BACKUP_FILE="backup_$(date +%Y%m%d_%H%M%S).sql"
|
||||||
|
SCRIPT_FILE="upgrade_production_final.sql"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# 颜色
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_info() { echo -e "${BLUE}ℹ ${1}${NC}"; }
|
||||||
|
print_success() { echo -e "${GREEN}✅ ${1}${NC}"; }
|
||||||
|
print_warning() { echo -e "${YELLOW}⚠️ ${1}${NC}"; }
|
||||||
|
print_error() { echo -e "${RED}❌ ${1}${NC}"; }
|
||||||
|
print_step() { echo -e "${CYAN}▶ ${1}${NC}"; }
|
||||||
|
|
||||||
|
# 检查容器
|
||||||
|
check_container() {
|
||||||
|
print_step "检查 Docker 容器状态..."
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
|
||||||
|
print_error "容器 ${CONTAINER} 未运行!"
|
||||||
|
docker ps --format "table {{.Names}}\t{{.Status}}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_success "容器运行正常"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查脚本
|
||||||
|
check_script() {
|
||||||
|
print_step "检查升级脚本..."
|
||||||
|
if [ ! -f "${SCRIPT_DIR}/${SCRIPT_FILE}" ]; then
|
||||||
|
print_error "找不到 ${SCRIPT_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_success "脚本就绪"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查权限
|
||||||
|
check_permissions() {
|
||||||
|
print_step "检查数据库权限..."
|
||||||
|
|
||||||
|
SUPERUSER=$(docker exec ${CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -t -c \
|
||||||
|
"SELECT usesuper FROM pg_user WHERE usename = current_user;" | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$SUPERUSER" != "t" ]; then
|
||||||
|
print_error "用户 ${DB_USER} 不是 superuser!"
|
||||||
|
echo ""
|
||||||
|
print_warning "session_replication_role 需要 superuser 权限"
|
||||||
|
echo "解决方案:"
|
||||||
|
echo " 1. 使用 superuser 账号执行升级"
|
||||||
|
echo " 2. 或临时授予权限: ALTER USER ${DB_USER} WITH SUPERUSER;"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "权限检查通过 (superuser)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 display_name 字段
|
||||||
|
check_display_name() {
|
||||||
|
print_step "检查 roles 表结构..."
|
||||||
|
|
||||||
|
HAS_DISPLAY_NAME=$(docker exec ${CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -t -c \
|
||||||
|
"SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_name = 'roles' AND column_name = 'display_name';" | tr -d ' ')
|
||||||
|
|
||||||
|
if [ "$HAS_DISPLAY_NAME" = "1" ]; then
|
||||||
|
print_info "检测到 display_name 字段(将使用对应版本)"
|
||||||
|
echo ""
|
||||||
|
print_warning "请确认 upgrade_production_final.sql 中:"
|
||||||
|
echo " - 第 20-27 行(带 display_name)未注释"
|
||||||
|
echo " - 第 29-36 行(不带 display_name)已注释"
|
||||||
|
echo ""
|
||||||
|
read -p "是否确认脚本已正确配置? (yes/no): " confirm
|
||||||
|
if [ "$confirm" != "yes" ]; then
|
||||||
|
print_info "升级已取消,请检查脚本配置"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_info "未检测到 display_name 字段"
|
||||||
|
echo ""
|
||||||
|
print_warning "请确认 upgrade_production_final.sql 中:"
|
||||||
|
echo " - 第 20-27 行(带 display_name)已注释"
|
||||||
|
echo " - 第 29-36 行(不带 display_name)未注释"
|
||||||
|
echo ""
|
||||||
|
read -p "是否确认脚本已正确配置? (yes/no): " confirm
|
||||||
|
if [ "$confirm" != "yes" ]; then
|
||||||
|
print_info "升级已取消,请检查脚本配置"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 备份数据库
|
||||||
|
backup_database() {
|
||||||
|
print_step "备份数据库..."
|
||||||
|
if docker exec ${CONTAINER} pg_dump -U ${DB_USER} -d ${DB_NAME} > "${SCRIPT_DIR}/${BACKUP_FILE}"; then
|
||||||
|
SIZE=$(ls -lh "${SCRIPT_DIR}/${BACKUP_FILE}" | awk '{print $5}')
|
||||||
|
print_success "备份完成: ${BACKUP_FILE} (${SIZE})"
|
||||||
|
else
|
||||||
|
print_error "备份失败!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行升级
|
||||||
|
execute_upgrade() {
|
||||||
|
print_step "执行数据库升级..."
|
||||||
|
echo "========================================================"
|
||||||
|
|
||||||
|
if cat "${SCRIPT_DIR}/${SCRIPT_FILE}" | docker exec -i ${CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}; then
|
||||||
|
echo "========================================================"
|
||||||
|
print_success "升级执行完成!"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "========================================================"
|
||||||
|
print_error "升级失败!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示验证结果
|
||||||
|
show_verification() {
|
||||||
|
print_step "数据验证..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
docker exec ${CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c "
|
||||||
|
SELECT
|
||||||
|
'celestial_bodies.short_name' as item,
|
||||||
|
CASE WHEN EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name='celestial_bodies' AND column_name='short_name'
|
||||||
|
) THEN '✓ 存在' ELSE '✗ 缺失' END as status
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'roles',
|
||||||
|
COUNT(*)::text || ' 条记录'
|
||||||
|
FROM roles
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'menus',
|
||||||
|
COUNT(*)::text || ' 条记录'
|
||||||
|
FROM menus
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'role_menus',
|
||||||
|
COUNT(*)::text || ' 条记录'
|
||||||
|
FROM role_menus
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'scheduled_jobs',
|
||||||
|
COUNT(*)::text || ' 条记录'
|
||||||
|
FROM scheduled_jobs
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'system_settings',
|
||||||
|
COUNT(*)::text || ' 条记录'
|
||||||
|
FROM system_settings;
|
||||||
|
" -t
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示回滚信息
|
||||||
|
show_rollback_info() {
|
||||||
|
echo ""
|
||||||
|
print_warning "如需回滚,执行:"
|
||||||
|
echo "cat ${SCRIPT_DIR}/${BACKUP_FILE} | docker exec -i ${CONTAINER} psql -U ${DB_USER} -d ${DB_NAME}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
echo "============================================================"
|
||||||
|
echo " 生产数据库终极升级脚本"
|
||||||
|
echo " 使用 session_replication_role 技术"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查
|
||||||
|
check_container
|
||||||
|
check_script
|
||||||
|
check_permissions
|
||||||
|
check_display_name
|
||||||
|
|
||||||
|
# 确认
|
||||||
|
echo ""
|
||||||
|
print_warning "即将执行以下操作:"
|
||||||
|
echo " 1. 备份当前数据库"
|
||||||
|
echo " 2. 使用 replica 模式绕过外键约束"
|
||||||
|
echo " 3. 导入所有数据(无需关心顺序)"
|
||||||
|
echo " 4. 恢复正常模式并验证数据完整性"
|
||||||
|
echo ""
|
||||||
|
echo "受影响的表:"
|
||||||
|
echo " • celestial_bodies - 添加 short_name 字段"
|
||||||
|
echo " • roles - 创建/更新记录"
|
||||||
|
echo " • menus - 清空并重新导入 (14条)"
|
||||||
|
echo " • role_menus - 清空并重新导入 (16条)"
|
||||||
|
echo " • celestial_events - 清空"
|
||||||
|
echo " • scheduled_jobs - 清空并重新导入 (2条)"
|
||||||
|
echo " • system_settings - 导入/更新 (3条)"
|
||||||
|
echo " • user_roles - 为现有用户分配角色"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "是否继续? (yes/no): " confirm
|
||||||
|
if [ "$confirm" != "yes" ]; then
|
||||||
|
print_info "升级已取消"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 执行
|
||||||
|
echo ""
|
||||||
|
backup_database
|
||||||
|
|
||||||
|
if execute_upgrade; then
|
||||||
|
show_verification
|
||||||
|
print_success "🎉 数据库升级成功!"
|
||||||
|
show_rollback_info
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_info "后续步骤:"
|
||||||
|
echo " 1. 重启后端服务: docker restart cosmo-backend"
|
||||||
|
echo " 2. 登录系统验证菜单显示"
|
||||||
|
echo " 3. 测试用户功能"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
print_error "升级失败(已自动回滚)"
|
||||||
|
show_rollback_info
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
|
|
@ -1,19 +1,49 @@
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Production Database Upgrade Script
|
-- Production Database Upgrade Script (Final Version)
|
||||||
|
-- ============================================================
|
||||||
|
-- 使用 session_replication_role 方法绕过外键约束检查
|
||||||
|
-- 这是数据迁移的最佳实践,显著提升升级效率
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- This script upgrades the production database with the following changes:
|
|
||||||
-- 1. Add short_name to celestial_bodies
|
|
||||||
-- 2. Import menus and role_menus
|
|
||||||
-- 3. Import celestial_events
|
|
||||||
-- 4. Import scheduled_jobs
|
|
||||||
-- 5. Import system_settings
|
|
||||||
-- 6. Import user_follows
|
|
||||||
--
|
--
|
||||||
-- IMPORTANT: Run this script in a transaction and test on a backup first!
|
-- 优势:
|
||||||
|
-- 1. 无需关心插入顺序
|
||||||
|
-- 2. 大幅提升导入速度
|
||||||
|
-- 3. 事务安全,失败自动回滚
|
||||||
|
--
|
||||||
|
-- 注意:需要 superuser 权限
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 开启"上帝模式":忽略外键约束和触发器
|
||||||
|
SET session_replication_role = 'replica';
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 0. Ensure roles exist (适配 display_name 字段)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 方式1: 如果 roles 表有 display_name 字段,使用这个
|
||||||
|
INSERT INTO roles (id, name, display_name, description, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 'admin', '管理员', '管理员角色,拥有所有权限', NOW(), NOW()),
|
||||||
|
(2, 'user', '普通用户', '普通用户角色,只能访问基本功能', NOW(), NOW())
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
display_name = EXCLUDED.display_name,
|
||||||
|
description = EXCLUDED.description,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
-- 方式2: 如果没有 display_name 字段,注释掉上面的,使用下面的
|
||||||
|
-- INSERT INTO roles (id, name, description, created_at, updated_at)
|
||||||
|
-- VALUES
|
||||||
|
-- (1, 'admin', '管理员角色,拥有所有权限', NOW(), NOW()),
|
||||||
|
-- (2, 'user', '普通用户角色,只能访问基本功能', NOW(), NOW())
|
||||||
|
-- ON CONFLICT (id) DO UPDATE SET
|
||||||
|
-- description = EXCLUDED.description,
|
||||||
|
-- updated_at = NOW();
|
||||||
|
|
||||||
|
-- Reset sequence for roles
|
||||||
|
SELECT setval('roles_id_seq', (SELECT COALESCE(MAX(id), 0) FROM roles));
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 1. Add short_name column to celestial_bodies
|
-- 1. Add short_name column to celestial_bodies
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
@ -35,14 +65,10 @@ END $$;
|
||||||
-- 2. Import menus and role_menus
|
-- 2. Import menus and role_menus
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
-- Clear existing menus (will cascade to role_menus due to foreign key)
|
-- 清空现有数据(因为禁用了约束,可以直接 TRUNCATE)
|
||||||
TRUNCATE TABLE menus CASCADE;
|
TRUNCATE TABLE menus CASCADE;
|
||||||
RAISE NOTICE 'Cleared existing menus and role_menus';
|
|
||||||
|
|
||||||
-- Disable triggers temporarily to handle circular foreign keys
|
-- 插入菜单数据(无需关心父子顺序!)
|
||||||
ALTER TABLE menus DISABLE TRIGGER ALL;
|
|
||||||
|
|
||||||
-- Insert menus (parent menus first, then child menus)
|
|
||||||
INSERT INTO menus (id, parent_id, name, title, icon, path, component, sort_order, is_active, description, created_at, updated_at) VALUES
|
INSERT INTO menus (id, parent_id, name, title, icon, path, component, sort_order, is_active, description, created_at, updated_at) VALUES
|
||||||
(1, NULL, 'dashboard', '控制台', 'dashboard', '/admin/dashboard', 'admin/Dashboard', 1, true, '系统控制台', '2025-11-28 18:07:11.767382', '2025-11-28 18:07:11.767382'),
|
(1, NULL, 'dashboard', '控制台', 'dashboard', '/admin/dashboard', 'admin/Dashboard', 1, true, '系统控制台', '2025-11-28 18:07:11.767382', '2025-11-28 18:07:11.767382'),
|
||||||
(2, NULL, 'data_management', '数据管理', 'database', '', '', 2, true, '数据管理模块', '2025-11-28 18:07:11.767382', '2025-11-28 18:07:11.767382'),
|
(2, NULL, 'data_management', '数据管理', 'database', '', '', 2, true, '数据管理模块', '2025-11-28 18:07:11.767382', '2025-11-28 18:07:11.767382'),
|
||||||
|
|
@ -59,48 +85,39 @@ INSERT INTO menus (id, parent_id, name, title, icon, path, component, sort_order
|
||||||
(12, 6, 'scheduled_jobs', '定时任务设置', 'ClockCircleOutlined', '/admin/scheduled-jobs', 'admin/ScheduledJobs', 5, true, '管理系统定时任务及脚本', '2025-12-10 17:42:38.031518', '2025-12-10 17:42:38.031518'),
|
(12, 6, 'scheduled_jobs', '定时任务设置', 'ClockCircleOutlined', '/admin/scheduled-jobs', 'admin/ScheduledJobs', 5, true, '管理系统定时任务及脚本', '2025-12-10 17:42:38.031518', '2025-12-10 17:42:38.031518'),
|
||||||
(10, 6, 'system_tasks', '系统任务监控', 'schedule', '/admin/tasks', 'admin/Tasks', 30, true, '', '2025-11-30 16:04:59.572869', '2025-11-30 16:04:59.572869');
|
(10, 6, 'system_tasks', '系统任务监控', 'schedule', '/admin/tasks', 'admin/Tasks', 30, true, '', '2025-11-30 16:04:59.572869', '2025-11-30 16:04:59.572869');
|
||||||
|
|
||||||
-- Re-enable triggers
|
|
||||||
ALTER TABLE menus ENABLE TRIGGER ALL;
|
|
||||||
RAISE NOTICE 'Imported menus data';
|
|
||||||
|
|
||||||
-- Reset sequence for menus
|
-- Reset sequence for menus
|
||||||
SELECT setval('menus_id_seq', (SELECT MAX(id) FROM menus));
|
SELECT setval('menus_id_seq', (SELECT MAX(id) FROM menus));
|
||||||
|
|
||||||
-- Insert role_menus
|
-- 插入 role_menus(无需担心 roles 是否存在!)
|
||||||
INSERT INTO role_menus (role_id, menu_id) VALUES
|
INSERT INTO role_menus (role_id, menu_id) VALUES
|
||||||
-- Admin role (role_id = 1) has access to all menus
|
-- Admin role (role_id = 1) has access to all menus
|
||||||
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 10), (1, 11), (1, 12), (1, 13), (1, 14), (1, 15),
|
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 10), (1, 11), (1, 12), (1, 13), (1, 14), (1, 15),
|
||||||
-- User role (role_id = 2) has access to user menus only
|
-- User role (role_id = 2) has access to user menus only
|
||||||
(2, 14), (2, 15);
|
(2, 14), (2, 15);
|
||||||
|
|
||||||
RAISE NOTICE 'Imported role_menus data';
|
-- ============================================================
|
||||||
|
-- 3. Import celestial_events
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 3. Import celestial_events (will be truncated and re-imported)
|
|
||||||
-- ============================================================
|
|
||||||
TRUNCATE TABLE celestial_events;
|
TRUNCATE TABLE celestial_events;
|
||||||
RAISE NOTICE 'Cleared existing celestial_events (data will be regenerated by scheduled jobs)';
|
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 4. Import scheduled_jobs
|
-- 4. Import scheduled_jobs
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Clear existing scheduled_jobs
|
|
||||||
TRUNCATE TABLE scheduled_jobs CASCADE;
|
|
||||||
RAISE NOTICE 'Cleared existing scheduled_jobs';
|
|
||||||
|
|
||||||
-- Insert scheduled_jobs
|
TRUNCATE TABLE scheduled_jobs CASCADE;
|
||||||
|
|
||||||
INSERT INTO scheduled_jobs (id, name, cron_expression, python_code, is_active, last_run_at, last_run_status, next_run_at, description, created_at, updated_at, job_type, predefined_function, function_params) VALUES
|
INSERT INTO scheduled_jobs (id, name, cron_expression, python_code, is_active, last_run_at, last_run_status, next_run_at, description, created_at, updated_at, job_type, predefined_function, function_params) VALUES
|
||||||
(1, '每日更新天体位置数据', '0 2 * * *', NULL, false, NULL, NULL, NULL, '每天凌晨2点自动从NASA Horizons下载主要天体的位置数据', '2025-12-10 17:43:01.234567', '2025-12-10 17:43:01.234567', 'predefined', 'download_positions_task', '{"body_ids": ["10", "199", "299", "399", "301", "499", "599", "699", "799", "899"], "days_range": "3"}'),
|
(1, '每日更新天体位置数据', '0 2 * * *', NULL, false, NULL, NULL, NULL, '每天凌晨2点自动从NASA Horizons下载主要天体的位置数据', '2025-12-10 17:43:01.234567', '2025-12-10 17:43:01.234567', 'predefined', 'download_positions_task', '{"body_ids": ["10", "199", "299", "399", "301", "499", "599", "699", "799", "899"], "days_range": "3"}'),
|
||||||
(2, '获取主要天体的食、合、冲等事件', '0 3 1 * *', NULL, true, NULL, NULL, NULL, '每月1日凌晨3点计算未来一年的主要天文事件', '2025-12-10 17:43:01.234567', '2025-12-10 17:43:01.234567', 'predefined', 'calculate_planetary_events', '{"body_ids": ["199", "299", "499", "599", "699", "799", "899"], "days_ahead": "365", "clean_old_events": true, "threshold_degrees": "5", "calculate_close_approaches": true}');
|
(2, '获取主要天体的食、合、冲等事件', '0 3 1 * *', NULL, true, NULL, NULL, NULL, '每月1日凌晨3点计算未来一年的主要天文事件', '2025-12-10 17:43:01.234567', '2025-12-10 17:43:01.234567', 'predefined', 'calculate_planetary_events', '{"body_ids": ["199", "299", "499", "599", "699", "799", "899"], "days_ahead": "365", "clean_old_events": true, "threshold_degrees": "5", "calculate_close_approaches": true}');
|
||||||
|
|
||||||
-- Reset sequence
|
-- Reset sequence
|
||||||
SELECT setval('scheduled_jobs_id_seq', (SELECT MAX(id) FROM scheduled_jobs));
|
SELECT setval('scheduled_jobs_id_seq', (SELECT MAX(id) FROM scheduled_jobs));
|
||||||
RAISE NOTICE 'Imported scheduled_jobs data';
|
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 5. Import system_settings
|
-- 5. Import system_settings
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Use INSERT ... ON CONFLICT to avoid duplicates
|
|
||||||
INSERT INTO system_settings (key, value, value_type, category, label, description, is_public, created_at, updated_at) VALUES
|
INSERT INTO system_settings (key, value, value_type, category, label, description, is_public, created_at, updated_at) VALUES
|
||||||
('view_mode', 'solar', 'string', 'ui', '默认视图模式', '系统默认的3D场景视图模式(solar或galaxy)', true, NOW(), NOW()),
|
('view_mode', 'solar', 'string', 'ui', '默认视图模式', '系统默认的3D场景视图模式(solar或galaxy)', true, NOW(), NOW()),
|
||||||
('nasa_api_timeout', '120', 'int', 'api', 'NASA API超时时间', 'NASA Horizons API请求超时时间(秒)', false, NOW(), NOW()),
|
('nasa_api_timeout', '120', 'int', 'api', 'NASA API超时时间', 'NASA Horizons API请求超时时间(秒)', false, NOW(), NOW()),
|
||||||
|
|
@ -114,31 +131,107 @@ ON CONFLICT (key) DO UPDATE SET
|
||||||
is_public = EXCLUDED.is_public,
|
is_public = EXCLUDED.is_public,
|
||||||
updated_at = NOW();
|
updated_at = NOW();
|
||||||
|
|
||||||
RAISE NOTICE 'Imported/updated system_settings data';
|
-- ============================================================
|
||||||
|
-- 6. Ensure existing users have roles assigned
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
user_record RECORD;
|
||||||
|
user_role_id INTEGER := 2; -- user role
|
||||||
|
BEGIN
|
||||||
|
FOR user_record IN SELECT id FROM users LOOP
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM user_roles WHERE user_id = user_record.id
|
||||||
|
) THEN
|
||||||
|
INSERT INTO user_roles (user_id, role_id)
|
||||||
|
VALUES (user_record.id, user_role_id);
|
||||||
|
RAISE NOTICE 'Assigned user role to user %', user_record.id;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 6. Import user_follows (keep existing data, don't truncate)
|
-- 提交事务
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Note: user_follows should retain existing production data
|
|
||||||
-- This section is intentionally left empty to preserve user data
|
|
||||||
RAISE NOTICE 'Skipped user_follows import (preserving existing user data)';
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Commit transaction
|
|
||||||
-- ============================================================
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 恢复正常模式(关键步骤!)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SET session_replication_role = 'origin';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 数据一致性验证(在恢复约束后执行)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 验证外键一致性
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
invalid_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- 检查 role_menus 中是否有无效的 role_id
|
||||||
|
SELECT COUNT(*) INTO invalid_count
|
||||||
|
FROM role_menus rm
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM roles r WHERE r.id = rm.role_id);
|
||||||
|
|
||||||
|
IF invalid_count > 0 THEN
|
||||||
|
RAISE WARNING 'Found % invalid role_id references in role_menus', invalid_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查 role_menus 中是否有无效的 menu_id
|
||||||
|
SELECT COUNT(*) INTO invalid_count
|
||||||
|
FROM role_menus rm
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM menus m WHERE m.id = rm.menu_id);
|
||||||
|
|
||||||
|
IF invalid_count > 0 THEN
|
||||||
|
RAISE WARNING 'Found % invalid menu_id references in role_menus', invalid_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查 menus 中是否有无效的 parent_id
|
||||||
|
SELECT COUNT(*) INTO invalid_count
|
||||||
|
FROM menus m1
|
||||||
|
WHERE m1.parent_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM menus m2 WHERE m2.id = m1.parent_id);
|
||||||
|
|
||||||
|
IF invalid_count > 0 THEN
|
||||||
|
RAISE WARNING 'Found % invalid parent_id references in menus', invalid_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Data integrity validation completed';
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- Verification queries
|
-- Verification queries
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
\echo '============================================================'
|
|
||||||
\echo 'Upgrade completed successfully!'
|
-- Check roles
|
||||||
\echo '============================================================'
|
SELECT id, name, description FROM roles ORDER BY id;
|
||||||
\echo 'Verification:'
|
|
||||||
SELECT 'celestial_bodies.short_name exists:' as check,
|
-- Check if short_name column exists
|
||||||
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='celestial_bodies' AND column_name='short_name') as result;
|
SELECT 'celestial_bodies.short_name' as "Item",
|
||||||
SELECT 'menus count:' as check, COUNT(*) as result FROM menus;
|
CASE WHEN EXISTS(
|
||||||
SELECT 'role_menus count:' as check, COUNT(*) as result FROM role_menus;
|
SELECT 1 FROM information_schema.columns
|
||||||
SELECT 'scheduled_jobs count:' as check, COUNT(*) as result FROM scheduled_jobs;
|
WHERE table_name='celestial_bodies' AND column_name='short_name'
|
||||||
SELECT 'system_settings count:' as check, COUNT(*) as result FROM system_settings;
|
) THEN '✓ EXISTS' ELSE '✗ MISSING' END as "Status";
|
||||||
\echo '============================================================'
|
|
||||||
|
-- Check record counts
|
||||||
|
SELECT 'roles' as "Table", COUNT(*)::text || ' records' as "Count" FROM roles
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'menus', COUNT(*)::text || ' records' FROM menus
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'role_menus', COUNT(*)::text || ' records' FROM role_menus
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'scheduled_jobs', COUNT(*)::text || ' records' FROM scheduled_jobs
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'system_settings', COUNT(*)::text || ' records' FROM system_settings;
|
||||||
|
|
||||||
|
-- Check user role assignments
|
||||||
|
SELECT u.id, u.username, COALESCE(array_agg(r.name), ARRAY[]::varchar[]) as roles
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
||||||
|
LEFT JOIN roles r ON ur.role_id = r.id
|
||||||
|
GROUP BY u.id, u.username
|
||||||
|
ORDER BY u.id;
|
||||||
Loading…
Reference in New Issue