新增积分转账功能:支持管理员手动扣除和划转用户积分
This commit is contained in:
@@ -1258,6 +1258,80 @@ class DBManager:
|
|||||||
"points_after": after,
|
"points_after": after,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def deduct_points_by_supabase_email(
|
||||||
|
self,
|
||||||
|
supabase_email: str,
|
||||||
|
amount: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
email = str(supabase_email or "").strip().lower()
|
||||||
|
points = int(amount or 0)
|
||||||
|
if not email:
|
||||||
|
return {"ok": False, "reason": "invalid_supabase_email"}
|
||||||
|
if points <= 0:
|
||||||
|
return {"ok": False, "reason": "invalid_amount"}
|
||||||
|
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT telegram_id, username, points, supabase_email
|
||||||
|
FROM users
|
||||||
|
WHERE lower(trim(COALESCE(supabase_email, ''))) = ?
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(email,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return {"ok": False, "reason": "user_not_found", "supabase_email": email}
|
||||||
|
|
||||||
|
telegram_id = int(row["telegram_id"] or 0)
|
||||||
|
before = int(row["points"] or 0)
|
||||||
|
if before < points:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"reason": "insufficient_points",
|
||||||
|
"points_available": before,
|
||||||
|
"points_needed": points,
|
||||||
|
}
|
||||||
|
after = before - points
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE users SET points = ? WHERE telegram_id = ?",
|
||||||
|
(after, telegram_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
self._sync_points_to_supabase_user_metadata(telegram_id)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"telegram_id": telegram_id,
|
||||||
|
"username": str(row["username"] or ""),
|
||||||
|
"supabase_email": str(row["supabase_email"] or email),
|
||||||
|
"points_before": before,
|
||||||
|
"points_deducted": points,
|
||||||
|
"points_after": after,
|
||||||
|
}
|
||||||
|
|
||||||
|
def transfer_points_by_email(
|
||||||
|
self,
|
||||||
|
from_email: str,
|
||||||
|
to_email: str,
|
||||||
|
amount: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Transfer points from one user to another within a single transaction."""
|
||||||
|
r_from = self.deduct_points_by_supabase_email(from_email, amount)
|
||||||
|
if not r_from.get("ok"):
|
||||||
|
return {"ok": False, "reason": f"deduct_failed: {r_from.get('reason')}", "from": r_from}
|
||||||
|
r_to = self.grant_points_by_supabase_email(to_email, amount)
|
||||||
|
if not r_to.get("ok"):
|
||||||
|
# Rollback: grant back to source
|
||||||
|
self.grant_points_by_supabase_email(from_email, amount)
|
||||||
|
return {"ok": False, "reason": f"grant_failed: {r_to.get('reason')}", "to": r_to}
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"from": r_from,
|
||||||
|
"to": r_to,
|
||||||
|
"amount": amount,
|
||||||
|
}
|
||||||
|
|
||||||
def upsert_user(self, telegram_id: int, username: str):
|
def upsert_user(self, telegram_id: int, username: str):
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from web.services.ops_api import (
|
|||||||
get_ops_weekly_leaderboard,
|
get_ops_weekly_leaderboard,
|
||||||
get_ops_user_subscriptions,
|
get_ops_user_subscriptions,
|
||||||
grant_ops_points,
|
grant_ops_points,
|
||||||
|
transfer_ops_points,
|
||||||
grant_ops_subscription,
|
grant_ops_subscription,
|
||||||
list_ops_memberships,
|
list_ops_memberships,
|
||||||
list_ops_payment_incidents,
|
list_ops_payment_incidents,
|
||||||
@@ -78,6 +79,17 @@ async def ops_grant_points(request: Request, body: GrantPointsRequest):
|
|||||||
return grant_ops_points(request, body)
|
return grant_ops_points(request, body)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/ops/users/transfer-points")
|
||||||
|
async def ops_transfer_points(request: Request):
|
||||||
|
import json as _json
|
||||||
|
body_bytes = await request.body()
|
||||||
|
body = _json.loads(body_bytes.decode("utf-8"))
|
||||||
|
from_email = str(body.get("from_email") or "").strip()
|
||||||
|
to_email = str(body.get("to_email") or "").strip()
|
||||||
|
amount = int(body.get("amount") or 0)
|
||||||
|
return transfer_ops_points(request, from_email=from_email, to_email=to_email, amount=amount)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/ops/analytics/funnel")
|
@router.get("/api/ops/analytics/funnel")
|
||||||
async def ops_analytics_funnel(request: Request, days: int = 30):
|
async def ops_analytics_funnel(request: Request, days: int = 30):
|
||||||
return get_ops_analytics_funnel(request, days=days)
|
return get_ops_analytics_funnel(request, days=days)
|
||||||
|
|||||||
@@ -239,6 +239,29 @@ def grant_ops_points(request: Request, body: GrantPointsRequest) -> Dict[str, An
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_ops_points(
|
||||||
|
request: Request,
|
||||||
|
from_email: str = "",
|
||||||
|
to_email: str = "",
|
||||||
|
amount: int = 0,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Transfer points from one user to another."""
|
||||||
|
admin = _require_ops(request) or {}
|
||||||
|
from_email = str(from_email or "").strip()
|
||||||
|
to_email = str(to_email or "").strip()
|
||||||
|
amount = int(amount or 0)
|
||||||
|
if not from_email or not to_email:
|
||||||
|
raise HTTPException(status_code=400, detail="from_email and to_email are required")
|
||||||
|
if amount <= 0:
|
||||||
|
raise HTTPException(status_code=400, detail="amount must be positive")
|
||||||
|
db = DBManager()
|
||||||
|
result = db.transfer_points_by_email(from_email, to_email, amount)
|
||||||
|
result["operator_email"] = admin.get("email")
|
||||||
|
if not result.get("ok"):
|
||||||
|
raise HTTPException(status_code=400, detail=result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_ops_analytics_funnel(request: Request, days: int = 30) -> Dict[str, Any]:
|
def get_ops_analytics_funnel(request: Request, days: int = 30) -> Dict[str, Any]:
|
||||||
_require_ops(request)
|
_require_ops(request)
|
||||||
db = DBManager()
|
db = DBManager()
|
||||||
|
|||||||
Reference in New Issue
Block a user