交易流水对账明细导出完成
This commit is contained in:
@@ -22,6 +22,7 @@ from app.models.video_engine import VideoEngine
|
||||
from app.models.image_engine import ImageEngine
|
||||
from app.models.credit_ratio import CreditRatio
|
||||
from app.models.operation_log import OperationLog
|
||||
from app.enums.user import FrontendUserKind, UserType
|
||||
from app.schemas.admin import (
|
||||
CreditAdjustRequest,
|
||||
ModelConfigCreate,
|
||||
@@ -33,6 +34,7 @@ from app.schemas.admin import (
|
||||
CreateUserRequest,
|
||||
UpdateMenusRequest,
|
||||
ResetPasswordRequest,
|
||||
UpdateFrontendUserKindRequest,
|
||||
OperationLogOut,
|
||||
)
|
||||
from app.schemas.industry import IndustryConfigCreate, IndustryConfigOut
|
||||
@@ -40,6 +42,8 @@ from app.schemas.video_engine import VideoEngineCreate, VideoEngineOut
|
||||
from app.schemas.image_engine import ImageEngineCreate, ImageEngineOut
|
||||
from app.schemas.credit_ratio import CreditRatioCreate, CreditRatioOut
|
||||
from app.services.credits import add_credits, deduct_credits
|
||||
from app.services.credit_record_meta_service import build_admin_adjust_meta
|
||||
from app.services.admin_credit_record_service import list_admin_credit_records
|
||||
from app.services.notification import create_notification
|
||||
from app.services.auth import hash_password, verify_password
|
||||
from app.services.operation_log import log_operation
|
||||
@@ -82,15 +86,24 @@ async def list_users(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=1000),
|
||||
search: str = Query(""),
|
||||
user_type: str | None = Query(None, pattern="^(frontend|admin)$"),
|
||||
frontend_user_kind: str | None = Query(None, pattern="^(internal|external)$"),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = select(User).order_by(User.created_at.desc())
|
||||
count_query = select(func.count(User.id))
|
||||
if search:
|
||||
search_filter = (User.username.ilike(f"%{search}%")) | (User.email.ilike(f"%{search}%"))
|
||||
like = f"%{search}%"
|
||||
search_filter = (User.username.ilike(like)) | (User.email.ilike(like)) | (User.phone.ilike(like))
|
||||
query = query.where(search_filter)
|
||||
count_query = count_query.where(search_filter)
|
||||
if user_type:
|
||||
query = query.where(User.user_type == user_type)
|
||||
count_query = count_query.where(User.user_type == user_type)
|
||||
if frontend_user_kind:
|
||||
query = query.where(User.user_type == UserType.FRONTEND.value, User.frontend_user_kind == frontend_user_kind)
|
||||
count_query = count_query.where(User.user_type == UserType.FRONTEND.value, User.frontend_user_kind == frontend_user_kind)
|
||||
total = (await db.execute(count_query)).scalar() or 0
|
||||
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
||||
items = result.scalars().all()
|
||||
@@ -132,6 +145,7 @@ async def create_user(
|
||||
credits=req.credits,
|
||||
is_admin=(req.user_type == "admin"),
|
||||
user_type=req.user_type,
|
||||
frontend_user_kind=req.frontend_user_kind if req.user_type == "frontend" else FrontendUserKind.EXTERNAL.value,
|
||||
allowed_menus=req.allowed_menus,
|
||||
)
|
||||
user.credits = round(user.credits, 2)
|
||||
@@ -180,9 +194,9 @@ async def adjust_credits(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if req.amount > 0:
|
||||
await add_credits(db, user_id, req.amount, f"管理员调整: {req.description}")
|
||||
await add_credits(db, user_id, req.amount, f"管理员调整: {req.description}", record_meta=build_admin_adjust_meta())
|
||||
else:
|
||||
await deduct_credits(db, user_id, abs(req.amount), f"管理员调整: {req.description}")
|
||||
await deduct_credits(db, user_id, abs(req.amount), f"管理员调整: {req.description}", record_meta=build_admin_adjust_meta())
|
||||
await create_notification(
|
||||
db, user_id, "积分变动通知",
|
||||
f"您的积分已{'增加' if req.amount > 0 else '扣除'}{abs(req.amount)}积分。原因:{req.description}",
|
||||
@@ -208,6 +222,25 @@ async def update_user_status(
|
||||
return {"message": "ok"}
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/frontend-kind", response_model=AdminUserOut)
|
||||
async def update_user_frontend_kind(
|
||||
user_id: str,
|
||||
req: UpdateFrontendUserKindRequest,
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(User).where(User.id == user_id).limit(1))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.user_type != UserType.FRONTEND.value:
|
||||
raise HTTPException(status_code=400, detail="仅前台用户支持设置内部/外部归类")
|
||||
user.frontend_user_kind = req.frontend_user_kind or FrontendUserKind.EXTERNAL.value
|
||||
await db.flush()
|
||||
await log_operation(db, admin.id, admin.username, f"设置前台用户归类为 {user.frontend_user_kind}", "PUT", f"/admin/users/{user_id}/frontend-kind")
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/users/{user_id}/reset-password")
|
||||
async def reset_user_password(
|
||||
user_id: str,
|
||||
@@ -248,66 +281,43 @@ async def admin_change_password(
|
||||
@router.get("/credit-records")
|
||||
async def list_credit_records(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=500),
|
||||
page_size: int = Query(20, ge=1, le=1000),
|
||||
user_id: str | None = Query(None),
|
||||
user_name: str | None = Query(None),
|
||||
user_type: str | None = Query(None),
|
||||
frontend_user_kind: str | None = Query(None),
|
||||
record_type: str | None = Query(None),
|
||||
type: str | None = Query(None),
|
||||
credit_subject: str | None = Query(None),
|
||||
media_type: str | None = Query(None),
|
||||
charge_kind: str | None = Query(None),
|
||||
source_module: str | None = Query(None),
|
||||
source_step_code: str | None = Query(None),
|
||||
billing_scene: str | None = Query(None),
|
||||
start_date: str = Query(None),
|
||||
end_date: str = Query(None),
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List all credit transaction records with filters."""
|
||||
query = select(CreditRecord, User.username).join(
|
||||
User, CreditRecord.user_id == User.id, isouter=True
|
||||
).order_by(CreditRecord.created_at.desc(), CreditRecord.id.desc())
|
||||
|
||||
count_query = select(func.count(CreditRecord.id))
|
||||
|
||||
if user_id:
|
||||
query = query.where(CreditRecord.user_id == user_id)
|
||||
count_query = count_query.where(CreditRecord.user_id == user_id)
|
||||
if user_name:
|
||||
query = query.where(User.username.like(f'%{user_name}%'))
|
||||
count_query = count_query.join(User, CreditRecord.user_id == User.id).where(User.username.like(f'%{user_name}%'))
|
||||
if type:
|
||||
query = query.where(CreditRecord.type == type)
|
||||
count_query = count_query.where(CreditRecord.type == type)
|
||||
|
||||
try:
|
||||
if start_date:
|
||||
date_start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
query = query.where(CreditRecord.created_at >= date_start)
|
||||
count_query = count_query.where(CreditRecord.created_at >= date_start)
|
||||
if end_date:
|
||||
date_end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
query = query.where(CreditRecord.created_at <= date_end)
|
||||
count_query = count_query.where(CreditRecord.created_at <= date_end)
|
||||
except:
|
||||
pass
|
||||
|
||||
total = (await db.execute(count_query)).scalar() or 0
|
||||
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
|
||||
rows = result.all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"items": [
|
||||
{
|
||||
"id": record.id,
|
||||
"user_id": record.user_id,
|
||||
"username": username,
|
||||
"type": record.type,
|
||||
"amount": round(record.amount, 2),
|
||||
"balance_after": round(record.balance_after, 2),
|
||||
"description": record.description,
|
||||
"related_id": record.related_id,
|
||||
"created_at": _iso(record.created_at),
|
||||
}
|
||||
for record, username in rows
|
||||
],
|
||||
}
|
||||
"""List all credit transaction records with filters and full summary."""
|
||||
return await list_admin_credit_records(
|
||||
db,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
user_id=user_id,
|
||||
user_name=user_name,
|
||||
user_type=user_type,
|
||||
frontend_user_kind=frontend_user_kind,
|
||||
record_type=record_type or type,
|
||||
credit_subject=credit_subject,
|
||||
media_type=media_type,
|
||||
charge_kind=charge_kind,
|
||||
source_module=source_module,
|
||||
source_step_code=source_step_code,
|
||||
billing_scene=billing_scene,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
|
||||
|
||||
# ── Notification Admin ───────────────────────────────────
|
||||
|
||||
@@ -35,12 +35,15 @@ from app.services.resource_accounting_service import (
|
||||
)
|
||||
from app.services.resource_signed_url_service import build_resource_signed_url
|
||||
from app.services.generation_billing_service import (
|
||||
CHARGE_TEXT_PROMPT,
|
||||
OWNER_GENERATION_RECORD,
|
||||
build_credit_biz_key,
|
||||
charge_generation_media_by_params,
|
||||
charge_generation_media_for_record,
|
||||
get_next_credit_attempt_no,
|
||||
)
|
||||
from app.services.generation_refund_service import mark_generation_record_failed_and_refund_once
|
||||
from app.services.credit_record_meta_service import build_generation_record_prompt_meta
|
||||
from app.services.video_cover_service import async_create_video_cover_for_local_video
|
||||
from app.utils.id_gen import generate_id
|
||||
from app.utils.exceptions import InsufficientCreditsError, RecordNotFoundError, InvalidStatusError
|
||||
@@ -304,9 +307,27 @@ async def optimize(
|
||||
failed_record_id = record.id
|
||||
failed_user_id = current_user.id
|
||||
try:
|
||||
prompt_attempt_no = 1
|
||||
prompt_biz_key = build_credit_biz_key(
|
||||
owner_type=OWNER_GENERATION_RECORD,
|
||||
owner_id=record.id,
|
||||
attempt_no=prompt_attempt_no,
|
||||
charge_kind=CHARGE_TEXT_PROMPT,
|
||||
action="charge",
|
||||
)
|
||||
prompt_meta = await build_generation_record_prompt_meta(
|
||||
db,
|
||||
record_id=record.id,
|
||||
attempt_no=prompt_attempt_no,
|
||||
charge_kind=CHARGE_TEXT_PROMPT,
|
||||
usage=token_usage,
|
||||
)
|
||||
await deduct_credits(
|
||||
db, current_user.id, text_credits,
|
||||
f"提示词优化 - {project.name}",
|
||||
related_id=record.id,
|
||||
biz_key=prompt_biz_key,
|
||||
record_meta=prompt_meta,
|
||||
)
|
||||
except InsufficientCreditsError as e:
|
||||
# /optimize 阶段只处理提示词优化扣费。
|
||||
|
||||
Reference in New Issue
Block a user