OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
286
app/Domain/Tickets/Services/TicketResource.php
Normal file
286
app/Domain/Tickets/Services/TicketResource.php
Normal file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\Tickets\Services;
|
||||
|
||||
use Leantime\Core\Domains\BaseService;
|
||||
use Leantime\Domain\Bom\Permissions\BomPermissions;
|
||||
use Leantime\Domain\Bom\Services\Bom as BomService;
|
||||
use Leantime\Domain\Files\Permissions\FilesPermissions;
|
||||
use Leantime\Domain\Files\Repositories\Files as FileRepository;
|
||||
use Leantime\Domain\Files\Services\Files as FilesService;
|
||||
use Leantime\Domain\Tickets\Permissions\TicketsPermissions;
|
||||
use Leantime\Domain\Tickets\Repositories\TicketResource as TicketResourceRepository;
|
||||
use Leantime\Domain\Tickets\Repositories\Tickets as TicketsRepository;
|
||||
use Leantime\Domain\Wiki\Permissions\WikiPermissions;
|
||||
use Leantime\Domain\Wiki\Services\Wiki as WikiService;
|
||||
|
||||
/**
|
||||
* 待办事项(ticket/milestone) ↔ 资源关联服务。
|
||||
*
|
||||
* 5 类资源:BOM / 工艺文件 / 工具清单(共用 zp_bom,type 区分)、文件(zp_file)、
|
||||
* wiki(zp_canvas/zp_canvas_items)。关联边落在 zp_entity_relationship(LinkedResource),
|
||||
* entityBType 用资源类型标识:bom | process | tooling | file | wiki。
|
||||
*/
|
||||
class TicketResource extends BaseService
|
||||
{
|
||||
/** 资源类型 → 显示名 */
|
||||
public const TYPE_NAMES = [
|
||||
'bom' => 'BOM',
|
||||
'process' => '工艺文件',
|
||||
'tooling' => '工具清单',
|
||||
'file' => '文件',
|
||||
'wiki' => 'Wiki',
|
||||
];
|
||||
|
||||
/** 主数据类型(对应 zp_bom.type) */
|
||||
public const MASTER_TYPES = ['bom', 'process', 'tooling'];
|
||||
|
||||
public function __construct(
|
||||
protected TicketResourceRepository $repo,
|
||||
protected TicketsRepository $ticketRepo,
|
||||
protected BomService $bomService,
|
||||
protected FilesService $filesService,
|
||||
protected WikiService $wikiService,
|
||||
protected FileRepository $fileRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 关联一个资源到 ticket(幂等)。
|
||||
*
|
||||
* @return array{success:bool,message?:string}
|
||||
*/
|
||||
public function link(int $ticketId, string $resourceType, int $resourceId): array
|
||||
{
|
||||
if ($ticketId <= 0 || $resourceId <= 0 || ! array_key_exists($resourceType, self::TYPE_NAMES)) {
|
||||
return ['success' => false, 'message' => '无效的关联参数'];
|
||||
}
|
||||
|
||||
$ticket = $this->ticketRepo->getTicket($ticketId);
|
||||
if ($ticket === false) {
|
||||
return ['success' => false, 'message' => '待办事项不存在'];
|
||||
}
|
||||
$projectId = (int) $ticket->projectId;
|
||||
|
||||
// 写关联需 tickets.edit;读资源详情需对应资源 view 权限
|
||||
$this->authorize(TicketsPermissions::EDIT, $projectId);
|
||||
|
||||
if (! $this->resourceExists($resourceType, $resourceId, $projectId)) {
|
||||
return ['success' => false, 'message' => '资源不存在或不属于当前项目'];
|
||||
}
|
||||
|
||||
$userId = (int) (session('userdata.id') ?? 0);
|
||||
$ok = $this->repo->addLink($ticketId, $resourceType, $resourceId, $userId);
|
||||
|
||||
return $ok
|
||||
? ['success' => true]
|
||||
: ['success' => false, 'message' => '关联失败'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除一个资源关联。
|
||||
*/
|
||||
public function unlink(int $ticketId, string $resourceType, int $resourceId): array
|
||||
{
|
||||
if ($ticketId <= 0 || $resourceId <= 0) {
|
||||
return ['success' => false, 'message' => '无效参数'];
|
||||
}
|
||||
$ticket = $this->ticketRepo->getTicket($ticketId);
|
||||
if ($ticket === false) {
|
||||
return ['success' => false, 'message' => '待办事项不存在'];
|
||||
}
|
||||
$this->authorize(TicketsPermissions::EDIT, (int) $ticket->projectId);
|
||||
|
||||
return $this->repo->removeLink($ticketId, $resourceType, $resourceId)
|
||||
? ['success' => true]
|
||||
: ['success' => false, 'message' => '解除失败'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 某 ticket 已关联的资源详情列表(供详情页 chips 渲染)。
|
||||
*
|
||||
* @return array<int, array{type:string,typeName:string,id:int,title:string,subtitle?:string,url?:string}>
|
||||
*/
|
||||
public function getLinkedResources(int $ticketId): array
|
||||
{
|
||||
$links = $this->repo->getLinks($ticketId);
|
||||
$result = [];
|
||||
foreach ($links as $link) {
|
||||
$detail = $this->resolveResourceDetail($link['type'], $link['id']);
|
||||
if ($detail !== null) {
|
||||
$result[] = $detail + ['type' => $link['type'], 'id' => $link['id']];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 候选资源列表(供「添加关联」下拉)。
|
||||
*
|
||||
* @return array<int, array{type:string,typeName:string,items:array<int,array{id:int,title:string,subtitle?:string}>}>
|
||||
*/
|
||||
public function listCandidates(int $projectId): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
// 主数据:BOM / 工艺文件 / 工具清单(全局,projectId=0)
|
||||
foreach (self::MASTER_TYPES as $type) {
|
||||
$masters = $this->bomService->getMasters($type, 0);
|
||||
$items = [];
|
||||
foreach ($masters as $m) {
|
||||
$title = trim(($m['bomNo'] ?? '').' '.($m['productName'] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = '#'.($m['id'] ?? '');
|
||||
}
|
||||
$items[] = [
|
||||
'id' => (int) $m['id'],
|
||||
'title' => $title,
|
||||
'subtitle' => ($m['version'] ?? '') !== '' ? '版本 '.$m['version'] : null,
|
||||
];
|
||||
}
|
||||
$groups[] = ['type' => $type, 'typeName' => self::TYPE_NAMES[$type], 'items' => $items];
|
||||
}
|
||||
|
||||
// 文件(项目文件)
|
||||
try {
|
||||
$files = $this->filesService->getFilesByModule('project', $projectId);
|
||||
$items = [];
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $f) {
|
||||
$items[] = [
|
||||
'id' => (int) $f['id'],
|
||||
'title' => (string) ($f['realName'] ?? ''),
|
||||
'subtitle' => ($f['firstname'] ?? '').' '.($f['lastname'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
$groups[] = ['type' => 'file', 'typeName' => self::TYPE_NAMES['file'], 'items' => $items];
|
||||
} catch (\Throwable $e) {
|
||||
$groups[] = ['type' => 'file', 'typeName' => self::TYPE_NAMES['file'], 'items' => []];
|
||||
}
|
||||
|
||||
// Wiki(项目 wiki 的标题作为可关联单元)
|
||||
try {
|
||||
$wikis = $this->wikiService->getAllProjectWikis($projectId);
|
||||
$items = [];
|
||||
if (is_array($wikis)) {
|
||||
foreach ($wikis as $w) {
|
||||
$items[] = [
|
||||
'id' => (int) ($w['id'] ?? $w->id ?? 0),
|
||||
'title' => (string) ($w['title'] ?? 'Wiki'),
|
||||
];
|
||||
}
|
||||
}
|
||||
$groups[] = ['type' => 'wiki', 'typeName' => self::TYPE_NAMES['wiki'], 'items' => $items];
|
||||
} catch (\Throwable $e) {
|
||||
$groups[] = ['type' => 'wiki', 'typeName' => self::TYPE_NAMES['wiki'], 'items' => []];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验资源是否存在(并做项目作用域兜底校验,主数据为全局)。
|
||||
*/
|
||||
private function resourceExists(string $resourceType, int $resourceId, int $projectId): bool
|
||||
{
|
||||
return match ($resourceType) {
|
||||
'bom', 'process', 'tooling' => $this->bomService->getBom($resourceId) !== false,
|
||||
'file' => $this->fileExists($resourceId),
|
||||
'wiki' => $this->wikiExists($resourceId, $projectId),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
private function fileExists(int $fileId): bool
|
||||
{
|
||||
try {
|
||||
$f = $this->filesService->getFileById($fileId);
|
||||
|
||||
return $f !== false;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function wikiExists(int $wikiId, int $projectId): bool
|
||||
{
|
||||
try {
|
||||
$wikis = $this->wikiService->getAllProjectWikis($projectId);
|
||||
foreach ($wikis as $w) {
|
||||
if ((int) ($w['id'] ?? $w->id ?? 0) === $wikiId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资源详情(供 chips / 弹窗)。
|
||||
*
|
||||
* @return array{type:string,typeName:string,id:int,title:string,subtitle?:string,url?:string}|null
|
||||
*/
|
||||
private function resolveResourceDetail(string $resourceType, int $resourceId): ?array
|
||||
{
|
||||
$typeName = self::TYPE_NAMES[$resourceType] ?? $resourceType;
|
||||
|
||||
if (in_array($resourceType, self::MASTER_TYPES, true)) {
|
||||
$bom = $this->bomService->getBom($resourceId);
|
||||
if ($bom === false) {
|
||||
return null;
|
||||
}
|
||||
$title = trim(($bom['bomNo'] ?? '').' '.($bom['productName'] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = '#'.$resourceId;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $resourceType,
|
||||
'typeName' => $typeName,
|
||||
'id' => $resourceId,
|
||||
'title' => $title,
|
||||
'subtitle' => ($bom['version'] ?? '') !== '' ? '版本 '.$bom['version'] : null,
|
||||
'url' => BASE_URL.'/bom/show/'.$resourceId,
|
||||
];
|
||||
}
|
||||
|
||||
if ($resourceType === 'file') {
|
||||
$f = $this->fileRepository->getFile($resourceId);
|
||||
if ($f === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'file',
|
||||
'typeName' => $typeName,
|
||||
'id' => $resourceId,
|
||||
'title' => (string) ($f['realName'] ?? ('文件 #'.$resourceId)),
|
||||
'subtitle' => trim(($f['firstname'] ?? '').' '.($f['lastname'] ?? '')),
|
||||
'url' => BASE_URL.'/files/get?fileId='.$resourceId,
|
||||
];
|
||||
}
|
||||
|
||||
if ($resourceType === 'wiki') {
|
||||
try {
|
||||
$article = $this->wikiService->getArticle($resourceId);
|
||||
$title = (string) ($article->title ?? ('Wiki #'.$resourceId));
|
||||
|
||||
return [
|
||||
'type' => 'wiki',
|
||||
'typeName' => $typeName,
|
||||
'id' => $resourceId,
|
||||
'title' => $title,
|
||||
'url' => BASE_URL.'/wiki/showArticle/'.$resourceId,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user