Files
datahub/backend/app/Controller/api/v1/DataScopeController.php
T

313 lines
12 KiB
PHP
Raw Normal View History

2026-03-13 09:07:42 +08:00
<?php
declare(strict_types=1);
namespace App\Controller\Api\V1;
use App\Controller\AbstractController;
use App\Middleware\AuthMiddleware;
use App\Middleware\PermissionMiddleware;
use App\Model\Company;
2026-03-17 15:54:53 +08:00
use App\Service\OperationLogService;
2026-03-18 08:49:10 +08:00
use App\Utils\RequestHelper;
2026-03-13 09:07:42 +08:00
use App\Model\Platform;
use App\Model\Store;
use App\Model\User;
use App\Model\UserDataScope;
use App\Service\ScopeBitmapService;
use App\Service\ScopeTableManager;
use Hyperf\DbConnection\Db;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
#[OA\Tag(name: 'DataScope', description: '用户数据范围管理')]
#[Controller(prefix: "/api/v1/users")]
class DataScopeController extends AbstractController
{
public function __construct(
protected readonly ScopeTableManager $scopeTableManager,
protected readonly ScopeBitmapService $bitmapService,
) {
}
/**
* 查看用户数据权限
*
* 返回用户的 scope 列表(含实体名称)和解析后的 store_ids
*
* @param int $id 用户 ID
*/
#[OA\Get(
path: '/users/{id}/data-scope',
summary: '查看用户数据权限',
description: '返回用户的 scope 列表(含实体名称)和解析后的 store_ids',
security: [['bearerAuth' => []]],
tags: ['DataScope'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: '获取成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
new OA\Property(property: 'data', properties: [
new OA\Property(property: 'user_id', type: 'integer', example: 1),
new OA\Property(property: 'role', type: 'string', example: 'admin'),
new OA\Property(
property: 'scopes',
type: 'array',
items: new OA\Items(properties: [
new OA\Property(property: 'scope_type', type: 'string', example: 'company'),
new OA\Property(property: 'scope_id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: '示例公司'),
], type: 'object')
),
new OA\Property(property: 'resolved_store_ids', type: 'array', items: new OA\Items(type: 'integer'), example: [1, 2, 3]),
], type: 'object'),
])
),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "{id}/data-scope", methods: "GET")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function show(int $id): ResponseInterface|array
{
$user = User::query()->with('role')->find($id);
if (!$user) {
return $this->response->json([
'code' => 404,
'message' => '用户不存在',
])->withStatus(404);
}
$scopes = UserDataScope::query()->where('user_id', $id)->get();
// 批量查询实体名称,避免 N+1
$scope_data = $this->enrichScopes($scopes->toArray());
// 解析最终 store_ids
$resolved_store_ids = [];
if ($user->role && $user->role->name !== 'administrator') {
$bitmap = $this->bitmapService->buildForUser($id);
$resolved_store_ids = $this->bitmapService->decode($bitmap);
}
return [
'code' => 0,
'message' => '获取成功',
'data' => [
'user_id' => $id,
'role' => $user->role?->name,
'scopes' => $scope_data,
'resolved_store_ids' => $resolved_store_ids,
],
];
}
/**
* 设置用户数据权限
*
* 全量替换用户的 scope 绑定,并重建 bitmap
*
* @param int $id 用户 ID
*/
#[OA\Put(
path: '/users/{id}/data-scope',
summary: '设置用户数据权限',
description: '全量替换用户的 scope 绑定,并重建 bitmap',
security: [['bearerAuth' => []]],
tags: ['DataScope'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: '用户 ID', schema: new OA\Schema(type: 'integer')),
],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['scopes'],
properties: [
new OA\Property(
property: 'scopes',
type: 'array',
items: new OA\Items(
required: ['scope_type', 'scope_id'],
properties: [
new OA\Property(property: 'scope_type', type: 'string', enum: ['company', 'platform', 'store'], example: 'company'),
new OA\Property(property: 'scope_id', type: 'integer', example: 1),
],
type: 'object'
)
),
]
)
),
responses: [
new OA\Response(
response: 200,
description: '数据权限更新成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '数据权限更新成功'),
])
),
new OA\Response(response: 400, description: '参数校验失败(无效 scope_type 等)', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 404, description: '用户不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "{id}/data-scope", methods: "PUT")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
public function update(int $id): ResponseInterface|array
{
$user = User::query()->with('role')->find($id);
if (!$user) {
return $this->response->json([
'code' => 404,
'message' => '用户不存在',
])->withStatus(404);
}
$body = $this->request->getParsedBody();
if (!array_key_exists('scopes', $body)) {
return $this->response->json([
'code' => 400,
'message' => '缺少 scopes 参数',
])->withStatus(400);
}
$scopes = $body['scopes'];
if (!is_array($scopes)) {
return $this->response->json([
'code' => 400,
'message' => 'scopes 必须为数组',
])->withStatus(400);
}
// 校验每条 scope
$valid_scope_types = ['company', 'platform', 'store'];
$records = [];
foreach ($scopes as $item) {
if (!is_array($item) || !isset($item['scope_type']) || !isset($item['scope_id'])) {
return $this->response->json([
'code' => 400,
'message' => '每条 scope 必须包含 scope_type 和 scope_id',
])->withStatus(400);
}
if (!in_array($item['scope_type'], $valid_scope_types, true)) {
return $this->response->json([
'code' => 400,
'message' => "scope_type 必须为 company、platform 或 store",
])->withStatus(400);
}
$records[] = [
'user_id' => $id,
'scope_type' => $item['scope_type'],
'scope_id' => (int) $item['scope_id'],
'created_at' => date('Y-m-d H:i:s'),
];
}
// 事务内全量替换
Db::beginTransaction();
try {
UserDataScope::query()->where('user_id', $id)->delete();
if (!empty($records)) {
UserDataScope::query()->insert($records);
}
Db::commit();
} catch (\Throwable $e) {
Db::rollBack();
throw $e;
}
// 重建 bitmap 并更新 Swoole\Table
$this->scopeTableManager->rebuildUserScope($id);
2026-03-17 15:54:53 +08:00
OperationLogService::log(
2026-03-18 08:37:14 +08:00
user_id: OperationLogService::getCurrentUserId(),
2026-03-17 15:54:53 +08:00
action: 'scope.update',
target_type: 'user',
target_id: $id,
description: "更新用户 #{$id} 数据权限",
detail: ['scopes' => $scopes],
2026-03-18 08:49:10 +08:00
ip: RequestHelper::getClientIp($this->request),
2026-03-17 15:54:53 +08:00
);
2026-03-13 09:07:42 +08:00
return [
'code' => 0,
'message' => '数据权限更新成功',
];
}
/**
* 批量查询 scope 关联的实体名称
*
* @param array $scopes scope 记录数组
* @return array 带名称的 scope 列表
*/
protected function enrichScopes(array $scopes): array
{
if (empty($scopes)) {
return [];
}
// 按 scope_type 分组收集 IDs
$company_ids = [];
$platform_ids = [];
$store_ids = [];
foreach ($scopes as $scope) {
match ($scope['scope_type']) {
'company' => $company_ids[] = $scope['scope_id'],
'platform' => $platform_ids[] = $scope['scope_id'],
'store' => $store_ids[] = $scope['scope_id'],
2026-03-13 09:15:52 +08:00
default => null,
2026-03-13 09:07:42 +08:00
};
}
// 批量查询名称
$company_names = !empty($company_ids)
? Company::query()->whereIn('id', $company_ids)->pluck('label', 'id')->toArray()
: [];
$platform_names = !empty($platform_ids)
? Platform::query()->whereIn('id', $platform_ids)->pluck('id', 'id')->toArray()
: [];
$store_names = !empty($store_ids)
? Store::query()->whereIn('id', $store_ids)->pluck('label', 'id')->toArray()
: [];
// 组装结果
return array_map(function (array $scope) use ($company_names, $platform_names, $store_names): array {
$name = match ($scope['scope_type']) {
'company' => $company_names[$scope['scope_id']] ?? null,
'platform' => isset($platform_names[$scope['scope_id']]) ? "Platform #{$scope['scope_id']}" : null,
'store' => $store_names[$scope['scope_id']] ?? null,
2026-03-13 09:15:52 +08:00
default => null,
2026-03-13 09:07:42 +08:00
};
return [
'scope_type' => $scope['scope_type'],
'scope_id' => $scope['scope_id'],
'name' => $name,
];
}, $scopes);
}
}