""" gvsdsdk User 表迁移脚本 将 user_main 数据迁移到 gvsdsdk.User + user_sensitive_data 执行前确保已全量备份数据库! 分阶段执行,每阶段验证后才继续下一阶段。 """ import uuid import sys import MySQLdb # ==================== 配置 ==================== DB_CONFIG = { 'host': 'gvsds.com', 'port': 50030, 'user': 'root', 'passwd': 'sajksh.sdfGH3YUge.wjkd+', 'db': 'xaio_cheng_xu', 'charset': 'utf8mb4', 'connect_timeout': 10, } # UUID 命名空间(固定值,确保可重现) UUID_NAMESPACE = uuid.UUID('6e6d6163-0000-0000-0000-000000000000') # 旧 RBAC 表 → 重命名映射 RBAC_RENAME_MAP = { 'role': 'legacy_role', 'user_role': 'legacy_user_role', 'permission': 'legacy_permission', 'role_permission': 'legacy_role_permission', 'account_permission': 'legacy_account_permission', } # 扩展表 + admin_profile(有 FK 约束指向 user_main.id) EXT_TABLES_WITH_FK = [ 'user_boss', 'user_dashou', 'user_guanshi', 'user_kefu', 'user_shangjia', 'user_shenheguan', 'user_zuzhang', 'admin_profile', ] def get_conn(): return MySQLdb.connect(**DB_CONFIG) def table_exists(cur, table_name): cur.execute(""" SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s """, (DB_CONFIG['db'], table_name)) return cur.fetchone()[0] > 0 def fk_constraint_exists(cur, table_name, ref_table): cur.execute(""" SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND REFERENCED_TABLE_NAME = %s """, (DB_CONFIG['db'], table_name, ref_table)) return [r[0] for r in cur.fetchall()] def phase1_rename_rbac_tables(): """Phase 1: 重命名旧 RBAC 表,避免与 gvsdsdk PascalCase 表名冲突""" print('\n' + '='*60) print('Phase 1: 重命名旧 RBAC 表') print('='*60) conn = get_conn() cur = conn.cursor() for old_name, new_name in RBAC_RENAME_MAP.items(): if table_exists(cur, old_name) and not table_exists(cur, new_name): sql = f'RENAME TABLE `{old_name}` TO `{new_name}`' print(f' {old_name} -> {new_name}') cur.execute(sql) elif table_exists(cur, new_name): print(f' {new_name} 已存在,跳过') else: print(f' {old_name} 不存在,跳过') conn.commit() cur.close() conn.close() print('Phase 1 完成') def phase2_create_tables(): """Phase 2: 创建 User 表和 user_sensitive_data 表""" print('\n' + '='*60) print('Phase 2: 创建 User 表 + user_sensitive_data 表') print('='*60) conn = get_conn() cur = conn.cursor() # 2.1 创建 User 表 if table_exists(cur, 'User'): print(' User 表已存在,跳过创建') else: print(' 创建 User 表...') cur.execute(""" CREATE TABLE `User` ( `UserUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserName` varchar(256) NOT NULL UNIQUE, `UserPassword` varchar(200) NOT NULL DEFAULT '', `UserPositionStatus` smallint NOT NULL DEFAULT 0, `UserPersonalSignature` varchar(1024) NOT NULL DEFAULT '', `UserEmail` varchar(256) NOT NULL DEFAULT '', `UserEmailVerify` smallint NOT NULL DEFAULT 0, `UserAccountLicense` smallint NOT NULL DEFAULT 1, `UserGender` smallint NOT NULL DEFAULT -1, `UserTele` varchar(16) NULL, `UserCreateTime` datetime(6) NOT NULL, `UserLastLoginDate` datetime(6) NOT NULL, `IsStaff` tinyint(1) NOT NULL DEFAULT 0, `IsSuperuser` tinyint(1) NOT NULL DEFAULT 0, `YonghuID` varchar(7) NULL UNIQUE, `UserAvatar` varchar(500) NULL DEFAULT 'a_long/morentouxiang.jpg', `UserType` varchar(10) NOT NULL DEFAULT 'normal', INDEX `User_UserUUID_0a9d8a_idx` (`UserUUID`), INDEX `User_YonghuID_idx` (`YonghuID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci """) print(' User 表创建完成') # 2.2 创建 user_sensitive_data 表 if table_exists(cur, 'user_sensitive_data'): print(' user_sensitive_data 表已存在,跳过创建') else: print(' 创建 user_sensitive_data 表...') cur.execute(""" CREATE TABLE `user_sensitive_data` ( `id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL UNIQUE, `OpenID` varchar(64) NULL UNIQUE, `UnionID` varchar(64) NULL, `Password` varchar(200) NULL, `PhoneVerified` tinyint(1) NOT NULL DEFAULT 0, `Zhifu` varchar(500) NULL, `SKZhanghao` varchar(11) NULL, `IP` varchar(50) NULL, `create_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `update_time` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), INDEX `user_sensitive_data_OpenID_idx` (`OpenID`), INDEX `user_sensitive_data_UnionID_idx` (`UnionID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci """) print(' user_sensitive_data 表创建完成') conn.commit() cur.close() conn.close() print('Phase 2 完成') def phase3_migrate_user_data(): """Phase 3: 迁移 user_main 数据到 User + user_sensitive_data""" print('\n' + '='*60) print('Phase 3: 迁移 user_main 数据') print('='*60) conn = get_conn() cur = conn.cursor() # 检查 User 表是否已有数据 cur.execute('SELECT COUNT(*) FROM `User`') existing = cur.fetchone()[0] if existing > 0: print(f' User 表已有 {existing} 行数据,跳过迁移') cur.close() conn.close() return # 获取所有 user_main 数据 cur.execute(""" SELECT id, yonghuid, openid, avatar, phone, password, zhifu, skzhanghao, ip, last_login_time, user_type, create_time, update_time, unionid, shoujihao_renzheng, last_login FROM user_main """) rows = cur.fetchall() print(f' 从 user_main 读取 {len(rows)} 行') # 生成 UUID 并批量插入 user_data = [] sensitive_data = [] for row in rows: (id_, yonghuid, openid, avatar, phone, password, zhifu, skzhanghao, ip, last_login_time, user_type, create_time, update_time, unionid, shoujihao_renzheng, last_login) = row # 生成确定性 UUID user_uuid = uuid.uuid5(UUID_NAMESPACE, yonghuid).bytes # User 表数据 is_superuser = 1 if user_type == 'admin' else 0 is_staff = 1 if user_type in ('admin', 'kefu') else 0 user_name = yonghuid # 用 yonghuid 作为 UserName avatar_val = avatar if avatar else 'a_long/morentouxiang.jpg' last_login = last_login_time or last_login or create_time user_data.append(( user_uuid, user_name, '', # UserPassword 留空 0, # UserPositionStatus '', # UserPersonalSignature '', # UserEmail 0, # UserEmailVerify 1, # UserAccountLicense -1, # UserGender phone, # UserTele create_time, # UserCreateTime last_login, # UserLastLoginDate is_staff, is_superuser, yonghuid, # YonghuID avatar_val, # UserAvatar user_type, # UserType )) # user_sensitive_data 表数据 sensitive_data.append(( user_uuid, openid if openid else None, unionid if unionid else None, password if password else None, 1 if shoujihao_renzheng else 0, zhifu if zhifu else None, skzhanghao if skzhanghao else None, ip if ip else None, )) # 批量插入 User 表 print(f' 插入 {len(user_data)} 行到 User 表...') cur.executemany(""" INSERT INTO `User` ( UserUUID, UserName, UserPassword, UserPositionStatus, UserPersonalSignature, UserEmail, UserEmailVerify, UserAccountLicense, UserGender, UserTele, UserCreateTime, UserLastLoginDate, IsStaff, IsSuperuser, YonghuID, UserAvatar, UserType ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, user_data) # 批量插入 user_sensitive_data 表 print(f' 插入 {len(sensitive_data)} 行到 user_sensitive_data 表...') cur.executemany(""" INSERT INTO user_sensitive_data ( UserUUID, OpenID, UnionID, Password, PhoneVerified, Zhifu, SKZhanghao, IP ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """, sensitive_data) conn.commit() # 验证 cur.execute('SELECT COUNT(*) FROM `User`') user_count = cur.fetchone()[0] cur.execute('SELECT COUNT(*) FROM user_sensitive_data') sd_count = cur.fetchone()[0] print(f' 验证: User={user_count}, UserSensitiveData={sd_count}') if user_count != len(rows): print(f' 警告: User 表行数({user_count}) != user_main 行数({len(rows)})') cur.close() conn.close() print('Phase 3 完成') def phase4_alter_extension_tables(): """Phase 4: 删除 FK 约束,修改扩展表 user_id 列类型 int → BINARY(16)""" print('\n' + '='*60) print('Phase 4: 修改扩展表 FK 列类型') print('='*60) conn = get_conn() cur = conn.cursor() for table in EXT_TABLES_WITH_FK: print(f'\n 处理 {table}...') # 4.1 删除 FK 约束 fk_names = fk_constraint_exists(cur, table, 'user_main') for fk_name in fk_names: print(f' 删除 FK: {fk_name}') cur.execute(f'ALTER TABLE `{table}` DROP FOREIGN KEY `{fk_name}`') # 4.2 检查是否已有 new_user_id 列 cur.execute(""" SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'new_user_id' """, (DB_CONFIG['db'], table)) has_new = cur.fetchone()[0] > 0 if not has_new: # 4.3 添加新 BINARY(16) 列 print(f' 添加 new_user_id BINARY(16) 列') cur.execute(f'ALTER TABLE `{table}` ADD COLUMN `new_user_id` BINARY(16) NULL') # 4.4 填充新列:通过 user_main.id → user_main.yonghuid → User.UserUUID print(f' 填充 new_user_id...') cur.execute(f""" UPDATE `{table}` ext JOIN user_main um ON ext.user_id = um.id JOIN `User` u ON u.YonghuID = um.yonghuid COLLATE utf8mb4_general_ci SET ext.new_user_id = u.UserUUID """) affected = cur.rowcount print(f' 更新 {affected} 行') # 4.5 删除旧 user_id 列 print(f' 删除旧 user_id 列') cur.execute(f'ALTER TABLE `{table}` DROP COLUMN `user_id`') # 4.6 重命名 new_user_id → user_id print(f' 重命名 new_user_id → user_id') cur.execute(f'ALTER TABLE `{table}` CHANGE `new_user_id` `user_id` BINARY(16) NULL') # 4.7 添加索引 print(f' 添加索引') cur.execute(f'ALTER TABLE `{table}` ADD INDEX `{table}_user_id_idx` (`user_id`)') conn.commit() # 4.8 处理 django_admin_log(空表,直接改列类型) print(f'\n 处理 django_admin_log...') fk_names = fk_constraint_exists(cur, 'django_admin_log', 'user_main') for fk_name in fk_names: print(f' 删除 FK: {fk_name}') cur.execute(f'ALTER TABLE `django_admin_log` DROP FOREIGN KEY `{fk_name}`') # 检查并修改 user_id 列类型 cur.execute(""" SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'django_admin_log' AND COLUMN_NAME = 'user_id' """, (DB_CONFIG['db'],)) col_type = cur.fetchone() if col_type and col_type[0] != 'binary': print(f' 修改 user_id 列类型: {col_type[0]} → BINARY(16)') cur.execute('ALTER TABLE `django_admin_log` MODIFY COLUMN `user_id` BINARY(16) NULL') else: print(f' user_id 已是 BINARY 类型,跳过') conn.commit() cur.close() conn.close() print('\nPhase 4 完成') def phase5_update_migrations(): """Phase 5: 更新 django_migrations 记录""" print('\n' + '='*60) print('Phase 5: 更新 django_migrations 记录') print('='*60) conn = get_conn() cur = conn.cursor() # 删除旧迁移记录 apps = ['gvsdsdk', 'users', 'shop', 'backend', 'rank', 'orders', 'config', 'products'] for app in apps: cur.execute('DELETE FROM django_migrations WHERE app = %s', (app,)) if cur.rowcount > 0: print(f' 删除 {app} 旧迁移记录: {cur.rowcount} 条') # 插入新迁移记录 from datetime import datetime now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') new_migrations = [ ('gvsdsdk', '0001_initial'), ('users', '0001_initial'), ('shop', '0001_initial'), ('backend', '0001_initial'), ('rank', '0001_initial'), ('orders', '0001_initial'), ('config', '0001_initial'), ('products', '0001_initial'), ('products', '0002_initial'), ] for app, name in new_migrations: cur.execute( 'INSERT INTO django_migrations (app, name, applied) VALUES (%s, %s, %s)', (app, name, now) ) print(f' 插入 {app}.{name}') conn.commit() cur.close() conn.close() print('Phase 5 完成') def phase6_verify(): """Phase 6: 验证迁移结果""" print('\n' + '='*60) print('Phase 6: 验证迁移结果') print('='*60) conn = get_conn() cur = conn.cursor() # 1. User 表数据量 cur.execute('SELECT COUNT(*) FROM `User`') user_count = cur.fetchone()[0] print(f' User 表: {user_count} 行') # 2. user_sensitive_data 数据量 cur.execute('SELECT COUNT(*) FROM user_sensitive_data') sd_count = cur.fetchone()[0] print(f' user_sensitive_data: {sd_count} 行') # 3. user_main 对比 cur.execute('SELECT COUNT(*) FROM user_main') um_count = cur.fetchone()[0] print(f' user_main: {um_count} 行 (原表保留)') # 4. 验证 User 表字段 cur.execute('DESCRIBE `User`') print(f'\n User 表结构:') for c in cur.fetchall(): print(f' {c[0]} | {c[1]}') # 5. 验证扩展表 user_id 列类型 print(f'\n 扩展表 user_id 列类型:') for table in EXT_TABLES_WITH_FK: cur.execute(""" SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'user_id' """, (DB_CONFIG['db'], table)) result = cur.fetchone() print(f' {table}.user_id: {result[0] if result else "不存在"}') # 6. 验证扩展表数据完整性 print(f'\n 扩展表数据完整性:') for table in EXT_TABLES_WITH_FK: cur.execute(f'SELECT COUNT(*) FROM `{table}`') total = cur.fetchone()[0] cur.execute(f'SELECT COUNT(*) FROM `{table}` WHERE user_id IS NULL') null_count = cur.fetchone()[0] print(f' {table}: total={total}, null_user_id={null_count}') # 7. 验证 RBAC 表重命名 print(f'\n RBAC 表重命名验证:') for old, new in RBAC_RENAME_MAP.items(): cur.execute(""" SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s """, (DB_CONFIG['db'], new)) exists = cur.fetchone()[0] > 0 print(f' {new}: {"存在" if exists else "不存在"}') # 8. 验证 django_migrations print(f'\n django_migrations:') cur.execute("SELECT app, name FROM django_migrations WHERE app IN ('gvsdsdk','users','shop') ORDER BY app") for r in cur.fetchall(): print(f' {r[0]}.{r[1]}') # 9. 抽样验证数据一致性 print(f'\n 抽样验证 (前5行):') cur.execute(""" SELECT u.YonghuID, u.UserType, u.UserTele, u.UserAvatar, sd.OpenID, sd.Password, sd.PhoneVerified FROM `User` u LEFT JOIN user_sensitive_data sd ON u.UserUUID = sd.UserUUID LIMIT 5 """) for r in cur.fetchall(): print(f' YonghuID={r[0]}, Type={r[1]}, Tele={r[2]}, OpenID={r[4]}, Verified={r[6]}') cur.close() conn.close() print('\nPhase 6 验证完成') def main(): print('='*60) print('gvsdsdk User 表迁移脚本') print('='*60) print(f'目标数据库: {DB_CONFIG["db"]} @ {DB_CONFIG["host"]}:{DB_CONFIG["port"]}') # 确认 if '--yes' not in sys.argv: print('\n警告: 此脚本将修改生产数据库!') print('请确保已全量备份数据库。') print('使用 --yes 参数跳过确认。') response = input('输入 YES 确认执行: ') if response != 'YES': print('已取消') return # 执行各阶段 phase1_rename_rbac_tables() phase2_create_tables() phase3_migrate_user_data() phase4_alter_extension_tables() phase5_update_migrations() phase6_verify() print('\n' + '='*60) print('迁移完成!') print('='*60) print('\n后续步骤:') print('1. 在服务器上部署新代码 (git pull)') print('2. 重启 Django 服务') print('3. 测试登录和核心功能') print('4. 确认无误后 DROP TABLE user_main') if __name__ == '__main__': main()