feat: SBTI 测评接入与管理端题目/财务/用户联动
1、修复了用户详情、定价与题目管理、财务与分销等管理端展示与接口问题。 2、新增 SBTI 小程序测评/结果页、sbtiData 引擎、题库 SQL 与脚本;admin sbtiDisplay。 3、优化 AppConfig/CrmReport/Test、历史与个人中心、test-select 与支付相关逻辑。 Made-with: Cursor
This commit is contained in:
@@ -227,6 +227,7 @@ class AppUser extends BaseController
|
||||
$row['mbtiType'] = $this->extractResultType($testsForUser, 'mbti');
|
||||
$row['pdpType'] = $this->extractResultType($testsForUser, 'pdp');
|
||||
$row['discType'] = $this->extractResultType($testsForUser, 'disc');
|
||||
$row['sbtiType'] = $this->extractResultType($testsForUser, 'sbti');
|
||||
$row['faceType'] = $this->extractResultType($testsForUser, 'face');
|
||||
$row['faceMbtiType'] = $this->extractFaceSubType($testsForUser, 'mbti');
|
||||
$row['faceDiscType'] = $this->extractFaceSubType($testsForUser, 'disc');
|
||||
@@ -315,6 +316,7 @@ class AppUser extends BaseController
|
||||
$data['mbtiType'] = $this->extractResultType($tests, 'mbti');
|
||||
$data['pdpType'] = $this->extractResultType($tests, 'pdp');
|
||||
$data['discType'] = $this->extractResultType($tests, 'disc');
|
||||
$data['sbtiType'] = $this->extractResultType($tests, 'sbti');
|
||||
$data['faceType'] = $this->extractResultType($tests, 'face');
|
||||
$data['faceMbtiType'] = $this->extractFaceSubType($tests, 'mbti');
|
||||
$data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc');
|
||||
|
||||
@@ -1,204 +1,204 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\service\WechatService;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 企业财务控制器(企业管理端)
|
||||
*/
|
||||
class Finance extends BaseController
|
||||
{
|
||||
/**
|
||||
* 财务概览
|
||||
*/
|
||||
public function overview()
|
||||
{
|
||||
$enterpriseId = $this->resolveEnterpriseId();
|
||||
if (!$enterpriseId) {
|
||||
return error('未获取到企业信息', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$enterprise = Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->field('id, name, balance')
|
||||
->find();
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$monthStart = strtotime(date('Y-m-01 00:00:00'));
|
||||
|
||||
$baseOrderQuery = Db::name('orders')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->whereIn('status', ['paid', 'completed'])
|
||||
->whereIn('productType', ['face', 'mbti', 'disc', 'pdp']);
|
||||
|
||||
$totalIncomeFen = (int) ((clone $baseOrderQuery)->sum('amount') ?? 0);
|
||||
$todayIncomeFen = (int) ((clone $baseOrderQuery)->where('payTime', '>=', $todayStart)->sum('amount') ?? 0);
|
||||
$monthIncomeFen = (int) ((clone $baseOrderQuery)->where('payTime', '>=', $monthStart)->sum('amount') ?? 0);
|
||||
$paidOrderCount = (int) ((clone $baseOrderQuery)->count());
|
||||
|
||||
$manualRechargeFen = (int) (Db::name('finance_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('type', 'recharge')
|
||||
->whereNull('orderId')
|
||||
->sum('amount') ?? 0);
|
||||
|
||||
$frozenCommissionFen = (int) (Db::name('commission_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('status', 'frozen')
|
||||
->sum('commissionFen') ?? 0);
|
||||
|
||||
return success([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'enterpriseName' => $enterprise['name'] ?? '',
|
||||
'balanceFen' => (int) ($enterprise['balance'] ?? 0),
|
||||
'totalIncomeFen' => $totalIncomeFen,
|
||||
'todayIncomeFen' => $todayIncomeFen,
|
||||
'monthIncomeFen' => $monthIncomeFen,
|
||||
'manualRechargeFen' => $manualRechargeFen,
|
||||
'frozenCommissionFen' => $frozenCommissionFen,
|
||||
'paidOrderCount' => $paidOrderCount,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取企业财务概览失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务流水
|
||||
*/
|
||||
public function records()
|
||||
{
|
||||
$enterpriseId = $this->resolveEnterpriseId();
|
||||
if (!$enterpriseId) {
|
||||
return error('未获取到企业信息', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
|
||||
|
||||
$query = Db::name('finance_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->order('createdAt', 'desc')
|
||||
->order('id', 'desc');
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$list = (clone $query)
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$result = array_map(function ($row) {
|
||||
$type = (string) ($row['type'] ?? '');
|
||||
$orderId = isset($row['orderId']) ? (int) $row['orderId'] : 0;
|
||||
$direction = $type === 'consume' ? 'out' : 'in';
|
||||
$description = (string) ($row['description'] ?? '');
|
||||
$typeLabel = $type === 'consume'
|
||||
? '佣金扣减'
|
||||
: (strpos($description, '企业余额充值') !== false ? '余额充值' : ($orderId > 0 ? '测试收入' : '余额充值'));
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'type' => $type,
|
||||
'typeLabel' => $typeLabel,
|
||||
'direction' => $direction,
|
||||
'amountFen' => (int) ($row['amount'] ?? 0),
|
||||
'balanceBeforeFen' => (int) ($row['balanceBefore'] ?? 0),
|
||||
'balanceAfterFen' => (int) ($row['balanceAfter'] ?? 0),
|
||||
'description' => $description,
|
||||
'orderId' => $orderId ?: null,
|
||||
'createdAt' => (int) ($row['createdAt'] ?? 0),
|
||||
];
|
||||
}, $list);
|
||||
|
||||
return success([
|
||||
'list' => $result,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取财务流水失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业手动充值
|
||||
*/
|
||||
public function rechargeQrcode()
|
||||
{
|
||||
$enterpriseId = $this->resolveEnterpriseId();
|
||||
if (!$enterpriseId) {
|
||||
return error('未获取到企业信息', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$amountFen = (int) Request::param('amountFen', 0);
|
||||
if ($amountFen <= 0) {
|
||||
return error('充值金额必须大于 0', 400);
|
||||
}
|
||||
$enterprise = Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->field('id, name')
|
||||
->find();
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// scene 长度要尽量短,避免超过微信限制
|
||||
$scene = 'eid=' . $enterpriseId . '&a=' . $amountFen . '&r=1';
|
||||
$page = 'pages/recharge/index';
|
||||
$result = WechatService::getWxacodeUnlimited($scene, $page, 430);
|
||||
if (isset($result['errcode'])) {
|
||||
return error('获取充值小程序码失败:' . ($result['errmsg'] ?? ''), 500);
|
||||
}
|
||||
|
||||
$binary = $result['binary'] ?? '';
|
||||
if ($binary === '') {
|
||||
return error('充值小程序码生成失败', 500);
|
||||
}
|
||||
|
||||
return success([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'enterpriseName' => (string) ($enterprise['name'] ?? ''),
|
||||
'amountFen' => $amountFen,
|
||||
'amountYuan' => number_format($amountFen / 100, 2, '.', ''),
|
||||
'scene' => $scene,
|
||||
'page' => $page,
|
||||
'qrcode' => 'data:image/png;base64,' . base64_encode($binary),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('生成充值二维码失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前管理账号所属企业
|
||||
*/
|
||||
protected function resolveEnterpriseId(): ?int
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'] ?? '', ['admin', 'enterprise_admin'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
if ($enterpriseId > 0) {
|
||||
return $enterpriseId;
|
||||
}
|
||||
|
||||
$adminId = (int) ($user['userId'] ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) (Db::name('users')->where('id', $adminId)->value('enterpriseId') ?? 0) ?: null;
|
||||
}
|
||||
}
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\service\WechatService;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 企业财务控制器(企业管理端)
|
||||
*/
|
||||
class Finance extends BaseController
|
||||
{
|
||||
/**
|
||||
* 财务概览
|
||||
*/
|
||||
public function overview()
|
||||
{
|
||||
$enterpriseId = $this->resolveEnterpriseId();
|
||||
if (!$enterpriseId) {
|
||||
return error('未获取到企业信息', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$enterprise = Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->field('id, name, balance')
|
||||
->find();
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$monthStart = strtotime(date('Y-m-01 00:00:00'));
|
||||
|
||||
$baseOrderQuery = Db::name('orders')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->whereIn('status', ['paid', 'completed'])
|
||||
->whereIn('productType', ['face', 'mbti', 'sbti', 'disc', 'pdp']);
|
||||
|
||||
$totalIncomeFen = (int) ((clone $baseOrderQuery)->sum('amount') ?? 0);
|
||||
$todayIncomeFen = (int) ((clone $baseOrderQuery)->where('payTime', '>=', $todayStart)->sum('amount') ?? 0);
|
||||
$monthIncomeFen = (int) ((clone $baseOrderQuery)->where('payTime', '>=', $monthStart)->sum('amount') ?? 0);
|
||||
$paidOrderCount = (int) ((clone $baseOrderQuery)->count());
|
||||
|
||||
$manualRechargeFen = (int) (Db::name('finance_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('type', 'recharge')
|
||||
->whereNull('orderId')
|
||||
->sum('amount') ?? 0);
|
||||
|
||||
$frozenCommissionFen = (int) (Db::name('commission_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('status', 'frozen')
|
||||
->sum('commissionFen') ?? 0);
|
||||
|
||||
return success([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'enterpriseName' => $enterprise['name'] ?? '',
|
||||
'balanceFen' => (int) ($enterprise['balance'] ?? 0),
|
||||
'totalIncomeFen' => $totalIncomeFen,
|
||||
'todayIncomeFen' => $todayIncomeFen,
|
||||
'monthIncomeFen' => $monthIncomeFen,
|
||||
'manualRechargeFen' => $manualRechargeFen,
|
||||
'frozenCommissionFen' => $frozenCommissionFen,
|
||||
'paidOrderCount' => $paidOrderCount,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取企业财务概览失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 财务流水
|
||||
*/
|
||||
public function records()
|
||||
{
|
||||
$enterpriseId = $this->resolveEnterpriseId();
|
||||
if (!$enterpriseId) {
|
||||
return error('未获取到企业信息', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, max(1, (int) Request::param('pageSize', 20)));
|
||||
|
||||
$query = Db::name('finance_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->order('createdAt', 'desc')
|
||||
->order('id', 'desc');
|
||||
|
||||
$total = (int) (clone $query)->count();
|
||||
$list = (clone $query)
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$result = array_map(function ($row) {
|
||||
$type = (string) ($row['type'] ?? '');
|
||||
$orderId = isset($row['orderId']) ? (int) $row['orderId'] : 0;
|
||||
$direction = $type === 'consume' ? 'out' : 'in';
|
||||
$description = (string) ($row['description'] ?? '');
|
||||
$typeLabel = $type === 'consume'
|
||||
? '佣金扣减'
|
||||
: (strpos($description, '企业余额充值') !== false ? '余额充值' : ($orderId > 0 ? '测试收入' : '余额充值'));
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'type' => $type,
|
||||
'typeLabel' => $typeLabel,
|
||||
'direction' => $direction,
|
||||
'amountFen' => (int) ($row['amount'] ?? 0),
|
||||
'balanceBeforeFen' => (int) ($row['balanceBefore'] ?? 0),
|
||||
'balanceAfterFen' => (int) ($row['balanceAfter'] ?? 0),
|
||||
'description' => $description,
|
||||
'orderId' => $orderId ?: null,
|
||||
'createdAt' => (int) ($row['createdAt'] ?? 0),
|
||||
];
|
||||
}, $list);
|
||||
|
||||
return success([
|
||||
'list' => $result,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('获取财务流水失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业手动充值
|
||||
*/
|
||||
public function rechargeQrcode()
|
||||
{
|
||||
$enterpriseId = $this->resolveEnterpriseId();
|
||||
if (!$enterpriseId) {
|
||||
return error('未获取到企业信息', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$amountFen = (int) Request::param('amountFen', 0);
|
||||
if ($amountFen <= 0) {
|
||||
return error('充值金额必须大于 0', 400);
|
||||
}
|
||||
$enterprise = Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->field('id, name')
|
||||
->find();
|
||||
if (!$enterprise) {
|
||||
return error('企业不存在', 404);
|
||||
}
|
||||
|
||||
// scene 长度要尽量短,避免超过微信限制
|
||||
$scene = 'eid=' . $enterpriseId . '&a=' . $amountFen . '&r=1';
|
||||
$page = 'pages/recharge/index';
|
||||
$result = WechatService::getWxacodeUnlimited($scene, $page, 430);
|
||||
if (isset($result['errcode'])) {
|
||||
return error('获取充值小程序码失败:' . ($result['errmsg'] ?? ''), 500);
|
||||
}
|
||||
|
||||
$binary = $result['binary'] ?? '';
|
||||
if ($binary === '') {
|
||||
return error('充值小程序码生成失败', 500);
|
||||
}
|
||||
|
||||
return success([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'enterpriseName' => (string) ($enterprise['name'] ?? ''),
|
||||
'amountFen' => $amountFen,
|
||||
'amountYuan' => number_format($amountFen / 100, 2, '.', ''),
|
||||
'scene' => $scene,
|
||||
'page' => $page,
|
||||
'qrcode' => 'data:image/png;base64,' . base64_encode($binary),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return error('生成充值二维码失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前管理账号所属企业
|
||||
*/
|
||||
protected function resolveEnterpriseId(): ?int
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'] ?? '', ['admin', 'enterprise_admin'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
if ($enterpriseId > 0) {
|
||||
return $enterpriseId;
|
||||
}
|
||||
|
||||
$adminId = (int) ($user['userId'] ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) (Db::name('users')->where('id', $adminId)->value('enterpriseId') ?? 0) ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,185 +1,185 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 定价管理控制器(普通管理员)
|
||||
* 支持同时配置个人版和企业版定价:
|
||||
* - 个人版:type=admin_personal + enterpriseId(企业管理员)或 enterpriseId=NULL(普通管理员)
|
||||
* - 企业版:type=admin_enterprise + enterpriseId(企业管理员)
|
||||
* 无自定义配置时回落到超管全局定价
|
||||
*/
|
||||
class Pricing extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取定价配置(个人版 + 企业版)
|
||||
* GET /api/v1/admin/pricing
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$enterpriseId = $this->resolveEnterpriseId($user);
|
||||
|
||||
// ── 个人版定价 ──
|
||||
$adminPersonalConfig = $this->queryConfig('admin_personal', $enterpriseId);
|
||||
$superPersonalConfig = PricingConfigModel::where('type', 'personal')->whereNull('enterpriseId')->find();
|
||||
$personalConfig = $adminPersonalConfig
|
||||
? $adminPersonalConfig->config
|
||||
: ($superPersonalConfig ? $superPersonalConfig->config : []);
|
||||
$isUsingSuperAdminPersonalConfig = !$adminPersonalConfig;
|
||||
|
||||
// ── 企业版定价 ──
|
||||
$adminEnterpriseConfig = $enterpriseId
|
||||
? $this->queryConfig('admin_enterprise', $enterpriseId)
|
||||
: null;
|
||||
$superEnterpriseConfig = PricingConfigModel::where('type', 'enterprise')->whereNull('enterpriseId')->find();
|
||||
$enterpriseConfig = $adminEnterpriseConfig
|
||||
? $adminEnterpriseConfig->config
|
||||
: ($superEnterpriseConfig ? $superEnterpriseConfig->config : []);
|
||||
$isUsingSuperAdminEnterpriseConfig = !$adminEnterpriseConfig;
|
||||
|
||||
return success([
|
||||
'personal' => $personalConfig,
|
||||
'enterprise' => $enterpriseConfig,
|
||||
'isUsingSuperAdminConfig' => $isUsingSuperAdminPersonalConfig,
|
||||
'isUsingSuperAdminPersonalConfig' => $isUsingSuperAdminPersonalConfig,
|
||||
'isUsingSuperAdminEnterpriseConfig' => $isUsingSuperAdminEnterpriseConfig,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取定价配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定价配置(个人版 + 企业版)
|
||||
* PUT /api/v1/admin/pricing
|
||||
* Body: { personalConfig: {...}, enterpriseConfig: {...} }
|
||||
* 兼容旧格式:{ config: {...} } → 仅更新个人版
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$rawBody = $this->request->getContent();
|
||||
if (empty($rawBody)) {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
}
|
||||
$input = $rawBody ? json_decode($rawBody, true) : null;
|
||||
if (!is_array($input)) {
|
||||
$input = [];
|
||||
}
|
||||
|
||||
// 兼容旧版仅传 config 的情况
|
||||
$personalConfig = $input['personalConfig'] ?? $input['config'] ?? null;
|
||||
$enterpriseConfig = $input['enterpriseConfig'] ?? null;
|
||||
|
||||
if ($personalConfig === null && $enterpriseConfig === null) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$enterpriseId = $this->resolveEnterpriseId($user);
|
||||
|
||||
$result = [];
|
||||
|
||||
// ── 保存个人版定价 ──
|
||||
if ($personalConfig !== null) {
|
||||
if (!is_array($personalConfig)) {
|
||||
return error('个人版定价格式错误', 400);
|
||||
}
|
||||
foreach (['face', 'mbti', 'disc', 'pdp'] as $field) {
|
||||
if (!array_key_exists($field, $personalConfig)) {
|
||||
return error("个人版定价缺少字段:{$field}", 400);
|
||||
}
|
||||
}
|
||||
$cfg = $this->queryConfig('admin_personal', $enterpriseId);
|
||||
if (!$cfg) {
|
||||
$cfg = PricingConfigModel::create([
|
||||
'type' => 'admin_personal',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $personalConfig,
|
||||
]);
|
||||
} else {
|
||||
$cfg->config = $personalConfig;
|
||||
$cfg->save();
|
||||
}
|
||||
$result['personal'] = $cfg->config;
|
||||
}
|
||||
|
||||
// ── 保存企业版定价(仅企业管理员)──
|
||||
if ($enterpriseConfig !== null) {
|
||||
if (!$enterpriseId) {
|
||||
return error('仅企业管理员可设置企业版定价', 403);
|
||||
}
|
||||
if (!is_array($enterpriseConfig)) {
|
||||
return error('企业版定价格式错误', 400);
|
||||
}
|
||||
foreach (['face', 'mbti', 'disc', 'pdp'] as $field) {
|
||||
if (!array_key_exists($field, $enterpriseConfig)) {
|
||||
return error("企业版定价缺少字段:{$field}", 400);
|
||||
}
|
||||
}
|
||||
$cfg = $this->queryConfig('admin_enterprise', $enterpriseId);
|
||||
if (!$cfg) {
|
||||
$cfg = PricingConfigModel::create([
|
||||
'type' => 'admin_enterprise',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $enterpriseConfig,
|
||||
]);
|
||||
} else {
|
||||
$cfg->config = $enterpriseConfig;
|
||||
$cfg->save();
|
||||
}
|
||||
$result['enterprise'] = $cfg->config;
|
||||
}
|
||||
|
||||
return success($result, '定价配置已保存');
|
||||
} catch (\Exception $e) {
|
||||
return error('保存失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JWT 用户信息中解析 enterpriseId
|
||||
*/
|
||||
private function resolveEnterpriseId(array $user): ?int
|
||||
{
|
||||
if (($user['role'] ?? '') !== 'enterprise_admin') {
|
||||
return null;
|
||||
}
|
||||
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
|
||||
$eid = $adminRow['enterpriseId'] ?? null;
|
||||
return $eid ? (int) $eid : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 type + enterpriseId 查询定价配置
|
||||
*/
|
||||
private function queryConfig(string $type, ?int $enterpriseId): ?PricingConfigModel
|
||||
{
|
||||
$q = PricingConfigModel::where('type', $type);
|
||||
if ($enterpriseId) {
|
||||
$q->where('enterpriseId', $enterpriseId);
|
||||
} else {
|
||||
$q->whereNull('enterpriseId');
|
||||
}
|
||||
return $q->find();
|
||||
}
|
||||
}
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 定价管理控制器(普通管理员)
|
||||
* 支持同时配置个人版和企业版定价:
|
||||
* - 个人版:type=admin_personal + enterpriseId(企业管理员)或 enterpriseId=NULL(普通管理员)
|
||||
* - 企业版:type=admin_enterprise + enterpriseId(企业管理员)
|
||||
* 无自定义配置时回落到超管全局定价
|
||||
*/
|
||||
class Pricing extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取定价配置(个人版 + 企业版)
|
||||
* GET /api/v1/admin/pricing
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$enterpriseId = $this->resolveEnterpriseId($user);
|
||||
|
||||
// ── 个人版定价 ──
|
||||
$adminPersonalConfig = $this->queryConfig('admin_personal', $enterpriseId);
|
||||
$superPersonalConfig = PricingConfigModel::where('type', 'personal')->whereNull('enterpriseId')->find();
|
||||
$personalConfig = $adminPersonalConfig
|
||||
? $adminPersonalConfig->config
|
||||
: ($superPersonalConfig ? $superPersonalConfig->config : []);
|
||||
$isUsingSuperAdminPersonalConfig = !$adminPersonalConfig;
|
||||
|
||||
// ── 企业版定价 ──
|
||||
$adminEnterpriseConfig = $enterpriseId
|
||||
? $this->queryConfig('admin_enterprise', $enterpriseId)
|
||||
: null;
|
||||
$superEnterpriseConfig = PricingConfigModel::where('type', 'enterprise')->whereNull('enterpriseId')->find();
|
||||
$enterpriseConfig = $adminEnterpriseConfig
|
||||
? $adminEnterpriseConfig->config
|
||||
: ($superEnterpriseConfig ? $superEnterpriseConfig->config : []);
|
||||
$isUsingSuperAdminEnterpriseConfig = !$adminEnterpriseConfig;
|
||||
|
||||
return success([
|
||||
'personal' => $personalConfig,
|
||||
'enterprise' => $enterpriseConfig,
|
||||
'isUsingSuperAdminConfig' => $isUsingSuperAdminPersonalConfig,
|
||||
'isUsingSuperAdminPersonalConfig' => $isUsingSuperAdminPersonalConfig,
|
||||
'isUsingSuperAdminEnterpriseConfig' => $isUsingSuperAdminEnterpriseConfig,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取定价配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定价配置(个人版 + 企业版)
|
||||
* PUT /api/v1/admin/pricing
|
||||
* Body: { personalConfig: {...}, enterpriseConfig: {...} }
|
||||
* 兼容旧格式:{ config: {...} } → 仅更新个人版
|
||||
*/
|
||||
public function update()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$rawBody = $this->request->getContent();
|
||||
if (empty($rawBody)) {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
}
|
||||
$input = $rawBody ? json_decode($rawBody, true) : null;
|
||||
if (!is_array($input)) {
|
||||
$input = [];
|
||||
}
|
||||
|
||||
// 兼容旧版仅传 config 的情况
|
||||
$personalConfig = $input['personalConfig'] ?? $input['config'] ?? null;
|
||||
$enterpriseConfig = $input['enterpriseConfig'] ?? null;
|
||||
|
||||
if ($personalConfig === null && $enterpriseConfig === null) {
|
||||
return error('配置数据不能为空', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$enterpriseId = $this->resolveEnterpriseId($user);
|
||||
|
||||
$result = [];
|
||||
|
||||
// ── 保存个人版定价 ──
|
||||
if ($personalConfig !== null) {
|
||||
if (!is_array($personalConfig)) {
|
||||
return error('个人版定价格式错误', 400);
|
||||
}
|
||||
foreach (['face', 'mbti', 'disc', 'pdp', 'sbti'] as $field) {
|
||||
if (!array_key_exists($field, $personalConfig)) {
|
||||
return error("个人版定价缺少字段:{$field}", 400);
|
||||
}
|
||||
}
|
||||
$cfg = $this->queryConfig('admin_personal', $enterpriseId);
|
||||
if (!$cfg) {
|
||||
$cfg = PricingConfigModel::create([
|
||||
'type' => 'admin_personal',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $personalConfig,
|
||||
]);
|
||||
} else {
|
||||
$cfg->config = $personalConfig;
|
||||
$cfg->save();
|
||||
}
|
||||
$result['personal'] = $cfg->config;
|
||||
}
|
||||
|
||||
// ── 保存企业版定价(仅企业管理员)──
|
||||
if ($enterpriseConfig !== null) {
|
||||
if (!$enterpriseId) {
|
||||
return error('仅企业管理员可设置企业版定价', 403);
|
||||
}
|
||||
if (!is_array($enterpriseConfig)) {
|
||||
return error('企业版定价格式错误', 400);
|
||||
}
|
||||
foreach (['face', 'mbti', 'disc', 'pdp', 'sbti'] as $field) {
|
||||
if (!array_key_exists($field, $enterpriseConfig)) {
|
||||
return error("企业版定价缺少字段:{$field}", 400);
|
||||
}
|
||||
}
|
||||
$cfg = $this->queryConfig('admin_enterprise', $enterpriseId);
|
||||
if (!$cfg) {
|
||||
$cfg = PricingConfigModel::create([
|
||||
'type' => 'admin_enterprise',
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'config' => $enterpriseConfig,
|
||||
]);
|
||||
} else {
|
||||
$cfg->config = $enterpriseConfig;
|
||||
$cfg->save();
|
||||
}
|
||||
$result['enterprise'] = $cfg->config;
|
||||
}
|
||||
|
||||
return success($result, '定价配置已保存');
|
||||
} catch (\Exception $e) {
|
||||
return error('保存失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JWT 用户信息中解析 enterpriseId
|
||||
*/
|
||||
private function resolveEnterpriseId(array $user): ?int
|
||||
{
|
||||
if (($user['role'] ?? '') !== 'enterprise_admin') {
|
||||
return null;
|
||||
}
|
||||
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
|
||||
$eid = $adminRow['enterpriseId'] ?? null;
|
||||
return $eid ? (int) $eid : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 type + enterpriseId 查询定价配置
|
||||
*/
|
||||
private function queryConfig(string $type, ?int $enterpriseId): ?PricingConfigModel
|
||||
{
|
||||
$q = PricingConfigModel::where('type', $type);
|
||||
if ($enterpriseId) {
|
||||
$q->where('enterpriseId', $enterpriseId);
|
||||
} else {
|
||||
$q->whereNull('enterpriseId');
|
||||
}
|
||||
return $q->find();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,461 +1,459 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Question as QuestionModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 题库管理控制器(企业管理员和普通管理员)
|
||||
* 企业管理员只能管理自己企业的题库,如果没有则使用超管题库
|
||||
*/
|
||||
class Question extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取题库列表
|
||||
* 如果企业没有自己的题库,返回超管题库
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 确定企业ID
|
||||
$enterpriseId = null;
|
||||
if ($user['role'] === 'enterprise_admin') {
|
||||
// 企业管理员:使用自己的企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
} elseif ($user['role'] === 'admin') {
|
||||
// 普通管理员:可以查看所有企业的题库,但优先显示超管题库
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
} else {
|
||||
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 = [];
|
||||
|
||||
// 如果指定了企业ID,优先查询企业题库
|
||||
// 如果没有企业题库,则查询超管题库(enterpriseId = NULL)
|
||||
if ($enterpriseId !== null) {
|
||||
// 先检查企业是否有自己的题库(未指定 type 时需统计 mbti/disc/pdp 三类)
|
||||
$countQuery = QuestionModel::where('enterpriseId', $enterpriseId);
|
||||
if ($type !== '') {
|
||||
$countQuery->where('type', $type);
|
||||
} else {
|
||||
$countQuery->whereIn('type', ['mbti', 'disc', 'pdp']);
|
||||
}
|
||||
$enterpriseQuestionCount = $countQuery->count();
|
||||
|
||||
if ($enterpriseQuestionCount > 0) {
|
||||
// 使用企业题库
|
||||
$where['enterpriseId'] = $enterpriseId;
|
||||
} else {
|
||||
// 使用超管题库
|
||||
$where['enterpriseId'] = null;
|
||||
}
|
||||
} else {
|
||||
// 普通管理员查看超管题库
|
||||
$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();
|
||||
|
||||
// 标识当前使用的是企业题库还是超管题库
|
||||
$isUsingSuperAdminBank = ($where['enterpriseId'] === null);
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'isUsingSuperAdminBank' => $isUsingSuperAdminBank,
|
||||
'enterpriseId' => $enterpriseId
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id)
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 确定企业ID
|
||||
$enterpriseId = null;
|
||||
if ($user['role'] === 'enterprise_admin') {
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
} elseif ($user['role'] === 'admin') {
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
} else {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 先查询企业题库,如果没有则查询超管题库
|
||||
$question = null;
|
||||
if ($enterpriseId !== null) {
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
}
|
||||
|
||||
if (!$question) {
|
||||
$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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以创建题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以创建题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
// 设置企业ID
|
||||
$data['enterpriseId'] = $enterpriseId;
|
||||
|
||||
// 设置默认值
|
||||
$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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以更新题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以更新题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
// 只能更新自己企业的题目
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以删除题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以删除题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
// 只能删除自己企业的题目
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以导入题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以导入题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
// 设置企业ID
|
||||
$q['enterpriseId'] = $enterpriseId;
|
||||
$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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以切换状态
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以切换题目状态', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
// 只能操作自己企业的题目
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在或无权限操作', 404);
|
||||
}
|
||||
|
||||
$question->status = $question->status == 1 ? 0 : 1;
|
||||
$question->save();
|
||||
|
||||
return success($question->toArray(), '状态更新成功');
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\Question as QuestionModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 题库管理控制器(企业管理员和普通管理员)
|
||||
* 企业管理员只能管理自己企业的题库,如果没有则使用超管题库
|
||||
*/
|
||||
class Question extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取题库列表
|
||||
* 如果企业没有自己的题库,返回超管题库
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 确定企业ID
|
||||
$enterpriseId = null;
|
||||
if ($user['role'] === 'enterprise_admin') {
|
||||
// 企业管理员:使用自己的企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
} elseif ($user['role'] === 'admin') {
|
||||
// 普通管理员:可以查看所有企业的题库,但优先显示超管题库
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
} else {
|
||||
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 = [];
|
||||
|
||||
// 如果指定了企业ID,优先查询企业题库
|
||||
// 如果没有企业题库,则查询超管题库(enterpriseId = NULL)
|
||||
if ($enterpriseId !== null) {
|
||||
// 先检查企业是否有自己的题库(未指定 type 时需统计 mbti/disc/pdp 三类)
|
||||
$countQuery = QuestionModel::where('enterpriseId', $enterpriseId);
|
||||
if ($type !== '') {
|
||||
$countQuery->where('type', $type);
|
||||
} else {
|
||||
$countQuery->whereIn('type', ['mbti', 'sbti', 'disc', 'pdp']);
|
||||
}
|
||||
$enterpriseQuestionCount = $countQuery->count();
|
||||
|
||||
if ($enterpriseQuestionCount > 0) {
|
||||
// 使用企业题库
|
||||
$where['enterpriseId'] = $enterpriseId;
|
||||
} else {
|
||||
// 使用超管题库
|
||||
$where['enterpriseId'] = null;
|
||||
}
|
||||
} else {
|
||||
// 普通管理员查看超管题库
|
||||
$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();
|
||||
|
||||
// 标识当前使用的是企业题库还是超管题库
|
||||
$isUsingSuperAdminBank = ($where['enterpriseId'] === null);
|
||||
|
||||
return success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'isUsingSuperAdminBank' => $isUsingSuperAdminBank,
|
||||
'enterpriseId' => $enterpriseId
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id)
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 确定企业ID
|
||||
$enterpriseId = null;
|
||||
if ($user['role'] === 'enterprise_admin') {
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
} elseif ($user['role'] === 'admin') {
|
||||
$enterpriseId = Request::param('enterpriseId', null);
|
||||
} else {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 先查询企业题库,如果没有则查询超管题库
|
||||
$question = null;
|
||||
if ($enterpriseId !== null) {
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
}
|
||||
|
||||
if (!$question) {
|
||||
$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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以创建题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以创建题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
$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', 'sbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、sbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (!is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
if (in_array($data['type'], ['mbti', 'sbti'], true) && empty($data['dimension'])) {
|
||||
return error($data['type'] === 'sbti' ? 'SBTI 题目必须指定维度' : 'MBTI类型题目必须指定维度(EI/SN/TF/JP)', 400);
|
||||
}
|
||||
|
||||
// 设置企业ID
|
||||
$data['enterpriseId'] = $enterpriseId;
|
||||
|
||||
// 设置默认值
|
||||
$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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以更新题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以更新题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
// 只能更新自己企业的题目
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在或无权限修改', 404);
|
||||
}
|
||||
|
||||
$data = Request::only(['type', 'question', 'options', 'dimension', 'sort', 'status']);
|
||||
|
||||
// 验证类型
|
||||
if (isset($data['type']) && !in_array($data['type'], ['mbti', 'sbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、sbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (isset($data['options']) && !is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
$effType = $data['type'] ?? $question->type;
|
||||
if (in_array($effType, ['mbti', 'sbti'], true) && empty($data['dimension'] ?? $question->dimension)) {
|
||||
return error($effType === 'sbti' ? 'SBTI 题目必须指定维度' : '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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以删除题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以删除题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
// 只能删除自己企业的题目
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以导入题目
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以导入题目', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
$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', 'sbti', 'disc', 'pdp'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:题目类型必须是 mbti、sbti、disc 或 pdp";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($q['type'], ['mbti', 'sbti'], true) && empty($q['dimension'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:MBTI/SBTI 题目必须指定维度";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 设置企业ID
|
||||
$q['enterpriseId'] = $enterpriseId;
|
||||
$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) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 只有企业管理员可以切换状态
|
||||
if ($user['role'] !== 'enterprise_admin') {
|
||||
return error('只有企业管理员可以切换题目状态', 403);
|
||||
}
|
||||
|
||||
// 获取企业ID
|
||||
$userModel = Db::name('users')->where('id', $user['userId'])->find();
|
||||
$enterpriseId = $userModel['enterpriseId'] ?? null;
|
||||
|
||||
if (!$enterpriseId) {
|
||||
return error('企业信息不存在', 400);
|
||||
}
|
||||
|
||||
// 只能操作自己企业的题目
|
||||
$question = QuestionModel::where('id', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
|
||||
if (!$question) {
|
||||
return error('题目不存在或无权限操作', 404);
|
||||
}
|
||||
|
||||
$question->status = $question->status == 1 ? 0 : 1;
|
||||
$question->save();
|
||||
|
||||
return success($question->toArray(), '状态更新成功');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,29 @@ trait ExtractsTestResults
|
||||
return $this->coerceResultLabel($dec['pdp'] ?? null);
|
||||
}
|
||||
|
||||
if ($targetType === 'sbti') {
|
||||
$code = $this->coerceResultLabel($dec['sbtiType'] ?? null);
|
||||
if ($code === '' && isset($dec['finalType']) && is_array($dec['finalType'])) {
|
||||
$code = $this->coerceResultLabel($dec['finalType']['code'] ?? null);
|
||||
}
|
||||
$cn = '';
|
||||
if (!empty($dec['sbtiCn']) && is_string($dec['sbtiCn'])) {
|
||||
$cn = trim($dec['sbtiCn']);
|
||||
} elseif (isset($dec['finalType']) && is_array($dec['finalType']) && !empty($dec['finalType']['cn']) && is_string($dec['finalType']['cn'])) {
|
||||
$cn = trim((string) $dec['finalType']['cn']);
|
||||
}
|
||||
if ($code !== '' && $cn !== '') {
|
||||
return $code . '(' . $cn . ')';
|
||||
}
|
||||
if ($code !== '') {
|
||||
return $code;
|
||||
}
|
||||
if ($cn !== '') {
|
||||
return $cn;
|
||||
}
|
||||
return $this->coerceResultLabel($dec['code'] ?? null);
|
||||
}
|
||||
|
||||
$fallback = $this->coerceResultLabel($dec['type'] ?? null);
|
||||
if ($fallback !== '') {
|
||||
return $fallback;
|
||||
|
||||
@@ -103,8 +103,8 @@ class AppConfig extends BaseController
|
||||
}
|
||||
|
||||
// 报告付费开关:完全根据定价配置判断(价格 > 0 视为需要付费)
|
||||
$reportRequiresPayment = ['face' => 0, 'mbti' => 0, 'disc' => 0, 'pdp' => 0];
|
||||
foreach (['face', 'mbti', 'disc', 'pdp'] as $k) {
|
||||
$reportRequiresPayment = ['face' => 0, 'mbti' => 0, 'disc' => 0, 'pdp' => 0, 'sbti' => 0];
|
||||
foreach (['face', 'mbti', 'disc', 'pdp', 'sbti'] as $k) {
|
||||
$key = $k === 'team_analysis' ? 'teamAnalysis' : $k;
|
||||
if (isset($pricing[$key]) && (float) $pricing[$key] > 0) {
|
||||
$reportRequiresPayment[$k] = 1;
|
||||
|
||||
@@ -134,7 +134,7 @@ class CrmReport extends BaseController
|
||||
int $testResultId,
|
||||
int $contextEnterpriseId
|
||||
): ?string {
|
||||
$allowed = ['face', 'mbti', 'pdp', 'disc'];
|
||||
$allowed = ['face', 'mbti', 'sbti', 'pdp', 'disc'];
|
||||
if (!in_array($testType, $allowed, true)) {
|
||||
return null;
|
||||
}
|
||||
@@ -187,7 +187,7 @@ class CrmReport extends BaseController
|
||||
|
||||
return $e !== '' ? $e : $p;
|
||||
}
|
||||
foreach (['face', 'mbti', 'pdp', 'disc'] as $t) {
|
||||
foreach (['face', 'mbti', 'sbti', 'pdp', 'disc'] as $t) {
|
||||
if (!isset($decoded[$t]) || !is_array($decoded[$t])) {
|
||||
continue;
|
||||
}
|
||||
@@ -221,6 +221,7 @@ class CrmReport extends BaseController
|
||||
$map = [
|
||||
'face' => 'AI人脸性格分析',
|
||||
'mbti' => 'MBTI性格测试',
|
||||
'sbti' => 'SBTI人格测试',
|
||||
'pdp' => 'PDP动物性格测试',
|
||||
'disc' => 'DISC行为风格测试',
|
||||
];
|
||||
@@ -275,6 +276,14 @@ class CrmReport extends BaseController
|
||||
$t = $data['mbtiType'] ?? $data['mbti'] ?? '';
|
||||
|
||||
return is_string($t) ? trim($t) : '';
|
||||
case 'sbti':
|
||||
$code = (string) ($data['sbtiType'] ?? $data['finalType']['code'] ?? '');
|
||||
$cn = (string) ($data['sbtiCn'] ?? $data['finalType']['cn'] ?? '');
|
||||
if ($code === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $cn !== '' ? $code . '(' . $cn . ')' : $code;
|
||||
case 'face':
|
||||
$fa = $data['faceAnalysis'] ?? '';
|
||||
if (is_string($fa) && $fa !== '') {
|
||||
@@ -350,7 +359,7 @@ class CrmReport extends BaseController
|
||||
string $testScope = 'personal'
|
||||
): void {
|
||||
try {
|
||||
$allowed = ['face', 'mbti', 'pdp', 'disc'];
|
||||
$allowed = ['face', 'mbti', 'sbti', 'pdp', 'disc'];
|
||||
if (!in_array($testType, $allowed, true) || $userId <= 0 || $testResultId <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -429,7 +438,7 @@ class CrmReport extends BaseController
|
||||
|
||||
return in_array($t, ['after_paid', 'after_test'], true) ? $t : '';
|
||||
}
|
||||
foreach (['face', 'mbti', 'pdp', 'disc'] as $tKey) {
|
||||
foreach (['face', 'mbti', 'sbti', 'pdp', 'disc'] as $tKey) {
|
||||
if (!isset($decoded[$tKey]['reportTiming'])) {
|
||||
continue;
|
||||
}
|
||||
@@ -438,7 +447,7 @@ class CrmReport extends BaseController
|
||||
return 'after_test';
|
||||
}
|
||||
}
|
||||
foreach (['face', 'mbti', 'pdp', 'disc'] as $tKey) {
|
||||
foreach (['face', 'mbti', 'sbti', 'pdp', 'disc'] as $tKey) {
|
||||
if (!isset($decoded[$tKey]['reportTiming'])) {
|
||||
continue;
|
||||
}
|
||||
@@ -515,7 +524,7 @@ class CrmReport extends BaseController
|
||||
// 一次查出所有相关类型的最新记录(按时间倒序)
|
||||
$rows = Db::name('test_results')
|
||||
->where('userId', $userId)
|
||||
->whereIn('testType', ['mbti', 'disc', 'pdp'])
|
||||
->whereIn('testType', ['mbti', 'sbti', 'disc', 'pdp'])
|
||||
->field('testType, resultData, createdAt')
|
||||
->order('createdAt', 'desc')
|
||||
->select()
|
||||
@@ -545,6 +554,12 @@ class CrmReport extends BaseController
|
||||
$val = $data['description']['type'] ?? $data['pdp'] ?? '';
|
||||
if ($val !== '') $found['pdp'] = (string) $val;
|
||||
break;
|
||||
case 'sbti':
|
||||
$val = $data['sbtiType'] ?? $data['finalType']['code'] ?? '';
|
||||
if ($val !== '') {
|
||||
$found['sbti'] = (string) $val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -867,7 +867,9 @@ class Distribution extends BaseController
|
||||
|
||||
$inviteeId = (int) $order['userId'];
|
||||
$orderAmount = (int) $order['amount'];
|
||||
$enterpriseId = !empty($order['enterpriseId']) ? (int) $order['enterpriseId'] : null;
|
||||
// 订单所属企业(资金应从该账户扣,不因绑定回退 personal 而丢失)
|
||||
$orderEnterpriseId = !empty($order['enterpriseId']) ? (int) $order['enterpriseId'] : null;
|
||||
$enterpriseId = $orderEnterpriseId;
|
||||
$scope = $enterpriseId ? 'enterprise' : 'personal';
|
||||
$now = time();
|
||||
|
||||
@@ -887,7 +889,7 @@ class Distribution extends BaseController
|
||||
->find();
|
||||
|
||||
// 【第二步】回退匹配:精确未命中时,查找任意有效的 personal 绑定
|
||||
// 跨 scope 场景(如企业版订单但推荐人仅持有 personal 绑定),以个人版配置+平台资金结算
|
||||
// 跨 scope 时仍用 personal 佣金配置,但资金仍从订单 enterpriseId 扣(见 distribution-design 2.2)
|
||||
$fallbackScope = $scope;
|
||||
$fallbackEnterpriseId = $enterpriseId;
|
||||
if (!$binding && $scope === 'enterprise') {
|
||||
@@ -902,9 +904,12 @@ class Distribution extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
// 实际用于结算的 scope/enterpriseId(可能已回退为 personal)
|
||||
// 实际用于结算的 scope/绑定侧 enterprise(可能已回退为 personal 且为 null)
|
||||
$scope = $fallbackScope;
|
||||
$enterpriseId = $fallbackEnterpriseId;
|
||||
// 扣款企业:始终为订单上的企业;佣金配置优先用订单企业读 testSettings
|
||||
$billingEnterpriseId = $orderEnterpriseId;
|
||||
$configEnterpriseId = $orderEnterpriseId !== null ? $orderEnterpriseId : $enterpriseId;
|
||||
|
||||
$inviterId = (int) $binding['inviterId'];
|
||||
|
||||
@@ -916,7 +921,7 @@ class Distribution extends BaseController
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
// 读取佣金配置(优先 per-test testSettings,回退全局)
|
||||
list($rate, $amountFen) = self::getTestCommissionConfig($testType, $scope, $enterpriseId);
|
||||
list($rate, $amountFen) = self::getTestCommissionConfig($testType, $scope, $configEnterpriseId);
|
||||
$commissionFen = 0;
|
||||
if ($amountFen > 0) {
|
||||
$commissionFen = $amountFen;
|
||||
@@ -940,10 +945,10 @@ class Distribution extends BaseController
|
||||
try {
|
||||
$commissionStatus = 'pending';
|
||||
|
||||
if ($enterpriseId) {
|
||||
// 企业上下文:优先从企业余额扣款,余额不足则冻结
|
||||
if ($billingEnterpriseId) {
|
||||
// 企业订单:从订单所属企业余额扣款,余额不足则冻结
|
||||
$enterprise = Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->where('id', $billingEnterpriseId)
|
||||
->field('id, balance')
|
||||
->lock(true)
|
||||
->find();
|
||||
@@ -954,14 +959,14 @@ class Distribution extends BaseController
|
||||
// 余额充足,直接结算
|
||||
$newBalanceFen = $balanceFen - $commissionFen;
|
||||
Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->where('id', $billingEnterpriseId)
|
||||
->update([
|
||||
'balance' => $newBalanceFen,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
Db::name('finance_records')->insert([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'enterpriseId' => $billingEnterpriseId,
|
||||
'type' => 'consume',
|
||||
'amount' => $commissionFen,
|
||||
'balanceBefore' => $balanceFen,
|
||||
@@ -1004,7 +1009,7 @@ class Distribution extends BaseController
|
||||
'agentId' => $inviterId,
|
||||
'orderId' => $orderId,
|
||||
'scope' => $scope,
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'enterpriseId' => $billingEnterpriseId,
|
||||
'inviterId' => $inviterId,
|
||||
'inviteeId' => $inviteeId,
|
||||
'bindingId' => (int) $binding['id'],
|
||||
@@ -1300,6 +1305,7 @@ class Distribution extends BaseController
|
||||
$map = [
|
||||
'face' => '人脸',
|
||||
'mbti' => 'MBTI',
|
||||
'sbti' => 'SBTI',
|
||||
'disc' => 'DISC',
|
||||
'pdp' => 'PDP',
|
||||
];
|
||||
@@ -1330,7 +1336,7 @@ class Distribution extends BaseController
|
||||
public static function settleTestCommission(int $testResultId, int $inviteeId, string $testType): void
|
||||
{
|
||||
// 仅支持指定测试类型
|
||||
$allowedTypes = ['face', 'mbti', 'disc', 'pdp'];
|
||||
$allowedTypes = ['face', 'mbti', 'sbti', 'disc', 'pdp'];
|
||||
// face/ai 统一归类为 face
|
||||
$normalizedType = ($testType === 'ai') ? 'face' : $testType;
|
||||
if (!in_array($normalizedType, $allowedTypes, true)) {
|
||||
|
||||
@@ -68,6 +68,7 @@ class Payment extends BaseController
|
||||
$testTypeMap = [
|
||||
'face' => 'face',
|
||||
'mbti' => 'mbti',
|
||||
'sbti' => 'sbti',
|
||||
'disc' => 'disc',
|
||||
'pdp' => 'pdp',
|
||||
'resume' => 'resume',
|
||||
@@ -191,6 +192,7 @@ class Payment extends BaseController
|
||||
$testTypeMap = [
|
||||
'face' => 'face',
|
||||
'mbti' => 'mbti',
|
||||
'sbti' => 'sbti',
|
||||
'disc' => 'disc',
|
||||
'pdp' => 'pdp',
|
||||
'resume' => 'resume',
|
||||
@@ -571,7 +573,7 @@ class Payment extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($productType, ['face', 'mbti', 'disc', 'pdp', 'resume', 'recharge'], true)) {
|
||||
if (!in_array($productType, ['face', 'mbti', 'sbti', 'disc', 'pdp', 'resume', 'recharge'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -665,7 +667,7 @@ class Payment extends BaseController
|
||||
$quantity = $quantity > 0 ? $quantity : 1;
|
||||
|
||||
// 1)测试类产品:定价配置中为元,转为分(企业用户按企业ID取价)
|
||||
$testProductTypes = ['face', 'mbti', 'disc', 'pdp', 'resume', 'report', 'team_analysis'];
|
||||
$testProductTypes = ['face', 'mbti', 'sbti', 'disc', 'pdp', 'resume', 'report', 'team_analysis'];
|
||||
if (in_array($productType, $testProductTypes, true)) {
|
||||
$pricingConfig = PricingConfigModel::getByTypeAndEnterprise($pricingType, $pricingEnterpriseId ?? $enterpriseId);
|
||||
$config = [];
|
||||
|
||||
@@ -32,7 +32,7 @@ class Test extends BaseController
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
$type = Request::param('type', 'all'); // all|mbti|disc|pdp|face|ai|resume
|
||||
$type = Request::param('type', 'all'); // all|mbti|sbti|disc|pdp|face|ai|resume
|
||||
$scope = Request::param('scope', 'all'); // all|personal|enterprise
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = (int) Request::param('pageSize', 0);
|
||||
@@ -186,6 +186,27 @@ class Test extends BaseController
|
||||
'data' => null,
|
||||
], $paymentFields);
|
||||
break;
|
||||
case 'sbti':
|
||||
$sbtiTxt = '未知';
|
||||
if (is_array($data)) {
|
||||
$sbtiTxt = (string) ($data['sbtiType'] ?? $data['finalType']['code'] ?? $data['code'] ?? '未知');
|
||||
if (($data['sbtiCn'] ?? '') !== '') {
|
||||
$sbtiTxt .= '(' . $data['sbtiCn'] . ')';
|
||||
} elseif (isset($data['finalType']['cn']) && $data['finalType']['cn'] !== '') {
|
||||
$sbtiTxt .= '(' . $data['finalType']['cn'] . ')';
|
||||
}
|
||||
}
|
||||
$list[] = array_merge([
|
||||
'id' => $id,
|
||||
'type' => 'sbti',
|
||||
'key' => 'sbti_' . $id,
|
||||
'emoji' => '🎭',
|
||||
'typeName' => 'SBTI 人格测试',
|
||||
'resultText'=> $sbtiTxt,
|
||||
'testTime' => $timeLabel,
|
||||
'data' => null,
|
||||
], $paymentFields);
|
||||
break;
|
||||
case 'face':
|
||||
case 'ai':
|
||||
$mbtiShort = '';
|
||||
@@ -261,7 +282,7 @@ class Test extends BaseController
|
||||
|
||||
$allowedAll = $this->wechatAllowedTestTypes($userId);
|
||||
// 「我的」卡片不含简历,但 totalCount 与列表需与 history 权限一致
|
||||
$allowedForRecent = array_values(array_intersect($allowedAll, ['mbti', 'pdp', 'disc', 'face', 'ai']));
|
||||
$allowedForRecent = array_values(array_intersect($allowedAll, ['mbti', 'sbti', 'pdp', 'disc', 'face', 'ai']));
|
||||
if ($allowedForRecent === []) {
|
||||
return success([
|
||||
'records' => new \stdClass(),
|
||||
@@ -291,7 +312,7 @@ class Test extends BaseController
|
||||
// face 和 ai 视为同一种类型
|
||||
$effectiveType = in_array($type, ['face', 'ai']) ? 'ai' : $type;
|
||||
|
||||
if (!isset($foundTypes[$effectiveType]) && in_array($effectiveType, ['mbti', 'disc', 'pdp', 'ai'])) {
|
||||
if (!isset($foundTypes[$effectiveType]) && in_array($effectiveType, ['mbti', 'sbti', 'disc', 'pdp', 'ai'])) {
|
||||
$records[$effectiveType] = $this->_formatRecentRow($row);
|
||||
$foundTypes[$effectiveType] = true;
|
||||
}
|
||||
@@ -342,6 +363,9 @@ class Test extends BaseController
|
||||
if (!empty($enterprisePerms['disc'])) {
|
||||
$allowed[] = 'disc';
|
||||
}
|
||||
if (!empty($enterprisePerms['sbti'])) {
|
||||
$allowed[] = 'sbti';
|
||||
}
|
||||
if (!empty($enterprisePerms['face']) && !$reviewMode) {
|
||||
$allowed[] = 'face';
|
||||
$allowed[] = 'ai';
|
||||
@@ -437,6 +461,16 @@ class Test extends BaseController
|
||||
$emoji = $data['description']['emoji'] ?? '🦁';
|
||||
$typeName = 'PDP行为';
|
||||
break;
|
||||
case 'sbti':
|
||||
$resultText = (string) ($data['sbtiType'] ?? $data['finalType']['code'] ?? '未知');
|
||||
if (!empty($data['sbtiCn'])) {
|
||||
$resultText .= '(' . $data['sbtiCn'] . ')';
|
||||
} elseif (!empty($data['finalType']['cn'])) {
|
||||
$resultText .= '(' . $data['finalType']['cn'] . ')';
|
||||
}
|
||||
$emoji = '🎭';
|
||||
$typeName = 'SBTI';
|
||||
break;
|
||||
case 'face':
|
||||
case 'ai':
|
||||
$mbtiShort = '';
|
||||
@@ -481,6 +515,12 @@ class Test extends BaseController
|
||||
'description' => $data['description'] ?? null,
|
||||
'pdp' => $data['pdp'] ?? null,
|
||||
];
|
||||
} elseif ($testType === 'sbti') {
|
||||
$resultMeta = [
|
||||
'sbtiType' => $data['sbtiType'] ?? null,
|
||||
'sbtiCn' => $data['sbtiCn'] ?? null,
|
||||
'levels' => $data['levels'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
$out = [
|
||||
@@ -609,7 +649,7 @@ class Test extends BaseController
|
||||
}
|
||||
|
||||
// 仅允许已知类型,避免脏数据
|
||||
if (!in_array($testType, ['mbti', 'disc', 'pdp', 'face', 'ai'], true)) {
|
||||
if (!in_array($testType, ['mbti', 'sbti', 'disc', 'pdp', 'face', 'ai'], true)) {
|
||||
return error('不支持的测试类型', 400);
|
||||
}
|
||||
|
||||
@@ -796,6 +836,13 @@ class Test extends BaseController
|
||||
'locked' => true,
|
||||
];
|
||||
}
|
||||
if ($testType === 'sbti') {
|
||||
return [
|
||||
'sbtiType' => $data['sbtiType'] ?? $data['finalType']['code'] ?? '',
|
||||
'sbtiCn' => $data['sbtiCn'] ?? $data['finalType']['cn'] ?? '',
|
||||
'locked' => true,
|
||||
];
|
||||
}
|
||||
if ($testType === 'resume') {
|
||||
$preview = '';
|
||||
if (!empty($data['content']) && is_string($data['content'])) {
|
||||
@@ -948,7 +995,7 @@ class Test extends BaseController
|
||||
|
||||
/**
|
||||
* 小程序拉取做题题库(仅启用题):企业本题库有题则用企业,否则用超管 enterpriseId 为空
|
||||
* GET /api/test/questions?type=mbti|disc|pdp&enterpriseId=可选
|
||||
* GET /api/test/questions?type=mbti|sbti|disc|pdp&enterpriseId=可选
|
||||
*/
|
||||
public function questions()
|
||||
{
|
||||
@@ -958,8 +1005,8 @@ class Test extends BaseController
|
||||
}
|
||||
|
||||
$type = (string) Request::param('type', '');
|
||||
if (!in_array($type, ['mbti', 'disc', 'pdp'], true)) {
|
||||
return error('type 须为 mbti、disc 或 pdp', 400);
|
||||
if (!in_array($type, ['mbti', 'sbti', 'disc', 'pdp'], true)) {
|
||||
return error('type 须为 mbti、sbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
$rawEid = Request::param('enterpriseId', null);
|
||||
@@ -1018,7 +1065,8 @@ class Test extends BaseController
|
||||
}
|
||||
}
|
||||
unset($opt);
|
||||
if ($type !== 'mbti') {
|
||||
// MBTI / SBTI 前端组卷与计分依赖 dimension,其余题型不对外返回该字段
|
||||
if (!in_array($type, ['mbti', 'sbti'], true)) {
|
||||
unset($item['dimension']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,6 +477,7 @@ class AppUser extends BaseController
|
||||
$row['mbtiType'] = $this->extractResultType($testsForUser, 'mbti');
|
||||
$row['pdpType'] = $this->extractResultType($testsForUser, 'pdp');
|
||||
$row['discType'] = $this->extractResultType($testsForUser, 'disc');
|
||||
$row['sbtiType'] = $this->extractResultType($testsForUser, 'sbti');
|
||||
$row['faceType'] = $this->extractResultType($testsForUser, 'face');
|
||||
$row['faceMbtiType'] = $this->extractFaceSubType($testsForUser, 'mbti');
|
||||
$row['faceDiscType'] = $this->extractFaceSubType($testsForUser, 'disc');
|
||||
@@ -547,6 +548,7 @@ class AppUser extends BaseController
|
||||
$data['mbtiType'] = $this->extractResultType($tests, 'mbti');
|
||||
$data['pdpType'] = $this->extractResultType($tests, 'pdp');
|
||||
$data['discType'] = $this->extractResultType($tests, 'disc');
|
||||
$data['sbtiType'] = $this->extractResultType($tests, 'sbti');
|
||||
$data['faceType'] = $this->extractResultType($tests, 'face');
|
||||
$data['faceMbtiType'] = $this->extractFaceSubType($tests, 'mbti');
|
||||
$data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc');
|
||||
|
||||
@@ -1,332 +1,331 @@
|
||||
<?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', 'sbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、sbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (!is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
// MBTI / SBTI 需 dimension
|
||||
if (in_array($data['type'], ['mbti', 'sbti'], true) && empty($data['dimension'])) {
|
||||
return error($data['type'] === 'sbti' ? 'SBTI 题目必须指定维度(如 S1、DG1)' : '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', 'sbti', 'disc', 'pdp'])) {
|
||||
return error('题目类型必须是 mbti、sbti、disc 或 pdp', 400);
|
||||
}
|
||||
|
||||
// 验证选项格式
|
||||
if (isset($data['options']) && !is_array($data['options'])) {
|
||||
return error('选项必须是数组格式', 400);
|
||||
}
|
||||
|
||||
$effType = $data['type'] ?? $question->type;
|
||||
if (in_array($effType, ['mbti', 'sbti'], true) && empty($data['dimension'] ?? $question->dimension)) {
|
||||
return error($effType === 'sbti' ? 'SBTI 题目必须指定维度' : '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', 'sbti', 'disc', 'pdp'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:题目类型必须是 mbti、sbti、disc 或 pdp";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($q['type'], ['mbti', 'sbti'], true) && empty($q['dimension'])) {
|
||||
$failCount++;
|
||||
$errors[] = "第" . ($index + 1) . "题:MBTI/SBTI 题目必须指定维度";
|
||||
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(), '状态更新成功');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user