feat: SBTI 测评接入与管理端题目/财务/用户联动

1、修复了用户详情、定价与题目管理、财务与分销等管理端展示与接口问题。

2、新增 SBTI 小程序测评/结果页、sbtiData 引擎、题库 SQL 与脚本;admin sbtiDisplay。

3、优化 AppConfig/CrmReport/Test、历史与个人中心、test-select 与支付相关逻辑。

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-11 16:16:32 +08:00
parent 1a66e77ab7
commit 6fec2965aa
50 changed files with 4189 additions and 1259 deletions

View File

@@ -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');

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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(), '状态更新成功');
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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)) {

View File

@@ -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 = [];

View File

@@ -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']);
}
}

View File

@@ -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');

View File

@@ -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(), '状态更新成功');
}
}

View File

@@ -58,6 +58,7 @@ class Enterprise extends Model
return [
'face' => true,
'mbti' => true,
'sbti' => true,
'pdp' => true,
'disc' => true,
'distribution' => true,