8c30e2c45b
- compute_usage_records加4字段(cost_amount/gross_profit/model_cost_ratio/subsidy_eligible) - 新增alembic迁移0080 - compute_internal新增subsidy-grant接口(增加用户算力余额) - deduct_by_engine_cost透传成本/毛利字段并保存
668 lines
26 KiB
Python
668 lines
26 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""算力多级折扣价格计算与余额扣费服务。
|
||
|
||
折扣体系(从高到低优先级):
|
||
1. 载体端账号折扣(tenant_user_discounts)- 载体给特定账号的折扣
|
||
2. 载体端企业折扣(park_companies.compute_discount)- 载体给下属企业的折扣
|
||
3. 运营端载体折扣(tenant_discounts)- 运营给载体的折扣(载体进货价)
|
||
4. 运营端标准价 - 未绑定载体的用户使用
|
||
|
||
折扣表示:0-100,0=无折扣,100=全免费;实际价格=标准价×(1-discount/100)
|
||
|
||
扣费优先级:
|
||
1. 企业分配的余额(优先选择折扣最低的企业)
|
||
2. 个人余额
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import select, and_
|
||
|
||
from ..infrastructure.models import (
|
||
ParkTenant, ParkCompany, CompanyMember, User,
|
||
TenantDiscount, TenantUserDiscount,
|
||
ComputeRecharge, ComputeBalanceAllocation, ComputeUsageRecord,
|
||
)
|
||
from ..infrastructure.repositories import Database
|
||
|
||
MICRO_PER_FEN = 10000 # 1分 = 10000 micro(1元=100分=1,000,000 micro)
|
||
|
||
|
||
# ── 标准价配置(兜底:compute 引擎不可达/未收录模型时使用) ──
|
||
# 真源为 compute 引擎 models 表(input_price/output_price,元/百万token),此处仅作降级。
|
||
STANDARD_PRICES = {
|
||
# model: 每1000token价格(分)
|
||
"default": 2, # 默认模型
|
||
"gpt-4": 10, # 高价模型
|
||
"gpt-3.5-turbo": 2, # 基础模型
|
||
}
|
||
|
||
|
||
async def get_standard_price(model: str, token_count: int) -> int:
|
||
"""获取标准价(分)。优先从 compute 引擎取真实单价,不可达时回退硬编码。
|
||
|
||
compute 单价单位为 元/百万token;换算为 分/1k token:per_1k_fen = avg(元/百万) / 10。
|
||
"""
|
||
from . import compute_catalog
|
||
try:
|
||
input_p, output_p, _cache_p = await compute_catalog.get_model_price(model)
|
||
except Exception: # noqa: BLE001
|
||
input_p, output_p = 0.0, 0.0
|
||
if input_p > 0 or output_p > 0:
|
||
avg_yuan_per_million = (input_p + output_p) / 2.0
|
||
per_1k_fen = avg_yuan_per_million * 100.0 / 1000.0 # 元/百万 → 分/1k
|
||
return max(1, int(per_1k_fen * token_count / 1000))
|
||
# 兜底:硬编码价
|
||
per_1k = STANDARD_PRICES.get(model, STANDARD_PRICES["default"])
|
||
return max(1, int(per_1k * token_count / 1000))
|
||
|
||
|
||
def now_str() -> str:
|
||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
|
||
def new_id() -> str:
|
||
return uuid.uuid4().hex[:24]
|
||
|
||
|
||
# ── 价格计算 ──
|
||
|
||
async def calculate_compute_price(db: Database, user_id: str, model: str, token_count: int) -> dict:
|
||
"""
|
||
计算用户使用算力的实际价格。
|
||
|
||
返回:
|
||
{
|
||
"standard_price": int, # 标准价(分)
|
||
"tenant_discount": int, # 载体折扣(0-100)
|
||
"discount": int, # 实际折扣(0-100)
|
||
"discount_source": str, # standard/tenant/company/tenant_user
|
||
"actual_price": int, # 实际价格(分)
|
||
"tenant_id": str, # 载体ID(如有)
|
||
"company_id": str, # 企业ID(如有)
|
||
}
|
||
"""
|
||
standard_price = await get_standard_price(model, token_count)
|
||
result = {
|
||
"standard_price": standard_price,
|
||
"tenant_discount": 0,
|
||
"discount": 0,
|
||
"discount_source": "standard",
|
||
"actual_price": standard_price,
|
||
"tenant_id": "",
|
||
"company_id": "",
|
||
}
|
||
|
||
# 1. 检查用户是否绑定载体(通过企业成员关系或直接绑定)
|
||
tenant_id = await _get_user_tenant_id(db, user_id)
|
||
if not tenant_id:
|
||
return result
|
||
|
||
result["tenant_id"] = tenant_id
|
||
|
||
# 2. 获取运营端给载体的折扣(载体进货价)
|
||
tenant_discount = await _get_tenant_discount(db, tenant_id)
|
||
result["tenant_discount"] = tenant_discount
|
||
|
||
# 载体基础价 = 标准价 × (1 - 载体折扣/100)
|
||
tenant_base_price = int(standard_price * (1 - tenant_discount / 100))
|
||
|
||
# 3. 检查载体端是否给该账号设置了单独折扣(优先级最高)
|
||
user_discount = await _get_tenant_user_discount(db, tenant_id, user_id)
|
||
if user_discount > 0:
|
||
actual_price = int(tenant_base_price * (1 - user_discount / 100))
|
||
result.update({
|
||
"discount": user_discount,
|
||
"discount_source": "tenant_user",
|
||
"actual_price": actual_price,
|
||
})
|
||
return result
|
||
|
||
# 4. 检查用户是否属于载体下属企业
|
||
company = await _get_user_company(db, user_id, tenant_id)
|
||
if company:
|
||
company_discount = getattr(company, "compute_discount", 0) or 0
|
||
actual_price = int(tenant_base_price * (1 - company_discount / 100))
|
||
result.update({
|
||
"discount": company_discount,
|
||
"discount_source": "company",
|
||
"actual_price": actual_price,
|
||
"company_id": company.id,
|
||
})
|
||
return result
|
||
|
||
# 5. 使用载体默认账号折扣
|
||
tenant = await db.session.get(ParkTenant, tenant_id)
|
||
default_discount = getattr(tenant, "default_user_discount", 0) or 0 if tenant else 0
|
||
if default_discount > 0:
|
||
actual_price = int(tenant_base_price * (1 - default_discount / 100))
|
||
result.update({
|
||
"discount": default_discount,
|
||
"discount_source": "tenant_default",
|
||
"actual_price": actual_price,
|
||
})
|
||
return result
|
||
|
||
# 6. 仅使用载体折扣
|
||
result.update({
|
||
"discount": tenant_discount,
|
||
"discount_source": "tenant",
|
||
"actual_price": tenant_base_price,
|
||
})
|
||
return result
|
||
|
||
|
||
async def _get_user_tenant_id(db: Database, user_id: str) -> Optional[str]:
|
||
"""获取用户绑定的载体ID(通过企业成员关系)。"""
|
||
# 通过企业成员关系查找
|
||
result = await db.session.execute(
|
||
select(CompanyMember).where(
|
||
and_(CompanyMember.user_id == user_id, CompanyMember.status == "active")
|
||
)
|
||
)
|
||
members = result.scalars().all()
|
||
for m in members:
|
||
company = await db.session.get(ParkCompany, m.company_id)
|
||
if company and company.tenant_id:
|
||
return company.tenant_id
|
||
return None
|
||
|
||
|
||
async def _get_tenant_discount(db: Database, tenant_id: str) -> int:
|
||
"""获取运营端给载体的折扣。"""
|
||
result = await db.session.execute(
|
||
select(TenantDiscount).where(TenantDiscount.tenant_id == tenant_id)
|
||
)
|
||
td = result.scalars().first()
|
||
if td:
|
||
# 检查有效期
|
||
now = now_str()
|
||
if td.effective_date and td.effective_date > now:
|
||
return 0
|
||
if td.expire_date and td.expire_date < now:
|
||
return 0
|
||
return td.discount or 0
|
||
return 0
|
||
|
||
|
||
async def _get_tenant_user_discount(db: Database, tenant_id: str, user_id: str) -> int:
|
||
"""获取载体端给特定账号的折扣。"""
|
||
result = await db.session.execute(
|
||
select(TenantUserDiscount).where(
|
||
and_(TenantUserDiscount.tenant_id == tenant_id, TenantUserDiscount.user_id == user_id)
|
||
)
|
||
)
|
||
tud = result.scalars().first()
|
||
return tud.discount if tud else 0
|
||
|
||
|
||
async def _get_user_company(db: Database, user_id: str, tenant_id: str) -> Optional[ParkCompany]:
|
||
"""获取用户在指定载体下的企业。"""
|
||
result = await db.session.execute(
|
||
select(CompanyMember).where(
|
||
and_(CompanyMember.user_id == user_id, CompanyMember.status == "active")
|
||
)
|
||
)
|
||
members = result.scalars().all()
|
||
for m in members:
|
||
company = await db.session.get(ParkCompany, m.company_id)
|
||
if company and company.tenant_id == tenant_id:
|
||
return company
|
||
return None
|
||
|
||
|
||
# ── 余额扣费 ──
|
||
|
||
async def deduct_compute_balance(
|
||
db: Database, user_id: str, model: str, token_count: int
|
||
) -> dict:
|
||
"""
|
||
算力扣费。
|
||
|
||
扣费优先级:
|
||
1. 企业分配的余额(优先选择折扣最低的企业)
|
||
2. 个人余额
|
||
|
||
返回:
|
||
{
|
||
"success": bool,
|
||
"amount": int, # 实际扣费(分)
|
||
"source": str, # company/personal
|
||
"company_id": str, # 从哪家企业扣费(如有)
|
||
"price_info": dict, # 价格计算结果
|
||
"reason": str, # 失败原因
|
||
}
|
||
"""
|
||
price_info = await calculate_compute_price(db, user_id, model, token_count)
|
||
amount = price_info["actual_price"]
|
||
|
||
# 1. 尝试从企业余额扣费
|
||
company_balances = await _get_user_company_balances(db, user_id)
|
||
# 按折扣从低到高排序(折扣越低,价格越低,优先使用)
|
||
company_balances.sort(key=lambda x: x["discount"])
|
||
|
||
for cb in company_balances:
|
||
if cb["balance"] >= amount:
|
||
success = await _deduct_from_company_balance(db, cb["company_id"], user_id, amount)
|
||
if success:
|
||
await _record_usage(db, user_id, model, token_count, price_info, amount, "company", cb["company_id"])
|
||
return {
|
||
"success": True,
|
||
"amount": amount,
|
||
"source": "company",
|
||
"company_id": cb["company_id"],
|
||
"price_info": price_info,
|
||
}
|
||
|
||
# 2. 尝试从个人余额扣费
|
||
user_balance = await _get_user_balance(db, user_id)
|
||
if user_balance >= amount:
|
||
success = await _deduct_from_user_balance(db, user_id, amount)
|
||
if success:
|
||
await _record_usage(db, user_id, model, token_count, price_info, amount, "personal", "")
|
||
return {
|
||
"success": True,
|
||
"amount": amount,
|
||
"source": "personal",
|
||
"price_info": price_info,
|
||
}
|
||
|
||
# 3. 余额不足
|
||
return {
|
||
"success": False,
|
||
"reason": "insufficient_balance",
|
||
"required": amount,
|
||
"available": user_balance,
|
||
"price_info": price_info,
|
||
}
|
||
|
||
|
||
async def _get_user_company_balances(db: Database, user_id: str) -> list:
|
||
"""获取用户所有企业的余额和折扣。"""
|
||
result = await db.session.execute(
|
||
select(CompanyMember).where(
|
||
and_(CompanyMember.user_id == user_id, CompanyMember.status == "active")
|
||
)
|
||
)
|
||
members = result.scalars().all()
|
||
balances = []
|
||
for m in members:
|
||
company = await db.session.get(ParkCompany, m.company_id)
|
||
if company:
|
||
balances.append({
|
||
"company_id": m.company_id,
|
||
"company_name": company.name,
|
||
"balance": m.compute_balance or 0,
|
||
"discount": company.compute_discount or 0,
|
||
})
|
||
return balances
|
||
|
||
|
||
async def _get_user_balance(db: Database, user_id: str) -> int:
|
||
"""获取用户个人余额(users.compute_personal_balance,单位分;仅个人充值/企业转入)。"""
|
||
user = await db.session.get(User, user_id)
|
||
if user is None:
|
||
return 0
|
||
return int(getattr(user, "compute_personal_balance", 0) or 0)
|
||
|
||
|
||
async def _deduct_from_company_balance(db: Database, company_id: str, user_id: str, amount: int) -> bool:
|
||
"""从企业分配的成员余额扣费。"""
|
||
result = await db.session.execute(
|
||
select(CompanyMember).where(
|
||
and_(CompanyMember.company_id == company_id, CompanyMember.user_id == user_id)
|
||
)
|
||
)
|
||
member = result.scalars().first()
|
||
if not member or (member.compute_balance or 0) < amount:
|
||
return False
|
||
member.compute_balance = (member.compute_balance or 0) - amount
|
||
member.compute_balance_used = (member.compute_balance_used or 0) + amount
|
||
await db.session.commit()
|
||
return True
|
||
|
||
|
||
async def _deduct_from_user_balance(db: Database, user_id: str, amount: int) -> bool:
|
||
"""从用户个人余额账本扣费(users.compute_personal_balance,单位分)。"""
|
||
from sqlalchemy import update as _sa_update
|
||
result = await db.session.execute(
|
||
_sa_update(User)
|
||
.where(User.id == user_id, User.compute_personal_balance >= amount)
|
||
.values(compute_personal_balance=User.compute_personal_balance - amount)
|
||
)
|
||
return result.rowcount > 0
|
||
|
||
|
||
async def deduct_usage_post(
|
||
db: Database, user_id: str, model: str, prompt_tokens: int, completion_tokens: int
|
||
) -> dict:
|
||
"""模型调用完成后按实际用量记账(后扣模式)。
|
||
|
||
扣费优先级(与 deduct_compute_balance 一致):
|
||
1. 企业分配的余额(按折扣从低到高)
|
||
2. 个人余额(users.compute_personal_balance)
|
||
|
||
与引擎侧扣费并行:引擎扣混合 quota(执行层),平台按来源记账(账本层)。
|
||
返回 {"ok": bool, "amount": int, "source": str, "reason": str}。
|
||
"""
|
||
token_count = max(1, int(prompt_tokens or 0) + int(completion_tokens or 0))
|
||
price_info = await calculate_compute_price(db, user_id, model, token_count)
|
||
amount = price_info["actual_price"]
|
||
if amount <= 0:
|
||
return {"ok": True, "amount": 0, "source": "free", "reason": ""}
|
||
|
||
# 1. 先扣企业分配余额(折扣从低到高,即价格从低到高)
|
||
company_balances = await _get_user_company_balances(db, user_id)
|
||
company_balances.sort(key=lambda x: x["discount"])
|
||
remaining = amount
|
||
charged_from = ""
|
||
for cb in company_balances:
|
||
if remaining <= 0:
|
||
break
|
||
take = min(remaining, cb["balance"])
|
||
if take > 0:
|
||
ok = await _deduct_from_company_balance(db, cb["company_id"], user_id, take)
|
||
if ok:
|
||
remaining -= take
|
||
charged_from = cb["company_id"]
|
||
if remaining <= 0:
|
||
await _record_usage(db, user_id, model, token_count, price_info, amount, "company", charged_from)
|
||
return {"ok": True, "amount": amount, "source": "company", "company_id": charged_from}
|
||
|
||
# 2. 企业余额不足部分,从个人余额扣
|
||
if remaining > 0:
|
||
ok = await _deduct_from_user_balance(db, user_id, remaining)
|
||
if ok:
|
||
await _record_usage(db, user_id, model, token_count, price_info, amount, "personal", "")
|
||
return {"ok": True, "amount": amount, "source": "personal", "reason": ""}
|
||
|
||
# 3. 全部不足:引擎已放行,账本记为欠费(扣到 0),不阻断
|
||
await _record_usage(db, user_id, model, token_count, price_info, amount, "personal", "")
|
||
return {
|
||
"ok": False, "amount": amount, "source": "personal",
|
||
"reason": "insufficient_balance",
|
||
}
|
||
|
||
|
||
async def check_user_balance_available(db: Database, user_id: str) -> bool:
|
||
"""请求前预检查:企业分配余额 + 个人余额合计 > 0 才放行(避免无余额仍调引擎)。"""
|
||
company_balances = await _get_user_company_balances(db, user_id)
|
||
company_total = sum(cb["balance"] for cb in company_balances)
|
||
personal = await _get_user_balance(db, user_id)
|
||
return (company_total + personal) > 0
|
||
|
||
|
||
async def deduct_by_engine_cost(
|
||
db: Database, username: str, actual_cost_micro: int,
|
||
engine_log_id: int, model_name: str = "", token_count: int = 0,
|
||
cost_micro: int = 0, model_cost_ratio: float = 1.0, subsidy_eligible: int = 1,
|
||
) -> dict:
|
||
"""compute 引擎扣费后回调:按引擎实际费用(微元)扣平台来源账本。
|
||
|
||
幂等:engine_log_id 已存在记录则直接返回成功(防网络重试重复扣)。
|
||
扣费优先级:企业分配余额(折扣从低到高)→ 个人余额。
|
||
actual_cost_micro 单位微元(1元=1e6),换算为分(1元=100分):amount_fen = micro // 10000。
|
||
cost_micro 为采购成本,model_cost_ratio 为成本比例快照,subsidy_eligible 是否参与补贴。
|
||
"""
|
||
# 幂等检查
|
||
from sqlalchemy import select as _sa_select
|
||
existing = (await db.session.execute(
|
||
_sa_select(ComputeUsageRecord).where(ComputeUsageRecord.engine_log_id == int(engine_log_id))
|
||
)).scalar_one_or_none()
|
||
if existing is not None:
|
||
return {"ok": True, "amount": existing.actual_amount, "source": existing.balance_source,
|
||
"idempotent": True, "reason": "already_recorded"}
|
||
|
||
amount_fen = max(0, int(actual_cost_micro) // MICRO_PER_FEN)
|
||
if amount_fen <= 0:
|
||
return {"ok": True, "amount": 0, "source": "free", "reason": "zero_cost"}
|
||
|
||
user = await db.users.get_by_username(username)
|
||
if not user:
|
||
return {"ok": False, "amount": 0, "source": "", "reason": "user_not_found"}
|
||
user_id = user["id"]
|
||
|
||
# 成本/毛利(微元→分)
|
||
cost_fen = max(0, int(cost_micro) // MICRO_PER_FEN)
|
||
gross_fen = max(0, amount_fen - cost_fen)
|
||
|
||
# 构造 price_info(引擎已按折扣扣费,平台侧记录 actual_amount 即可,standard_price 用 actual 反推)
|
||
price_info = {
|
||
"standard_price": amount_fen,
|
||
"tenant_discount": 0,
|
||
"discount": 0,
|
||
"discount_source": "engine",
|
||
"actual_price": amount_fen,
|
||
"tenant_id": "",
|
||
"company_id": "",
|
||
}
|
||
|
||
# 1. 先扣企业分配余额
|
||
company_balances = await _get_user_company_balances(db, user_id)
|
||
company_balances.sort(key=lambda x: x["discount"])
|
||
remaining = amount_fen
|
||
charged_from = ""
|
||
for cb in company_balances:
|
||
if remaining <= 0:
|
||
break
|
||
take = min(remaining, cb["balance"])
|
||
if take > 0:
|
||
ok = await _deduct_from_company_balance(db, cb["company_id"], user_id, take)
|
||
if ok:
|
||
remaining -= take
|
||
charged_from = cb["company_id"]
|
||
if remaining <= 0:
|
||
await _record_usage_with_engine_id(db, user_id, model_name, token_count, price_info,
|
||
amount_fen, "company", charged_from, int(engine_log_id),
|
||
cost_fen, gross_fen, model_cost_ratio, subsidy_eligible)
|
||
return {"ok": True, "amount": amount_fen, "source": "company", "company_id": charged_from}
|
||
|
||
# 2. 企业不足部分从个人余额扣
|
||
if remaining > 0:
|
||
ok = await _deduct_from_user_balance(db, user_id, remaining)
|
||
if ok:
|
||
await _record_usage_with_engine_id(db, user_id, model_name, token_count, price_info,
|
||
amount_fen, "personal", "", int(engine_log_id),
|
||
cost_fen, gross_fen, model_cost_ratio, subsidy_eligible)
|
||
return {"ok": True, "amount": amount_fen, "source": "personal", "reason": ""}
|
||
|
||
# 3. 全部不足:记为欠费(引擎已放行),不阻断
|
||
await _record_usage_with_engine_id(db, user_id, model_name, token_count, price_info,
|
||
amount_fen, "personal", "", int(engine_log_id),
|
||
cost_fen, gross_fen, model_cost_ratio, subsidy_eligible)
|
||
return {"ok": False, "amount": amount_fen, "source": "personal", "reason": "insufficient_balance"}
|
||
|
||
|
||
async def _record_usage_with_engine_id(
|
||
db: Database, user_id: str, model: str, token_count: int,
|
||
price_info: dict, amount: int, balance_source: str, company_id: str, engine_log_id: int,
|
||
cost_amount: int = 0, gross_profit: int = 0, model_cost_ratio: float = 1.0,
|
||
subsidy_eligible: int = 1,
|
||
):
|
||
"""记录算力使用(含引擎日志 id 幂等键 + 成本/毛利字段)。"""
|
||
record = ComputeUsageRecord(
|
||
id=new_id(),
|
||
user_id=user_id,
|
||
company_id=company_id,
|
||
model=model,
|
||
token_count=token_count,
|
||
standard_price=price_info["standard_price"],
|
||
discount=price_info["discount"],
|
||
discount_source=price_info["discount_source"],
|
||
actual_amount=amount,
|
||
balance_source=balance_source,
|
||
engine_log_id=engine_log_id,
|
||
cost_amount=cost_amount,
|
||
gross_profit=gross_profit,
|
||
model_cost_ratio=model_cost_ratio,
|
||
subsidy_eligible=subsidy_eligible,
|
||
created_at=now_str(),
|
||
)
|
||
db.session.add(record)
|
||
await db.session.commit()
|
||
|
||
|
||
async def _record_usage(
|
||
db: Database, user_id: str, model: str, token_count: int,
|
||
price_info: dict, amount: int, balance_source: str, company_id: str
|
||
):
|
||
"""记录算力使用。"""
|
||
record = ComputeUsageRecord(
|
||
id=new_id(),
|
||
user_id=user_id,
|
||
company_id=company_id,
|
||
model=model,
|
||
token_count=token_count,
|
||
standard_price=price_info["standard_price"],
|
||
discount=price_info["discount"],
|
||
discount_source=price_info["discount_source"],
|
||
actual_amount=amount,
|
||
balance_source=balance_source,
|
||
created_at=now_str(),
|
||
)
|
||
db.session.add(record)
|
||
await db.session.commit()
|
||
|
||
|
||
# ── 企业余额管理 ──
|
||
|
||
async def get_company_balance(db: Database, company_id: str) -> dict:
|
||
"""获取企业余额总览。"""
|
||
company = await db.session.get(ParkCompany, company_id)
|
||
if not company:
|
||
return {"error": "企业不存在"}
|
||
return {
|
||
"company_id": company.id,
|
||
"company_name": company.name,
|
||
"total_balance": company.compute_balance or 0,
|
||
"allocated_balance": company.compute_balance_used or 0,
|
||
"available_balance": (company.compute_balance or 0) - (company.compute_balance_used or 0),
|
||
}
|
||
|
||
|
||
async def allocate_balance_to_member(
|
||
db: Database, company_id: str, from_user_id: str, to_user_id: str, amount: int, reason: str = ""
|
||
) -> dict:
|
||
"""给成员分配余额。
|
||
企业扣(compute_balance_used 增加),成员个人算力增(company_members.compute_balance + compute-engine quota 同步)。
|
||
"""
|
||
company = await db.session.get(ParkCompany, company_id)
|
||
if not company:
|
||
return {"success": False, "reason": "企业不存在"}
|
||
|
||
available = (company.compute_balance or 0) - (company.compute_balance_used or 0)
|
||
if available < amount:
|
||
return {"success": False, "reason": "企业可用余额不足", "available": available}
|
||
|
||
# 查找成员关系
|
||
result = await db.session.execute(
|
||
select(CompanyMember).where(
|
||
and_(CompanyMember.company_id == company_id, CompanyMember.user_id == to_user_id)
|
||
)
|
||
)
|
||
member = result.scalars().first()
|
||
if not member:
|
||
return {"success": False, "reason": "用户不是该企业成员"}
|
||
|
||
# 获取成员 username,用于 compute-engine 同步
|
||
to_user = await db.session.get(User, to_user_id)
|
||
if not to_user:
|
||
return {"success": False, "reason": "成员用户不存在"}
|
||
|
||
# 先同步 compute-engine(增加成员 quota),成功后再提交数据库
|
||
from ..pay.service import _resolve_engine_user_id
|
||
from ..services import compute_client
|
||
total_micro = amount * MICRO_PER_FEN
|
||
try:
|
||
engine_user_id = await _resolve_engine_user_id(to_user.username or "")
|
||
if not engine_user_id:
|
||
return {"success": False, "reason": "成员算力账号未就绪"}
|
||
await compute_client.adjust_user_quota(engine_user_id, total_micro, "add")
|
||
await compute_client.sync_user_mirror(db, engine_user_id)
|
||
except Exception as exc:
|
||
return {"success": False, "reason": f"算力同步失败: {exc}"}
|
||
|
||
# 分配余额(数据库记录)
|
||
company.compute_balance_used = (company.compute_balance_used or 0) + amount
|
||
member.compute_balance = (member.compute_balance or 0) + amount
|
||
|
||
# 记录分配
|
||
allocation = ComputeBalanceAllocation(
|
||
id=new_id(),
|
||
company_id=company_id,
|
||
from_user_id=from_user_id,
|
||
to_user_id=to_user_id,
|
||
amount=amount,
|
||
type="allocate",
|
||
reason=reason,
|
||
created_at=now_str(),
|
||
)
|
||
db.session.add(allocation)
|
||
await db.session.commit()
|
||
|
||
return {"success": True, "allocation_id": allocation.id}
|
||
|
||
|
||
async def reclaim_balance_from_member(
|
||
db: Database, company_id: str, from_user_id: str, to_user_id: str, amount: int, reason: str = ""
|
||
) -> dict:
|
||
"""回收成员余额。
|
||
成员个人算力扣(company_members.compute_balance + compute-engine quota 同步),企业增(compute_balance_used 减少)。
|
||
"""
|
||
result = await db.session.execute(
|
||
select(CompanyMember).where(
|
||
and_(CompanyMember.company_id == company_id, CompanyMember.user_id == to_user_id)
|
||
)
|
||
)
|
||
member = result.scalars().first()
|
||
if not member:
|
||
return {"success": False, "reason": "用户不是该企业成员"}
|
||
|
||
if (member.compute_balance or 0) < amount:
|
||
return {"success": False, "reason": "成员余额不足", "available": member.compute_balance or 0}
|
||
|
||
company = await db.session.get(ParkCompany, company_id)
|
||
if not company:
|
||
return {"success": False, "reason": "企业不存在"}
|
||
|
||
# 获取成员 username,用于 compute-engine 同步
|
||
to_user = await db.session.get(User, to_user_id)
|
||
if not to_user:
|
||
return {"success": False, "reason": "成员用户不存在"}
|
||
|
||
# 先同步 compute-engine(扣减成员 quota),成功后再提交数据库
|
||
from ..pay.service import _resolve_engine_user_id
|
||
from ..services import compute_client
|
||
total_micro = amount * MICRO_PER_FEN
|
||
try:
|
||
engine_user_id = await _resolve_engine_user_id(to_user.username or "")
|
||
if not engine_user_id:
|
||
return {"success": False, "reason": "成员算力账号未就绪"}
|
||
await compute_client.adjust_user_quota(engine_user_id, total_micro, "subtract")
|
||
await compute_client.sync_user_mirror(db, engine_user_id)
|
||
except Exception as exc:
|
||
return {"success": False, "reason": f"算力同步失败: {exc}"}
|
||
|
||
# 回收余额(数据库记录)
|
||
member.compute_balance = (member.compute_balance or 0) - amount
|
||
company.compute_balance_used = max(0, (company.compute_balance_used or 0) - amount)
|
||
|
||
# 记录回收
|
||
allocation = ComputeBalanceAllocation(
|
||
id=new_id(),
|
||
company_id=company_id,
|
||
from_user_id=from_user_id,
|
||
to_user_id=to_user_id,
|
||
amount=amount,
|
||
type="reclaim",
|
||
reason=reason,
|
||
created_at=now_str(),
|
||
)
|
||
db.session.add(allocation)
|
||
await db.session.commit()
|
||
|
||
return {"success": True, "allocation_id": allocation.id}
|