初始化本地仓库
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\admin\review;
|
||||
|
||||
use app\model\review\Review;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* 审核字段映射服务
|
||||
*
|
||||
* 功能:
|
||||
* 1. 自动扫描并合并系统和所有插件的审核配置
|
||||
* 2. 根据审核类型动态映射字段(title/content/applicant)
|
||||
* 3. 支持多种字段映射方式(属性/关联/回调)
|
||||
* 4. 插件无需修改系统代码即可扩展审核类型
|
||||
*/
|
||||
class ReviewFieldMapper
|
||||
{
|
||||
/**
|
||||
* @var array 缓存的配置(系统 + 所有插件)
|
||||
*/
|
||||
protected static array $configCache = [];
|
||||
|
||||
/**
|
||||
* @var array 缓存的字段映射
|
||||
*/
|
||||
protected static array $fieldMappings = [];
|
||||
|
||||
/**
|
||||
* @var array 缓存的格式化函数
|
||||
*/
|
||||
protected static array $formatters = [];
|
||||
|
||||
/**
|
||||
* 初始化配置(懒加载)
|
||||
*/
|
||||
protected static function init(): void
|
||||
{
|
||||
if (!empty(self::$configCache)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 加载系统主配置
|
||||
$config = config('review', []);
|
||||
|
||||
// 2. 扫描并合并插件配置
|
||||
$pluginConfigs = self::scanPluginConfigs($config['scan_plugins'] ?? true);
|
||||
|
||||
// 3. 合并配置
|
||||
self::$configCache = self::mergeConfigs($config, $pluginConfigs);
|
||||
|
||||
// 4. 构建字段映射索引
|
||||
self::buildFieldMappings();
|
||||
|
||||
// 5. 初始化格式化函数
|
||||
self::initFormatters();
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描插件配置文件
|
||||
*/
|
||||
protected static function scanPluginConfigs(bool $enabled): array
|
||||
{
|
||||
$pluginConfigs = [];
|
||||
|
||||
if (!$enabled) {
|
||||
return $pluginConfigs;
|
||||
}
|
||||
|
||||
$ignoredPlugins = config('review.ignored_plugins', []);
|
||||
$pluginConfigFile = config('review.plugin_config_file', 'review.php');
|
||||
$pluginPath = base_path() . '/plugin';
|
||||
|
||||
if (!is_dir($pluginPath)) {
|
||||
return $pluginConfigs;
|
||||
}
|
||||
|
||||
$plugins = scandir($pluginPath);
|
||||
foreach ($plugins as $plugin) {
|
||||
if ($plugin === '.' || $plugin === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否在忽略列表中
|
||||
if (in_array($plugin, $ignoredPlugins, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$configFile = "{$pluginPath}/{$plugin}/config/{$pluginConfigFile}";
|
||||
if (!is_file($configFile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$config = include $configFile;
|
||||
if (is_array($config)) {
|
||||
// 为插件配置添加标识
|
||||
$config['_plugin'] = [
|
||||
'name' => $plugin,
|
||||
'display_name' => $config['plugin']['display_name'] ?? $plugin,
|
||||
];
|
||||
|
||||
// 按插件名存储,避免冲突
|
||||
$pluginConfigs[$plugin] = $config;
|
||||
}
|
||||
}
|
||||
|
||||
return $pluginConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并系统配置和插件配置
|
||||
*/
|
||||
protected static function mergeConfigs(array $systemConfig, array $pluginConfigs): array
|
||||
{
|
||||
$merged = $systemConfig;
|
||||
|
||||
// 初始化合并结构
|
||||
if (!isset($merged['types'])) {
|
||||
$merged['types'] = [];
|
||||
}
|
||||
|
||||
if (!isset($merged['field_mappings'])) {
|
||||
$merged['field_mappings'] = [];
|
||||
}
|
||||
|
||||
if (!isset($merged['default_field_mappings'])) {
|
||||
$merged['default_field_mappings'] = [];
|
||||
}
|
||||
|
||||
// 合并插件配置
|
||||
foreach ($pluginConfigs as $pluginName => $pluginConfig) {
|
||||
// 合并审核类型
|
||||
if (isset($pluginConfig['types'])) {
|
||||
foreach ($pluginConfig['types'] as $typeKey => $typeConfig) {
|
||||
$merged['types'][$typeKey] = $typeConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// 合并字段映射
|
||||
if (isset($pluginConfig['field_mappings'])) {
|
||||
foreach ($pluginConfig['field_mappings'] as $typeKey => $mappings) {
|
||||
$merged['field_mappings'][$typeKey] = $mappings;
|
||||
}
|
||||
}
|
||||
|
||||
// 合并格式化函数
|
||||
if (isset($pluginConfig['formatters'])) {
|
||||
if (!isset($merged['formatters'])) {
|
||||
$merged['formatters'] = [];
|
||||
}
|
||||
$merged['formatters'] = array_merge($merged['formatters'], $pluginConfig['formatters']);
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建字段映射索引
|
||||
*/
|
||||
protected static function buildFieldMappings(): void
|
||||
{
|
||||
self::$fieldMappings = [];
|
||||
$config = self::$configCache;
|
||||
|
||||
// 1. 处理所有审核类型
|
||||
foreach ($config['field_mappings'] ?? [] as $typeKey => $mapping) {
|
||||
if (is_string($mapping) && class_exists($mapping)) {
|
||||
// 简单映射:类型 => 模型类
|
||||
self::$fieldMappings[$typeKey] = [
|
||||
'model' => $mapping,
|
||||
'fields' => [],
|
||||
];
|
||||
} elseif (is_array($mapping)) {
|
||||
// 详细映射配置
|
||||
self::$fieldMappings[$typeKey] = $mapping;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 获取 morph_map 映射(用于通过别名查找模型)
|
||||
$morphMap = config('morph_map.map', []);
|
||||
|
||||
// 3. 关联 morph_map 和 field_mappings
|
||||
foreach ($morphMap as $alias => $modelClass) {
|
||||
// 如果 field_mappings 中有此模型的配置,建立关联
|
||||
foreach (self::$fieldMappings as $typeKey => $mapping) {
|
||||
$mappingModel = $mapping['model'] ?? null;
|
||||
if ($mappingModel && $mappingModel === $modelClass) {
|
||||
// 建立别名到配置的映射
|
||||
self::$fieldMappings['_by_morph'][$alias] = $typeKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化格式化函数
|
||||
*/
|
||||
protected static function initFormatters(): void
|
||||
{
|
||||
$config = self::$configCache;
|
||||
self::$formatters = $config['formatters'] ?? [];
|
||||
|
||||
// 内置格式化函数
|
||||
$builtinFormatters = [
|
||||
'trim' => fn($value) => trim($value ?? ''),
|
||||
'html_to_text' => fn($html) => strip_tags($html ?? ''),
|
||||
'implode' => function($value, $glue = ',') {
|
||||
if (is_array($value)) {
|
||||
return implode($glue, $value);
|
||||
}
|
||||
return $value;
|
||||
},
|
||||
'datetime' => function($timestamp, $format = 'Y-m-d H:i:s') {
|
||||
if (is_numeric($timestamp)) {
|
||||
return date($format, (int)$timestamp);
|
||||
}
|
||||
return $timestamp;
|
||||
},
|
||||
'limit' => function($text, $length = 50) {
|
||||
return Str::limit($text, $length);
|
||||
},
|
||||
];
|
||||
|
||||
self::$formatters = array_merge($builtinFormatters, self::$formatters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字段值
|
||||
*/
|
||||
protected static function formatValue($value, ?array $fieldConfig): mixed
|
||||
{
|
||||
if ($value === null || empty($fieldConfig)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// 应用格式化函数
|
||||
$format = $fieldConfig['format'] ?? null;
|
||||
if ($format) {
|
||||
// 处理带参数的格式化函数,如 "implode:、"
|
||||
if (is_string($format) && str_contains($format, ':')) {
|
||||
[$func, $param] = explode(':', $format, 2);
|
||||
$formatter = self::$formatters[$func] ?? null;
|
||||
if ($formatter) {
|
||||
return $formatter($value, $param);
|
||||
}
|
||||
} elseif (isset(self::$formatters[$format])) {
|
||||
return self::$formatters[$format]($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 morph_map 别名获取字段配置
|
||||
*/
|
||||
protected static function getFieldConfigByMorphAlias(string $morphAlias): ?array
|
||||
{
|
||||
$config = self::$fieldMappings['_by_morph'][$morphAlias] ?? null;
|
||||
if (!$config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::$fieldMappings[$config] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据审核记录获取字段映射配置
|
||||
*/
|
||||
protected static function getFieldConfig(Review $review): ?array
|
||||
{
|
||||
$reviewableType = $review->reviewable_type;
|
||||
|
||||
// 1. 首先尝试通过 morph_map 别名查找
|
||||
$morphMap = config('morph_map.map', []);
|
||||
$morphAlias = array_search($reviewableType, $morphMap, true);
|
||||
|
||||
if ($morphAlias !== false) {
|
||||
$config = self::getFieldConfigByMorphAlias($morphAlias);
|
||||
if ($config) {
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 直接通过 reviewable_type(完整类名)查找
|
||||
return self::$fieldMappings[$reviewableType] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个字段的值
|
||||
*/
|
||||
protected static function getFieldValue($model, string $fieldName, array $fieldConfig): mixed
|
||||
{
|
||||
if (!$model || empty($fieldConfig)) {
|
||||
return $fieldConfig['fallback'] ?? null;
|
||||
}
|
||||
|
||||
$type = $fieldConfig['type'] ?? 'attribute';
|
||||
$source = $fieldConfig['source'] ?? $fieldName;
|
||||
|
||||
switch ($type) {
|
||||
case 'attribute':
|
||||
// 直接属性
|
||||
$value = $model->{$source} ?? null;
|
||||
break;
|
||||
|
||||
case 'relation':
|
||||
// 通过关联获取
|
||||
if (method_exists($model, $source)) {
|
||||
$relation = $model->{$source};
|
||||
if ($relation) {
|
||||
$attribute = $fieldConfig['attribute'] ?? 'name';
|
||||
$value = $relation->{$attribute} ?? null;
|
||||
} else {
|
||||
$value = null;
|
||||
}
|
||||
} else {
|
||||
$value = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'callback':
|
||||
// 回调函数
|
||||
$callback = $fieldConfig['callback'] ?? null;
|
||||
if (is_callable($callback)) {
|
||||
$value = $callback($model);
|
||||
} else {
|
||||
$value = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fixed':
|
||||
// 固定值
|
||||
$value = $fieldConfig['value'] ?? null;
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = null;
|
||||
}
|
||||
|
||||
// 应用默认值
|
||||
if ($value === null || $value === '') {
|
||||
$value = $fieldConfig['fallback'] ?? null;
|
||||
}
|
||||
|
||||
// 格式化
|
||||
return self::formatValue($value, $fieldConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射审核记录字段
|
||||
*
|
||||
* @param Review $review 审核记录
|
||||
* @param array $extraFields 额外需要映射的字段
|
||||
* @return array 映射后的字段数组
|
||||
*/
|
||||
public static function mapReview(Review $review, array $extraFields = []): array
|
||||
{
|
||||
self::init();
|
||||
|
||||
$reviewable = $review->reviewable;
|
||||
$fieldConfig = self::getFieldConfig($review);
|
||||
|
||||
// 如果找不到配置,返回基本信息
|
||||
if (!$fieldConfig || !$reviewable) {
|
||||
return self::getFallbackFields($review);
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
|
||||
// 1. 映射标准字段
|
||||
$standardFields = ['title', 'content', 'applicant'];
|
||||
foreach ($standardFields as $field) {
|
||||
$fieldConfigItem = $fieldConfig['fields'][$field] ?? self::$configCache['default_field_mappings'][$field] ?? null;
|
||||
$fields[$field] = self::getFieldValue($reviewable, $field, $fieldConfigItem);
|
||||
}
|
||||
|
||||
// 2. 映射额外字段
|
||||
foreach ($extraFields as $field) {
|
||||
if (isset($fieldConfig['fields'][$field])) {
|
||||
$fields[$field] = self::getFieldValue($reviewable, $field, $fieldConfig['fields'][$field]);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 添加审核基础信息
|
||||
$fields = array_merge($fields, self::getBaseReviewFields($review, $fieldConfig));
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量映射审核记录
|
||||
*/
|
||||
public static function mapReviews(Collection $reviews, array $extraFields = []): Collection
|
||||
{
|
||||
return $reviews->map(function (Review $review) use ($extraFields) {
|
||||
$mapped = self::mapReview($review, $extraFields);
|
||||
|
||||
// 将映射字段合并到审核记录中
|
||||
foreach ($mapped as $key => $value) {
|
||||
if (!isset($review->{$key})) {
|
||||
$review->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $review;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核基础字段(无需配置)
|
||||
*/
|
||||
protected static function getBaseReviewFields(Review $review, ?array $fieldConfig = null): array
|
||||
{
|
||||
return [
|
||||
'id' => $review->id,
|
||||
'reviewable_type' => $review->reviewable_type,
|
||||
'reviewable_id' => $review->reviewable_id,
|
||||
'status' => $review->status,
|
||||
'status_text' => $review->status_text,
|
||||
'reason' => $review->reason,
|
||||
'reviewer_id' => $review->reviewer_id,
|
||||
'reviewer_name' => $review->reviewer?->real_name,
|
||||
'reviewed_at' => $review->reviewed_at,
|
||||
'created_at' => $review->created_at,
|
||||
'updated_at' => $review->updated_at,
|
||||
'display_name' => $fieldConfig['display_name'] ?? $review->reviewable_type,
|
||||
'morph_alias' => self::getMorphAlias($review->reviewable_type),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 fallback 字段(当找不到配置时)
|
||||
*/
|
||||
protected static function getFallbackFields(Review $review): array
|
||||
{
|
||||
$reviewable = $review->reviewable;
|
||||
|
||||
return array_merge(
|
||||
self::getBaseReviewFields($review),
|
||||
[
|
||||
'title' => $reviewable ? '已删除的数据' : '数据不存在',
|
||||
'content' => '',
|
||||
'applicant' => '未知',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过类名获取 morph_map 别名
|
||||
*/
|
||||
protected static function getMorphAlias(string $className): ?string
|
||||
{
|
||||
$morphMap = config('morph_map.map', []);
|
||||
return array_search($className, $morphMap, true) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已配置的审核类型
|
||||
*/
|
||||
public static function getConfiguredTypes(): array
|
||||
{
|
||||
self::init();
|
||||
|
||||
$types = [];
|
||||
foreach (self::$fieldMappings as $key => $config) {
|
||||
if ($key === '_by_morph') continue;
|
||||
|
||||
$types[$key] = [
|
||||
'display_name' => $config['display_name'] ?? $key,
|
||||
'model' => $config['model'] ?? null,
|
||||
'morph_alias' => self::getMorphAlias($config['model'] ?? ''),
|
||||
'priority' => $config['priority'] ?? 0,
|
||||
'icon' => $config['extra']['icon'] ?? null,
|
||||
'color' => $config['extra']['color'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
// 按优先级排序
|
||||
uasort($types, fn($a, $b) => ($b['priority'] ?? 0) <=> ($a['priority'] ?? 0));
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的字段配置
|
||||
*/
|
||||
public static function getTypeConfig(string $typeKey): ?array
|
||||
{
|
||||
self::init();
|
||||
return self::$fieldMappings[$typeKey] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\service\admin\review;
|
||||
|
||||
use app\adminapi\CurrentUser;
|
||||
use app\dao\review\ReviewDao;
|
||||
use app\enum\review\ReviewStatus;
|
||||
use app\adminapi\event\ReviewApprovedEvent;
|
||||
use app\adminapi\event\ReviewRejectedEvent;
|
||||
use app\model\review\Review;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\AdminException;
|
||||
use Illuminate\Support\Collection;
|
||||
use support\Container;
|
||||
|
||||
/**
|
||||
* 审核服务类
|
||||
* 支持两种审核模式:
|
||||
* 1. 简单审核模式:直接审核通过/拒绝
|
||||
* 2. 审批流模式:关联第三方审批流实例,由审批流模块处理
|
||||
*/
|
||||
class ReviewService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public function __construct(ReviewDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建审核记录
|
||||
*
|
||||
* @param string $reviewableType 模型类型
|
||||
* @param int|string $reviewableId 模型ID
|
||||
* @param array $options 额外参数
|
||||
* - reason: string 审核原因
|
||||
* - flow_instance_id: string|null 审批流实例ID(传入则使用审批流模式)
|
||||
* - extra_data: array 扩展数据
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model|null
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function createReview(string $reviewableType, int|string $reviewableId, array $options = []): ?\Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
try {
|
||||
// 检查是否已存在审核记录(无论状态,避免重复创建)
|
||||
$exists = $this->dao->getByReviewable($reviewableType, $reviewableId);
|
||||
if ($exists) {
|
||||
throw new AdminException('该记录已有审核记录');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'reviewable_type' => $reviewableType,
|
||||
'reviewable_id' => $reviewableId,
|
||||
'status' => ReviewStatus::PENDING->value,
|
||||
];
|
||||
|
||||
// 审核原因
|
||||
if (!empty($options['reason'])) {
|
||||
$data['reason'] = $options['reason'];
|
||||
}
|
||||
|
||||
// 审批流实例ID(审批流模式)
|
||||
if (!empty($options['flow_instance_id'])) {
|
||||
$data['flow_instance_id'] = $options['flow_instance_id'];
|
||||
}
|
||||
|
||||
// 扩展数据
|
||||
if (!empty($options['extra_data'])) {
|
||||
$data['extra_data'] = $options['extra_data'];
|
||||
}
|
||||
|
||||
return $this->dao->save($data);
|
||||
} catch (\Throwable $e) {
|
||||
throw new AdminException('创建审核记录失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过
|
||||
*
|
||||
* @param int|string $id 审核记录ID
|
||||
* @param int|null $reviewerId 审核人ID(默认当前用户)
|
||||
* @param array $options 额外参数
|
||||
* - reason: string 审核原因/备注
|
||||
* - flow_callback: bool 是否为审批流回调(内部使用)
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function approve(int|string $id, ?int $reviewerId = null, array $options = []): bool
|
||||
{
|
||||
return $this->transaction(function () use ($id, $reviewerId, $options) {
|
||||
return $this->approveWithoutTransaction($id, $reviewerId, $options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过(不带事务)
|
||||
* 用于事件监听器等已经在事务中的场景
|
||||
*
|
||||
* @param int|string $id 审核记录ID
|
||||
* @param int|null $reviewerId 审核人ID(默认当前用户)
|
||||
* @param array $options 额外参数
|
||||
* - reason: string 审核原因/备注
|
||||
* - flow_callback: bool 是否为审批流回调(内部使用)
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function approveWithoutTransaction(int|string $id, ?int $reviewerId = null, array $options = []): bool
|
||||
{
|
||||
$review = $this->dao->get($id);
|
||||
if (empty($review)) {
|
||||
throw new AdminException('审核记录不存在');
|
||||
}
|
||||
if ($review->status !== ReviewStatus::PENDING->value) {
|
||||
throw new AdminException('该记录已审核');
|
||||
}
|
||||
|
||||
// 判断审核模式
|
||||
if ($review->isWorkflowMode() && empty($options['flow_callback'])) {
|
||||
// 审批流模式:调用第三方审批流模块
|
||||
return $this->handleWorkflowApprove($review, $reviewerId, $options);
|
||||
}
|
||||
|
||||
// 简单审核模式:直接审核通过
|
||||
$reviewerId = $reviewerId ?? config('review.auto_review.reviewer_id', 0);
|
||||
$review->status = ReviewStatus::APPROVED->value;
|
||||
$review->reviewer_id = $reviewerId;
|
||||
$review->reviewed_at = time();
|
||||
|
||||
if (!empty($options['reason'])) {
|
||||
$review->reason = $options['reason'];
|
||||
}
|
||||
|
||||
$review->save();
|
||||
|
||||
// 触发审核通过事件
|
||||
$event = new ReviewApprovedEvent($review);
|
||||
$event->dispatch();
|
||||
|
||||
// 执行审核通过回调(配置中的回调)
|
||||
$this->executeApprovedCallbacks($review);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行审核通过回调
|
||||
*
|
||||
* @param \app\model\review\Review $review
|
||||
* @return void
|
||||
*/
|
||||
protected function executeApprovedCallbacks($review): void
|
||||
{
|
||||
try {
|
||||
// 通过反射获取 ReviewFieldMapper 的 configCache(包含完整插件配置)
|
||||
$typeKey = $this->getReviewTypeKey($review);
|
||||
$typeConfig = $this->getReviewTypeConfigFromMapper($typeKey);
|
||||
|
||||
if (empty($typeConfig['callbacks']['approved'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callbacks = $typeConfig['callbacks']['approved'];
|
||||
|
||||
// 支持单个类名或类名数组
|
||||
if (is_string($callbacks)) {
|
||||
$callbacks = [$callbacks];
|
||||
}
|
||||
|
||||
foreach ($callbacks as $class) {
|
||||
if (!class_exists($class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$instance = new $class();
|
||||
|
||||
if (method_exists($instance, 'handle')) {
|
||||
$instance->handle($review);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 回调执行失败不影响审核流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核类型 key
|
||||
*/
|
||||
protected function getReviewTypeKey($review): string
|
||||
{
|
||||
// 尝试通过 morph_map 查找别名
|
||||
$morphMap = config('morph_map.map', []);
|
||||
$alias = array_search($review->reviewable_type, $morphMap, true);
|
||||
|
||||
return $alias !== false ? $alias : $review->reviewable_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ReviewFieldMapper 获取完整类型配置
|
||||
*/
|
||||
protected function getReviewTypeConfigFromMapper(string $typeKey): ?array
|
||||
{
|
||||
try {
|
||||
// 先调用 getTypeConfig 确保 init() 被执行,configCache 被填充
|
||||
ReviewFieldMapper::getTypeConfig($typeKey);
|
||||
|
||||
// 使用反射获取 configCache
|
||||
$reflection = new \ReflectionClass(ReviewFieldMapper::class);
|
||||
$property = $reflection->getProperty('configCache');
|
||||
$property->setAccessible(true);
|
||||
$configCache = $property->getValue();
|
||||
|
||||
// configCache 结构: ['types' => [...], 'field_mappings' => [...]]
|
||||
return $configCache['types'][$typeKey] ?? null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核拒绝
|
||||
*
|
||||
* @param int|string $id 审核记录ID
|
||||
* @param string $reason 拒绝原因
|
||||
* @param int|null $reviewerId 审核人ID(默认当前用户)
|
||||
* @param array $options 额外参数
|
||||
* - flow_callback: bool 是否为审批流回调(内部使用)
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function reject(int|string $id, string $reason = '', ?int $reviewerId = null, array $options = []): bool
|
||||
{
|
||||
return $this->transaction(function () use ($id, $reason, $reviewerId, $options) {
|
||||
return $this->rejectWithoutTransaction($id, $reason, $reviewerId, $options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核拒绝(不带事务)
|
||||
* 用于事件监听器等已经在事务中的场景
|
||||
*
|
||||
* @param int|string $id 审核记录ID
|
||||
* @param string $reason 拒绝原因
|
||||
* @param int|null $reviewerId 审核人ID(默认当前用户)
|
||||
* @param array $options 额外参数
|
||||
* - flow_callback: bool 是否为审批流回调(内部使用)
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function rejectWithoutTransaction(int|string $id, string $reason = '', ?int $reviewerId = null, array $options = []): bool
|
||||
{
|
||||
$review = $this->dao->get($id);
|
||||
if (empty($review)) {
|
||||
throw new AdminException('审核记录不存在');
|
||||
}
|
||||
if ($review->status !== ReviewStatus::PENDING->value) {
|
||||
throw new AdminException('该记录已审核');
|
||||
}
|
||||
|
||||
// 判断审核模式
|
||||
if ($review->isWorkflowMode() && empty($options['flow_callback'])) {
|
||||
// 审批流模式:调用第三方审批流模块
|
||||
return $this->handleWorkflowReject($review, $reason, $reviewerId, $options);
|
||||
}
|
||||
|
||||
// 简单审核模式:直接审核拒绝
|
||||
$reviewerId = $reviewerId ?? config('review.auto_review.reviewer_id', 0);
|
||||
$review->status = ReviewStatus::REJECTED->value;
|
||||
$review->reviewer_id = $reviewerId;
|
||||
$review->reviewed_at = time();
|
||||
if (!empty($reason)) {
|
||||
$review->reason = $reason;
|
||||
}
|
||||
$review->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量审核通过
|
||||
*
|
||||
* @param array $ids 审核记录ID数组
|
||||
* @param array $options 额外参数
|
||||
*
|
||||
* @return int 审核通过的数量
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function batchApprove(array $ids, array $options = []): int
|
||||
{
|
||||
return $this->transaction(function () use ($ids, $options) {
|
||||
// 直接使用 whereIn 查询待审核的记录,避免 selectList 的条件处理问题
|
||||
$pendingReviews = $this->dao->getModel()
|
||||
->whereIn('id', $ids)
|
||||
->where('status', ReviewStatus::PENDING->value)
|
||||
->get(['id']);
|
||||
|
||||
$pendingIds = $pendingReviews->pluck('id')->toArray();
|
||||
|
||||
// 调试日志
|
||||
\support\Log::info('batchApprove', [
|
||||
'input_ids' => $ids,
|
||||
'pending_ids' => $pendingIds,
|
||||
'options' => $options
|
||||
]);
|
||||
|
||||
if (empty($pendingIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$reviewerId = Container::make(CurrentUser::class)->id();
|
||||
$reviewerId = $reviewerId ? (int)$reviewerId : null;
|
||||
$count = 0;
|
||||
|
||||
// 批量更新待审核的记录
|
||||
foreach ($pendingIds as $id) {
|
||||
try {
|
||||
$review = $this->dao->get($id);
|
||||
if (empty($review)) {
|
||||
\support\Log::warning("Review not found: {$id}");
|
||||
continue;
|
||||
}
|
||||
if ($review->status !== ReviewStatus::PENDING->value) {
|
||||
\support\Log::warning("Review not pending: {$id}, status: {$review->status}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断审核模式
|
||||
if ($review->isWorkflowMode() && empty($options['flow_callback'])) {
|
||||
// 审批流模式:调用第三方审批流模块
|
||||
$this->handleWorkflowApprove($review, $reviewerId, $options);
|
||||
} else {
|
||||
// 简单审核模式:直接审核通过
|
||||
$review->status = ReviewStatus::APPROVED->value;
|
||||
$review->reviewer_id = $reviewerId;
|
||||
$review->reviewed_at = time();
|
||||
|
||||
if (!empty($options['reason'])) {
|
||||
$review->reason = $options['reason'];
|
||||
}
|
||||
|
||||
$review->save();
|
||||
|
||||
$event = new ReviewApprovedEvent($review);
|
||||
$event->dispatch();
|
||||
}
|
||||
|
||||
$count++;
|
||||
\support\Log::info("Approved review: {$id}, count: {$count}");
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但继续处理其他记录
|
||||
\support\Log::error("Failed to approve review: {$id}", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
\support\Log::info('batchApprove completed', ['total_count' => $count]);
|
||||
return $count;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量审核拒绝
|
||||
*
|
||||
* @param array $ids 审核记录ID数组
|
||||
* @param string $reason 拒绝原因
|
||||
* @param array $options 额外参数
|
||||
*
|
||||
* @return int 审核拒绝的数量
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function batchReject(array $ids, string $reason = '', array $options = []): int
|
||||
{
|
||||
return $this->transaction(function () use ($ids, $reason, $options) {
|
||||
// 直接使用 whereIn 查询待审核的记录,避免 selectList 的条件处理问题
|
||||
$pendingReviews = $this->dao->getModel()
|
||||
->whereIn('id', $ids)
|
||||
->where('status', ReviewStatus::PENDING->value)
|
||||
->get(['id']);
|
||||
|
||||
$pendingIds = $pendingReviews->pluck('id')->toArray();
|
||||
|
||||
// 调试日志
|
||||
\support\Log::info('batchReject', [
|
||||
'input_ids' => $ids,
|
||||
'pending_ids' => $pendingIds,
|
||||
'reason' => $reason,
|
||||
'options' => $options
|
||||
]);
|
||||
|
||||
if (empty($pendingIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$reviewerId = Container::make(CurrentUser::class)->id();
|
||||
$reviewerId = $reviewerId ? (int)$reviewerId : null;
|
||||
$count = 0;
|
||||
|
||||
// 批量更新待审核的记录
|
||||
foreach ($pendingIds as $id) {
|
||||
try {
|
||||
$review = $this->dao->get($id);
|
||||
if (empty($review)) {
|
||||
\support\Log::warning("Review not found: {$id}");
|
||||
continue;
|
||||
}
|
||||
if ($review->status !== ReviewStatus::PENDING->value) {
|
||||
\support\Log::warning("Review not pending: {$id}, status: {$review->status}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断审核模式
|
||||
if ($review->isWorkflowMode() && empty($options['flow_callback'])) {
|
||||
// 审批流模式:调用第三方审批流模块
|
||||
$this->handleWorkflowReject($review, $reason, $reviewerId, $options);
|
||||
} else {
|
||||
// 简单审核模式:直接审核拒绝
|
||||
$review->status = ReviewStatus::REJECTED->value;
|
||||
$review->reviewer_id = $reviewerId;
|
||||
$review->reviewed_at = time();
|
||||
if (!empty($reason)) {
|
||||
$review->reason = $reason;
|
||||
}
|
||||
$review->save();
|
||||
|
||||
$event = new ReviewRejectedEvent($review);
|
||||
$event->dispatch();
|
||||
}
|
||||
|
||||
$count++;
|
||||
\support\Log::info("Rejected review: {$id}, count: {$count}");
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但继续处理其他记录
|
||||
\support\Log::error("Failed to reject review: {$id}", [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
\support\Log::info('batchReject completed', ['total_count' => $count]);
|
||||
return $count;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批流回调 - 审核通过
|
||||
* 由第三方审批流模块调用,更新审核状态
|
||||
*
|
||||
* @param string $flowInstanceId 审批流实例ID
|
||||
* @param int|null $reviewerId 审核人ID
|
||||
* @param string $reason 审核原因
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function flowApproveCallback(string $flowInstanceId, ?int $reviewerId = null, string $reason = ''): bool
|
||||
{
|
||||
$review = $this->dao->getOne(['flow_instance_id' => $flowInstanceId]);
|
||||
if (empty($review)) {
|
||||
throw new AdminException('审核记录不存在');
|
||||
}
|
||||
|
||||
return $this->approve($review->id, $reviewerId, [
|
||||
'reason' => $reason,
|
||||
'flow_callback' => true, // 标记为审批流回调
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批流回调 - 审核拒绝
|
||||
* 由第三方审批流模块调用,更新审核状态
|
||||
*
|
||||
* @param string $flowInstanceId 审批流实例ID
|
||||
* @param string $reason 拒绝原因
|
||||
* @param int|null $reviewerId 审核人ID
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function flowRejectCallback(string $flowInstanceId, string $reason = '', ?int $reviewerId = null): bool
|
||||
{
|
||||
$review = $this->dao->getOne(['flow_instance_id' => $flowInstanceId]);
|
||||
if (empty($review)) {
|
||||
throw new AdminException('审核记录不存在');
|
||||
}
|
||||
|
||||
return $this->reject($review->id, $reason, $reviewerId, [
|
||||
'flow_callback' => true, // 标记为审批流回调
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核统计
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getStatistics(): array
|
||||
{
|
||||
$total = $this->dao->count();
|
||||
$pending = $this->dao->getPendingCount();
|
||||
$approved = $this->dao->count(['status' => ReviewStatus::APPROVED->value]);
|
||||
$rejected = $this->dao->count(['status' => ReviewStatus::REJECTED->value]);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'pending' => $pending,
|
||||
'approved' => $approved,
|
||||
'rejected' => $rejected,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联对象获取审核记录
|
||||
*
|
||||
* @param string $reviewableType 模型类型
|
||||
* @param int|string $reviewableId 模型ID
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getByReviewable(string $reviewableType, int|string $reviewableId): ?\Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
return $this->dao->getByReviewable($reviewableType, $reviewableId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据审批流实例ID获取审核记录
|
||||
*
|
||||
* @param string $flowInstanceId 审批流实例ID
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getByFlowInstanceId(string $flowInstanceId): ?\Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
return $this->dao->getOne(['flow_instance_id' => $flowInstanceId]);
|
||||
}
|
||||
|
||||
// ==================== 审批流模式处理 ====================
|
||||
|
||||
/**
|
||||
* 处理审批流模式 - 审核通过
|
||||
* 调用第三方审批流模块的审核通过接口
|
||||
*
|
||||
* @param Review $review 审核记录
|
||||
* @param int|null $reviewerId 审核人ID
|
||||
* @param array $options 额外参数
|
||||
*
|
||||
* @return bool
|
||||
* @throws AdminException
|
||||
*/
|
||||
protected function handleWorkflowApprove(Review $review, ?int $reviewerId, array $options): bool
|
||||
{
|
||||
// 调用第三方审批流模块的审核通过接口
|
||||
// 示例:
|
||||
// $flowService = app(config('review.flow_service'));
|
||||
// return $flowService->approve($review->flow_instance_id, $reviewerId, $options);
|
||||
|
||||
// 未配置第三方审批流模块时,抛出异常
|
||||
throw new AdminException('审批流模块未配置,请联系管理员');
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理审批流模式 - 审核拒绝
|
||||
* 调用第三方审批流模块的审核拒绝接口
|
||||
*
|
||||
* @param Review $review 审核记录
|
||||
* @param string $reason 拒绝原因
|
||||
* @param int|null $reviewerId 审核人ID
|
||||
* @param array $options 额外参数
|
||||
*
|
||||
* @return bool
|
||||
* @throws AdminException
|
||||
*/
|
||||
protected function handleWorkflowReject(Review $review, string $reason, ?int $reviewerId, array $options): bool
|
||||
{
|
||||
// 调用第三方审批流模块的审核拒绝接口
|
||||
// 示例:
|
||||
// $flowService = app(config('review.flow_service'));
|
||||
// return $flowService->reject($review->flow_instance_id, $reason, $reviewerId, $options);
|
||||
|
||||
// 未配置第三方审批流模块时,抛出异常
|
||||
throw new AdminException('审批流模块未配置,请联系管理员');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取映射后的审核列表(支持插件配置)
|
||||
*
|
||||
* @param array $where 查询条件
|
||||
* @param string $field 查询字段
|
||||
* @param int $page 页码
|
||||
* @param int $limit 每页数量
|
||||
* @param string $order 排序
|
||||
* @param array $with 关联预加载
|
||||
* @param bool $search 是否搜索模式
|
||||
*
|
||||
* @return Collection
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getMappedList(array $where = [], string $field = '*', int $page = 0, int $limit = 0, string $order = '', array $with = [], bool $search = false): Collection
|
||||
{
|
||||
$reviews = $this->dao->selectList($where, $field, $page, $limit, $order, $with, $search);
|
||||
return ReviewFieldMapper::mapReviews($reviews);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条映射后的审核记录
|
||||
*
|
||||
* @param int|string $id 审核记录ID
|
||||
* @param array $extraFields 额外需要映射的字段
|
||||
*
|
||||
* @return array|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getMappedReview(int|string $id, array $extraFields = []): ?array
|
||||
{
|
||||
$review = $this->dao->get($id,['*'], ['reviewer', 'reviewable']);
|
||||
if (!$review) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReviewFieldMapper::mapReview($review, $extraFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已配置的审核类型(用于前端下拉选择)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getReviewTypes(): array
|
||||
{
|
||||
return ReviewFieldMapper::getConfiguredTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定审核类型的配置信息
|
||||
*
|
||||
* @param string $typeKey 类型键(morph_map别名)
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getReviewTypeConfig(string $typeKey): ?array
|
||||
{
|
||||
return ReviewFieldMapper::getTypeConfig($typeKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建审核记录(增强版,自动填充extra_data)
|
||||
* 简化版:直接调用基础方法,extra_data由调用方提供
|
||||
*
|
||||
* @param string $reviewableType 模型类型(可以是类名或morph_map别名)
|
||||
* @param int|string $reviewableId 模型ID
|
||||
* @param array $options 额外参数
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model|null
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function createReviewWithData(string $reviewableType, int|string $reviewableId, array $options = []): ?\Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
return $this->createReview($reviewableType, $reviewableId, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据morph_map别名获取模型类名
|
||||
*/
|
||||
protected function getModelClassFromMorphAlias(string $alias): ?string
|
||||
{
|
||||
$morphMap = config('morph_map.map', []);
|
||||
return $morphMap[$alias] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置构建extra_data
|
||||
*/
|
||||
protected function buildExtraDataFromConfig($reviewable, array $fieldConfigs): array
|
||||
{
|
||||
$extraData = [];
|
||||
|
||||
foreach ($fieldConfigs as $fieldName => $config) {
|
||||
if (in_array($fieldName, ['title', 'content', 'applicant'])) {
|
||||
// 获取字段值
|
||||
$value = $this->getFieldValueFromConfig($reviewable, $fieldName, $config);
|
||||
$extraData[$fieldName] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $extraData;
|
||||
}
|
||||
|
||||
// ==================== 搜索处理相关方法 ====================
|
||||
|
||||
/**
|
||||
* 获取数量(支持 extra_data 字段搜索)
|
||||
*
|
||||
* @param array $where
|
||||
* @param bool $search
|
||||
*
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getCountWithSearch(array $where = [], bool $search = false): int
|
||||
{
|
||||
return $this->dao->count($where, $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置获取字段值
|
||||
*
|
||||
* @param mixed $model 模型对象
|
||||
* @param string $fieldName 字段名
|
||||
* @param array $fieldConfig 字段配置
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getFieldValueFromConfig($model, string $fieldName, array $fieldConfig): mixed
|
||||
{
|
||||
if (!$model || empty($fieldConfig)) {
|
||||
return $fieldConfig['fallback'] ?? null;
|
||||
}
|
||||
|
||||
$type = $fieldConfig['type'] ?? 'attribute';
|
||||
$source = $fieldConfig['source'] ?? $fieldName;
|
||||
|
||||
switch ($type) {
|
||||
case 'attribute':
|
||||
// 直接获取属性
|
||||
return $model->{$source} ?? ($fieldConfig['fallback'] ?? null);
|
||||
|
||||
case 'relation':
|
||||
// 通过关联获取
|
||||
if ($model->{$source} && isset($fieldConfig['attribute'])) {
|
||||
return $model->{$source}->{$fieldConfig['attribute']} ?? ($fieldConfig['fallback'] ?? null);
|
||||
}
|
||||
return $fieldConfig['fallback'] ?? null;
|
||||
|
||||
case 'callback':
|
||||
// 回调函数
|
||||
if (isset($fieldConfig['callback']) && is_callable($fieldConfig['callback'])) {
|
||||
return call_user_func($fieldConfig['callback'], $model);
|
||||
}
|
||||
return $fieldConfig['fallback'] ?? null;
|
||||
|
||||
case 'fixed':
|
||||
// 固定值
|
||||
return $fieldConfig['value'] ?? ($fieldConfig['fallback'] ?? null);
|
||||
|
||||
default:
|
||||
return $fieldConfig['fallback'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user