From 5c4663c8ecd304fb2d72a3733209b337fbe93579 Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 24 Jun 2026 05:27:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20config.0006=20=E5=8D=8A=E8=B7=AF?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=81=A2=E5=A4=8D=E5=91=BD=E4=BB=A4=20recove?= =?UTF-8?q?r=5Fconfig=5F0006?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/recover_config_0006.py | 104 ++++++++++++++++++ deploy/pre_migrate_dedupe.sql | 9 +- 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 config/management/commands/recover_config_0006.py diff --git a/config/management/commands/recover_config_0006.py b/config/management/commands/recover_config_0006.py new file mode 100644 index 0000000..7b81d53 --- /dev/null +++ b/config/management/commands/recover_config_0006.py @@ -0,0 +1,104 @@ +"""修复 config.0006 半路失败:去重 + 补唯一约束/索引,然后 fake 0006。""" +from django.core.management.base import BaseCommand +from django.db import connection +from django.db.models import Max + +from config.models import PopupPage, Tupianpeizhi + + +def _table_indexes(table): + with connection.cursor() as cursor: + cursor.execute(f'SHOW INDEX FROM `{table}`') + rows = cursor.fetchall() + # Key_name, Column_name, Non_unique + by_key = {} + for row in rows: + key_name = row[2] + col = row[4] + non_unique = row[1] + by_key.setdefault(key_name, {'cols': [], 'unique': non_unique == 0}) + by_key[key_name]['cols'].append(col) + return by_key + + +def _has_unique_on(table, columns): + cols = list(columns) + for info in _table_indexes(table).values(): + if info['unique'] and info['cols'] == cols: + return True + return False + + +def _drop_index(table, key_name): + with connection.cursor() as cursor: + cursor.execute(f'ALTER TABLE `{table}` DROP INDEX `{key_name}`') + + +def _add_unique(table, name, columns): + cols = ', '.join(f'`{c}`' for c in columns) + with connection.cursor() as cursor: + cursor.execute(f'ALTER TABLE `{table}` ADD UNIQUE `{name}` ({cols})') + + +def _add_index(table, name, columns): + cols = ', '.join(f'`{c}`' for c in columns) + with connection.cursor() as cursor: + cursor.execute(f'CREATE INDEX `{name}` ON `{table}` ({cols})') + + +class Command(BaseCommand): + help = 'config.0006 半路失败时:清重复数据、补约束,再执行 migrate config 0006 --fake' + + def handle(self, *args, **options): + self.stdout.write('1) 清理重复数据…') + + for row in Tupianpeizhi.objects.values('club_id', 'ImageType').annotate(max_id=Max('id')): + n = Tupianpeizhi.objects.filter( + club_id=row['club_id'], + ImageType=row['ImageType'], + ).exclude(id=row['max_id']).delete()[0] + if n: + self.stdout.write(f' tupianpeizhi 删除 {n} 条重复') + + for row in PopupPage.objects.values('club_id', 'page_key').annotate(max_id=Max('id')): + n = PopupPage.objects.filter( + club_id=row['club_id'], + page_key=row['page_key'], + ).exclude(id=row['max_id']).delete()[0] + if n: + self.stdout.write(f' popup_page 删除 {n} 条重复') + + self.stdout.write('2) 检查并补约束/索引…') + + # popup_page:去掉 page_key 单独唯一,改为 (club_id, page_key) + popup_indexes = _table_indexes('popup_page') + for key_name, info in popup_indexes.items(): + if info['unique'] and info['cols'] == ['page_key']: + self.stdout.write(f' 删除 popup_page 旧唯一索引 {key_name}') + _drop_index('popup_page', key_name) + + if not _has_unique_on('popup_page', ['club_id', 'page_key']): + self.stdout.write(' 添加 popup_page UNIQUE (club_id, page_key)') + _add_unique('popup_page', 'popup_page_club_page_key_uniq', ['club_id', 'page_key']) + else: + self.stdout.write(' popup_page UNIQUE (club_id, page_key) 已存在') + + if not _has_unique_on('tupianpeizhi', ['club_id', 'ImageType']): + self.stdout.write(' 添加 tupianpeizhi UNIQUE (club_id, ImageType)') + _add_unique('tupianpeizhi', 'tupianpeizhi_club_id_ImageType_uniq', ['club_id', 'ImageType']) + else: + self.stdout.write(' tupianpeizhi UNIQUE (club_id, ImageType) 已存在') + + lunbo_indexes = _table_indexes('lunbo') + if 'lunbo_club_page_type_idx' not in lunbo_indexes: + self.stdout.write(' 添加 lunbo 索引 lunbo_club_page_type_idx') + _add_index('lunbo', 'lunbo_club_page_type_idx', ['club_id', 'page_key', 'ImageType']) + else: + self.stdout.write(' lunbo 索引 lunbo_club_page_type_idx 已存在') + + self.stdout.write(self.style.SUCCESS( + '\n完成。请执行:\n' + ' python manage.py migrate config 0006 --fake\n' + ' python manage.py migrate config\n' + ' python manage.py migrate\n' + )) diff --git a/deploy/pre_migrate_dedupe.sql b/deploy/pre_migrate_dedupe.sql index c039dfd..bb034bb 100644 --- a/deploy/pre_migrate_dedupe.sql +++ b/deploy/pre_migrate_dedupe.sql @@ -1,5 +1,10 @@ --- 部署 jituan 前若 migrate 报 Duplicate entry 唯一约束冲突,在 dbshell 执行。 --- 每种组合只保留 id 最大的一条;不影响订单/资金主表。 +-- 部署 jituan 前若 migrate 报 Duplicate entry 唯一约束冲突时使用。 +-- 不要用 mysql 命令行(1Panel MySQL 不在本机 socket),请用: +-- python manage.py dbshell < deploy/pre_migrate_dedupe.sql +-- 或半路失败的 config.0006: +-- python manage.py recover_config_0006 +-- python manage.py migrate config 0006 --fake +-- python manage.py migrate -- config.0006 tupianpeizhi DELETE FROM tupianpeizhi