OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
170
app/Core/WorkStructure/Services/MappingService.php
Normal file
170
app/Core/WorkStructure/Services/MappingService.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\WorkStructure\Services;
|
||||
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Core\WorkStructure\Models\WorkStructure;
|
||||
use Leantime\Core\WorkStructure\Repositories\WorkStructureRepository;
|
||||
|
||||
/**
|
||||
* Cross-structure mapping query service.
|
||||
*
|
||||
* Resolves element type mappings between different work structures
|
||||
* (e.g., Logic Model "output" → Project "milestone").
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class MappingService
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
/**
|
||||
* @param WorkStructureRepository $repo WorkStructure repository
|
||||
*/
|
||||
public function __construct(
|
||||
private WorkStructureRepository $repo
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all mappings between two structures.
|
||||
*
|
||||
* @param int $sourceStructureId Source structure ID
|
||||
* @param int $targetStructureId Target structure ID
|
||||
* @return \Leantime\Core\WorkStructure\Models\StructureMapping[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getMappings(int $sourceStructureId, int $targetStructureId): array
|
||||
{
|
||||
return $this->repo->getMappings($sourceStructureId, $targetStructureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target element type key for a given source element type key.
|
||||
*
|
||||
* @param int $sourceStructureId Source structure ID
|
||||
* @param string $sourceTypeKey Source element type key
|
||||
* @param int $targetStructureId Target structure ID
|
||||
* @return string|null Target element type key, or null if no mapping exists
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getTargetElementKey(int $sourceStructureId, string $sourceTypeKey, int $targetStructureId): ?string
|
||||
{
|
||||
$sourceElement = $this->repo->getElementByTypeKey($sourceStructureId, $sourceTypeKey);
|
||||
|
||||
if ($sourceElement === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mappings = $this->repo->getMappings($sourceStructureId, $targetStructureId);
|
||||
|
||||
// Resolve target element IDs → typeKeys with a single fetch instead of
|
||||
// re-querying all target elements inside the mappings loop.
|
||||
$targetIdToKey = [];
|
||||
foreach ($this->repo->getElements($targetStructureId) as $el) {
|
||||
$targetIdToKey[$el->id] = $el->typeKey;
|
||||
}
|
||||
|
||||
foreach ($mappings as $mapping) {
|
||||
if ($mapping->sourceElementId === $sourceElement->id) {
|
||||
return $targetIdToKey[$mapping->targetElementId] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all target structures that have mappings from a given source structure.
|
||||
*
|
||||
* Each returned structure includes its elements populated.
|
||||
*
|
||||
* @param int $sourceStructureId Source structure ID
|
||||
* @return WorkStructure[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getTargetStructures(int $sourceStructureId): array
|
||||
{
|
||||
$targetIds = $this->repo->getTargetStructureIds($sourceStructureId);
|
||||
$structures = [];
|
||||
|
||||
foreach ($targetIds as $targetId) {
|
||||
$structure = $this->repo->getStructure($targetId);
|
||||
|
||||
if ($structure !== null) {
|
||||
$structure->elements = $this->repo->getElements($targetId);
|
||||
$structures[] = $structure;
|
||||
}
|
||||
}
|
||||
|
||||
return $structures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get target element type keys that have mappings from the source structure.
|
||||
*
|
||||
* @param int $sourceStructureId Source structure ID
|
||||
* @param int $targetStructureId Target structure ID
|
||||
* @return string[] Array of target element typeKeys (e.g., ['milestone', 'task', 'goal'])
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getMappedElementKeys(int $sourceStructureId, int $targetStructureId): array
|
||||
{
|
||||
$mappings = $this->repo->getMappings($sourceStructureId, $targetStructureId);
|
||||
$targetElements = $this->repo->getElements($targetStructureId);
|
||||
|
||||
$elementIdToKey = [];
|
||||
foreach ($targetElements as $el) {
|
||||
$elementIdToKey[$el->id] = $el->typeKey;
|
||||
}
|
||||
|
||||
$keys = [];
|
||||
foreach ($mappings as $mapping) {
|
||||
$key = $elementIdToKey[$mapping->targetElementId] ?? null;
|
||||
if ($key !== null && ! in_array($key, $keys, true)) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source element type key for a given target element type key.
|
||||
*
|
||||
* @param int $targetStructureId Target structure ID
|
||||
* @param string $targetTypeKey Target element type key
|
||||
* @param int $sourceStructureId Source structure ID
|
||||
* @return string|null Source element type key, or null if no mapping exists
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getSourceElementKey(int $targetStructureId, string $targetTypeKey, int $sourceStructureId): ?string
|
||||
{
|
||||
$targetElement = $this->repo->getElementByTypeKey($targetStructureId, $targetTypeKey);
|
||||
|
||||
if ($targetElement === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mappings = $this->repo->getMappings($sourceStructureId, $targetStructureId);
|
||||
|
||||
// Resolve source element IDs → typeKeys with a single fetch instead of
|
||||
// re-querying all source elements inside the mappings loop.
|
||||
$sourceIdToKey = [];
|
||||
foreach ($this->repo->getElements($sourceStructureId) as $el) {
|
||||
$sourceIdToKey[$el->id] = $el->typeKey;
|
||||
}
|
||||
|
||||
foreach ($mappings as $mapping) {
|
||||
if ($mapping->targetElementId === $targetElement->id) {
|
||||
return $sourceIdToKey[$mapping->sourceElementId] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
221
app/Core/WorkStructure/Services/StructureRegistry.php
Normal file
221
app/Core/WorkStructure/Services/StructureRegistry.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\WorkStructure\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Core\WorkStructure\Events\ElementTypeRegistered;
|
||||
use Leantime\Core\WorkStructure\Events\MappingCreated;
|
||||
use Leantime\Core\WorkStructure\Events\StructureRegistered;
|
||||
use Leantime\Core\WorkStructure\Models\WorkStructure;
|
||||
use Leantime\Core\WorkStructure\Repositories\WorkStructureRepository;
|
||||
|
||||
/**
|
||||
* Plugin registration service for work structures.
|
||||
*
|
||||
* Provides an idempotent API for plugins to register their structure
|
||||
* definitions, elements, relationships, and cross-structure mappings.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class StructureRegistry
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
private const CACHE_PREFIX = 'workstructure.';
|
||||
|
||||
/**
|
||||
* @param WorkStructureRepository $repo WorkStructure repository
|
||||
*/
|
||||
public function __construct(
|
||||
private WorkStructureRepository $repo
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Register a structure with elements and relationships (idempotent).
|
||||
*
|
||||
* @param string $title Structure title (unique)
|
||||
* @param string $type 'system', 'plugin', 'custom'
|
||||
* @param array $elements Array of element definitions [{typeKey, label, description?, domainReference?, sortOrder, meta?}]
|
||||
* @param array $relationships Array of relationship defs [{fromTypeKey, toTypeKey, relationshipType, description?}]
|
||||
* @return int Structure ID
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function register(string $title, string $type, array $elements, array $relationships = []): int
|
||||
{
|
||||
// Plugins call this from boot-time register.php. During a fresh install or
|
||||
// a pending update the WorkStructure tables may not exist yet — skip rather
|
||||
// than crash boot; the plugin re-registers idempotently on the next request.
|
||||
if (! $this->schemaReady()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->has($title)) {
|
||||
$structure = $this->get($title);
|
||||
|
||||
// Cache said it exists but the row is gone (deleted/cache drift):
|
||||
// fall through and recreate instead of dereferencing null.
|
||||
if ($structure !== null) {
|
||||
return $structure->id;
|
||||
}
|
||||
|
||||
$this->clearCache($title);
|
||||
}
|
||||
|
||||
$structureId = $this->repo->createStructure([
|
||||
'title' => $title,
|
||||
'type' => $type,
|
||||
]);
|
||||
|
||||
// Add elements
|
||||
$elementIds = [];
|
||||
foreach ($elements as $element) {
|
||||
$elementId = $this->repo->addElement([
|
||||
'structureId' => $structureId,
|
||||
'typeKey' => $element['typeKey'],
|
||||
'label' => $element['label'],
|
||||
'description' => $element['description'] ?? '',
|
||||
'domainReference' => $element['domainReference'] ?? null,
|
||||
'sortOrder' => $element['sortOrder'] ?? 0,
|
||||
'meta' => $element['meta'] ?? null,
|
||||
]);
|
||||
$elementIds[$element['typeKey']] = $elementId;
|
||||
|
||||
ElementTypeRegistered::dispatch($structureId, $element['typeKey'], $element['label']);
|
||||
}
|
||||
|
||||
// Add relationships (resolve typeKey → element ID)
|
||||
foreach ($relationships as $rel) {
|
||||
$fromId = $elementIds[$rel['fromTypeKey']] ?? null;
|
||||
$toId = $elementIds[$rel['toTypeKey']] ?? null;
|
||||
|
||||
if ($fromId !== null && $toId !== null) {
|
||||
$this->repo->addRelationship([
|
||||
'structureId' => $structureId,
|
||||
'fromElementId' => $fromId,
|
||||
'toElementId' => $toId,
|
||||
'relationshipType' => $rel['relationshipType'],
|
||||
'description' => $rel['description'] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearCache($title);
|
||||
StructureRegistered::dispatch($structureId, $title, $type);
|
||||
|
||||
return $structureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register cross-structure mappings (idempotent per source element + target structure).
|
||||
*
|
||||
* @param int $sourceStructureId Source structure ID
|
||||
* @param int $targetStructureId Target structure ID
|
||||
* @param array $mappings Array of [{sourceTypeKey, targetTypeKey, mappingType}]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function registerMappings(int $sourceStructureId, int $targetStructureId, array $mappings): void
|
||||
{
|
||||
if (! $this->schemaReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($mappings as $mapping) {
|
||||
$sourceElement = $this->repo->getElementByTypeKey($sourceStructureId, $mapping['sourceTypeKey']);
|
||||
$targetElement = $this->repo->getElementByTypeKey($targetStructureId, $mapping['targetTypeKey']);
|
||||
|
||||
if ($sourceElement === null || $targetElement === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->repo->mappingExists($sourceStructureId, $sourceElement->id, $targetStructureId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->repo->addMapping([
|
||||
'sourceStructureId' => $sourceStructureId,
|
||||
'sourceElementId' => $sourceElement->id,
|
||||
'targetStructureId' => $targetStructureId,
|
||||
'targetElementId' => $targetElement->id,
|
||||
'mappingType' => $mapping['mappingType'] ?? 'generates',
|
||||
]);
|
||||
|
||||
MappingCreated::dispatch($sourceStructureId, $targetStructureId, $mapping['mappingType'] ?? 'generates');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a structure is registered.
|
||||
*
|
||||
* @param string $title Structure title
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function has(string $title): bool
|
||||
{
|
||||
if (! $this->schemaReady()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Cache::rememberForever(self::CACHE_PREFIX.'exists.'.$title, function () use ($title) {
|
||||
return $this->repo->structureExists($title);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registered structure by title (cached).
|
||||
*
|
||||
* @param string $title Structure title
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function get(string $title): ?WorkStructure
|
||||
{
|
||||
if (! $this->schemaReady()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Cache::rememberForever(self::CACHE_PREFIX.'structure.'.$title, function () use ($title) {
|
||||
$structure = $this->repo->getStructureByTitle($title);
|
||||
|
||||
if ($structure !== null && $structure->id !== null) {
|
||||
$structure->elements = $this->repo->getElements($structure->id);
|
||||
$structure->relationships = $this->repo->getRelationships($structure->id);
|
||||
}
|
||||
|
||||
return $structure;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for a structure.
|
||||
*
|
||||
* @param string $title Structure title
|
||||
*/
|
||||
private function clearCache(string $title): void
|
||||
{
|
||||
Cache::forget(self::CACHE_PREFIX.'exists.'.$title);
|
||||
Cache::forget(self::CACHE_PREFIX.'structure.'.$title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the WorkStructure schema has been created yet.
|
||||
*
|
||||
* Guards the boot-time plugin registration path against a fresh install or a
|
||||
* pending update where migration 30502 hasn't run. Cached per request so the
|
||||
* hasTable lookup doesn't repeat for every register()/has()/get() call.
|
||||
*/
|
||||
private function schemaReady(): bool
|
||||
{
|
||||
static $ready = null;
|
||||
|
||||
if ($ready === null) {
|
||||
$ready = Schema::hasTable('zp_work_structures');
|
||||
}
|
||||
|
||||
return $ready;
|
||||
}
|
||||
}
|
||||
129
app/Core/WorkStructure/Services/WorkStructureService.php
Normal file
129
app/Core/WorkStructure/Services/WorkStructureService.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Core\WorkStructure\Services;
|
||||
|
||||
use Leantime\Core\Events\DispatchesEvents;
|
||||
use Leantime\Core\WorkStructure\Models\ElementDefinition;
|
||||
use Leantime\Core\WorkStructure\Models\RelationshipDefinition;
|
||||
use Leantime\Core\WorkStructure\Models\WorkStructure;
|
||||
use Leantime\Core\WorkStructure\Repositories\WorkStructureRepository;
|
||||
|
||||
/**
|
||||
* CRUD service for work structures, elements, and relationships.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class WorkStructureService
|
||||
{
|
||||
use DispatchesEvents;
|
||||
|
||||
/**
|
||||
* @param WorkStructureRepository $repo WorkStructure repository
|
||||
*/
|
||||
public function __construct(
|
||||
private WorkStructureRepository $repo
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a structure by ID with its elements and relationships.
|
||||
*
|
||||
* @param int $id Structure ID
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getStructure(int $id): ?WorkStructure
|
||||
{
|
||||
$structure = $this->repo->getStructure($id);
|
||||
|
||||
if ($structure !== null) {
|
||||
$structure->elements = $this->repo->getElements($id);
|
||||
$structure->relationships = $this->repo->getRelationships($id);
|
||||
}
|
||||
|
||||
return $structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a structure by title with its elements and relationships.
|
||||
*
|
||||
* @param string $title Structure title
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getStructureByTitle(string $title): ?WorkStructure
|
||||
{
|
||||
$structure = $this->repo->getStructureByTitle($title);
|
||||
|
||||
if ($structure !== null && $structure->id !== null) {
|
||||
$structure->elements = $this->repo->getElements($structure->id);
|
||||
$structure->relationships = $this->repo->getRelationships($structure->id);
|
||||
}
|
||||
|
||||
return $structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all elements for a structure, ordered by sort_order.
|
||||
*
|
||||
* @param int $structureId Structure ID
|
||||
* @return ElementDefinition[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getElements(int $structureId): array
|
||||
{
|
||||
return $this->repo->getElements($structureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all intra-structure relationships.
|
||||
*
|
||||
* @param int $structureId Structure ID
|
||||
* @return RelationshipDefinition[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getRelationships(int $structureId): array
|
||||
{
|
||||
return $this->repo->getRelationships($structureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new structure.
|
||||
*
|
||||
* @param array $values Structure data (title, description, type, createdBy, meta)
|
||||
* @return int Created structure ID
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function createStructure(array $values): int
|
||||
{
|
||||
return $this->repo->createStructure($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an element to a structure.
|
||||
*
|
||||
* @param array $values Element data (structureId, typeKey, label, description, domainReference, sortOrder, meta)
|
||||
* @return int Created element ID
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function addElement(array $values): int
|
||||
{
|
||||
return $this->repo->addElement($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an intra-structure relationship.
|
||||
*
|
||||
* @param array $values Relationship data (structureId, fromElementId, toElementId, relationshipType, description, meta)
|
||||
* @return int Created relationship ID
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function addRelationship(array $values): int
|
||||
{
|
||||
return $this->repo->addRelationship($values);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user