From 6467e67bdb8d95a5cf245aba7d132f41ec1b2fa9 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Mon, 6 Jul 2026 10:27:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20legacy=20database=20migration=20?= =?UTF-8?q?=E2=80=94=20import=20old=20dump,=20map=20to=20new=20schema,=20d?= =?UTF-8?q?eploy=20to=20Tencent=20Cloud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy.sh | 18 ++--- migrate-legacy.sql | 168 +++++++++++++++++++++++++++++++++++++++++++++ migrate.sh | 37 ++++++++++ 3 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 migrate-legacy.sql create mode 100755 migrate.sh diff --git a/deploy.sh b/deploy.sh index 964ddc2..ca68cc8 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash # 恭学教育 — PM2 部署脚本 # 用法: ./deploy.sh [ssh_host] -# ssh_host: SSH 主机别名,默认 tencent set -euo pipefail SSH_HOST="${1:-tencent}" @@ -23,18 +22,16 @@ rsync -avz --delete \ --exclude='.turbo/' \ ./ "${SSH_HOST}:${REMOTE_DIR}/" -echo "=== 4/5 启动 MySQL ===" +echo "=== 4/5 启动 MySQL + 安装依赖 ===" ssh "${SSH_HOST}" " cd ${REMOTE_DIR} docker compose up -d mysql mkdir -p logs - # 等 MySQL 就绪 for i in \$(seq 1 30); do docker compose exec -T mysql mysqladmin ping -h localhost --silent 2>/dev/null && break echo '等待 MySQL...' sleep 2 done - # 首次部署需安装依赖,后续跳过 if [ ! -d node_modules ]; then echo '首次部署,安装依赖...' npm ci --omit=dev @@ -53,7 +50,12 @@ echo "" echo "部署完成!" echo "访问: http://$(ssh "${SSH_HOST}" 'hostname -I 2>/dev/null | awk "{print \$1}" || curl -s ifconfig.me')" echo "" -echo "首次部署还需在服务器执行:" -echo " ssh ${SSH_HOST} && cd ${REMOTE_DIR}" -echo " cp .env.example .env && vim .env # 修改密码和 JWT_SECRET" -echo " sudo setcap 'cap_net_bind_service=+ep' \$(which node) # PM2 绑 80 端口" +echo "══════ 首次部署/数据迁移 ══════" +echo "1) 上传旧数据库:" +echo " scp ~/Downloads/dorm_billing_*.sql.gz ${SSH_HOST}:${REMOTE_DIR}/" +echo "2) 服务器上执行:" +echo " ssh ${SSH_HOST}" +echo " cd ${REMOTE_DIR}" +echo " cp .env.example .env && vim .env # 改密码" +echo " sudo setcap 'cap_net_bind_service=+ep' \$(which node)" +echo " ./migrate.sh dorm_billing_*.sql.gz # 迁移旧数据" diff --git a/migrate-legacy.sql b/migrate-legacy.sql new file mode 100644 index 0000000..dcad5b5 --- /dev/null +++ b/migrate-legacy.sql @@ -0,0 +1,168 @@ +-- ============================================================ +-- 恭学教育 — 旧库 dorm_billing → 新库 gongxue 数据迁移 +-- +-- 用法(在服务器上): +-- 1. 导入旧 dump: +-- mysql -h 127.0.0.1 -u root -p${MYSQL_ROOT_PASSWORD} dorm_billing < dump.sql +-- 2. 启动新应用一次(创建新表,然后立即停掉) +-- 3. 执行迁移: +-- mysql -h 127.0.0.1 -u root -p${MYSQL_ROOT_PASSWORD} < migrate-legacy.sql +-- 4. PM2 重启应用 +-- ============================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- ── 1. organizations → tenants ── +INSERT IGNORE INTO gongxue.tenants (id, name, contact, phone, color, notes, status, created_at, updated_at) +SELECT id, name, '', '', '#007AFF', '', 'active', created_at, updated_at +FROM dorm_billing.organizations; + +-- ── 2. departments → departments ── +INSERT IGNORE INTO gongxue.departments (id, name, type, parent_id, sort_order, created_at) +SELECT id, name, type, parent_id, sort_order, created_at +FROM dorm_billing.departments; + +-- ── 3. rooms → rooms ── +INSERT IGNORE INTO gongxue.rooms (id, room_number, building, floor, capacity, status, + room_type, gender, rental_category, monthly_rate, department_id, created_at) +SELECT + r.id, r.room_number, + COALESCE(NULLIF(r.building, ''), 'A栋'), + r.floor, r.capacity, + CASE WHEN r.status = 'archived' THEN 'archived' ELSE 'available' END, + NULLIF(r.room_type, ''), NULLIF(r.gender, ''), + 'short', 0, + (SELECT id FROM dorm_billing.departments WHERE name = '天津校区' LIMIT 1), + r.created_at +FROM dorm_billing.rooms r; + +-- ── 4. students → students ── +INSERT INTO gongxue.students (id, name, phone, id_number, student_no, gender, ethnicity, + emergency_contact, emergency_phone, supervisor, status, tenant_id, department_id, + created_at, updated_at) +SELECT + s.id, s.name, + NULLIF(s.phone, ''), + COALESCE(NULLIF(s.id_number, ''), NULLIF(s.id_card, '')), + NULLIF(s.student_no, ''), + NULLIF(s.gender, ''), + NULLIF(s.ethnicity, ''), + NULLIF(s.emergency_contact, ''), + NULLIF(s.emergency_phone, ''), + NULLIF(s.supervisor, ''), + CASE s.status + WHEN 'inactive' THEN 'inactive' + WHEN 'graduated' THEN 'graduated' + WHEN 'archived' THEN 'graduated' + ELSE 'active' + END, + (SELECT t.id FROM gongxue.tenants t WHERE t.name = s.organization LIMIT 1), + s.department_id, + s.created_at, s.updated_at +FROM dorm_billing.students s; + +-- ── 5. occupancies → occupancies ── +-- 仅迁移有对应 room 和 student 的记录 +INSERT INTO gongxue.occupancies (id, student_id, room_id, check_in_date, check_out_date, + billing_start_date, billing_end_date, check_out_reason, notes, + rental_type, tenant_id, department_id, created_at) +SELECT + o.id, o.student_id, o.room_id, + o.check_in_date, o.check_out_date, + o.billing_start_date, o.billing_end_date, + o.check_out_reason, o.notes, + 'short', NULL, + (SELECT department_id FROM gongxue.students WHERE id = o.student_id LIMIT 1), + o.created_at +FROM dorm_billing.occupancies o +WHERE EXISTS (SELECT 1 FROM gongxue.students WHERE id = o.student_id) + AND EXISTS (SELECT 1 FROM gongxue.rooms WHERE id = o.room_id AND status != 'archived'); + +-- ── 6. room_expenses → room_expenses ── +INSERT INTO gongxue.room_expenses (id, room_id, expense_type, amount, period_start, period_end, + description, recorded_by, department_id, created_at) +SELECT + re.id, re.room_id, re.expense_type, re.amount, re.period_start, re.period_end, + re.description, re.recorded_by, + (SELECT department_id FROM gongxue.rooms WHERE id = re.room_id LIMIT 1), + re.created_at +FROM dorm_billing.room_expenses re +WHERE EXISTS (SELECT 1 FROM gongxue.rooms WHERE id = re.room_id); + +-- ── 7. bills → bills ── +INSERT INTO gongxue.bills (id, student_id, period_start, period_end, + shared_amount, personal_amount, total_amount, status, department_id, generated_at) +SELECT + b.id, b.student_id, b.period_start, b.period_end, + b.shared_amount, b.personal_amount, b.total_amount, b.status, + (SELECT department_id FROM gongxue.students WHERE id = b.student_id LIMIT 1), + b.generated_at +FROM dorm_billing.bills b +WHERE EXISTS (SELECT 1 FROM gongxue.students WHERE id = b.student_id); + +-- ── 8. bill_items → bill_items ── +INSERT INTO gongxue.bill_items (id, bill_id, room_id, expense_type, description, + days, total_room_days, room_total_amount, student_amount) +SELECT + bi.id, bi.bill_id, bi.room_id, bi.expense_type, bi.description, + bi.days, bi.total_room_days, bi.room_total_amount, bi.student_amount +FROM dorm_billing.bill_items bi +WHERE EXISTS (SELECT 1 FROM gongxue.bills WHERE id = bi.bill_id); + +-- ── 9. deposits → deposits ── +INSERT INTO gongxue.deposits (id, student_id, amount, status, paid_date, + refund_date, refund_amount, deduction_amount, deduction_reason, notes, recorded_by, + department_id, created_at) +SELECT + d.id, d.student_id, d.amount, d.status, d.paid_date, + d.refund_date, d.refund_amount, d.deduction_amount, d.deduction_reason, d.notes, d.recorded_by, + (SELECT department_id FROM gongxue.students WHERE id = d.student_id LIMIT 1), + d.created_at +FROM dorm_billing.deposits d +WHERE EXISTS (SELECT 1 FROM gongxue.students WHERE id = d.student_id); + +-- ── 10. classrooms → classrooms ── +INSERT INTO gongxue.classrooms (id, name, building, floor, capacity, room_type, + course_type, supervisor, status, notes, department_id, created_at) +SELECT + id, name, building, floor, capacity, room_type, + course_type, supervisor, status, notes, + (SELECT id FROM gongxue.departments WHERE name = '天津校区' LIMIT 1), + created_at +FROM dorm_billing.classrooms; + +-- ── 11. classroom_rentals → classroom_rentals ── +INSERT INTO gongxue.classroom_rentals (id, classroom_id, tenant_id, start_date, end_date, + contract_path, contract_original_name, daily_rate, total_amount, status, notes, + created_by, department_id, created_at, updated_at) +SELECT + cr.id, cr.classroom_id, cr.tenant_id, cr.start_date, cr.end_date, + cr.contract_path, cr.contract_original_name, cr.daily_rate, cr.total_amount, cr.status, cr.notes, + cr.created_by, + (SELECT id FROM gongxue.departments WHERE name = '天津校区' LIMIT 1), + cr.created_at, cr.updated_at +FROM dorm_billing.classroom_rentals cr; + +-- ── 12. operation_logs → operation_logs ── +INSERT INTO gongxue.operation_logs (id, user_id, username, module, action, + target_id, target_type, detail, ip_address, user_agent, status, created_at) +SELECT + ol.id, ol.user_id, ol.username, ol.module, ol.action, + ol.target_id, ol.target_type, ol.detail, ol.ip_address, ol.user_agent, ol.status, ol.created_at +FROM dorm_billing.operation_logs ol; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ── 统计 ── +SELECT 'Migration complete!' AS status; +SELECT 'students' AS tbl, COUNT(*) AS cnt FROM gongxue.students +UNION ALL SELECT 'occupancies', COUNT(*) FROM gongxue.occupancies +UNION ALL SELECT 'rooms', COUNT(*) FROM gongxue.rooms +UNION ALL SELECT 'bills', COUNT(*) FROM gongxue.bills +UNION ALL SELECT 'bill_items', COUNT(*) FROM gongxue.bill_items +UNION ALL SELECT 'deposits', COUNT(*) FROM gongxue.deposits +UNION ALL SELECT 'departments', COUNT(*) FROM gongxue.departments +UNION ALL SELECT 'tenants', COUNT(*) FROM gongxue.tenants +UNION ALL SELECT 'room_expenses', COUNT(*) FROM gongxue.room_expenses +UNION ALL SELECT 'classrooms', COUNT(*) FROM gongxue.classrooms +UNION ALL SELECT 'classroom_rentals', COUNT(*) FROM gongxue.classroom_rentals; diff --git a/migrate.sh b/migrate.sh new file mode 100755 index 0000000..6d303cc --- /dev/null +++ b/migrate.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# 恭学教育 — 数据迁移脚本(仅首次运行) +# 用法: +# 1. 确认 MySQL 已启动:docker compose up -d mysql +# 2. ./migrate.sh /path/to/dump.sql.gz +set -euo pipefail + +DUMP="${1:-}" +if [ -z "$DUMP" ]; then + echo "用法: ./migrate.sh /path/to/dump.sql.gz" + echo "示例: ./migrate.sh ~/Downloads/dorm_billing_2026-07-06_01-30-02_mysql_data.sql.gz" + exit 1 +fi + +# 从 .env 读取密码 +MYSQL_PASS="${MYSQL_ROOT_PASSWORD:-gongxue_2024}" + +echo "=== 1/4 导入旧 dump ===" +gunzip -c "$DUMP" | docker compose exec -T mysql mysql -u root -p"${MYSQL_PASS}" dorm_billing +echo "旧库导入完成: $(docker compose exec -T mysql mysql -u root -p"${MYSQL_PASS}" -e 'SELECT COUNT(*) AS cnt FROM dorm_billing.students' -N)" + +echo "=== 2/4 启动应用(创建新表结构) ===" +# 短暂启动让 TypeORM synchronize 创建表,然后停掉 +DB_SYNCHRONIZE=true pm2 start ecosystem.config.cjs --only gongxue-backend +sleep 8 +pm2 stop gongxue-backend + +echo "=== 3/4 执行数据迁移 ===" +docker compose exec -T mysql mysql -u root -p"${MYSQL_PASS}" < migrate-legacy.sql + +echo "=== 4/4 重启应用 ===" +pm2 startOrReload ecosystem.config.cjs --update-env +pm2 save + +echo "" +echo "迁移完成!运行以下验证:" +echo " docker compose exec mysql mysql -u root -p${MYSQL_PASS} gongxue -e 'SELECT COUNT(*) FROM students'"