299 lines
11 KiB
PHP
299 lines
11 KiB
PHP
|
|
<?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;
|
||
|
|
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);
|
||
|
|
|
||
|
|
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'],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// 批量查询名称
|
||
|
|
$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,
|
||
|
|
};
|
||
|
|
|
||
|
|
return [
|
||
|
|
'scope_type' => $scope['scope_type'],
|
||
|
|
'scope_id' => $scope['scope_id'],
|
||
|
|
'name' => $name,
|
||
|
|
];
|
||
|
|
}, $scopes);
|
||
|
|
}
|
||
|
|
}
|