数据同步

This commit is contained in:
乘风
2026-01-05 10:20:08 +08:00
parent f1056bab01
commit 1c79e15f85
207 changed files with 52458 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
<?php
namespace app\controller;
use app\repository\ConsumptionRecordRepository;
use app\repository\UserProfileRepository;
use app\service\ConsumptionService;
use app\utils\ApiResponseHelper;
use app\utils\LoggerHelper;
use support\Request;
use support\Response;
class ConsumptionController
{
/**
* 创建消费记录
*
* POST /api/consumption/record
*/
public function store(Request $request): Response
{
$startTime = microtime(true);
try {
// 记录请求日志
LoggerHelper::logRequest('POST', '/api/consumption/record', [
'ip' => $request->getRealIp(),
'user_agent' => $request->header('user-agent'),
]);
// 获取 JSON 请求体
$rawBody = $request->rawBody();
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$payload = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ApiResponseHelper::error('JSON 格式错误: ' . json_last_error_msg(), 400);
}
// 验证用户标识:必须提供 user_id、phone_number 或 id_card 之一
// 注意如果手机号和身份证号都为空但提供了user_id仍然可以处理
$phoneNumber = trim($payload['phone_number'] ?? '');
$idCard = trim($payload['id_card'] ?? '');
if (empty($payload['user_id']) && empty($phoneNumber) && empty($idCard)) {
throw new \InvalidArgumentException('缺少用户标识:必须提供 user_id、phone_number 或 id_card 之一');
}
// 简单手动组装 Service 依赖,后续可接入容器配置
$tagService = new \app\service\TagService(
new \app\repository\TagDefinitionRepository(),
new \app\repository\UserProfileRepository(),
new \app\repository\UserTagRepository(),
new \app\repository\TagHistoryRepository(),
new \app\service\TagRuleEngine\SimpleRuleEngine()
);
$identifierService = new \app\service\IdentifierService(
new UserProfileRepository(),
new \app\service\UserPhoneService(
new \app\repository\UserPhoneRelationRepository()
)
);
$service = new ConsumptionService(
new ConsumptionRecordRepository(),
new UserProfileRepository(),
$identifierService,
$tagService
);
$result = $service->createRecord($payload);
// 如果返回 null说明手机号和身份证号都为空跳过该记录
if ($result === null) {
LoggerHelper::logBusiness('consumption_record_skipped_no_identifier', [
'reason' => 'phone_number and id_card are both empty',
'consume_time' => $payload['consume_time'] ?? null,
]);
return ApiResponseHelper::error('记录已跳过:手机号和身份证号都为空', 400, 400);
}
// 记录业务日志
LoggerHelper::logBusiness('consumption_record_created', [
'user_id' => $result['user_id'] ?? null,
'record_id' => $result['record_id'] ?? null,
'amount' => $payload['amount'] ?? null,
'phone_number' => $payload['phone_number'] ?? null,
'id_card_provided' => !empty($payload['id_card']),
]);
$duration = microtime(true) - $startTime;
LoggerHelper::logPerformance('consumption_record_create', $duration, [
'user_id' => $result['user_id'] ?? null,
]);
return ApiResponseHelper::success($result);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
}

View File

@@ -0,0 +1,850 @@
<?php
namespace app\controller;
use app\service\DataCollectionTaskService;
use app\utils\ApiResponseHelper;
use support\Request;
use support\Response;
/**
* 数据采集任务管理控制器
*
* 提供任务创建、管理、进度查询等接口
*/
class DataCollectionTaskController
{
/**
* 获取任务服务实例
*/
private function getService(): DataCollectionTaskService
{
return new DataCollectionTaskService(
new \app\repository\DataCollectionTaskRepository()
);
}
/**
* 创建采集任务
*
* POST /api/data-collection-tasks
*/
public function create(Request $request): Response
{
try {
$data = $request->post();
// 验证必填字段
$requiredFields = ['name', 'data_source_id', 'database', 'target_type'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
return ApiResponseHelper::error("缺少必填字段: {$field}", 400);
}
}
// 验证目标类型
if (!in_array($data['target_type'], ['consumption_record', 'generic'])) {
return ApiResponseHelper::error("目标类型必须是 consumption_record 或 generic", 400);
}
// 如果是通用Handler需要目标数据源配置后端会自动处理consumption_record的配置
if ($data['target_type'] === 'generic') {
$genericRequiredFields = ['target_data_source_id', 'target_database', 'target_collection'];
foreach ($genericRequiredFields as $field) {
if (empty($data[$field])) {
return ApiResponseHelper::error("通用Handler缺少必填字段: {$field}", 400);
}
}
}
// 验证模式
if (isset($data['mode']) && !in_array($data['mode'], ['batch', 'realtime'])) {
return ApiResponseHelper::error("模式必须是 batch 或 realtime", 400);
}
// 验证集合配置
if (empty($data['collection']) && empty($data['collections'])) {
return ApiResponseHelper::error("必须指定 collection 或 collections", 400);
}
$service = $this->getService();
$task = $service->createTask($data);
return ApiResponseHelper::success($task, '任务创建成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 更新任务
*
* PUT /api/data-collection-tasks/{task_id}
*/
public function update(Request $request): Response
{
try {
// 从请求路径中解析 task_id
$path = $request->path();
if (preg_match('#/api/data-collection-tasks/([^/]+)#', $path, $matches)) {
$taskId = $matches[1];
} else {
$taskId = $request->get('task_id');
if (!$taskId) {
throw new \InvalidArgumentException('缺少 task_id 参数');
}
}
$data = $request->post();
$service = $this->getService();
$result = $service->updateTask($taskId, $data);
if ($result) {
return ApiResponseHelper::success(null, '任务更新成功');
} else {
return ApiResponseHelper::error('任务更新失败', 500);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 删除任务
*
* DELETE /api/data-collection-tasks/{task_id}
*/
public function delete(Request $request): Response
{
try {
// 从请求路径中解析 task_id
$path = $request->path();
if (preg_match('#/api/data-collection-tasks/([^/]+)#', $path, $matches)) {
$taskId = $matches[1];
} else {
$taskId = $request->get('task_id');
if (!$taskId) {
throw new \InvalidArgumentException('缺少 task_id 参数');
}
}
$service = $this->getService();
$result = $service->deleteTask($taskId);
if ($result) {
return ApiResponseHelper::success(null, '任务删除成功');
} else {
return ApiResponseHelper::error('任务删除失败', 500);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 启动任务
*
* POST /api/data-collection-tasks/{task_id}/start
*/
public function start(Request $request): Response
{
try {
// 从请求路径中解析 task_id
$path = $request->path();
if (preg_match('#/api/data-collection-tasks/([^/]+)/start#', $path, $matches)) {
$taskId = $matches[1];
} else {
$taskId = $request->get('task_id');
if (!$taskId) {
throw new \InvalidArgumentException('缺少 task_id 参数');
}
}
$service = $this->getService();
$result = $service->startTask($taskId);
if ($result) {
return ApiResponseHelper::success(null, '任务启动成功');
} else {
return ApiResponseHelper::error('任务启动失败', 500);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 暂停任务
*
* POST /api/data-collection-tasks/{task_id}/pause
*/
public function pause(Request $request): Response
{
try {
// 从请求路径中解析 task_id
$path = $request->path();
if (preg_match('#/api/data-collection-tasks/([^/]+)/pause#', $path, $matches)) {
$taskId = $matches[1];
} else {
$taskId = $request->get('task_id');
if (!$taskId) {
throw new \InvalidArgumentException('缺少 task_id 参数');
}
}
$service = $this->getService();
$result = $service->pauseTask($taskId);
if ($result) {
return ApiResponseHelper::success(null, '任务暂停成功');
} else {
return ApiResponseHelper::error('任务暂停失败', 500);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 停止任务
*
* POST /api/data-collection-tasks/{task_id}/stop
*/
public function stop(Request $request): Response
{
try {
// 从请求路径中解析 task_id
$path = $request->path();
if (preg_match('#/api/data-collection-tasks/([^/]+)/stop#', $path, $matches)) {
$taskId = $matches[1];
} else {
$taskId = $request->get('task_id');
if (!$taskId) {
throw new \InvalidArgumentException('缺少 task_id 参数');
}
}
$service = $this->getService();
$result = $service->stopTask($taskId);
if ($result) {
return ApiResponseHelper::success(null, '任务停止成功');
} else {
return ApiResponseHelper::error('任务停止失败', 500);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取任务列表
*
* GET /api/data-collection-tasks
*/
public function list(Request $request): Response
{
try {
// 只收集非空的筛选条件
$filters = [];
if ($request->get('status') !== null && $request->get('status') !== '') {
$filters['status'] = $request->get('status');
}
if ($request->get('data_source_id') !== null && $request->get('data_source_id') !== '') {
$filters['data_source_id'] = $request->get('data_source_id');
}
if ($request->get('name') !== null && $request->get('name') !== '') {
$filters['name'] = $request->get('name');
}
$page = (int)($request->get('page', 1));
$pageSize = (int)($request->get('page_size', 20));
$service = $this->getService();
$result = $service->getTaskList($filters, $page, $pageSize);
return ApiResponseHelper::success($result, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取任务详情
*
* GET /api/data-collection-tasks/{task_id}
*/
public function detail(Request $request): Response
{
try {
// 从请求路径中解析 task_id
$path = $request->path();
if (preg_match('#/api/data-collection-tasks/([^/]+)$#', $path, $matches)) {
$taskId = $matches[1];
} else {
$taskId = $request->get('task_id');
if (!$taskId) {
throw new \InvalidArgumentException('缺少 task_id 参数');
}
}
$service = $this->getService();
$task = $service->getTask($taskId);
if ($task === null) {
return ApiResponseHelper::error('任务不存在', 404);
}
return ApiResponseHelper::success($task, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取任务进度
*
* GET /api/data-collection-tasks/{task_id}/progress
*/
public function progress(Request $request): Response
{
try {
// 从请求路径中解析 task_id
$path = $request->path();
if (preg_match('#/api/data-collection-tasks/([^/]+)/progress#', $path, $matches)) {
$taskId = $matches[1];
} else {
$taskId = $request->get('task_id');
if (!$taskId) {
throw new \InvalidArgumentException('缺少 task_id 参数');
}
}
$service = $this->getService();
$task = $service->getTask($taskId);
if ($task === null) {
return ApiResponseHelper::error('任务不存在', 404);
}
$progress = $task['progress'] ?? [];
return ApiResponseHelper::success($progress, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取数据源列表
*
* GET /api/data-collection-tasks/data-sources
*/
public function getDataSources(Request $request): Response
{
try {
// 优先使用数据库中的数据源,如果没有则使用配置文件
$service = new \app\service\DataSourceService(new \app\repository\DataSourceRepository());
$result = $service->getDataSourceList(['status' => 1]);
if (!empty($result['list'])) {
// 使用数据库中的数据源
$list = array_map(function ($ds) {
return [
'id' => $ds['data_source_id'],
'name' => $ds['name'] ?? $ds['data_source_id'], // 添加名称字段
'type' => $ds['type'] ?? 'unknown',
'host' => $ds['host'] ?? '',
'port' => $ds['port'] ?? 0,
'database' => $ds['database'] ?? '',
];
}, $result['list']);
}
// 注意现在数据源配置统一从数据库读取不再使用config('data_sources')
// 如果数据库中没有数据源,返回空列表
if (!isset($list)) {
$list = [];
}
return ApiResponseHelper::success($list, '查询成功');
} catch (\MongoDB\Driver\Exception\Exception $e) {
// MongoDB 连接错误,返回友好提示
$errorMessage = '无法连接到 MongoDB 数据库,请检查数据库服务是否正常运行。错误详情:' . $e->getMessage();
return ApiResponseHelper::error($errorMessage, 500);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取数据源的数据库列表
*
* GET /api/data-collection-tasks/data-sources/{data_source_id}/databases
*/
public function getDatabases(Request $request, string $data_source_id): Response
{
try {
// 从数据库获取数据源配置
$service = new \app\service\DataSourceService(new \app\repository\DataSourceRepository());
$dataSourceConfig = $service->getDataSourceConfig($data_source_id);
if (!$dataSourceConfig) {
return ApiResponseHelper::error('数据源不存在', 404);
}
$dataSource = $dataSourceConfig;
// 如果是MongoDB连接并获取数据库列表
if ($dataSource['type'] === 'mongodb') {
$client = $this->getMongoClient($dataSource);
$databases = $client->listDatabases();
$list = [];
foreach ($databases as $database) {
$dbName = $database->getName();
// 同时返回原始名称和base64编码的IDURL友好
$list[] = [
'name' => $dbName,
'id' => base64_encode($dbName), // URL友好的标识符
];
}
return ApiResponseHelper::success($list, '查询成功');
}
return ApiResponseHelper::error('不支持的数据源类型', 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取数据库的集合列表
*
* GET /api/data-collection-tasks/data-sources/{data_source_id}/databases/{database}/collections
*/
public function getCollections(Request $request, string $data_source_id, string $database): Response
{
try {
// 解码数据库名称支持base64编码和URL编码
$database = $this->decodeName($database);
// 从数据库获取数据源配置
$service = new \app\service\DataSourceService(new \app\repository\DataSourceRepository());
$dataSourceConfig = $service->getDataSourceConfig($data_source_id);
if (!$dataSourceConfig) {
return ApiResponseHelper::error('数据源不存在', 404);
}
$dataSource = $dataSourceConfig;
// 如果是MongoDB连接并获取集合列表
if ($dataSource['type'] === 'mongodb') {
$client = $this->getMongoClient($dataSource);
$db = $client->selectDatabase($database);
$collections = $db->listCollections();
$list = [];
foreach ($collections as $collection) {
$collName = $collection->getName();
// 同时返回原始名称和base64编码的IDURL友好
$list[] = [
'name' => $collName,
'id' => base64_encode($collName), // URL友好的标识符
];
}
return ApiResponseHelper::success($list, '查询成功');
}
return ApiResponseHelper::error('不支持的数据源类型', 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取Handler的目标字段列表
*
* GET /api/data-collection-tasks/handlers/{handler_type}/target-fields
*/
public function getHandlerTargetFields(Request $request, string $handler_type): Response
{
try {
$fields = [];
switch ($handler_type) {
case 'consumption_record':
// 消费记录Handler的目标字段列表
// 包含原始输入字段(推荐)和转换后字段(可选)
// Handler会自动进行转换phone_number/id_card -> user_id, store_name -> store_id
$fields = [
// 用户标识字段(原始输入,推荐使用)
['name' => 'phone_number', 'label' => '手机号', 'type' => 'string', 'required' => false, 'description' => '手机号Handler会自动解析为user_id', 'is_original' => true],
['name' => 'id_card', 'label' => '身份证', 'type' => 'string', 'required' => false, 'description' => '身份证号Handler会自动解析为user_id', 'is_original' => true],
// 用户ID转换后字段由Handler自动生成不需要映射
['name' => 'user_id', 'label' => '用户ID', 'type' => 'string', 'required' => false, 'description' => '用户ID由Handler通过phone_number/id_card自动解析生成无需映射', 'is_original' => false, 'no_mapping' => true],
// 门店标识字段(原始输入,推荐使用)
['name' => 'store_name', 'label' => '门店名称', 'type' => 'string', 'required' => false, 'description' => '门店名称Handler会自动转换为store_id', 'is_original' => true],
// 门店ID转换后字段由Handler自动生成不需要映射
['name' => 'store_id', 'label' => '门店ID', 'type' => 'string', 'required' => false, 'description' => '门店ID由Handler通过store_name自动转换生成无需映射', 'is_original' => false, 'no_mapping' => true],
// 订单标识字段(用于去重)
['name' => 'source_order_id', 'label' => '原始订单ID', 'type' => 'string', 'required' => false, 'description' => '原始订单ID配合店铺名称做去重唯一标识建议配置', 'is_original' => true],
// 注意order_no 由系统自动生成(自动递增),不需要映射
// 金额和时间字段(直接字段)
['name' => 'amount', 'label' => '消费金额', 'type' => 'float', 'required' => true, 'description' => '消费金额(必填)', 'is_original' => true],
['name' => 'actual_amount', 'label' => '实际金额', 'type' => 'float', 'required' => true, 'description' => '实际支付金额(必填)', 'is_original' => true],
['name' => 'consume_time', 'label' => '消费时间', 'type' => 'datetime', 'required' => true, 'description' => '消费时间,用于时间分片存储(必填)', 'is_original' => true],
// 其他可选字段
['name' => 'currency', 'label' => '币种', 'type' => 'string', 'required' => false, 'description' => '币种默认CNY人民币', 'is_original' => true, 'fixed_options' => true, 'options' => [['value' => 'CNY', 'label' => '人民币(CNY)'], ['value' => 'USD', 'label' => '美元(USD)']], 'default_value' => 'CNY'],
['name' => 'status', 'label' => '记录状态', 'type' => 'int', 'required' => false, 'description' => '记录状态0-正常1-异常2-已删除。默认0。需要配置源状态值到标准状态值的映射', 'is_original' => true, 'value_mapping' => true, 'target_values' => [['value' => 0, 'label' => '正常(0)'], ['value' => 1, 'label' => '异常(1)'], ['value' => 2, 'label' => '已删除(2)']], 'default_value' => 0],
];
break;
case 'generic':
// 通用Handler - 没有固定的字段列表,由用户自定义
$fields = [];
break;
default:
return ApiResponseHelper::error("未知的Handler类型: {$handler_type}", 400);
}
return ApiResponseHelper::success($fields, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取集合的字段列表(采样)
*
* GET /api/data-collection-tasks/data-sources/{data_source_id}/databases/{database}/collections/{collection}/fields
*/
public function getFields(Request $request, string $data_source_id, string $database, string $collection): Response
{
try {
// 解码数据库名称和集合名称支持base64编码和URL编码
$database = $this->decodeName($database);
$collection = $this->decodeName($collection);
// 从数据库获取数据源配置
$service = new \app\service\DataSourceService(new \app\repository\DataSourceRepository());
$dataSourceConfig = $service->getDataSourceConfig($data_source_id);
if (!$dataSourceConfig) {
return ApiResponseHelper::error('数据源不存在', 404);
}
$dataSource = $dataSourceConfig;
// 如果是MongoDB采样获取字段
if ($dataSource['type'] === 'mongodb') {
$client = $this->getMongoClient($dataSource);
$db = $client->selectDatabase($database);
$coll = $db->selectCollection($collection);
// 采样一条数据
$sample = $coll->findOne([]);
if ($sample) {
$fields = [];
$this->extractFields($sample, '', $fields);
return ApiResponseHelper::success($fields, '查询成功');
} else {
return ApiResponseHelper::success([], '集合为空,无法获取字段');
}
}
return ApiResponseHelper::error('不支持的数据源类型', 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 递归提取字段
*/
private function extractFields($data, string $prefix, array &$fields): void
{
if (is_array($data) || is_object($data)) {
foreach ($data as $key => $value) {
$fieldName = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value) || is_object($value)) {
if (empty($value)) {
$fields[] = [
'name' => $fieldName,
'type' => 'array',
];
} else {
$this->extractFields($value, $fieldName, $fields);
}
} else {
$fields[] = [
'name' => $fieldName,
'type' => gettype($value),
];
}
}
}
}
/**
* 预览查询结果包含lookup
*
* POST /api/data-collection-tasks/preview-query
*/
public function previewQuery(Request $request): Response
{
try {
$data = $request->post();
$dataSourceId = $data['data_source_id'] ?? '';
$database = $data['database'] ?? '';
$collection = $data['collection'] ?? '';
$lookups = $data['lookups'] ?? [];
$filterConditions = $data['filter_conditions'] ?? [];
$limit = (int)($data['limit'] ?? 5); // 默认预览5条
if (empty($dataSourceId) || empty($database) || empty($collection)) {
return ApiResponseHelper::error('缺少必要参数data_source_id, database, collection', 400);
}
// 获取数据源配置
$service = new \app\service\DataSourceService(new \app\repository\DataSourceRepository());
$dataSourceConfig = $service->getDataSourceConfig($dataSourceId);
if (!$dataSourceConfig) {
return ApiResponseHelper::error('数据源不存在', 404);
}
if ($dataSourceConfig['type'] !== 'mongodb') {
return ApiResponseHelper::error('目前只支持MongoDB数据源预览', 400);
}
// 连接MongoDB
$client = $this->getMongoClient($dataSourceConfig);
$db = $client->selectDatabase($database);
$coll = $db->selectCollection($collection);
// 构建聚合管道
$pipeline = [];
// 1. 添加过滤条件($match- 必须在最前面
$filter = $this->buildFilterForPreview($filterConditions);
if (!empty($filter)) {
$pipeline[] = ['$match' => $filter];
}
// 2. 添加lookup查询
foreach ($lookups as $lookup) {
if (empty($lookup['from']) || empty($lookup['local_field']) || empty($lookup['foreign_field'])) {
continue;
}
$lookupStage = [
'$lookup' => [
'from' => $lookup['from'],
'localField' => $lookup['local_field'],
'foreignField' => $lookup['foreign_field'],
'as' => $lookup['as'] ?? 'joined'
]
];
$pipeline[] = $lookupStage;
// 如果配置了解构
if (!empty($lookup['unwrap'])) {
$pipeline[] = [
'$unwind' => [
'path' => '$' . ($lookup['as'] ?? 'joined'),
'preserveNullAndEmptyArrays' => !empty($lookup['preserve_null'])
]
];
}
}
// 3. 限制返回数量
$pipeline[] = ['$limit' => $limit];
// 执行聚合查询
$cursor = $coll->aggregate($pipeline);
$results = [];
$fields = [];
foreach ($cursor as $doc) {
$docArray = $this->convertMongoDocumentToArray($doc);
$results[] = $docArray;
// 提取字段
$this->extractFields($docArray, '', $fields);
}
// 去重字段
$uniqueFields = [];
$fieldMap = [];
foreach ($fields as $field) {
if (!isset($fieldMap[$field['name']])) {
$fieldMap[$field['name']] = true;
$uniqueFields[] = $field;
}
}
return ApiResponseHelper::success([
'fields' => $uniqueFields,
'data' => $results,
'count' => count($results)
], '预览成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 将MongoDB文档转换为数组
*/
private function convertMongoDocumentToArray($document): array
{
if (is_array($document)) {
return $document;
}
if (is_object($document)) {
$array = [];
foreach ($document as $key => $value) {
if ($value instanceof \MongoDB\BSON\UTCDateTime) {
$array[$key] = $value->toDateTime()->format('Y-m-d H:i:s');
} elseif (is_object($value) && method_exists($value, '__toString')) {
$array[$key] = (string)$value;
} elseif (is_array($value) || is_object($value)) {
$array[$key] = $this->convertMongoDocumentToArray($value);
} else {
$array[$key] = $value;
}
}
return $array;
}
return [];
}
/**
* 构建过滤条件(用于预览查询)
*
* @param array $filterConditions 过滤条件列表
* @return array MongoDB查询过滤器
*/
private function buildFilterForPreview(array $filterConditions): array
{
$filter = [];
foreach ($filterConditions as $condition) {
$field = $condition['field'] ?? '';
$operator = $condition['operator'] ?? 'eq';
$value = $condition['value'] ?? null;
if (empty($field)) {
continue;
}
// 处理值的类型转换
if ($value !== null && $value !== '') {
// 尝试转换为数字(如果是数字字符串)
if (is_numeric($value)) {
// 判断是整数还是浮点数
if (strpos($value, '.') !== false) {
$value = (float)$value;
} else {
$value = (int)$value;
}
}
}
switch ($operator) {
case 'eq':
$filter[$field] = $value;
break;
case 'ne':
$filter[$field] = ['$ne' => $value];
break;
case 'gt':
$filter[$field] = ['$gt' => $value];
break;
case 'gte':
$filter[$field] = ['$gte' => $value];
break;
case 'lt':
$filter[$field] = ['$lt' => $value];
break;
case 'lte':
$filter[$field] = ['$lte' => $value];
break;
case 'in':
// in操作符的值应该是数组
$valueArray = is_array($value) ? $value : explode(',', (string)$value);
$filter[$field] = ['$in' => $valueArray];
break;
case 'nin':
// nin操作符的值应该是数组
$valueArray = is_array($value) ? $value : explode(',', (string)$value);
$filter[$field] = ['$nin' => $valueArray];
break;
}
}
return $filter;
}
/**
* 解码数据库或集合名称支持base64编码和URL编码
*
* @param string $name 编码后的名称
* @return string 解码后的名称
*/
private function decodeName(string $name): string
{
// 尝试base64解码如果前端使用的是编码后的ID
// 检查是否可能是base64编码只包含base64字符且长度合理
if (preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $name) && strlen($name) > 0) {
$decoded = @base64_decode($name, true);
if ($decoded !== false && $decoded !== '') {
// 解码成功,使用解码后的值
return $decoded;
}
}
// 不是base64格式或解码失败使用URL解码处理中文等特殊字符
return rawurldecode($name);
}
/**
* 获取MongoDB客户端
*/
private function getMongoClient(array $config): \MongoDB\Client
{
$host = $config['host'] ?? '';
$port = (int)($config['port'] ?? 27017);
$username = $config['username'] ?? '';
$password = $config['password'] ?? '';
$authSource = $config['auth_source'] ?? 'admin';
if (!empty($username) && !empty($password)) {
$dsn = "mongodb://{$username}:{$password}@{$host}:{$port}/{$authSource}";
} else {
$dsn = "mongodb://{$host}:{$port}";
}
return new \MongoDB\Client($dsn, $config['options'] ?? []);
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace app\controller;
use app\repository\DataSourceRepository;
use app\service\DataSourceService;
use app\utils\ApiResponseHelper;
use support\Request;
use support\Response;
/**
* 数据源管理控制器
*/
class DataSourceController
{
/**
* 获取数据源服务实例
*/
private function getService(): DataSourceService
{
return new DataSourceService(new DataSourceRepository());
}
/**
* 获取数据源列表
*
* GET /api/data-sources
*/
public function list(Request $request): Response
{
try {
$service = $this->getService();
$filters = [
'type' => $request->get('type'),
'status' => $request->get('status'),
'name' => $request->get('name'),
'page' => $request->get('page', 1),
'page_size' => $request->get('page_size', 20),
];
$result = $service->getDataSourceList($filters);
return ApiResponseHelper::success([
'data_sources' => $result['list'],
'total' => $result['total'],
'page' => (int)$filters['page'],
'page_size' => (int)$filters['page_size'],
], '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取数据源详情
*
* GET /api/data-sources/{data_source_id}
*/
public function detail(Request $request, string $data_source_id): Response
{
try {
$service = $this->getService();
$dataSource = $service->getDataSourceDetail($data_source_id);
if (!$dataSource) {
return ApiResponseHelper::error('数据源不存在', 404);
}
return ApiResponseHelper::success($dataSource, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 创建数据源
*
* POST /api/data-sources
*/
public function create(Request $request): Response
{
try {
$data = $request->post();
$service = $this->getService();
$dataSource = $service->createDataSource($data);
// 不返回密码
$result = $dataSource->toArray();
unset($result['password']);
return ApiResponseHelper::success($result, '创建成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 更新数据源
*
* PUT /api/data-sources/{data_source_id}
*/
public function update(Request $request, string $data_source_id): Response
{
try {
$data = $request->post();
$service = $this->getService();
$result = $service->updateDataSource($data_source_id, $data);
if ($result) {
return ApiResponseHelper::success(null, '更新成功');
} else {
return ApiResponseHelper::error('更新失败', 500);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 删除数据源
*
* DELETE /api/data-sources/{data_source_id}
*/
public function delete(Request $request, string $data_source_id): Response
{
try {
$service = $this->getService();
$result = $service->deleteDataSource($data_source_id);
if ($result) {
return ApiResponseHelper::success(null, '删除成功');
} else {
return ApiResponseHelper::error('删除失败', 500);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 测试数据源连接
*
* POST /api/data-sources/test-connection
*/
public function testConnection(Request $request): Response
{
try {
$data = $request->post();
// 验证必填字段
$requiredFields = ['type', 'host', 'port', 'database'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
return ApiResponseHelper::error("缺少必填字段: {$field}", 400);
}
}
$service = $this->getService();
$connected = $service->testConnection($data);
if ($connected) {
return ApiResponseHelper::success(['connected' => true], '连接成功');
} else {
return ApiResponseHelper::error('连接失败,请检查配置', 400);
}
} catch (\Throwable $e) {
return ApiResponseHelper::error('连接测试失败: ' . $e->getMessage(), 400);
}
}
}

View File

@@ -0,0 +1,455 @@
<?php
namespace app\controller;
use app\service\DatabaseSyncService;
use app\utils\ApiResponseHelper;
use support\Request;
use support\Response;
/**
* 数据库同步控制器
*
* 提供同步进度查询接口
*/
class DatabaseSyncController
{
/**
* 获取同步进度看板页面
*
* GET /database-sync/dashboard
*/
public function dashboard(Request $request): Response
{
$htmlPath = __DIR__ . '/../../public/database-sync-dashboard.html';
if (!file_exists($htmlPath)) {
return response('<h1>404 Not Found</h1><p>看板页面不存在</p>', 404)
->withHeader('Content-Type', 'text/html; charset=utf-8');
}
$html = file_get_contents($htmlPath);
return response($html)->withHeader('Content-Type', 'text/html; charset=utf-8');
}
/**
* 获取同步进度
*
* GET /api/database-sync/progress
*/
public function progress(Request $request): Response
{
try {
// 创建 DatabaseSyncService 实例(传递最小配置,仅用于读取进度)
// 注意:数据库同步功能已迁移到 data_collection_tasks.php这里仅用于查询进度
$minimalConfig = [
'source' => ['host' => '', 'port' => 27017], // 占位符,不会实际连接
'target' => ['host' => '', 'port' => 27017], // 占位符,不会实际连接
'sync' => [],
'monitoring' => [],
];
$syncService = new DatabaseSyncService($minimalConfig);
// 加载最新进度
$syncService->loadProgress();
$progress = $syncService->getProgress();
// 获取多进程状态信息
$workerStatus = $this->getWorkerStatus();
$progress['worker_status'] = $workerStatus;
// 获取数据库连接状态
$connectionStatus = $this->getConnectionStatus();
$progress['connection_status'] = $connectionStatus;
// 获取数据库列表信息(已完成和待同步)
$databaseList = $this->getDatabaseList($syncService);
$progress['database_list'] = $databaseList;
// 检查进度文件最后修改时间
$runtimePath = function_exists('runtime_path') ? runtime_path() : (config('app.runtime_path', base_path() . DIRECTORY_SEPARATOR . 'runtime'));
$progressFile = $runtimePath . DIRECTORY_SEPARATOR . 'database_sync_progress.json';
if (file_exists($progressFile)) {
$fileTime = filemtime($progressFile);
$progress['progress_file_last_modified'] = date('Y-m-d H:i:s', $fileTime);
$progress['progress_file_age_seconds'] = time() - $fileTime;
} else {
$progress['progress_file_last_modified'] = null;
$progress['progress_file_age_seconds'] = null;
}
// 如果状态是idle且没有开始时间尝试检查是否真的在运行
if ($progress['status'] === 'idle' && $progress['time']['start_time'] === null) {
if (!file_exists($progressFile)) {
$progress['hint'] = '请执行: php start.php status 查看 data_sync_scheduler 进程是否运行(数据库同步任务由 data_sync_scheduler 管理)';
}
}
return ApiResponseHelper::success($progress, '同步进度查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取 Worker 进程状态信息
*
* @return array<string, mixed> Worker 状态信息
*/
private function getWorkerStatus(): array
{
$status = [
'total_workers' => 0,
'active_workers' => 0,
'workers' => [],
];
try {
// 从配置中获取 Worker 数量
$processConfig = config('process.data_sync_scheduler', []);
$totalWorkers = (int)($processConfig['count'] ?? 10);
$status['total_workers'] = $totalWorkers;
// 检查进度文件中的 checkpoints推断每个 Worker 处理的数据库
$runtimePath = function_exists('runtime_path') ? runtime_path() : (config('app.runtime_path', base_path() . DIRECTORY_SEPARATOR . 'runtime'));
$progressFile = $runtimePath . DIRECTORY_SEPARATOR . 'database_sync_progress.json';
if (file_exists($progressFile)) {
// 使用文件锁读取,避免并发问题
$fp = fopen($progressFile, 'r');
if ($fp && flock($fp, LOCK_SH)) {
try {
$content = stream_get_contents($fp);
$progressData = json_decode($content, true);
if ($progressData && isset($progressData['checkpoints'])) {
$checkpoints = $progressData['checkpoints'];
$databases = array_keys($checkpoints);
// 根据数据库分配推断每个 Worker 的状态(使用取模分配)
foreach ($databases as $index => $database) {
$workerId = $index % $totalWorkers;
if (!isset($status['workers'][$workerId])) {
$status['workers'][$workerId] = [
'worker_id' => $workerId,
'databases' => [],
'collections' => 0,
'documents_processed' => 0,
'status' => 'active',
];
}
$status['workers'][$workerId]['databases'][] = $database;
// 统计该 Worker 处理的集合和文档数
if (isset($checkpoints[$database]) && is_array($checkpoints[$database])) {
foreach ($checkpoints[$database] as $collection => $checkpoint) {
$status['workers'][$workerId]['collections']++;
if (isset($checkpoint['processed'])) {
$status['workers'][$workerId]['documents_processed'] += (int)$checkpoint['processed'];
}
}
}
}
$status['active_workers'] = count($status['workers']);
// 将 workers 数组转换为索引数组(便于前端遍历)
$status['workers'] = array_values($status['workers']);
}
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
} else {
if ($fp) {
fclose($fp);
}
}
}
// 如果没有活动的 Worker但配置了 Worker 数量,显示所有 Worker等待状态
if ($status['active_workers'] === 0 && $status['total_workers'] > 0) {
// 创建所有 Worker 的占位信息
for ($i = 0; $i < $totalWorkers; $i++) {
$status['workers'][] = [
'worker_id' => $i,
'databases' => [],
'collections' => 0,
'documents_processed' => 0,
'status' => 'waiting', // 等待状态
];
}
$status['message'] = '所有 Worker 处于等待状态,同步尚未开始或进度文件为空';
} elseif ($status['total_workers'] === 0) {
$status['message'] = '未配置 Worker 数量,请检查 config/process.php';
}
} catch (\Throwable $e) {
$status['error'] = '获取 Worker 状态失败: ' . $e->getMessage();
}
return $status;
}
/**
* 获取同步统计信息
*
* GET /api/database-sync/stats
*/
public function stats(Request $request): Response
{
try {
// 创建 DatabaseSyncService 实例(传递最小配置,仅用于读取统计)
$minimalConfig = [
'source' => ['host' => '', 'port' => 27017],
'target' => ['host' => '', 'port' => 27017],
];
$syncService = new DatabaseSyncService($minimalConfig);
$stats = $syncService->getStats();
return ApiResponseHelper::success($stats, '统计信息查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 重置同步进度
*
* POST /api/database-sync/reset
*/
public function reset(Request $request): Response
{
try {
// 创建 DatabaseSyncService 实例(传递最小配置,仅用于重置进度)
$minimalConfig = [
'source' => ['host' => '', 'port' => 27017],
'target' => ['host' => '', 'port' => 27017],
];
$syncService = new DatabaseSyncService($minimalConfig);
$syncService->resetProgress();
return ApiResponseHelper::success(null, '同步进度已重置');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 跳过错误数据库,继续同步
*
* POST /api/database-sync/skip-error
*/
public function skipError(Request $request): Response
{
try {
// 创建 DatabaseSyncService 实例(传递最小配置,仅用于跳过错误)
$minimalConfig = [
'source' => ['host' => '', 'port' => 27017],
'target' => ['host' => '', 'port' => 27017],
];
$syncService = new DatabaseSyncService($minimalConfig);
$syncService->loadProgress();
$skipped = $syncService->skipErrorDatabase();
if ($skipped) {
return ApiResponseHelper::success(null, '已跳过错误数据库,将继续同步下一个数据库');
} else {
return ApiResponseHelper::error('当前没有错误数据库需要跳过', 400);
}
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取数据库连接状态
*
* @return array<string, mixed> 连接状态信息
*/
private function getConnectionStatus(): array
{
$status = [
'source' => [
'connected' => false,
'host' => '',
'port' => 0,
'error' => null,
],
'target' => [
'connected' => false,
'host' => '',
'port' => 0,
'error' => null,
],
];
try {
// 从数据库获取数据源配置
$dataSourceService = new \app\service\DataSourceService(new \app\repository\DataSourceRepository());
// 检查源数据库连接
$sourceDataSourceId = 'kr_mongodb'; // 默认源数据库ID可以从任务配置中获取
$sourceConfig = $dataSourceService->getDataSourceConfigById($sourceDataSourceId);
if ($sourceConfig) {
$status['source']['host'] = $sourceConfig['host'] ?? '';
$status['source']['port'] = (int)($sourceConfig['port'] ?? 27017);
// 尝试连接源数据库
try {
$sourceDsn = $this->buildDsn($sourceConfig);
$sourceClient = new \MongoDB\Client($sourceDsn, $sourceConfig['options'] ?? []);
// 执行一个简单的命令来测试连接
$sourceClient->selectDatabase('admin')->command(['ping' => 1]);
$status['source']['connected'] = true;
} catch (\Throwable $e) {
$status['source']['connected'] = false;
$status['source']['error'] = $e->getMessage();
}
} else {
$status['source']['error'] = '源数据库配置不存在';
}
// 检查目标数据库连接
$targetDataSourceId = 'sync_mongodb'; // 默认目标数据库ID可以从任务配置中获取
$targetConfig = $dataSourceService->getDataSourceConfigById($targetDataSourceId);
if ($targetConfig) {
$status['target']['host'] = $targetConfig['host'] ?? '';
$status['target']['port'] = (int)($targetConfig['port'] ?? 27017);
// 尝试连接目标数据库
try {
$targetDsn = $this->buildDsn($targetConfig);
$targetClient = new \MongoDB\Client($targetDsn, $targetConfig['options'] ?? []);
// 执行一个简单的命令来测试连接
$targetClient->selectDatabase('admin')->command(['ping' => 1]);
$status['target']['connected'] = true;
} catch (\Throwable $e) {
$status['target']['connected'] = false;
$status['target']['error'] = $e->getMessage();
}
} else {
$status['target']['error'] = '目标数据库配置不存在';
}
} catch (\Throwable $e) {
$status['error'] = '检查连接状态失败: ' . $e->getMessage();
}
return $status;
}
/**
* 构建 MongoDB DSN
*
* @param array<string, mixed> $config 数据库配置
* @return string DSN 字符串
*/
private function buildDsn(array $config): string
{
$host = $config['host'] ?? '';
$port = (int)($config['port'] ?? 27017);
$dsn = 'mongodb://';
if (!empty($config['username']) && !empty($config['password'])) {
$dsn .= urlencode($config['username']) . ':' . urlencode($config['password']) . '@';
}
$dsn .= $host . ':' . $port;
if (!empty($config['auth_source'])) {
$dsn .= '/?authSource=' . urlencode($config['auth_source']);
}
return $dsn;
}
/**
* 获取数据库列表信息(已完成和待同步)
*
* @param DatabaseSyncService $syncService 同步服务实例
* @return array<string, mixed> 数据库列表信息
*/
private function getDatabaseList(DatabaseSyncService $syncService): array
{
$list = [
'completed' => [],
'pending' => [],
'processing' => [],
];
try {
$runtimePath = function_exists('runtime_path') ? runtime_path() : (config('app.runtime_path', base_path() . DIRECTORY_SEPARATOR . 'runtime'));
$progressFile = $runtimePath . DIRECTORY_SEPARATOR . 'database_sync_progress.json';
if (file_exists($progressFile)) {
$fp = fopen($progressFile, 'r');
if ($fp && flock($fp, LOCK_SH)) {
try {
$content = stream_get_contents($fp);
$progressData = json_decode($content, true);
if ($progressData) {
$checkpoints = $progressData['checkpoints'] ?? [];
$collectionsSnapshot = $progressData['collections_snapshot'] ?? [];
$currentDatabase = $progressData['current_database'] ?? null;
// 获取所有数据库名称
$allDatabases = array_keys($collectionsSnapshot);
foreach ($allDatabases as $database) {
$dbCheckpoints = $checkpoints[$database] ?? [];
// 检查该数据库的所有集合是否都已完成
$collections = $collectionsSnapshot[$database] ?? [];
$allCompleted = true;
$hasData = false;
foreach ($collections as $collection) {
$checkpoint = $dbCheckpoints[$collection] ?? null;
if ($checkpoint) {
$hasData = true;
if (!($checkpoint['completed'] ?? false)) {
$allCompleted = false;
break;
}
} else {
$allCompleted = false;
}
}
if ($database === $currentDatabase) {
$list['processing'][] = [
'name' => $database,
'collections' => count($collections),
'collections_completed' => count(array_filter($dbCheckpoints, fn($cp) => $cp['completed'] ?? false)),
];
} elseif ($allCompleted && $hasData) {
$list['completed'][] = [
'name' => $database,
'collections' => count($collections),
];
} else {
$list['pending'][] = [
'name' => $database,
'collections' => count($collections),
];
}
}
}
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
} else {
if ($fp) {
fclose($fp);
}
}
}
} catch (\Throwable $e) {
// 忽略错误,返回空列表
}
return $list;
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace app\controller;
use support\Request;
class IndexController
{
public function index(Request $request)
{
return "我是数据中心,有何贵干?";
}
public function view(Request $request)
{
return view('index/view', ['name' => 'webman']);
}
public function json(Request $request)
{
return json(['code' => 0, 'msg' => 'ok']);
}
/**
* 测试 MongoDB 数据库连接
* GET /api/test/db
*/
public function testDb(Request $request)
{
$result = [
'code' => 0,
'msg' => 'ok',
'data' => [
'config' => [],
'connection' => [],
'test_query' => [],
],
];
try {
// 读取数据库配置
$dbConfig = config('database', []);
$mongoConfig = $dbConfig['connections']['mongodb'] ?? null;
if (!$mongoConfig) {
throw new \Exception('MongoDB 配置不存在');
}
$result['data']['config'] = [
'driver' => $mongoConfig['driver'] ?? 'unknown',
'database' => $mongoConfig['database'] ?? 'unknown',
'dsn' => $mongoConfig['dsn'] ?? 'unknown',
'has_username' => !empty($mongoConfig['username']),
'has_password' => !empty($mongoConfig['password']),
];
// 尝试使用 MongoDB 客户端直接连接
try {
// 构建包含认证信息的 DSN如果配置了用户名和密码
$dsn = $mongoConfig['dsn'];
if (!empty($mongoConfig['username']) && !empty($mongoConfig['password'])) {
// 如果 DSN 中不包含认证信息,则添加
if (strpos($dsn, '@') === false) {
// 从 mongodb://host:port 格式转换为 mongodb://username:password@host:port/database
$dsn = str_replace(
'mongodb://',
'mongodb://' . urlencode($mongoConfig['username']) . ':' . urlencode($mongoConfig['password']) . '@',
$dsn
);
// 添加数据库名和认证源
$dsn .= '/' . $mongoConfig['database'];
if (!empty($mongoConfig['options']['authSource'])) {
$dsn .= '?authSource=' . urlencode($mongoConfig['options']['authSource']);
}
}
}
// 过滤掉空字符串的选项MongoDB 客户端不允许空字符串)
$options = array_filter($mongoConfig['options'] ?? [], function ($value) {
return $value !== '';
});
$client = new \MongoDB\Client(
$dsn,
$options
);
// 尝试执行 ping 命令
$adminDb = $client->selectDatabase('admin');
$pingResult = $adminDb->command(['ping' => 1])->toArray();
$result['data']['connection'] = [
'status' => 'connected',
'ping' => 'ok',
'server_info' => $client->getManager()->getServers(),
];
// 尝试选择目标数据库并列出集合
$targetDb = $client->selectDatabase($mongoConfig['database']);
$collections = $targetDb->listCollections();
$collectionNames = [];
foreach ($collections as $collection) {
$collectionNames[] = $collection->getName();
}
$result['data']['test_query'] = [
'database' => $mongoConfig['database'],
'collections_count' => count($collectionNames),
'collections' => $collectionNames,
];
} catch (\MongoDB\Driver\Exception\Exception $e) {
$result['data']['connection'] = [
'status' => 'failed',
'error' => $e->getMessage(),
'code' => $e->getCode(),
];
$result['code'] = 500;
$result['msg'] = 'MongoDB 连接失败';
}
// 尝试使用 Repository 查询(如果连接成功)
if ($result['data']['connection']['status'] === 'connected') {
try {
$userRepo = new \app\repository\UserProfileRepository();
$count = $userRepo->newQuery()->count();
$result['data']['repository_test'] = [
'status' => 'ok',
'user_profile_count' => $count,
];
} catch (\Throwable $e) {
$result['data']['repository_test'] = [
'status' => 'failed',
'error' => $e->getMessage(),
];
}
}
} catch (\Throwable $e) {
$result = [
'code' => 500,
'msg' => '测试失败: ' . $e->getMessage(),
'data' => [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
],
];
}
return json($result);
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace app\controller;
use app\service\PersonMergeService;
use app\service\IdentifierService;
use app\repository\UserProfileRepository;
use app\repository\UserTagRepository;
use app\repository\UserPhoneRelationRepository;
use app\service\UserPhoneService;
use app\service\TagService;
use app\repository\TagDefinitionRepository;
use app\repository\TagHistoryRepository;
use app\service\TagRuleEngine\SimpleRuleEngine;
use app\utils\ApiResponseHelper;
use app\utils\LoggerHelper;
use support\Request;
use support\Response;
/**
* 身份合并控制器
*
* 提供身份合并相关接口实现场景4手机号发现身份证后合并
*/
class PersonMergeController
{
/**
* 合并手机号到身份证场景4的实现
*
* 如果某个手机号发现了对应的身份证号,查询该身份下是否有标签,
* 如果有就会将对应的这个身份证号的所有标签重新计算同步。
*
* POST /api/person-merge/phone-to-id-card
*/
public function mergePhoneToIdCard(Request $request): Response
{
try {
LoggerHelper::logRequest('POST', '/api/person-merge/phone-to-id-card');
$rawBody = $request->rawBody();
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$body = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ApiResponseHelper::error('JSON 格式错误: ' . json_last_error_msg(), 400);
}
// 验证必填字段
if (empty($body['phone_number'])) {
throw new \InvalidArgumentException('缺少必填字段phone_number');
}
if (empty($body['id_card'])) {
throw new \InvalidArgumentException('缺少必填字段id_card');
}
$phoneNumber = (string)$body['phone_number'];
$idCard = (string)$body['id_card'];
// 创建服务实例
$mergeService = new PersonMergeService(
new UserProfileRepository(),
new UserTagRepository(),
new UserPhoneService(
new UserPhoneRelationRepository()
),
new TagService(
new TagDefinitionRepository(),
new UserProfileRepository(),
new UserTagRepository(),
new TagHistoryRepository(),
new SimpleRuleEngine()
)
);
// 执行合并
$formalUserId = $mergeService->mergePhoneToIdCard($phoneNumber, $idCard);
LoggerHelper::logBusiness('person_merge_phone_to_id_card', [
'phone_number' => $phoneNumber,
'id_card_provided' => true,
'formal_user_id' => $formalUserId,
]);
return ApiResponseHelper::success([
'phone_number' => $phoneNumber,
'formal_user_id' => $formalUserId,
'message' => '身份合并成功,标签已重新计算',
]);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 合并临时人到正式人
*
* POST /api/person-merge/temporary-to-formal
*/
public function mergeTemporaryToFormal(Request $request): Response
{
try {
LoggerHelper::logRequest('POST', '/api/person-merge/temporary-to-formal');
$rawBody = $request->rawBody();
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$body = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ApiResponseHelper::error('JSON 格式错误: ' . json_last_error_msg(), 400);
}
// 验证必填字段
if (empty($body['user_id'])) {
throw new \InvalidArgumentException('缺少必填字段user_id');
}
if (empty($body['id_card'])) {
throw new \InvalidArgumentException('缺少必填字段id_card');
}
$tempUserId = (string)$body['user_id'];
$idCard = (string)$body['id_card'];
// 创建服务实例
$mergeService = new PersonMergeService(
new UserProfileRepository(),
new UserTagRepository(),
new UserPhoneService(
new UserPhoneRelationRepository()
),
new TagService(
new TagDefinitionRepository(),
new UserProfileRepository(),
new UserTagRepository(),
new TagHistoryRepository(),
new SimpleRuleEngine()
)
);
// 执行合并
$formalUserId = $mergeService->mergeTemporaryToFormal($tempUserId, $idCard);
LoggerHelper::logBusiness('person_merge_temporary_to_formal', [
'temp_user_id' => $tempUserId,
'formal_user_id' => $formalUserId,
]);
return ApiResponseHelper::success([
'temp_user_id' => $tempUserId,
'formal_user_id' => $formalUserId,
'message' => '临时人已转为正式人,标签已重新计算',
]);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
}

View File

@@ -0,0 +1,308 @@
<?php
namespace app\controller;
use app\repository\TagCohortRepository;
use app\repository\UserTagRepository;
use app\service\TagService;
use app\repository\TagDefinitionRepository;
use app\repository\UserProfileRepository;
use app\repository\TagHistoryRepository;
use app\service\TagRuleEngine\SimpleRuleEngine;
use app\utils\ApiResponseHelper;
use app\utils\LoggerHelper;
use Ramsey\Uuid\Uuid;
use support\Request;
use support\Response;
class TagCohortController
{
/**
* 获取人群快照列表
*
* GET /api/tag-cohorts
*/
public function list(Request $request): Response
{
try {
LoggerHelper::logRequest('GET', '/api/tag-cohorts');
$page = (int)($request->get('page') ?? 1);
$pageSize = (int)($request->get('page_size') ?? 20);
if ($page < 1) {
$page = 1;
}
if ($pageSize < 1 || $pageSize > 100) {
$pageSize = 20;
}
$cohortRepo = new TagCohortRepository();
$total = $cohortRepo->newQuery()->count();
$cohorts = $cohortRepo->newQuery()
->orderBy('created_at', 'desc')
->skip(($page - 1) * $pageSize)
->take($pageSize)
->get();
$result = [];
foreach ($cohorts as $cohort) {
$result[] = [
'cohort_id' => $cohort->cohort_id,
'name' => $cohort->name,
'user_count' => $cohort->user_count ?? 0,
'created_at' => $cohort->created_at ? $cohort->created_at->format('Y-m-d H:i:s') : null,
];
}
LoggerHelper::logBusiness('get_tag_cohort_list', [
'total' => $total,
'page' => $page,
]);
return ApiResponseHelper::success([
'cohorts' => $result,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
]);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取人群快照详情
*
* GET /api/tag-cohorts/{cohort_id}
*/
public function detail(Request $request, string $cohortId): Response
{
try {
LoggerHelper::logRequest('GET', "/api/tag-cohorts/{$cohortId}");
$cohortRepo = new TagCohortRepository();
$cohort = $cohortRepo->newQuery()->where('cohort_id', $cohortId)->first();
if (!$cohort) {
return ApiResponseHelper::error('人群快照不存在', 404, 404);
}
$result = [
'cohort_id' => $cohort->cohort_id,
'name' => $cohort->name,
'description' => $cohort->description ?? '',
'conditions' => $cohort->conditions ?? [],
'logic' => $cohort->logic ?? 'AND',
'user_ids' => $cohort->user_ids ?? [],
'user_count' => $cohort->user_count ?? 0,
'created_at' => $cohort->created_at ? $cohort->created_at->format('Y-m-d H:i:s') : null,
];
LoggerHelper::logBusiness('get_tag_cohort_detail', [
'cohort_id' => $cohortId,
]);
return ApiResponseHelper::success($result);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 创建人群快照
*
* POST /api/tag-cohorts
*/
public function create(Request $request): Response
{
try {
LoggerHelper::logRequest('POST', '/api/tag-cohorts');
$rawBody = $request->rawBody();
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$body = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ApiResponseHelper::error('JSON 格式错误: ' . json_last_error_msg(), 400);
}
// 验证必填字段
if (empty($body['name'])) {
throw new \InvalidArgumentException('缺少必填字段name');
}
if (empty($body['conditions']) || !is_array($body['conditions'])) {
throw new \InvalidArgumentException('缺少必填字段conditions必须为数组');
}
if (empty($body['user_ids']) || !is_array($body['user_ids'])) {
throw new \InvalidArgumentException('缺少必填字段user_ids必须为数组');
}
// 使用标签筛选服务获取用户列表
$tagService = new TagService(
new TagDefinitionRepository(),
new UserProfileRepository(),
new UserTagRepository(),
new TagHistoryRepository(),
new SimpleRuleEngine()
);
$conditions = $body['conditions'];
$logic = $body['logic'] ?? 'AND';
// 筛选用户(获取所有用户,不包含用户信息)
$filterResult = $tagService->filterUsersByTags(
$conditions,
$logic,
1,
10000, // 最多获取10000个用户
false
);
// 从返回结果中提取用户ID
$userIds = [];
if (isset($filterResult['users']) && is_array($filterResult['users'])) {
foreach ($filterResult['users'] as $user) {
if (isset($user['user_id'])) {
$userIds[] = $user['user_id'];
} elseif (is_string($user)) {
// 如果直接返回的是用户ID字符串
$userIds[] = $user;
}
}
}
// 如果用户提供了 user_ids使用提供的列表优先级更高
if (!empty($body['user_ids']) && is_array($body['user_ids'])) {
$userIds = $body['user_ids'];
}
// 创建人群快照
$cohortRepo = new TagCohortRepository();
$cohort = new TagCohortRepository();
$cohort->cohort_id = Uuid::uuid4()->toString();
$cohort->name = $body['name'];
$cohort->description = $body['description'] ?? '';
$cohort->conditions = $conditions;
$cohort->logic = $logic;
$cohort->user_ids = $userIds;
$cohort->user_count = count($userIds);
$cohort->created_by = $body['created_by'] ?? 'system';
$cohort->created_at = new \DateTime();
$cohort->updated_at = new \DateTime();
$cohort->save();
LoggerHelper::logBusiness('create_tag_cohort', [
'cohort_id' => $cohort->cohort_id,
'name' => $cohort->name,
'user_count' => $cohort->user_count,
]);
return ApiResponseHelper::success([
'cohort_id' => $cohort->cohort_id,
'name' => $cohort->name,
'user_count' => $cohort->user_count,
], '人群快照创建成功');
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 删除人群快照
*
* DELETE /api/tag-cohorts/{cohort_id}
*/
public function delete(Request $request, string $cohortId): Response
{
try {
LoggerHelper::logRequest('DELETE', "/api/tag-cohorts/{$cohortId}");
$cohortRepo = new TagCohortRepository();
$cohort = $cohortRepo->newQuery()->where('cohort_id', $cohortId)->first();
if (!$cohort) {
return ApiResponseHelper::error('人群快照不存在', 404, 404);
}
$cohort->delete();
LoggerHelper::logBusiness('delete_tag_cohort', [
'cohort_id' => $cohortId,
]);
return ApiResponseHelper::success(null, '人群快照删除成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 导出人群快照
*
* POST /api/tag-cohorts/{cohort_id}/export
*/
public function export(Request $request, string $cohortId): Response
{
try {
LoggerHelper::logRequest('POST', "/api/tag-cohorts/{$cohortId}/export");
$cohortRepo = new TagCohortRepository();
$cohort = $cohortRepo->newQuery()->where('cohort_id', $cohortId)->first();
if (!$cohort) {
return ApiResponseHelper::error('人群快照不存在', 404, 404);
}
$userIds = $cohort->user_ids ?? [];
$userProfileRepo = new UserProfileRepository();
// 获取用户信息
$users = [];
foreach ($userIds as $userId) {
$user = $userProfileRepo->findByUserId($userId);
if ($user) {
$users[] = [
'user_id' => $user->user_id,
'phone' => $user->phone ?? '',
'name' => $user->name ?? '',
];
}
}
// 生成 CSV 内容
$csvContent = "用户ID,手机号,姓名\n";
foreach ($users as $user) {
$csvContent .= sprintf(
"%s,%s,%s\n",
$user['user_id'],
$user['phone'],
$user['name']
);
}
LoggerHelper::logBusiness('export_tag_cohort', [
'cohort_id' => $cohortId,
'user_count' => count($users),
]);
// 返回 CSV 文件
return response($csvContent)
->header('Content-Type', 'text/csv; charset=utf-8')
->header('Content-Disposition', "attachment; filename=\"cohort_{$cohortId}.csv\"");
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
}

View File

@@ -0,0 +1,521 @@
<?php
namespace app\controller;
use app\repository\TagDefinitionRepository;
use app\repository\UserProfileRepository;
use app\repository\UserTagRepository;
use app\repository\TagHistoryRepository;
use app\service\TagService;
use app\service\TagRuleEngine\SimpleRuleEngine;
use app\utils\ApiResponseHelper;
use app\utils\DataMaskingHelper;
use app\utils\LoggerHelper;
use support\Request;
use support\Response;
class TagController
{
/**
* 查询用户的标签列表
*
* GET /api/users/{user_id}/tags
*/
public function listByUser(Request $request): Response
{
try {
// 从请求路径中解析 user_id
$path = $request->path();
if (preg_match('#/api/users/([^/]+)/tags#', $path, $matches)) {
$userId = $matches[1];
} else {
// 如果路径解析失败,尝试从查询参数获取
$userId = $request->get('user_id');
if (!$userId) {
throw new \InvalidArgumentException('缺少 user_id 参数');
}
}
LoggerHelper::logRequest('GET', $path, ['user_id' => $userId]);
$userTagRepo = new UserTagRepository();
$tagDefRepo = new TagDefinitionRepository();
// 查询用户的所有标签
$userTags = $userTagRepo->newQuery()
->where('user_id', $userId)
->get();
// 关联标签定义信息
$result = [];
foreach ($userTags as $userTag) {
$tagDef = $tagDefRepo->newQuery()
->where('tag_id', $userTag->tag_id)
->first();
$result[] = [
'tag_id' => $userTag->tag_id,
'tag_code' => $tagDef ? $tagDef->tag_code : null,
'tag_name' => $tagDef ? $tagDef->tag_name : null,
'category' => $tagDef ? $tagDef->category : null,
'tag_value' => $userTag->tag_value,
'tag_value_type' => $userTag->tag_value_type,
'confidence' => $userTag->confidence,
'effective_time' => $userTag->effective_time,
'expire_time' => $userTag->expire_time,
'update_time' => $userTag->update_time,
];
}
LoggerHelper::logBusiness('get_user_tags', [
'user_id' => $userId,
'tag_count' => count($result),
]);
return ApiResponseHelper::success([
'user_id' => $userId,
'tags' => $result,
'count' => count($result),
]);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 更新/计算用户标签
*
* PUT /api/users/{user_id}/tags
*/
public function calculate(Request $request): Response
{
$startTime = microtime(true);
try {
// 从请求路径中解析 user_id
$path = $request->path();
if (preg_match('#/api/users/([^/]+)/tags#', $path, $matches)) {
$userId = $matches[1];
} else {
// 如果路径解析失败,尝试从查询参数获取
$userId = $request->get('user_id');
if (!$userId) {
throw new \InvalidArgumentException('缺少 user_id 参数');
}
}
LoggerHelper::logRequest('PUT', $path, ['user_id' => $userId]);
$tagService = new TagService(
new TagDefinitionRepository(),
new UserProfileRepository(),
new UserTagRepository(),
new TagHistoryRepository(),
new SimpleRuleEngine()
);
$tags = $tagService->calculateTags($userId);
$duration = microtime(true) - $startTime;
LoggerHelper::logBusiness('calculate_tags', [
'user_id' => $userId,
'updated_count' => count($tags),
], 'info');
LoggerHelper::logPerformance('tag_calculation', $duration, [
'user_id' => $userId,
'tag_count' => count($tags),
]);
return ApiResponseHelper::success([
'user_id' => $userId,
'updated_tags' => $tags,
'count' => count($tags),
]);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 删除用户的指定标签
*
* DELETE /api/users/{user_id}/tags/{tag_id}
*/
public function destroy(Request $request): Response
{
try {
// 从请求路径中解析 user_id 和 tag_id
$path = $request->path();
if (preg_match('#/api/users/([^/]+)/tags/([^/]+)#', $path, $matches)) {
$userId = $matches[1];
$tagId = $matches[2];
} else {
throw new \InvalidArgumentException('缺少 user_id 或 tag_id 参数');
}
LoggerHelper::logRequest('DELETE', $path, ['user_id' => $userId, 'tag_id' => $tagId]);
$tagService = new TagService(
new TagDefinitionRepository(),
new UserProfileRepository(),
new UserTagRepository(),
new TagHistoryRepository(),
new SimpleRuleEngine()
);
$deleted = $tagService->deleteUserTag($userId, $tagId);
if (!$deleted) {
return ApiResponseHelper::error('标签不存在', 404, 404);
}
LoggerHelper::logBusiness('tag_deleted', [
'user_id' => $userId,
'tag_id' => $tagId,
]);
return ApiResponseHelper::success(null, '标签删除成功');
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 根据标签筛选用户
*
* POST /api/tags/filter
*/
public function filter(Request $request): Response
{
try {
LoggerHelper::logRequest('POST', '/api/tags/filter');
$rawBody = $request->rawBody();
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$body = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ApiResponseHelper::error('JSON 格式错误: ' . json_last_error_msg(), 400);
}
// 验证必填字段
if (empty($body['tag_conditions']) || !is_array($body['tag_conditions'])) {
throw new \InvalidArgumentException('缺少必填字段tag_conditions必须为数组');
}
// 验证条件格式
foreach ($body['tag_conditions'] as $condition) {
if (!isset($condition['tag_code']) || !isset($condition['operator']) || !isset($condition['value'])) {
throw new \InvalidArgumentException('每个条件必须包含 tag_code、operator 和 value 字段');
}
}
$tagService = new TagService(
new TagDefinitionRepository(),
new UserProfileRepository(),
new UserTagRepository(),
new TagHistoryRepository(),
new SimpleRuleEngine()
);
$conditions = $body['tag_conditions'];
$logic = $body['logic'] ?? 'AND';
$page = (int)($body['page'] ?? 1);
$pageSize = (int)($body['page_size'] ?? 20);
$includeUserInfo = (bool)($body['include_user_info'] ?? false);
if ($page < 1) {
$page = 1;
}
if ($pageSize < 1 || $pageSize > 100) {
$pageSize = 20;
}
$result = $tagService->filterUsersByTags(
$conditions,
$logic,
$page,
$pageSize,
$includeUserInfo
);
// 对返回的用户信息进行脱敏处理
if ($includeUserInfo && isset($result['users']) && is_array($result['users'])) {
foreach ($result['users'] as &$user) {
$user = DataMaskingHelper::maskArray($user, ['phone', 'email']);
}
unset($user);
}
LoggerHelper::logBusiness('filter_users_by_tags', [
'conditions_count' => count($conditions),
'logic' => $logic,
'result_count' => $result['total'] ?? 0,
]);
return ApiResponseHelper::success($result);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 批量初始化标签定义
*
* POST /api/tag-definitions/batch
*/
public function init(Request $request): Response
{
try {
LoggerHelper::logRequest('POST', '/api/tag-definitions/batch');
$initService = new \app\service\TagInitService(
new TagDefinitionRepository()
);
$initService->initBasicTags();
LoggerHelper::logBusiness('init_tags', []);
return ApiResponseHelper::success([
'message' => '标签初始化完成',
]);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取标签统计信息
*
* GET /api/tags/statistics
*/
public function statistics(Request $request): Response
{
try {
LoggerHelper::logRequest('GET', '/api/tags/statistics');
$tagId = $request->get('tag_id');
$startDate = $request->get('start_date');
$endDate = $request->get('end_date');
$userTagRepo = new UserTagRepository();
$tagDefRepo = new TagDefinitionRepository();
$result = [
'value_distribution' => [],
'trend_data' => [],
'coverage_stats' => [],
];
// 如果指定了 tag_id统计该标签的值分布
if ($tagId) {
$tagDef = $tagDefRepo->newQuery()->where('tag_id', $tagId)->first();
if ($tagDef) {
// 统计标签值分布
$userTags = $userTagRepo->newQuery()
->where('tag_id', $tagId)
->get(['tag_value']);
$valueCounts = [];
foreach ($userTags as $userTag) {
$value = (string)$userTag->tag_value;
if (!isset($valueCounts[$value])) {
$valueCounts[$value] = 0;
}
$valueCounts[$value]++;
}
// 按数量排序
arsort($valueCounts);
$valueCounts = array_slice($valueCounts, 0, 20, true);
foreach ($valueCounts as $value => $count) {
$result['value_distribution'][] = [
'value' => $value,
'count' => $count
];
}
// 统计标签覆盖度
$totalUsers = $userTagRepo->newQuery()
->distinct('user_id')
->count();
$taggedUsers = $userTagRepo->newQuery()
->where('tag_id', $tagId)
->distinct('user_id')
->count();
$result['coverage_stats'][] = [
'tag_id' => $tagId,
'tag_name' => $tagDef->tag_name ?? '',
'total_users' => $totalUsers,
'tagged_users' => $taggedUsers,
'coverage_rate' => $totalUsers > 0 ? round($taggedUsers / $totalUsers * 100, 2) : 0
];
}
} else {
// 统计所有标签的覆盖度
$tagDefs = $tagDefRepo->newQuery()->where('status', 1)->get();
$totalUsers = $userTagRepo->newQuery()->distinct('user_id')->count();
foreach ($tagDefs as $tagDef) {
$taggedUsers = $userTagRepo->newQuery()
->where('tag_id', $tagDef->tag_id)
->distinct('user_id')
->count();
$result['coverage_stats'][] = [
'tag_id' => $tagDef->tag_id,
'tag_name' => $tagDef->tag_name ?? '',
'total_users' => $totalUsers,
'tagged_users' => $taggedUsers,
'coverage_rate' => $totalUsers > 0 ? round($taggedUsers / $totalUsers * 100, 2) : 0
];
}
}
// 趋势数据(如果有时间范围)
if ($startDate && $endDate) {
$historyRepo = new TagHistoryRepository();
$start = new \DateTime($startDate);
$end = new \DateTime($endDate);
// 按日期统计标签变更次数
$trendData = [];
$current = clone $start;
while ($current <= $end) {
$dateStr = $current->format('Y-m-d');
$nextDay = clone $current;
$nextDay->modify('+1 day');
$count = $historyRepo->newQuery()
->where('change_time', '>=', $current)
->where('change_time', '<', $nextDay)
->count();
$trendData[] = [
'date' => $dateStr,
'count' => $count
];
$current->modify('+1 day');
}
$result['trend_data'] = $trendData;
}
LoggerHelper::logBusiness('get_tag_statistics', [
'tag_id' => $tagId,
'start_date' => $startDate,
'end_date' => $endDate,
]);
return ApiResponseHelper::success($result);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取标签历史记录
*
* GET /api/tags/history
*/
public function history(Request $request): Response
{
try {
LoggerHelper::logRequest('GET', '/api/tags/history');
$userId = $request->get('user_id');
$tagId = $request->get('tag_id');
$startDate = $request->get('start_date');
$endDate = $request->get('end_date');
$page = (int)($request->get('page') ?? 1);
$pageSize = (int)($request->get('page_size') ?? 20);
if ($page < 1) {
$page = 1;
}
if ($pageSize < 1 || $pageSize > 100) {
$pageSize = 20;
}
$historyRepo = new TagHistoryRepository();
$tagDefRepo = new TagDefinitionRepository();
$query = $historyRepo->newQuery();
if ($userId) {
$query->where('user_id', $userId);
}
if ($tagId) {
$query->where('tag_id', $tagId);
}
if ($startDate) {
$query->where('change_time', '>=', new \DateTime($startDate));
}
if ($endDate) {
$endDateTime = new \DateTime($endDate);
$endDateTime->modify('+1 day');
$query->where('change_time', '<', $endDateTime);
}
$total = $query->count();
$histories = $query->orderBy('change_time', 'desc')
->skip(($page - 1) * $pageSize)
->take($pageSize)
->get();
$items = [];
foreach ($histories as $history) {
$tagDef = $tagDefRepo->newQuery()->where('tag_id', $history->tag_id)->first();
$items[] = [
'user_id' => $history->user_id,
'tag_id' => $history->tag_id,
'tag_name' => $tagDef ? $tagDef->tag_name : null,
'old_value' => $history->old_value,
'new_value' => $history->new_value,
'change_reason' => $history->change_reason,
'change_time' => $history->change_time ? $history->change_time->format('Y-m-d H:i:s') : null,
'operator' => $history->operator,
];
}
LoggerHelper::logBusiness('get_tag_history', [
'user_id' => $userId,
'tag_id' => $tagId,
'total' => $total,
]);
return ApiResponseHelper::success([
'items' => $items,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
]);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace app\controller;
use app\repository\TagDefinitionRepository;
use app\utils\ApiResponseHelper;
use support\Request;
use support\Response;
/**
* 标签定义管理控制器
*/
class TagDefinitionController
{
/**
* 获取标签定义列表
*
* GET /api/tag-definitions
*/
public function list(Request $request): Response
{
try {
$repo = new TagDefinitionRepository();
$query = $repo->query();
// 筛选条件
if ($request->get('category')) {
$query->where('category', $request->get('category'));
}
if ($request->get('status')) {
$query->where('status', $request->get('status'));
}
if ($request->get('name')) {
$query->where('tag_name', 'like', '%' . $request->get('name') . '%');
}
$page = (int)($request->get('page', 1));
$pageSize = (int)($request->get('page_size', 20));
$total = $query->count();
$definitions = $query->orderBy('created_at', 'desc')
->skip(($page - 1) * $pageSize)
->take($pageSize)
->get()
->toArray();
return ApiResponseHelper::success([
'definitions' => $definitions,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
'total_pages' => ceil($total / $pageSize),
], '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取标签定义详情
*
* GET /api/tag-definitions/{tag_id}
*/
public function detail(Request $request, string $tagId): Response
{
try {
$repo = new TagDefinitionRepository();
$definition = $repo->find($tagId);
if (!$definition) {
return ApiResponseHelper::error('标签定义不存在', 404);
}
return ApiResponseHelper::success($definition->toArray(), '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 创建标签定义
*
* POST /api/tag-definitions
*/
public function create(Request $request): Response
{
try {
$data = $request->post();
$requiredFields = ['tag_code', 'tag_name', 'category'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
return ApiResponseHelper::error("缺少必填字段: {$field}", 400);
}
}
$repo = new TagDefinitionRepository();
$definition = $repo->create($data);
return ApiResponseHelper::success($definition->toArray(), '创建成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 更新标签定义
*
* PUT /api/tag-definitions/{tag_id}
*/
public function update(Request $request, string $tagId): Response
{
try {
$data = $request->post();
$repo = new TagDefinitionRepository();
$definition = $repo->find($tagId);
if (!$definition) {
return ApiResponseHelper::error('标签定义不存在', 404);
}
$definition->fill($data);
$definition->save();
return ApiResponseHelper::success($definition->toArray(), '更新成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 删除标签定义
*
* DELETE /api/tag-definitions/{tag_id}
*/
public function delete(Request $request, string $tagId): Response
{
try {
$repo = new TagDefinitionRepository();
$definition = $repo->find($tagId);
if (!$definition) {
return ApiResponseHelper::error('标签定义不存在', 404);
}
$definition->delete();
return ApiResponseHelper::success(null, '删除成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
}

View File

@@ -0,0 +1,227 @@
<?php
namespace app\controller;
use app\service\TagTaskService;
use app\repository\TagTaskRepository;
use app\repository\TagTaskExecutionRepository;
use app\repository\UserProfileRepository;
use app\service\TagService;
use app\repository\TagDefinitionRepository;
use app\repository\UserTagRepository;
use app\repository\TagHistoryRepository;
use app\service\TagRuleEngine\SimpleRuleEngine;
use app\utils\ApiResponseHelper;
use support\Request;
use support\Response;
/**
* 标签任务管理控制器
*/
class TagTaskController
{
public function __construct()
{
// 初始化服务(使用依赖注入或直接创建)
}
/**
* 创建标签任务
*
* POST /api/tag-tasks
*/
public function create(Request $request): Response
{
try {
$data = $request->post();
$requiredFields = ['name', 'task_type'];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
return ApiResponseHelper::error("缺少必填字段: {$field}", 400);
}
}
$service = $this->getService();
$task = $service->createTask($data);
return ApiResponseHelper::success($task, '任务创建成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 更新任务
*
* PUT /api/tag-tasks/{task_id}
*/
public function update(Request $request, string $taskId): Response
{
try {
$data = $request->post();
$service = $this->getService();
$result = $service->updateTask($taskId, $data);
return ApiResponseHelper::success(null, '任务更新成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 删除任务
*
* DELETE /api/tag-tasks/{task_id}
*/
public function delete(Request $request, string $taskId): Response
{
try {
$service = $this->getService();
$result = $service->deleteTask($taskId);
return ApiResponseHelper::success(null, '任务删除成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 启动任务
*
* POST /api/tag-tasks/{task_id}/start
*/
public function start(Request $request, string $taskId): Response
{
try {
$service = $this->getService();
$result = $service->startTask($taskId);
return ApiResponseHelper::success(null, '任务启动成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 暂停任务
*
* POST /api/tag-tasks/{task_id}/pause
*/
public function pause(Request $request, string $taskId): Response
{
try {
$service = $this->getService();
$result = $service->pauseTask($taskId);
return ApiResponseHelper::success(null, '任务暂停成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 停止任务
*
* POST /api/tag-tasks/{task_id}/stop
*/
public function stop(Request $request, string $taskId): Response
{
try {
$service = $this->getService();
$result = $service->stopTask($taskId);
return ApiResponseHelper::success(null, '任务停止成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取任务列表
*
* GET /api/tag-tasks
*/
public function list(Request $request): Response
{
try {
$filters = [
'status' => $request->get('status'),
'task_type' => $request->get('task_type'),
'name' => $request->get('name'),
];
$page = (int)($request->get('page', 1));
$pageSize = (int)($request->get('page_size', 20));
$service = $this->getService();
$result = $service->getTaskList($filters, $page, $pageSize);
return ApiResponseHelper::success($result, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取任务详情
*
* GET /api/tag-tasks/{task_id}
*/
public function detail(Request $request, string $taskId): Response
{
try {
$service = $this->getService();
$task = $service->getTask($taskId);
if ($task === null) {
return ApiResponseHelper::error('任务不存在', 404);
}
return ApiResponseHelper::success($task, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取任务执行记录
*
* GET /api/tag-tasks/{task_id}/executions
*/
public function executions(Request $request, string $taskId): Response
{
try {
$page = (int)($request->get('page', 1));
$pageSize = (int)($request->get('page_size', 20));
$service = $this->getService();
$result = $service->getExecutions($taskId, $page, $pageSize);
return ApiResponseHelper::success($result, '查询成功');
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 获取服务实例
*/
private function getService(): TagTaskService
{
return new TagTaskService(
new TagTaskRepository(),
new TagTaskExecutionRepository(),
new UserProfileRepository(),
new TagService(
new TagDefinitionRepository(),
new UserProfileRepository(),
new UserTagRepository(),
new TagHistoryRepository(),
new SimpleRuleEngine()
)
);
}
}

View File

@@ -0,0 +1,557 @@
<?php
namespace app\controller;
use app\repository\UserProfileRepository;
use app\service\UserService;
use app\utils\ApiResponseHelper;
use app\utils\DataMaskingHelper;
use app\utils\LoggerHelper;
use support\Request;
use support\Response;
class UserController
{
/**
* 创建用户
*
* POST /api/users
*
* 请求体示例:
* {
* "id_card": "110101199001011234",
* "id_card_type": "身份证",
* "name": "张三",
* "phone": "13800138000",
* "email": "zhangsan@example.com",
* "gender": 1,
* "birthday": "1990-01-01",
* "address": "北京市朝阳区"
* }
*/
public function store(Request $request): Response
{
try {
LoggerHelper::logRequest('POST', '/api/users');
$rawBody = $request->rawBody();
// 调试:记录原始请求体
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$body = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$errorMsg = '请求体必须是有效的 JSON 格式';
$jsonError = json_last_error_msg();
if ($jsonError) {
$errorMsg .= ': ' . $jsonError;
}
// 开发环境输出更多调试信息
if (getenv('APP_DEBUG') === 'true') {
$errorMsg .= ' (原始请求体: ' . substr($rawBody, 0, 200) . ')';
}
return ApiResponseHelper::error($errorMsg, 400);
}
$userService = new UserService(new UserProfileRepository());
$result = $userService->createUser($body);
return ApiResponseHelper::success($result, '用户创建成功');
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 查询用户信息
*
* GET /api/users/{user_id}?decrypt_id_card=1
*
* @param Request $request
* @return Response
*/
public function show(Request $request): Response
{
try {
// 从请求路径中解析 user_id
$path = $request->path();
if (preg_match('#/api/users/([^/]+)#', $path, $matches)) {
$userId = $matches[1];
} else {
$userId = $request->get('user_id');
if (!$userId) {
throw new \InvalidArgumentException('缺少 user_id 参数');
}
}
LoggerHelper::logRequest('GET', $path, ['user_id' => $userId]);
// 检查是否需要解密身份证(需要权限控制,这里简单用参数控制)
$decryptIdCard = (bool)$request->get('decrypt_id_card', false);
$userService = new UserService(new UserProfileRepository());
$user = $userService->getUserById($userId, $decryptIdCard);
if (!$user) {
return ApiResponseHelper::error('用户不存在', 404, 404);
}
// 如果不需要解密身份证,对敏感字段进行脱敏
if (!$decryptIdCard) {
$user = DataMaskingHelper::maskArray($user, ['phone', 'email']);
}
LoggerHelper::logBusiness('get_user_info', [
'user_id' => $userId,
'decrypt_id_card' => $decryptIdCard,
]);
return ApiResponseHelper::success($user);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 更新用户信息
*
* PUT /api/users/{user_id}
*
* 请求体示例:
* {
* "name": "张三",
* "phone": "13800138000",
* "email": "zhangsan@example.com",
* "gender": 1,
* "birthday": "1990-01-01",
* "address": "北京市朝阳区",
* "status": 0
* }
*/
public function update(Request $request): Response
{
try {
// 从请求路径中解析 user_id
$path = $request->path();
if (preg_match('#/api/users/([^/]+)#', $path, $matches)) {
$userId = $matches[1];
} else {
$userId = $request->get('user_id');
if (!$userId) {
throw new \InvalidArgumentException('缺少 user_id 参数');
}
}
LoggerHelper::logRequest('PUT', $path, ['user_id' => $userId]);
$rawBody = $request->rawBody();
// 调试:记录原始请求体
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$body = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$errorMsg = '请求体必须是有效的 JSON 格式';
$jsonError = json_last_error_msg();
if ($jsonError) {
$errorMsg .= ': ' . $jsonError;
}
// 开发环境输出更多调试信息
if (getenv('APP_DEBUG') === 'true') {
$errorMsg .= ' (原始请求体: ' . substr($rawBody, 0, 200) . ')';
}
return ApiResponseHelper::error($errorMsg, 400);
}
if (empty($body)) {
return ApiResponseHelper::error('请求体不能为空', 400);
}
$userService = new UserService(new UserProfileRepository());
$result = $userService->updateUser($userId, $body);
// 脱敏处理
$result = DataMaskingHelper::maskArray($result, ['phone', 'email']);
return ApiResponseHelper::success($result, '用户更新成功');
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 解密身份证号
*
* GET /api/users/{user_id}/decrypt-id-card
*/
public function decryptIdCard(Request $request): Response
{
try {
// 从请求路径中解析 user_id
$path = $request->path();
if (preg_match('#/api/users/([^/]+)/decrypt-id-card#', $path, $matches)) {
$userId = $matches[1];
} else {
$userId = $request->get('user_id');
if (!$userId) {
throw new \InvalidArgumentException('缺少 user_id 参数');
}
}
LoggerHelper::logRequest('GET', $path, ['user_id' => $userId]);
$userService = new UserService(new UserProfileRepository());
$user = $userService->getUserById($userId, true); // 强制解密
if (!$user) {
return ApiResponseHelper::error('用户不存在', 404, 404);
}
LoggerHelper::logBusiness('decrypt_id_card', [
'user_id' => $userId,
]);
return ApiResponseHelper::success([
'user_id' => $user['user_id'],
'id_card' => $user['id_card'] ?? ''
]);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 删除用户(软删除)
*
* DELETE /api/users/{user_id}
*/
public function destroy(Request $request): Response
{
try {
// 从请求路径中解析 user_id
$path = $request->path();
if (preg_match('#/api/users/([^/]+)#', $path, $matches)) {
$userId = $matches[1];
} else {
$userId = $request->get('user_id');
if (!$userId) {
throw new \InvalidArgumentException('缺少 user_id 参数');
}
}
LoggerHelper::logRequest('DELETE', $path, ['user_id' => $userId]);
$userService = new UserService(new UserProfileRepository());
$userService->deleteUser($userId);
return ApiResponseHelper::success(null, '用户删除成功');
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
/**
* 搜索用户(支持多种搜索条件组合)
*
* POST /api/users/search
*
* 支持以下搜索方式:
* 1. 基础字段搜索:姓名、手机号、邮箱、身份证号等
* 2. 标签筛选:根据用户标签筛选
* 3. 组合搜索:基础字段 + 标签筛选
*
* 请求体示例1姓名模糊搜索
* {
* "name": "张三",
* "page": 1,
* "page_size": 20
* }
*
* 请求体示例2组合搜索姓名 + 手机号):
* {
* "name": "张",
* "phone": "138",
* "page": 1,
* "page_size": 20
* }
*
* 请求体示例3根据标签筛选
* {
* "tag_conditions": [
* {
* "tag_code": "high_consumer",
* "operator": "=",
* "value": "high"
* }
* ],
* "logic": "AND",
* "page": 1,
* "page_size": 20
* }
*
* 请求体示例4组合搜索基础字段 + 标签):
* {
* "name": "张",
* "min_total_amount": 1000,
* "tag_conditions": [
* {
* "tag_code": "active_user",
* "operator": "=",
* "value": "active"
* }
* ],
* "page": 1,
* "page_size": 20
* }
*/
public function search(Request $request): Response
{
try {
LoggerHelper::logRequest('POST', '/api/users/search');
$rawBody = $request->rawBody();
// 调试:记录原始请求体
if (empty($rawBody)) {
return ApiResponseHelper::error('请求体为空,请确保 Content-Type 为 application/json 并发送有效的 JSON 数据', 400);
}
$body = json_decode($rawBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$errorMsg = '请求体必须是有效的 JSON 格式';
$jsonError = json_last_error_msg();
if ($jsonError) {
$errorMsg .= ': ' . $jsonError;
}
// 开发环境输出更多调试信息
if (getenv('APP_DEBUG') === 'true') {
$errorMsg .= ' (原始请求体: ' . substr($rawBody, 0, 200) . ')';
}
return ApiResponseHelper::error($errorMsg, 400);
}
$page = (int)($body['page'] ?? 1);
$pageSize = (int)($body['page_size'] ?? 20);
if ($page < 1) {
$page = 1;
}
if ($pageSize < 1 || $pageSize > 100) {
$pageSize = 20;
}
$userService = new UserService(new UserProfileRepository());
// 情况1仅根据身份证号查找返回单个用户不分页
if (!empty($body['id_card']) && empty($body['tag_conditions']) && empty($body['name']) && empty($body['phone']) && empty($body['email'])) {
$user = $userService->findUserByIdCard($body['id_card']);
if (!$user) {
return ApiResponseHelper::error('未找到该身份证号对应的用户', 404, 404);
}
// 脱敏处理
$user = DataMaskingHelper::maskArray($user, ['phone', 'email']);
LoggerHelper::logBusiness('search_user_by_id_card', [
'found' => true,
]);
return ApiResponseHelper::success($user);
}
// 情况2根据标签筛选用户可能结合基础字段搜索
if (!empty($body['tag_conditions'])) {
$tagService = new \app\service\TagService(
new \app\repository\TagDefinitionRepository(),
new UserProfileRepository(),
new \app\repository\UserTagRepository(),
new \app\repository\TagHistoryRepository(),
new \app\service\TagRuleEngine\SimpleRuleEngine()
);
$conditions = $body['tag_conditions'];
$logic = $body['logic'] ?? 'AND';
$includeUserInfo = true; // 标签筛选需要用户信息
// 验证条件格式
foreach ($conditions as $condition) {
if (!isset($condition['tag_code']) || !isset($condition['operator']) || !isset($condition['value'])) {
throw new \InvalidArgumentException('每个条件必须包含 tag_code、operator 和 value 字段');
}
}
// 先根据标签筛选用户
$tagResult = $tagService->filterUsersByTags(
$conditions,
$logic,
1, // 先获取所有符合条件的用户ID
10000, // 临时设置大值获取所有用户ID
true
);
$userIds = array_column($tagResult['users'], 'user_id');
if (empty($userIds)) {
return ApiResponseHelper::success([
'users' => [],
'total' => 0,
'page' => $page,
'page_size' => $pageSize,
'total_pages' => 0,
]);
}
// 如果有基础字段搜索条件,进一步筛选
$baseConditions = [];
if (!empty($body['name'])) {
$baseConditions['name'] = $body['name'];
}
if (!empty($body['phone'])) {
$baseConditions['phone'] = $body['phone'];
$baseConditions['phone_exact'] = $body['phone_exact'] ?? false;
}
if (!empty($body['email'])) {
$baseConditions['email'] = $body['email'];
$baseConditions['email_exact'] = $body['email_exact'] ?? false;
}
if (isset($body['gender']) && $body['gender'] !== '') {
$baseConditions['gender'] = $body['gender'];
}
if (isset($body['status']) && $body['status'] !== '') {
$baseConditions['status'] = $body['status'];
}
if (isset($body['min_total_amount'])) {
$baseConditions['min_total_amount'] = $body['min_total_amount'];
}
if (isset($body['max_total_amount'])) {
$baseConditions['max_total_amount'] = $body['max_total_amount'];
}
if (isset($body['min_total_count'])) {
$baseConditions['min_total_count'] = $body['min_total_count'];
}
if (isset($body['max_total_count'])) {
$baseConditions['max_total_count'] = $body['max_total_count'];
}
// 如果有基础字段条件,需要进一步筛选
if (!empty($baseConditions)) {
$baseConditions['user_ids'] = $userIds; // 限制在标签筛选的用户范围内
$result = $userService->searchUsers($baseConditions, $page, $pageSize);
} else {
// 没有基础字段条件,直接使用标签筛选结果并分页
$total = count($userIds);
$offset = ($page - 1) * $pageSize;
$pagedUserIds = array_slice($userIds, $offset, $pageSize);
// 获取用户详细信息
$users = [];
foreach ($pagedUserIds as $userId) {
$user = $userService->getUserById($userId, false);
if ($user) {
$users[] = $user;
}
}
$result = [
'users' => $users,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
'total_pages' => (int)ceil($total / $pageSize),
];
}
// 对返回的用户信息进行脱敏处理
if (isset($result['users']) && is_array($result['users'])) {
foreach ($result['users'] as &$user) {
$user = DataMaskingHelper::maskArray($user, ['phone', 'email']);
}
unset($user);
}
LoggerHelper::logBusiness('search_users_by_tags', [
'conditions_count' => count($conditions),
'base_conditions' => !empty($baseConditions),
'result_count' => $result['total'] ?? 0,
]);
return ApiResponseHelper::success($result);
}
// 情况3仅基础字段搜索无标签条件
$baseConditions = [];
if (!empty($body['name'])) {
$baseConditions['name'] = $body['name'];
}
if (!empty($body['phone'])) {
$baseConditions['phone'] = $body['phone'];
$baseConditions['phone_exact'] = $body['phone_exact'] ?? false;
}
if (!empty($body['email'])) {
$baseConditions['email'] = $body['email'];
$baseConditions['email_exact'] = $body['email_exact'] ?? false;
}
if (!empty($body['id_card'])) {
$baseConditions['id_card'] = $body['id_card'];
}
if (isset($body['gender']) && $body['gender'] !== '') {
$baseConditions['gender'] = $body['gender'];
}
if (isset($body['status']) && $body['status'] !== '') {
$baseConditions['status'] = $body['status'];
}
if (isset($body['min_total_amount'])) {
$baseConditions['min_total_amount'] = $body['min_total_amount'];
}
if (isset($body['max_total_amount'])) {
$baseConditions['max_total_amount'] = $body['max_total_amount'];
}
if (isset($body['min_total_count'])) {
$baseConditions['min_total_count'] = $body['min_total_count'];
}
if (isset($body['max_total_count'])) {
$baseConditions['max_total_count'] = $body['max_total_count'];
}
if (empty($baseConditions)) {
return ApiResponseHelper::error('请提供至少一个搜索条件', 400);
}
$result = $userService->searchUsers($baseConditions, $page, $pageSize);
// 对返回的用户信息进行脱敏处理
if (isset($result['users']) && is_array($result['users'])) {
foreach ($result['users'] as &$user) {
$user = DataMaskingHelper::maskArray($user, ['phone', 'email']);
}
unset($user);
}
LoggerHelper::logBusiness('search_users_by_base_fields', [
'conditions' => array_keys($baseConditions),
'result_count' => $result['total'] ?? 0,
]);
return ApiResponseHelper::success($result);
} catch (\InvalidArgumentException $e) {
return ApiResponseHelper::error($e->getMessage(), 400);
} catch (\Throwable $e) {
return ApiResponseHelper::exception($e);
}
}
}