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); $subQuery = 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); if ($masterUserId > 0) { $subQuery->where('u.id', '<>', $masterUserId); } $subRows = $subQuery ->field('u.id,u.account,u.username,u.phone,u.isAdmin,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'] ?: '', 'isAdmin' => (int) $row['isAdmin'], 'roleLabel'=> (int) $row['isAdmin'] === UserModel::ADMIN_STP ? '操盘手' : '客服', '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; } } }