Merge branch 'main' of https://github.com/fnvtk/MBTI_wang
# Conflicts: # api/app/controller/api/Analyze.php # api/app/controller/api/AppConfig.php # api/app/controller/api/Payment.php # api/app/controller/api/Test.php # miniprogram/app.js # miniprogram/pages/enterprise/index.js # miniprogram/pages/enterprise/resume-history.js # miniprogram/pages/index/result.js # miniprogram/pages/index/result.wxml # miniprogram/pages/index/result.wxss # miniprogram/pages/promo/poster.js # miniprogram/pages/recharge/index.js # miniprogram/pages/result/disc.wxss # miniprogram/pages/result/mbti.wxss # miniprogram/pages/result/pdp.wxss # miniprogram/pages/result/resume.js # miniprogram/utils/payment.js # miniprogram/utils/share.js
This commit is contained in:
@@ -1,482 +1,482 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\AiProvider as AiProviderModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* AI服务商配置管理控制器(超管专用)
|
||||
*/
|
||||
class AiConfig extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取所有AI服务商配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 列表只返回“显示”的配置(visible=1 或未设);隐藏的由数据库 visible=0 控制,不在此列表展示
|
||||
$providers = AiProviderModel::order('id', 'asc')
|
||||
->whereRaw('(visible IS NULL OR visible = 1)')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 处理返回数据
|
||||
$result = [];
|
||||
foreach ($providers as $provider) {
|
||||
$result[] = [
|
||||
'id' => $provider['providerId'],
|
||||
'name' => $provider['name'],
|
||||
'enabled' => $provider['enabled'] == 1,
|
||||
'visible' => isset($provider['visible']) ? ($provider['visible'] == 1) : true,
|
||||
'apiKey' => $provider['apiKey'] ?? '', // 脱敏后的密钥
|
||||
'apiEndpoint' => $provider['apiEndpoint'] ?? '',
|
||||
'model' => $provider['model'] ?? '',
|
||||
'organizationId' => $provider['organizationId'] ?? '',
|
||||
'maxTokens' => $provider['maxTokens'] ?? 4096,
|
||||
'balanceAlertEnabled' => $provider['balanceAlertEnabled'] == 1,
|
||||
'balanceAlertThreshold' => floatval($provider['balanceAlertThreshold'] ?? 10),
|
||||
'notes' => $provider['notes'] ?? '',
|
||||
'docUrl' => $provider['docUrl'] ?? '',
|
||||
'isFree' => $provider['isFree'] == 1,
|
||||
'supportsBalance' => $provider['supportsBalance'] == 1,
|
||||
'_hasKey' => !empty($provider['apiKey']),
|
||||
'lastBalance' => $provider['lastBalance'] ? floatval($provider['lastBalance']) : null,
|
||||
'lastBalanceCurrency' => $provider['lastBalanceCurrency'] ?? null,
|
||||
'lastBalanceCheckedAt' => $provider['lastBalanceCheckedAt'] ? date('Y-m-d H:i:s', $provider['lastBalanceCheckedAt']) : null,
|
||||
'extraConfig' => is_array($provider['extraConfig'] ?? null) ? $provider['extraConfig'] : (isset($provider['extraConfig']) && is_string($provider['extraConfig']) ? (json_decode($provider['extraConfig'], true) ?: []) : [])
|
||||
];
|
||||
}
|
||||
|
||||
return success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新AI服务商配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providerId = Request::param('providerId', '');
|
||||
$data = Request::only([
|
||||
'name', 'enabled', 'visible', 'apiKey', 'apiEndpoint', 'model', 'organizationId',
|
||||
'maxTokens', 'balanceAlertEnabled', 'balanceAlertThreshold', 'notes',
|
||||
'extraConfig'
|
||||
]);
|
||||
|
||||
if (empty($providerId)) {
|
||||
return error('服务商ID不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找服务商配置
|
||||
$provider = AiProviderModel::where('providerId', $providerId)->find();
|
||||
|
||||
if (!$provider) {
|
||||
return error('服务商配置不存在', 404);
|
||||
}
|
||||
|
||||
// 处理 enabled 字段(前端传的是布尔值)
|
||||
if (isset($data['enabled'])) {
|
||||
$data['enabled'] = $data['enabled'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// 处理 balanceAlertEnabled 字段
|
||||
if (isset($data['balanceAlertEnabled'])) {
|
||||
$data['balanceAlertEnabled'] = $data['balanceAlertEnabled'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// 处理 visible 字段(显示/隐藏,数据库直接控制)
|
||||
if (isset($data['visible'])) {
|
||||
$data['visible'] = $data['visible'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// extraConfig 可为数组或 JSON 字符串,模型 type=json 会处理
|
||||
if (isset($data['extraConfig']) && is_string($data['extraConfig'])) {
|
||||
$decoded = json_decode($data['extraConfig'], true);
|
||||
$data['extraConfig'] = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
// 如果API Key为空或包含脱敏标记(****),不更新(保持原值)
|
||||
if (isset($data['apiKey'])) {
|
||||
if (empty($data['apiKey']) || strpos($data['apiKey'], '****') !== false) {
|
||||
unset($data['apiKey']);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
$provider->save($data);
|
||||
|
||||
// 返回更新后的数据(脱敏)
|
||||
$result = [
|
||||
'id' => $provider->providerId,
|
||||
'name' => $provider->name,
|
||||
'enabled' => $provider->enabled == 1,
|
||||
'visible' => isset($provider->visible) ? ($provider->visible == 1) : true,
|
||||
'apiKey' => $provider->apiKey ?? '',
|
||||
'apiEndpoint' => $provider->apiEndpoint ?? '',
|
||||
'model' => $provider->model ?? '',
|
||||
'organizationId' => $provider->organizationId ?? '',
|
||||
'maxTokens' => $provider->maxTokens ?? 4096,
|
||||
'balanceAlertEnabled' => $provider->balanceAlertEnabled == 1,
|
||||
'balanceAlertThreshold' => floatval($provider->balanceAlertThreshold ?? 10),
|
||||
'notes' => $provider->notes ?? '',
|
||||
'isFree' => $provider->isFree == 1,
|
||||
'supportsBalance' => $provider->supportsBalance == 1,
|
||||
'_hasKey' => !empty($provider->apiKey),
|
||||
'extraConfig' => $provider->extraConfig ?? []
|
||||
];
|
||||
|
||||
return success($result, '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新AI服务商配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchUpdate()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providers = Request::param('providers', []);
|
||||
|
||||
if (empty($providers) || !is_array($providers)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$errors = [];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($providers as $providerData) {
|
||||
$providerId = $providerData['id'] ?? $providerData['providerId'] ?? '';
|
||||
|
||||
if (empty($providerId)) {
|
||||
$errors[] = '服务商ID不能为空';
|
||||
continue;
|
||||
}
|
||||
|
||||
$provider = AiProviderModel::where('providerId', $providerId)->find();
|
||||
|
||||
if (!$provider) {
|
||||
$errors[] = "服务商 {$providerId} 不存在";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [];
|
||||
if (isset($providerData['enabled'])) {
|
||||
$updateData['enabled'] = $providerData['enabled'] ? 1 : 0;
|
||||
}
|
||||
if (isset($providerData['apiKey']) && !empty($providerData['apiKey'])) {
|
||||
$updateData['apiKey'] = $providerData['apiKey'];
|
||||
}
|
||||
if (isset($providerData['apiEndpoint'])) {
|
||||
$updateData['apiEndpoint'] = $providerData['apiEndpoint'];
|
||||
}
|
||||
if (isset($providerData['model'])) {
|
||||
$updateData['model'] = $providerData['model'];
|
||||
}
|
||||
if (isset($providerData['organizationId'])) {
|
||||
$updateData['organizationId'] = $providerData['organizationId'];
|
||||
}
|
||||
if (isset($providerData['maxTokens'])) {
|
||||
$updateData['maxTokens'] = intval($providerData['maxTokens']);
|
||||
}
|
||||
if (isset($providerData['balanceAlertEnabled'])) {
|
||||
$updateData['balanceAlertEnabled'] = $providerData['balanceAlertEnabled'] ? 1 : 0;
|
||||
}
|
||||
if (isset($providerData['balanceAlertThreshold'])) {
|
||||
$updateData['balanceAlertThreshold'] = floatval($providerData['balanceAlertThreshold']);
|
||||
}
|
||||
if (isset($providerData['notes'])) {
|
||||
$updateData['notes'] = $providerData['notes'];
|
||||
}
|
||||
if (isset($providerData['visible'])) {
|
||||
$updateData['visible'] = $providerData['visible'] ? 1 : 0;
|
||||
}
|
||||
if (isset($providerData['extraConfig'])) {
|
||||
$updateData['extraConfig'] = is_array($providerData['extraConfig'])
|
||||
? $providerData['extraConfig']
|
||||
: (is_string($providerData['extraConfig']) ? json_decode($providerData['extraConfig'], true) : []);
|
||||
if (!is_array($updateData['extraConfig'])) {
|
||||
$updateData['extraConfig'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
$provider->save($updateData);
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return error('批量保存失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return error('部分配置保存失败:' . implode(';', $errors), 400);
|
||||
}
|
||||
|
||||
return success(null, "成功保存 {$successCount} 个配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询余额(单个服务商)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function queryBalance()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providerId = Request::param('providerId', '');
|
||||
|
||||
if (empty($providerId)) {
|
||||
return error('服务商ID不能为空', 400);
|
||||
}
|
||||
|
||||
$provider = AiProviderModel::where('providerId', $providerId)->find();
|
||||
|
||||
if (!$provider) {
|
||||
return error('服务商配置不存在', 404);
|
||||
}
|
||||
|
||||
if (empty($provider->apiKey)) {
|
||||
return error('请先配置 API Key', 400);
|
||||
}
|
||||
|
||||
if (!$provider->supportsBalance) {
|
||||
return error('该服务商暂不支持余额查询', 400);
|
||||
}
|
||||
|
||||
// 调用余额查询服务
|
||||
$balanceResult = $this->queryProviderBalance($provider);
|
||||
|
||||
// 更新最后查询的余额
|
||||
if ($balanceResult['status'] === 'success' && isset($balanceResult['balance'])) {
|
||||
$provider->lastBalance = $balanceResult['balance'];
|
||||
$provider->lastBalanceCurrency = $balanceResult['currency'] ?? 'CNY';
|
||||
$provider->lastBalanceCheckedAt = time();
|
||||
$provider->save();
|
||||
}
|
||||
|
||||
return success($balanceResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询余额
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function queryAllBalances()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providerIds = Request::param('providerIds', []);
|
||||
|
||||
// 如果没有指定,查询所有已启用且已配置密钥的服务商
|
||||
if (empty($providerIds)) {
|
||||
$providers = AiProviderModel::where('enabled', 1)
|
||||
->where('apiKey', '<>', '')
|
||||
->where('apiKey', '<>', null)
|
||||
->select();
|
||||
} else {
|
||||
$providers = AiProviderModel::where('providerId', 'in', $providerIds)
|
||||
->where('apiKey', '<>', '')
|
||||
->where('apiKey', '<>', null)
|
||||
->select();
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($providers as $provider) {
|
||||
if (!$provider->supportsBalance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$balanceResult = $this->queryProviderBalance($provider);
|
||||
|
||||
// 更新最后查询的余额
|
||||
if ($balanceResult['status'] === 'success' && isset($balanceResult['balance'])) {
|
||||
$provider->lastBalance = $balanceResult['balance'];
|
||||
$provider->lastBalanceCurrency = $balanceResult['currency'] ?? 'CNY';
|
||||
$provider->lastBalanceCheckedAt = time();
|
||||
$provider->save();
|
||||
}
|
||||
|
||||
$results[] = $balanceResult;
|
||||
}
|
||||
|
||||
return success($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询服务商余额(内部方法)
|
||||
* @param AiProviderModel $provider
|
||||
* @return array
|
||||
*/
|
||||
private function queryProviderBalance($provider)
|
||||
{
|
||||
// 这里需要实现各服务商的余额查询逻辑
|
||||
// 由于各服务商的API不同,这里提供一个基础框架
|
||||
|
||||
$providerId = $provider->providerId;
|
||||
$apiKey = $provider->getRawApiKey(); // 获取原始密钥用于API调用
|
||||
|
||||
// TODO: 实现各服务商的余额查询API调用
|
||||
// 目前返回模拟数据,实际需要调用各服务商的API
|
||||
|
||||
try {
|
||||
switch ($providerId) {
|
||||
case 'openai':
|
||||
// OpenAI余额查询逻辑
|
||||
return $this->queryOpenAIBalance($apiKey);
|
||||
|
||||
case 'deepseek':
|
||||
// DeepSeek余额查询逻辑
|
||||
return $this->queryDeepSeekBalance($apiKey);
|
||||
|
||||
case 'moonshot':
|
||||
// Moonshot余额查询逻辑
|
||||
return $this->queryMoonshotBalance($apiKey);
|
||||
|
||||
default:
|
||||
return [
|
||||
'providerId' => $providerId,
|
||||
'providerName' => $provider->name,
|
||||
'status' => 'unsupported',
|
||||
'message' => '该服务商暂不支持余额查询',
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'providerId' => $providerId,
|
||||
'providerName' => $provider->name,
|
||||
'status' => 'error',
|
||||
'message' => '查询失败:' . $e->getMessage(),
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询OpenAI余额
|
||||
* @param string $apiKey
|
||||
* @return array
|
||||
*/
|
||||
private function queryOpenAIBalance($apiKey)
|
||||
{
|
||||
// TODO: 实现OpenAI余额查询
|
||||
// OpenAI没有直接的余额查询API,需要通过使用情况估算
|
||||
return [
|
||||
'providerId' => 'openai',
|
||||
'providerName' => 'OpenAI (GPT)',
|
||||
'status' => 'success',
|
||||
'message' => '余额查询成功:$100.00',
|
||||
'balance' => 100.00,
|
||||
'currency' => 'USD',
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询DeepSeek余额
|
||||
* @param string $apiKey
|
||||
* @return array
|
||||
*/
|
||||
private function queryDeepSeekBalance($apiKey)
|
||||
{
|
||||
// TODO: 实现DeepSeek余额查询
|
||||
try {
|
||||
// 示例:调用DeepSeek API查询余额
|
||||
// $response = file_get_contents('https://api.deepseek.com/v1/balance', [
|
||||
// 'http' => [
|
||||
// 'method' => 'GET',
|
||||
// 'header' => "Authorization: Bearer {$apiKey}\r\n"
|
||||
// ]
|
||||
// ]);
|
||||
|
||||
return [
|
||||
'providerId' => 'deepseek',
|
||||
'providerName' => 'DeepSeek',
|
||||
'status' => 'success',
|
||||
'message' => '余额查询成功:¥500.00',
|
||||
'balance' => 500.00,
|
||||
'currency' => 'CNY',
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'providerId' => 'deepseek',
|
||||
'providerName' => 'DeepSeek',
|
||||
'status' => 'error',
|
||||
'message' => '查询失败:' . $e->getMessage(),
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询Moonshot余额
|
||||
* @param string $apiKey
|
||||
* @return array
|
||||
*/
|
||||
private function queryMoonshotBalance($apiKey)
|
||||
{
|
||||
// TODO: 实现Moonshot余额查询
|
||||
try {
|
||||
// 示例:调用Moonshot API查询余额
|
||||
return [
|
||||
'providerId' => 'moonshot',
|
||||
'providerName' => 'Moonshot (Kimi)',
|
||||
'status' => 'success',
|
||||
'message' => '余额查询成功:¥200.00',
|
||||
'balance' => 200.00,
|
||||
'currency' => 'CNY',
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'providerId' => 'moonshot',
|
||||
'providerName' => 'Moonshot (Kimi)',
|
||||
'status' => 'error',
|
||||
'message' => '查询失败:' . $e->getMessage(),
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\AiProvider as AiProviderModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* AI服务商配置管理控制器(超管专用)
|
||||
*/
|
||||
class AiConfig extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取所有AI服务商配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 列表只返回“显示”的配置(visible=1 或未设);隐藏的由数据库 visible=0 控制,不在此列表展示
|
||||
$providers = AiProviderModel::order('id', 'asc')
|
||||
->whereRaw('(visible IS NULL OR visible = 1)')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 处理返回数据
|
||||
$result = [];
|
||||
foreach ($providers as $provider) {
|
||||
$result[] = [
|
||||
'id' => $provider['providerId'],
|
||||
'name' => $provider['name'],
|
||||
'enabled' => $provider['enabled'] == 1,
|
||||
'visible' => isset($provider['visible']) ? ($provider['visible'] == 1) : true,
|
||||
'apiKey' => $provider['apiKey'] ?? '', // 脱敏后的密钥
|
||||
'apiEndpoint' => $provider['apiEndpoint'] ?? '',
|
||||
'model' => $provider['model'] ?? '',
|
||||
'organizationId' => $provider['organizationId'] ?? '',
|
||||
'maxTokens' => $provider['maxTokens'] ?? 4096,
|
||||
'balanceAlertEnabled' => $provider['balanceAlertEnabled'] == 1,
|
||||
'balanceAlertThreshold' => floatval($provider['balanceAlertThreshold'] ?? 10),
|
||||
'notes' => $provider['notes'] ?? '',
|
||||
'docUrl' => $provider['docUrl'] ?? '',
|
||||
'isFree' => $provider['isFree'] == 1,
|
||||
'supportsBalance' => $provider['supportsBalance'] == 1,
|
||||
'_hasKey' => !empty($provider['apiKey']),
|
||||
'lastBalance' => $provider['lastBalance'] ? floatval($provider['lastBalance']) : null,
|
||||
'lastBalanceCurrency' => $provider['lastBalanceCurrency'] ?? null,
|
||||
'lastBalanceCheckedAt' => $provider['lastBalanceCheckedAt'] ? date('Y-m-d H:i:s', $provider['lastBalanceCheckedAt']) : null,
|
||||
'extraConfig' => is_array($provider['extraConfig'] ?? null) ? $provider['extraConfig'] : (isset($provider['extraConfig']) && is_string($provider['extraConfig']) ? (json_decode($provider['extraConfig'], true) ?: []) : [])
|
||||
];
|
||||
}
|
||||
|
||||
return success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新AI服务商配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providerId = Request::param('providerId', '');
|
||||
$data = Request::only([
|
||||
'name', 'enabled', 'visible', 'apiKey', 'apiEndpoint', 'model', 'organizationId',
|
||||
'maxTokens', 'balanceAlertEnabled', 'balanceAlertThreshold', 'notes',
|
||||
'extraConfig'
|
||||
]);
|
||||
|
||||
if (empty($providerId)) {
|
||||
return error('服务商ID不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找服务商配置
|
||||
$provider = AiProviderModel::where('providerId', $providerId)->find();
|
||||
|
||||
if (!$provider) {
|
||||
return error('服务商配置不存在', 404);
|
||||
}
|
||||
|
||||
// 处理 enabled 字段(前端传的是布尔值)
|
||||
if (isset($data['enabled'])) {
|
||||
$data['enabled'] = $data['enabled'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// 处理 balanceAlertEnabled 字段
|
||||
if (isset($data['balanceAlertEnabled'])) {
|
||||
$data['balanceAlertEnabled'] = $data['balanceAlertEnabled'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// 处理 visible 字段(显示/隐藏,数据库直接控制)
|
||||
if (isset($data['visible'])) {
|
||||
$data['visible'] = $data['visible'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// extraConfig 可为数组或 JSON 字符串,模型 type=json 会处理
|
||||
if (isset($data['extraConfig']) && is_string($data['extraConfig'])) {
|
||||
$decoded = json_decode($data['extraConfig'], true);
|
||||
$data['extraConfig'] = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
// 如果API Key为空或包含脱敏标记(****),不更新(保持原值)
|
||||
if (isset($data['apiKey'])) {
|
||||
if (empty($data['apiKey']) || strpos($data['apiKey'], '****') !== false) {
|
||||
unset($data['apiKey']);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
$provider->save($data);
|
||||
|
||||
// 返回更新后的数据(脱敏)
|
||||
$result = [
|
||||
'id' => $provider->providerId,
|
||||
'name' => $provider->name,
|
||||
'enabled' => $provider->enabled == 1,
|
||||
'visible' => isset($provider->visible) ? ($provider->visible == 1) : true,
|
||||
'apiKey' => $provider->apiKey ?? '',
|
||||
'apiEndpoint' => $provider->apiEndpoint ?? '',
|
||||
'model' => $provider->model ?? '',
|
||||
'organizationId' => $provider->organizationId ?? '',
|
||||
'maxTokens' => $provider->maxTokens ?? 4096,
|
||||
'balanceAlertEnabled' => $provider->balanceAlertEnabled == 1,
|
||||
'balanceAlertThreshold' => floatval($provider->balanceAlertThreshold ?? 10),
|
||||
'notes' => $provider->notes ?? '',
|
||||
'isFree' => $provider->isFree == 1,
|
||||
'supportsBalance' => $provider->supportsBalance == 1,
|
||||
'_hasKey' => !empty($provider->apiKey),
|
||||
'extraConfig' => $provider->extraConfig ?? []
|
||||
];
|
||||
|
||||
return success($result, '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新AI服务商配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchUpdate()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providers = Request::param('providers', []);
|
||||
|
||||
if (empty($providers) || !is_array($providers)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$errors = [];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($providers as $providerData) {
|
||||
$providerId = $providerData['id'] ?? $providerData['providerId'] ?? '';
|
||||
|
||||
if (empty($providerId)) {
|
||||
$errors[] = '服务商ID不能为空';
|
||||
continue;
|
||||
}
|
||||
|
||||
$provider = AiProviderModel::where('providerId', $providerId)->find();
|
||||
|
||||
if (!$provider) {
|
||||
$errors[] = "服务商 {$providerId} 不存在";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [];
|
||||
if (isset($providerData['enabled'])) {
|
||||
$updateData['enabled'] = $providerData['enabled'] ? 1 : 0;
|
||||
}
|
||||
if (isset($providerData['apiKey']) && !empty($providerData['apiKey'])) {
|
||||
$updateData['apiKey'] = $providerData['apiKey'];
|
||||
}
|
||||
if (isset($providerData['apiEndpoint'])) {
|
||||
$updateData['apiEndpoint'] = $providerData['apiEndpoint'];
|
||||
}
|
||||
if (isset($providerData['model'])) {
|
||||
$updateData['model'] = $providerData['model'];
|
||||
}
|
||||
if (isset($providerData['organizationId'])) {
|
||||
$updateData['organizationId'] = $providerData['organizationId'];
|
||||
}
|
||||
if (isset($providerData['maxTokens'])) {
|
||||
$updateData['maxTokens'] = intval($providerData['maxTokens']);
|
||||
}
|
||||
if (isset($providerData['balanceAlertEnabled'])) {
|
||||
$updateData['balanceAlertEnabled'] = $providerData['balanceAlertEnabled'] ? 1 : 0;
|
||||
}
|
||||
if (isset($providerData['balanceAlertThreshold'])) {
|
||||
$updateData['balanceAlertThreshold'] = floatval($providerData['balanceAlertThreshold']);
|
||||
}
|
||||
if (isset($providerData['notes'])) {
|
||||
$updateData['notes'] = $providerData['notes'];
|
||||
}
|
||||
if (isset($providerData['visible'])) {
|
||||
$updateData['visible'] = $providerData['visible'] ? 1 : 0;
|
||||
}
|
||||
if (isset($providerData['extraConfig'])) {
|
||||
$updateData['extraConfig'] = is_array($providerData['extraConfig'])
|
||||
? $providerData['extraConfig']
|
||||
: (is_string($providerData['extraConfig']) ? json_decode($providerData['extraConfig'], true) : []);
|
||||
if (!is_array($updateData['extraConfig'])) {
|
||||
$updateData['extraConfig'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
$provider->save($updateData);
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return error('批量保存失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return error('部分配置保存失败:' . implode(';', $errors), 400);
|
||||
}
|
||||
|
||||
return success(null, "成功保存 {$successCount} 个配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询余额(单个服务商)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function queryBalance()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providerId = Request::param('providerId', '');
|
||||
|
||||
if (empty($providerId)) {
|
||||
return error('服务商ID不能为空', 400);
|
||||
}
|
||||
|
||||
$provider = AiProviderModel::where('providerId', $providerId)->find();
|
||||
|
||||
if (!$provider) {
|
||||
return error('服务商配置不存在', 404);
|
||||
}
|
||||
|
||||
if (empty($provider->apiKey)) {
|
||||
return error('请先配置 API Key', 400);
|
||||
}
|
||||
|
||||
if (!$provider->supportsBalance) {
|
||||
return error('该服务商暂不支持余额查询', 400);
|
||||
}
|
||||
|
||||
// 调用余额查询服务
|
||||
$balanceResult = $this->queryProviderBalance($provider);
|
||||
|
||||
// 更新最后查询的余额
|
||||
if ($balanceResult['status'] === 'success' && isset($balanceResult['balance'])) {
|
||||
$provider->lastBalance = $balanceResult['balance'];
|
||||
$provider->lastBalanceCurrency = $balanceResult['currency'] ?? 'CNY';
|
||||
$provider->lastBalanceCheckedAt = time();
|
||||
$provider->save();
|
||||
}
|
||||
|
||||
return success($balanceResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询余额
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function queryAllBalances()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$providerIds = Request::param('providerIds', []);
|
||||
|
||||
// 如果没有指定,查询所有已启用且已配置密钥的服务商
|
||||
if (empty($providerIds)) {
|
||||
$providers = AiProviderModel::where('enabled', 1)
|
||||
->where('apiKey', '<>', '')
|
||||
->where('apiKey', '<>', null)
|
||||
->select();
|
||||
} else {
|
||||
$providers = AiProviderModel::where('providerId', 'in', $providerIds)
|
||||
->where('apiKey', '<>', '')
|
||||
->where('apiKey', '<>', null)
|
||||
->select();
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($providers as $provider) {
|
||||
if (!$provider->supportsBalance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$balanceResult = $this->queryProviderBalance($provider);
|
||||
|
||||
// 更新最后查询的余额
|
||||
if ($balanceResult['status'] === 'success' && isset($balanceResult['balance'])) {
|
||||
$provider->lastBalance = $balanceResult['balance'];
|
||||
$provider->lastBalanceCurrency = $balanceResult['currency'] ?? 'CNY';
|
||||
$provider->lastBalanceCheckedAt = time();
|
||||
$provider->save();
|
||||
}
|
||||
|
||||
$results[] = $balanceResult;
|
||||
}
|
||||
|
||||
return success($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询服务商余额(内部方法)
|
||||
* @param AiProviderModel $provider
|
||||
* @return array
|
||||
*/
|
||||
private function queryProviderBalance($provider)
|
||||
{
|
||||
// 这里需要实现各服务商的余额查询逻辑
|
||||
// 由于各服务商的API不同,这里提供一个基础框架
|
||||
|
||||
$providerId = $provider->providerId;
|
||||
$apiKey = $provider->getRawApiKey(); // 获取原始密钥用于API调用
|
||||
|
||||
// TODO: 实现各服务商的余额查询API调用
|
||||
// 目前返回模拟数据,实际需要调用各服务商的API
|
||||
|
||||
try {
|
||||
switch ($providerId) {
|
||||
case 'openai':
|
||||
// OpenAI余额查询逻辑
|
||||
return $this->queryOpenAIBalance($apiKey);
|
||||
|
||||
case 'deepseek':
|
||||
// DeepSeek余额查询逻辑
|
||||
return $this->queryDeepSeekBalance($apiKey);
|
||||
|
||||
case 'moonshot':
|
||||
// Moonshot余额查询逻辑
|
||||
return $this->queryMoonshotBalance($apiKey);
|
||||
|
||||
default:
|
||||
return [
|
||||
'providerId' => $providerId,
|
||||
'providerName' => $provider->name,
|
||||
'status' => 'unsupported',
|
||||
'message' => '该服务商暂不支持余额查询',
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'providerId' => $providerId,
|
||||
'providerName' => $provider->name,
|
||||
'status' => 'error',
|
||||
'message' => '查询失败:' . $e->getMessage(),
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询OpenAI余额
|
||||
* @param string $apiKey
|
||||
* @return array
|
||||
*/
|
||||
private function queryOpenAIBalance($apiKey)
|
||||
{
|
||||
// TODO: 实现OpenAI余额查询
|
||||
// OpenAI没有直接的余额查询API,需要通过使用情况估算
|
||||
return [
|
||||
'providerId' => 'openai',
|
||||
'providerName' => 'OpenAI (GPT)',
|
||||
'status' => 'success',
|
||||
'message' => '余额查询成功:$100.00',
|
||||
'balance' => 100.00,
|
||||
'currency' => 'USD',
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询DeepSeek余额
|
||||
* @param string $apiKey
|
||||
* @return array
|
||||
*/
|
||||
private function queryDeepSeekBalance($apiKey)
|
||||
{
|
||||
// TODO: 实现DeepSeek余额查询
|
||||
try {
|
||||
// 示例:调用DeepSeek API查询余额
|
||||
// $response = file_get_contents('https://api.deepseek.com/v1/balance', [
|
||||
// 'http' => [
|
||||
// 'method' => 'GET',
|
||||
// 'header' => "Authorization: Bearer {$apiKey}\r\n"
|
||||
// ]
|
||||
// ]);
|
||||
|
||||
return [
|
||||
'providerId' => 'deepseek',
|
||||
'providerName' => 'DeepSeek',
|
||||
'status' => 'success',
|
||||
'message' => '余额查询成功:¥500.00',
|
||||
'balance' => 500.00,
|
||||
'currency' => 'CNY',
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'providerId' => 'deepseek',
|
||||
'providerName' => 'DeepSeek',
|
||||
'status' => 'error',
|
||||
'message' => '查询失败:' . $e->getMessage(),
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询Moonshot余额
|
||||
* @param string $apiKey
|
||||
* @return array
|
||||
*/
|
||||
private function queryMoonshotBalance($apiKey)
|
||||
{
|
||||
// TODO: 实现Moonshot余额查询
|
||||
try {
|
||||
// 示例:调用Moonshot API查询余额
|
||||
return [
|
||||
'providerId' => 'moonshot',
|
||||
'providerName' => 'Moonshot (Kimi)',
|
||||
'status' => 'success',
|
||||
'message' => '余额查询成功:¥200.00',
|
||||
'balance' => 200.00,
|
||||
'currency' => 'CNY',
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'providerId' => 'moonshot',
|
||||
'providerName' => 'Moonshot (Kimi)',
|
||||
'status' => 'error',
|
||||
'message' => '查询失败:' . $e->getMessage(),
|
||||
'balance' => null,
|
||||
'currency' => null,
|
||||
'checkedAt' => date('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,140 +1,140 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\service\JwtService;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 超级管理员认证控制器
|
||||
*/
|
||||
class Auth extends BaseController
|
||||
{
|
||||
/**
|
||||
* 超级管理员登录
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$username = Request::param('username', '');
|
||||
$password = Request::param('password', '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
return error('用户名和密码不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户(只允许超级管理员登录)
|
||||
$user = Db::name('users')
|
||||
->where('username', $username)
|
||||
->where('role', 'superadmin')
|
||||
->find();
|
||||
|
||||
if (!$user) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if ($user['status'] != 1) {
|
||||
return error('账号已被禁用', 403);
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
Db::name('users')
|
||||
->where('id', $user['id'])
|
||||
->update([
|
||||
'lastLoginTime' => time(),
|
||||
'lastLoginIp' => Request::ip(),
|
||||
'updatedAt' => time()
|
||||
]);
|
||||
|
||||
// 生成Token
|
||||
$payload = [
|
||||
'userId' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role']
|
||||
];
|
||||
|
||||
$token = JwtService::generateToken($payload);
|
||||
|
||||
unset($user['password']);
|
||||
|
||||
return success([
|
||||
'token' => $token,
|
||||
'expiresIn' => config('jwt.expire'),
|
||||
'user' => $user
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录超级管理员信息(需要认证)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function me()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 验证是否为超级管理员
|
||||
if ($user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
if (!$userModel) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
unset($userModel['password']);
|
||||
|
||||
return success($userModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录(需要认证)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if ($user && isset($user['userId'])) {
|
||||
JwtService::deleteToken($user['userId']);
|
||||
}
|
||||
|
||||
return success(null, '退出成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
$token = JwtService::getTokenFromRequest($this->request);
|
||||
|
||||
if (!$token) {
|
||||
return error('未提供Token', 401);
|
||||
}
|
||||
|
||||
$newToken = JwtService::refreshToken($token);
|
||||
|
||||
if (!$newToken) {
|
||||
return error('Token无效或已过期', 401);
|
||||
}
|
||||
|
||||
return success([
|
||||
'token' => $newToken,
|
||||
'expiresIn' => config('jwt.expire')
|
||||
], '刷新成功');
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\service\JwtService;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 超级管理员认证控制器
|
||||
*/
|
||||
class Auth extends BaseController
|
||||
{
|
||||
/**
|
||||
* 超级管理员登录
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$username = Request::param('username', '');
|
||||
$password = Request::param('password', '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
return error('用户名和密码不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户(只允许超级管理员登录)
|
||||
$user = Db::name('users')
|
||||
->where('username', $username)
|
||||
->where('role', 'superadmin')
|
||||
->find();
|
||||
|
||||
if (!$user) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if ($user['status'] != 1) {
|
||||
return error('账号已被禁用', 403);
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
Db::name('users')
|
||||
->where('id', $user['id'])
|
||||
->update([
|
||||
'lastLoginTime' => time(),
|
||||
'lastLoginIp' => Request::ip(),
|
||||
'updatedAt' => time()
|
||||
]);
|
||||
|
||||
// 生成Token
|
||||
$payload = [
|
||||
'userId' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role']
|
||||
];
|
||||
|
||||
$token = JwtService::generateToken($payload);
|
||||
|
||||
unset($user['password']);
|
||||
|
||||
return success([
|
||||
'token' => $token,
|
||||
'expiresIn' => config('jwt.expire'),
|
||||
'user' => $user
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录超级管理员信息(需要认证)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function me()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 验证是否为超级管理员
|
||||
if ($user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
if (!$userModel) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
unset($userModel['password']);
|
||||
|
||||
return success($userModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录(需要认证)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if ($user && isset($user['userId'])) {
|
||||
JwtService::deleteToken($user['userId']);
|
||||
}
|
||||
|
||||
return success(null, '退出成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
$token = JwtService::getTokenFromRequest($this->request);
|
||||
|
||||
if (!$token) {
|
||||
return error('未提供Token', 401);
|
||||
}
|
||||
|
||||
$newToken = JwtService::refreshToken($token);
|
||||
|
||||
if (!$newToken) {
|
||||
return error('Token无效或已过期', 401);
|
||||
}
|
||||
|
||||
return success([
|
||||
'token' => $newToken,
|
||||
'expiresIn' => config('jwt.expire')
|
||||
], '刷新成功');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,433 +1,433 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 分销管理控制器(超管端 - 个人版分销)
|
||||
* 路由前缀:/api/v1/superadmin/distribution
|
||||
*/
|
||||
class Distribution extends BaseController
|
||||
{
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/overview 全平台分销数据概览
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function overview()
|
||||
{
|
||||
try {
|
||||
$now = time();
|
||||
|
||||
$totalCommission = Db::name('commission_records')->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0;
|
||||
$paidCommission = Db::name('commission_records')->where('status', 'paid')->sum('commissionFen') ?: 0;
|
||||
$frozenCommission = Db::name('commission_records')->where('status', 'frozen')->sum('commissionFen') ?: 0;
|
||||
$totalOrders = Db::name('commission_records')->whereIn('status', ['paid', 'frozen'])->count();
|
||||
|
||||
$personalCommission = Db::name('commission_records')->where('scope', 'personal')
|
||||
->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0;
|
||||
$enterpriseCommission = Db::name('commission_records')->where('scope', 'enterprise')
|
||||
->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0;
|
||||
|
||||
$bindingCount = Db::name('distribution_bindings')
|
||||
->where('status', 'active')
|
||||
->where('expireAt', '>', $now)
|
||||
->count();
|
||||
|
||||
// 待处理提现:status=0 审核中
|
||||
$pendingWithdraw = Db::name('distribution_withdrawals')
|
||||
->where('status', 0)
|
||||
->sum('amountFen') ?: 0;
|
||||
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayCommission = Db::name('commission_records')
|
||||
->where('status', 'paid')
|
||||
->where('paidAt', '>=', $todayStart)
|
||||
->sum('commissionFen') ?: 0;
|
||||
|
||||
return success([
|
||||
'totalCommission' => number_format($totalCommission / 100, 2, '.', ''),
|
||||
'paidCommission' => number_format($paidCommission / 100, 2, '.', ''),
|
||||
'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''),
|
||||
'personalCommission' => number_format($personalCommission / 100, 2, '.', ''),
|
||||
'enterpriseCommission'=> number_format($enterpriseCommission / 100, 2, '.', ''),
|
||||
'totalOrders' => $totalOrders,
|
||||
'bindingCount' => $bindingCount,
|
||||
'pendingWithdraw' => number_format($pendingWithdraw / 100, 2, '.', ''),
|
||||
'todayCommission' => number_format($todayCommission / 100, 2, '.', ''),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取数据失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/bindings 全平台绑定记录
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function bindings()
|
||||
{
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$scope = Request::param('scope', '');
|
||||
$status = Request::param('status', '');
|
||||
$enterpriseId = (int) Request::param('enterpriseId', 0);
|
||||
|
||||
try {
|
||||
$query = Db::name('distribution_bindings')
|
||||
->alias('b')
|
||||
->leftJoin('wechat_users inv', 'b.inviterId = inv.id')
|
||||
->leftJoin('wechat_users invt', 'b.inviteeId = invt.id')
|
||||
->leftJoin('enterprises e', 'b.enterpriseId = e.id')
|
||||
->field('b.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName');
|
||||
|
||||
if ($scope) $query->where('b.scope', $scope);
|
||||
if ($status) $query->where('b.status', $status);
|
||||
if ($enterpriseId) $query->where('b.enterpriseId', $enterpriseId);
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('b.updatedAt', 'desc')->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
$now = time();
|
||||
foreach ($list as &$row) {
|
||||
$row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400));
|
||||
$row['inviterName'] = $row['inviterName'] ?: '未知';
|
||||
$row['inviteeName'] = $row['inviteeName'] ?: '未知';
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取绑定记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/commissions 全平台佣金记录
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function commissions()
|
||||
{
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$scope = Request::param('scope', '');
|
||||
$status = Request::param('status', '');
|
||||
|
||||
try {
|
||||
$query = Db::name('commission_records')
|
||||
->alias('c')
|
||||
->leftJoin('wechat_users inv', 'c.inviterId = inv.id')
|
||||
->leftJoin('wechat_users invt', 'c.inviteeId = invt.id')
|
||||
->leftJoin('enterprises e', 'c.enterpriseId = e.id')
|
||||
->field('c.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName');
|
||||
|
||||
if ($scope) $query->where('c.scope', $scope);
|
||||
if ($status) $query->where('c.status', $status);
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('c.createdAt', 'desc')->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', '');
|
||||
$row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', '');
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取佣金记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/withdrawals 全平台提现申请
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function withdrawals()
|
||||
{
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$status = Request::param('status', '');
|
||||
|
||||
try {
|
||||
$query = Db::name('distribution_withdrawals')
|
||||
->alias('w')
|
||||
->leftJoin('wechat_users u', 'w.userId = u.id')
|
||||
->field('w.*, u.nickname, u.avatar');
|
||||
|
||||
if ($status !== '') {
|
||||
// 支持字符串或数字,统一转 int
|
||||
$query->where('w.status', (int)$status);
|
||||
}
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('w.createdAt', 'desc')->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', '');
|
||||
$row['nickname'] = $row['nickname'] ?: '未知用户';
|
||||
|
||||
// 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常)
|
||||
$code = (int) ($row['status'] ?? 0);
|
||||
$row['status'] = $code;
|
||||
|
||||
// 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期
|
||||
switch ($code) {
|
||||
case 0:
|
||||
$row['statusLabel'] = '审核中';
|
||||
break;
|
||||
case 1:
|
||||
$row['statusLabel'] = '已驳回';
|
||||
break;
|
||||
case 2:
|
||||
$row['statusLabel'] = '待收款';
|
||||
break;
|
||||
case 3:
|
||||
$row['statusLabel'] = '已收款';
|
||||
break;
|
||||
case 4:
|
||||
$row['statusLabel'] = '已过期';
|
||||
break;
|
||||
default:
|
||||
$row['statusLabel'] = '未知';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取提现记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// POST distribution/withdrawals/:id/approve 审核通过提现
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function approveWithdrawal(int $id)
|
||||
{
|
||||
$note = Request::param('note', '');
|
||||
$now = time();
|
||||
|
||||
$record = Db::name('distribution_withdrawals')
|
||||
->alias('w')
|
||||
->leftJoin('wechat_users u', 'w.userId = u.id')
|
||||
->field('w.*, u.openid')
|
||||
->where('w.id', $id)
|
||||
->find();
|
||||
// 仅允许处理审核中(status=0)的记录
|
||||
if (!$record || (int)$record['status'] !== 0) {
|
||||
return error('提现申请不存在或已处理', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成商户单号:TX + 时间戳 + 随机数 + 提现ID(示例:TX202603121526520005123)
|
||||
$outBillNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999) . $record['id'];
|
||||
|
||||
// 调用微信商家转账到零钱接口(参数对齐 ckb-admin Withdrawal::handleWechatPay)
|
||||
$service = new \app\common\service\WechatTransferService();
|
||||
$result = $service->createTransfer([
|
||||
'out_bill_no' => $outBillNo,
|
||||
'openid' => $record['openid'],
|
||||
'transfer_amount' => (int) $record['amountFen'], // 单位:分
|
||||
'transfer_remark' => '推广佣金提现',
|
||||
'transfer_scene_id' => env('TRANSFER_SCENE_ID', '1005'),
|
||||
'transfer_scene_report_infos' => [
|
||||
[
|
||||
'info_type' => '岗位类型',
|
||||
'info_content' => '推广人员',
|
||||
],
|
||||
[
|
||||
'info_type' => '报酬说明',
|
||||
'info_content' => '推广佣金提现',
|
||||
],
|
||||
],
|
||||
'notify_url' => env('WITHDRAW_NOTIFY_URL', ''), // 可选:提现专用回调
|
||||
]);
|
||||
|
||||
if ($result['success'] !== true) {
|
||||
$err = $result['error'] ?? [];
|
||||
$code = $err['code'] ?? 'UNKNOWN';
|
||||
$msg = $err['message'] ?? '微信转账接口调用失败';
|
||||
return error("微信转账发起失败({$code}):{$msg}", 500);
|
||||
}
|
||||
|
||||
$wechatData = $result['data'] ?? [];
|
||||
Db::name('distribution_withdrawals')->where('id', $id)->update([
|
||||
// 2=待收款(已发起转账,等待用户确认)
|
||||
'status' => 2,
|
||||
'auditNote' => $note,
|
||||
'auditAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
'pay_type' => 'wechat',
|
||||
'out_bill_no' => $outBillNo,
|
||||
'transfer_bill_no' => $wechatData['transfer_bill_no'] ?? null,
|
||||
'wechat_pay_state' => $wechatData['state'] ?? 'PROCESSING',
|
||||
'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'),
|
||||
'package_info' => $wechatData['package_info'] ?? '',
|
||||
'mch_id' => env('MCH_ID', null),
|
||||
]);
|
||||
return success(null, '审核通过,已发起微信转账');
|
||||
} catch (\Exception $e) {
|
||||
return error('操作失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// POST distribution/withdrawals/:id/reject 拒绝提现
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function rejectWithdrawal(int $id)
|
||||
{
|
||||
$note = Request::param('note', '');
|
||||
$now = time();
|
||||
|
||||
$record = Db::name('distribution_withdrawals')->where('id', $id)->find();
|
||||
// 仅允许处理审核中(status=0)的记录
|
||||
if (!$record || (int)$record['status'] !== 0) {
|
||||
return error('提现申请不存在或已处理', 400);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('wechat_users')
|
||||
->where('id', $record['userId'])
|
||||
->inc('walletBalance', $record['amountFen'])
|
||||
->update(['updatedAt' => $now]);
|
||||
|
||||
Db::name('distribution_withdrawals')->where('id', $id)->update([
|
||||
// 1=已驳回
|
||||
'status' => 1,
|
||||
'auditNote' => $note,
|
||||
'auditAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
return success(null, '已拒绝,余额已退回');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return error('操作失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/settings 个人版分销全局配置
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function settings()
|
||||
{
|
||||
try {
|
||||
$config = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find();
|
||||
$default = [
|
||||
'enabled' => true,
|
||||
'promoCenterTitle' => '推广中心',
|
||||
'bindingDays' => 30,
|
||||
'minWithdrawFen' => 100,
|
||||
'maxWithdrawFen' => 0,
|
||||
'requireAudit' => true,
|
||||
'withdrawFee' => 0,
|
||||
'testSettings' => self::defaultTestSettings(),
|
||||
];
|
||||
if ($config && $config['value']) {
|
||||
$settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value'];
|
||||
$settings = array_merge($default, $settings ?? []);
|
||||
} else {
|
||||
$settings = $default;
|
||||
}
|
||||
$settings['minWithdraw'] = round((float)($settings['minWithdrawFen'] ?? 100) / 100, 2);
|
||||
$settings['maxWithdraw'] = ($max = (int)($settings['maxWithdrawFen'] ?? 0)) > 0 ? round($max / 100, 2) : 0;
|
||||
$settings['testSettings'] = self::appendTestSettingsAmount($settings['testSettings'] ?? self::defaultTestSettings());
|
||||
return success($settings);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// PUT distribution/settings 更新个人版分销全局配置
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function updateSettings()
|
||||
{
|
||||
$settings = Request::only([
|
||||
'enabled', 'promoCenterTitle', 'bindingDays',
|
||||
'minWithdrawFen', 'minWithdraw', 'maxWithdrawFen', 'maxWithdraw',
|
||||
'requireAudit', 'withdrawFee', 'testSettings'
|
||||
]);
|
||||
|
||||
$minWithdrawFen = isset($settings['minWithdraw'])
|
||||
? (int) round((float)$settings['minWithdraw'] * 100)
|
||||
: (int)($settings['minWithdrawFen'] ?? 100);
|
||||
$maxWithdrawFen = isset($settings['maxWithdraw'])
|
||||
? (int) round((float)$settings['maxWithdraw'] * 100)
|
||||
: (int)($settings['maxWithdrawFen'] ?? 0);
|
||||
$minWithdrawFen = max(100, min(20000, $minWithdrawFen));
|
||||
$maxWithdrawFen = $maxWithdrawFen > 0 ? min(20000, max(100, $maxWithdrawFen)) : 0;
|
||||
|
||||
$promoTitle = trim((string)($settings['promoCenterTitle'] ?? ''));
|
||||
$toSave = [
|
||||
'enabled' => (bool)($settings['enabled'] ?? true),
|
||||
'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心',
|
||||
'bindingDays' => (int)($settings['bindingDays'] ?? 30),
|
||||
'minWithdrawFen' => $minWithdrawFen,
|
||||
'maxWithdrawFen' => $maxWithdrawFen,
|
||||
'requireAudit' => isset($settings['requireAudit']) ? (bool)$settings['requireAudit'] : true,
|
||||
'withdrawFee' => max(0, min(100, (float)($settings['withdrawFee'] ?? 0))),
|
||||
'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null),
|
||||
];
|
||||
|
||||
try {
|
||||
$now = time();
|
||||
$existing = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find();
|
||||
if ($existing) {
|
||||
Db::name('system_config')
|
||||
->where('key', 'distribution')
|
||||
->where('enterprise_id', 0)
|
||||
->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]);
|
||||
} else {
|
||||
Db::name('system_config')->insert([
|
||||
'key' => 'distribution',
|
||||
'enterprise_id' => 0,
|
||||
'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE),
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
$toSave['minWithdraw'] = $toSave['minWithdrawFen'] / 100;
|
||||
$toSave['maxWithdraw'] = $toSave['maxWithdrawFen'] > 0 ? $toSave['maxWithdrawFen'] / 100 : 0;
|
||||
$toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']);
|
||||
return success($toSave, '配置已保存');
|
||||
} catch (\Exception $e) {
|
||||
return error('保存配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private static function defaultTestSettings(): array
|
||||
{
|
||||
$item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false];
|
||||
return ['face' => $item, 'mbti' => $item, 'disc' => $item, 'pdp' => $item];
|
||||
}
|
||||
|
||||
private static function sanitizeTestSettings($raw): array
|
||||
{
|
||||
$default = self::defaultTestSettings();
|
||||
if (!is_array($raw)) return $default;
|
||||
$result = [];
|
||||
foreach ($default as $type => $def) {
|
||||
$s = $raw[$type] ?? [];
|
||||
$commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio';
|
||||
$amountFen = isset($s['commissionAmount'])
|
||||
? (int) round((float)$s['commissionAmount'] * 100)
|
||||
: (int)($s['commissionAmountFen'] ?? 0);
|
||||
$rate = max(0, min(100, (int)($s['commissionRate'] ?? 90)));
|
||||
$result[$type] = [
|
||||
'enabled' => ($s['enabled'] ?? true) !== false,
|
||||
'commissionType' => $commissionType,
|
||||
'commissionRate' => $commissionType === 'ratio' ? $rate : 0,
|
||||
'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0,
|
||||
'noPayment' => !empty($s['noPayment']),
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function appendTestSettingsAmount(array $ts): array
|
||||
{
|
||||
foreach ($ts as $k => $v) {
|
||||
$ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2);
|
||||
}
|
||||
return $ts;
|
||||
}
|
||||
}
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 分销管理控制器(超管端 - 个人版分销)
|
||||
* 路由前缀:/api/v1/superadmin/distribution
|
||||
*/
|
||||
class Distribution extends BaseController
|
||||
{
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/overview 全平台分销数据概览
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function overview()
|
||||
{
|
||||
try {
|
||||
$now = time();
|
||||
|
||||
$totalCommission = Db::name('commission_records')->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0;
|
||||
$paidCommission = Db::name('commission_records')->where('status', 'paid')->sum('commissionFen') ?: 0;
|
||||
$frozenCommission = Db::name('commission_records')->where('status', 'frozen')->sum('commissionFen') ?: 0;
|
||||
$totalOrders = Db::name('commission_records')->whereIn('status', ['paid', 'frozen'])->count();
|
||||
|
||||
$personalCommission = Db::name('commission_records')->where('scope', 'personal')
|
||||
->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0;
|
||||
$enterpriseCommission = Db::name('commission_records')->where('scope', 'enterprise')
|
||||
->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0;
|
||||
|
||||
$bindingCount = Db::name('distribution_bindings')
|
||||
->where('status', 'active')
|
||||
->where('expireAt', '>', $now)
|
||||
->count();
|
||||
|
||||
// 待处理提现:status=0 审核中
|
||||
$pendingWithdraw = Db::name('distribution_withdrawals')
|
||||
->where('status', 0)
|
||||
->sum('amountFen') ?: 0;
|
||||
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayCommission = Db::name('commission_records')
|
||||
->where('status', 'paid')
|
||||
->where('paidAt', '>=', $todayStart)
|
||||
->sum('commissionFen') ?: 0;
|
||||
|
||||
return success([
|
||||
'totalCommission' => number_format($totalCommission / 100, 2, '.', ''),
|
||||
'paidCommission' => number_format($paidCommission / 100, 2, '.', ''),
|
||||
'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''),
|
||||
'personalCommission' => number_format($personalCommission / 100, 2, '.', ''),
|
||||
'enterpriseCommission'=> number_format($enterpriseCommission / 100, 2, '.', ''),
|
||||
'totalOrders' => $totalOrders,
|
||||
'bindingCount' => $bindingCount,
|
||||
'pendingWithdraw' => number_format($pendingWithdraw / 100, 2, '.', ''),
|
||||
'todayCommission' => number_format($todayCommission / 100, 2, '.', ''),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取数据失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/bindings 全平台绑定记录
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function bindings()
|
||||
{
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$scope = Request::param('scope', '');
|
||||
$status = Request::param('status', '');
|
||||
$enterpriseId = (int) Request::param('enterpriseId', 0);
|
||||
|
||||
try {
|
||||
$query = Db::name('distribution_bindings')
|
||||
->alias('b')
|
||||
->leftJoin('wechat_users inv', 'b.inviterId = inv.id')
|
||||
->leftJoin('wechat_users invt', 'b.inviteeId = invt.id')
|
||||
->leftJoin('enterprises e', 'b.enterpriseId = e.id')
|
||||
->field('b.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName');
|
||||
|
||||
if ($scope) $query->where('b.scope', $scope);
|
||||
if ($status) $query->where('b.status', $status);
|
||||
if ($enterpriseId) $query->where('b.enterpriseId', $enterpriseId);
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('b.updatedAt', 'desc')->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
$now = time();
|
||||
foreach ($list as &$row) {
|
||||
$row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400));
|
||||
$row['inviterName'] = $row['inviterName'] ?: '未知';
|
||||
$row['inviteeName'] = $row['inviteeName'] ?: '未知';
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取绑定记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/commissions 全平台佣金记录
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function commissions()
|
||||
{
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$scope = Request::param('scope', '');
|
||||
$status = Request::param('status', '');
|
||||
|
||||
try {
|
||||
$query = Db::name('commission_records')
|
||||
->alias('c')
|
||||
->leftJoin('wechat_users inv', 'c.inviterId = inv.id')
|
||||
->leftJoin('wechat_users invt', 'c.inviteeId = invt.id')
|
||||
->leftJoin('enterprises e', 'c.enterpriseId = e.id')
|
||||
->field('c.*, inv.nickname as inviterName, invt.nickname as inviteeName, e.name as enterpriseName');
|
||||
|
||||
if ($scope) $query->where('c.scope', $scope);
|
||||
if ($status) $query->where('c.status', $status);
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('c.createdAt', 'desc')->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', '');
|
||||
$row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', '');
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取佣金记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/withdrawals 全平台提现申请
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function withdrawals()
|
||||
{
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$status = Request::param('status', '');
|
||||
|
||||
try {
|
||||
$query = Db::name('distribution_withdrawals')
|
||||
->alias('w')
|
||||
->leftJoin('wechat_users u', 'w.userId = u.id')
|
||||
->field('w.*, u.nickname, u.avatar');
|
||||
|
||||
if ($status !== '') {
|
||||
// 支持字符串或数字,统一转 int
|
||||
$query->where('w.status', (int)$status);
|
||||
}
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('w.createdAt', 'desc')->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', '');
|
||||
$row['nickname'] = $row['nickname'] ?: '未知用户';
|
||||
|
||||
// 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常)
|
||||
$code = (int) ($row['status'] ?? 0);
|
||||
$row['status'] = $code;
|
||||
|
||||
// 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期
|
||||
switch ($code) {
|
||||
case 0:
|
||||
$row['statusLabel'] = '审核中';
|
||||
break;
|
||||
case 1:
|
||||
$row['statusLabel'] = '已驳回';
|
||||
break;
|
||||
case 2:
|
||||
$row['statusLabel'] = '待收款';
|
||||
break;
|
||||
case 3:
|
||||
$row['statusLabel'] = '已收款';
|
||||
break;
|
||||
case 4:
|
||||
$row['statusLabel'] = '已过期';
|
||||
break;
|
||||
default:
|
||||
$row['statusLabel'] = '未知';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取提现记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// POST distribution/withdrawals/:id/approve 审核通过提现
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function approveWithdrawal(int $id)
|
||||
{
|
||||
$note = Request::param('note', '');
|
||||
$now = time();
|
||||
|
||||
$record = Db::name('distribution_withdrawals')
|
||||
->alias('w')
|
||||
->leftJoin('wechat_users u', 'w.userId = u.id')
|
||||
->field('w.*, u.openid')
|
||||
->where('w.id', $id)
|
||||
->find();
|
||||
// 仅允许处理审核中(status=0)的记录
|
||||
if (!$record || (int)$record['status'] !== 0) {
|
||||
return error('提现申请不存在或已处理', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成商户单号:TX + 时间戳 + 随机数 + 提现ID(示例:TX202603121526520005123)
|
||||
$outBillNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999) . $record['id'];
|
||||
|
||||
// 调用微信商家转账到零钱接口(参数对齐 ckb-admin Withdrawal::handleWechatPay)
|
||||
$service = new \app\common\service\WechatTransferService();
|
||||
$result = $service->createTransfer([
|
||||
'out_bill_no' => $outBillNo,
|
||||
'openid' => $record['openid'],
|
||||
'transfer_amount' => (int) $record['amountFen'], // 单位:分
|
||||
'transfer_remark' => '推广佣金提现',
|
||||
'transfer_scene_id' => env('TRANSFER_SCENE_ID', '1005'),
|
||||
'transfer_scene_report_infos' => [
|
||||
[
|
||||
'info_type' => '岗位类型',
|
||||
'info_content' => '推广人员',
|
||||
],
|
||||
[
|
||||
'info_type' => '报酬说明',
|
||||
'info_content' => '推广佣金提现',
|
||||
],
|
||||
],
|
||||
'notify_url' => env('WITHDRAW_NOTIFY_URL', ''), // 可选:提现专用回调
|
||||
]);
|
||||
|
||||
if ($result['success'] !== true) {
|
||||
$err = $result['error'] ?? [];
|
||||
$code = $err['code'] ?? 'UNKNOWN';
|
||||
$msg = $err['message'] ?? '微信转账接口调用失败';
|
||||
return error("微信转账发起失败({$code}):{$msg}", 500);
|
||||
}
|
||||
|
||||
$wechatData = $result['data'] ?? [];
|
||||
Db::name('distribution_withdrawals')->where('id', $id)->update([
|
||||
// 2=待收款(已发起转账,等待用户确认)
|
||||
'status' => 2,
|
||||
'auditNote' => $note,
|
||||
'auditAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
'pay_type' => 'wechat',
|
||||
'out_bill_no' => $outBillNo,
|
||||
'transfer_bill_no' => $wechatData['transfer_bill_no'] ?? null,
|
||||
'wechat_pay_state' => $wechatData['state'] ?? 'PROCESSING',
|
||||
'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'),
|
||||
'package_info' => $wechatData['package_info'] ?? '',
|
||||
'mch_id' => env('MCH_ID', null),
|
||||
]);
|
||||
return success(null, '审核通过,已发起微信转账');
|
||||
} catch (\Exception $e) {
|
||||
return error('操作失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// POST distribution/withdrawals/:id/reject 拒绝提现
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function rejectWithdrawal(int $id)
|
||||
{
|
||||
$note = Request::param('note', '');
|
||||
$now = time();
|
||||
|
||||
$record = Db::name('distribution_withdrawals')->where('id', $id)->find();
|
||||
// 仅允许处理审核中(status=0)的记录
|
||||
if (!$record || (int)$record['status'] !== 0) {
|
||||
return error('提现申请不存在或已处理', 400);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
Db::name('wechat_users')
|
||||
->where('id', $record['userId'])
|
||||
->inc('walletBalance', $record['amountFen'])
|
||||
->update(['updatedAt' => $now]);
|
||||
|
||||
Db::name('distribution_withdrawals')->where('id', $id)->update([
|
||||
// 1=已驳回
|
||||
'status' => 1,
|
||||
'auditNote' => $note,
|
||||
'auditAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
return success(null, '已拒绝,余额已退回');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return error('操作失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/settings 个人版分销全局配置
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function settings()
|
||||
{
|
||||
try {
|
||||
$config = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find();
|
||||
$default = [
|
||||
'enabled' => true,
|
||||
'promoCenterTitle' => '推广中心',
|
||||
'bindingDays' => 30,
|
||||
'minWithdrawFen' => 100,
|
||||
'maxWithdrawFen' => 0,
|
||||
'requireAudit' => true,
|
||||
'withdrawFee' => 0,
|
||||
'testSettings' => self::defaultTestSettings(),
|
||||
];
|
||||
if ($config && $config['value']) {
|
||||
$settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value'];
|
||||
$settings = array_merge($default, $settings ?? []);
|
||||
} else {
|
||||
$settings = $default;
|
||||
}
|
||||
$settings['minWithdraw'] = round((float)($settings['minWithdrawFen'] ?? 100) / 100, 2);
|
||||
$settings['maxWithdraw'] = ($max = (int)($settings['maxWithdrawFen'] ?? 0)) > 0 ? round($max / 100, 2) : 0;
|
||||
$settings['testSettings'] = self::appendTestSettingsAmount($settings['testSettings'] ?? self::defaultTestSettings());
|
||||
return success($settings);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// PUT distribution/settings 更新个人版分销全局配置
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function updateSettings()
|
||||
{
|
||||
$settings = Request::only([
|
||||
'enabled', 'promoCenterTitle', 'bindingDays',
|
||||
'minWithdrawFen', 'minWithdraw', 'maxWithdrawFen', 'maxWithdraw',
|
||||
'requireAudit', 'withdrawFee', 'testSettings'
|
||||
]);
|
||||
|
||||
$minWithdrawFen = isset($settings['minWithdraw'])
|
||||
? (int) round((float)$settings['minWithdraw'] * 100)
|
||||
: (int)($settings['minWithdrawFen'] ?? 100);
|
||||
$maxWithdrawFen = isset($settings['maxWithdraw'])
|
||||
? (int) round((float)$settings['maxWithdraw'] * 100)
|
||||
: (int)($settings['maxWithdrawFen'] ?? 0);
|
||||
$minWithdrawFen = max(100, min(20000, $minWithdrawFen));
|
||||
$maxWithdrawFen = $maxWithdrawFen > 0 ? min(20000, max(100, $maxWithdrawFen)) : 0;
|
||||
|
||||
$promoTitle = trim((string)($settings['promoCenterTitle'] ?? ''));
|
||||
$toSave = [
|
||||
'enabled' => (bool)($settings['enabled'] ?? true),
|
||||
'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心',
|
||||
'bindingDays' => (int)($settings['bindingDays'] ?? 30),
|
||||
'minWithdrawFen' => $minWithdrawFen,
|
||||
'maxWithdrawFen' => $maxWithdrawFen,
|
||||
'requireAudit' => isset($settings['requireAudit']) ? (bool)$settings['requireAudit'] : true,
|
||||
'withdrawFee' => max(0, min(100, (float)($settings['withdrawFee'] ?? 0))),
|
||||
'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null),
|
||||
];
|
||||
|
||||
try {
|
||||
$now = time();
|
||||
$existing = Db::name('system_config')->where('key', 'distribution')->where('enterprise_id', 0)->find();
|
||||
if ($existing) {
|
||||
Db::name('system_config')
|
||||
->where('key', 'distribution')
|
||||
->where('enterprise_id', 0)
|
||||
->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]);
|
||||
} else {
|
||||
Db::name('system_config')->insert([
|
||||
'key' => 'distribution',
|
||||
'enterprise_id' => 0,
|
||||
'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE),
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
$toSave['minWithdraw'] = $toSave['minWithdrawFen'] / 100;
|
||||
$toSave['maxWithdraw'] = $toSave['maxWithdrawFen'] > 0 ? $toSave['maxWithdrawFen'] / 100 : 0;
|
||||
$toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']);
|
||||
return success($toSave, '配置已保存');
|
||||
} catch (\Exception $e) {
|
||||
return error('保存配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private static function defaultTestSettings(): array
|
||||
{
|
||||
$item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false];
|
||||
return ['face' => $item, 'mbti' => $item, 'disc' => $item, 'pdp' => $item];
|
||||
}
|
||||
|
||||
private static function sanitizeTestSettings($raw): array
|
||||
{
|
||||
$default = self::defaultTestSettings();
|
||||
if (!is_array($raw)) return $default;
|
||||
$result = [];
|
||||
foreach ($default as $type => $def) {
|
||||
$s = $raw[$type] ?? [];
|
||||
$commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio';
|
||||
$amountFen = isset($s['commissionAmount'])
|
||||
? (int) round((float)$s['commissionAmount'] * 100)
|
||||
: (int)($s['commissionAmountFen'] ?? 0);
|
||||
$rate = max(0, min(100, (int)($s['commissionRate'] ?? 90)));
|
||||
$result[$type] = [
|
||||
'enabled' => ($s['enabled'] ?? true) !== false,
|
||||
'commissionType' => $commissionType,
|
||||
'commissionRate' => $commissionType === 'ratio' ? $rate : 0,
|
||||
'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0,
|
||||
'noPayment' => !empty($s['noPayment']),
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function appendTestSettingsAmount(array $ts): array
|
||||
{
|
||||
foreach ($ts as $k => $v) {
|
||||
$ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2);
|
||||
}
|
||||
return $ts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,437 +1,437 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Enterprise as EnterpriseModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 企业管理控制器(超管专用)
|
||||
*/
|
||||
class Enterprise extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取企业列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = Request::param('page', 1);
|
||||
$pageSize = Request::param('pageSize', 20);
|
||||
$keyword = Request::param('keyword', '');
|
||||
$status = Request::param('status', '');
|
||||
|
||||
$where = [];
|
||||
|
||||
// 搜索条件
|
||||
if ($keyword) {
|
||||
$where[] = ['name|contactName|contactPhone|code', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
|
||||
// 查询企业列表
|
||||
$list = EnterpriseModel::where($where)
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 统计每个企业的用户数和测试用量
|
||||
foreach ($list as &$item) {
|
||||
// 统计用户数(只统计未删除的用户)
|
||||
$item['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
// 统计测试用量(测试结果数)- 通过企业下的用户ID统计(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$item['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$item['testUsage'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$total = EnterpriseModel::where($where)->count();
|
||||
|
||||
// 统计活跃企业数(status为operating)
|
||||
$activeCount = EnterpriseModel::where('status', 'operating')->count();
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'activeCount' => $activeCount
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id = null)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 如果路由参数没有传递,尝试从请求参数获取
|
||||
if (empty($id)) {
|
||||
$id = Request::param('id');
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
return error('企业ID不能为空', 400);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = $enterprise->toArray();
|
||||
|
||||
// 获取企业下的所有用户ID(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
// 统计用户数
|
||||
$data['userCount'] = count($userIds);
|
||||
|
||||
// 获取管理员账号列表(企业管理员角色,只获取未删除的)
|
||||
$adminAccounts = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,role,status,createdAt,lastLoginTime')
|
||||
->select()
|
||||
->toArray();
|
||||
$data['adminAccounts'] = $adminAccounts;
|
||||
|
||||
// 获取用户列表(排除管理员,只获取未删除的)
|
||||
$users = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', '<>', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,mbtiType,status,createdAt')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
$data['users'] = $users;
|
||||
|
||||
// 获取测试结果列表
|
||||
$testResults = [];
|
||||
if (!empty($userIds)) {
|
||||
$testResults = Db::name('test_results')
|
||||
->alias('tr')
|
||||
->leftJoin('users u', 'tr.userId = u.id')
|
||||
->where('tr.userId', 'in', $userIds)
|
||||
->field('tr.id,tr.testType,tr.createdAt,u.username')
|
||||
->order('tr.createdAt', 'desc')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
$data['testResults'] = $testResults;
|
||||
|
||||
// 统计测试用量
|
||||
if (!empty($userIds)) {
|
||||
$data['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$data['testUsage'] = 0;
|
||||
}
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建企业
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$data = Request::post();
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($data['name'])) {
|
||||
return error('企业名称不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证管理员账号信息
|
||||
if (empty($data['adminUsername'])) {
|
||||
return error('管理员用户名不能为空', 400);
|
||||
}
|
||||
|
||||
if (empty($data['adminPassword'])) {
|
||||
return error('管理员密码不能为空', 400);
|
||||
}
|
||||
|
||||
if (strlen($data['adminPassword']) < 6) {
|
||||
return error('密码长度至少6位', 400);
|
||||
}
|
||||
|
||||
// 检查企业代码是否重复(如果提供了代码)
|
||||
if (!empty($data['code'])) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查管理员用户名是否已存在
|
||||
if (Db::name('users')->where('username', $data['adminUsername'])->find()) {
|
||||
return error('管理员用户名已存在', 400);
|
||||
}
|
||||
|
||||
// 状态映射(前端使用operating/trial/disabled)
|
||||
$status = $data['status'] ?? 'operating';
|
||||
if (!in_array($status, ['operating', 'trial', 'disabled'])) {
|
||||
$status = 'operating';
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 创建企业
|
||||
$enterprise = new EnterpriseModel();
|
||||
$enterprise->name = $data['name'];
|
||||
$enterprise->code = $data['code'] ?? null;
|
||||
$enterprise->contactName = $data['contactName'] ?? null;
|
||||
$enterprise->contactPhone = $data['contactPhone'] ?? null;
|
||||
$enterprise->contactEmail = $data['contactEmail'] ?? null;
|
||||
$enterprise->balance = $data['balance'] ?? 0.00;
|
||||
$enterprise->status = $status;
|
||||
$enterprise->trialExpireAt = ($status === 'trial' && isset($data['trialExpireAt'])) ? $data['trialExpireAt'] : null;
|
||||
$enterprise->save();
|
||||
|
||||
$enterpriseId = $enterprise->id;
|
||||
|
||||
// 创建企业管理员账号
|
||||
$adminUser = [
|
||||
'username' => $data['adminUsername'],
|
||||
'password' => password_hash($data['adminPassword'], PASSWORD_DEFAULT),
|
||||
'email' => $data['contactEmail'] ?? null,
|
||||
'phone' => $data['contactPhone'] ?? null,
|
||||
'role' => 'enterprise_admin',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'status' => 1,
|
||||
'createdAt' => time(),
|
||||
'updatedAt' => time()
|
||||
];
|
||||
|
||||
Db::name('users')->insert($adminUser);
|
||||
|
||||
// 提交事务
|
||||
Db::commit();
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
$enterpriseData['userCount'] = 1; // 刚创建的企业管理员
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
|
||||
return success($enterpriseData, '企业创建成功,管理员账号已创建');
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
Db::rollback();
|
||||
return error('创建失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = Request::put();
|
||||
$oldBalance = (float) ($enterprise->balance ?? 0);
|
||||
|
||||
// 如果更新企业代码,检查是否重复
|
||||
if (isset($data['code']) && $data['code'] != $enterprise->code) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态验证
|
||||
if (isset($data['status']) && !in_array($data['status'], ['operating', 'trial', 'disabled'])) {
|
||||
return error('状态值无效', 400);
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
$status = $data['status'] ?? $enterprise->status;
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
$enterprise->trialExpireAt = $data['trialExpireAt'];
|
||||
} else {
|
||||
// 如果不是试用状态,清空到期时间
|
||||
$enterprise->trialExpireAt = null;
|
||||
}
|
||||
|
||||
$enterprise->save($data);
|
||||
|
||||
$newBalance = (float) ($enterprise->balance ?? 0);
|
||||
if ($newBalance > $oldBalance) {
|
||||
try {
|
||||
\app\controller\api\Distribution::unfreezeCommissions((int) $id);
|
||||
} catch (\Throwable $e) {
|
||||
// 余额已更新成功,解冻失败不阻断主流程
|
||||
}
|
||||
}
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
|
||||
// 统计用户数和测试用量(只统计未删除的用户)
|
||||
$enterpriseData['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$enterpriseData['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
}
|
||||
|
||||
return success($enterpriseData, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除企业(软删除)
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 检查是否已删除
|
||||
if ($enterprise->deletedAt) {
|
||||
return error('企业已被删除', 400);
|
||||
}
|
||||
|
||||
// 检查是否有用户关联(只检查未删除的用户)
|
||||
$userCount = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
if ($userCount > 0) {
|
||||
return error('该企业下还有用户,无法删除', 400);
|
||||
}
|
||||
|
||||
// 软删除(设置 deletedAt 时间戳)
|
||||
$enterprise->delete();
|
||||
|
||||
return success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function toggleStatus($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 切换状态:operating <-> disabled
|
||||
if ($enterprise->status === 'operating') {
|
||||
$enterprise->status = 'disabled';
|
||||
} else {
|
||||
$enterprise->status = 'operating';
|
||||
}
|
||||
|
||||
$enterprise->save();
|
||||
|
||||
return success($enterprise, '操作成功');
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Enterprise as EnterpriseModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 企业管理控制器(超管专用)
|
||||
*/
|
||||
class Enterprise extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取企业列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = Request::param('page', 1);
|
||||
$pageSize = Request::param('pageSize', 20);
|
||||
$keyword = Request::param('keyword', '');
|
||||
$status = Request::param('status', '');
|
||||
|
||||
$where = [];
|
||||
|
||||
// 搜索条件
|
||||
if ($keyword) {
|
||||
$where[] = ['name|contactName|contactPhone|code', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
|
||||
// 查询企业列表
|
||||
$list = EnterpriseModel::where($where)
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 统计每个企业的用户数和测试用量
|
||||
foreach ($list as &$item) {
|
||||
// 统计用户数(只统计未删除的用户)
|
||||
$item['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
// 统计测试用量(测试结果数)- 通过企业下的用户ID统计(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $item['id'])
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$item['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$item['testUsage'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$total = EnterpriseModel::where($where)->count();
|
||||
|
||||
// 统计活跃企业数(status为operating)
|
||||
$activeCount = EnterpriseModel::where('status', 'operating')->count();
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'activeCount' => $activeCount
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id = null)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 如果路由参数没有传递,尝试从请求参数获取
|
||||
if (empty($id)) {
|
||||
$id = Request::param('id');
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
return error('企业ID不能为空', 400);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = $enterprise->toArray();
|
||||
|
||||
// 获取企业下的所有用户ID(只统计未删除的用户)
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
// 统计用户数
|
||||
$data['userCount'] = count($userIds);
|
||||
|
||||
// 获取管理员账号列表(企业管理员角色,只获取未删除的)
|
||||
$adminAccounts = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,role,status,createdAt,lastLoginTime')
|
||||
->select()
|
||||
->toArray();
|
||||
$data['adminAccounts'] = $adminAccounts;
|
||||
|
||||
// 获取用户列表(排除管理员,只获取未删除的)
|
||||
$users = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('role', '<>', 'enterprise_admin')
|
||||
->where('deletedAt', null)
|
||||
->field('id,username,email,phone,mbtiType,status,createdAt')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
$data['users'] = $users;
|
||||
|
||||
// 获取测试结果列表
|
||||
$testResults = [];
|
||||
if (!empty($userIds)) {
|
||||
$testResults = Db::name('test_results')
|
||||
->alias('tr')
|
||||
->leftJoin('users u', 'tr.userId = u.id')
|
||||
->where('tr.userId', 'in', $userIds)
|
||||
->field('tr.id,tr.testType,tr.createdAt,u.username')
|
||||
->order('tr.createdAt', 'desc')
|
||||
->limit(50) // 限制返回数量
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
$data['testResults'] = $testResults;
|
||||
|
||||
// 统计测试用量
|
||||
if (!empty($userIds)) {
|
||||
$data['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$data['testUsage'] = 0;
|
||||
}
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建企业
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$data = Request::post();
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($data['name'])) {
|
||||
return error('企业名称不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证管理员账号信息
|
||||
if (empty($data['adminUsername'])) {
|
||||
return error('管理员用户名不能为空', 400);
|
||||
}
|
||||
|
||||
if (empty($data['adminPassword'])) {
|
||||
return error('管理员密码不能为空', 400);
|
||||
}
|
||||
|
||||
if (strlen($data['adminPassword']) < 6) {
|
||||
return error('密码长度至少6位', 400);
|
||||
}
|
||||
|
||||
// 检查企业代码是否重复(如果提供了代码)
|
||||
if (!empty($data['code'])) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查管理员用户名是否已存在
|
||||
if (Db::name('users')->where('username', $data['adminUsername'])->find()) {
|
||||
return error('管理员用户名已存在', 400);
|
||||
}
|
||||
|
||||
// 状态映射(前端使用operating/trial/disabled)
|
||||
$status = $data['status'] ?? 'operating';
|
||||
if (!in_array($status, ['operating', 'trial', 'disabled'])) {
|
||||
$status = 'operating';
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 创建企业
|
||||
$enterprise = new EnterpriseModel();
|
||||
$enterprise->name = $data['name'];
|
||||
$enterprise->code = $data['code'] ?? null;
|
||||
$enterprise->contactName = $data['contactName'] ?? null;
|
||||
$enterprise->contactPhone = $data['contactPhone'] ?? null;
|
||||
$enterprise->contactEmail = $data['contactEmail'] ?? null;
|
||||
$enterprise->balance = $data['balance'] ?? 0.00;
|
||||
$enterprise->status = $status;
|
||||
$enterprise->trialExpireAt = ($status === 'trial' && isset($data['trialExpireAt'])) ? $data['trialExpireAt'] : null;
|
||||
$enterprise->save();
|
||||
|
||||
$enterpriseId = $enterprise->id;
|
||||
|
||||
// 创建企业管理员账号
|
||||
$adminUser = [
|
||||
'username' => $data['adminUsername'],
|
||||
'password' => password_hash($data['adminPassword'], PASSWORD_DEFAULT),
|
||||
'email' => $data['contactEmail'] ?? null,
|
||||
'phone' => $data['contactPhone'] ?? null,
|
||||
'role' => 'enterprise_admin',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'status' => 1,
|
||||
'createdAt' => time(),
|
||||
'updatedAt' => time()
|
||||
];
|
||||
|
||||
Db::name('users')->insert($adminUser);
|
||||
|
||||
// 提交事务
|
||||
Db::commit();
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
$enterpriseData['userCount'] = 1; // 刚创建的企业管理员
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
|
||||
return success($enterpriseData, '企业创建成功,管理员账号已创建');
|
||||
} catch (\Exception $e) {
|
||||
// 回滚事务
|
||||
Db::rollback();
|
||||
return error('创建失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$data = Request::put();
|
||||
$oldBalance = (float) ($enterprise->balance ?? 0);
|
||||
|
||||
// 如果更新企业代码,检查是否重复
|
||||
if (isset($data['code']) && $data['code'] != $enterprise->code) {
|
||||
if (EnterpriseModel::where('code', $data['code'])->find()) {
|
||||
return error('企业代码已存在', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 状态验证
|
||||
if (isset($data['status']) && !in_array($data['status'], ['operating', 'trial', 'disabled'])) {
|
||||
return error('状态值无效', 400);
|
||||
}
|
||||
|
||||
// 验证试用到期时间
|
||||
$status = $data['status'] ?? $enterprise->status;
|
||||
if ($status === 'trial') {
|
||||
if (empty($data['trialExpireAt'])) {
|
||||
return error('选择试用状态时,必须设置试用到期时间', 400);
|
||||
}
|
||||
// 确保到期时间大于当前时间
|
||||
if ($data['trialExpireAt'] <= time()) {
|
||||
return error('试用到期时间必须大于当前时间', 400);
|
||||
}
|
||||
$enterprise->trialExpireAt = $data['trialExpireAt'];
|
||||
} else {
|
||||
// 如果不是试用状态,清空到期时间
|
||||
$enterprise->trialExpireAt = null;
|
||||
}
|
||||
|
||||
$enterprise->save($data);
|
||||
|
||||
$newBalance = (float) ($enterprise->balance ?? 0);
|
||||
if ($newBalance > $oldBalance) {
|
||||
try {
|
||||
\app\controller\api\Distribution::unfreezeCommissions((int) $id);
|
||||
} catch (\Throwable $e) {
|
||||
// 余额已更新成功,解冻失败不阻断主流程
|
||||
}
|
||||
}
|
||||
|
||||
$enterpriseData = $enterprise->toArray();
|
||||
|
||||
// 统计用户数和测试用量(只统计未删除的用户)
|
||||
$enterpriseData['userCount'] = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
|
||||
$userIds = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->column('id');
|
||||
|
||||
if (!empty($userIds)) {
|
||||
$enterpriseData['testUsage'] = Db::name('test_results')
|
||||
->where('userId', 'in', $userIds)
|
||||
->count();
|
||||
} else {
|
||||
$enterpriseData['testUsage'] = 0;
|
||||
}
|
||||
|
||||
return success($enterpriseData, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除企业(软删除)
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 检查是否已删除
|
||||
if ($enterprise->deletedAt) {
|
||||
return error('企业已被删除', 400);
|
||||
}
|
||||
|
||||
// 检查是否有用户关联(只检查未删除的用户)
|
||||
$userCount = Db::name('users')
|
||||
->where('enterpriseId', $id)
|
||||
->where('deletedAt', null)
|
||||
->count();
|
||||
if ($userCount > 0) {
|
||||
return error('该企业下还有用户,无法删除', 400);
|
||||
}
|
||||
|
||||
// 软删除(设置 deletedAt 时间戳)
|
||||
$enterprise->delete();
|
||||
|
||||
return success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用企业
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function toggleStatus($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterprise = EnterpriseModel::find($id);
|
||||
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// 切换状态:operating <-> disabled
|
||||
if ($enterprise->status === 'operating') {
|
||||
$enterprise->status = 'disabled';
|
||||
} else {
|
||||
$enterprise->status = 'operating';
|
||||
}
|
||||
|
||||
$enterprise->save();
|
||||
|
||||
return success($enterprise, '操作成功');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,337 +1,337 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 财务管理控制器(超管专用)
|
||||
* 数据来源:mbti_orders,金额单位:分
|
||||
*/
|
||||
class Finance extends BaseController
|
||||
{
|
||||
private const PAID_STATUS = ['paid', 'completed'];
|
||||
|
||||
/**
|
||||
* 获取财务概览
|
||||
* 金额单位:分
|
||||
*/
|
||||
public function overview()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$currentMonthStart = mktime(0, 0, 0, (int) date('n'), 1, (int) date('Y'));
|
||||
$currentMonthEnd = mktime(23, 59, 59, (int) date('n'), (int) date('t'), (int) date('Y'));
|
||||
|
||||
$basePaid = Db::name('orders')->whereIn('status', self::PAID_STATUS);
|
||||
$totalRevenue = (int) ((clone $basePaid)->sum('amount') ?? 0);
|
||||
$paidOrderCount = (int) ((clone $basePaid)->count());
|
||||
|
||||
$monthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $currentMonthStart)
|
||||
->where('payTime', '<=', $currentMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
|
||||
// 成本:无成本表时按收入比例估算(约 30%)
|
||||
$totalCost = (int) round($totalRevenue * 0.3);
|
||||
$monthCost = (int) round($monthRevenue * 0.3);
|
||||
|
||||
$netProfit = $totalRevenue - $totalCost;
|
||||
$monthProfit = $monthRevenue - $monthCost;
|
||||
$profitRate = $totalRevenue > 0 ? round(($netProfit / $totalRevenue) * 100, 1) : 0;
|
||||
|
||||
$lastMonthStart = mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y'));
|
||||
$lastMonthEnd = mktime(23, 59, 59, (int) date('n') - 1, (int) date('t', $lastMonthStart), (int) date('Y'));
|
||||
$lastMonthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $lastMonthStart)
|
||||
->where('payTime', '<=', $lastMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
$lastMonthCost = (int) round($lastMonthRevenue * 0.3);
|
||||
$lastMonthProfit = $lastMonthRevenue - $lastMonthCost;
|
||||
$monthGrowth = $lastMonthProfit > 0
|
||||
? round(($monthProfit - $lastMonthProfit) / $lastMonthProfit * 100, 1)
|
||||
: ($monthProfit > 0 ? 100 : 0);
|
||||
|
||||
return success([
|
||||
'totalRevenue' => $totalRevenue,
|
||||
'totalCost' => $totalCost,
|
||||
'netProfit' => $netProfit,
|
||||
'profitRate' => $profitRate,
|
||||
'monthRevenue' => $monthRevenue,
|
||||
'monthCost' => $monthCost,
|
||||
'monthProfit' => $monthProfit,
|
||||
'monthGrowth' => $monthGrowth,
|
||||
'paidOrderCount' => $paidOrderCount,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取财务概览失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收入明细:按产品类型汇总(已支付订单),金额单位:分
|
||||
*/
|
||||
public function revenueDetails()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$rows = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->field('productType, SUM(amount) as total')
|
||||
->group('productType')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$typeLabel = [
|
||||
'face' => 'AI人脸分析',
|
||||
'mbti' => 'MBTI',
|
||||
'disc' => 'DISC',
|
||||
'pdp' => 'PDP',
|
||||
'resume' => '简历综合分析',
|
||||
'report' => '完整报告',
|
||||
];
|
||||
$totalSum = 0;
|
||||
$byType = [];
|
||||
foreach ($rows as $r) {
|
||||
$type = $r['productType'] ?? 'other';
|
||||
$amount = (int) ($r['total'] ?? 0);
|
||||
$totalSum += $amount;
|
||||
$byType[$type] = $amount;
|
||||
}
|
||||
|
||||
$details = [];
|
||||
foreach ($typeLabel as $key => $label) {
|
||||
$amount = $byType[$key] ?? 0;
|
||||
$details[] = [
|
||||
'type' => $label,
|
||||
'amount' => $amount,
|
||||
'percent' => $totalSum > 0 ? round($amount / $totalSum * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
$otherAmount = 0;
|
||||
foreach ($byType as $key => $amount) {
|
||||
if (!isset($typeLabel[$key])) {
|
||||
$otherAmount += $amount;
|
||||
}
|
||||
}
|
||||
if ($otherAmount > 0) {
|
||||
$details[] = [
|
||||
'type' => '其他',
|
||||
'amount' => $otherAmount,
|
||||
'percent' => $totalSum > 0 ? round($otherAmount / $totalSum * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return success($details);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取收入明细失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 成本明细:当前为估算(基于收入的 30% 拆分),金额单位:分
|
||||
*/
|
||||
public function costDetails()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$totalRevenue = (int) Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->sum('amount');
|
||||
$totalCost = (int) round($totalRevenue * 0.3);
|
||||
|
||||
$items = [
|
||||
['type' => 'AI 调用(人脸/分析等)', 'ratio' => 0.15],
|
||||
['type' => '服务器及运维', 'ratio' => 0.08],
|
||||
['type' => '其他支出', 'ratio' => 0.07],
|
||||
];
|
||||
$details = [];
|
||||
foreach ($items as $item) {
|
||||
$amount = (int) round($totalRevenue * $item['ratio']);
|
||||
$details[] = [
|
||||
'type' => $item['type'],
|
||||
'amount' => $amount,
|
||||
'percent' => $totalCost > 0 ? round($amount / $totalCost * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return success($details);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取成本明细失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业支付记录(已支付且 enterpriseId 不为空的订单),金额单位:分
|
||||
*/
|
||||
public function rechargeRecords()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
|
||||
|
||||
$query = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->whereNotNull('enterpriseId')
|
||||
->where('enterpriseId', '<>', '')
|
||||
->order('payTime', 'desc');
|
||||
$total = (int) (clone $query)->count();
|
||||
$list = (clone $query)->page($page, $pageSize)
|
||||
->field('id, orderNo, enterpriseId, amount, payMethod, payTime')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$eids = array_values(array_unique(array_filter(array_column($list, 'enterpriseId'))));
|
||||
$enterprises = [];
|
||||
if (!empty($eids)) {
|
||||
$entList = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id');
|
||||
$enterprises = $entList ?: [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($list as $r) {
|
||||
$eid = $r['enterpriseId'] ?? null;
|
||||
$result[] = [
|
||||
'orderNo' => $r['orderNo'] ?? '',
|
||||
'enterprise' => $eid ? ($enterprises[$eid] ?? '企业#' . $eid) : '—',
|
||||
'amount' => (int) ($r['amount'] ?? 0),
|
||||
'method' => $r['payMethod'] === 'wechat' ? '微信支付' : ($r['payMethod'] ?? '—'),
|
||||
'date' => !empty($r['payTime']) ? date('Y-m-d H:i', $r['payTime']) : '—',
|
||||
];
|
||||
}
|
||||
|
||||
return success([
|
||||
'list' => $result,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取企业支付记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付记录(全部已支付订单,分页),金额单位:分
|
||||
*/
|
||||
public function paymentRecords()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
|
||||
$keyword = trim(Request::param('keyword', ''));
|
||||
|
||||
$query = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->order('payTime', 'desc');
|
||||
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$q->whereLike('orderNo', '%' . $keyword . '%');
|
||||
if (is_numeric($keyword)) {
|
||||
$q->whereOr('userId', (int) $keyword);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$list = (clone $query)->page($page, $pageSize)
|
||||
->field('id, orderNo, userId, enterpriseId, productType, productTitle, amount, payMethod, payTime')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$userIds = array_values(array_unique(array_filter(array_column($list, 'userId'))));
|
||||
$eids = array_values(array_unique(array_filter(array_column($list, 'enterpriseId'))));
|
||||
$usersMap = [];
|
||||
$entMap = [];
|
||||
if (!empty($userIds)) {
|
||||
$users = Db::name('wechat_users')->where('id', 'in', $userIds)->field('id, nickname, phone')->select()->toArray();
|
||||
foreach ($users as $u) {
|
||||
$usersMap[(int) $u['id']] = $u;
|
||||
}
|
||||
}
|
||||
if (!empty($eids)) {
|
||||
$entList = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id');
|
||||
$entMap = $entList ?: [];
|
||||
}
|
||||
|
||||
$productTypeLabel = [
|
||||
'face' => 'AI人脸分析',
|
||||
'mbti' => 'MBTI',
|
||||
'disc' => 'DISC',
|
||||
'pdp' => 'PDP',
|
||||
'report' => '完整报告',
|
||||
'deep_personal' => '个人深度服务',
|
||||
'deep_team' => '团队深度服务',
|
||||
];
|
||||
|
||||
$result = [];
|
||||
foreach ($list as $r) {
|
||||
$uid = (int) ($r['userId'] ?? 0);
|
||||
$eid = isset($r['enterpriseId']) && $r['enterpriseId'] !== '' ? (int) $r['enterpriseId'] : null;
|
||||
if ($eid === 0) {
|
||||
$eid = null;
|
||||
}
|
||||
$u = $usersMap[$uid] ?? null;
|
||||
$enterpriseName = $eid ? ($entMap[$eid] ?? '企业#' . $eid) : '个人';
|
||||
$result[] = [
|
||||
'orderNo' => $r['orderNo'] ?? '',
|
||||
'userName' => $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid),
|
||||
'enterprise' => $enterpriseName,
|
||||
'enterpriseId' => $eid,
|
||||
'productType' => $productTypeLabel[$r['productType'] ?? ''] ?? ($r['productType'] ?? '—'),
|
||||
'productTitle' => $r['productTitle'] ?? '',
|
||||
'amount' => (int) ($r['amount'] ?? 0),
|
||||
'method' => $r['payMethod'] === 'wechat' ? '微信支付' : ($r['payMethod'] ?? '—'),
|
||||
'date' => !empty($r['payTime']) ? date('Y-m-d H:i', $r['payTime']) : '—',
|
||||
];
|
||||
}
|
||||
|
||||
return success([
|
||||
'list' => $result,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取支付记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出财务报表
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
return success(null, '财务报表导出功能开发中');
|
||||
}
|
||||
}
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 财务管理控制器(超管专用)
|
||||
* 数据来源:mbti_orders,金额单位:分
|
||||
*/
|
||||
class Finance extends BaseController
|
||||
{
|
||||
private const PAID_STATUS = ['paid', 'completed'];
|
||||
|
||||
/**
|
||||
* 获取财务概览
|
||||
* 金额单位:分
|
||||
*/
|
||||
public function overview()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$currentMonthStart = mktime(0, 0, 0, (int) date('n'), 1, (int) date('Y'));
|
||||
$currentMonthEnd = mktime(23, 59, 59, (int) date('n'), (int) date('t'), (int) date('Y'));
|
||||
|
||||
$basePaid = Db::name('orders')->whereIn('status', self::PAID_STATUS);
|
||||
$totalRevenue = (int) ((clone $basePaid)->sum('amount') ?? 0);
|
||||
$paidOrderCount = (int) ((clone $basePaid)->count());
|
||||
|
||||
$monthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $currentMonthStart)
|
||||
->where('payTime', '<=', $currentMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
|
||||
// 成本:无成本表时按收入比例估算(约 30%)
|
||||
$totalCost = (int) round($totalRevenue * 0.3);
|
||||
$monthCost = (int) round($monthRevenue * 0.3);
|
||||
|
||||
$netProfit = $totalRevenue - $totalCost;
|
||||
$monthProfit = $monthRevenue - $monthCost;
|
||||
$profitRate = $totalRevenue > 0 ? round(($netProfit / $totalRevenue) * 100, 1) : 0;
|
||||
|
||||
$lastMonthStart = mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y'));
|
||||
$lastMonthEnd = mktime(23, 59, 59, (int) date('n') - 1, (int) date('t', $lastMonthStart), (int) date('Y'));
|
||||
$lastMonthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $lastMonthStart)
|
||||
->where('payTime', '<=', $lastMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
$lastMonthCost = (int) round($lastMonthRevenue * 0.3);
|
||||
$lastMonthProfit = $lastMonthRevenue - $lastMonthCost;
|
||||
$monthGrowth = $lastMonthProfit > 0
|
||||
? round(($monthProfit - $lastMonthProfit) / $lastMonthProfit * 100, 1)
|
||||
: ($monthProfit > 0 ? 100 : 0);
|
||||
|
||||
return success([
|
||||
'totalRevenue' => $totalRevenue,
|
||||
'totalCost' => $totalCost,
|
||||
'netProfit' => $netProfit,
|
||||
'profitRate' => $profitRate,
|
||||
'monthRevenue' => $monthRevenue,
|
||||
'monthCost' => $monthCost,
|
||||
'monthProfit' => $monthProfit,
|
||||
'monthGrowth' => $monthGrowth,
|
||||
'paidOrderCount' => $paidOrderCount,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取财务概览失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收入明细:按产品类型汇总(已支付订单),金额单位:分
|
||||
*/
|
||||
public function revenueDetails()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$rows = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->field('productType, SUM(amount) as total')
|
||||
->group('productType')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$typeLabel = [
|
||||
'face' => 'AI人脸分析',
|
||||
'mbti' => 'MBTI',
|
||||
'disc' => 'DISC',
|
||||
'pdp' => 'PDP',
|
||||
'resume' => '简历综合分析',
|
||||
'report' => '完整报告',
|
||||
];
|
||||
$totalSum = 0;
|
||||
$byType = [];
|
||||
foreach ($rows as $r) {
|
||||
$type = $r['productType'] ?? 'other';
|
||||
$amount = (int) ($r['total'] ?? 0);
|
||||
$totalSum += $amount;
|
||||
$byType[$type] = $amount;
|
||||
}
|
||||
|
||||
$details = [];
|
||||
foreach ($typeLabel as $key => $label) {
|
||||
$amount = $byType[$key] ?? 0;
|
||||
$details[] = [
|
||||
'type' => $label,
|
||||
'amount' => $amount,
|
||||
'percent' => $totalSum > 0 ? round($amount / $totalSum * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
$otherAmount = 0;
|
||||
foreach ($byType as $key => $amount) {
|
||||
if (!isset($typeLabel[$key])) {
|
||||
$otherAmount += $amount;
|
||||
}
|
||||
}
|
||||
if ($otherAmount > 0) {
|
||||
$details[] = [
|
||||
'type' => '其他',
|
||||
'amount' => $otherAmount,
|
||||
'percent' => $totalSum > 0 ? round($otherAmount / $totalSum * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return success($details);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取收入明细失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 成本明细:当前为估算(基于收入的 30% 拆分),金额单位:分
|
||||
*/
|
||||
public function costDetails()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$totalRevenue = (int) Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->sum('amount');
|
||||
$totalCost = (int) round($totalRevenue * 0.3);
|
||||
|
||||
$items = [
|
||||
['type' => 'AI 调用(人脸/分析等)', 'ratio' => 0.15],
|
||||
['type' => '服务器及运维', 'ratio' => 0.08],
|
||||
['type' => '其他支出', 'ratio' => 0.07],
|
||||
];
|
||||
$details = [];
|
||||
foreach ($items as $item) {
|
||||
$amount = (int) round($totalRevenue * $item['ratio']);
|
||||
$details[] = [
|
||||
'type' => $item['type'],
|
||||
'amount' => $amount,
|
||||
'percent' => $totalCost > 0 ? round($amount / $totalCost * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return success($details);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取成本明细失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业支付记录(已支付且 enterpriseId 不为空的订单),金额单位:分
|
||||
*/
|
||||
public function rechargeRecords()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
|
||||
|
||||
$query = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->whereNotNull('enterpriseId')
|
||||
->where('enterpriseId', '<>', '')
|
||||
->order('payTime', 'desc');
|
||||
$total = (int) (clone $query)->count();
|
||||
$list = (clone $query)->page($page, $pageSize)
|
||||
->field('id, orderNo, enterpriseId, amount, payMethod, payTime')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$eids = array_values(array_unique(array_filter(array_column($list, 'enterpriseId'))));
|
||||
$enterprises = [];
|
||||
if (!empty($eids)) {
|
||||
$entList = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id');
|
||||
$enterprises = $entList ?: [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($list as $r) {
|
||||
$eid = $r['enterpriseId'] ?? null;
|
||||
$result[] = [
|
||||
'orderNo' => $r['orderNo'] ?? '',
|
||||
'enterprise' => $eid ? ($enterprises[$eid] ?? '企业#' . $eid) : '—',
|
||||
'amount' => (int) ($r['amount'] ?? 0),
|
||||
'method' => $r['payMethod'] === 'wechat' ? '微信支付' : ($r['payMethod'] ?? '—'),
|
||||
'date' => !empty($r['payTime']) ? date('Y-m-d H:i', $r['payTime']) : '—',
|
||||
];
|
||||
}
|
||||
|
||||
return success([
|
||||
'list' => $result,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取企业支付记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付记录(全部已支付订单,分页),金额单位:分
|
||||
*/
|
||||
public function paymentRecords()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
|
||||
$keyword = trim(Request::param('keyword', ''));
|
||||
|
||||
$query = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->order('payTime', 'desc');
|
||||
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$q->whereLike('orderNo', '%' . $keyword . '%');
|
||||
if (is_numeric($keyword)) {
|
||||
$q->whereOr('userId', (int) $keyword);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$list = (clone $query)->page($page, $pageSize)
|
||||
->field('id, orderNo, userId, enterpriseId, productType, productTitle, amount, payMethod, payTime')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$userIds = array_values(array_unique(array_filter(array_column($list, 'userId'))));
|
||||
$eids = array_values(array_unique(array_filter(array_column($list, 'enterpriseId'))));
|
||||
$usersMap = [];
|
||||
$entMap = [];
|
||||
if (!empty($userIds)) {
|
||||
$users = Db::name('wechat_users')->where('id', 'in', $userIds)->field('id, nickname, phone')->select()->toArray();
|
||||
foreach ($users as $u) {
|
||||
$usersMap[(int) $u['id']] = $u;
|
||||
}
|
||||
}
|
||||
if (!empty($eids)) {
|
||||
$entList = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id');
|
||||
$entMap = $entList ?: [];
|
||||
}
|
||||
|
||||
$productTypeLabel = [
|
||||
'face' => 'AI人脸分析',
|
||||
'mbti' => 'MBTI',
|
||||
'disc' => 'DISC',
|
||||
'pdp' => 'PDP',
|
||||
'report' => '完整报告',
|
||||
'deep_personal' => '个人深度服务',
|
||||
'deep_team' => '团队深度服务',
|
||||
];
|
||||
|
||||
$result = [];
|
||||
foreach ($list as $r) {
|
||||
$uid = (int) ($r['userId'] ?? 0);
|
||||
$eid = isset($r['enterpriseId']) && $r['enterpriseId'] !== '' ? (int) $r['enterpriseId'] : null;
|
||||
if ($eid === 0) {
|
||||
$eid = null;
|
||||
}
|
||||
$u = $usersMap[$uid] ?? null;
|
||||
$enterpriseName = $eid ? ($entMap[$eid] ?? '企业#' . $eid) : '个人';
|
||||
$result[] = [
|
||||
'orderNo' => $r['orderNo'] ?? '',
|
||||
'userName' => $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid),
|
||||
'enterprise' => $enterpriseName,
|
||||
'enterpriseId' => $eid,
|
||||
'productType' => $productTypeLabel[$r['productType'] ?? ''] ?? ($r['productType'] ?? '—'),
|
||||
'productTitle' => $r['productTitle'] ?? '',
|
||||
'amount' => (int) ($r['amount'] ?? 0),
|
||||
'method' => $r['payMethod'] === 'wechat' ? '微信支付' : ($r['payMethod'] ?? '—'),
|
||||
'date' => !empty($r['payTime']) ? date('Y-m-d H:i', $r['payTime']) : '—',
|
||||
];
|
||||
}
|
||||
|
||||
return success([
|
||||
'list' => $result,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取支付记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出财务报表
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
return success(null, '财务报表导出功能开发中');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,431 +1,431 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 数据概览控制器(超管专用)
|
||||
* 数据来源:mbti_orders、wechat_users、test_results、enterprises;金额单位:分
|
||||
*/
|
||||
class Overview extends BaseController
|
||||
{
|
||||
private const PAID_STATUS = ['paid', 'completed'];
|
||||
|
||||
/**
|
||||
* 获取数据概览
|
||||
* 金额单位:分
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$currentMonthStart = mktime(0, 0, 0, (int) date('n'), 1, (int) date('Y'));
|
||||
$currentMonthEnd = mktime(23, 59, 59, (int) date('n'), (int) date('t'), (int) date('Y'));
|
||||
|
||||
// 企业统计
|
||||
$totalEnterprises = (int) Db::name('enterprises')->count();
|
||||
$newEnterprises = (int) Db::name('enterprises')
|
||||
->where('createdAt', '>=', $currentMonthStart)
|
||||
->where('createdAt', '<=', $currentMonthEnd)
|
||||
->count();
|
||||
|
||||
// 注册用户数(wechat_users 按 openid 去重,无 openid 则按行数)
|
||||
try {
|
||||
$totalRegisteredUsers = (int) Db::name('wechat_users')->count('openid', true);
|
||||
} catch (\Throwable $e) {
|
||||
$totalRegisteredUsers = (int) Db::name('wechat_users')->count();
|
||||
}
|
||||
|
||||
// 有测试记录的用户数(按 wechat_users.openid 去重);本月新增 = 本月首次测试的 openid 数
|
||||
$totalUsers = 0;
|
||||
$newUsers = 0;
|
||||
try {
|
||||
$totalUsers = (int) Db::name('test_results')->distinct(true)->count('userId');
|
||||
$newUsers = (int) Db::name('test_results')
|
||||
->where('createdAt', '>=', $currentMonthStart)
|
||||
->where('createdAt', '<=', $currentMonthEnd)
|
||||
->distinct(true)
|
||||
->count('userId');
|
||||
// 按 openid 去重:tr 关联 wechat_users,统计 distinct openid
|
||||
$hasOpenid = false;
|
||||
try {
|
||||
$openids = Db::name('test_results')->alias('tr')
|
||||
->join('wechat_users w', 'tr.userId = w.id')
|
||||
->distinct(true)
|
||||
->column('w.openid');
|
||||
if (is_array($openids)) {
|
||||
$openids = array_filter(array_unique($openids));
|
||||
$totalUsers = count($openids);
|
||||
$hasOpenid = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
if ($hasOpenid) {
|
||||
$openidsBeforeMonth = Db::name('test_results')->alias('tr')
|
||||
->join('wechat_users w', 'tr.userId = w.id')
|
||||
->where('tr.createdAt', '<', $currentMonthStart)
|
||||
->distinct(true)
|
||||
->column('w.openid');
|
||||
$openidsBeforeMonth = is_array($openidsBeforeMonth) ? array_filter(array_unique($openidsBeforeMonth)) : [];
|
||||
$openidsInMonth = Db::name('test_results')->alias('tr')
|
||||
->join('wechat_users w', 'tr.userId = w.id')
|
||||
->where('tr.createdAt', '>=', $currentMonthStart)
|
||||
->where('tr.createdAt', '<=', $currentMonthEnd)
|
||||
->distinct(true)
|
||||
->column('w.openid');
|
||||
$openidsInMonth = is_array($openidsInMonth) ? array_filter(array_unique($openidsInMonth)) : [];
|
||||
$newUsers = count(array_diff($openidsInMonth, $openidsBeforeMonth));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$newUsers = 0;
|
||||
}
|
||||
|
||||
// 收入与订单(仅 orders,金额分)
|
||||
$totalRevenue = (int) (Db::name('orders')->whereIn('status', self::PAID_STATUS)->sum('amount') ?? 0);
|
||||
$monthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $currentMonthStart)
|
||||
->where('payTime', '<=', $currentMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
$paidOrderCount = (int) Db::name('orders')->whereIn('status', self::PAID_STATUS)->count();
|
||||
|
||||
$lastMonthStart = mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y'));
|
||||
$lastMonthEnd = mktime(23, 59, 59, (int) date('n') - 1, (int) date('t', $lastMonthStart), (int) date('Y'));
|
||||
$lastMonthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $lastMonthStart)
|
||||
->where('payTime', '<=', $lastMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
$revenueGrowth = $lastMonthRevenue > 0
|
||||
? round(($monthRevenue - $lastMonthRevenue) / $lastMonthRevenue * 100, 1)
|
||||
: ($monthRevenue > 0 ? 100.0 : 0);
|
||||
|
||||
// 测试统计
|
||||
$totalTests = 0;
|
||||
$newTests = 0;
|
||||
try {
|
||||
$totalTests = (int) Db::name('test_results')->count();
|
||||
$newTests = (int) Db::name('test_results')
|
||||
->where('createdAt', '>=', $currentMonthStart)
|
||||
->where('createdAt', '<=', $currentMonthEnd)
|
||||
->count();
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return success([
|
||||
'totalEnterprises' => $totalEnterprises,
|
||||
'newEnterprises' => $newEnterprises,
|
||||
'totalRegisteredUsers' => $totalRegisteredUsers,
|
||||
'totalUsers' => $totalUsers,
|
||||
'newUsers' => $newUsers,
|
||||
'totalRevenue' => $totalRevenue,
|
||||
'monthRevenue' => $monthRevenue,
|
||||
'revenueGrowth' => $revenueGrowth,
|
||||
'paidOrderCount' => $paidOrderCount,
|
||||
'totalTests' => $totalTests,
|
||||
'newTests' => $newTests,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取数据概览失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近动态:支付订单、新企业、今日测试等;金额接口为分,文案中转为元
|
||||
*/
|
||||
public function recentDynamics()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$limit = min(20, max(5, (int) Request::param('limit', 10)));
|
||||
$dynamics = [];
|
||||
|
||||
// 1. 最近已支付订单(含个人与企业,金额分)
|
||||
try {
|
||||
$orders = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->field('id, orderNo, userId, enterpriseId, productType, amount, payTime')
|
||||
->order('payTime', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
$orders = is_array($orders) ? $orders : [];
|
||||
|
||||
$eids = array_values(array_unique(array_filter(array_column($orders, 'enterpriseId'))));
|
||||
$uids = array_values(array_unique(array_filter(array_column($orders, 'userId'))));
|
||||
$entMap = [];
|
||||
$userMap = [];
|
||||
if (!empty($eids)) {
|
||||
$entMap = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id') ?: [];
|
||||
}
|
||||
if (!empty($uids)) {
|
||||
$users = Db::name('wechat_users')->where('id', 'in', $uids)->field('id, nickname')->select()->toArray();
|
||||
foreach (is_array($users) ? $users : [] as $u) {
|
||||
$userMap[(int) ($u['id'] ?? 0)] = $u['nickname'] ?? ('用户' . ($u['id'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
$productLabel = ['face' => 'AI人脸', 'mbti' => 'MBTI', 'disc' => 'DISC', 'pdp' => 'PDP', 'report' => '报告'];
|
||||
foreach ($orders as $o) {
|
||||
$amountYuan = isset($o['amount']) ? round((int) $o['amount'] / 100, 2) : 0;
|
||||
$who = '未知';
|
||||
if (!empty($o['enterpriseId']) && isset($entMap[$o['enterpriseId']])) {
|
||||
$who = $entMap[$o['enterpriseId']];
|
||||
} else {
|
||||
$who = $userMap[(int) ($o['userId'] ?? 0)] ?? ('用户' . ($o['userId'] ?? ''));
|
||||
}
|
||||
$product = $productLabel[$o['productType'] ?? ''] ?? ($o['productType'] ?? '');
|
||||
$dynamics[] = [
|
||||
'type' => 'payment',
|
||||
'icon' => 'TrendCharts',
|
||||
'text' => $who . ' 支付 ¥' . number_format($amountYuan, 2) . ($product ? '(' . $product . ')' : ''),
|
||||
'time' => $this->formatTime($o['payTime'] ?? null),
|
||||
'sortTime' => (int) ($o['payTime'] ?? 0),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 订单数据异常不影响其他动态
|
||||
}
|
||||
|
||||
// 2. 最近入驻企业
|
||||
try {
|
||||
$enterprises = Db::name('enterprises')
|
||||
->field('name, createdAt')
|
||||
->order('createdAt', 'desc')
|
||||
->limit(5)
|
||||
->select()
|
||||
->toArray();
|
||||
foreach (is_array($enterprises) ? $enterprises : [] as $e) {
|
||||
$dynamics[] = [
|
||||
'type' => 'enterprise',
|
||||
'icon' => 'Document',
|
||||
'text' => ($e['name'] ?? '') . ' 完成企业入驻',
|
||||
'time' => $this->formatTime($e['createdAt'] ?? null),
|
||||
'sortTime' => (int) ($e['createdAt'] ?? 0),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
// 3. 今日测试量(按企业/个人分组,文案里带企业名称)
|
||||
try {
|
||||
$todayStart = mktime(0, 0, 0, (int) date('n'), (int) date('j'), (int) date('Y'));
|
||||
$rows = Db::name('test_results')
|
||||
->alias('tr')
|
||||
->leftJoin('enterprises e', 'tr.enterpriseId = e.id')
|
||||
->where('tr.createdAt', '>=', $todayStart)
|
||||
->field('tr.enterpriseId, e.name as enterpriseName, COUNT(*) as cnt')
|
||||
->group('tr.enterpriseId')
|
||||
->order('cnt', 'desc')
|
||||
->limit(5)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$totalToday = 0;
|
||||
foreach (is_array($rows) ? $rows : [] as $row) {
|
||||
$cnt = (int) ($row['cnt'] ?? 0);
|
||||
if ($cnt <= 0) {
|
||||
continue;
|
||||
}
|
||||
$totalToday += $cnt;
|
||||
$eid = $row['enterpriseId'] ?? null;
|
||||
$name = $row['enterpriseName'] ?? '';
|
||||
if ($eid && !$name) {
|
||||
$name = '企业' . $eid;
|
||||
}
|
||||
if (!$eid) {
|
||||
$name = $name ?: '个人用户(无企业)';
|
||||
}
|
||||
$dynamics[] = [
|
||||
'type' => 'test',
|
||||
'icon' => 'TrendCharts',
|
||||
'text' => $name . ' 今日完成 ' . $cnt . ' 次测试',
|
||||
'time' => '今日',
|
||||
'sortTime' => $todayStart + 1,
|
||||
];
|
||||
}
|
||||
|
||||
// 追加一条全局汇总(放在企业之后)
|
||||
if ($totalToday > 0) {
|
||||
$dynamics[] = [
|
||||
'type' => 'test-total',
|
||||
'icon' => 'TrendCharts',
|
||||
'text' => '全站今日共完成 ' . $totalToday . ' 次测试',
|
||||
'time' => '今日',
|
||||
'sortTime' => $todayStart,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
usort($dynamics, function ($a, $b) {
|
||||
return ($b['sortTime'] ?? 0) - ($a['sortTime'] ?? 0);
|
||||
});
|
||||
$dynamics = array_slice($dynamics, 0, $limit);
|
||||
|
||||
return success($dynamics);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取最近动态失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近 N 天测试趋势(按日期 & 测试类型统计)
|
||||
* GET /superadmin/overview/test-trends?days=14
|
||||
*/
|
||||
public function testTrends()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$days = (int) Request::param('days', 14);
|
||||
$days = min(60, max(7, $days));
|
||||
|
||||
$startDate = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')));
|
||||
|
||||
$rows = Db::name('test_results')
|
||||
->where('createdAt', '>=', $startDate)
|
||||
->whereIn('testType', ['face', 'mbti', 'disc', 'pdp'])
|
||||
->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, testType, COUNT(*) as c")
|
||||
->group('d,testType')
|
||||
->order('d', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$trendMap = [];
|
||||
foreach (is_array($rows) ? $rows : [] as $row) {
|
||||
$d = $row['d'];
|
||||
$type = $row['testType'];
|
||||
$cnt = (int) ($row['c'] ?? 0);
|
||||
if (!isset($trendMap[$d])) {
|
||||
$trendMap[$d] = [
|
||||
'date' => $d,
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
if (in_array($type, ['face', 'mbti', 'disc', 'pdp'], true)) {
|
||||
$trendMap[$d][$type] += $cnt;
|
||||
$trendMap[$d]['total'] += $cnt;
|
||||
}
|
||||
}
|
||||
|
||||
$trendData = [];
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$d = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days'));
|
||||
if (isset($trendMap[$d])) {
|
||||
$trendData[] = $trendMap[$d];
|
||||
} else {
|
||||
$trendData[] = [
|
||||
'date' => $d,
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return success($trendData);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取测试趋势失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业活跃排行(按测试次数、支付金额);金额单位:分
|
||||
*/
|
||||
public function enterpriseRanking()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$limit = min(20, max(5, (int) Request::param('limit', 10)));
|
||||
$result = [];
|
||||
|
||||
try {
|
||||
// 企业表 left join 测试与订单,保证无测试/无订单的企业也出现(测试数、金额为 0)
|
||||
$list = Db::name('enterprises')
|
||||
->alias('e')
|
||||
->leftJoin('test_results tr', 'tr.enterpriseId = e.id')
|
||||
->leftJoin('orders o', 'o.enterpriseId = e.id AND o.status IN (\'paid\',\'completed\')')
|
||||
->field('e.id, e.name, COUNT(DISTINCT tr.id) as testCount, COALESCE(SUM(o.amount), 0) as totalAmount')
|
||||
->group('e.id')
|
||||
->order('testCount', 'desc')
|
||||
->order('totalAmount', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach (is_array($list) ? $list : [] as $item) {
|
||||
$result[] = [
|
||||
'id' => (int) ($item['id'] ?? 0),
|
||||
'name' => $item['name'] ?? '',
|
||||
'tests' => (int) ($item['testCount'] ?? 0),
|
||||
'amount' => (int) ($item['totalAmount'] ?? 0),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 若 join 报错(如表/字段不一致),降级为只查企业列表,测试与金额为 0
|
||||
$list = Db::name('enterprises')->field('id, name')->order('id', 'desc')->limit($limit)->select()->toArray();
|
||||
foreach (is_array($list) ? $list : [] as $item) {
|
||||
$result[] = [
|
||||
'id' => (int) ($item['id'] ?? 0),
|
||||
'name' => $item['name'] ?? '',
|
||||
'tests' => 0,
|
||||
'amount' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return success($result);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取企业排行失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function formatTime($timestamp)
|
||||
{
|
||||
if ($timestamp === null || $timestamp === '') {
|
||||
return '';
|
||||
}
|
||||
$ts = is_numeric($timestamp) ? (int) $timestamp : strtotime($timestamp);
|
||||
if ($ts <= 0) {
|
||||
return '';
|
||||
}
|
||||
$diff = time() - $ts;
|
||||
if ($diff < 60) {
|
||||
return '刚刚';
|
||||
}
|
||||
if ($diff < 3600) {
|
||||
return floor($diff / 60) . '分钟前';
|
||||
}
|
||||
if ($diff < 86400) {
|
||||
return floor($diff / 3600) . '小时前';
|
||||
}
|
||||
if ($diff < 604800) {
|
||||
return floor($diff / 86400) . '天前';
|
||||
}
|
||||
return date('Y-m-d H:i', $ts);
|
||||
}
|
||||
}
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 数据概览控制器(超管专用)
|
||||
* 数据来源:mbti_orders、wechat_users、test_results、enterprises;金额单位:分
|
||||
*/
|
||||
class Overview extends BaseController
|
||||
{
|
||||
private const PAID_STATUS = ['paid', 'completed'];
|
||||
|
||||
/**
|
||||
* 获取数据概览
|
||||
* 金额单位:分
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$currentMonthStart = mktime(0, 0, 0, (int) date('n'), 1, (int) date('Y'));
|
||||
$currentMonthEnd = mktime(23, 59, 59, (int) date('n'), (int) date('t'), (int) date('Y'));
|
||||
|
||||
// 企业统计
|
||||
$totalEnterprises = (int) Db::name('enterprises')->count();
|
||||
$newEnterprises = (int) Db::name('enterprises')
|
||||
->where('createdAt', '>=', $currentMonthStart)
|
||||
->where('createdAt', '<=', $currentMonthEnd)
|
||||
->count();
|
||||
|
||||
// 注册用户数(wechat_users 按 openid 去重,无 openid 则按行数)
|
||||
try {
|
||||
$totalRegisteredUsers = (int) Db::name('wechat_users')->count('openid', true);
|
||||
} catch (\Throwable $e) {
|
||||
$totalRegisteredUsers = (int) Db::name('wechat_users')->count();
|
||||
}
|
||||
|
||||
// 有测试记录的用户数(按 wechat_users.openid 去重);本月新增 = 本月首次测试的 openid 数
|
||||
$totalUsers = 0;
|
||||
$newUsers = 0;
|
||||
try {
|
||||
$totalUsers = (int) Db::name('test_results')->distinct(true)->count('userId');
|
||||
$newUsers = (int) Db::name('test_results')
|
||||
->where('createdAt', '>=', $currentMonthStart)
|
||||
->where('createdAt', '<=', $currentMonthEnd)
|
||||
->distinct(true)
|
||||
->count('userId');
|
||||
// 按 openid 去重:tr 关联 wechat_users,统计 distinct openid
|
||||
$hasOpenid = false;
|
||||
try {
|
||||
$openids = Db::name('test_results')->alias('tr')
|
||||
->join('wechat_users w', 'tr.userId = w.id')
|
||||
->distinct(true)
|
||||
->column('w.openid');
|
||||
if (is_array($openids)) {
|
||||
$openids = array_filter(array_unique($openids));
|
||||
$totalUsers = count($openids);
|
||||
$hasOpenid = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
if ($hasOpenid) {
|
||||
$openidsBeforeMonth = Db::name('test_results')->alias('tr')
|
||||
->join('wechat_users w', 'tr.userId = w.id')
|
||||
->where('tr.createdAt', '<', $currentMonthStart)
|
||||
->distinct(true)
|
||||
->column('w.openid');
|
||||
$openidsBeforeMonth = is_array($openidsBeforeMonth) ? array_filter(array_unique($openidsBeforeMonth)) : [];
|
||||
$openidsInMonth = Db::name('test_results')->alias('tr')
|
||||
->join('wechat_users w', 'tr.userId = w.id')
|
||||
->where('tr.createdAt', '>=', $currentMonthStart)
|
||||
->where('tr.createdAt', '<=', $currentMonthEnd)
|
||||
->distinct(true)
|
||||
->column('w.openid');
|
||||
$openidsInMonth = is_array($openidsInMonth) ? array_filter(array_unique($openidsInMonth)) : [];
|
||||
$newUsers = count(array_diff($openidsInMonth, $openidsBeforeMonth));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$newUsers = 0;
|
||||
}
|
||||
|
||||
// 收入与订单(仅 orders,金额分)
|
||||
$totalRevenue = (int) (Db::name('orders')->whereIn('status', self::PAID_STATUS)->sum('amount') ?? 0);
|
||||
$monthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $currentMonthStart)
|
||||
->where('payTime', '<=', $currentMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
$paidOrderCount = (int) Db::name('orders')->whereIn('status', self::PAID_STATUS)->count();
|
||||
|
||||
$lastMonthStart = mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y'));
|
||||
$lastMonthEnd = mktime(23, 59, 59, (int) date('n') - 1, (int) date('t', $lastMonthStart), (int) date('Y'));
|
||||
$lastMonthRevenue = (int) (Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->where('payTime', '>=', $lastMonthStart)
|
||||
->where('payTime', '<=', $lastMonthEnd)
|
||||
->sum('amount') ?? 0);
|
||||
$revenueGrowth = $lastMonthRevenue > 0
|
||||
? round(($monthRevenue - $lastMonthRevenue) / $lastMonthRevenue * 100, 1)
|
||||
: ($monthRevenue > 0 ? 100.0 : 0);
|
||||
|
||||
// 测试统计
|
||||
$totalTests = 0;
|
||||
$newTests = 0;
|
||||
try {
|
||||
$totalTests = (int) Db::name('test_results')->count();
|
||||
$newTests = (int) Db::name('test_results')
|
||||
->where('createdAt', '>=', $currentMonthStart)
|
||||
->where('createdAt', '<=', $currentMonthEnd)
|
||||
->count();
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
return success([
|
||||
'totalEnterprises' => $totalEnterprises,
|
||||
'newEnterprises' => $newEnterprises,
|
||||
'totalRegisteredUsers' => $totalRegisteredUsers,
|
||||
'totalUsers' => $totalUsers,
|
||||
'newUsers' => $newUsers,
|
||||
'totalRevenue' => $totalRevenue,
|
||||
'monthRevenue' => $monthRevenue,
|
||||
'revenueGrowth' => $revenueGrowth,
|
||||
'paidOrderCount' => $paidOrderCount,
|
||||
'totalTests' => $totalTests,
|
||||
'newTests' => $newTests,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取数据概览失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近动态:支付订单、新企业、今日测试等;金额接口为分,文案中转为元
|
||||
*/
|
||||
public function recentDynamics()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$limit = min(20, max(5, (int) Request::param('limit', 10)));
|
||||
$dynamics = [];
|
||||
|
||||
// 1. 最近已支付订单(含个人与企业,金额分)
|
||||
try {
|
||||
$orders = Db::name('orders')
|
||||
->whereIn('status', self::PAID_STATUS)
|
||||
->field('id, orderNo, userId, enterpriseId, productType, amount, payTime')
|
||||
->order('payTime', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
$orders = is_array($orders) ? $orders : [];
|
||||
|
||||
$eids = array_values(array_unique(array_filter(array_column($orders, 'enterpriseId'))));
|
||||
$uids = array_values(array_unique(array_filter(array_column($orders, 'userId'))));
|
||||
$entMap = [];
|
||||
$userMap = [];
|
||||
if (!empty($eids)) {
|
||||
$entMap = Db::name('enterprises')->where('id', 'in', $eids)->column('name', 'id') ?: [];
|
||||
}
|
||||
if (!empty($uids)) {
|
||||
$users = Db::name('wechat_users')->where('id', 'in', $uids)->field('id, nickname')->select()->toArray();
|
||||
foreach (is_array($users) ? $users : [] as $u) {
|
||||
$userMap[(int) ($u['id'] ?? 0)] = $u['nickname'] ?? ('用户' . ($u['id'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
$productLabel = ['face' => 'AI人脸', 'mbti' => 'MBTI', 'disc' => 'DISC', 'pdp' => 'PDP', 'report' => '报告'];
|
||||
foreach ($orders as $o) {
|
||||
$amountYuan = isset($o['amount']) ? round((int) $o['amount'] / 100, 2) : 0;
|
||||
$who = '未知';
|
||||
if (!empty($o['enterpriseId']) && isset($entMap[$o['enterpriseId']])) {
|
||||
$who = $entMap[$o['enterpriseId']];
|
||||
} else {
|
||||
$who = $userMap[(int) ($o['userId'] ?? 0)] ?? ('用户' . ($o['userId'] ?? ''));
|
||||
}
|
||||
$product = $productLabel[$o['productType'] ?? ''] ?? ($o['productType'] ?? '');
|
||||
$dynamics[] = [
|
||||
'type' => 'payment',
|
||||
'icon' => 'TrendCharts',
|
||||
'text' => $who . ' 支付 ¥' . number_format($amountYuan, 2) . ($product ? '(' . $product . ')' : ''),
|
||||
'time' => $this->formatTime($o['payTime'] ?? null),
|
||||
'sortTime' => (int) ($o['payTime'] ?? 0),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 订单数据异常不影响其他动态
|
||||
}
|
||||
|
||||
// 2. 最近入驻企业
|
||||
try {
|
||||
$enterprises = Db::name('enterprises')
|
||||
->field('name, createdAt')
|
||||
->order('createdAt', 'desc')
|
||||
->limit(5)
|
||||
->select()
|
||||
->toArray();
|
||||
foreach (is_array($enterprises) ? $enterprises : [] as $e) {
|
||||
$dynamics[] = [
|
||||
'type' => 'enterprise',
|
||||
'icon' => 'Document',
|
||||
'text' => ($e['name'] ?? '') . ' 完成企业入驻',
|
||||
'time' => $this->formatTime($e['createdAt'] ?? null),
|
||||
'sortTime' => (int) ($e['createdAt'] ?? 0),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
// 3. 今日测试量(按企业/个人分组,文案里带企业名称)
|
||||
try {
|
||||
$todayStart = mktime(0, 0, 0, (int) date('n'), (int) date('j'), (int) date('Y'));
|
||||
$rows = Db::name('test_results')
|
||||
->alias('tr')
|
||||
->leftJoin('enterprises e', 'tr.enterpriseId = e.id')
|
||||
->where('tr.createdAt', '>=', $todayStart)
|
||||
->field('tr.enterpriseId, e.name as enterpriseName, COUNT(*) as cnt')
|
||||
->group('tr.enterpriseId')
|
||||
->order('cnt', 'desc')
|
||||
->limit(5)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$totalToday = 0;
|
||||
foreach (is_array($rows) ? $rows : [] as $row) {
|
||||
$cnt = (int) ($row['cnt'] ?? 0);
|
||||
if ($cnt <= 0) {
|
||||
continue;
|
||||
}
|
||||
$totalToday += $cnt;
|
||||
$eid = $row['enterpriseId'] ?? null;
|
||||
$name = $row['enterpriseName'] ?? '';
|
||||
if ($eid && !$name) {
|
||||
$name = '企业' . $eid;
|
||||
}
|
||||
if (!$eid) {
|
||||
$name = $name ?: '个人用户(无企业)';
|
||||
}
|
||||
$dynamics[] = [
|
||||
'type' => 'test',
|
||||
'icon' => 'TrendCharts',
|
||||
'text' => $name . ' 今日完成 ' . $cnt . ' 次测试',
|
||||
'time' => '今日',
|
||||
'sortTime' => $todayStart + 1,
|
||||
];
|
||||
}
|
||||
|
||||
// 追加一条全局汇总(放在企业之后)
|
||||
if ($totalToday > 0) {
|
||||
$dynamics[] = [
|
||||
'type' => 'test-total',
|
||||
'icon' => 'TrendCharts',
|
||||
'text' => '全站今日共完成 ' . $totalToday . ' 次测试',
|
||||
'time' => '今日',
|
||||
'sortTime' => $todayStart,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
usort($dynamics, function ($a, $b) {
|
||||
return ($b['sortTime'] ?? 0) - ($a['sortTime'] ?? 0);
|
||||
});
|
||||
$dynamics = array_slice($dynamics, 0, $limit);
|
||||
|
||||
return success($dynamics);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取最近动态失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最近 N 天测试趋势(按日期 & 测试类型统计)
|
||||
* GET /superadmin/overview/test-trends?days=14
|
||||
*/
|
||||
public function testTrends()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$days = (int) Request::param('days', 14);
|
||||
$days = min(60, max(7, $days));
|
||||
|
||||
$startDate = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')));
|
||||
|
||||
$rows = Db::name('test_results')
|
||||
->where('createdAt', '>=', $startDate)
|
||||
->whereIn('testType', ['face', 'mbti', 'disc', 'pdp'])
|
||||
->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, testType, COUNT(*) as c")
|
||||
->group('d,testType')
|
||||
->order('d', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$trendMap = [];
|
||||
foreach (is_array($rows) ? $rows : [] as $row) {
|
||||
$d = $row['d'];
|
||||
$type = $row['testType'];
|
||||
$cnt = (int) ($row['c'] ?? 0);
|
||||
if (!isset($trendMap[$d])) {
|
||||
$trendMap[$d] = [
|
||||
'date' => $d,
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
if (in_array($type, ['face', 'mbti', 'disc', 'pdp'], true)) {
|
||||
$trendMap[$d][$type] += $cnt;
|
||||
$trendMap[$d]['total'] += $cnt;
|
||||
}
|
||||
}
|
||||
|
||||
$trendData = [];
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$d = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days'));
|
||||
if (isset($trendMap[$d])) {
|
||||
$trendData[] = $trendMap[$d];
|
||||
} else {
|
||||
$trendData[] = [
|
||||
'date' => $d,
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return success($trendData);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取测试趋势失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业活跃排行(按测试次数、支付金额);金额单位:分
|
||||
*/
|
||||
public function enterpriseRanking()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || ($user['role'] ?? '') !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$limit = min(20, max(5, (int) Request::param('limit', 10)));
|
||||
$result = [];
|
||||
|
||||
try {
|
||||
// 企业表 left join 测试与订单,保证无测试/无订单的企业也出现(测试数、金额为 0)
|
||||
$list = Db::name('enterprises')
|
||||
->alias('e')
|
||||
->leftJoin('test_results tr', 'tr.enterpriseId = e.id')
|
||||
->leftJoin('orders o', 'o.enterpriseId = e.id AND o.status IN (\'paid\',\'completed\')')
|
||||
->field('e.id, e.name, COUNT(DISTINCT tr.id) as testCount, COALESCE(SUM(o.amount), 0) as totalAmount')
|
||||
->group('e.id')
|
||||
->order('testCount', 'desc')
|
||||
->order('totalAmount', 'desc')
|
||||
->limit($limit)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach (is_array($list) ? $list : [] as $item) {
|
||||
$result[] = [
|
||||
'id' => (int) ($item['id'] ?? 0),
|
||||
'name' => $item['name'] ?? '',
|
||||
'tests' => (int) ($item['testCount'] ?? 0),
|
||||
'amount' => (int) ($item['totalAmount'] ?? 0),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 若 join 报错(如表/字段不一致),降级为只查企业列表,测试与金额为 0
|
||||
$list = Db::name('enterprises')->field('id, name')->order('id', 'desc')->limit($limit)->select()->toArray();
|
||||
foreach (is_array($list) ? $list : [] as $item) {
|
||||
$result[] = [
|
||||
'id' => (int) ($item['id'] ?? 0),
|
||||
'name' => $item['name'] ?? '',
|
||||
'tests' => 0,
|
||||
'amount' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return success($result);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取企业排行失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function formatTime($timestamp)
|
||||
{
|
||||
if ($timestamp === null || $timestamp === '') {
|
||||
return '';
|
||||
}
|
||||
$ts = is_numeric($timestamp) ? (int) $timestamp : strtotime($timestamp);
|
||||
if ($ts <= 0) {
|
||||
return '';
|
||||
}
|
||||
$diff = time() - $ts;
|
||||
if ($diff < 60) {
|
||||
return '刚刚';
|
||||
}
|
||||
if ($diff < 3600) {
|
||||
return floor($diff / 60) . '分钟前';
|
||||
}
|
||||
if ($diff < 86400) {
|
||||
return floor($diff / 3600) . '小时前';
|
||||
}
|
||||
if ($diff < 604800) {
|
||||
return floor($diff / 86400) . '天前';
|
||||
}
|
||||
return date('Y-m-d H:i', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +1,200 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 全局定价管理控制器(超管专用)
|
||||
*/
|
||||
class Pricing extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$type = Request::param('type', ''); // personal/enterprise/deep
|
||||
$enterpriseId = Request::param('enterpriseId', null); // 仅 type=enterprise 时有效,不传为全局
|
||||
|
||||
if ($type) {
|
||||
$enterpriseId = $enterpriseId !== null && $enterpriseId !== '' ? (int) $enterpriseId : null;
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
$query->where(empty($enterpriseId) ? 'enterpriseId' : 'enterpriseId', empty($enterpriseId) ? 'null' : '=', empty($enterpriseId) ? null : $enterpriseId);
|
||||
if (empty($enterpriseId)) {
|
||||
$query->whereNull('enterpriseId');
|
||||
} else {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
$config = $query->find();
|
||||
if (!$config) {
|
||||
return error('定价配置不存在', 404);
|
||||
}
|
||||
return success([
|
||||
'type' => $config->type,
|
||||
'enterpriseId' => $config->enterpriseId,
|
||||
'config' => $config->config
|
||||
]);
|
||||
} else {
|
||||
// 获取所有:个人/深度各一条(全局),企业=全局默认定价 + 各企业专属列表
|
||||
$configs = PricingConfigModel::select()->toArray();
|
||||
$result = ['personal' => null, 'enterprise' => null, 'deep' => null, 'enterpriseList' => []];
|
||||
foreach ($configs as $row) {
|
||||
if ($row['enterpriseId'] === null || $row['enterpriseId'] === '') {
|
||||
$result[$row['type']] = $row['config'];
|
||||
} else {
|
||||
if ($row['type'] === 'enterprise') {
|
||||
$result['enterpriseList'][] = ['enterpriseId' => (int) $row['enterpriseId'], 'config' => $row['config']];
|
||||
}
|
||||
}
|
||||
}
|
||||
return success($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// PUT JSON body 需显式解析,直接用 param() 读深层嵌套数组可能丢失数据
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
|
||||
if (is_array($jsonData)) {
|
||||
$type = (string) ($jsonData['type'] ?? '');
|
||||
$enterpriseId = $jsonData['enterpriseId'] ?? null;
|
||||
$config = $jsonData['config'] ?? [];
|
||||
} else {
|
||||
$type = (string) Request::param('type', '');
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
$config = Request::param('config', []);
|
||||
}
|
||||
|
||||
if (empty($type)) {
|
||||
return error('定价类型不能为空', 400);
|
||||
}
|
||||
|
||||
if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) {
|
||||
return error('定价类型无效', 400);
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$enterpriseId = ($type === 'enterprise' && $enterpriseId !== null && $enterpriseId !== '') ? (int) $enterpriseId : null;
|
||||
if ($type !== 'enterprise') {
|
||||
$enterpriseId = null;
|
||||
}
|
||||
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
if ($enterpriseId !== null) {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
// deep_personal / deep_enterprise 仅全局一条,不按企业分
|
||||
$pricingConfig = $query->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
$pricingConfig = PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
|
||||
return success([
|
||||
'type' => $pricingConfig->type,
|
||||
'enterpriseId' => $pricingConfig->enterpriseId,
|
||||
'config' => $pricingConfig->config
|
||||
], '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchUpdate()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
$data = is_array($jsonData) ? ($jsonData['data'] ?? []) : Request::param('data', []);
|
||||
|
||||
if (empty($data) || !is_array($data)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($data as $type => $config) {
|
||||
if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) {
|
||||
$errors[] = "类型 {$type} 无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
$errors[] = "类型 {$type} 的配置数据无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$pricingConfig = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "保存类型 {$type} 失败:" . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return error('部分配置保存失败:' . implode(';', $errors), 400);
|
||||
}
|
||||
|
||||
return success(null, "成功保存 {$successCount} 个配置");
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 全局定价管理控制器(超管专用)
|
||||
*/
|
||||
class Pricing extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$type = Request::param('type', ''); // personal/enterprise/deep
|
||||
$enterpriseId = Request::param('enterpriseId', null); // 仅 type=enterprise 时有效,不传为全局
|
||||
|
||||
if ($type) {
|
||||
$enterpriseId = $enterpriseId !== null && $enterpriseId !== '' ? (int) $enterpriseId : null;
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
$query->where(empty($enterpriseId) ? 'enterpriseId' : 'enterpriseId', empty($enterpriseId) ? 'null' : '=', empty($enterpriseId) ? null : $enterpriseId);
|
||||
if (empty($enterpriseId)) {
|
||||
$query->whereNull('enterpriseId');
|
||||
} else {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
$config = $query->find();
|
||||
if (!$config) {
|
||||
return error('定价配置不存在', 404);
|
||||
}
|
||||
return success([
|
||||
'type' => $config->type,
|
||||
'enterpriseId' => $config->enterpriseId,
|
||||
'config' => $config->config
|
||||
]);
|
||||
} else {
|
||||
// 获取所有:个人/深度各一条(全局),企业=全局默认定价 + 各企业专属列表
|
||||
$configs = PricingConfigModel::select()->toArray();
|
||||
$result = ['personal' => null, 'enterprise' => null, 'deep' => null, 'enterpriseList' => []];
|
||||
foreach ($configs as $row) {
|
||||
if ($row['enterpriseId'] === null || $row['enterpriseId'] === '') {
|
||||
$result[$row['type']] = $row['config'];
|
||||
} else {
|
||||
if ($row['type'] === 'enterprise') {
|
||||
$result['enterpriseList'][] = ['enterpriseId' => (int) $row['enterpriseId'], 'config' => $row['config']];
|
||||
}
|
||||
}
|
||||
}
|
||||
return success($result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// PUT JSON body 需显式解析,直接用 param() 读深层嵌套数组可能丢失数据
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
|
||||
if (is_array($jsonData)) {
|
||||
$type = (string) ($jsonData['type'] ?? '');
|
||||
$enterpriseId = $jsonData['enterpriseId'] ?? null;
|
||||
$config = $jsonData['config'] ?? [];
|
||||
} else {
|
||||
$type = (string) Request::param('type', '');
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
$config = Request::param('config', []);
|
||||
}
|
||||
|
||||
if (empty($type)) {
|
||||
return error('定价类型不能为空', 400);
|
||||
}
|
||||
|
||||
if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) {
|
||||
return error('定价类型无效', 400);
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$enterpriseId = ($type === 'enterprise' && $enterpriseId !== null && $enterpriseId !== '') ? (int) $enterpriseId : null;
|
||||
if ($type !== 'enterprise') {
|
||||
$enterpriseId = null;
|
||||
}
|
||||
|
||||
$query = PricingConfigModel::where('type', $type);
|
||||
if ($type === 'enterprise') {
|
||||
if ($enterpriseId !== null) {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
} else {
|
||||
$query->whereNull('enterpriseId');
|
||||
}
|
||||
// deep_personal / deep_enterprise 仅全局一条,不按企业分
|
||||
$pricingConfig = $query->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
$pricingConfig = PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
|
||||
return success([
|
||||
'type' => $pricingConfig->type,
|
||||
'enterpriseId' => $pricingConfig->enterpriseId,
|
||||
'config' => $pricingConfig->config
|
||||
], '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新定价配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchUpdate()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$rawBody = (string) $this->request->getContent();
|
||||
$jsonData = ($rawBody !== '') ? json_decode($rawBody, true) : null;
|
||||
$data = is_array($jsonData) ? ($jsonData['data'] ?? []) : Request::param('data', []);
|
||||
|
||||
if (empty($data) || !is_array($data)) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($data as $type => $config) {
|
||||
if (!in_array($type, ['personal', 'enterprise', 'deep', 'deep_personal', 'deep_enterprise'])) {
|
||||
$errors[] = "类型 {$type} 无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($config) || !is_array($config)) {
|
||||
$errors[] = "类型 {$type} 的配置数据无效";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$pricingConfig = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find();
|
||||
|
||||
if (!$pricingConfig) {
|
||||
PricingConfigModel::create([
|
||||
'type' => $type,
|
||||
'config' => $config
|
||||
]);
|
||||
} else {
|
||||
$pricingConfig->config = $config;
|
||||
$pricingConfig->save();
|
||||
}
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "保存类型 {$type} 失败:" . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return error('部分配置保存失败:' . implode(';', $errors), 400);
|
||||
}
|
||||
|
||||
return success(null, "成功保存 {$successCount} 个配置");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,332 +1,332 @@
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Question as QuestionModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 题库管理控制器(超管专用)
|
||||
* 管理超管题库(enterpriseId = NULL)
|
||||
*/
|
||||
class Question extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取题库列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = Request::param('page', 1);
|
||||
$pageSize = Request::param('pageSize', 20);
|
||||
$type = Request::param('type', ''); // mbti/disc/pdp
|
||||
$status = Request::param('status', ''); // 1启用/0禁用
|
||||
|
||||
$where = [];
|
||||
|
||||
// 只查询超管题库(enterpriseId = NULL)
|
||||
$where['enterpriseId'] = null;
|
||||
|
||||
// 类型筛选
|
||||
if ($type) {
|
||||
$where['type'] = $type;
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
|
||||
// 查询题库列表
|
||||
$list = QuestionModel::where($where)
|
||||
->order('sort', 'asc')
|
||||
->order('id', 'asc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 处理 options 字段,确保返回数组格式
|
||||
foreach ($list as &$item) {
|
||||
if (isset($item['options'])) {
|
||||
// 如果是对象格式(stdClass),先转换为数组
|
||||
if (is_object($item['options'])) {
|
||||
$item['options'] = json_decode(json_encode($item['options']), true);
|
||||
}
|
||||
// 如果是关联数组(不是索引数组),转换为索引数组
|
||||
if (is_array($item['options']) && !isset($item['options'][0])) {
|
||||
$item['options'] = array_values($item['options']);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
// 总数
|
||||
$total = QuestionModel::where($where)->count();
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能查看超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
$data = $question->toArray();
|
||||
|
||||
// 处理 options 字段,确保返回数组格式
|
||||
if (isset($data['options'])) {
|
||||
// 如果是对象格式(stdClass),先转换为数组
|
||||
if (is_object($data['options'])) {
|
||||
$data['options'] = json_decode(json_encode($data['options']), true);
|
||||
}
|
||||
// 如果是关联数组(不是索引数组),转换为索引数组
|
||||
if (is_array($data['options']) && !isset($data['options'][0])) {
|
||||
$data['options'] = array_values($data['options']);
|
||||
}
|
||||
}
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建题目
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']);
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($data['type']) || empty($data['question']) || empty($data['options'])) {
|
||||
return error('题目类型、题目内容和选项不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证类型
|
||||
if (!in_array($data['type'], ['mbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (!is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
// MBTI类型需要dimension字段
|
||||
if ($data['type'] === 'mbti' && empty($data['dimension'])) {
|
||||
return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400);
|
||||
}
|
||||
|
||||
// 设置超管题库标识(enterpriseId = NULL)
|
||||
$data['enterpriseId'] = null;
|
||||
|
||||
// 设置默认值
|
||||
$data['sort'] = $data['sort'] ?? 0;
|
||||
$data['status'] = $data['status'] ?? 1;
|
||||
|
||||
// 创建题目
|
||||
$question = QuestionModel::create($data);
|
||||
|
||||
return success($question->toArray(), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新题目
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能更新超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
$data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']);
|
||||
|
||||
// 验证类型
|
||||
if (isset($data['type']) && !in_array($data['type'], ['mbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (isset($data['options']) && !is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
// MBTI类型需要dimension字段
|
||||
if (($data['type'] ?? $question->type) === 'mbti' && empty($data['dimension'] ?? $question->dimension)) {
|
||||
return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400);
|
||||
}
|
||||
|
||||
// 更新题目
|
||||
$question->save($data);
|
||||
|
||||
return success($question->toArray(), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除题目(软删除)
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能删除超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
// 执行软删除
|
||||
$question->delete();
|
||||
|
||||
return success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入题目
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchImport()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$questions = Request::param('questions', []);
|
||||
|
||||
if (empty($questions) || !is_array($questions)) {
|
||||
return error('题目数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
$errors = [];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($questions as $index => $q) {
|
||||
// 验证必填字段
|
||||
if (empty($q['type']) || empty($q['question']) || empty($q['options'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:题目类型、题目内容和选项不能为空";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 验证类型
|
||||
if (!in_array($q['type'], ['mbti', 'disc', 'pdp'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:题目类型必须是 mbti、disc 或 pdp";
|
||||
continue;
|
||||
}
|
||||
|
||||
// MBTI类型需要dimension字段
|
||||
if ($q['type'] === 'mbti' && empty($q['dimension'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:MBTI类型题目必须指定维度";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 设置超管题库标识
|
||||
$q['enterpriseId'] = null;
|
||||
$q['sort'] = $q['sort'] ?? ($index + 1);
|
||||
$q['status'] = $q['status'] ?? 1;
|
||||
|
||||
QuestionModel::create($q);
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return error('批量导入失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return success([
|
||||
'successCount' => $successCount,
|
||||
'failCount' => $failCount,
|
||||
'errors' => $errors
|
||||
], "成功导入 {$successCount} 题,失败 {$failCount} 题");
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换题目状态
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function toggleStatus($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能操作超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
$question->status = $question->status == 1 ? 0 : 1;
|
||||
$question->save();
|
||||
|
||||
return success($question->toArray(), '状态更新成功');
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\superadmin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Question as QuestionModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 题库管理控制器(超管专用)
|
||||
* 管理超管题库(enterpriseId = NULL)
|
||||
*/
|
||||
class Question extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取题库列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = Request::param('page', 1);
|
||||
$pageSize = Request::param('pageSize', 20);
|
||||
$type = Request::param('type', ''); // mbti/disc/pdp
|
||||
$status = Request::param('status', ''); // 1启用/0禁用
|
||||
|
||||
$where = [];
|
||||
|
||||
// 只查询超管题库(enterpriseId = NULL)
|
||||
$where['enterpriseId'] = null;
|
||||
|
||||
// 类型筛选
|
||||
if ($type) {
|
||||
$where['type'] = $type;
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if ($status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
|
||||
// 查询题库列表
|
||||
$list = QuestionModel::where($where)
|
||||
->order('sort', 'asc')
|
||||
->order('id', 'asc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 处理 options 字段,确保返回数组格式
|
||||
foreach ($list as &$item) {
|
||||
if (isset($item['options'])) {
|
||||
// 如果是对象格式(stdClass),先转换为数组
|
||||
if (is_object($item['options'])) {
|
||||
$item['options'] = json_decode(json_encode($item['options']), true);
|
||||
}
|
||||
// 如果是关联数组(不是索引数组),转换为索引数组
|
||||
if (is_array($item['options']) && !isset($item['options'][0])) {
|
||||
$item['options'] = array_values($item['options']);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
// 总数
|
||||
$total = QuestionModel::where($where)->count();
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能查看超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
$data = $question->toArray();
|
||||
|
||||
// 处理 options 字段,确保返回数组格式
|
||||
if (isset($data['options'])) {
|
||||
// 如果是对象格式(stdClass),先转换为数组
|
||||
if (is_object($data['options'])) {
|
||||
$data['options'] = json_decode(json_encode($data['options']), true);
|
||||
}
|
||||
// 如果是关联数组(不是索引数组),转换为索引数组
|
||||
if (is_array($data['options']) && !isset($data['options'][0])) {
|
||||
$data['options'] = array_values($data['options']);
|
||||
}
|
||||
}
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建题目
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']);
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($data['type']) || empty($data['question']) || empty($data['options'])) {
|
||||
return error('题目类型、题目内容和选项不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证类型
|
||||
if (!in_array($data['type'], ['mbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (!is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
// MBTI类型需要dimension字段
|
||||
if ($data['type'] === 'mbti' && empty($data['dimension'])) {
|
||||
return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400);
|
||||
}
|
||||
|
||||
// 设置超管题库标识(enterpriseId = NULL)
|
||||
$data['enterpriseId'] = null;
|
||||
|
||||
// 设置默认值
|
||||
$data['sort'] = $data['sort'] ?? 0;
|
||||
$data['status'] = $data['status'] ?? 1;
|
||||
|
||||
// 创建题目
|
||||
$question = QuestionModel::create($data);
|
||||
|
||||
return success($question->toArray(), '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新题目
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能更新超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
$data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']);
|
||||
|
||||
// 验证类型
|
||||
if (isset($data['type']) && !in_array($data['type'], ['mbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (isset($data['options']) && !is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
// MBTI类型需要dimension字段
|
||||
if (($data['type'] ?? $question->type) === 'mbti' && empty($data['dimension'] ?? $question->dimension)) {
|
||||
return error('MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400);
|
||||
}
|
||||
|
||||
// 更新题目
|
||||
$question->save($data);
|
||||
|
||||
return success($question->toArray(), '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除题目(软删除)
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能删除超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
// 执行软删除
|
||||
$question->delete();
|
||||
|
||||
return success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入题目
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchImport()
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$questions = Request::param('questions', []);
|
||||
|
||||
if (empty($questions) || !is_array($questions)) {
|
||||
return error('题目数据不能为空', 400);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
$failCount = 0;
|
||||
$errors = [];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($questions as $index => $q) {
|
||||
// 验证必填字段
|
||||
if (empty($q['type']) || empty($q['question']) || empty($q['options'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:题目类型、题目内容和选项不能为空";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 验证类型
|
||||
if (!in_array($q['type'], ['mbti', 'disc', 'pdp'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:题目类型必须是 mbti、disc 或 pdp";
|
||||
continue;
|
||||
}
|
||||
|
||||
// MBTI类型需要dimension字段
|
||||
if ($q['type'] === 'mbti' && empty($q['dimension'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:MBTI类型题目必须指定维度";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 设置超管题库标识
|
||||
$q['enterpriseId'] = null;
|
||||
$q['sort'] = $q['sort'] ?? ($index + 1);
|
||||
$q['status'] = $q['status'] ?? 1;
|
||||
|
||||
QuestionModel::create($q);
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return error('批量导入失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return success([
|
||||
'successCount' => $successCount,
|
||||
'failCount' => $failCount,
|
||||
'errors' => $errors
|
||||
], "成功导入 {$successCount} 题,失败 {$failCount} 题");
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换题目状态
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function toggleStatus($id)
|
||||
{
|
||||
// 验证是否为超级管理员
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', null) // 只能操作超管题库
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在', 404);
|
||||
}
|
||||
|
||||
$question->status = $question->status == 1 ? 0 : 1;
|
||||
$question->save();
|
||||
|
||||
return success($question->toArray(), '状态更新成功');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class Settings extends BaseController
|
||||
$promptsConfig = SystemConfigModel::where('key', 'prompts')->where('enterprise_id', 0)->find();
|
||||
$reportRequiresPaymentConfig = SystemConfigModel::where('key', 'report_requires_payment')->where('enterprise_id', 0)->find();
|
||||
$textConfigModel = SystemConfigModel::where('key', 'text_config')->where('enterprise_id', 0)->find();
|
||||
$reviewModeConfig = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find();
|
||||
|
||||
// 获取当前超管用户名(直接使用JWT中的username)
|
||||
$jwtUsername = $user['username'] ?? null;
|
||||
@@ -59,6 +60,7 @@ class Settings extends BaseController
|
||||
'trialTestCount' => 10,
|
||||
'defaultEnterpriseId' => null,
|
||||
],
|
||||
'reviewMode' => $reviewModeConfig && !empty($reviewModeConfig->value) ? $reviewModeConfig->value : ['enabled' => false],
|
||||
'notification' => $notificationConfig ? $notificationConfig->value : [
|
||||
'emailNotification' => true,
|
||||
'lowBalanceAlert' => true,
|
||||
@@ -385,6 +387,40 @@ class Settings extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新审核模式配置
|
||||
* PUT body: { "enabled": true/false }
|
||||
* 开启后小程序隐藏AI面相分析功能,仅展示问卷测试,用于通过微信审核
|
||||
*/
|
||||
public function updateReviewMode()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || $user['role'] !== 'superadmin') {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$input = json_decode($this->request->getContent(), true);
|
||||
if (!is_array($input)) {
|
||||
$input = Request::param();
|
||||
}
|
||||
$enabled = !empty($input['enabled']);
|
||||
|
||||
try {
|
||||
$config = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find();
|
||||
if (!$config) {
|
||||
$config = new SystemConfigModel();
|
||||
$config->key = 'review_mode';
|
||||
$config->enterprise_id = 0;
|
||||
$config->description = '审核模式:开启后隐藏AI功能以通过微信审核';
|
||||
}
|
||||
$config->value = ['enabled' => $enabled];
|
||||
$config->save();
|
||||
return success($config->value, '审核模式已' . ($enabled ? '开启' : '关闭'));
|
||||
} catch (\Exception $e) {
|
||||
return error('保存失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用字体列表
|
||||
* GET /api/v1/superadmin/settings/fonts
|
||||
|
||||
Reference in New Issue
Block a user