交易流水对账明细导出完成

This commit is contained in:
2026-06-25 16:53:04 +08:00
parent 8aafd7de32
commit 30db7d3bf5
34 changed files with 2491 additions and 327 deletions
+65 -55
View File
@@ -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 ───────────────────────────────────
+21
View File
@@ -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 阶段只处理提示词优化扣费。