This commit is contained in:
2026-06-26 13:47:14 +08:00
32 changed files with 1983 additions and 1215 deletions
+2
View File
@@ -25,6 +25,7 @@ from app.api.v1.pre_test_template import router as pre_test_template_router
from app.api.v1.material_consumption import router as material_consumption_router
from app.api.v1.open_type import router as open_type_router
from app.api.v1.resources_material import router as resources_material_router
from app.api.v1.contact import router as contact_router
from app.api.admin import router as admin_module_router
api_router = APIRouter()
@@ -53,4 +54,5 @@ api_router.include_router(pre_test_template_router)
api_router.include_router(material_consumption_router)
api_router.include_router(open_type_router)
api_router.include_router(resources_material_router)
api_router.include_router(contact_router)
api_router.include_router(admin_module_router)
+26
View File
@@ -10,6 +10,7 @@ from app.dependencies import (
get_current_user_allow_password_pending,
get_db,
)
from app.models.credit_record import CreditRecord
from app.models.system_config import SystemConfig
from app.models.user import User
from app.schemas.auth import (
@@ -86,6 +87,20 @@ async def _get_register_credits(db: AsyncSession) -> int:
return int(value) if value else 100
async def _add_register_credit_record(db: AsyncSession, user: User, credits: int) -> None:
if credits <= 0:
return
record = CreditRecord(
id=generate_id(),
user_id=user.id,
type="recharge",
amount=credits,
balance_after=user.credits,
description=f"注册赠送 {credits} 积分",
)
db.add(record)
async def _handle_daily_login_credits(db: AsyncSession, user: User) -> None:
enabled_result = await db.execute(
select(SystemConfig.value).where(SystemConfig.key == "user_login_credits_enabled").limit(1)
@@ -108,6 +123,16 @@ async def _handle_daily_login_credits(db: AsyncSession, user: User) -> None:
return
user.credits += credits
record = CreditRecord(
id=generate_id(),
user_id=user.id,
type="recharge",
amount=credits,
balance_after=user.credits,
description=f"每日登录赠送 {credits} 积分",
)
db.add(record)
@router.post(
@@ -204,6 +229,7 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
db.add(user)
await db.flush()
await _add_register_credit_record(db, user, register_credits)
await _assign_default_frontend_menus(db, user)
user.credits = round(user.credits, 2)
+140
View File
@@ -0,0 +1,140 @@
from datetime import datetime, timezone, timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_db, get_current_user
from app.models.contact_request import ContactRequest
from app.models.user import User
from app.schemas.contact import ContactRequestCreate, ContactRequestListOut, ContactRequestOut
from app.utils.id_gen import generate_id
router = APIRouter(prefix="/contact", tags=["contact"])
@router.post("/request", summary="提交联系请求", status_code=status.HTTP_201_CREATED)
async def create_contact_request(
request: ContactRequestCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
today_end = today_start + timedelta(days=1)
count = await db.execute(
select(func.count(ContactRequest.id))
.where(ContactRequest.user_id == user.id)
.where(ContactRequest.created_at >= today_start)
.where(ContactRequest.created_at < today_end)
)
daily_count = count.scalar_one()
if daily_count >= 1:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="每个账号每天只能提交一次联系我们"
)
contact_request = ContactRequest(
id=generate_id(),
user_id=user.id,
phone=request.phone,
company_name=request.company_name,
industry=request.industry,
name=request.name,
message=request.message,
)
db.add(contact_request)
await db.commit()
await db.refresh(contact_request)
return {"message": "提交成功,我们会尽快与您联系"}
@router.get("/requests", summary="获取联系请求列表", response_model=ContactRequestListOut)
async def get_contact_requests(
page: int = 1,
page_size: int = 20,
is_handled: bool | None = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
query = select(ContactRequest).order_by(ContactRequest.created_at.desc())
if is_handled is not None:
query = query.where(ContactRequest.is_handled == is_handled)
offset = (page - 1) * page_size
result = await db.execute(query.offset(offset).limit(page_size))
items = result.scalars().all()
count_result = await db.execute(select(func.count(ContactRequest.id)))
total = count_result.scalar_one()
return {"items": items, "total": total}
@router.get("/requests/{request_id}", summary="获取联系请求详情", response_model=ContactRequestOut)
async def get_contact_request(
request_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
contact_request = result.scalar_one_or_none()
if not contact_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
return contact_request
@router.put("/requests/{request_id}/handle", summary="标记为已处理")
async def mark_as_handled(
request_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
contact_request = result.scalar_one_or_none()
if not contact_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
contact_request.is_handled = True
await db.commit()
await db.refresh(contact_request)
return {"message": "已标记为处理"}
@router.delete("/requests/{request_id}", summary="删除联系请求")
async def delete_contact_request(
request_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
if not user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
contact_request = result.scalar_one_or_none()
if not contact_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
await db.delete(contact_request)
await db.commit()
return {"message": "删除成功"}
+140 -132
View File
@@ -55,144 +55,152 @@ async def async_batch_upload_material(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any | dict:
if not req.tasks:
try:
if not req.tasks:
return {
"code": 0,
"message": "上传任务列表不能为空",
"task_ids": [],
"errors": [],
}
task_ids = []
errors = []
source_model_map = {
"generation_records": "GenerationRecord",
"generated_resources": None,
"chat_generation_tasks": "ChatGenerationTask",
}
for task_index, task in enumerate(req.tasks, 1):
if not task.advertiser_ids:
errors.append({
"task_index": task_index,
"error": "广告主id数组不能为空",
})
continue
if not task.resource_ids:
errors.append({
"task_index": task_index,
"error": "资源id数组不能为空",
})
continue
if task.is_pre_test == "1" and not task.pre_test_template:
errors.append({
"task_index": task_index,
"error": "开启前测功能时,必须指定前测模板id",
})
continue
if task.is_pre_test == "1":
template = await db.execute(
select(PreTestTemplate).where(PreTestTemplate.id == task.pre_test_template).
where(PreTestTemplate.deleted_at.is_(None)).
where(PreTestTemplate.user_id == current_user.id)
)
template = template.scalar_one_or_none()
if not template:
errors.append({
"task_index": task_index,
"error": "前测模板id不存在",
})
continue
target_source_model = source_model_map.get(task.source_model)
# 检查资源id是否存在,非资源id
if target_source_model:
query = (
select(GeneratedResource.id)
.where(GeneratedResource.source_model == target_source_model)
.where(GeneratedResource.source_id.in_(task.resource_ids))
.where(GeneratedResource.user_id == current_user.id)
.where(GeneratedResource.deleted_at.is_(None))
)
result = await db.execute(query)
valid_resource_ids = [row[0] for row in result.all()]
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
if invalid_ids:
invalid_ids_str = ", ".join(invalid_ids)
errors.append({
"task_index": task_index,
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
})
continue
resource_ids_to_upload = valid_resource_ids
else:
#用户提交的直接是资源id
query = (
select(GeneratedResource.id)
.where(GeneratedResource.id.in_(task.resource_ids))
.where(GeneratedResource.user_id == current_user.id)
.where(GeneratedResource.deleted_at.is_(None))
)
result = await db.execute(query)
valid_resource_ids = [row[0] for row in result.all()]
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
if invalid_ids:
invalid_ids_str = ", ".join(invalid_ids)
errors.append({
"task_index": task_index,
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
})
continue
resource_ids_to_upload = valid_resource_ids
for advertiser_id in task.advertiser_ids:
for resource_id in resource_ids_to_upload:
other_info = {}
if task.is_pre_test == "1":
other_info["is_pre_test"] = task.is_pre_test
other_info["pre_test_template"] = task.pre_test_template
task_id = generate_id()
upload_task = UploadTask(
id=task_id,
user_id=current_user.id,
oauth_id=task.oauth_id,
advertiser_id=advertiser_id,
resource_id=resource_id,
status=1,
note=None,
other_info=json.dumps(other_info) if other_info else None,
)
db.add(upload_task)
await upload_queue.enqueue(task_id)
task_ids.append(task_id)
await db.commit()
message = "上传任务已提交"
if errors:
message = f"部分任务提交成功,{len(errors)} 个任务失败"
return {
"code": 0,
"message": "上传任务列表不能为空",
"message": message,
"task_ids": task_ids,
"errors": errors,
}
except Exception as e:
return {
"code": 1,
"message": str(e),
"task_ids": [],
"errors": [],
}
task_ids = []
errors = []
source_model_map = {
"generation_records": "GenerationRecord",
"generated_resources": None,
"chat_generation_tasks": "ChatGenerationTask",
}
for task_index, task in enumerate(req.tasks, 1):
if not task.advertiser_ids:
errors.append({
"task_index": task_index,
"error": "广告主id数组不能为空",
})
continue
if not task.resource_ids:
errors.append({
"task_index": task_index,
"error": "资源id数组不能为空",
})
continue
if task.is_pre_test == "1" and not task.pre_test_template:
errors.append({
"task_index": task_index,
"error": "开启前测功能时,必须指定前测模板id",
})
continue
if task.is_pre_test == "1":
template = await db.execute(
select(PreTestTemplate).where(PreTestTemplate.id == task.pre_test_template).
where(PreTestTemplate.deleted_at.is_(None)).
where(PreTestTemplate.user_id == current_user.id)
)
template = template.scalar_one_or_none()
if not template:
errors.append({
"task_index": task_index,
"error": "前测模板id不存在",
})
continue
target_source_model = source_model_map.get(task.source_model)
# 检查资源id是否存在,非资源id
if target_source_model:
query = (
select(GeneratedResource.id)
.where(GeneratedResource.source_model == target_source_model)
.where(GeneratedResource.source_id.in_(task.resource_ids))
.where(GeneratedResource.user_id == current_user.id)
.where(GeneratedResource.deleted_at.is_(None))
)
result = await db.execute(query)
valid_resource_ids = [row[0] for row in result.all()]
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
if invalid_ids:
invalid_ids_str = ", ".join(invalid_ids)
errors.append({
"task_index": task_index,
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
})
continue
resource_ids_to_upload = valid_resource_ids
else:
#用户提交的直接是资源id
query = (
select(GeneratedResource.id)
.where(GeneratedResource.id.in_(task.resource_ids))
.where(GeneratedResource.user_id == current_user.id)
.where(GeneratedResource.deleted_at.is_(None))
)
result = await db.execute(query)
valid_resource_ids = [row[0] for row in result.all()]
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
if invalid_ids:
invalid_ids_str = ", ".join(invalid_ids)
errors.append({
"task_index": task_index,
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
})
continue
resource_ids_to_upload = valid_resource_ids
for advertiser_id in task.advertiser_ids:
for resource_id in resource_ids_to_upload:
other_info = {}
if task.is_pre_test == "1":
other_info["is_pre_test"] = task.is_pre_test
other_info["pre_test_template"] = task.pre_test_template
task_id = generate_id()
upload_task = UploadTask(
id=task_id,
user_id=current_user.id,
oauth_id=task.oauth_id,
advertiser_id=advertiser_id,
resource_id=resource_id,
status=1,
note=None,
other_info=json.dumps(other_info) if other_info else None,
)
db.add(upload_task)
await upload_queue.enqueue(task_id)
task_ids.append(task_id)
await db.commit()
message = "上传任务已提交"
if errors:
message = f"部分任务提交成功,{len(errors)} 个任务失败"
return {
"code": 0,
"message": message,
"task_ids": task_ids,
"errors": errors,
}
@router.post(
"/batch-update-filename",
summary="批量修改资源文件名",
+60 -113
View File
@@ -125,7 +125,7 @@ async def _seed_data():
from app.models.model_config import ModelConfig
from app.services.auth import hash_password
from app.utils.id_gen import generate_id
from sqlalchemy import select
from sqlalchemy import select, func
async with async_session() as db:
# Check if admin exists
@@ -354,91 +354,42 @@ async def _seed_data():
# Seed menu configs
from app.models.menu_config import MenuConfig
frontend_groups = [
("AI项目行业生成", "HomeOutlined", 0),
("AI对话生成", "HomeOutlined", 1),
("AI视频创作", "HomeOutlined", 2),
("资产管理", "HomeOutlined", 3),
("媒体关联", "HomeOutlined", 4),
]
frontend_group_ids: dict[str, str] = {}
for label, icon, order in frontend_groups:
existing = await db.execute(
select(MenuConfig).where(
MenuConfig.label == label,
MenuConfig.menu_type == "group",
MenuConfig.menu_target == "frontend",
).limit(1)
)
group = existing.scalar_one_or_none()
if group:
frontend_group_ids[label] = group.id
else:
gid = generate_id()
frontend_group_ids[label] = gid
db.add(
MenuConfig(
id=gid,
path="",
label=label,
icon=icon,
sort_order=order,
is_active=True,
parent_id=None,
menu_type="group",
menu_target="frontend",
is_default=True,
)
)
menu_count = await db.execute(select(func.count(MenuConfig.id)))
menu_count_result = menu_count.scalar_one()
logging.info(f"Menu config count: {menu_count_result}")
if menu_count_result == 0:
logging.info("Inserting default menu configs...")
frontend_menus = [
{"id": "0019eca2549ba477069", "label": "制作素材", "path": "", "icon": "PlayCircleOutlined", "sort_order": 1, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
{"id": "0019f01914a66279ec0", "label": "灵感参考", "path": "", "icon": "HomeOutlined", "sort_order": 2, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
{"id": "0019eca2735b9f3d944", "label": "我的资产", "path": "", "icon": "HomeOutlined", "sort_order": 3, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
{"id": "0019eca27ec59922048", "label": "广告素材管理", "path": "", "icon": "HomeOutlined", "sort_order": 4, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
{"id": "0019f01b5717017792f", "label": "首页", "path": "/home", "icon": "HomeOutlined", "sort_order": 0, "is_active": True, "parent_id": None, "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019f01b445f51dee00", "label": "我的项目", "path": "/projects", "icon": "AppstoreOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019f01b445f854a14a", "label": "AI创作", "path": "/conversation", "icon": "StarOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019e49af896982b070", "label": "爆款开头复刻", "path": "/initial", "icon": "CodeOutlined", "sort_order": 2, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019e4f26a8c4c0de5a", "label": "拆镜复刻", "path": "/removelens", "icon": "CameraOutlined", "sort_order": 3, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019f01b7bb6e147445", "label": "爆款榜单", "path": "/popular", "icon": "FireOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019f01914a66279ec0", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019f019267a4de06a0", "label": "创意广场", "path": "/creativeplaza", "icon": "BulbOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019f01914a66279ec0", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019e80aff6d0ea5843", "label": "素材云", "path": "/generated", "icon": "CloudOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019eca2735b9f3d944", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019f01b44606cc4c81", "label": "投放平台授权", "path": "/authorization", "icon": "UserOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019ef924a3521924ab", "label": "素材ID列表", "path": "/materials", "icon": "FileTextOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019eb61d6fc2c14ebd", "label": "消耗列表", "path": "/consume", "icon": "FileTextOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
{"id": "0019ed36864f343d347", "label": "素材前测", "path": "/pretest", "icon": "DatabaseOutlined", "sort_order": 2, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
]
for menu in frontend_menus:
db.add(MenuConfig(**menu))
frontend_pages = [
("/projects", "我的项目", "HomeOutlined", 0, "AI项目行业生成", True),
("/conversation", "AI创作", "StarOutlined", 0, "AI对话生成", True),
("/initial", "爆款开头复刻", "CodeOutlined", 0, "AI视频创作", False),
("/removelens", "拆镜复刻", "CameraOutlined", 1, "AI视频创作", False),
("/records", "项目记录", "PlayCircleOutlined", 0, "资产管理", True),
("/generated", "素材云", "CloudOutlined", 1, "资产管理", True),
("/authorization", "授权管理", "UserOutlined", 0, "媒体关联", False),
("/consume", "消耗列表", "FileTextOutlined", 1, "媒体关联", False),
]
for path, label, icon, order, group_label, is_default in frontend_pages:
existing = await db.execute(
select(MenuConfig).where(MenuConfig.path == path).limit(1)
)
if not existing.scalar_one_or_none():
db.add(
MenuConfig(
id=generate_id(),
path=path,
label=label,
icon=icon,
sort_order=order,
is_active=True,
parent_id=frontend_group_ids.get(group_label),
menu_type="page",
menu_target="frontend",
is_default=is_default,
)
)
admin_groups = [
("模型设置", "RobotOutlined", 98),
("模型配置", "RobotOutlined", 6),
("系统设置", "SettingOutlined", 99),
]
admin_group_ids: dict[str, str] = {}
for label, icon, order in admin_groups:
existing = await db.execute(
select(MenuConfig).where(
MenuConfig.label == label,
MenuConfig.menu_type == "group",
MenuConfig.menu_target == "admin",
).limit(1)
)
group = existing.scalar_one_or_none()
if group:
admin_group_ids[label] = group.id
else:
admin_groups = [
("模型设置", "RobotOutlined", 98),
("模型配置", "RobotOutlined", 6),
("系统设置", "SettingOutlined", 99),
]
admin_group_ids: dict[str, str] = {}
for label, icon, order in admin_groups:
gid = generate_id()
admin_group_ids[label] = gid
db.add(
@@ -454,34 +405,28 @@ async def _seed_data():
)
)
admin_pages = [
("/", "数据概览", "DashboardOutlined", 0, None),
("/users", "用户管理", "UserOutlined", 1, None),
("/credit-records", "交易流水", "WalletOutlined", 2, None),
("/generation-ai", "创作记录", "BulbOutlined", 3, None),
("/generation-records", "项目记录", "VideoCameraOutlined", 3, None),
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
("/notifications", "消息推送", "BellOutlined", 5, None),
("/payment-stats", "支付统计", "LineChartOutlined", 6, None),
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型设置"),
("/models", "模型配置", "RobotOutlined", 1, "模型设置"),
("/image-engines", "图片模型", "PictureOutlined", 2, "模型设置"),
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型设置"),
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
("/operation-logs", "操作日志", "DatabaseOutlined", 5, "系统设置"),
("/oauthapp-list", "授权应用", "MenuOutlined", 28, "系统设置"),
]
for path, label, icon, order, parent_group in admin_pages:
existing = await db.execute(
select(MenuConfig).where(
MenuConfig.path == path,
MenuConfig.menu_target == "admin",
).limit(1)
)
if not existing.scalar_one_or_none():
admin_pages = [
("/", "数据概览", "DashboardOutlined", 0, None),
("/users", "用户管理", "UserOutlined", 1, None),
("/credit-records", "交易流水", "WalletOutlined", 2, None),
("/generation-ai", "创作记录", "BulbOutlined", 3, None),
("/generation-records", "项目记录", "VideoCameraOutlined", 3, None),
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
("/notifications", "消息推送", "BellOutlined", 5, None),
("/payment-stats", "支付统计", "LineChartOutlined", 6, None),
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型设置"),
("/models", "模型配置", "RobotOutlined", 1, "模型设置"),
("/image-engines", "图片模型", "PictureOutlined", 2, "模型设置"),
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型设置"),
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
("/operation-logs", "操作日志", "DatabaseOutlined", 5, "系统设置"),
("/oauthapp-list", "授权应用", "MenuOutlined", 28, "系统设置"),
("/contact-requests", "联系请求", "MessageCircleOutlined", 29, "系统设置"),
]
for path, label, icon, order, parent_group in admin_pages:
db.add(
MenuConfig(
id=generate_id(),
@@ -495,6 +440,8 @@ async def _seed_data():
parent_id=admin_group_ids.get(parent_group),
)
)
logging.info("Default menu configs inserted successfully")
# Seed recharge packages
from app.models.recharge_package import RechargePackage
@@ -0,0 +1,17 @@
from sqlalchemy import Boolean, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
class ContactRequest(Base, TimestampMixin):
__tablename__ = "contact_requests"
id: Mapped[str] = mapped_column(String(32), primary_key=True)
user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id"), index=True)
phone: Mapped[str] = mapped_column(String(20), index=True)
company_name: Mapped[str] = mapped_column(String(128))
industry: Mapped[str] = mapped_column(String(64))
name: Mapped[str] = mapped_column(String(64))
message: Mapped[str | None] = mapped_column(Text, nullable=True)
is_handled: Mapped[bool] = mapped_column(Boolean, default=False)
+31
View File
@@ -0,0 +1,31 @@
from datetime import datetime
from pydantic import BaseModel, Field
class ContactRequestCreate(BaseModel):
phone: str = Field(..., description="手机号")
company_name: str = Field(..., description="公司名称")
industry: str = Field(..., description="行业")
name: str = Field(..., description="姓名")
message: str | None = Field(None, description="留言")
class ContactRequestOut(BaseModel):
id: str
user_id: str
phone: str
company_name: str
industry: str
name: str
message: str | None
is_handled: bool
created_at: datetime
class Config:
from_attributes = True
class ContactRequestListOut(BaseModel):
items: list[ContactRequestOut]
total: int
+2 -33
View File
@@ -1,5 +1,4 @@
import asyncio
import logging
from datetime import datetime, timezone
from sqlalchemy import select, update
@@ -15,42 +14,12 @@ from app.models.user_oauth_account import UserOAuthAccount
from app.models.pre_test_template import PreTestTemplate
from app.utils.id_gen import generate_id
from app.utils.douyinApi import DouyinApi
from app.utils.logger import get_logger
import os
import hashlib
import json
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
os.makedirs(LOG_DIR, exist_ok=True)
logger = logging.getLogger("upload_queue")
logger.setLevel(logging.INFO)
class DailyRotatingFileHandler(logging.FileHandler):
def __init__(self, directory, encoding=None):
self.directory = directory
filename = self._get_log_filename()
super().__init__(filename, encoding=encoding)
def _get_log_filename(self):
return os.path.join(self.directory, f"upload_queue-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.log")
def emit(self, record):
current_filename = self._get_log_filename()
if self.baseFilename != current_filename:
self.close()
self.baseFilename = current_filename
self.stream = self._open()
super().emit(record)
if not logger.handlers:
handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
logger.addHandler(handler)
logger = get_logger("upload_queue", "upload_queue")
douyin_api = DouyinApi()
#上传素材队列,处理上传素材的任务
@@ -1,7 +1,5 @@
from datetime import datetime, timezone
import asyncio
import os
import logging
import json
from sqlalchemy import select, update
@@ -11,38 +9,9 @@ from app.models.base import async_session
from app.models.resources_material import ResourcesMaterial
from app.models.user_oauth import UserOAuth
from app.utils.douyinApi import DouyinApi
from app.utils.logger import get_logger
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
os.makedirs(LOG_DIR, exist_ok=True)
#前测结果和上传素材属于一种任务,放到一起日志里边
logger = logging.getLogger("upload_queue")
logger.setLevel(logging.INFO)
class DailyRotatingFileHandler(logging.FileHandler):
def __init__(self, directory, encoding=None):
self.directory = directory
filename = self._get_log_filename()
super().__init__(filename, encoding=encoding)
def _get_log_filename(self):
return os.path.join(self.directory, f"pre_test_result_task-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.log")
def emit(self, record):
current_filename = self._get_log_filename()
if self.baseFilename != current_filename:
self.close()
self.baseFilename = current_filename
self.stream = self._open()
super().emit(record)
if not logger.handlers:
handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
logger.addHandler(handler)
logger = get_logger("pre_test_result_task", "pre_test_result_task")
douyin_api = DouyinApi()
#获取前测结果并更新数据库,计划任务,每2分钟执行一次
+2 -32
View File
@@ -2,9 +2,6 @@ from datetime import datetime, timedelta, timezone
import asyncio
import httpx
import json
import os
import logging
from logging.handlers import TimedRotatingFileHandler
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,37 +11,10 @@ from app.models.user_oauth_app import UserOAuthApp
from app.models.base import async_session
from app.config import settings
from app.utils.redis import get_redis
from app.utils.logger import get_logger
REDIS_KEY = "douyin:tokens"
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
os.makedirs(LOG_DIR, exist_ok=True)
logger = logging.getLogger("token_refresh")
logger.setLevel(logging.INFO)
def get_log_filename():
return os.path.join(LOG_DIR, f"token_refresh-{datetime.now().strftime('%Y-%m-%d')}.log")
class DailyRotatingFileHandler(logging.FileHandler):
def __init__(self, directory, encoding=None):
self.directory = directory
filename = get_log_filename()
super().__init__(filename, encoding=encoding)
def emit(self, record):
current_filename = get_log_filename()
if self.baseFilename != current_filename:
self.close()
self.baseFilename = current_filename
self.stream = self._open()
super().emit(record)
handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
logger.addHandler(handler)
logger = get_logger("token_refresh", "token_refresh")
+4 -2
View File
@@ -34,7 +34,8 @@ class DouyinApi:
oauth_id,
url,
'POST',
options
options,
request_count = 3
)
#上传视频素材
@@ -52,7 +53,8 @@ class DouyinApi:
oauth_id,
url,
'POST',
options
options,
request_count = 3
)
#获取区域信息
+27 -5
View File
@@ -11,6 +11,9 @@ from app.models.user_oauth import UserOAuth
from app.models.user_oauth_app import UserOAuthApp
from app.models.base import async_session
from app.utils.redis import get_redis
from app.utils.logger import get_logger
logger = get_logger("douyin_request", "douyin_request")
class DouyinRequest:
@@ -244,8 +247,6 @@ class DouyinRequest:
await asyncio.sleep(i * 10)
continue
res = json.dumps(data) if 'data' in locals() else ''
options_log = {}
if options:
for key, value in options.items():
@@ -254,10 +255,31 @@ class DouyinRequest:
else:
options_log[key] = value
raise RuntimeError(
f'DouYin API request failed after 5 retries. '
f'url:{url};oauthId:{oauth_id};options:{json.dumps(options_log)};response:{res}'
res = json.dumps(data) if 'data' in locals() else ''
logger.error(
f'DouYin API request failed after {request_count} retries. '
f'url:{url};method:{method};oauth_id:{oauth_id};options:{json.dumps(options_log)};response:{res}'
)
if 'data' in locals() and data.get('code', 0) != 0:
raise ValueError(f'接口返回错误[code:{data.get("code", "接口编码")}]{data.get("message", "接口返回错误")}')
else:
raise ValueError('网络错误,稍后重试。')
# options_log = {}
# if options:
# for key, value in options.items():
# if key == 'files':
# options_log[key] = {k: (v[0], 'bytes_content', v[2]) for k, v in value.items()}
# else:
# options_log[key] = value
# raise RuntimeError(
# f'DouYin API request failed after 5 retries. '
# f'url:{url};oauthId:{oauth_id};options:{json.dumps(options_log)};response:{res}'
# )
# if code != 0:
# raise ValueError(f'response:{res}')
# 无token请求
async def request_with_context(
+49
View File
@@ -0,0 +1,49 @@
import os
import logging
from datetime import datetime
def get_logger(name: str, log_filename: str) -> logging.Logger:
"""
创建并配置一个每日滚动的日志记录器
Args:
name: 日志记录器名称
log_filename: 日志文件名(不含日期和扩展名)
Returns:
配置好的日志记录器对象
"""
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
if logger.handlers:
return logger
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
os.makedirs(LOG_DIR, exist_ok=True)
class DailyRotatingFileHandler(logging.FileHandler):
def __init__(self, directory, filename_prefix, encoding=None):
self.directory = directory
self.filename_prefix = filename_prefix
filename = self._get_log_filename()
super().__init__(filename, encoding=encoding)
def _get_log_filename(self):
return os.path.join(self.directory, f"{self.filename_prefix}-{datetime.now().strftime('%Y-%m-%d')}.log")
def emit(self, record):
current_filename = self._get_log_filename()
if self.baseFilename != current_filename:
self.close()
self.baseFilename = current_filename
self.stream = self._open()
super().emit(record)
handler = DailyRotatingFileHandler(LOG_DIR, log_filename, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
logger.addHandler(handler)
return logger