Files
cunkebao_v3/Server/application/superadmin/service/ProjectTokensService.php
Manus AI 5517457929 sync: 以本地为准同步全量变更至 GitHub
含四端需求文档、前端/后端/超管/触客宝迭代及部署脚本更新;未拉取远程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 04:24:56 +08:00

391 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\superadmin\service;
use app\common\model\User as UserModel;
use app\cunkebao\service\TokensService;
use think\Db;
/**
* 超管 · 项目算力管理(读写 ck_tokens_company / ck_tokens_record不改 cunkebao 代码)
*/
class ProjectTokensService
{
/** 超管充值/调整流水 form */
const FORM_ADMIN_ADJUST = 1001;
/** AI 相关扣费 formB 类) */
const AI_FORMS = [
TokensService::ACTION_AI_REWRITE,
TokensService::ACTION_AI_KEFU,
TokensService::ACTION_AI_CHAT,
];
public function getOverview(int $projectId): array
{
$ctx = ProjectContextService::resolveByProjectId($projectId);
$companyId = $ctx['companyId'];
$master = Db::name('users')
->where('companyId', $companyId)
->where('isAdmin', UserModel::ADMIN_STP)
->where('deleteTime', 0)
->field('id,account,username,phone,isAdmin')
->order('id asc')
->find();
$masterUserId = $master ? (int) $master['id'] : 0;
$masterBalance = $this->userBalance($companyId, $masterUserId, true);
$subRows = Db::name('users')->alias('u')
->leftJoin('tokens_company tc', 'tc.userId = u.id AND tc.companyId = u.companyId')
->where('u.companyId', $companyId)
->where('u.deleteTime', 0)
->where('u.isAdmin', UserModel::ADMIN_OTP)
->field('u.id,u.account,u.username,u.phone,COALESCE(tc.tokens,0) as tokens')
->order('u.id asc')
->select();
$subAccounts = [];
foreach ($subRows ?: [] as $row) {
$subAccounts[] = [
'userId' => (int) $row['id'],
'account' => $row['account'] ?: '',
'username' => $row['username'] ?: '',
'phone' => $row['phone'] ?: '',
'tokens' => (int) $row['tokens'],
];
}
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$monthStart = strtotime(date('Y-m-01 00:00:00'));
$companyTodayUsed = $this->sumConsumed($companyId, 0, $todayStart);
$companyMonthUsed = $this->sumConsumed($companyId, 0, $monthStart);
$aiTodayCount = $this->countAiUsage($companyId, $todayStart);
$aiMonthCount = $this->countAiUsage($companyId, $monthStart);
$usageBreakdown = $this->usageBreakdown($companyId, $monthStart);
return [
'projectId' => $projectId,
'companyId' => $companyId,
'projectName' => $ctx['name'],
'tokensPerYuan' => TokensService::TOKENS_PER_YUAN,
'master' => $master ? [
'userId' => $masterUserId,
'account' => $master['account'] ?: '',
'username' => $master['username'] ?: '',
'phone' => $master['phone'] ?: '',
'tokens' => $masterBalance,
] : null,
'subAccounts' => $subAccounts,
'stats' => [
'masterBalance' => $masterBalance,
'subTotalBalance'=> array_sum(array_column($subAccounts, 'tokens')),
'todayUsed' => $companyTodayUsed,
'monthUsed' => $companyMonthUsed,
'aiTodayCount' => $aiTodayCount,
'aiMonthCount' => $aiMonthCount,
],
'usageBreakdown' => $usageBreakdown,
];
}
/**
* 超管调整算力add / subtract / set / allocate主账号→子账号
*/
public function adjust(int $projectId, array $params): array
{
$companyId = ProjectContextService::resolveCompanyId($projectId);
$targetUserId = (int) ($params['userId'] ?? 0);
$mode = trim((string) ($params['mode'] ?? 'add'));
$amount = (int) ($params['amount'] ?? 0);
$remarks = trim((string) ($params['remarks'] ?? ''));
if ($targetUserId <= 0) {
throw new \Exception('请选择账号', 400);
}
$user = Db::name('users')
->where('id', $targetUserId)
->where('companyId', $companyId)
->where('deleteTime', 0)
->find();
if (!$user) {
throw new \Exception('账号不存在或不属于该项目', 404);
}
$isMaster = (int) $user['isAdmin'] === UserModel::ADMIN_STP;
if ($mode === 'allocate') {
if ($isMaster) {
throw new \Exception('不能向主账号分配', 400);
}
if ($amount <= 0) {
throw new \Exception('分配数量须大于 0', 400);
}
return $this->allocateFromMaster($companyId, $targetUserId, $amount, $remarks);
}
if (!in_array($mode, ['add', 'subtract', 'set'], true)) {
throw new \Exception('无效的调整方式', 400);
}
if ($mode !== 'set' && $amount <= 0) {
throw new \Exception('数量须大于 0', 400);
}
if ($mode === 'set' && $amount < 0) {
throw new \Exception('余额不能为负数', 400);
}
Db::startTrans();
try {
$row = $this->lockUserTokenRow($companyId, $targetUserId, $isMaster);
$current = (int) $row['tokens'];
$delta = 0;
$newBalance = $current;
if ($mode === 'add') {
$delta = $amount;
$newBalance = $current + $amount;
} elseif ($mode === 'subtract') {
if ($current < $amount) {
throw new \Exception('余额不足,当前:' . $current, 400);
}
$delta = -$amount;
$newBalance = $current - $amount;
} else {
$delta = $amount - $current;
$newBalance = $amount;
}
Db::name('tokens_company')->where('id', $row['id'])->update([
'tokens' => $newBalance,
'updateTime' => time(),
]);
if ($delta !== 0) {
Db::name('tokens_record')->insert([
'companyId' => $companyId,
'userId' => $targetUserId,
'form' => self::FORM_ADMIN_ADJUST,
'type' => $delta > 0 ? 1 : 0,
'tokens' => abs($delta),
'balanceTokens' => $newBalance,
'remarks' => $remarks ?: ('超管' . ($delta > 0 ? '增加' : '扣减') . '算力'),
'createTime' => time(),
'friendIdOrGroupId' => 0,
]);
}
Db::commit();
return [
'userId' => $targetUserId,
'balance' => $newBalance,
'delta' => $delta,
];
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
}
/** 项目列表用:主账号算力余额 + 本月 AI 调用次数 */
public static function statsForCompanies(array $companyIds): array
{
if (empty($companyIds)) {
return [];
}
$monthStart = strtotime(date('Y-m-01 00:00:00'));
$map = [];
foreach ($companyIds as $cid) {
$map[(int) $cid] = ['tokenBalance' => 0, 'aiMonthCount' => 0];
}
$balanceRows = Db::name('tokens_company')
->whereIn('companyId', $companyIds)
->where('isAdmin', 1)
->field('companyId, MAX(tokens) as tokenBalance')
->group('companyId')
->select();
foreach ($balanceRows ?: [] as $row) {
$cid = (int) $row['companyId'];
if (isset($map[$cid])) {
$map[$cid]['tokenBalance'] = (int) $row['tokenBalance'];
}
}
$aiRows = Db::name('tokens_record')
->whereIn('companyId', $companyIds)
->where('type', 0)
->whereIn('form', self::AI_FORMS)
->where('createTime', '>=', $monthStart)
->field('companyId, COUNT(*) as aiMonthCount')
->group('companyId')
->select();
foreach ($aiRows ?: [] as $row) {
$cid = (int) $row['companyId'];
if (isset($map[$cid])) {
$map[$cid]['aiMonthCount'] = (int) $row['aiMonthCount'];
}
}
return $map;
}
protected function userBalance(int $companyId, int $userId, bool $preferAdmin): int
{
if ($userId <= 0) {
return 0;
}
$query = Db::name('tokens_company')->where('companyId', $companyId)->where('userId', $userId);
if ($preferAdmin) {
$query->where('isAdmin', 1);
}
$row = $query->find();
if (!$row && $preferAdmin) {
$row = Db::name('tokens_company')->where('companyId', $companyId)->where('userId', $userId)->find();
}
return $row ? (int) $row['tokens'] : 0;
}
protected function sumConsumed(int $companyId, int $userId, int $since): int
{
$query = Db::name('tokens_record')
->where('companyId', $companyId)
->where('type', 0)
->where('createTime', '>=', $since);
if ($userId > 0) {
$query->where('userId', $userId);
}
return (int) $query->sum('tokens');
}
protected function countAiUsage(int $companyId, int $since): int
{
return (int) Db::name('tokens_record')
->where('companyId', $companyId)
->where('type', 0)
->whereIn('form', self::AI_FORMS)
->where('createTime', '>=', $since)
->count();
}
protected function usageBreakdown(int $companyId, int $since): array
{
$rows = Db::name('tokens_record')
->where('companyId', $companyId)
->where('type', 0)
->where('createTime', '>=', $since)
->field('form, COUNT(*) as useCount, SUM(tokens) as tokensUsed')
->group('form')
->order('useCount', 'desc')
->limit(20)
->select();
$rules = TokensService::rules();
$list = [];
foreach ($rows ?: [] as $row) {
$form = (int) $row['form'];
$rule = $rules[$form] ?? null;
$list[] = [
'form' => $form,
'name' => $rule['name'] ?? ('行为#' . $form),
'group' => $rule['group'] ?? '',
'isAi' => in_array($form, self::AI_FORMS, true),
'useCount' => (int) $row['useCount'],
'tokensUsed' => (int) $row['tokensUsed'],
];
}
return $list;
}
protected function lockUserTokenRow(int $companyId, int $userId, bool $isMaster): array
{
$row = Db::name('tokens_company')
->where('companyId', $companyId)
->where('userId', $userId)
->lock(true)
->find();
if ($row) {
return $row;
}
$id = Db::name('tokens_company')->insertGetId([
'userId' => $userId,
'companyId' => $companyId,
'tokens' => 0,
'isAdmin' => $isMaster ? 1 : 0,
'createTime' => time(),
'updateTime' => time(),
]);
return Db::name('tokens_company')->where('id', $id)->find();
}
protected function allocateFromMaster(int $companyId, int $targetUserId, int $amount, string $remarks): array
{
$master = Db::name('users')
->where('companyId', $companyId)
->where('isAdmin', UserModel::ADMIN_STP)
->where('deleteTime', 0)
->order('id asc')
->find();
if (!$master) {
throw new \Exception('项目主账号不存在', 404);
}
$masterUserId = (int) $master['id'];
Db::startTrans();
try {
$masterRow = $this->lockUserTokenRow($companyId, $masterUserId, true);
$masterBalance = (int) $masterRow['tokens'];
if ($masterBalance < $amount) {
throw new \Exception('主账号算力不足,当前:' . $masterBalance, 400);
}
$targetRow = $this->lockUserTokenRow($companyId, $targetUserId, false);
$masterNew = $masterBalance - $amount;
$targetNew = (int) $targetRow['tokens'] + $amount;
Db::name('tokens_company')->where('id', $masterRow['id'])->update([
'tokens' => $masterNew, 'updateTime' => time(),
]);
Db::name('tokens_company')->where('id', $targetRow['id'])->update([
'tokens' => $targetNew, 'updateTime' => time(),
]);
$note = $remarks ?: '超管分配至子账号';
Db::name('tokens_record')->insert([
'companyId' => $companyId, 'userId' => $masterUserId,
'form' => self::FORM_ADMIN_ADJUST, 'type' => 0, 'tokens' => $amount,
'balanceTokens' => $masterNew, 'remarks' => $note,
'friendIdOrGroupId' => $targetUserId, 'createTime' => time(),
]);
Db::name('tokens_record')->insert([
'companyId' => $companyId, 'userId' => $targetUserId,
'form' => self::FORM_ADMIN_ADJUST, 'type' => 1, 'tokens' => $amount,
'balanceTokens' => $targetNew, 'remarks' => $note,
'friendIdOrGroupId' => $masterUserId, 'createTime' => time(),
]);
Db::commit();
return [
'masterBalance' => $masterNew,
'receiverBalance'=> $targetNew,
'allocated' => $amount,
];
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
}
}