From 9ca5f8085f7bd47d0727132fa9da0529666f776b Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Tue, 17 Mar 2026 10:38:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=87=E4=BB=BD=E8=AE=B0=E5=BD=95=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0004_alter_generationrecord_model.py | 25 ++++ .../migrations/0005_convert_utf8mb4.py | 26 ++++ backend/apps/generation/models.py | 2 +- backend/config/settings.py | 2 +- repair_test_bug_65.py | 84 ++++++++++++ repair_test_bug_66.py | 129 ++++++++++++++++++ repair_test_bug_67_66.py | 127 +++++++++++++++++ 7 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 backend/apps/generation/migrations/0004_alter_generationrecord_model.py create mode 100644 backend/apps/generation/migrations/0005_convert_utf8mb4.py create mode 100644 repair_test_bug_65.py create mode 100644 repair_test_bug_66.py create mode 100644 repair_test_bug_67_66.py diff --git a/backend/apps/generation/migrations/0004_alter_generationrecord_model.py b/backend/apps/generation/migrations/0004_alter_generationrecord_model.py new file mode 100644 index 0000000..81fcc95 --- /dev/null +++ b/backend/apps/generation/migrations/0004_alter_generationrecord_model.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2 on 2026-03-15 21:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("generation", "0003_generationrecord_ark_task_id_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="generationrecord", + name="model", + field=models.CharField( + choices=[ + ("seedance_2.0", "AirDrama"), + ("seedance_2.0_fast", "AirDrama Fast"), + ], + max_length=30, + verbose_name="模型", + ), + ), + ] diff --git a/backend/apps/generation/migrations/0005_convert_utf8mb4.py b/backend/apps/generation/migrations/0005_convert_utf8mb4.py new file mode 100644 index 0000000..4a1003d --- /dev/null +++ b/backend/apps/generation/migrations/0005_convert_utf8mb4.py @@ -0,0 +1,26 @@ +"""Convert GenerationRecord table to utf8mb4 to support emoji/4-byte Unicode in prompt field. + +Bug #65: OperationalError (1366) "Incorrect string value" when prompt contains emoji characters. +""" + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('generation', '0004_alter_generationrecord_model'), + ] + + operations = [ + migrations.RunSQL( + sql=[ + "ALTER TABLE generation_generationrecord CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;", + "ALTER TABLE generation_quotaconfig CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;", + ], + reverse_sql=[ + "ALTER TABLE generation_generationrecord CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;", + "ALTER TABLE generation_quotaconfig CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;", + ], + ), + ] diff --git a/backend/apps/generation/models.py b/backend/apps/generation/models.py index b976b5d..1644f26 100644 --- a/backend/apps/generation/models.py +++ b/backend/apps/generation/models.py @@ -45,7 +45,7 @@ class GenerationRecord(models.Model): verbose_name_plural = '生成记录' ordering = ['-created_at'] indexes = [ - models.Index(fields=['user', 'created_at']), + models.Index(fields=['user', 'created_at'], name='generation__user_id_371350_idx'), ] def __str__(self): diff --git a/backend/config/settings.py b/backend/config/settings.py index 4ce911d..dbeb2a8 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -87,7 +87,7 @@ elif os.environ.get('USE_MYSQL', 'false').lower() in ('true', '1', 'yes'): 'PORT': os.environ.get('DB_PORT', '3306'), 'OPTIONS': { 'charset': 'utf8mb4', - 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", + 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'; SET NAMES utf8mb4;", }, } } diff --git a/repair_test_bug_65.py b/repair_test_bug_65.py new file mode 100644 index 0000000..5eccefa --- /dev/null +++ b/repair_test_bug_65.py @@ -0,0 +1,84 @@ +""" +Test for Bug #65 fix: OperationalError (1366) Incorrect string value for emoji in prompt. + +Verifies that GenerationRecord.prompt can store 4-byte UTF-8 characters (emoji). +""" + +import os +import sys +import django + +# Setup Django +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend')) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +os.environ.setdefault('TESTING', 'true') +django.setup() + +from django.test import TestCase +from django.contrib.auth import get_user_model +from apps.generation.models import GenerationRecord + +User = get_user_model() + + +class Bug65EmojiPromptTest(TestCase): + """Test that prompts with emoji characters can be saved to the database.""" + + def setUp(self): + self.user = User.objects.create_user( + username='testuser_bug65', + email='bug65@test.com', + password='testpass123', + ) + + def test_prompt_with_emoji_saves_successfully(self): + """Bug #65: prompt containing 🔊 (4-byte UTF-8) should not raise OperationalError.""" + emoji_prompt = '🔊 这是一个包含emoji的提示词 🎬🎥✨' + record = GenerationRecord.objects.create( + user=self.user, + prompt=emoji_prompt, + mode='universal', + model='seedance_2.0', + aspect_ratio='16:9', + duration=5, + ) + record.refresh_from_db() + self.assertEqual(record.prompt, emoji_prompt) + + def test_prompt_with_mixed_unicode(self): + """Prompt with mixed CJK + emoji + ASCII should save correctly.""" + mixed_prompt = '🔊 大象在草原上奔跑 🐘 — cinematic 4K, slow-motion 🎬' + record = GenerationRecord.objects.create( + user=self.user, + prompt=mixed_prompt, + mode='universal', + model='seedance_2.0', + aspect_ratio='16:9', + duration=5, + ) + record.refresh_from_db() + self.assertEqual(record.prompt, mixed_prompt) + + def test_prompt_with_only_basic_text(self): + """Ensure basic text still works after the charset change.""" + basic_prompt = '一只猫在跑步' + record = GenerationRecord.objects.create( + user=self.user, + prompt=basic_prompt, + mode='universal', + model='seedance_2.0', + aspect_ratio='16:9', + duration=5, + ) + record.refresh_from_db() + self.assertEqual(record.prompt, basic_prompt) + + def test_settings_mysql_charset(self): + """Verify MySQL OPTIONS includes utf8mb4 charset and SET NAMES utf8mb4.""" + from django.conf import settings + # Only check if MySQL config is present (prod uses MySQL, test uses SQLite) + db_config = settings.DATABASES.get('default', {}) + if db_config.get('ENGINE', '').endswith('mysql'): + options = db_config.get('OPTIONS', {}) + self.assertEqual(options.get('charset'), 'utf8mb4') + self.assertIn('SET NAMES utf8mb4', options.get('init_command', '')) diff --git a/repair_test_bug_66.py b/repair_test_bug_66.py new file mode 100644 index 0000000..a546608 --- /dev/null +++ b/repair_test_bug_66.py @@ -0,0 +1,129 @@ +""" +Verification script for Bug #66: CrashLoopBackOff fix. + +Root cause: Two uncommitted changes caused Django migration drift in the +Docker container: +1. MODEL_CHOICES labels changed from 'Seedance 2.0' to 'AirDrama' in models.py + but migration 0004 (which records this change) was untracked by git. +2. Index name='generation__user_id_371350_idx' was added to models.py but not + committed — the migration already had this name. + +Without migration 0004 in the Docker image, Django detected model-migration +mismatch at startup, causing the pod to enter CrashLoopBackOff. + +Fix: Commit both models.py (with explicit index name) and migration 0004. +""" +import os +import sys +import subprocess + +# Ensure we're in the backend directory +BACKEND_DIR = os.path.dirname(os.path.abspath(__file__)) +if os.path.basename(BACKEND_DIR) != 'backend': + BACKEND_DIR = os.path.join(BACKEND_DIR, 'backend') + +os.chdir(BACKEND_DIR) +sys.path.insert(0, BACKEND_DIR) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + + +def test_django_check(): + """Verify Django system check passes with no issues.""" + result = subprocess.run( + [sys.executable, 'manage.py', 'check'], + capture_output=True, text=True, cwd=BACKEND_DIR, + ) + assert result.returncode == 0, f"Django check failed:\n{result.stderr}" + print("PASS: Django system check — no issues found") + + +def test_no_pending_migrations(): + """Verify there are no pending migrations (the root cause of Bug #66).""" + result = subprocess.run( + [sys.executable, 'manage.py', 'makemigrations', '--check', '--dry-run'], + capture_output=True, text=True, cwd=BACKEND_DIR, + ) + assert result.returncode == 0, ( + f"Pending migrations detected (this was the Bug #66 root cause):\n" + f"{result.stdout}\n{result.stderr}" + ) + print("PASS: No pending migrations detected") + + +def test_index_name_matches_migration(): + """Verify the model index name matches what's in the migration file.""" + import django + django.setup() + + from apps.generation.models import GenerationRecord + + meta_indexes = GenerationRecord._meta.indexes + assert len(meta_indexes) >= 1, "GenerationRecord should have at least 1 index" + + user_created_index = meta_indexes[0] + assert user_created_index.name == 'generation__user_id_371350_idx', ( + f"Index name mismatch: got '{user_created_index.name}', " + f"expected 'generation__user_id_371350_idx'" + ) + print("PASS: Index name matches migration file") + + +def test_migration_0004_exists(): + """Verify migration 0004 for model choice label change exists.""" + migration_path = os.path.join( + BACKEND_DIR, 'apps', 'generation', 'migrations', + '0004_alter_generationrecord_model.py' + ) + assert os.path.exists(migration_path), ( + f"Migration 0004 not found at {migration_path}" + ) + print("PASS: Migration 0004 exists") + + +def test_model_choices_match_migration(): + """Verify MODEL_CHOICES values match what migration 0004 expects.""" + import django + django.setup() + + from apps.generation.models import GenerationRecord + + choices_dict = dict(GenerationRecord.MODEL_CHOICES) + assert choices_dict.get('seedance_2.0') == 'AirDrama', ( + f"Expected 'AirDrama', got {choices_dict.get('seedance_2.0')!r}" + ) + assert choices_dict.get('seedance_2.0_fast') == 'AirDrama Fast', ( + f"Expected 'AirDrama Fast', got {choices_dict.get('seedance_2.0_fast')!r}" + ) + print("PASS: MODEL_CHOICES labels match migration 0004") + + +def test_wsgi_loads(): + """Verify WSGI application loads (gunicorn entrypoint).""" + import django + django.setup() + + from config.wsgi import application + assert application is not None, "WSGI application failed to load" + print("PASS: WSGI application loads successfully") + + +if __name__ == '__main__': + tests = [ + test_django_check, + test_no_pending_migrations, + test_index_name_matches_migration, + test_migration_0004_exists, + test_model_choices_match_migration, + test_wsgi_loads, + ] + passed = failed = 0 + for test in tests: + try: + test() + passed += 1 + except Exception as e: + print(f"FAIL: {test.__name__}: {e}") + failed += 1 + + print(f"\nResults: {passed} passed, {failed} failed") + sys.exit(1 if failed else 0) diff --git a/repair_test_bug_67_66.py b/repair_test_bug_67_66.py new file mode 100644 index 0000000..4cda486 --- /dev/null +++ b/repair_test_bug_67_66.py @@ -0,0 +1,127 @@ +""" +Verification script for Bug #67 (DockerBuildError) and Bug #66 (CrashLoopBackOff). + +Bug #67: AdminLayout.tsx (and 3 other files) imported '../assets/logo_32.png' + which didn't exist, causing Vite build failure. + Fix: Created the missing logo_32.png asset file. + +Bug #66: GenerationRecord model's index lacked an explicit name, causing + Django to detect a model-migration mismatch on every container start. + Fix: Added name='generation__user_id_371350_idx' to the index, plus + created migration 0004 for MODEL_CHOICES display name change. +""" + +import os +import sys +import subprocess + +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +BACKEND_DIR = os.path.join(PROJECT_ROOT, 'backend') +WEB_DIR = os.path.join(PROJECT_ROOT, 'web') + + +def test_bug_67_logo_asset_exists(): + """Bug #67: Verify logo_32.png asset file exists.""" + logo_path = os.path.join(WEB_DIR, 'src', 'assets', 'logo_32.png') + assert os.path.isfile(logo_path), f"Missing file: {logo_path}" + assert os.path.getsize(logo_path) > 0, f"File is empty: {logo_path}" + # Verify it's a valid PNG (magic bytes) + with open(logo_path, 'rb') as f: + header = f.read(8) + assert header[:4] == b'\x89PNG', f"Not a valid PNG file: {logo_path}" + print("PASS: logo_32.png exists and is a valid PNG") + + +def test_bug_67_no_missing_imports(): + """Bug #67: Verify all files importing logo_32.png can resolve the asset.""" + files_using_logo = [ + 'src/pages/AdminLayout.tsx', + 'src/pages/TeamAdminLayout.tsx', + 'src/components/Sidebar.tsx', + 'src/components/LoginModal.tsx', + ] + logo_path = os.path.join(WEB_DIR, 'src', 'assets', 'logo_32.png') + for f in files_using_logo: + full_path = os.path.join(WEB_DIR, f) + if not os.path.isfile(full_path): + print(f"SKIP: {f} does not exist") + continue + with open(full_path, 'r') as fh: + content = fh.read() + if 'logo_32.png' in content: + assert os.path.isfile(logo_path), \ + f"{f} imports logo_32.png but asset doesn't exist" + print(f"PASS: {f} imports logo_32.png and asset exists") + else: + print(f"INFO: {f} no longer imports logo_32.png") + + +def test_bug_66_no_pending_migrations(): + """Bug #66: Verify Django detects no pending migration changes.""" + result = subprocess.run( + [sys.executable, 'manage.py', 'makemigrations', '--check'], + capture_output=True, text=True, cwd=BACKEND_DIR + ) + assert result.returncode == 0, \ + f"Pending migrations detected:\nstdout: {result.stdout}\nstderr: {result.stderr}" + print("PASS: No pending migrations detected") + + +def test_bug_66_index_has_name(): + """Bug #66: Verify GenerationRecord index has explicit name.""" + models_path = os.path.join(BACKEND_DIR, 'apps', 'generation', 'models.py') + with open(models_path, 'r') as f: + content = f.read() + assert "name='generation__user_id_371350_idx'" in content, \ + "Index name not found in models.py" + print("PASS: GenerationRecord index has explicit name") + + +def test_bug_66_django_check(): + """Bug #66: Verify Django system check passes.""" + result = subprocess.run( + [sys.executable, 'manage.py', 'check'], + capture_output=True, text=True, cwd=BACKEND_DIR + ) + assert result.returncode == 0, \ + f"Django check failed:\nstdout: {result.stdout}\nstderr: {result.stderr}" + print("PASS: Django system check passed") + + +def test_bug_66_migration_file_exists(): + """Bug #66: Verify migration 0004 exists for MODEL_CHOICES change.""" + migration_path = os.path.join( + BACKEND_DIR, 'apps', 'generation', 'migrations', + '0004_alter_generationrecord_model.py' + ) + assert os.path.isfile(migration_path), \ + f"Migration 0004 not found: {migration_path}" + print("PASS: Migration 0004 exists") + + +if __name__ == '__main__': + tests = [ + test_bug_67_logo_asset_exists, + test_bug_67_no_missing_imports, + test_bug_66_no_pending_migrations, + test_bug_66_index_has_name, + test_bug_66_django_check, + test_bug_66_migration_file_exists, + ] + failed = 0 + for test in tests: + try: + test() + except AssertionError as e: + print(f"FAIL: {test.__name__}: {e}") + failed += 1 + except Exception as e: + print(f"ERROR: {test.__name__}: {e}") + failed += 1 + + print(f"\n{'='*40}") + print(f"Results: {len(tests) - failed}/{len(tests)} passed") + if failed: + sys.exit(1) + else: + print("All verification tests passed!")