| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384 |
- <?php
- declare(strict_types=1);
- namespace app\miniapi\service;
- use app\miniapi\auth\Principal;
- use app\miniapi\exception\ApiException;
- use app\miniapi\model\MiniUser;
- use app\miniapi\model\MiniCommand;
- use app\miniapi\model\MiniAudit;
- /** A user row serializes retries; business writes and the response commit together. */
- final class Commands
- {
- public static function run(Principal $actor, string $key, string $action, array $payload, callable $operation, callable $authorize): array
- {
- if (!preg_match('/^[A-Za-z0-9_-]{8,80}$/D', $key)) {
- throw new ApiException(42201, 'request_key须为8至80位字母、数字、下划线或短横线', 422);
- }
- $normalize = function ($value) use (&$normalize) {
- if (!is_array($value)) {
- return $value;
- }
- if (!array_is_list($value)) {
- ksort($value);
- }
- return array_map($normalize, $value);
- };
- $hash = hash('sha256', json_encode($normalize($payload), JSON_THROW_ON_ERROR));
- return (new MiniUser())->getConnection()->transaction(function () use ($actor, $key, $action, $operation, $authorize, $hash) {
- $user = MiniUser::query()->where('id', $actor->userId)->lockForUpdate()->first();
- if (!$user || (int) $user->status !== 1) {
- throw new ApiException(40101, '登录状态已失效', 401);
- }
- // Grant changes use the same user lock. Replays must also retain current permission.
- $authorize();
- $old = MiniCommand::query()->where([
- 'user_id' => $actor->userId,
- 'request_key' => $key,
- ])->first();
- if ($old) {
- if ($old->action !== $action || !hash_equals($old->payload_hash, $hash)) {
- throw new ApiException(40901, '重复请求的内容发生变化', 409);
- }
- return json_decode($old->result, true, 512, JSON_THROW_ON_ERROR) + ['replayed' => true];
- }
- try {
- $result = $operation();
- } catch (\plugin\saiadmin\exception\ApiException $e) {
- throw new ApiException(40902, $e->getMessage(), 409);
- }
- MiniCommand::query()->insert([
- 'user_id' => $actor->userId,
- 'request_key' => $key,
- 'action' => $action,
- 'payload_hash' => $hash,
- 'result' => json_encode($result, JSON_THROW_ON_ERROR),
- 'created_at' => date('Y-m-d H:i:s'),
- ]);
- self::audit('mini', (int) $actor->userId, $action, (int) ($result['id'] ?? 0), [
- 'request_key' => $key,
- 'payload_hash' => $hash,
- ]);
- return $result;
- });
- }
- public static function audit(string $type, int $actor, string $action, int $resource, array $data): void
- {
- MiniAudit::query()->insert([
- 'actor_type' => $type,
- 'actor_id' => $actor,
- 'action' => $action,
- 'resource_id' => $resource,
- 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
- 'created_at' => date('Y-m-d H:i:s'),
- ]);
- }
- }
|