OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
<?php
namespace Leantime\Core\Domains;
use Leantime\Core\Auth\Permissions\PermissionService;
use Leantime\Core\Events\DispatchesEvents;
use Leantime\Core\Exceptions\ValidationException;
/**
* Base class for domain services, providing the cross-cutting authorization and validation
* helpers a service needs. All services should extend this (it also carries the
* {@see DomainService} marker and the {@see DispatchesEvents} trait, so event behavior is
* unchanged).
*
* Dependency wiring is handled by {@see \Leantime\Core\Auth\Permissions\PermissionServiceProvider}:
* an `afterResolving(BaseService::class, ...)` hook wires a LAZY resolver (not the instance) on
* every container-resolved subclass. That keeps the engine injected with zero constructor
* boilerplate in subclasses (which all have their own repo-injecting constructors) and without
* reaching for the `app()` helper inside service methods. The resolver — rather than eager
* injection — is essential: a service can sit inside PermissionService's own dependency graph
* (the Files service is reached via PermissionService → ChecksProjectAccess → Projects → Files),
* so eagerly making PermissionService inside that service's afterResolving hook would re-enter
* PermissionService's half-built construction and recurse forever. Resolving lazily on first
* authorize()/can() defers it until the singleton exists.
*/
abstract class BaseService implements DomainService
{
use DispatchesEvents;
protected ?PermissionService $permissions = null;
/** @var (\Closure(): PermissionService)|null Lazy resolver wired by PermissionServiceProvider. */
protected ?\Closure $permissionServiceResolver = null;
/**
* Set the engine instance directly. Used by unit tests; production uses the lazy resolver below.
*/
public function setPermissionService(PermissionService $permissions): void
{
$this->permissions = $permissions;
}
/**
* Wire a LAZY resolver instead of the instance (see the class docblock for why eager injection
* recurses). The engine is resolved on first authorize()/can().
*/
public function setPermissionServiceResolver(\Closure $resolver): void
{
$this->permissionServiceResolver = $resolver;
}
/** Resolve the engine, preferring a directly-set instance (tests) then the lazy resolver. */
private function permissionService(): PermissionService
{
if ($this->permissions === null) {
if ($this->permissionServiceResolver === null) {
throw new \LogicException(static::class.' has no PermissionService: it was neither resolved through the container nor wired in a test.');
}
$this->permissions = ($this->permissionServiceResolver)();
}
return $this->permissions;
}
/**
* Authorize the current user for a `domain.action` permission or throw. Replaces the
* silent `return false` pattern — a denial becomes an
* {@see \Leantime\Core\Exceptions\AuthorizationException} (403 web / RPC -32001).
*
* @throws \Leantime\Core\Exceptions\AuthorizationException
*/
protected function authorize(string $permission, ?int $projectId = null, ?bool $forceGlobal = null): void
{
$this->permissionService()->authorize($permission, $projectId, $forceGlobal);
}
/** Non-throwing capability check, for branching. */
protected function can(string $permission, ?int $projectId = null, ?bool $forceGlobal = null): bool
{
return $this->permissionService()->currentUserCan($permission, $projectId, $forceGlobal);
}
/** The authenticated user's id, or null when there is no session user. */
protected function currentUserId(): ?int
{
$id = session('userdata.id');
return ($id === null || $id === '') ? null : (int) $id;
}
/**
* Validate input against Laravel rules, returning the validated (whitelisted) subset or
* throwing a {@see ValidationException} (422 web / RPC -32602 with field errors). Works
* identically whether input arrived via a controller or JSON-RPC.
*
* @param array<string, mixed> $data
* @param array<string, mixed> $rules
* @param array<string, string> $messages
* @return array<string, mixed>
*
* @throws ValidationException
*/
protected function validate(array $data, array $rules, array $messages = []): array
{
return ValidationException::validate($data, $rules, $messages);
}
}

85
app/Core/Domains/DTO.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
namespace Leantime\Core\Domains;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class DTO
{
/**
* @param Response|array $data The data to map to the DTO
**/
public function __construct(private Response|array $data)
{
$data = Arr::dot($data instanceof Response ? $data->json() : $data);
$builder = build($this);
$propertyAttributes = collect((new \ReflectionClass($this))->getProperties())->mapWithKeys(function ($property) {
$property->setAccessible(true);
return [$property->getName() => collect($property->getAttributes())->mapWithKeys(fn ($attr) => [$attr->getName() => $attr->getArguments()])];
})->all();
$propertyAttributes = Arr::dot($propertyAttributes);
foreach ($data as $placement => $value) {
$propertyPath = explode('.', Str::beforeLast($placement, '.'));
$propertyName = array_shift($propertyPath);
if (
($propKey = array_search($placement, $propertyAttributes))
&& Str::afterLast($propKey, '.') == 'Map'
) {
$propertyPath = explode('.', Str::beforeLast($propKey, '.'));
$propertyName = array_shift($propertyPath);
}
$placement = implode('.', array_filter([$propertyName, ...$propertyPath]));
$attributes = array_filter(
$propertyAttributes,
fn ($key) => Str::beforeLast('.', $key) == $placement && Str::afterLast('.', $key) !== 'Map',
ARRAY_FILTER_USE_KEY
);
foreach ($attributes as $key => $attrValue) {
$attrName = Str::afterLast($key, '.');
$value = $this->{Str::camel($attrName)}(params: $attrValue, value: $value);
}
$builder->set($placement, $value);
}
}
/**
* Validates values.
*
* @param string[] $params validations rules to apply to the value
*
* @todo Implement. May use illuminate/validation later on.
*
* @see https://github.com/mattstauffer/Torch/tree/master/components/validation
**/
private function validate(array $params, mixed $value): mixed
{
return $value;
}
/**
* Gets the DTO data as multidimensional an array
**/
public function toArray(): array
{
$props = get_class_vars($this::class);
unset($props['data']);
return collect($props)->map(fn ($defaultVal, $key) => $this->{$key} ?? $defaultVal)->all();
}
/**
* Gets the DTO data as multidimensional an array
**/
public function all(): array
{
return $this->toArray();
}
}

View File

@@ -0,0 +1,5 @@
<?php
namespace Leantime\Core\Domains;
interface DomainModel {}

View File

@@ -0,0 +1,58 @@
<?php
namespace Leantime\Core\Domains;
/**
* Service Interface - Base interface for all services
*/
interface DomainRepository
{
/**
* patches the object by key.
*
* @param int $id Id of the object to be patched
* @param array $params Key=>value array where key represents the object field name and value the value.
* @return bool returns true on success, false on failure
*/
public function patch(int $id, array $params): bool;
/**
* updates the object by key.
*
* @param object|array $object expects the entire object to be updated as object or array
* @return array|bool Returns true on success, false on failure
*/
public function update(object|array $object): array|bool;
/**
* Creates a new object
*
* @param object|array $object Object or array to be created
* @return int|false Returns id of new element or false
*/
public function create(object|array $object): int|false;
/**
* Deletes object
*
* @param int $id Id of the object to be deleted
* @return bool Returns id of new element or false
*/
public function delete(int $id);
/**
* Gets 1 specific item
*
* @param int $id Id of the object to be retrieved
* @return object|array|false Returns object or array. False on failure or if item cannot be found
*/
public function get(int $id);
/**
* Get all items
*
* @param array|null $searchparams Search parameters
* @return array|false Returns array on success, false on failure. No results should return empty array
*/
public function query(?array $searchparams = null);
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Leantime\Core\Domains;
/**
* Marker interface for all domain (and plugin) service classes.
*
* It carries no required methods on purpose. Real services have wildly different shapes
* (the Tickets service alone has ~75 heterogeneous methods), so a fixed CRUD contract
* never fit — which is exactly why the previous patch/update/create/delete/get/query
* interface had zero implementers. This marker instead gives the service layer a single
* type to scan for and a shared home (via {@see BaseService}) for the cross-cutting
* authorize()/validate() helpers, without forcing a fictional method surface.
*
* Granular capability interfaces (e.g. a real Crudable) may be introduced alongside this
* marker where they genuinely apply, opt-in per service.
*/
interface DomainService {}