备份记录修改

This commit is contained in:
zyc 2026-03-17 10:38:35 +08:00
parent e5273540e9
commit 9ca5f8085f
7 changed files with 393 additions and 2 deletions

View File

@ -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="模型",
),
),
]

View File

@ -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;",
],
),
]

View File

@ -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):

View File

@ -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;",
},
}
}

84
repair_test_bug_65.py Normal file
View File

@ -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', ''))

129
repair_test_bug_66.py Normal file
View File

@ -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)

127
repair_test_bug_67_66.py Normal file
View File

@ -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!")