Files
docs/thinkphp/app/service/DataIsolation.php
cenzuhai 3a04eede84 feat: 初始化进销存数据开放平台项目
初始化完整的前后端项目结构,包含ThinkPHP后端API服务和Next.js前端管理系统,添加基础配置、中间件、模型、路由等核心代码,以及项目文档和静态资源。
2026-07-08 17:48:00 +08:00

169 lines
5.6 KiB
PHP

<?php
namespace app\service;
use app\model\Permission;
use think\facade\Log;
/**
* 数据隔离服务
*/
class DataIsolation
{
/**
* 构建数据隔离WHERE子句
*
* @param Permission $permission 权限记录
* @param string $endpoint 接口标识 chuku/ruku
* @param string|null $customerName 客户名称(全量数据权限时可选)
* @return array ['sql' => string, 'params' => array]
* @throws \Exception
*/
public static function buildIsolationClause($permission, $endpoint, $customerName = null)
{
// 1. 检查接口权限
if ($endpoint === 'chuku' && !$permission->canAccess('chuku')) {
throw new \Exception('该 API Key 未被授权访问销售数据接口');
}
if ($endpoint === 'ruku' && !$permission->canAccess('ruku')) {
throw new \Exception('该 API Key 未被授权访问入库数据接口');
}
// 2. 全量数据权限:检查是否传入供方名称过滤
if ($permission->isFullData()) {
Log::debug("API Key {$permission->api_key_id} 拥有全量数据权限");
if (!empty($customerName)) {
$filterField = 'k.[供方全称]';
$sqlFragment = " AND {$filterField} LIKE :supplier_name";
$params = ['supplier_name' => '%' . $customerName . '%'];
Log::debug("全量数据模式下按供方名称过滤: {$filterField} LIKE %{$customerName}%");
return ['sql' => $sqlFragment, 'params' => $params];
}
return ['sql' => '', 'params' => []];
}
// 3. 根据数据模式确定过滤方式
$dataMode = $permission->data_mode;
if ($dataMode === Permission::MODE_FULL) {
return ['sql' => '', 'params' => []];
}
// 4. 使用数据库中已配置的过滤规则
if (!empty($permission->filter_field) && !empty($permission->filter_values)) {
return self::buildCustomFilter($permission, $endpoint);
}
// 5. 自动过滤:根据接口类型自动选择过滤字段和值
$apiKey = $permission->apiKey;
$client = $apiKey->client;
if (!$client) {
throw new \Exception('API Key 未关联客户');
}
return self::buildAutoFilter($permission, $endpoint, $client);
}
/**
* 构建自定义过滤规则
*/
private static function buildCustomFilter($permission, $endpoint)
{
$filterField = $permission->filter_field;
$filterValues = $permission->filter_values;
if (empty($filterField) || empty($filterValues)) {
Log::warning("API Key {$permission->api_key_id} 使用自定义模式,但未配置过滤规则,拒绝访问");
throw new \Exception('权限配置不完整:需要手动配置过滤字段和过滤值');
}
if (!is_array($filterValues)) {
$filterValues = json_decode($filterValues, true);
}
if ($filterValues === null || !is_array($filterValues) || empty($filterValues)) {
Log::warning("API Key {$permission->api_key_id} 过滤值为空或无效");
throw new \Exception('过滤值列表为空或无效,请联系管理员');
}
if (!self::validateFilterField($filterField, $endpoint)) {
throw new \Exception('非法的过滤字段');
}
$prefixedField = self::prefixFilterField($filterField, $endpoint);
$placeholders = [];
$params = [];
foreach ($filterValues as $index => $value) {
$placeholder = ':iso_' . $index;
$placeholders[] = $placeholder;
$params['iso_' . $index] = $value;
}
$sqlFragment = " AND {$prefixedField} IN (" . implode(',', $placeholders) . ')';
Log::debug("数据隔离: {$prefixedField} IN " . json_encode($filterValues));
return ['sql' => $sqlFragment, 'params' => $params];
}
/**
* 为过滤字段添加表别名前缀
*/
private static function prefixFilterField($field, $endpoint)
{
if (strpos($field, '[') === 0) {
$field = substr($field, 1, -1);
}
$prefixMap = [
'供方全称' => 'k.[供方全称]',
'供方序号' => 'k.[供方序号]',
'产品名称' => 'p.[产品名称]',
'产品序号' => 'p.[产品序号]',
];
if ($endpoint === 'chuku') {
$prefixMap['销方全称'] = 'f.[销方全称]';
$prefixMap['销方序号'] = 'f.[销方序号]';
}
return $prefixMap[$field] ?? "[{$field}]";
}
/**
* 构建自动过滤规则
*/
private static function buildAutoFilter($permission, $endpoint, $client)
{
$filterField = 'k.[供方全称]';
$filterValue = $client->name;
$sqlFragment = " AND {$filterField} = :iso_0";
$params = ['iso_0' => $filterValue];
Log::debug("数据隔离: {$filterField} = {$filterValue}");
return ['sql' => $sqlFragment, 'params' => $params];
}
/**
* 验证过滤字段是否在白名单内(防注入)
*/
private static function validateFilterField($field, $endpoint)
{
$allowedFields = [
'供方全称', '供方序号',
'产品名称', '产品序号',
'customer_name', 'supplier_name', 'customer_code', 'supplier_code',
];
if ($endpoint === 'chuku') {
$allowedFields = array_merge($allowedFields, ['销方全称', '销方序号']);
}
return in_array($field, $allowedFields);
}
}