Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f279b49a90 | |||
| 97b436d016 | |||
| 60afbf450d | |||
| 7545a5ccdc | |||
| e56678455d | |||
| 6f00dce56e | |||
| 7beeecd467 | |||
| 79cc6b0dcc | |||
| 40cbc6a010 | |||
| abd2ee63da | |||
| 3292a5d655 | |||
| 1ea75e5d33 | |||
| e418577097 | |||
| 02f21e0f1d | |||
| 3ce406d4ce | |||
| ff3fdfb451 | |||
| 93e0ec5ea4 | |||
| 926d0fa272 | |||
| ebcfa7280d |
@@ -0,0 +1,190 @@
|
||||
"""add foreign key constraints and missing indexes
|
||||
|
||||
Revision ID: 010
|
||||
Revises: 009
|
||||
Create Date: 2026-06-28
|
||||
|
||||
Summary:
|
||||
- Add ForeignKey constraints to all 35 logical FK columns across 16 tables
|
||||
- ON DELETE CASCADE for parent-child relationships (project → children)
|
||||
- ON DELETE SET NULL for optional user references (created_by, assignee, etc.)
|
||||
- Add missing indexes on 10 FK columns
|
||||
- Alter columns from NOT NULL to nullable where SET NULL is needed
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "010"
|
||||
down_revision = "009"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── projects ──────────────────────────────────────────────
|
||||
op.create_foreign_key("fk_projects_owner_user_id", "projects", "users", ["owner_user_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── asset_libraries ───────────────────────────────────────
|
||||
op.create_foreign_key("fk_asset_libraries_project_id", "asset_libraries", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── assets ────────────────────────────────────────────────
|
||||
# uploaded_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("assets", "uploaded_by_user_id", existing_type=sa.String(36), nullable=True)
|
||||
op.create_foreign_key("fk_assets_project_id", "assets", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_assets_asset_library_id", "assets", "asset_libraries", ["asset_library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_assets_uploaded_by_user_id", "assets", "users", ["uploaded_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
# Add missing index
|
||||
op.create_index("ix_assets_uploaded_by_user_id", "assets", ["uploaded_by_user_id"])
|
||||
|
||||
# ── project_titles ────────────────────────────────────────
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("project_titles", "created_by_user_id", existing_type=sa.String(36), nullable=True)
|
||||
op.create_foreign_key("fk_project_titles_project_id", "project_titles", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_project_titles_created_by_user_id", "project_titles", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
# Add missing index
|
||||
op.create_index("ix_project_titles_created_by_user_id", "project_titles", ["created_by_user_id"])
|
||||
|
||||
# ── edit_templates ────────────────────────────────────────
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("edit_templates", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_edit_templates_project_id", "edit_templates", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_templates_created_by_user_id", "edit_templates", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── edit_plans ────────────────────────────────────────────
|
||||
# title_id: nullable=False → nullable=True for SET NULL
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("edit_plans", "title_id", existing_type=sa.String(32), nullable=True)
|
||||
op.alter_column("edit_plans", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_edit_plans_project_id", "edit_plans", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plans_template_id", "edit_plans", "edit_templates", ["template_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plans_asset_library_id", "edit_plans", "asset_libraries", ["asset_library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plans_title_id", "edit_plans", "project_titles", ["title_id"], ["id"], ondelete="SET NULL")
|
||||
op.create_foreign_key("fk_edit_plans_created_by_user_id", "edit_plans", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── edit_plan_clips ───────────────────────────────────────
|
||||
op.create_foreign_key("fk_edit_plan_clips_edit_plan_id", "edit_plan_clips", "edit_plans", ["edit_plan_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_edit_plan_clips_asset_id", "edit_plan_clips", "assets", ["asset_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── ingest_jobs ───────────────────────────────────────────
|
||||
# result_asset_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("ingest_jobs", "result_asset_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_ingest_jobs_project_id", "ingest_jobs", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_ingest_jobs_library_id", "ingest_jobs", "asset_libraries", ["library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_ingest_jobs_result_asset_id", "ingest_jobs", "assets", ["result_asset_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── classification_jobs ───────────────────────────────────
|
||||
op.create_foreign_key("fk_classification_jobs_project_id", "classification_jobs", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_classification_jobs_asset_id", "classification_jobs", "assets", ["asset_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── generation_tasks ──────────────────────────────────────
|
||||
# edit_plan_id: nullable=False → nullable=True for SET NULL
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("generation_tasks", "edit_plan_id", existing_type=sa.String(32), nullable=True)
|
||||
op.alter_column("generation_tasks", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_generation_tasks_project_id", "generation_tasks", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generation_tasks_asset_library_id", "generation_tasks", "asset_libraries", ["asset_library_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generation_tasks_edit_plan_id", "generation_tasks", "edit_plans", ["edit_plan_id"], ["id"], ondelete="SET NULL")
|
||||
op.create_foreign_key("fk_generation_tasks_created_by_user_id", "generation_tasks", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── generated_videos ──────────────────────────────────────
|
||||
op.create_foreign_key("fk_generated_videos_project_id", "generated_videos", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generated_videos_generation_task_id", "generated_videos", "generation_tasks", ["generation_task_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_generated_videos_duplicate_of", "generated_videos", "generated_videos", ["duplicate_of"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── tasks ─────────────────────────────────────────────────
|
||||
# parent_task_id: nullable=False → nullable=True for SET NULL
|
||||
# assignee_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("tasks", "parent_task_id", existing_type=sa.String(32), nullable=True)
|
||||
op.alter_column("tasks", "assignee_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_tasks_project_id", "tasks", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_tasks_parent_task_id", "tasks", "tasks", ["parent_task_id"], ["id"], ondelete="SET NULL")
|
||||
op.create_foreign_key("fk_tasks_assignee_user_id", "tasks", "users", ["assignee_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
# ── milestones ────────────────────────────────────────────
|
||||
op.create_foreign_key("fk_milestones_project_id", "milestones", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
|
||||
# ── task_issues ───────────────────────────────────────────
|
||||
# created_by_user_id: nullable=False → nullable=True for SET NULL
|
||||
op.alter_column("task_issues", "created_by_user_id", existing_type=sa.String(32), nullable=True)
|
||||
op.create_foreign_key("fk_task_issues_task_id", "task_issues", "tasks", ["task_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_task_issues_project_id", "task_issues", "projects", ["project_id"], ["id"], ondelete="CASCADE")
|
||||
op.create_foreign_key("fk_task_issues_created_by_user_id", "task_issues", "users", ["created_by_user_id"], ["id"], ondelete="SET NULL")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ── task_issues ───────────────────────────────────────────
|
||||
op.drop_constraint("fk_task_issues_created_by_user_id", "task_issues", type_="foreignkey")
|
||||
op.drop_constraint("fk_task_issues_project_id", "task_issues", type_="foreignkey")
|
||||
op.drop_constraint("fk_task_issues_task_id", "task_issues", type_="foreignkey")
|
||||
op.alter_column("task_issues", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── milestones ────────────────────────────────────────────
|
||||
op.drop_constraint("fk_milestones_project_id", "milestones", type_="foreignkey")
|
||||
|
||||
# ── tasks ─────────────────────────────────────────────────
|
||||
op.drop_constraint("fk_tasks_assignee_user_id", "tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_tasks_parent_task_id", "tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_tasks_project_id", "tasks", type_="foreignkey")
|
||||
op.alter_column("tasks", "assignee_user_id", existing_type=sa.String(32), nullable=False)
|
||||
op.alter_column("tasks", "parent_task_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── generated_videos ──────────────────────────────────────
|
||||
op.drop_constraint("fk_generated_videos_duplicate_of", "generated_videos", type_="foreignkey")
|
||||
op.drop_constraint("fk_generated_videos_generation_task_id", "generated_videos", type_="foreignkey")
|
||||
op.drop_constraint("fk_generated_videos_project_id", "generated_videos", type_="foreignkey")
|
||||
|
||||
# ── generation_tasks ──────────────────────────────────────
|
||||
op.drop_constraint("fk_generation_tasks_created_by_user_id", "generation_tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_generation_tasks_edit_plan_id", "generation_tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_generation_tasks_asset_library_id", "generation_tasks", type_="foreignkey")
|
||||
op.drop_constraint("fk_generation_tasks_project_id", "generation_tasks", type_="foreignkey")
|
||||
op.alter_column("generation_tasks", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
op.alter_column("generation_tasks", "edit_plan_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── classification_jobs ───────────────────────────────────
|
||||
op.drop_constraint("fk_classification_jobs_asset_id", "classification_jobs", type_="foreignkey")
|
||||
op.drop_constraint("fk_classification_jobs_project_id", "classification_jobs", type_="foreignkey")
|
||||
|
||||
# ── ingest_jobs ───────────────────────────────────────────
|
||||
op.drop_constraint("fk_ingest_jobs_result_asset_id", "ingest_jobs", type_="foreignkey")
|
||||
op.drop_constraint("fk_ingest_jobs_library_id", "ingest_jobs", type_="foreignkey")
|
||||
op.drop_constraint("fk_ingest_jobs_project_id", "ingest_jobs", type_="foreignkey")
|
||||
op.alter_column("ingest_jobs", "result_asset_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── edit_plan_clips ───────────────────────────────────────
|
||||
op.drop_constraint("fk_edit_plan_clips_asset_id", "edit_plan_clips", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plan_clips_edit_plan_id", "edit_plan_clips", type_="foreignkey")
|
||||
|
||||
# ── edit_plans ────────────────────────────────────────────
|
||||
op.drop_constraint("fk_edit_plans_created_by_user_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_title_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_asset_library_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_template_id", "edit_plans", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_plans_project_id", "edit_plans", type_="foreignkey")
|
||||
op.alter_column("edit_plans", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
op.alter_column("edit_plans", "title_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── edit_templates ────────────────────────────────────────
|
||||
op.drop_constraint("fk_edit_templates_created_by_user_id", "edit_templates", type_="foreignkey")
|
||||
op.drop_constraint("fk_edit_templates_project_id", "edit_templates", type_="foreignkey")
|
||||
op.alter_column("edit_templates", "created_by_user_id", existing_type=sa.String(32), nullable=False)
|
||||
|
||||
# ── project_titles ────────────────────────────────────────
|
||||
op.drop_index("ix_project_titles_created_by_user_id", table_name="project_titles")
|
||||
op.drop_constraint("fk_project_titles_created_by_user_id", "project_titles", type_="foreignkey")
|
||||
op.drop_constraint("fk_project_titles_project_id", "project_titles", type_="foreignkey")
|
||||
op.alter_column("project_titles", "created_by_user_id", existing_type=sa.String(36), nullable=False)
|
||||
|
||||
# ── assets ────────────────────────────────────────────────
|
||||
op.drop_index("ix_assets_uploaded_by_user_id", table_name="assets")
|
||||
op.drop_constraint("fk_assets_uploaded_by_user_id", "assets", type_="foreignkey")
|
||||
op.drop_constraint("fk_assets_asset_library_id", "assets", type_="foreignkey")
|
||||
op.drop_constraint("fk_assets_project_id", "assets", type_="foreignkey")
|
||||
op.alter_column("assets", "uploaded_by_user_id", existing_type=sa.String(36), nullable=False)
|
||||
|
||||
# ── asset_libraries ───────────────────────────────────────
|
||||
op.drop_constraint("fk_asset_libraries_project_id", "asset_libraries", type_="foreignkey")
|
||||
|
||||
# ── projects ──────────────────────────────────────────────
|
||||
op.drop_constraint("fk_projects_owner_user_id", "projects", type_="foreignkey")
|
||||
@@ -79,7 +79,7 @@ def _ensure_library_has_ready_video_assets(assets) -> None:
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tasks/", response_model=GenerationTaskResponse)
|
||||
@router.post("/tasks", response_model=GenerationTaskResponse)
|
||||
def create_generation_task(
|
||||
request: CreateGenerationTaskRequest,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
@@ -130,7 +130,7 @@ def get_generation_task(
|
||||
return _to_generation_task_response(task)
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}/results/", response_model=ListGeneratedVideosResponse)
|
||||
@router.get("/tasks/{task_id}/results", response_model=ListGeneratedVideosResponse)
|
||||
def list_generation_results(
|
||||
task_id: str,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
|
||||
@@ -51,13 +51,13 @@ def _to_response(item) -> TitleLibraryItemResponse:
|
||||
|
||||
|
||||
def _get_user_plan(user_id: str, user_repository: UserRepository) -> str:
|
||||
user = user_repository.get_by_id(user_id)
|
||||
user = user_repository.find_by_id(user_id)
|
||||
if user is None:
|
||||
return "free"
|
||||
return getattr(user, "subscription_plan", "free") or "free"
|
||||
|
||||
|
||||
@router.get("/", response_model=ListTitleLibraryResponse)
|
||||
@router.get("", response_model=ListTitleLibraryResponse)
|
||||
def list_titles(
|
||||
category: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
@@ -89,7 +89,7 @@ def get_title(
|
||||
return _to_response(item)
|
||||
|
||||
|
||||
@router.post("/", response_model=TitleLibraryItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("", response_model=TitleLibraryItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_title(
|
||||
request: CreateTitleLibraryRequest,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
|
||||
@@ -55,13 +55,13 @@ def _to_response(item) -> VoiceLibraryItemResponse:
|
||||
|
||||
|
||||
def _get_user_plan(user_id: str, user_repository: UserRepository) -> str:
|
||||
user = user_repository.get_by_id(user_id)
|
||||
user = user_repository.find_by_id(user_id)
|
||||
if user is None:
|
||||
return "free"
|
||||
return getattr(user, "subscription_plan", "free") or "free"
|
||||
|
||||
|
||||
@router.get("/", response_model=ListVoiceLibraryResponse)
|
||||
@router.get("", response_model=ListVoiceLibraryResponse)
|
||||
def list_voices(
|
||||
status_filter: Optional[str] = Query(None, alias="status"),
|
||||
skip: int = Query(0, ge=0),
|
||||
@@ -93,7 +93,7 @@ def get_voice(
|
||||
return _to_response(item)
|
||||
|
||||
|
||||
@router.post("/", response_model=VoiceLibraryItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("", response_model=VoiceLibraryItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_voice(
|
||||
request: CreateVoiceLibraryRequest,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
|
||||
@@ -14,4 +14,4 @@ class IngestJobResponse(BaseModel):
|
||||
storage_key: str
|
||||
status: str
|
||||
error_message: str
|
||||
result_asset_id: str
|
||||
result_asset_id: str | None
|
||||
|
||||
+6
-4
@@ -24,6 +24,7 @@ app = FastAPI(
|
||||
version=settings.APP_VERSION,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
redirect_slashes=False,
|
||||
)
|
||||
|
||||
app.add_exception_handler(APIException, api_exception_handler)
|
||||
@@ -38,10 +39,11 @@ if settings.DEBUG:
|
||||
allow_origins = settings.CORS_ORIGINS # Allow localhost in debug mode
|
||||
else:
|
||||
# In production, filter out any wildcard "*" origins
|
||||
allow_origins = [origin for origin in settings.CORS_ORIGINS if origin != "*"]
|
||||
if not allow_origins:
|
||||
# Default to production domain if no valid origins configured
|
||||
allow_origins = ["https://xiaoxiajianji.com"]
|
||||
allow_origins = list({origin for origin in settings.CORS_ORIGINS if origin != "*"})
|
||||
# Always ensure production domains are included
|
||||
for domain in ("https://xiaoxiajianji.com", "https://saas.xiaoxiajianji.com"):
|
||||
if domain not in allow_origins:
|
||||
allow_origins.append(domain)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
||||
+73
-11
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* 标题相关 API
|
||||
* Phase 1 新增:全局标题库
|
||||
* 注意:后端 schema 使用 name + text 字段,前端 UI 用 content 展示
|
||||
*/
|
||||
import apiClient from './client';
|
||||
|
||||
/** 标题条目 */
|
||||
/** 标题条目(前端展示用) */
|
||||
export interface TitleItem {
|
||||
id: string;
|
||||
content: string;
|
||||
@@ -16,7 +17,50 @@ export interface TitleItem {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/** 创建标题请求 */
|
||||
/** 后端标题响应格式 */
|
||||
interface BackendTitleResponse {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
text: string;
|
||||
category: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
usage_count: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** 后端创建标题请求格式 */
|
||||
interface BackendCreateTitleRequest {
|
||||
name: string;
|
||||
text: string;
|
||||
category: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/** 后端更新标题请求格式 */
|
||||
interface BackendUpdateTitleRequest {
|
||||
name?: string;
|
||||
text?: string;
|
||||
category?: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/** 将后端响应映射为前端 TitleItem */
|
||||
const toTitleItem = (item: BackendTitleResponse): TitleItem => ({
|
||||
id: item.id,
|
||||
content: item.text,
|
||||
category: item.category,
|
||||
word_count: item.text?.length || 0,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
});
|
||||
|
||||
/** 创建标题请求(前端接口,保持向后兼容) */
|
||||
export interface CreateTitleRequest {
|
||||
content: string;
|
||||
category?: string;
|
||||
@@ -24,25 +68,43 @@ export interface CreateTitleRequest {
|
||||
|
||||
/** 获取当前用户的所有标题 */
|
||||
export const getTitles = async (): Promise<TitleItem[]> => {
|
||||
const response = await apiClient.get('/titles');
|
||||
return response.data.items || response.data || [];
|
||||
const response = await apiClient.get<{ items: BackendTitleResponse[] }>('/titles');
|
||||
return (response.data.items || []).map(toTitleItem);
|
||||
};
|
||||
|
||||
/** 创建标题 */
|
||||
export const createTitle = async (
|
||||
data: CreateTitleRequest
|
||||
data: CreateTitleRequest,
|
||||
): Promise<TitleItem> => {
|
||||
const response = await apiClient.post('/titles', data);
|
||||
return response.data;
|
||||
// 后端要求 name(≤255)和 text(≤500),name 从 content 截取
|
||||
const payload: BackendCreateTitleRequest = {
|
||||
name: data.content.slice(0, 255),
|
||||
text: data.content.slice(0, 500),
|
||||
category: data.category || 'default',
|
||||
};
|
||||
const response = await apiClient.post<BackendTitleResponse>('/titles', payload);
|
||||
return toTitleItem(response.data);
|
||||
};
|
||||
|
||||
/** 更新标题 */
|
||||
export const updateTitle = async (
|
||||
titleId: string,
|
||||
data: Partial<CreateTitleRequest>
|
||||
data: Partial<CreateTitleRequest>,
|
||||
): Promise<TitleItem> => {
|
||||
const response = await apiClient.patch(`/titles/${titleId}`, data);
|
||||
return response.data;
|
||||
const payload: BackendUpdateTitleRequest = {};
|
||||
if (data.content !== undefined) {
|
||||
payload.name = data.content.slice(0, 255);
|
||||
payload.text = data.content.slice(0, 500);
|
||||
}
|
||||
if (data.category !== undefined) {
|
||||
payload.category = data.category;
|
||||
}
|
||||
// 后端用 PUT,非 PATCH
|
||||
const response = await apiClient.put<BackendTitleResponse>(
|
||||
`/titles/${titleId}`,
|
||||
payload,
|
||||
);
|
||||
return toTitleItem(response.data);
|
||||
};
|
||||
|
||||
/** 删除标题 */
|
||||
@@ -52,7 +114,7 @@ export const deleteTitle = async (titleId: string): Promise<void> => {
|
||||
|
||||
/** 批量导入标题 */
|
||||
export const batchImportTitles = async (
|
||||
titles: string[]
|
||||
titles: string[],
|
||||
): Promise<{ imported_count: number }> => {
|
||||
const response = await apiClient.post('/titles/batch-import', { titles });
|
||||
return response.data;
|
||||
|
||||
@@ -72,11 +72,11 @@ services:
|
||||
# =========================================
|
||||
# 资源限制建议(生产环境建议启用)
|
||||
# =========================================
|
||||
mem_limit: 2g
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0'
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: '0.5'
|
||||
memory: 512M
|
||||
@@ -112,11 +112,11 @@ services:
|
||||
# 资源限制建议(生产环境建议启用)
|
||||
# =========================================
|
||||
# 注意: Worker 需要处理视频,建议分配更多资源
|
||||
mem_limit: 2g
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2.0'
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: '0.5'
|
||||
memory: 1G
|
||||
|
||||
@@ -54,12 +54,13 @@ class SQLAlchemyProjectRepository:
|
||||
|
||||
def find_accessible_projects(self, user_id: str) -> list[Project]:
|
||||
"""查找用户可访问的所有项目(自己拥有的 + 被共享的)"""
|
||||
from sqlalchemy import or_
|
||||
|
||||
from sqlalchemy import or_, cast
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
models = self.session.query(ProjectModel).filter(
|
||||
or_(
|
||||
ProjectModel.owner_user_id == user_id,
|
||||
ProjectModel.shared_users.contains([user_id])
|
||||
cast(ProjectModel.shared_users, JSONB).contains([user_id])
|
||||
)
|
||||
).all()
|
||||
return [self._to_entity(model) for model in models]
|
||||
|
||||
Reference in New Issue
Block a user