Explorar o código

新增剧本及资产转让接口

lh hai 1 semana
pai
achega
f4822535b9

+ 11 - 0
app/Http/Controllers/Anime/AnimeController.php

@@ -2486,6 +2486,17 @@ class AnimeController extends BaseController
     }
 
     /**
+     * 转让剧本归属(剧本及其关联资产,逐个剧本独立事务)
+     * @param Request $request
+     * @return mixed
+     */
+    public function transferScriptOwner(Request $request) {
+        $data = $request->all();
+        $result = $this->AnimeService->transferScriptOwner($data);
+        return $this->success($result);
+    }
+
+    /**
      * 获取剧本关联的资产列表
      * @param Request $request
      * @return mixed

+ 295 - 0
app/Services/Anime/AnimeService.php

@@ -8301,6 +8301,301 @@ class AnimeService
     }
 
     /**
+     * 转让剧本归属(剧本及其关联资产)
+     *
+     * 仅调整 mp_scripts.user_id 与 mp_products.user_id,其他表保持不变。
+     * 权限:仅 admin / superadmin 可操作;目标用户必须是同组(cpid 相同)且角色为 user(组员)的用户,禁止跨组转让。
+     * 多个剧本逐个独立事务处理,单个剧本失败不影响其他剧本;
+     * 单个剧本内的剧本记录与关联资产必须全部更新成功,否则整体回滚。
+     *
+     * @param array $data 请求参数:script_ids(数组或英文逗号分隔,兼容单个 script_id)、target_user_id
+     * @return array
+     */
+    public function transferScriptOwner($data) {
+        $operatorRole = Site::getRole();
+        $operatorCpid = Site::getCpid();
+
+        // 1.权限校验:仅管理员和超管可操作
+        if (!in_array($operatorRole, ['admin', 'superadmin'], true)) {
+            Utils::throwError(ErrorConst::NOT_ACCESS);
+        }
+
+        // 2.目标用户校验:必须存在且角色为 user(组员)
+        $targetUserId = (int)getProp($data, 'target_user_id', 0);
+        if ($targetUserId < 1) {
+            Utils::throwError('1002:请选择转让的目标用户');
+        }
+
+        $targetUser = DB::table('mp_manage_users')
+            ->where('id', $targetUserId)
+            ->where('is_deleted', 0)
+            ->first();
+
+        if (!$targetUser) {
+            Utils::throwError('20003:目标用户不存在');
+        }
+        if ((string)getProp($targetUser, 'role') !== 'user') {
+            Utils::throwError('20003:只能转让给角色为组员(user)的用户');
+        }
+
+        $targetCpid = (int)getProp($targetUser, 'cpid');
+
+        // 3.规范化剧本ID列表(兼容数组、逗号分隔字符串、script_id 单值)
+        $rawScriptIds = getProp($data, 'script_ids');
+        if (empty($rawScriptIds)) {
+            $rawScriptIds = getProp($data, 'script_id');
+        }
+        $scriptIds = $this->normalizeIdList($rawScriptIds);
+        if (empty($scriptIds)) {
+            Utils::throwError('20003:请提供剧本ID');
+        }
+
+        // 预取剧本名称,便于失败结果中回显
+        $scriptNameMap = DB::table('mp_scripts')
+            ->whereIn('id', $scriptIds)
+            ->pluck('script_name', 'id')
+            ->all();
+
+        $successList = [];
+        $failedList = [];
+
+        // 4.逐个剧本独立事务处理,单个失败不影响其他剧本
+        foreach ($scriptIds as $scriptId) {
+            DB::beginTransaction();
+
+            try {
+                $successList[] = $this->transferSingleScriptOwner(
+                    $scriptId,
+                    $targetUserId,
+                    $targetCpid,
+                    $operatorRole,
+                    $operatorCpid,
+                    Site::getUid()
+                );
+
+                DB::commit();
+            } catch (\Throwable $e) {
+                DB::rollBack();
+
+                $failedList[] = [
+                    'script_id'   => (int)$scriptId,
+                    'script_name' => (string)($scriptNameMap[$scriptId] ?? ''),
+                    'reason'      => $e->getMessage() !== '' ? $e->getMessage() : '转让失败',
+                ];
+
+                dLog('anime')->error('剧本归属转让失败', [
+                    'script_id'      => $scriptId,
+                    'target_user_id' => $targetUserId,
+                    'operator_uid'   => Site::getUid(),
+                    'error'          => $e->getMessage(),
+                ]);
+            }
+        }
+
+        return [
+            'target_user_id' => $targetUserId,
+            'success_count'  => count($successList),
+            'failed_count'   => count($failedList),
+            'success_list'   => $successList,
+            'failed_list'    => $failedList,
+        ];
+    }
+
+    /**
+     * 单个剧本的归属转让(需在事务中调用)
+     *
+     * @param int    $scriptId      剧本ID
+     * @param int    $targetUserId  目标用户ID
+     * @param int    $targetCpid    目标用户所属公司ID
+     * @param string $operatorRole  操作人角色
+     * @param int    $operatorCpid  操作人所属公司ID
+     * @param int    $operatorUid   操作人用户ID
+     * @return array
+     */
+    private function transferSingleScriptOwner($scriptId, $targetUserId, $targetCpid, $operatorRole, $operatorCpid, $operatorUid) {
+        $script = DB::table('mp_scripts')
+            ->where('id', $scriptId)
+            ->where('is_deleted', 0)
+            ->first();
+
+        if (!$script) {
+            Utils::throwError('20003:剧本不存在');
+        }
+
+        $scriptCpid = (int)getProp($script, 'cpid');
+        $fromUserId = (int)getProp($script, 'user_id');
+
+        // 管理员仅可操作本组剧本
+        if ($operatorRole !== 'superadmin' && $scriptCpid !== $operatorCpid) {
+            Utils::throwError(ErrorConst::NOT_ACCESS);
+        }
+
+        // 管理员仅可转让自己创建的剧本(超管不做此验证)
+        if ($operatorRole !== 'superadmin' && $fromUserId !== (int)$operatorUid) {
+            Utils::throwError('20003:管理员仅能转让自己创建的剧本和资产');
+        }
+
+        // 禁止跨组转让:目标用户必须与剧本同组
+        if ($targetCpid !== $scriptCpid) {
+            Utils::throwError('20003:不能跨组转让,目标用户与剧本不在同一组');
+        }
+
+        // 关联资产ID(同一资产可能关联多个剧集序号,需去重)
+        $productIds = $this->normalizeIdList(
+            DB::table('mp_script_product_mappings')
+                ->where('script_id', $scriptId)
+                ->pluck('product_id')
+                ->all()
+        );
+
+        $folderIds = [];
+
+        if (!empty($productIds)) {
+            $products = DB::table('mp_products')
+                ->whereIn('id', $productIds)
+                ->select('id', 'cpid', 'parent_id', 'user_id')
+                ->get();
+
+            // 关联资产必须全部存在,否则视为数据异常,不提交
+            if ($products->count() !== count($productIds)) {
+                Utils::throwError('20003:剧本存在已丢失的关联资产,无法完成转让');
+            }
+
+            foreach ($products as $product) {
+                // 关联资产必须与剧本同组,避免误转让跨组资产
+                if ((int)getProp($product, 'cpid') !== $scriptCpid) {
+                    Utils::throwError('20003:剧本存在跨组关联资产,无法完成转让');
+                }
+
+                $parentId = (int)getProp($product, 'parent_id', 0);
+                if ($parentId > 0 && !in_array($parentId, $folderIds, true)) {
+                    $folderIds[] = $parentId;
+                }
+            }
+
+            // 仅转让归属于原剧本所有者的资产
+            $folderIds = DB::table('mp_products')
+                ->whereIn('id', $folderIds)
+                ->where('type', 2)
+                ->where('cpid', $scriptCpid)
+                ->where('user_id', $fromUserId)
+                ->pluck('id')
+                ->map('intval')
+                ->all();
+
+            // 管理员仅可转让自己创建的资产及其所属文件夹(超管不做此验证)
+            if ($operatorRole !== 'superadmin') {
+                foreach ($products as $product) {
+                    if ((int)getProp($product, 'user_id') !== (int)$operatorUid) {
+                        Utils::throwError('20003:管理员仅能转让自己创建的剧本和资产');
+                    }
+                }
+
+                foreach ($products as $product) {
+                    $parentId = (int)getProp($product, 'parent_id', 0);
+                    if ($parentId > 0 && !in_array($parentId, $folderIds, true)) {
+                        Utils::throwError('20003:管理员仅能转让自己创建的剧本和资产');
+                    }
+                }
+            }
+        }
+
+        $now = now();
+
+        // 5.更新剧本归属
+        DB::table('mp_scripts')
+            ->where('id', $scriptId)
+            ->update([
+                'user_id'    => $targetUserId,
+                'updated_at' => $now,
+            ]);
+
+        // 6.更新关联资产归属
+        if (!empty($productIds)) {
+            DB::table('mp_products')
+                ->whereIn('id', $productIds)
+                ->update([
+                    'user_id'    => $targetUserId,
+                    'updated_at' => $now,
+                ]);
+        }
+
+        // 7.更新资产所属的类型根文件夹归属(文件夹属于 mp_products,随资产一并转让)
+        if (!empty($folderIds)) {
+            DB::table('mp_products')
+                ->whereIn('id', $folderIds)
+                ->update([
+                    'user_id'    => $targetUserId,
+                    'updated_at' => $now,
+                ]);
+        }
+
+        // 8.校验剧本与资产是否全部更新成功,任一未成功则回滚
+        $scriptOwner = (int)DB::table('mp_scripts')->where('id', $scriptId)->value('user_id');
+        if ($scriptOwner !== $targetUserId) {
+            Utils::throwError('20003:剧本归属更新失败');
+        }
+
+        if (!empty($productIds)) {
+            $updatedProductCount = DB::table('mp_products')
+                ->whereIn('id', $productIds)
+                ->where('user_id', $targetUserId)
+                ->count();
+
+            if ($updatedProductCount !== count($productIds)) {
+                Utils::throwError('20003:部分剧本资产转让失败,已回滚');
+            }
+        }
+
+        if (!empty($folderIds)) {
+            $updatedFolderCount = DB::table('mp_products')
+                ->whereIn('id', $folderIds)
+                ->where('user_id', $targetUserId)
+                ->count();
+
+            if ($updatedFolderCount !== count($folderIds)) {
+                Utils::throwError('20003:部分资产文件夹转让失败,已回滚');
+            }
+        }
+
+        return [
+            'script_id'     => (int)$scriptId,
+            'script_name'   => (string)getProp($script, 'script_name', ''),
+            'from_user_id'  => $fromUserId,
+            'to_user_id'    => $targetUserId,
+            'product_count' => count($productIds),
+            'folder_count'  => count($folderIds),
+        ];
+    }
+
+    /**
+     * 规范化ID列表,兼容数组、逗号分隔字符串以及字符串数组
+     *
+     * @param mixed $ids 待处理的ID集合
+     * @return array 去重后的正整数ID数组
+     */
+    private function normalizeIdList($ids) {
+        if (!is_array($ids)) {
+            $ids = explode(',', (string)$ids);
+        }
+
+        $result = [];
+        foreach ($ids as $item) {
+            if (is_array($item)) {
+                continue;
+            }
+            foreach (explode(',', (string)$item) as $part) {
+                $part = trim($part);
+                if ($part !== '' && is_numeric($part) && (int)$part > 0) {
+                    $result[] = (int)$part;
+                }
+            }
+        }
+
+        return array_values(array_unique($result));
+    }
+
+    /**
      * 保存剧本资产关联关系
      * @param array $data 请求参数
      * @return bool

+ 1 - 0
routes/api.php

@@ -298,6 +298,7 @@ Route::group(['middleware' => ['bindToken', 'bindExportToken', 'checkLogin']], f
             
             // 剧本资产关联管理
             Route::post('saveScriptProducts', [AnimeController::class, 'saveScriptProducts']);     // 保存剧本资产
+            Route::post('transferScriptOwner', [AnimeController::class, 'transferScriptOwner']);  // 转让剧本归属(剧本+关联资产)
             Route::get('getScriptProducts', [AnimeController::class, 'getScriptProducts']);        // 获取剧本资产
             Route::get('getScriptTotalEpisodes', [AnimeController::class, 'getScriptTotalEpisodes']);   // 获取剧本总集数