Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 148b41a5d6 | |||
| f34b076dcd |
@@ -6,6 +6,7 @@ Create Date: 2026-08-31
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "060_migrate_segments"
|
||||
@@ -41,8 +42,7 @@ def upgrade() -> None:
|
||||
"s.id, s.template_id, 'main', s.segment_order, "
|
||||
"s.duration_min, s.duration_max, "
|
||||
"'', " + empty_json + ", "
|
||||
"'cut', "
|
||||
+ config_expr + ", "
|
||||
"'cut', " + config_expr + ", "
|
||||
"s.created_at, s.updated_at "
|
||||
"FROM template_segments s "
|
||||
"WHERE NOT EXISTS ("
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Build stage
|
||||
FROM git.xiaoxiajianji.com/xiaoxia/base/node:20 AS builder
|
||||
ARG SOURCE_HASH=""
|
||||
WORKDIR /app
|
||||
ARG VITE_API_URL=https://saas-api.xiaoxiajianji.com
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
@@ -18,8 +19,10 @@ COPY apps/web/ ./
|
||||
|
||||
# 构建:TS增量编译 + Vite构建,tsbuildinfo用cache mount持久化
|
||||
# node_modules直接使用镜像中已安装的(layer缓存保证完整性)
|
||||
# SOURCE_HASH 变化时强制重新执行(防止 buildkit 幽灵缓存命中)
|
||||
RUN --mount=type=cache,target=/app/apps/web/.tscache,sharing=locked \
|
||||
mkdir -p .tscache \
|
||||
&& echo "SOURCE_HASH=${SOURCE_HASH}" > .cache_bust \
|
||||
&& ./node_modules/.bin/tsc --incremental --tsBuildInfoFile .tscache/tsconfig.tsbuildinfo \
|
||||
&& ./node_modules/.bin/vite build
|
||||
|
||||
|
||||
@@ -212,14 +212,16 @@ class SQLAlchemyTemplateRepository:
|
||||
# 复用 create_segments 写入 template_clip_configs
|
||||
new_segments: List[TemplateSegment] = []
|
||||
for seg in source.segments:
|
||||
new_segments.append(TemplateSegment(
|
||||
id=str(uuid.uuid4()),
|
||||
template_id=created.id,
|
||||
segment_order=seg.segment_order,
|
||||
duration_min=seg.duration_min,
|
||||
duration_max=seg.duration_max,
|
||||
material_type=seg.material_type,
|
||||
))
|
||||
new_segments.append(
|
||||
TemplateSegment(
|
||||
id=str(uuid.uuid4()),
|
||||
template_id=created.id,
|
||||
segment_order=seg.segment_order,
|
||||
duration_min=seg.duration_min,
|
||||
duration_max=seg.duration_max,
|
||||
material_type=seg.material_type,
|
||||
)
|
||||
)
|
||||
if new_segments:
|
||||
self.create_segments(new_segments)
|
||||
else:
|
||||
|
||||
@@ -19,6 +19,18 @@ for arg in "$@"; do
|
||||
BUILD_ARGS="$BUILD_ARGS --build-arg $arg"
|
||||
done
|
||||
|
||||
# Web 镜像 cache bust:计算 apps/web/ 的 git tree hash
|
||||
# 当源码变化时 hash 变化,buildx 的 ARG 缓存键失效 → vite build 必定重新执行
|
||||
if [ "${DOCKERFILE##*/}" = "web.Dockerfile" ]; then
|
||||
SOURCE_HASH=$(git rev-parse HEAD:apps/web 2>/dev/null || echo "")
|
||||
if [ -n "$SOURCE_HASH" ]; then
|
||||
echo "Web cache bust: SOURCE_HASH=${SOURCE_HASH}"
|
||||
BUILD_ARGS="$BUILD_ARGS --build-arg SOURCE_HASH=${SOURCE_HASH}"
|
||||
else
|
||||
echo "⚠️ 无法计算 apps/web tree hash,跳过 cache bust"
|
||||
fi
|
||||
fi
|
||||
|
||||
BUILDER_NAME="ci-builder-persist"
|
||||
if ! docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then
|
||||
echo "持久 builder 不存在,创建中..."
|
||||
|
||||
@@ -31,6 +31,18 @@ for arg in "$@"; do
|
||||
BUILD_ARGS="$BUILD_ARGS --build-arg $arg"
|
||||
done
|
||||
|
||||
# Web 镜像 cache bust:计算 apps/web/ 的 git tree hash
|
||||
# 当源码变化时 hash 变化,buildx 的 ARG 缓存键失效 → vite build 必定重新执行
|
||||
if [ "${DOCKERFILE##*/}" = "web.Dockerfile" ]; then
|
||||
SOURCE_HASH=$(git rev-parse HEAD:apps/web 2>/dev/null || echo "")
|
||||
if [ -n "$SOURCE_HASH" ]; then
|
||||
echo "Web cache bust: SOURCE_HASH=${SOURCE_HASH}"
|
||||
BUILD_ARGS="$BUILD_ARGS --build-arg SOURCE_HASH=${SOURCE_HASH}"
|
||||
else
|
||||
echo "⚠️ 无法计算 apps/web tree hash,跳过 cache bust"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 确保持久 builder 存在并使用(幂等)
|
||||
if ! docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then
|
||||
echo "持久 builder 不存在,创建中..."
|
||||
|
||||
@@ -48,15 +48,27 @@ def repo(session):
|
||||
def _make_template(template_id=None, user_id="u1", name="测试模板", mode="one_take"):
|
||||
tid = template_id or str(uuid.uuid4())
|
||||
return Template(
|
||||
id=tid, user_id=user_id, name=name, mode=mode,
|
||||
category="", tags=[], estimated_duration=30.0, is_active=True, segments=[])
|
||||
id=tid,
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
mode=mode,
|
||||
category="",
|
||||
tags=[],
|
||||
estimated_duration=30.0,
|
||||
is_active=True,
|
||||
segments=[],
|
||||
)
|
||||
|
||||
|
||||
def _make_segment(template_id, order=1, material_type=None):
|
||||
return TemplateSegment(
|
||||
id=str(uuid.uuid4()), template_id=template_id,
|
||||
segment_order=order, duration_min=5.0, duration_max=10.0,
|
||||
material_type=material_type)
|
||||
id=str(uuid.uuid4()),
|
||||
template_id=template_id,
|
||||
segment_order=order,
|
||||
duration_min=5.0,
|
||||
duration_max=10.0,
|
||||
material_type=material_type,
|
||||
)
|
||||
|
||||
|
||||
class TestCreateSegments:
|
||||
@@ -65,8 +77,7 @@ class TestCreateSegments:
|
||||
repo.create(tpl)
|
||||
seg = _make_segment(tpl.id, order=1)
|
||||
repo.create_segments([seg])
|
||||
clips = session.query(TemplateClipConfigModel).filter(
|
||||
TemplateClipConfigModel.template_id == tpl.id).all()
|
||||
clips = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == tpl.id).all()
|
||||
assert len(clips) == 1
|
||||
assert clips[0].clip_type == "main"
|
||||
assert clips[0].order == 1
|
||||
@@ -77,8 +88,7 @@ class TestCreateSegments:
|
||||
repo.create(tpl)
|
||||
seg = _make_segment(tpl.id, order=1, material_type="voiceover")
|
||||
repo.create_segments([seg])
|
||||
clip = session.query(TemplateClipConfigModel).filter(
|
||||
TemplateClipConfigModel.template_id == tpl.id).first()
|
||||
clip = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == tpl.id).first()
|
||||
assert clip.config["material_type"] == "voiceover"
|
||||
|
||||
|
||||
@@ -96,8 +106,13 @@ class TestListSegments:
|
||||
tpl = _make_template()
|
||||
repo.create(tpl)
|
||||
old = TemplateSegmentModel(
|
||||
id=str(uuid.uuid4()), template_id=tpl.id,
|
||||
segment_order=1, duration_min=3.0, duration_max=8.0, material_type="场景")
|
||||
id=str(uuid.uuid4()),
|
||||
template_id=tpl.id,
|
||||
segment_order=1,
|
||||
duration_min=3.0,
|
||||
duration_max=8.0,
|
||||
material_type="场景",
|
||||
)
|
||||
session.add(old)
|
||||
session.commit()
|
||||
result = repo.list_segments(tpl.id)
|
||||
@@ -110,8 +125,8 @@ class TestListSegments:
|
||||
seg = _make_segment(tpl.id, order=1)
|
||||
repo.create_segments([seg])
|
||||
old = TemplateSegmentModel(
|
||||
id=str(uuid.uuid4()), template_id=tpl.id,
|
||||
segment_order=1, duration_min=1.0, duration_max=2.0)
|
||||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=1, duration_min=1.0, duration_max=2.0
|
||||
)
|
||||
session.add(old)
|
||||
session.commit()
|
||||
result = repo.list_segments(tpl.id)
|
||||
@@ -134,8 +149,8 @@ class TestListByUser:
|
||||
tpl = _make_template()
|
||||
repo.create(tpl)
|
||||
old = TemplateSegmentModel(
|
||||
id=str(uuid.uuid4()), template_id=tpl.id,
|
||||
segment_order=1, duration_min=2.0, duration_max=6.0)
|
||||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=1, duration_min=2.0, duration_max=6.0
|
||||
)
|
||||
session.add(old)
|
||||
session.commit()
|
||||
result = repo.list_by_user("u1")
|
||||
@@ -152,8 +167,7 @@ class TestCopyTemplate:
|
||||
repo.create_segments([seg])
|
||||
copied = repo.copy_template(tpl.id, "u1", "副本模板")
|
||||
assert copied.id != tpl.id
|
||||
clips = session.query(TemplateClipConfigModel).filter(
|
||||
TemplateClipConfigModel.template_id == copied.id).all()
|
||||
clips = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == copied.id).all()
|
||||
assert len(clips) == 1
|
||||
assert clips[0].config["material_type"] == "voiceover"
|
||||
|
||||
@@ -171,15 +185,13 @@ class TestDelete:
|
||||
seg = _make_segment(tpl.id, order=1)
|
||||
repo.create_segments([seg])
|
||||
old = TemplateSegmentModel(
|
||||
id=str(uuid.uuid4()), template_id=tpl.id,
|
||||
segment_order=1, duration_min=1.0, duration_max=2.0)
|
||||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=1, duration_min=1.0, duration_max=2.0
|
||||
)
|
||||
session.add(old)
|
||||
session.commit()
|
||||
repo.delete(tpl.id, "u1")
|
||||
c1 = session.query(TemplateClipConfigModel).filter(
|
||||
TemplateClipConfigModel.template_id == tpl.id).count()
|
||||
c2 = session.query(TemplateSegmentModel).filter(
|
||||
TemplateSegmentModel.template_id == tpl.id).count()
|
||||
c1 = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == tpl.id).count()
|
||||
c2 = session.query(TemplateSegmentModel).filter(TemplateSegmentModel.template_id == tpl.id).count()
|
||||
assert c1 == 0
|
||||
assert c2 == 0
|
||||
|
||||
@@ -189,8 +201,8 @@ class TestDelete:
|
||||
seg = _make_segment(tpl.id, order=1)
|
||||
repo.create_segments([seg])
|
||||
old = TemplateSegmentModel(
|
||||
id=str(uuid.uuid4()), template_id=tpl.id,
|
||||
segment_order=2, duration_min=1.0, duration_max=2.0)
|
||||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=2, duration_min=1.0, duration_max=2.0
|
||||
)
|
||||
session.add(old)
|
||||
session.commit()
|
||||
count = repo.delete_segments_by_template(tpl.id)
|
||||
|
||||
Reference in New Issue
Block a user