ProjectController.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Api\ShareApi;
  4. use App\Http\Api\StudioApi;
  5. use App\Http\Resources\ProjectResource;
  6. use App\Models\Project;
  7. use App\Services\AuthService;
  8. use Illuminate\Http\Request;
  9. use Illuminate\Support\Str;
  10. class ProjectController extends Controller
  11. {
  12. /**
  13. * 列出工程(project)
  14. *
  15. * 按 view 指定的口径返回工程列表,支持关键字搜索、状态过滤、排序和分页。
  16. * 必须登录,否则返回 401;view 缺失或不在枚举内时返回错误提示 view。
  17. * 未传 studio 时,studio 默认取当前用户自己。
  18. *
  19. * @queryParam view string required 查询口径:studio=某 studio 下的顶层工程,project-tree=某工程及其所有子工程,shared=分享给该 studio 的工程,community=其他 studio 的公开顶层工程。Enum: studio,project-tree,shared,community
  20. * @queryParam studio string studio 名称;不传则使用当前登录用户
  21. * @queryParam type string 工程类型,用于 studio/shared/community 口径。Enum: instance,article Default: instance
  22. * @queryParam project_id string view=project-tree 时必填,工程 uid;同时匹配该工程本身及 path 中包含它的子工程
  23. * @queryParam keyword string 标题模糊搜索关键字
  24. * @queryParam status string 工程状态,逗号分隔可多选
  25. * @queryParam order string 排序字段。Default: id
  26. * @queryParam dir string 排序方向。Enum: asc,desc Default: asc
  27. * @queryParam offset integer 分页偏移量。Default: 0
  28. * @queryParam limit integer 每页条数。Default: 10000
  29. */
  30. public function index(Request $request)
  31. {
  32. $user = AuthService::current($request);
  33. if (! $user) {
  34. return $this->error(__('auth.failed'), 401, 401);
  35. }
  36. if ($request->has('studio')) {
  37. $studioId = StudioApi::getIdByName($request->input('studio'));
  38. } else {
  39. $studioId = $user['user_uid'];
  40. }
  41. switch ($request->input('view')) {
  42. case 'studio':
  43. $table = Project::where('owner_id', $studioId)
  44. ->whereNull('parent_id')
  45. ->where('type', $request->input('type', 'instance'));
  46. break;
  47. case 'project-tree':
  48. $table = Project::where('uid', $request->input('project_id'))
  49. ->orWhereJsonContains('path', $request->input('project_id'));
  50. break;
  51. case 'shared':
  52. $type = $request->input('type', 'instance');
  53. $resList = ShareApi::getResList($studioId, $type === 'instance' ? 7 : 6);
  54. $resId = [];
  55. foreach ($resList as $res) {
  56. $resId[] = $res['res_id'];
  57. }
  58. $table = Project::whereIn('uid', $resId);
  59. break;
  60. case 'community':
  61. $table = Project::where('owner_id', '<>', $studioId)
  62. ->whereNull('parent_id')
  63. ->where('privacy', 'public')
  64. ->where('type', $request->input('type', 'instance'));
  65. break;
  66. default:
  67. // FIXME: view 非法时用 200 状态码表达错误,与项目其他接口的 4xx 约定不一致,
  68. // 建议改为 422 或 400。
  69. return $this->error('view', 200, 200);
  70. break;
  71. }
  72. if ($request->has('keyword')) {
  73. $table = $table->where('title', 'like', '%'.$request->input('keyword').'%');
  74. }
  75. if ($request->has('status')) {
  76. // FIXME: projects.status 是 jsonb 列,这里却按字符串 whereIn 比较,PostgreSQL 下匹配不到或报类型错误,
  77. // 建议改用 whereJsonContains 或把 status 规整为字符串列。
  78. $table = $table->whereIn('status', explode(',', $request->input('status')));
  79. }
  80. $count = $table->count();
  81. $table = $table->orderBy($request->input('order', 'id'), $request->input('dir', 'asc'));
  82. $table = $table->skip($request->input('offset', 0))
  83. ->take($request->input('limit', 10000));
  84. $result = $table->get();
  85. return $this->ok(
  86. [
  87. 'rows' => ProjectResource::collection($result),
  88. 'count' => $count,
  89. ]
  90. );
  91. }
  92. /**
  93. * 判断用户是否有权编辑该 studio 下的工程
  94. *
  95. * 目前仅允许 studio 本人,即用户 uid 与 studio uid 相同。
  96. *
  97. * @param string $user_uid 用户 uuid
  98. * @param string $studio_uid studio uuid
  99. */
  100. public static function canEdit($user_uid, $studio_uid)
  101. {
  102. return $user_uid == $studio_uid;
  103. }
  104. /**
  105. * 新建工程
  106. *
  107. * 在指定 studio 下创建工程。必须登录(否则 401),且当前用户必须是该 studio 本人
  108. * (studio_name 解析出的 id 等于当前用户 uid),否则返回 403。
  109. * 传入 parent_id 时会读取父工程的 path 并追加父工程 id,写入本工程的 path,
  110. * 以便按树查询子工程。
  111. *
  112. * @bodyParam studio_name string required studio 名称,用于解析归属 studio 并做权限校验
  113. * @bodyParam id string 工程 uid;为合法 uuid 时用它(已存在则更新该工程),否则服务端生成新 uuid
  114. * @bodyParam title string required 工程标题
  115. * @bodyParam description string 工程描述
  116. * @bodyParam parent_id string 父工程 uid,填写后作为子工程并自动计算 path
  117. * @bodyParam type string 工程类型。Enum: instance,article Default: instance
  118. */
  119. public function store(Request $request)
  120. {
  121. $user = AuthService::current($request);
  122. if (! $user) {
  123. return $this->error(__('auth.failed'), 401, 401);
  124. }
  125. $studioId = StudioApi::getIdByName($request->input('studio_name'));
  126. if (! self::canEdit($user['user_uid'], $studioId)) {
  127. return $this->error(__('auth.failed'), 403, 403);
  128. }
  129. $new = Project::firstOrNew(['uid' => $request->input('id')]);
  130. if (Str::isUuid($request->input('id'))) {
  131. $new->uid = $request->input('id');
  132. } else {
  133. $new->uid = Str::uuid();
  134. }
  135. $new->title = $request->input('title');
  136. $new->description = $request->input('description');
  137. $new->parent_id = $request->input('parent_id');
  138. $new->editor_id = $user['user_uid'];
  139. $new->owner_id = $studioId;
  140. $new->type = $request->input('type', 'instance');
  141. if (Str::isUuid($request->input('parent_id'))) {
  142. $parentPath = Project::where('uid', $request->input('parent_id'))->value('path');
  143. $parentPath = json_decode($parentPath);
  144. if (! is_array($parentPath)) {
  145. $parentPath = [];
  146. }
  147. array_push($parentPath, $new->parent_id);
  148. $new->path = json_encode($parentPath, JSON_UNESCAPED_UNICODE);
  149. }
  150. $new->save();
  151. return $this->ok(new ProjectResource($new));
  152. }
  153. /**
  154. * 查看单个工程
  155. *
  156. * 通过路由模型绑定按工程 uid 取出工程并返回详情,工程不存在时返回 404。
  157. * 该接口不做登录与权限校验,私有工程同样可被读取。
  158. *
  159. * @urlParam project string required 工程 uid
  160. */
  161. public function show(Project $project)
  162. {
  163. // FIXME: 该接口完全不鉴权,privacy=private 的工程也能被任意匿名用户按 uid 读取,
  164. // 建议补上登录校验并对私有工程限制为拥有者或被分享者可见。
  165. return $this->ok(new ProjectResource($project));
  166. }
  167. /**
  168. * 修改工程
  169. *
  170. * 整体覆盖式更新:title、description、parent_id、privacy 会无条件按请求体写入,
  171. * 未传的字段会被置空,调用方需回传完整数据。必须登录(否则 401),
  172. * 且只有工程拥有者本人可以修改,否则返回 403。
  173. * 传入合法的 parent_id 时会按父工程重新计算 path。
  174. *
  175. * @urlParam project string required 工程 uid
  176. *
  177. * @bodyParam title string required 工程标题
  178. * @bodyParam description string 工程描述
  179. * @bodyParam parent_id string 父工程 uid;为合法 uuid 时重算 path
  180. * @bodyParam privacy string 隐私性:public 表示社区可见。Enum: private,public Default: private
  181. */
  182. public function update(Request $request, Project $project)
  183. {
  184. $user = AuthService::current($request);
  185. if (! $user) {
  186. return $this->error(__('auth.failed'), 401, 401);
  187. }
  188. if (! self::canEdit($user['user_uid'], $project->owner_id)) {
  189. return $this->error(__('auth.failed'), 403, 403);
  190. }
  191. // FIXME: 以下字段无条件整体覆盖,前端只想改单个字段时会把其余字段清空,且 privacy 未做白名单校验,
  192. // 建议改为 $request->has() 逐字段更新并校验 privacy 只能为 private|public。
  193. $project->title = $request->input('title');
  194. $project->description = $request->input('description');
  195. $project->parent_id = $request->input('parent_id');
  196. $project->editor_id = $user['user_uid'];
  197. $project->privacy = $request->input('privacy');
  198. if (Str::isUuid($request->input('parent_id'))) {
  199. $parentPath = Project::where('uid', $request->input('parent_id'))->value('path');
  200. $parentPath = json_decode($parentPath);
  201. if (! is_array($parentPath)) {
  202. $parentPath = [];
  203. }
  204. array_push($parentPath, $project->parent_id);
  205. $project->path = json_encode($parentPath, JSON_UNESCAPED_UNICODE);
  206. }
  207. $project->save();
  208. return $this->ok(new ProjectResource($project));
  209. }
  210. /**
  211. * 删除工程
  212. *
  213. * 尚未实现:方法体为空,不会删除任何数据,也不返回任何内容(响应为 200 空 body)。
  214. *
  215. * @urlParam project string required 工程 uid
  216. */
  217. public function destroy(Project $project)
  218. {
  219. //
  220. }
  221. }