chore: 首次提交 - 关联 GitHub fnvtk/MBTI_wang
Made-with: Cursor
This commit is contained in:
358
api/app/controller/admin/AppUser.php
Normal file
358
api/app/controller/admin/AppUser.php
Normal file
@@ -0,0 +1,358 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 测试用户(小程序用户)管理 - 只读列表与详情
|
||||
* 数据来源:wechat_users,测试记录来自 test_results(userId 关联 wechat_users.id)
|
||||
*/
|
||||
class AppUser extends BaseController
|
||||
{
|
||||
/**
|
||||
* 测试用户列表:分页、关键词搜索
|
||||
* GET /api/v1/admin/app-users?page=1&pageSize=20&keyword=
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = (int) Request::param('page', 1);
|
||||
$pageSize = (int) Request::param('pageSize', 20);
|
||||
$pageSize = min(max($pageSize, 1), 100);
|
||||
$keyword = trim(Request::param('keyword', ''));
|
||||
|
||||
$where = [];
|
||||
if ($keyword !== '') {
|
||||
$where[] = ['nickname|phone|city|province', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
// admin / enterprise_admin 均只能看本企业数据
|
||||
$enterpriseId = $user['enterpriseId'] ?? null;
|
||||
if (!$enterpriseId) {
|
||||
// JWT 未含 enterpriseId 时回退查库(兼容旧 token)
|
||||
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
|
||||
$enterpriseId = $adminRow['enterpriseId'] ?? null;
|
||||
}
|
||||
|
||||
// 若有企业ID:先从 user_profile 中取出属于本企业的 userId 列表(以画像为主表)
|
||||
$profileUserIds = [];
|
||||
if ($enterpriseId) {
|
||||
$profileUserIds = Db::name('user_profile')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->column('userId');
|
||||
$profileUserIds = $profileUserIds ? array_values(array_unique(array_filter($profileUserIds))) : [];
|
||||
if (empty($profileUserIds)) {
|
||||
return paginate_response([], 0, $page, $pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 按 openid 去重:每个 openid 只保留 id 最大的一条;失败则不去重
|
||||
try {
|
||||
$dedupIds = Db::name('wechat_users')->field('openid, MAX(id) as mid')->group('openid')->column('mid');
|
||||
$dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : [];
|
||||
} catch (\Throwable $e) {
|
||||
$dedupIds = Db::name('wechat_users')->column('id');
|
||||
$dedupIds = $dedupIds ? array_values(array_filter($dedupIds)) : [];
|
||||
}
|
||||
if (empty($dedupIds)) {
|
||||
return paginate_response([], 0, $page, $pageSize);
|
||||
}
|
||||
|
||||
$baseQuery = Db::name('wechat_users')->where('id', 'in', $dedupIds);
|
||||
// 若从画像表中筛出了当前企业的用户池,则仅保留这些 userId
|
||||
if (!empty($profileUserIds)) {
|
||||
$baseQuery->whereIn('id', $profileUserIds);
|
||||
}
|
||||
if ($where) {
|
||||
$baseQuery->where($where);
|
||||
}
|
||||
|
||||
$total = (int) $baseQuery->count();
|
||||
$list = (clone $baseQuery)
|
||||
->field('id,nickname,openid,avatar,phone,gender,country,province,city,status,lastLoginAt,createdAt')
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 为每条用户附加测试统计(test_results.userId 对应 wechat_users.id)
|
||||
$ids = array_column($list, 'id');
|
||||
$testCounts = [];
|
||||
$lastTestAt = [];
|
||||
$testTypes = []; // 每个用户最新几条测试类型,用于展示 MBTI/PDP/DISC
|
||||
$payStats = [];
|
||||
$enterpriseName = null;
|
||||
if ($enterpriseId) {
|
||||
$ent = Db::name('enterprises')->where('id', $enterpriseId)->find();
|
||||
$enterpriseName = $ent['name'] ?? ('企业' . $enterpriseId);
|
||||
}
|
||||
if (!empty($ids)) {
|
||||
// 测试统计严格按 test_results.enterpriseId 归属企业过滤
|
||||
$trBase = Db::name('test_results')->where('userId', 'in', $ids);
|
||||
if ($enterpriseId) {
|
||||
$trBase->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
$counts = (clone $trBase)
|
||||
->group('userId')
|
||||
->column('COUNT(*) as cnt', 'userId');
|
||||
$testCounts = $counts ?: [];
|
||||
|
||||
$lastRows = (clone $trBase)
|
||||
->field('id, userId, testType, resultData, createdAt, enterpriseId as testEnterpriseId')
|
||||
->order('createdAt', 'desc')
|
||||
->select();
|
||||
foreach ($lastRows as $row) {
|
||||
$uid = $row['userId'];
|
||||
if (!isset($lastTestAt[$uid])) {
|
||||
$lastTestAt[$uid] = $row['createdAt'];
|
||||
}
|
||||
if (!isset($testTypes[$uid])) {
|
||||
$testTypes[$uid] = [];
|
||||
}
|
||||
$testTypes[$uid][] = [
|
||||
'testType' => $row['testType'],
|
||||
'result' => is_string($row['resultData'] ?? '') ? ($row['resultData'] ?? '') : json_encode($row['resultData'] ?? '', JSON_UNESCAPED_UNICODE),
|
||||
'createdAt' => $row['createdAt'],
|
||||
'testScope' => !empty($row['testEnterpriseId']) ? 'enterprise' : 'personal',
|
||||
];
|
||||
}
|
||||
// 付款统计:user_profile(按当前企业过滤)
|
||||
try {
|
||||
$profilesQuery = Db::name('user_profile')
|
||||
->where('userId', 'in', $ids);
|
||||
if ($enterpriseId) {
|
||||
$profilesQuery->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
$profiles = $profilesQuery
|
||||
->field('userId, SUM(paidOrders) AS paidOrders, SUM(totalPaidAmount) AS totalPaidAmount')
|
||||
->group('userId')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($profiles as $p) {
|
||||
$uid = (int) ($p['userId'] ?? 0);
|
||||
if ($uid > 0) {
|
||||
$payStats[$uid] = [
|
||||
'paidOrders' => (int) ($p['paidOrders'] ?? 0),
|
||||
'totalPaidAmount' => (int) ($p['totalPaidAmount'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$payStats = [];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$id = $row['id'];
|
||||
$testsForUser = $testTypes[$id] ?? [];
|
||||
$row['username'] = $row['nickname'] ?? ('用户' . $id);
|
||||
$row['testCount'] = (int) ($testCounts[$id] ?? 0);
|
||||
$row['lastTestAt'] = $lastTestAt[$id] ?? null;
|
||||
$row['tests'] = $testsForUser;
|
||||
$row['mbtiType'] = $this->extractResultType($testsForUser, 'mbti');
|
||||
$row['pdpType'] = $this->extractResultType($testsForUser, 'pdp');
|
||||
$row['discType'] = $this->extractResultType($testsForUser, 'disc');
|
||||
$row['faceType'] = $this->extractResultType($testsForUser, 'face');
|
||||
$row['faceMbtiType'] = $this->extractFaceSubType($testsForUser, 'mbti');
|
||||
$row['faceDiscType'] = $this->extractFaceSubType($testsForUser, 'disc');
|
||||
$row['facePdpType'] = $this->extractFaceSubType($testsForUser, 'pdp');
|
||||
$row['enterprise'] = $enterpriseName !== null ? $enterpriseName : '全部';
|
||||
$pay = $payStats[$id] ?? null;
|
||||
$row['paidOrders'] = $pay ? (int) ($pay['paidOrders'] ?? 0) : 0;
|
||||
$row['totalPaidAmount'] = $pay ? (int) ($pay['totalPaidAmount'] ?? 0) : 0;
|
||||
}
|
||||
|
||||
return paginate_response($list, $total, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用户详情:基本信息 + 测试记录列表
|
||||
* GET /api/v1/admin/app-users/:id
|
||||
*/
|
||||
public function detail($id)
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// admin / enterprise_admin 均只能查看本企业的用户
|
||||
$enterpriseId = $user['enterpriseId'] ?? null;
|
||||
if (!$enterpriseId) {
|
||||
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
|
||||
$enterpriseId = $adminRow['enterpriseId'] ?? null;
|
||||
}
|
||||
if ($enterpriseId) {
|
||||
// 使用 user_profile 判断该用户是否属于当前企业(以画像为主表)
|
||||
$has = Db::name('user_profile')
|
||||
->where('userId', $id)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->find();
|
||||
if (!$has) {
|
||||
return error('无权限查看该用户', 403);
|
||||
}
|
||||
}
|
||||
|
||||
$row = Db::name('wechat_users')->where('id', $id)->find();
|
||||
if (!$row) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'id' => (int) $row['id'],
|
||||
'username' => $row['nickname'] ?? ('用户' . $row['id']),
|
||||
'nickname' => $row['nickname'] ?? '',
|
||||
'avatar' => $row['avatar'] ?? '',
|
||||
'phone' => $row['phone'] ?? '',
|
||||
'email' => '',
|
||||
'gender' => (int) ($row['gender'] ?? 0),
|
||||
'country' => $row['country'] ?? '',
|
||||
'province' => $row['province'] ?? '',
|
||||
'city' => $row['city'] ?? '',
|
||||
'status' => (int) ($row['status'] ?? 1),
|
||||
'lastLoginAt' => isset($row['lastLoginAt']) ? (int) $row['lastLoginAt'] : null,
|
||||
'createdAt' => isset($row['createdAt']) ? (int) $row['createdAt'] : null,
|
||||
'updatedAt' => isset($row['updatedAt']) ? (int) $row['updatedAt'] : null,
|
||||
];
|
||||
|
||||
// 测试列表:严格按 test_results.enterpriseId 归属本企业过滤
|
||||
$testQuery = Db::name('test_results')->where('userId', $id);
|
||||
if ($enterpriseId) {
|
||||
$testQuery->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
$tests = $testQuery
|
||||
->field('id, testType, resultData, enterpriseId as testEnterpriseId, createdAt, requiresPayment, isPaid, paidAmount, paidAt, orderId')
|
||||
->order('createdAt', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($tests as &$t) {
|
||||
$raw = $t['resultData'] ?? '';
|
||||
$t['result'] = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE);
|
||||
$t['testScope'] = !empty($t['testEnterpriseId']) ? 'enterprise' : 'personal';
|
||||
unset($t['testEnterpriseId']);
|
||||
}
|
||||
|
||||
$data['testCount'] = count($tests);
|
||||
$data['testList'] = $tests;
|
||||
$data['mbtiType'] = $this->extractResultType($tests, 'mbti');
|
||||
$data['pdpType'] = $this->extractResultType($tests, 'pdp');
|
||||
$data['discType'] = $this->extractResultType($tests, 'disc');
|
||||
$data['faceType'] = $this->extractResultType($tests, 'face');
|
||||
$data['faceMbtiType'] = $this->extractFaceSubType($tests, 'mbti');
|
||||
$data['faceDiscType'] = $this->extractFaceSubType($tests, 'disc');
|
||||
$data['facePdpType'] = $this->extractFaceSubType($tests, 'pdp');
|
||||
|
||||
return success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从测试记录中取出某类型的最近结果(result 可能是 JSON 字符串,取 type 或 result 字段)
|
||||
*/
|
||||
private function extractResultType(array $tests, string $type): string
|
||||
{
|
||||
$targetType = strtolower($type);
|
||||
foreach ($tests as $t) {
|
||||
if (strtolower($t['testType'] ?? '') !== $targetType) {
|
||||
continue;
|
||||
}
|
||||
$result = $t['result'] ?? '';
|
||||
if (!is_string($result)) {
|
||||
continue;
|
||||
}
|
||||
$dec = json_decode($result, true);
|
||||
if (!is_array($dec)) {
|
||||
return $targetType === 'face' ? '人脸分析' : trim($result);
|
||||
}
|
||||
|
||||
if ($targetType === 'face') {
|
||||
return '人脸分析';
|
||||
}
|
||||
|
||||
if ($targetType === 'mbti') {
|
||||
return (string) ($dec['mbtiType'] ?? $dec['type'] ?? $dec['result'] ?? '');
|
||||
}
|
||||
|
||||
if ($targetType === 'disc') {
|
||||
$desc = $dec['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
}
|
||||
if (!empty($dec['dominantType'])) {
|
||||
return (string) $dec['dominantType'];
|
||||
}
|
||||
return (string) ($dec['disc'] ?? '');
|
||||
}
|
||||
|
||||
if ($targetType === 'pdp') {
|
||||
$desc = $dec['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
}
|
||||
if (!empty($dec['dominantType'])) {
|
||||
return (string) $dec['dominantType'];
|
||||
}
|
||||
return (string) ($dec['pdp'] ?? '');
|
||||
}
|
||||
|
||||
return (string) ($dec['type'] ?? $dec['result'] ?? '');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从人脸分析结果中提取对应的 MBTI / DISC / PDP 文本
|
||||
*/
|
||||
private function extractFaceSubType(array $tests, string $subType): string
|
||||
{
|
||||
$target = strtolower($subType);
|
||||
foreach ($tests as $t) {
|
||||
if (strtolower($t['testType'] ?? '') !== 'face') {
|
||||
continue;
|
||||
}
|
||||
$result = $t['result'] ?? '';
|
||||
if (!is_string($result)) {
|
||||
continue;
|
||||
}
|
||||
$dec = json_decode($result, true);
|
||||
if (!is_array($dec)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($target === 'mbti') {
|
||||
if (!empty($dec['mbti']['type'])) {
|
||||
return (string) $dec['mbti']['type'];
|
||||
}
|
||||
if (!empty($dec['mbtiType'])) {
|
||||
return (string) $dec['mbtiType'];
|
||||
}
|
||||
} elseif ($target === 'disc') {
|
||||
if (!empty($dec['disc']['primary'])) {
|
||||
return (string) $dec['disc']['primary'];
|
||||
}
|
||||
if (!empty($dec['disc'])) {
|
||||
return (string) $dec['disc'];
|
||||
}
|
||||
} elseif ($target === 'pdp') {
|
||||
if (!empty($dec['pdp']['primary'])) {
|
||||
return (string) $dec['pdp']['primary'];
|
||||
}
|
||||
if (!empty($dec['pdp'])) {
|
||||
return (string) $dec['pdp'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
265
api/app/controller/admin/Auth.php
Normal file
265
api/app/controller/admin/Auth.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\User as UserModel;
|
||||
use app\common\service\JwtService;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 后台管理员认证控制器
|
||||
*/
|
||||
class Auth extends BaseController
|
||||
{
|
||||
/**
|
||||
* 后台管理员登录(兼容旧版路由)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$username = Request::param('username', '');
|
||||
$password = Request::param('password', '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
return error('用户名和密码不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户(使用原生查询获取密码字段)- 只允许普通管理员和企业管理员登录
|
||||
$user = Db::name('users')
|
||||
->where('username', $username)
|
||||
->where('role', 'in', ['admin', 'enterprise_admin']) // 只允许普通管理员和企业管理员登录
|
||||
->find();
|
||||
|
||||
if (!$user) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if ($user['status'] != 1) {
|
||||
return error('账号已被禁用', 403);
|
||||
}
|
||||
|
||||
// 更新登录信息(使用时间戳,驼峰命名)
|
||||
Db::name('users')
|
||||
->where('id', $user['id'])
|
||||
->update([
|
||||
'lastLoginTime' => time(),
|
||||
'lastLoginIp' => Request::ip(),
|
||||
'updatedAt' => time()
|
||||
]);
|
||||
|
||||
// 生成Token
|
||||
$payload = [
|
||||
'userId' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'enterpriseId' => $user['enterpriseId'] ?? null
|
||||
];
|
||||
|
||||
$token = JwtService::generateToken($payload);
|
||||
|
||||
unset($user['password']);
|
||||
|
||||
return success([
|
||||
'token' => $token,
|
||||
'expires_in' => config('jwt.expire'),
|
||||
'user' => $user
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登录(新路由:/api/v1/auth/admin/login)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function adminLogin()
|
||||
{
|
||||
$username = Request::param('username', '');
|
||||
$password = Request::param('password', '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
return error('用户名和密码不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户(使用原生查询获取密码字段)
|
||||
$user = Db::name('users')
|
||||
->where('username', $username)
|
||||
->where('role', 'in', ['admin', 'enterprise_admin']) // 只允许普通管理员和企业管理员
|
||||
->find();
|
||||
|
||||
if (!$user) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if ($user['status'] != 1) {
|
||||
return error('账号已被禁用', 403);
|
||||
}
|
||||
|
||||
// 更新登录信息(使用时间戳,驼峰命名)
|
||||
Db::name('users')
|
||||
->where('id', $user['id'])
|
||||
->update([
|
||||
'lastLoginTime' => time(),
|
||||
'lastLoginIp' => Request::ip(),
|
||||
'updatedAt' => time()
|
||||
]);
|
||||
|
||||
// 生成Token
|
||||
$payload = [
|
||||
'userId' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role'],
|
||||
'enterpriseId' => $user['enterpriseId'] ?? null
|
||||
];
|
||||
|
||||
$token = JwtService::generateToken($payload);
|
||||
|
||||
unset($user['password']);
|
||||
|
||||
return success([
|
||||
'token' => $token,
|
||||
'expires_in' => config('jwt.expire'),
|
||||
'user' => $user
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 超级管理员登录(新路由:/api/v1/auth/superadmin/login)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function superAdminLogin()
|
||||
{
|
||||
$username = Request::param('username', '');
|
||||
$password = Request::param('password', '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
return error('用户名和密码不能为空', 400);
|
||||
}
|
||||
|
||||
// 查找用户(使用原生查询获取密码字段)
|
||||
$user = Db::name('users')
|
||||
->where('username', $username)
|
||||
->where('role', 'superadmin') // 只允许超级管理员
|
||||
->find();
|
||||
|
||||
if (!$user) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!password_verify($password, $user['password'])) {
|
||||
return error('用户名或密码错误', 401);
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if ($user['status'] != 1) {
|
||||
return error('账号已被禁用', 403);
|
||||
}
|
||||
|
||||
// 更新登录信息(使用时间戳,驼峰命名)
|
||||
Db::name('users')
|
||||
->where('id', $user['id'])
|
||||
->update([
|
||||
'lastLoginTime' => time(),
|
||||
'lastLoginIp' => Request::ip(),
|
||||
'updatedAt' => time()
|
||||
]);
|
||||
|
||||
// 生成Token
|
||||
$payload = [
|
||||
'userId' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'role' => $user['role']
|
||||
];
|
||||
|
||||
$token = JwtService::generateToken($payload);
|
||||
|
||||
unset($user['password']);
|
||||
|
||||
return success([
|
||||
'token' => $token,
|
||||
'expires_in' => config('jwt.expire'),
|
||||
'user' => $user
|
||||
], '登录成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录管理员信息(需要认证)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function me()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
$userModel = Db::name('users')->where('id', $user['userId'] ?? $user['user_id'] ?? null)->find();
|
||||
if (!$userModel) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 检查角色(必须是普通管理员或企业管理员,不包括超级管理员)
|
||||
if (!in_array($userModel['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问后台', 403);
|
||||
}
|
||||
|
||||
unset($userModel['password']);
|
||||
|
||||
return success($userModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录(需要认证)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if ($user && isset($user['userId'])) {
|
||||
JwtService::deleteToken($user['userId']);
|
||||
} elseif ($user && isset($user['user_id'])) {
|
||||
JwtService::deleteToken($user['user_id']);
|
||||
}
|
||||
|
||||
return success(null, '退出成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
$token = JwtService::getTokenFromRequest($this->request);
|
||||
|
||||
if (!$token) {
|
||||
return error('未提供Token', 401);
|
||||
}
|
||||
|
||||
$newToken = JwtService::refreshToken($token);
|
||||
|
||||
if (!$newToken) {
|
||||
return error('Token无效或已过期', 401);
|
||||
}
|
||||
|
||||
return success([
|
||||
'token' => $newToken,
|
||||
'expires_in' => config('jwt.expire')
|
||||
], '刷新成功');
|
||||
}
|
||||
}
|
||||
|
||||
179
api/app/controller/admin/Dashboard.php
Normal file
179
api/app/controller/admin/Dashboard.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 数据概览控制器(普通管理员)
|
||||
*/
|
||||
class Dashboard extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取统计数据
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
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 {
|
||||
// admin / enterprise_admin 均只统计本企业数据
|
||||
$enterpriseId = $user['enterpriseId'] ?? null;
|
||||
if (!$enterpriseId) {
|
||||
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
|
||||
$enterpriseId = $adminRow['enterpriseId'] ?? null;
|
||||
}
|
||||
|
||||
// 企业用户 ID 集合(用于后续统计个人版测试)
|
||||
$enterpriseUserIds = [];
|
||||
if ($enterpriseId) {
|
||||
$enterpriseUserIds = Db::name('wechat_users')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->column('id');
|
||||
$enterpriseUserIds = array_values(array_filter($enterpriseUserIds));
|
||||
}
|
||||
|
||||
// 总用户数:wechat_users.enterpriseId = 本企业
|
||||
if ($enterpriseId) {
|
||||
$totalUsers = count($enterpriseUserIds);
|
||||
} else {
|
||||
try {
|
||||
$totalUsers = (int) Db::name('wechat_users')->count('openid', true);
|
||||
} catch (\Throwable $e) {
|
||||
$totalUsers = (int) Db::name('wechat_users')->count();
|
||||
}
|
||||
}
|
||||
|
||||
// 已完成测试数:严格按 test_results.enterpriseId 归属企业统计
|
||||
if ($enterpriseId) {
|
||||
$testsCompleted = (int) Db::name('test_results')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->count();
|
||||
} else {
|
||||
$testsCompleted = (int) Db::name('test_results')->count();
|
||||
}
|
||||
|
||||
// 今日活跃用户数
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$todayEnd = strtotime(date('Y-m-d 23:59:59'));
|
||||
$activeQuery = Db::name('test_results')
|
||||
->where('createdAt', '>=', $todayStart)
|
||||
->where('createdAt', '<=', $todayEnd);
|
||||
if ($enterpriseId) {
|
||||
$activeQuery->where('enterpriseId', $enterpriseId);
|
||||
$activeIds = $activeQuery->distinct(true)->column('userId');
|
||||
$activeToday = count(array_filter($activeIds));
|
||||
} else {
|
||||
$activeIds = $activeQuery->distinct(true)->column('userId');
|
||||
$activeToday = count(array_filter($activeIds));
|
||||
}
|
||||
|
||||
// 待审核(暂返回0)
|
||||
$pendingReviews = 0;
|
||||
|
||||
// 最近 14 天测试趋势
|
||||
$days = 14;
|
||||
$startDate = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')));
|
||||
$trendQuery = Db::name('test_results')
|
||||
->where('createdAt', '>=', $startDate)
|
||||
->whereIn('testType', ['face', 'mbti', 'disc', 'pdp']);
|
||||
if ($enterpriseId) {
|
||||
$trendQuery->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
$trendRows = $trendQuery
|
||||
->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, testType, COUNT(*) as c")
|
||||
->group('d,testType')
|
||||
->order('d', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 组装为按日期汇总的数组
|
||||
$trendMap = [];
|
||||
foreach ($trendRows as $row) {
|
||||
$d = $row['d'];
|
||||
$type = $row['testType'];
|
||||
$cnt = (int) ($row['c'] ?? 0);
|
||||
if (!isset($trendMap[$d])) {
|
||||
$trendMap[$d] = [
|
||||
'date' => $d,
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
if (in_array($type, ['face', 'mbti', 'disc', 'pdp'], true)) {
|
||||
$trendMap[$d][$type] += $cnt;
|
||||
$trendMap[$d]['total'] += $cnt;
|
||||
}
|
||||
}
|
||||
|
||||
// 补齐没有数据的日期
|
||||
$trendData = [];
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$d = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days'));
|
||||
if (isset($trendMap[$d])) {
|
||||
$trendData[] = $trendMap[$d];
|
||||
} else {
|
||||
$trendData[] = [
|
||||
'date' => $d,
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return success([
|
||||
'totalUsers' => $totalUsers,
|
||||
'testsCompleted' => $testsCompleted,
|
||||
'activeToday' => $activeToday,
|
||||
'pendingReviews' => $pendingReviews,
|
||||
'testTrends' => $trendData,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取统计数据失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param int $timestamp
|
||||
* @return string
|
||||
*/
|
||||
private function formatTime($timestamp)
|
||||
{
|
||||
if (!$timestamp) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$diff = $now - $timestamp;
|
||||
|
||||
if ($diff < 60) {
|
||||
return '刚刚';
|
||||
} elseif ($diff < 3600) {
|
||||
return floor($diff / 60) . '分钟前';
|
||||
} elseif ($diff < 86400) {
|
||||
return floor($diff / 3600) . '小时前';
|
||||
} elseif ($diff < 604800) {
|
||||
return floor($diff / 86400) . '天前';
|
||||
} else {
|
||||
return date('Y-m-d H:i', $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
772
api/app/controller/admin/Distribution.php
Normal file
772
api/app/controller/admin/Distribution.php
Normal file
@@ -0,0 +1,772 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 分销管理控制器(企业管理端)
|
||||
* 路由前缀:/api/v1/admin/distribution
|
||||
*/
|
||||
class Distribution extends BaseController
|
||||
{
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/overview
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function overview()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$days = 7;
|
||||
$trendStart = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' days')));
|
||||
|
||||
try {
|
||||
$query = Db::name('commission_records')
|
||||
->where('enterpriseId', $enterpriseId);
|
||||
|
||||
$totalCommission = (clone $query)->whereIn('status', ['paid', 'frozen'])->sum('commissionFen') ?: 0;
|
||||
$paidCommission = (clone $query)->where('status', 'paid')->sum('commissionFen') ?: 0;
|
||||
$frozenCommission = (clone $query)->where('status', 'frozen')->sum('commissionFen') ?: 0;
|
||||
$totalOrders = (clone $query)->whereIn('status', ['paid', 'frozen'])->count();
|
||||
$todayCommission = (clone $query)
|
||||
->whereIn('status', ['paid', 'frozen'])
|
||||
->where('createdAt', '>=', $todayStart)
|
||||
->sum('commissionFen') ?: 0;
|
||||
$pendingCount = (clone $query)->where('status', 'frozen')->count();
|
||||
|
||||
$bindingQuery = Db::name('distribution_bindings')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('status', 'active')
|
||||
->where('expireAt', '>', time());
|
||||
$bindingCount = (clone $bindingQuery)->count();
|
||||
$totalAgents = (clone $bindingQuery)->distinct(true)->count('inviterId');
|
||||
$todayAgents = Db::name('distribution_bindings')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('createdAt', '>=', $todayStart)
|
||||
->distinct(true)
|
||||
->count('inviterId');
|
||||
|
||||
$trendRows = Db::name('commission_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->whereIn('status', ['paid', 'frozen'])
|
||||
->where('createdAt', '>=', $trendStart)
|
||||
->field("FROM_UNIXTIME(createdAt, '%Y-%m-%d') as d, SUM(commissionFen) as totalFen")
|
||||
->group('d')
|
||||
->order('d', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
$trendMap = [];
|
||||
foreach ($trendRows as $row) {
|
||||
$trendMap[$row['d']] = (int) ($row['totalFen'] ?? 0);
|
||||
}
|
||||
$commissionTrend = [];
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$date = date('Y-m-d', strtotime('-' . ($days - 1 - $i) . ' days'));
|
||||
$commissionTrend[] = [
|
||||
'date' => $date,
|
||||
'amount' => round(($trendMap[$date] ?? 0) / 100, 2),
|
||||
];
|
||||
}
|
||||
|
||||
$productSeries = self::buildProductCommissionSeries($enterpriseId);
|
||||
|
||||
return success([
|
||||
'totalAgents' => (int) $totalAgents,
|
||||
'todayAgents' => (int) $todayAgents,
|
||||
'totalCommission' => number_format($totalCommission / 100, 2, '.', ''),
|
||||
'todayCommission' => number_format($todayCommission / 100, 2, '.', ''),
|
||||
'pendingCommission' => number_format($frozenCommission / 100, 2, '.', ''),
|
||||
'pendingCount' => (int) $pendingCount,
|
||||
'paidCommission' => number_format($paidCommission / 100, 2, '.', ''),
|
||||
'frozenCommission' => number_format($frozenCommission / 100, 2, '.', ''),
|
||||
'totalOrders' => (int) $totalOrders,
|
||||
'bindingCount' => (int) $bindingCount,
|
||||
'commissionTrend' => $commissionTrend,
|
||||
'productCommissionSeries' => $productSeries,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取数据失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/distributors 分销商列表(有过邀请行为的用户)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function distributors()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$search = trim((string) Request::param('search', ''));
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
|
||||
try {
|
||||
// 找出与本企业关联的所有有过邀请行为的用户(不限 scope,按 enterpriseId 筛选)
|
||||
$query = Db::name('distribution_bindings')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->distinct(true)
|
||||
->field('inviterId')
|
||||
->buildSql();
|
||||
|
||||
$inviterQuery = Db::name('wechat_users')
|
||||
->alias('u')
|
||||
->whereRaw("u.id IN {$query}")
|
||||
->field('u.id, u.nickname, u.avatar, u.createdAt');
|
||||
|
||||
if ($search !== '') {
|
||||
$inviterQuery->where(function ($q) use ($search) {
|
||||
$q->where('u.nickname', 'like', "%{$search}%")
|
||||
->whereOr('u.id', '=', is_numeric($search) ? (int)$search : -1);
|
||||
});
|
||||
}
|
||||
|
||||
$total = (clone $inviterQuery)->count();
|
||||
$inviters = $inviterQuery->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
$inviterIds = array_column($inviters, 'id');
|
||||
|
||||
// 各邀请人的累计佣金与可提现佣金
|
||||
$commStats = [];
|
||||
if (!empty($inviterIds)) {
|
||||
$rows = Db::name('commission_records')
|
||||
->whereIn('inviterId', $inviterIds)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->field('inviterId,
|
||||
SUM(IF(status IN ("paid","frozen"), commissionFen, 0)) as totalFen,
|
||||
SUM(IF(status = "paid", commissionFen, 0)) as paidFen')
|
||||
->group('inviterId')
|
||||
->select()->toArray();
|
||||
foreach ($rows as $r) {
|
||||
$commStats[$r['inviterId']] = $r;
|
||||
}
|
||||
|
||||
// 已提现金额
|
||||
$withdrawnRows = Db::name('distribution_withdrawals')
|
||||
->whereIn('userId', $inviterIds)
|
||||
// 提现金额统计:0=审核中,2=待收款,3=已收款
|
||||
->whereIn('status', [0, 2, 3])
|
||||
->field('userId, SUM(amountFen) as withdrawnFen')
|
||||
->group('userId')
|
||||
->select()->toArray();
|
||||
$withdrawnMap = [];
|
||||
foreach ($withdrawnRows as $r) {
|
||||
$withdrawnMap[$r['userId']] = (int)$r['withdrawnFen'];
|
||||
}
|
||||
|
||||
// 团队人数(绑定人数,不限 scope)
|
||||
$teamRows = Db::name('distribution_bindings')
|
||||
->whereIn('inviterId', $inviterIds)
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->field('inviterId, COUNT(DISTINCT inviteeId) as teamCount')
|
||||
->group('inviterId')
|
||||
->select()->toArray();
|
||||
$teamMap = [];
|
||||
foreach ($teamRows as $r) {
|
||||
$teamMap[$r['inviterId']] = (int)$r['teamCount'];
|
||||
}
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($inviters as $inv) {
|
||||
$uid = $inv['id'];
|
||||
$totalFen = (int)($commStats[$uid]['totalFen'] ?? 0);
|
||||
$paidFen = (int)($commStats[$uid]['paidFen'] ?? 0);
|
||||
$withdrawn = $withdrawnMap[$uid] ?? 0;
|
||||
$avail = max(0, $paidFen - $withdrawn);
|
||||
$list[] = [
|
||||
'id' => $uid,
|
||||
'agentName' => $inv['nickname'] ?: ('用户' . $uid),
|
||||
'avatar' => $inv['avatar'] ?? '',
|
||||
'totalCommission' => number_format($totalFen / 100, 2, '.', ''),
|
||||
'availableCommission'=> number_format($avail / 100, 2, '.', ''),
|
||||
'teamCount' => $teamMap[$uid] ?? 0,
|
||||
'teamPerformance' => '-',
|
||||
'inviteCode' => '-',
|
||||
'level' => '-',
|
||||
'createdAt' => $inv['createdAt'],
|
||||
];
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取分销商列表失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/bindings 绑定记录列表
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function bindings()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$status = Request::param('status', '');
|
||||
$inviterId = (int) Request::param('inviterId', 0);
|
||||
|
||||
try {
|
||||
$query = Db::name('distribution_bindings')
|
||||
->alias('b')
|
||||
->leftJoin('wechat_users inv', 'b.inviterId = inv.id')
|
||||
->leftJoin('wechat_users invt', 'b.inviteeId = invt.id')
|
||||
->field('b.*, inv.nickname as inviterName, inv.avatar as inviterAvatar,
|
||||
invt.nickname as inviteeName, invt.avatar as inviteeAvatar')
|
||||
->where('b.enterpriseId', $enterpriseId);
|
||||
|
||||
if ($inviterId > 0) {
|
||||
$query->where('b.inviterId', $inviterId);
|
||||
}
|
||||
if ($status) {
|
||||
$query->where('b.status', $status);
|
||||
}
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('b.updatedAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$now = time();
|
||||
foreach ($list as &$row) {
|
||||
$row['remainDays'] = max(0, (int) ceil(($row['expireAt'] - $now) / 86400));
|
||||
$row['inviterName'] = $row['inviterName'] ?: '未知';
|
||||
$row['inviteeName'] = $row['inviteeName'] ?: '未知';
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取绑定记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/commissions 佣金记录列表
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function commissions()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$status = Request::param('status', '');
|
||||
$inviterId = (int) Request::param('inviterId', 0);
|
||||
|
||||
try {
|
||||
$query = Db::name('commission_records')
|
||||
->alias('c')
|
||||
->leftJoin('wechat_users inv', 'c.inviterId = inv.id')
|
||||
->leftJoin('wechat_users invt', 'c.inviteeId = invt.id')
|
||||
->field('c.*, inv.nickname as inviterName, inv.avatar as inviterAvatar, invt.nickname as inviteeName, invt.avatar as inviteeAvatar')
|
||||
->where('c.enterpriseId', $enterpriseId);
|
||||
|
||||
if ($inviterId > 0) {
|
||||
$query->where('c.inviterId', $inviterId);
|
||||
}
|
||||
if ($status) {
|
||||
$query->where('c.status', $status);
|
||||
}
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('c.createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$orderIds = [];
|
||||
$testResultIds = [];
|
||||
foreach ($list as $row) {
|
||||
if (!empty($row['orderId'])) {
|
||||
$orderIds[] = (int) $row['orderId'];
|
||||
}
|
||||
if (!empty($row['testResultId'])) {
|
||||
$testResultIds[] = (int) $row['testResultId'];
|
||||
}
|
||||
}
|
||||
|
||||
$orderTypeMap = [];
|
||||
if (!empty($orderIds)) {
|
||||
$rows = Db::name('test_results')
|
||||
->whereIn('orderId', array_values(array_unique($orderIds)))
|
||||
->field('orderId, testType')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rows as $item) {
|
||||
$orderTypeMap[(int) $item['orderId']] = self::normalizeTestType($item['testType'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$resultTypeMap = [];
|
||||
if (!empty($testResultIds)) {
|
||||
$rows = Db::name('test_results')
|
||||
->whereIn('id', array_values(array_unique($testResultIds)))
|
||||
->field('id, testType')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rows as $item) {
|
||||
$resultTypeMap[(int) $item['id']] = self::normalizeTestType($item['testType'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$testType = 'other';
|
||||
if (($row['commissionSource'] ?? '') === 'test_completion' && !empty($row['testResultId'])) {
|
||||
$testType = $resultTypeMap[(int) $row['testResultId']] ?? 'other';
|
||||
} elseif (!empty($row['orderId'])) {
|
||||
$testType = $orderTypeMap[(int) $row['orderId']] ?? 'other';
|
||||
}
|
||||
|
||||
$row['testType'] = $testType;
|
||||
$row['testTypeLabel'] = self::getTestTypeLabel($testType);
|
||||
$row['commissionYuan'] = number_format($row['commissionFen'] / 100, 2, '.', '');
|
||||
$row['orderYuan'] = number_format($row['orderAmount'] / 100, 2, '.', '');
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取佣金记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/withdrawals 提现申请列表
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function withdrawals()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$page = max(1, (int) Request::param('page', 1));
|
||||
$pageSize = min(100, (int) Request::param('pageSize', 20));
|
||||
$status = Request::param('status', '');
|
||||
|
||||
try {
|
||||
$query = Db::name('distribution_withdrawals')
|
||||
->alias('w')
|
||||
->join('wechat_users u', 'w.userId = u.id')
|
||||
->field('w.*, u.nickname, u.avatar')
|
||||
->where('u.enterpriseId', $enterpriseId);
|
||||
|
||||
if ($status !== '') {
|
||||
// 后台传入可以是字符串或数字,这里统一转 int
|
||||
$query->where('w.status', (int)$status);
|
||||
}
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$list = $query->order('w.createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$row['amountYuan'] = number_format($row['amountFen'] / 100, 2, '.', '');
|
||||
$row['nickname'] = $row['nickname'] ?: '未知用户';
|
||||
|
||||
// 确保前端拿到的是数字 status(避免 '0' 和 0 比较异常)
|
||||
$code = (int) ($row['status'] ?? 0);
|
||||
$row['status'] = $code;
|
||||
|
||||
// 统一后台状态文案:0审核中、1已驳回、2待收款、3已收款、4已过期
|
||||
switch ($code) {
|
||||
case 0:
|
||||
$row['statusLabel'] = '审核中';
|
||||
break;
|
||||
case 1:
|
||||
$row['statusLabel'] = '已驳回';
|
||||
break;
|
||||
case 2:
|
||||
$row['statusLabel'] = '待收款';
|
||||
break;
|
||||
case 3:
|
||||
$row['statusLabel'] = '已收款';
|
||||
break;
|
||||
case 4:
|
||||
$row['statusLabel'] = '已过期';
|
||||
break;
|
||||
default:
|
||||
$row['statusLabel'] = '未知';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return success(['list' => $list, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取提现记录失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// POST distribution/withdrawals/:id/approve 审核通过
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function approveWithdrawal(int $id)
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$note = Request::param('note', '');
|
||||
$now = time();
|
||||
|
||||
$record = Db::name('distribution_withdrawals')
|
||||
->alias('w')
|
||||
->join('wechat_users u', 'w.userId = u.id')
|
||||
->where('w.id', $id)
|
||||
->where('u.enterpriseId', $enterpriseId)
|
||||
->field('w.*, u.openid')
|
||||
->find();
|
||||
// 仅允许处理审核中(status=0)的记录
|
||||
if (!$record || (int)$record['status'] !== 0) {
|
||||
return error('提现申请不存在、已处理或无权限', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成商户明细单号:TX + 时间戳 + 随机数(示例:TX202603121526520005)
|
||||
$outDetailNo = 'TX' . date('YmdHis') . mt_rand(1000, 9999);
|
||||
|
||||
// 调用微信商家转账到零钱接口
|
||||
$service = new \app\common\service\WechatTransferService();
|
||||
$result = $service->createTransfer([
|
||||
'out_detail_no' => $outDetailNo,
|
||||
'transfer_amount'=> (int) $record['amountFen'],
|
||||
'transfer_remark'=> '推广佣金提现',
|
||||
'openid' => $record['openid'],
|
||||
'batch_name' => '推广佣金提现',
|
||||
'batch_remark' => '用户提现',
|
||||
]);
|
||||
|
||||
if ($result['success'] !== true) {
|
||||
$err = $result['error'] ?? [];
|
||||
$code = $err['code'] ?? 'UNKNOWN';
|
||||
$msg = $err['message'] ?? '微信转账接口调用失败';
|
||||
return error("微信转账发起失败({$code}):{$msg}", 500);
|
||||
}
|
||||
|
||||
$wechatData = $result['data'] ?? [];
|
||||
Db::name('distribution_withdrawals')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
// 2=待收款(已发起微信转账,等待用户确认)
|
||||
'status' => 2,
|
||||
'auditNote' => $note,
|
||||
'auditAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
'pay_type' => 'wechat',
|
||||
'out_bill_no' => $outDetailNo,
|
||||
'transfer_bill_no' => $wechatData['batch_id'] ?? null,
|
||||
'wechat_pay_state' => $wechatData['batch_status'] ?? 'PROCESSING',
|
||||
'transfer_scene_id'=> $wechatData['transfer_scene_id'] ?? env('TRANSFER_SCENE_ID', '1005'),
|
||||
'mch_id' => env('MCH_ID', null),
|
||||
]);
|
||||
|
||||
return success(null, '审核通过,已发起微信转账');
|
||||
} catch (\Exception $e) {
|
||||
return error('操作失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// POST distribution/withdrawals/:id/reject 审核拒绝
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function rejectWithdrawal(int $id)
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$note = Request::param('note', '');
|
||||
$now = time();
|
||||
|
||||
$record = Db::name('distribution_withdrawals')
|
||||
->alias('w')
|
||||
->join('wechat_users u', 'w.userId = u.id')
|
||||
->where('w.id', $id)
|
||||
->where('u.enterpriseId', $enterpriseId)
|
||||
->field('w.*')
|
||||
->find();
|
||||
// 仅允许处理审核中(status=0)的记录
|
||||
if (!$record || (int)$record['status'] !== 0) {
|
||||
return error('提现申请不存在、已处理或无权限', 400);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 退回余额
|
||||
Db::name('wechat_users')
|
||||
->where('id', $record['userId'])
|
||||
->inc('walletBalance', $record['amountFen'])
|
||||
->update(['updatedAt' => $now]);
|
||||
|
||||
Db::name('distribution_withdrawals')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
// 1=已驳回
|
||||
'status' => 1,
|
||||
'auditNote' => $note,
|
||||
'auditAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
return success(null, '已拒绝,余额已退回');
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return error('操作失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GET distribution/settings 获取企业分销配置
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function settings()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
|
||||
try {
|
||||
$config = Db::name('system_config')
|
||||
->where('key', 'distribution')
|
||||
->where('enterprise_id', $enterpriseId)
|
||||
->find();
|
||||
|
||||
$tsDefault = self::defaultTestSettings();
|
||||
$default = [
|
||||
'enabled' => true,
|
||||
'promoCenterTitle' => '推广中心',
|
||||
'bindingDays' => 30,
|
||||
'testSettings' => $tsDefault,
|
||||
];
|
||||
|
||||
if ($config && $config['value']) {
|
||||
$settings = is_string($config['value']) ? json_decode($config['value'], true) : $config['value'];
|
||||
$settings = array_merge($default, $settings ?? []);
|
||||
} else {
|
||||
$settings = $default;
|
||||
}
|
||||
|
||||
// 附加前端可读的 commissionAmount(元)
|
||||
$settings['testSettings'] = self::appendTestSettingsAmount(
|
||||
$settings['testSettings'] ?? $tsDefault
|
||||
);
|
||||
|
||||
return success($settings);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// PUT distribution/settings 更新企业分销配置
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public function updateSettings()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = (int) ($user['enterpriseId'] ?? 0);
|
||||
$settings = Request::only(['enabled', 'promoCenterTitle', 'bindingDays', 'testSettings']);
|
||||
|
||||
$promoTitle = trim((string)($settings['promoCenterTitle'] ?? ''));
|
||||
$toSave = [
|
||||
'enabled' => (bool)($settings['enabled'] ?? true),
|
||||
'promoCenterTitle' => $promoTitle !== '' ? $promoTitle : '推广中心',
|
||||
'bindingDays' => (int)($settings['bindingDays'] ?? 30),
|
||||
'testSettings' => self::sanitizeTestSettings($settings['testSettings'] ?? null),
|
||||
];
|
||||
|
||||
try {
|
||||
$now = time();
|
||||
$existing = Db::name('system_config')
|
||||
->where('key', 'distribution')
|
||||
->where('enterprise_id', $enterpriseId)
|
||||
->find();
|
||||
|
||||
if ($existing) {
|
||||
Db::name('system_config')
|
||||
->where('key', 'distribution')
|
||||
->where('enterprise_id', $enterpriseId)
|
||||
->update(['value' => json_encode($toSave, JSON_UNESCAPED_UNICODE), 'updatedAt' => $now]);
|
||||
} else {
|
||||
Db::name('system_config')->insert([
|
||||
'key' => 'distribution',
|
||||
'enterprise_id' => $enterpriseId,
|
||||
'value' => json_encode($toSave, JSON_UNESCAPED_UNICODE),
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
$toSave['testSettings'] = self::appendTestSettingsAmount($toSave['testSettings']);
|
||||
return success($toSave, '配置已保存');
|
||||
} catch (\Exception $e) {
|
||||
return error('保存配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private static function defaultTestSettings(): array
|
||||
{
|
||||
$item = ['enabled' => true, 'commissionType' => 'ratio', 'commissionRate' => 90, 'commissionAmountFen' => 0, 'noPayment' => false];
|
||||
return ['face' => $item, 'mbti' => $item, 'disc' => $item, 'pdp' => $item];
|
||||
}
|
||||
|
||||
private static function sanitizeTestSettings($raw): array
|
||||
{
|
||||
$default = self::defaultTestSettings();
|
||||
if (!is_array($raw)) return $default;
|
||||
$result = [];
|
||||
foreach ($default as $type => $def) {
|
||||
$s = $raw[$type] ?? [];
|
||||
$commissionType = in_array($s['commissionType'] ?? '', ['ratio', 'amount']) ? $s['commissionType'] : 'ratio';
|
||||
$amountFen = isset($s['commissionAmount'])
|
||||
? (int) round((float)$s['commissionAmount'] * 100)
|
||||
: (int)($s['commissionAmountFen'] ?? 0);
|
||||
$rate = max(0, min(100, (int)($s['commissionRate'] ?? 90)));
|
||||
$result[$type] = [
|
||||
'enabled' => ($s['enabled'] ?? true) !== false,
|
||||
'commissionType' => $commissionType,
|
||||
'commissionRate' => $commissionType === 'ratio' ? $rate : 0,
|
||||
'commissionAmountFen'=> $commissionType === 'amount' ? max(0, $amountFen) : 0,
|
||||
'noPayment' => !empty($s['noPayment']),
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function appendTestSettingsAmount(array $ts): array
|
||||
{
|
||||
foreach ($ts as $k => $v) {
|
||||
$ts[$k]['commissionAmount'] = round(($v['commissionAmountFen'] ?? 0) / 100, 2);
|
||||
}
|
||||
return $ts;
|
||||
}
|
||||
|
||||
private static function buildProductCommissionSeries(int $enterpriseId): array
|
||||
{
|
||||
$records = Db::name('commission_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->whereIn('status', ['paid', 'frozen'])
|
||||
->field('orderId, testResultId, commissionSource, commissionFen')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$orderIds = [];
|
||||
$testResultIds = [];
|
||||
foreach ($records as $record) {
|
||||
if (!empty($record['orderId'])) {
|
||||
$orderIds[] = (int) $record['orderId'];
|
||||
}
|
||||
if (!empty($record['testResultId'])) {
|
||||
$testResultIds[] = (int) $record['testResultId'];
|
||||
}
|
||||
}
|
||||
|
||||
$orderTypeMap = [];
|
||||
if (!empty($orderIds)) {
|
||||
$rows = Db::name('test_results')
|
||||
->whereIn('orderId', array_values(array_unique($orderIds)))
|
||||
->field('orderId, testType')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rows as $row) {
|
||||
$orderTypeMap[(int) $row['orderId']] = self::normalizeTestType($row['testType'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$resultTypeMap = [];
|
||||
if (!empty($testResultIds)) {
|
||||
$rows = Db::name('test_results')
|
||||
->whereIn('id', array_values(array_unique($testResultIds)))
|
||||
->field('id, testType')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rows as $row) {
|
||||
$resultTypeMap[(int) $row['id']] = self::normalizeTestType($row['testType'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$totals = [
|
||||
'face' => 0,
|
||||
'mbti' => 0,
|
||||
'disc' => 0,
|
||||
'pdp' => 0,
|
||||
'other' => 0,
|
||||
];
|
||||
|
||||
foreach ($records as $record) {
|
||||
$type = 'other';
|
||||
if (($record['commissionSource'] ?? '') === 'test_completion' && !empty($record['testResultId'])) {
|
||||
$type = $resultTypeMap[(int) $record['testResultId']] ?? 'other';
|
||||
} elseif (!empty($record['orderId'])) {
|
||||
$type = $orderTypeMap[(int) $record['orderId']] ?? 'other';
|
||||
}
|
||||
|
||||
if (!isset($totals[$type])) {
|
||||
$type = 'other';
|
||||
}
|
||||
$totals[$type] += (int) ($record['commissionFen'] ?? 0);
|
||||
}
|
||||
|
||||
return [
|
||||
['label' => '人脸分析', 'value' => round($totals['face'] / 100, 2)],
|
||||
['label' => 'MBTI', 'value' => round($totals['mbti'] / 100, 2)],
|
||||
['label' => 'DISC', 'value' => round($totals['disc'] / 100, 2)],
|
||||
['label' => 'PDP', 'value' => round($totals['pdp'] / 100, 2)],
|
||||
['label' => '其他', 'value' => round($totals['other'] / 100, 2)],
|
||||
];
|
||||
}
|
||||
|
||||
private static function normalizeTestType(string $testType): string
|
||||
{
|
||||
$normalized = strtolower(trim($testType));
|
||||
if ($normalized === 'ai') {
|
||||
return 'face';
|
||||
}
|
||||
if (in_array($normalized, ['face', 'mbti', 'disc', 'pdp'], true)) {
|
||||
return $normalized;
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
|
||||
private static function getTestTypeLabel(string $testType): string
|
||||
{
|
||||
$map = [
|
||||
'face' => '人脸',
|
||||
'mbti' => 'MBTI',
|
||||
'disc' => 'DISC',
|
||||
'pdp' => 'PDP',
|
||||
'other' => '其他',
|
||||
];
|
||||
|
||||
return $map[$testType] ?? strtoupper($testType ?: '其他');
|
||||
}
|
||||
}
|
||||
204
api/app/controller/admin/Finance.php
Normal file
204
api/app/controller/admin/Finance.php
Normal file
@@ -0,0 +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;
|
||||
}
|
||||
}
|
||||
64
api/app/controller/admin/Invite.php
Normal file
64
api/app/controller/admin/Invite.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\service\WechatService;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 管理端 - 小程序邀请二维码(带企业参数)
|
||||
*/
|
||||
class Invite extends BaseController
|
||||
{
|
||||
/**
|
||||
* 生成专属邀请小程序码,scene 带企业 ID,扫码进入 pages/enterprise/index 可解析
|
||||
* GET /api/v1/admin/invite/qrcode
|
||||
* 可选:?enterpriseId=1 仅普通管理员指定企业时传;企业管理员用自身 enterpriseId
|
||||
*
|
||||
* 返回 data:image/png;base64,... 形式的图片地址
|
||||
*/
|
||||
public function qrcode()
|
||||
{
|
||||
$admin = $this->request->user ?? null;
|
||||
if (!$admin || !in_array($admin['role'] ?? '', ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$enterpriseId = null;
|
||||
if (($admin['role'] ?? '') === 'enterprise_admin') {
|
||||
$row = Db::name('users')->where('id', (int) ($admin['userId'] ?? 0))->find();
|
||||
$enterpriseId = isset($row['enterpriseId']) ? (int) $row['enterpriseId'] : null;
|
||||
} else {
|
||||
$enterpriseId = (int) $this->request->param('enterpriseId', 0);
|
||||
if ($enterpriseId <= 0) {
|
||||
return error('请指定企业(企业管理员无需传参,使用所属企业)', 400);
|
||||
}
|
||||
}
|
||||
if ($enterpriseId <= 0) {
|
||||
return error('无法确定企业,仅企业管理员或指定 enterpriseId 可生成邀请码', 400);
|
||||
}
|
||||
|
||||
// 场景值:e_企业ID,小程序 onLoad(options.scene) 可解析
|
||||
$scene = 'e_' . $enterpriseId;
|
||||
$page = 'pages/enterprise/index';
|
||||
|
||||
$result = WechatService::getWxacodeUnlimited($scene, $page, 430);
|
||||
if (isset($result['errcode'])) {
|
||||
return error('获取小程序码失败:' . ($result['errmsg'] ?? ''), 500);
|
||||
}
|
||||
|
||||
$binary = $result['binary'] ?? '';
|
||||
if ($binary === '') {
|
||||
return error('小程序码生成失败', 500);
|
||||
}
|
||||
|
||||
$base64 = 'data:image/png;base64,' . base64_encode($binary);
|
||||
|
||||
return success([
|
||||
'qrcode' => $base64,
|
||||
'scene' => $scene,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
168
api/app/controller/admin/Order.php
Normal file
168
api/app/controller/admin/Order.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 管理端订单列表(只读),包含用户信息与关联的测试数据
|
||||
*/
|
||||
class Order extends BaseController
|
||||
{
|
||||
/**
|
||||
* 订单列表:分页、关键词、状态/产品筛选;企业管理员仅本企业订单
|
||||
* GET /api/v1/admin/orders?page=1&pageSize=20&keyword=&status=&productType=
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
if (!in_array($user['role'] ?? '', ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$page = (int) Request::param('page', 1);
|
||||
$pageSize = (int) Request::param('pageSize', 20);
|
||||
$pageSize = min(max($pageSize, 1), 100);
|
||||
$keyword = trim(Request::param('keyword', ''));
|
||||
$status = trim(Request::param('status', ''));
|
||||
$productType = trim(Request::param('productType', ''));
|
||||
|
||||
// admin / enterprise_admin 均只能看本企业订单
|
||||
$enterpriseId = $user['enterpriseId'] ?? null;
|
||||
if (!$enterpriseId) {
|
||||
$adminRow = Db::name('users')->where('id', $user['userId'] ?? 0)->find();
|
||||
$enterpriseId = $adminRow['enterpriseId'] ?? null;
|
||||
}
|
||||
|
||||
$query = Db::name('orders');
|
||||
|
||||
if ($enterpriseId !== null) {
|
||||
$query->where('enterpriseId', $enterpriseId);
|
||||
}
|
||||
if ($status !== '') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
if ($productType !== '') {
|
||||
$query->where('productType', $productType);
|
||||
}
|
||||
if ($keyword !== '') {
|
||||
if (is_numeric($keyword)) {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$q->whereLike('orderNo', '%' . $keyword . '%')->whereOr('userId', (int) $keyword);
|
||||
});
|
||||
} else {
|
||||
$userIdsMatch = Db::name('wechat_users')->where('nickname|phone', 'like', '%' . $keyword . '%')->column('id');
|
||||
$userIdsMatch = array_values(array_filter($userIdsMatch));
|
||||
$query->where(function ($q) use ($keyword, $userIdsMatch) {
|
||||
$q->whereLike('orderNo', '%' . $keyword . '%');
|
||||
if (!empty($userIdsMatch)) {
|
||||
$q->whereOr('userId', 'in', $userIdsMatch);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$query->order('createdAt', 'desc');
|
||||
$total = (int) (clone $query)->count();
|
||||
$list = (clone $query)->page($page, $pageSize)->select()->toArray();
|
||||
|
||||
$userIds = array_values(array_unique(array_filter(array_column($list, 'userId'))));
|
||||
$usersMap = [];
|
||||
if (!empty($userIds)) {
|
||||
$users = Db::name('wechat_users')
|
||||
->where('id', 'in', $userIds)
|
||||
->field('id, nickname, phone')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($users as $u) {
|
||||
$usersMap[(int) $u['id']] = $u;
|
||||
}
|
||||
}
|
||||
|
||||
$orderIds = array_column($list, 'id');
|
||||
$testsByOrder = [];
|
||||
if (!empty($orderIds)) {
|
||||
$tests = Db::name('test_results')
|
||||
->where('orderId', 'in', $orderIds)
|
||||
->field('id, orderId, userId, testType, resultData, createdAt')
|
||||
->order('createdAt', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($tests as $t) {
|
||||
$oid = (int) ($t['orderId'] ?? 0);
|
||||
if ($oid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($testsByOrder[$oid])) {
|
||||
$testsByOrder[$oid] = [];
|
||||
}
|
||||
$raw = $t['resultData'] ?? '';
|
||||
$resultStr = is_string($raw) ? $raw : json_encode($raw, JSON_UNESCAPED_UNICODE);
|
||||
$testsByOrder[$oid][] = [
|
||||
'id' => (int) $t['id'],
|
||||
'testType' => $t['testType'] ?? '',
|
||||
'resultSummary' => $this->extractResultSummary($t['testType'] ?? '', $resultStr),
|
||||
'createdAt' => isset($t['createdAt']) ? (int) $t['createdAt'] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($list as &$row) {
|
||||
$uid = (int) ($row['userId'] ?? 0);
|
||||
$u = $usersMap[$uid] ?? null;
|
||||
$row['userName'] = $u ? ($u['nickname'] ?? ('用户' . $uid)) : ('用户' . $uid);
|
||||
$row['userPhone'] = $u ? ($u['phone'] ?? '') : '';
|
||||
$row['testData'] = $testsByOrder[$row['id']] ?? [];
|
||||
}
|
||||
|
||||
return paginate_response($list, $total, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 resultData 字符串中提取简要结果(用于列表展示)
|
||||
*/
|
||||
private function extractResultSummary(string $testType, string $resultStr): string
|
||||
{
|
||||
if ($resultStr === '') {
|
||||
return '-';
|
||||
}
|
||||
$data = json_decode($resultStr, true);
|
||||
if (!is_array($data)) {
|
||||
return mb_substr($resultStr, 0, 30) . (mb_strlen($resultStr) > 30 ? '…' : '');
|
||||
}
|
||||
$type = strtolower($testType);
|
||||
|
||||
if ($type === 'mbti') {
|
||||
return (string) ($data['mbtiType'] ?? $data['type'] ?? $data['result'] ?? '');
|
||||
}
|
||||
if ($type === 'disc') {
|
||||
$desc = $data['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
}
|
||||
if (!empty($data['dominantType'])) {
|
||||
return (string) $data['dominantType'] . '型';
|
||||
}
|
||||
return (string) ($data['disc'] ?? '');
|
||||
}
|
||||
if ($type === 'pdp') {
|
||||
$desc = $data['description']['type'] ?? null;
|
||||
if (is_string($desc) && $desc !== '') {
|
||||
return $desc;
|
||||
}
|
||||
if (!empty($data['dominantType'])) {
|
||||
return (string) $data['dominantType'];
|
||||
}
|
||||
return (string) ($data['pdp'] ?? '');
|
||||
}
|
||||
if ($type === 'face' || $type === 'ai') {
|
||||
return '人脸分析';
|
||||
}
|
||||
|
||||
return (string) ($data['type'] ?? $data['result'] ?? '');
|
||||
}
|
||||
}
|
||||
185
api/app/controller/admin/Pricing.php
Normal file
185
api/app/controller/admin/Pricing.php
Normal file
@@ -0,0 +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();
|
||||
}
|
||||
}
|
||||
457
api/app/controller/admin/Question.php
Normal file
457
api/app/controller/admin/Question.php
Normal file
@@ -0,0 +1,457 @@
|
||||
<?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) {
|
||||
// 先检查企业是否有自己的题库
|
||||
$enterpriseQuestionCount = QuestionModel::where('enterpriseId', $enterpriseId)
|
||||
->where('type', $type ?: ['mbti', 'disc', 'pdp'])
|
||||
->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(), '状态更新成功');
|
||||
}
|
||||
}
|
||||
|
||||
418
api/app/controller/admin/Settings.php
Normal file
418
api/app/controller/admin/Settings.php
Normal file
@@ -0,0 +1,418 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\SystemConfig as SystemConfigModel;
|
||||
use app\model\User as UserModel;
|
||||
use think\facade\Request;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 系统设置控制器(普通管理员)
|
||||
*/
|
||||
class Settings extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取系统配置
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
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 {
|
||||
// 获取当前管理员用户名
|
||||
$jwtUsername = $user['username'] ?? null;
|
||||
$username = 'admin';
|
||||
|
||||
if ($jwtUsername) {
|
||||
$currentUser = UserModel::where('username', $jwtUsername)
|
||||
->whereIn('role', ['admin', 'enterprise_admin'])
|
||||
->find();
|
||||
if ($currentUser) {
|
||||
$username = $currentUser->username;
|
||||
} else {
|
||||
$username = $jwtUsername;
|
||||
}
|
||||
}
|
||||
|
||||
return success([
|
||||
'username' => $username
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用字体列表
|
||||
* GET /api/v1/admin/settings/fonts
|
||||
*/
|
||||
public function getFonts()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
$fonts = \app\common\service\PosterService::getAvailableFonts();
|
||||
return success([
|
||||
'fonts' => $fonts,
|
||||
'fontDir' => root_path() . 'public/fonts/',
|
||||
'dirExist' => is_dir(root_path() . 'public/fonts/'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取海报配置
|
||||
* GET /api/v1/admin/settings/poster
|
||||
* 有 enterpriseId 则读企业专属行,否则读全局(enterprise_id=0)
|
||||
*/
|
||||
public function getPosterConfig()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$eid = (int)($user['enterpriseId'] ?? 0);
|
||||
$row = self::getConfig('poster_config', $eid);
|
||||
$poster = $row ?: ['bgColor' => '#ffffff', 'bgImage' => '', 'elements' => []];
|
||||
return success(['poster' => $poster]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存海报配置
|
||||
* PUT /api/v1/admin/settings/poster
|
||||
*/
|
||||
public function updatePosterConfig()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$input = json_decode($this->request->getContent(), true);
|
||||
if (!is_array($input)) {
|
||||
$input = [];
|
||||
}
|
||||
$data = [
|
||||
'bgColor' => $input['bgColor'] ?? '#ffffff',
|
||||
'bgImage' => $input['bgImage'] ?? '',
|
||||
'elements' => $input['elements'] ?? []
|
||||
];
|
||||
$eid = (int)($user['enterpriseId'] ?? 0);
|
||||
|
||||
try {
|
||||
self::saveConfig('poster_config', $data, $eid, '分销海报可视化配置');
|
||||
return success(null, '海报配置已保存');
|
||||
} catch (\Exception $e) {
|
||||
return error('保存失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取配置:key + enterprise_id,有企业专属则取,否则降级到 enterprise_id=0
|
||||
*/
|
||||
private static function getConfig(string $key, int $enterpriseId = 0, bool $fallbackGlobal = false): ?array
|
||||
{
|
||||
$row = Db::name('system_config')
|
||||
->where('key', $key)
|
||||
->where('enterprise_id', $enterpriseId)
|
||||
->find();
|
||||
if ($row && !empty($row['value'])) {
|
||||
$val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value'];
|
||||
if (is_array($val)) return $val;
|
||||
}
|
||||
if ($fallbackGlobal && $enterpriseId > 0) {
|
||||
$row = Db::name('system_config')
|
||||
->where('key', $key)
|
||||
->where('enterprise_id', 0)
|
||||
->find();
|
||||
if ($row && !empty($row['value'])) {
|
||||
$val = is_string($row['value']) ? json_decode($row['value'], true) : $row['value'];
|
||||
if (is_array($val)) return $val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置:key + enterprise_id,存在则 update,否则 insert
|
||||
*/
|
||||
private static function saveConfig(string $key, array $value, int $enterpriseId = 0, string $description = ''): void
|
||||
{
|
||||
$now = time();
|
||||
$json = json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
$exists = Db::name('system_config')
|
||||
->where('key', $key)
|
||||
->where('enterprise_id', $enterpriseId)
|
||||
->find();
|
||||
if ($exists) {
|
||||
Db::name('system_config')
|
||||
->where('key', $key)
|
||||
->where('enterprise_id', $enterpriseId)
|
||||
->update(['value' => $json, 'updatedAt' => $now]);
|
||||
} else {
|
||||
Db::name('system_config')->insert([
|
||||
'key' => $key,
|
||||
'enterprise_id' => $enterpriseId,
|
||||
'value' => $json,
|
||||
'description' => $description,
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解码 JSON(处理可能的多重编码)
|
||||
*/
|
||||
private static function decodeJsonSafe($raw): ?array
|
||||
{
|
||||
if (!$raw) return null;
|
||||
$val = $raw;
|
||||
for ($i = 0; $i < 5 && is_string($val); $i++) {
|
||||
$decoded = json_decode($val, true);
|
||||
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break;
|
||||
$val = $decoded;
|
||||
}
|
||||
return is_array($val) ? $val : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小程序配置
|
||||
* 读取全局 text_config(enterprise_id=0)作为默认值,再用企业专属行覆盖
|
||||
* GET /api/v1/admin/settings/miniprogram
|
||||
*/
|
||||
public function getMiniprogramConfig()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$eid = (int)($user['enterpriseId'] ?? 0);
|
||||
|
||||
// 全局小程序名称(仅超管可改,此处只读)
|
||||
$miniprogramName = '神仙团队AI性格测试';
|
||||
$siteInfo = Db::name('system_config')
|
||||
->where('key', 'site_info')
|
||||
->where('enterprise_id', 0)
|
||||
->find();
|
||||
if ($siteInfo && !empty($siteInfo['value'])) {
|
||||
$val = is_string($siteInfo['value']) ? json_decode($siteInfo['value'], true) : $siteInfo['value'];
|
||||
$miniprogramName = (string) ($val['miniprogramName'] ?? $val['siteName'] ?? $miniprogramName);
|
||||
}
|
||||
|
||||
$tcDefaults = [
|
||||
'analyzingTitle' => '正在分析中',
|
||||
'startButtonText' => '开始面相测试',
|
||||
'startButtonEnterprise' => '开始面部测试',
|
||||
'reportTitle' => '分析报告',
|
||||
'aiAnalysisText' => '智能分析',
|
||||
];
|
||||
|
||||
// 全局文案(enterprise_id=0)作为基础
|
||||
$globalTc = self::getConfig('text_config', 0);
|
||||
$textConfigData = $globalTc
|
||||
? array_merge($tcDefaults, array_intersect_key($globalTc, $tcDefaults))
|
||||
: $tcDefaults;
|
||||
|
||||
// 企业专属文案 + 小程序名称 覆盖
|
||||
if ($eid > 0) {
|
||||
$eidTc = self::getConfig('text_config', $eid);
|
||||
if ($eidTc) {
|
||||
$textConfigData = array_merge($textConfigData, array_intersect_key($eidTc, $tcDefaults));
|
||||
if (!empty($eidTc['miniprogramName'])) {
|
||||
$miniprogramName = (string) $eidTc['miniprogramName'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success([
|
||||
'miniprogramName' => $miniprogramName,
|
||||
'textConfig' => $textConfigData,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return error('获取配置失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新小程序配置
|
||||
* 写入 text_config 行:enterprise_id={eid}(有企业)或 0(无企业)
|
||||
* PUT /api/v1/admin/settings/miniprogram
|
||||
*/
|
||||
public function updateMiniprogramConfig()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user || !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 = [
|
||||
'miniprogramName' => Request::param('miniprogramName', ''),
|
||||
'textConfig' => Request::param('textConfig', []),
|
||||
];
|
||||
}
|
||||
|
||||
$miniprogramName = trim((string) ($input['miniprogramName'] ?? ''));
|
||||
$textConfig = $input['textConfig'] ?? [];
|
||||
|
||||
if ($miniprogramName === '') {
|
||||
return error('小程序名称不能为空', 400);
|
||||
}
|
||||
|
||||
$tcKeys = ['analyzingTitle', 'startButtonText', 'startButtonEnterprise', 'reportTitle', 'aiAnalysisText'];
|
||||
$tcDefaults = [
|
||||
'analyzingTitle' => '正在分析中',
|
||||
'startButtonText' => '开始面相测试',
|
||||
'startButtonEnterprise' => '开始面部测试',
|
||||
'reportTitle' => '分析报告',
|
||||
'aiAnalysisText' => '智能分析',
|
||||
];
|
||||
$tcData = is_array($textConfig) ? array_intersect_key($textConfig, array_flip($tcKeys)) : [];
|
||||
$tcMerge = array_merge($tcDefaults, $tcData);
|
||||
$eid = (int)($user['enterpriseId'] ?? 0);
|
||||
|
||||
try {
|
||||
// eid=0:更新 site_info 的小程序名称(全局)
|
||||
if ($eid === 0) {
|
||||
$siteRow = Db::name('system_config')->where('key', 'site_info')->where('enterprise_id', 0)->find();
|
||||
$siteInfo = $siteRow && !empty($siteRow['value'])
|
||||
? (is_string($siteRow['value']) ? json_decode($siteRow['value'], true) : $siteRow['value'])
|
||||
: [];
|
||||
$siteInfo = is_array($siteInfo) ? $siteInfo : [];
|
||||
$siteInfo['miniprogramName'] = $miniprogramName;
|
||||
$siteInfo['siteName'] = $siteInfo['siteName'] ?? $miniprogramName;
|
||||
$siteInfo['updatedAt'] = time();
|
||||
self::saveConfig('site_info', $siteInfo, 0, '站点信息');
|
||||
} else {
|
||||
// 企业专属:把 miniprogramName 一并写入 text_config
|
||||
$tcMerge['miniprogramName'] = $miniprogramName;
|
||||
}
|
||||
|
||||
// 统一写到 text_config(企业行已含 miniprogramName,全局行不含)
|
||||
self::saveConfig('text_config', $tcMerge, $eid, $eid > 0 ? "小程序文案配置(企业{$eid})" : '小程序文案配置(全局)');
|
||||
return success([
|
||||
'miniprogramName' => $miniprogramName,
|
||||
'textConfig' => $tcMerge,
|
||||
], '小程序配置已保存');
|
||||
} catch (\Exception $e) {
|
||||
return error('保存失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新管理员账户信息
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function updateCredentials()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
// 验证是否为管理员
|
||||
if (!in_array($user['role'], ['admin', 'enterprise_admin'])) {
|
||||
return error('无权限访问', 403);
|
||||
}
|
||||
|
||||
// 兼容 axios JSON PUT 与表单提交
|
||||
$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 = [];
|
||||
}
|
||||
|
||||
$username = trim((string)($input['username'] ?? Request::param('username', '')));
|
||||
$currentPassword = (string)($input['currentPassword'] ?? Request::param('currentPassword', ''));
|
||||
$newPassword = (string)($input['newPassword'] ?? Request::param('newPassword', ''));
|
||||
$confirmPassword = (string)($input['confirmPassword'] ?? Request::param('confirmPassword', ''));
|
||||
|
||||
if (empty($username)) {
|
||||
return error('用户名不能为空', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// 优先使用JWT中的username来查找用户
|
||||
$jwtUsername = $user['username'] ?? null;
|
||||
|
||||
if (empty($jwtUsername)) {
|
||||
return error('无法获取用户信息,请重新登录', 400);
|
||||
}
|
||||
|
||||
// 直接通过username查找用户
|
||||
$userModel = UserModel::where('username', $jwtUsername)
|
||||
->whereIn('role', ['admin', 'enterprise_admin'])
|
||||
->find();
|
||||
|
||||
if (!$userModel) {
|
||||
return error('用户不存在,请检查登录状态', 404);
|
||||
}
|
||||
|
||||
// 如果要修改密码,需要验证当前密码
|
||||
if (!empty($newPassword)) {
|
||||
if (empty($currentPassword)) {
|
||||
return error('请输入当前密码', 400);
|
||||
}
|
||||
|
||||
if ($newPassword !== $confirmPassword) {
|
||||
return error('两次输入的密码不一致', 400);
|
||||
}
|
||||
|
||||
// 验证当前密码(User 模型已有原始加密密码)
|
||||
if (!password_verify($currentPassword, $userModel->password)) {
|
||||
return error('当前密码错误', 400);
|
||||
}
|
||||
|
||||
// 更新密码:传入明文,交由 User 模型的 setPasswordAttr 自动加密
|
||||
$userModel->password = $newPassword;
|
||||
}
|
||||
|
||||
// 更新用户名
|
||||
if ($username !== $userModel->username) {
|
||||
// 检查用户名是否已存在(排除当前用户)
|
||||
$exists = UserModel::where('username', $username)
|
||||
->where('id', '<>', $userModel->id)
|
||||
->find();
|
||||
|
||||
if ($exists) {
|
||||
return error('用户名已存在', 400);
|
||||
}
|
||||
|
||||
$userModel->username = $username;
|
||||
}
|
||||
|
||||
$userModel->save();
|
||||
|
||||
return success([
|
||||
'username' => $userModel->username
|
||||
], '账户信息已更新');
|
||||
} catch (\Exception $e) {
|
||||
return error('更新失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
347
api/app/controller/admin/Upload.php
Normal file
347
api/app/controller/admin/Upload.php
Normal file
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\UploadFile;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 上传控制器(支持本地 & 阿里云 OSS)
|
||||
* 完全参考 BaseCrawler.php 的实现
|
||||
*/
|
||||
class Upload extends BaseController
|
||||
{
|
||||
/**
|
||||
* 上传图片(新闻封面等)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function image()
|
||||
{
|
||||
$file = Request::file('file');
|
||||
|
||||
if (!$file) {
|
||||
return error('未找到上传文件');
|
||||
}
|
||||
|
||||
// 基本校验:大小 & 类型
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
$allowExts = ['jpg', 'jpeg', 'jfif', 'jpe', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif'];
|
||||
$extension = strtolower($file->extension());
|
||||
$fileSize = $file->getSize();
|
||||
|
||||
if (!in_array($extension, $allowExts, true)) {
|
||||
return error('不支持的文件类型,仅支持:jpg、jpeg、jfif、png、gif、webp、heic');
|
||||
}
|
||||
|
||||
if ($fileSize > $maxSize) {
|
||||
return error('文件过大,最大支持 5MB');
|
||||
}
|
||||
|
||||
$config = config('upload');
|
||||
$driver = $config['driver'] ?? 'oss';
|
||||
|
||||
try {
|
||||
if ($driver === 'oss') {
|
||||
$result = $this->uploadToOss($file, $config['oss'] ?? []);
|
||||
} else {
|
||||
$result = $this->uploadToLocal($file, $config['local'] ?? []);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return error('上传失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
return success($result, '上传成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件(简历等,可包含图片 / PDF / Word)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function file()
|
||||
{
|
||||
$file = Request::file('file');
|
||||
|
||||
if (!$file) {
|
||||
return error('未找到上传文件');
|
||||
}
|
||||
|
||||
// 基本校验:大小 & 类型(放宽为 10MB)
|
||||
$maxSize = 10 * 1024 * 1024; // 10MB
|
||||
$allowExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx'];
|
||||
$extension = strtolower($file->extension());
|
||||
$fileSize = $file->getSize();
|
||||
|
||||
if (!in_array($extension, $allowExts, true)) {
|
||||
return error('不支持的文件类型,仅支持:jpg、jpeg、png、gif、webp、pdf、doc、docx');
|
||||
}
|
||||
|
||||
if ($fileSize > $maxSize) {
|
||||
return error('文件过大,最大支持 10MB');
|
||||
}
|
||||
|
||||
$config = config('upload');
|
||||
$driver = $config['driver'] ?? 'oss';
|
||||
|
||||
try {
|
||||
if ($driver === 'oss') {
|
||||
$result = $this->uploadToOss($file, $config['oss'] ?? []);
|
||||
} else {
|
||||
$result = $this->uploadToLocal($file, $config['local'] ?? []);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return error('上传失败:' . $e->getMessage());
|
||||
}
|
||||
|
||||
return success($result, '上传成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地上传
|
||||
*/
|
||||
protected function uploadToLocal($file, array $config): array
|
||||
{
|
||||
$root = $config['root'] ?? (app()->getRootPath() . 'public/uploads');
|
||||
$driver = 'local';
|
||||
|
||||
// 先计算文件哈希,用于去重
|
||||
$hash = md5_file($file->getPathname());
|
||||
$mimeType = $this->getFileMimeSafe($file);
|
||||
$size = $file->getSize();
|
||||
$extension = strtolower($file->extension());
|
||||
|
||||
// 如果已存在相同文件(同一驱动 + hash),直接返回
|
||||
$exists = UploadFile::where(['hash' => $hash, 'driver' => $driver])->find();
|
||||
if ($exists) {
|
||||
return [
|
||||
'path' => $exists->path,
|
||||
'url' => $exists->url,
|
||||
'id' => $exists->id,
|
||||
];
|
||||
}
|
||||
|
||||
// 子目录:按 年/月 分目录,例如 2025/12
|
||||
$year = date('Y');
|
||||
$month = date('m');
|
||||
$subDir = $year . DIRECTORY_SEPARATOR . $month;
|
||||
$dir = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $subDir;
|
||||
|
||||
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
|
||||
throw new \RuntimeException('创建上传目录失败');
|
||||
}
|
||||
|
||||
$filename = uniqid('img_', true) . '.' . $extension;
|
||||
|
||||
// 保存文件
|
||||
$file->move($dir, $filename);
|
||||
|
||||
// web 访问路径使用 / 分隔,例如 uploads/2025/12/xxx.jpg
|
||||
$relativePath = 'uploads/' . $year . '/' . $month . '/' . $filename;
|
||||
$urlPrefix = rtrim($config['url'] ?? '', '/');
|
||||
|
||||
// 如果未配置,使用 API 域名(生产环境:api.737270.com)
|
||||
if (!$urlPrefix) {
|
||||
$apiDomain = env('API_DOMAIN', 'https://api.737270.com');
|
||||
$urlPrefix = rtrim($apiDomain, '/');
|
||||
}
|
||||
|
||||
$url = $urlPrefix . '/' . $relativePath;
|
||||
|
||||
// 记录上传信息
|
||||
$record = new UploadFile();
|
||||
$record->path = $relativePath;
|
||||
$record->url = $url;
|
||||
$record->driver = $driver;
|
||||
$record->hash = $hash;
|
||||
$record->size = $size;
|
||||
$record->mimeType = $mimeType;
|
||||
$record->extension = $extension;
|
||||
$record->save();
|
||||
|
||||
return [
|
||||
'path' => $relativePath,
|
||||
'url' => $url,
|
||||
'id' => $record->id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传到阿里云 OSS
|
||||
* 完全参考 BaseCrawler.php 的实现
|
||||
*/
|
||||
protected function uploadToOss($file, array $config): array
|
||||
{
|
||||
if (!class_exists('\OSS\OssClient')) {
|
||||
throw new \RuntimeException('未安装 Aliyun OSS SDK,请先执行:composer require aliyuncs/oss-sdk-php');
|
||||
}
|
||||
|
||||
// 参考 database.php 的配置读取方式,直接从 config 读取(config 已通过 env() 读取 .env)
|
||||
$accessKeyId = $config['access_key_id'] ?? '';
|
||||
$accessKeySecret = $config['access_key_secret'] ?? '';
|
||||
$endpoint = $config['endpoint'] ?? '';
|
||||
$bucket = $config['bucket'] ?? '';
|
||||
$prefix = trim($config['prefix'] ?? 'mbti', '/');
|
||||
$baseUrl = rtrim($config['url'] ?? '', '/');
|
||||
|
||||
// 如果未配置 OSS_URL,自动使用 OSS 自带域名:https://{bucket}.{endpoint}
|
||||
if (empty($baseUrl) && !empty($bucket) && !empty($endpoint)) {
|
||||
// 移除 endpoint 中的协议前缀(如果有)
|
||||
$endpointClean = preg_replace('#^https?://#', '', $endpoint);
|
||||
$baseUrl = 'https://' . $bucket . '.' . $endpointClean;
|
||||
}
|
||||
|
||||
if (empty($accessKeyId) || empty($accessKeySecret) || empty($endpoint) || empty($bucket)) {
|
||||
throw new \RuntimeException('OSS 配置不完整,请在 .env 文件中配置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_ENDPOINT、OSS_BUCKET(OSS_URL 可选,不配置则使用 OSS 自带域名)');
|
||||
}
|
||||
|
||||
$driver = 'oss';
|
||||
$extension = strtolower($file->extension());
|
||||
$hash = md5_file($file->getPathname());
|
||||
$size = $file->getSize();
|
||||
$mimeType = $this->getFileMimeSafe($file);
|
||||
|
||||
// 先查重(完全参考 BaseCrawler.php)
|
||||
try {
|
||||
$exists = UploadFile::where(['hash' => $hash, 'driver' => $driver])->find();
|
||||
if ($exists) {
|
||||
$tempFilePath = $file->getPathname();
|
||||
if (file_exists($tempFilePath)) {
|
||||
@unlink($tempFilePath);
|
||||
}
|
||||
$latestUrl = $baseUrl . '/' . ltrim($exists->path, '/');
|
||||
if ($exists->url !== $latestUrl) {
|
||||
$exists->url = $latestUrl;
|
||||
$exists->save();
|
||||
}
|
||||
return [
|
||||
'path' => $exists->path,
|
||||
'url' => $latestUrl,
|
||||
'id' => $exists->id,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 查重失败不影响上传流程
|
||||
}
|
||||
|
||||
$object = $prefix . '/' . date('Ymd') . '/' . uniqid('img_', true) . '.' . $extension;
|
||||
|
||||
// 保存并临时清除代理设置(完全参考 BaseCrawler.php)
|
||||
// 如果 putenv 函数可用则使用,否则跳过代理处理
|
||||
$putenvAvailable = function_exists('putenv');
|
||||
$originalHttpProxy = false;
|
||||
$originalHttpsProxy = false;
|
||||
$originalHttpProxyVar = false;
|
||||
$originalHttpsProxyVar = false;
|
||||
|
||||
if ($putenvAvailable) {
|
||||
$originalHttpProxy = getenv('HTTP_PROXY');
|
||||
$originalHttpsProxy = getenv('HTTPS_PROXY');
|
||||
$originalHttpProxyVar = getenv('http_proxy');
|
||||
$originalHttpsProxyVar = getenv('https_proxy');
|
||||
|
||||
\putenv('HTTP_PROXY=');
|
||||
\putenv('HTTPS_PROXY=');
|
||||
\putenv('http_proxy=');
|
||||
\putenv('https_proxy=');
|
||||
}
|
||||
|
||||
try {
|
||||
$client = new \OSS\OssClient($accessKeyId, $accessKeySecret, $endpoint);
|
||||
|
||||
if (!$client->doesBucketExist($bucket)) {
|
||||
throw new \RuntimeException("OSS Bucket '{$bucket}' 不存在或无法访问");
|
||||
}
|
||||
|
||||
$client->uploadFile($bucket, $object, $file->getPathname());
|
||||
} finally {
|
||||
// 恢复代理设置(完全参考 BaseCrawler.php)
|
||||
if ($putenvAvailable) {
|
||||
if ($originalHttpProxy !== false) {
|
||||
\putenv('HTTP_PROXY=' . $originalHttpProxy);
|
||||
} else {
|
||||
\putenv('HTTP_PROXY');
|
||||
}
|
||||
if ($originalHttpsProxy !== false) {
|
||||
\putenv('HTTPS_PROXY=' . $originalHttpsProxy);
|
||||
} else {
|
||||
\putenv('HTTPS_PROXY');
|
||||
}
|
||||
if ($originalHttpProxyVar !== false) {
|
||||
\putenv('http_proxy=' . $originalHttpProxyVar);
|
||||
} else {
|
||||
\putenv('http_proxy');
|
||||
}
|
||||
if ($originalHttpsProxyVar !== false) {
|
||||
\putenv('https_proxy=' . $originalHttpsProxyVar);
|
||||
} else {
|
||||
\putenv('https_proxy');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 上传成功后,删除本地临时文件(完全参考 BaseCrawler.php)
|
||||
$tempFilePath = $file->getPathname();
|
||||
if (file_exists($tempFilePath)) {
|
||||
@unlink($tempFilePath);
|
||||
}
|
||||
|
||||
// 生成文件访问 URL(完全参考 BaseCrawler.php)
|
||||
$ossUrl = $baseUrl . '/' . ltrim($object, '/');
|
||||
|
||||
// 记录上传信息(完全参考 BaseCrawler.php,try-catch 包裹)
|
||||
try {
|
||||
$record = new UploadFile();
|
||||
$record->path = $object;
|
||||
$record->url = $ossUrl;
|
||||
$record->driver = $driver;
|
||||
$record->hash = $hash;
|
||||
$record->size = $size;
|
||||
$record->mimeType = $mimeType;
|
||||
$record->extension = $extension;
|
||||
$record->save();
|
||||
} catch (\Exception $e) {
|
||||
// 记录失败不影响返回 URL
|
||||
}
|
||||
|
||||
return [
|
||||
'path' => $object,
|
||||
'url' => $ossUrl,
|
||||
'id' => $record->id ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取文件 MIME:服务器未开 fileinfo 扩展时用扩展名推断,避免 finfo_open() 报错
|
||||
*/
|
||||
protected function getFileMimeSafe($file): string
|
||||
{
|
||||
if (function_exists('finfo_open')) {
|
||||
try {
|
||||
return $file->getMime() ?: $this->mimeByExtension($file->extension());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->mimeByExtension($file->extension());
|
||||
}
|
||||
}
|
||||
return $this->mimeByExtension($file->extension());
|
||||
}
|
||||
|
||||
private function mimeByExtension(string $ext): string
|
||||
{
|
||||
$ext = strtolower($ext ?: '');
|
||||
$map = [
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'jfif' => 'image/jpeg',
|
||||
'jpe' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'bmp' => 'image/bmp',
|
||||
'heic' => 'image/heic',
|
||||
'heif' => 'image/heif',
|
||||
'pdf' => 'application/pdf',
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
];
|
||||
return $map[$ext] ?? 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
206
api/app/controller/admin/User.php
Normal file
206
api/app/controller/admin/User.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
namespace app\controller\admin;
|
||||
|
||||
use app\BaseController;
|
||||
use app\model\User as UserModel;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 后台用户管理控制器
|
||||
*/
|
||||
class User extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取用户列表(普通管理员和企业管理员)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->user ?? null;
|
||||
if (!$user) {
|
||||
return error('未登录', 401);
|
||||
}
|
||||
|
||||
$page = Request::param('page', 1);
|
||||
$pageSize = Request::param('pageSize', 10);
|
||||
$keyword = Request::param('keyword', '');
|
||||
$role = Request::param('role', '');
|
||||
$status = Request::param('status', '');
|
||||
|
||||
$where = [];
|
||||
if ($keyword) {
|
||||
$where[] = ['username|email|phone', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
if ($role) {
|
||||
$where['role'] = $role;
|
||||
}
|
||||
if ($status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
|
||||
// 根据角色过滤
|
||||
if (($user['role'] ?? '') === 'enterprise_admin') {
|
||||
// 企业管理员只能查看自己企业的用户
|
||||
$where['enterpriseId'] = $user['enterpriseId'] ?? null;
|
||||
} else {
|
||||
// 普通管理员可以查看所有管理员(不包括超级管理员)
|
||||
$where[] = ['role', 'in', ['admin', 'enterprise_admin']];
|
||||
}
|
||||
|
||||
$list = UserModel::where($where)
|
||||
->order('createdAt', 'desc')
|
||||
->page($page, $pageSize)
|
||||
->select();
|
||||
|
||||
$total = UserModel::where($where)->count();
|
||||
|
||||
return paginate_response($list, $total, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户详情
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id)
|
||||
{
|
||||
$user = UserModel::find($id);
|
||||
if (!$user) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
$userData = $user->toArray();
|
||||
unset($userData['password']);
|
||||
|
||||
return success($userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$data = Request::post();
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (UserModel::where('username', $data['username'])->find()) {
|
||||
return error('用户名已存在');
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (!empty($data['email']) && UserModel::where('email', $data['email'])->find()) {
|
||||
return error('邮箱已被注册');
|
||||
}
|
||||
|
||||
// 验证角色(只允许管理员角色)
|
||||
$allowedRoles = ['admin', 'enterprise_admin', 'superadmin'];
|
||||
$role = $data['role'] ?? 'admin';
|
||||
if (!in_array($role, $allowedRoles)) {
|
||||
return error('角色必须是管理员类型', 400);
|
||||
}
|
||||
|
||||
$user = new UserModel();
|
||||
$user->username = $data['username'];
|
||||
$user->password = $data['password'] ?? '123456'; // 默认密码
|
||||
$user->email = $data['email'] ?? '';
|
||||
$user->phone = $data['phone'] ?? '';
|
||||
$user->role = $role;
|
||||
$user->enterpriseId = $data['enterpriseId'] ?? $data['enterprise_id'] ?? null;
|
||||
$user->status = $data['status'] ?? 1;
|
||||
$user->save();
|
||||
|
||||
$userData = $user->toArray();
|
||||
unset($userData['password']);
|
||||
|
||||
return success($userData, '创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
$user = UserModel::find($id);
|
||||
if (!$user) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
$data = Request::put();
|
||||
|
||||
// 如果更新用户名,检查是否重复
|
||||
if (isset($data['username']) && $data['username'] != $user->username) {
|
||||
if (UserModel::where('username', $data['username'])->find()) {
|
||||
return error('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果更新邮箱,检查是否重复
|
||||
if (isset($data['email']) && $data['email'] != $user->email) {
|
||||
if (!empty($data['email']) && UserModel::where('email', $data['email'])->find()) {
|
||||
return error('邮箱已被注册');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果更新密码
|
||||
if (isset($data['password'])) {
|
||||
$user->password = $data['password']; // 会自动加密
|
||||
}
|
||||
|
||||
$user->save($data);
|
||||
|
||||
$userData = $user->toArray();
|
||||
unset($userData['password']);
|
||||
|
||||
return success($userData, '更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户(软删除)
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$user = UserModel::find($id);
|
||||
if (!$user) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
// 检查是否已删除
|
||||
if ($user->deletedAt) {
|
||||
return error('用户已被删除', 400);
|
||||
}
|
||||
|
||||
// 不能删除自己
|
||||
$currentUser = $this->request->user ?? null;
|
||||
if ($currentUser && ($currentUser['userId'] ?? $currentUser['user_id'] ?? null) == $id) {
|
||||
return error('不能删除自己', 400);
|
||||
}
|
||||
|
||||
// 软删除(设置 deletedAt 时间戳)
|
||||
$user->delete();
|
||||
return success(null, '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用用户
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function toggleStatus($id)
|
||||
{
|
||||
$user = UserModel::find($id);
|
||||
if (!$user) {
|
||||
return error('用户不存在', 404);
|
||||
}
|
||||
|
||||
$user->status = $user->status == 1 ? 0 : 1;
|
||||
$user->save();
|
||||
|
||||
return success($user, '操作成功');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user