DiscussionController.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Api\ChannelApi;
  4. use App\Http\Api\CourseApi;
  5. use App\Http\Api\MdRender;
  6. use App\Http\Api\Mq;
  7. use App\Http\Api\UserApi;
  8. use App\Http\Resources\DiscussionResource;
  9. use App\Models\Channel;
  10. use App\Models\Discussion;
  11. use App\Models\PaliSentence;
  12. use App\Models\Sentence;
  13. use App\Models\Wbw;
  14. use App\Models\WbwBlock;
  15. use App\Services\AuthService;
  16. use Illuminate\Http\Request;
  17. class DiscussionController extends Controller
  18. {
  19. /**
  20. * 列出讨论 / 批注
  21. *
  22. * 按 view 指定的口径返回 discussion 列表,支持状态过滤、分页与排序。
  23. * 权限:未登录用户请求 type=discussion 时直接返回空结果;basic 角色用户在自己无编辑权限的
  24. * channel 上只能看到自己发表的 discussion;view=topic-by-user 必须登录,否则返回 403。
  25. * 返回 rows、count(当前口径总数)、active/close(顶级节点的活跃与关闭数)、
  26. * can_create/can_reply(当前用户能否发起与回复,按 type 与 res_type 计算)。
  27. *
  28. * @queryParam view string required 查询口径。question=按资源查顶级讨论;question-by-topic=按某条讨论所属资源查同资源讨论;answer=查某讨论的回复;res_id=某资源的顶级节点及其直接子节点;topic-by-user=当前用户发表的全部顶级讨论;all=全部顶级讨论。Enum: question,question-by-topic,answer,res_id,topic-by-user,all
  29. * @queryParam id string required 资源 uid(question/res_id)、讨论 id(question-by-topic/answer)
  30. * @queryParam type string 讨论类型。Enum: discussion,qa,help,note,commentary Default: discussion
  31. * @queryParam res_type string 资源类型,影响权限判断与学员提问聚合。Enum: sentence,wbw,article
  32. * @queryParam status string 状态过滤;res_id 与 topic-by-user 口径支持逗号分隔多选。Enum: active,close Default: active
  33. * @queryParam course string 课程 uid,配合 show_student 聚合学员提问
  34. * @queryParam show_student string 是否把学员 channel 上的同位置提问一并返回。Enum: true,false
  35. * @queryParam order string 排序字段。Default: created_at
  36. * @queryParam dir string 排序方向。Enum: asc,desc Default: desc
  37. * @queryParam offset integer 跳过的记录数。Default: 0
  38. * @queryParam limit integer 返回条数上限。Default: 100
  39. */
  40. public function index(Request $request)
  41. {
  42. //
  43. $user = AuthService::current($request);
  44. if ($user) {
  45. $userInfo = UserApi::getByUuid($user['user_uid']);
  46. }
  47. switch ($request->input('view')) {
  48. case 'question-by-topic':
  49. // FIXME: $topic 拿到的是 Builder 而不是模型,->first() 的结果被丢弃,
  50. // 因此下面的 if (! $topic) 永远为 false,$topic->res_id 走的是 Builder 魔术属性。
  51. // 应写成 $topic = Discussion::where('id', ...)->where('status', ...)->select('res_id')->first();
  52. $topic = Discussion::where('id', $request->input('id'));
  53. $topic->where('status', $request->input('status', 'active'))
  54. ->select('res_id')->first();
  55. if (! $topic) {
  56. return $this->error('无效的id');
  57. }
  58. $table = Discussion::where('res_id', $topic->res_id);
  59. $activeNumber = Discussion::where('res_id', $topic->res_id)
  60. ->where('status', 'active')->count();
  61. $closeNumber = Discussion::where('res_id', $topic->res_id)
  62. ->where('status', 'close')->count();
  63. $table->where('status', $request->input('status', 'active'))
  64. ->where('parent', null);
  65. break;
  66. case 'question':
  67. /**
  68. * 禁止:
  69. * 未注册用户看到任何人发表的discussion
  70. * basic用户看到别人在别人channel发表的discussion
  71. */
  72. if (! $user && $request->input('type') === 'discussion') {
  73. return $this->ok([
  74. 'rows' => [],
  75. 'count' => 0,
  76. 'active' => 0,
  77. 'close' => 0,
  78. 'can_create' => false,
  79. 'can_reply' => false,
  80. ]);
  81. }
  82. $resType = $request->input('res_type');
  83. if ($user) {
  84. switch ($resType) {
  85. case 'sentence':
  86. // code...
  87. break;
  88. case 'wbw':
  89. $block_uid = Wbw::where('uid', $request->input('id'))->value('block_uid');
  90. if ($block_uid) {
  91. $channelId = WbwBlock::where('uid', $block_uid)->value('channel_uid');
  92. if ($channelId) {
  93. $canEdit = ChannelApi::userCanEdit($user['user_uid'], $channelId);
  94. }
  95. }
  96. break;
  97. default:
  98. // code...
  99. break;
  100. }
  101. }
  102. $resId = [$request->input('id')];
  103. if (! empty($request->input('course'))) {
  104. //
  105. /**
  106. * 如果res id 是答案,获取学员提问
  107. * 如果是学员
  108. */
  109. // 获取学员提问
  110. // 获取学员channel
  111. if ($request->input('show_student') === 'true') {
  112. $channelsId = CourseApi::getStudentChannels($request->input('course'));
  113. switch ($resType) {
  114. case 'wbw':
  115. // 获取答案单词编号
  116. $wbwWord = Wbw::where('uid', $request->input('id'))
  117. ->first();
  118. $wbwId = WbwSentenceController::getWbwIdByChannels(
  119. $channelsId,
  120. $wbwWord->book_id,
  121. $wbwWord->paragraph,
  122. $wbwWord->wid
  123. );
  124. $resId = array_merge($resId, $wbwId);
  125. break;
  126. case 'sentence':
  127. break;
  128. }
  129. }
  130. }
  131. $table = Discussion::whereIn('res_id', $resId)
  132. ->where('type', $request->input('type', 'discussion'))
  133. ->where('status', $request->input('status', 'active'))
  134. ->where('parent', null);
  135. if ($request->input('type') === 'discussion') {
  136. if (
  137. isset($userInfo) &&
  138. isset($userInfo['roles']) &&
  139. in_array('basic', $userInfo['roles'])
  140. ) {
  141. if (isset($canEdit) && $canEdit === true) {
  142. } else {
  143. $table = $table->where('editor_uid', $userInfo['id']);
  144. }
  145. }
  146. }
  147. $activeNumber = Discussion::whereIn('res_id', $resId)
  148. ->where('parent', null)
  149. ->where('type', $request->input('type', 'discussion'))
  150. ->where('status', 'active')->count();
  151. $closeNumber = Discussion::whereIn('res_id', $resId)
  152. ->where('parent', null)
  153. ->where('type', $request->input('type', 'discussion'))
  154. ->where('status', 'close')->count();
  155. break;
  156. case 'answer':
  157. $table = Discussion::where('parent', $request->input('id'));
  158. $activeNumber = Discussion::where('parent', $request->input('id'))
  159. ->where('status', 'active')->count();
  160. $closeNumber = Discussion::where('parent', $request->input('id'))
  161. ->where('status', 'close')->count();
  162. break;
  163. case 'res_id':
  164. /**
  165. * 先获取顶级节点
  166. * 需要确定用户身份,manager查看全部topic 普通用户只显示自己提交的topic
  167. */
  168. $roots = Discussion::where('res_id', $request->input('id'))
  169. ->where('type', $request->input('type', 'discussion'))
  170. ->whereIn('status', explode(',', $request->input('status', 'active')))
  171. ->where('parent', null)
  172. ->select('id')
  173. ->get();
  174. $table = Discussion::where(function ($query) use ($roots) {
  175. $query->whereIn('id', $roots)
  176. ->orWhereIn('parent', $roots);
  177. });
  178. $activeNumber = Discussion::where('res_id', $request->input('id'))
  179. ->where('type', $request->input('type', 'discussion'))
  180. ->where('status', 'active')->count();
  181. $closeNumber = Discussion::where('res_id', $request->input('id'))
  182. ->where('type', $request->input('type', 'discussion'))
  183. ->where('status', 'close')->count();
  184. break;
  185. case 'topic-by-user':
  186. /**
  187. * 某用户发表的全部topic
  188. */
  189. if (! $user) {
  190. return $this->error('', 403, 403);
  191. }
  192. $table = Discussion::where('editor_uid', $user['user_uid'])
  193. ->where('type', $request->input('type', 'discussion'))
  194. ->whereIn('status', explode(',', $request->input('status', 'active')))
  195. ->where('parent', null);
  196. $activeNumber = Discussion::where('editor_uid', $user['user_uid'])
  197. ->where('parent', null)
  198. ->where('type', $request->input('type', 'discussion'))
  199. ->where('status', 'active')->count();
  200. $closeNumber = Discussion::where('editor_uid', $user['user_uid'])
  201. ->where('parent', null)
  202. ->where('type', $request->input('type', 'discussion'))
  203. ->where('status', 'close')->count();
  204. break;
  205. case 'all':
  206. $table = Discussion::where('parent', null);
  207. $activeNumber = Discussion::where('parent', null)
  208. ->where('status', 'active')->count();
  209. $closeNumber = Discussion::where('parent', null)
  210. ->where('status', 'close')->count();
  211. break;
  212. }
  213. // FIXME: $search 从未定义,这段标题搜索是永不执行的死代码。
  214. // 应改为从 $request->input('search') 取值,或直接删除。
  215. if (! empty($search)) {
  216. $table = $table->where('title', 'like', $search.'%');
  217. }
  218. // FIXME: view 不在枚举内(含缺省)时 $table 从未被赋值,这里会 fatal error。
  219. // 建议 switch 补 default 分支直接返回参数错误。
  220. $count = $table->count();
  221. $table = $table->orderBy($request->input('order', 'created_at'), $request->input('dir', 'desc'));
  222. $table = $table->skip($request->input('offset', 0))
  223. ->take($request->input('limit', 100));
  224. $result = $table->get();
  225. $can_create = false;
  226. $can_reply = false;
  227. $user = AuthService::current($request);
  228. switch ($request->input('type', 'discussion')) {
  229. case 'qa':
  230. switch ($request->input('res_type')) {
  231. case 'article':
  232. if ($user && ArticleController::userCanEditId($user['user_uid'], $request->input('id'))) {
  233. $can_create = true;
  234. $can_reply = true;
  235. }
  236. break;
  237. }
  238. break;
  239. case 'help':
  240. switch ($request->input('res_type')) {
  241. case 'article':
  242. if ($user) {
  243. $can_reply = true;
  244. if (ArticleController::userCanEditId($user['user_uid'], $request->input('id'))) {
  245. $can_create = true;
  246. }
  247. }
  248. break;
  249. }
  250. break;
  251. case 'discussion':
  252. if ($user) {
  253. $can_create = true;
  254. $can_reply = true;
  255. }
  256. break;
  257. }
  258. return $this->ok([
  259. 'rows' => DiscussionResource::collection($result),
  260. 'count' => $count,
  261. 'active' => $activeNumber,
  262. 'close' => $closeNumber,
  263. 'can_create' => $can_create,
  264. 'can_reply' => $can_reply,
  265. ]);
  266. }
  267. /**
  268. * 按句子批量查询讨论概览
  269. *
  270. * 对传入的每个句子定位(book/paragraph/word_start/word_end/channel_id)先查到 sentence,
  271. * 再取该句上全部顶级讨论(title、children_count、editor_uid,按创建时间倒序)。
  272. * 没有讨论的句子会被跳过,不做登录与权限校验。返回 rows 与 count。
  273. *
  274. * @bodyParam data array required 句子定位数组
  275. * @bodyParam data[].book integer required 卷号
  276. * @bodyParam data[].paragraph integer required 段号
  277. * @bodyParam data[].word_start integer required 句子起始词序号
  278. * @bodyParam data[].word_end integer required 句子结束词序号
  279. * @bodyParam data[].channel_id string required 译文集 channel uid
  280. */
  281. public function discussion_tree(Request $request)
  282. {
  283. $output = [];
  284. $sentences = $request->input('data');
  285. foreach ($sentences as $key => $sentence) {
  286. // 先查句子信息
  287. $sentInfo = Sentence::where('book_id', $sentence['book'])
  288. ->where('paragraph', $sentence['paragraph'])
  289. ->where('word_start', $sentence['word_start'])
  290. ->where('word_end', $sentence['word_end'])
  291. ->where('channel_uid', $sentence['channel_id'])
  292. ->first();
  293. if ($sentInfo) {
  294. $sentPr = Discussion::where('res_id', $sentInfo['uid'])
  295. ->whereNull('parent')
  296. ->select('title', 'children_count', 'editor_uid')
  297. ->orderBy('created_at', 'desc')->get();
  298. if (count($sentPr) > 0) {
  299. $output[] = [
  300. 'sentence' => [
  301. 'book' => $sentInfo->book_id,
  302. 'paragraph' => $sentInfo->paragraph,
  303. 'word_start' => $sentInfo->word_start,
  304. 'word_end' => $sentInfo->word_end,
  305. 'channel_id' => $sentInfo->channel_uid,
  306. 'content' => $sentInfo->content,
  307. 'pr_count' => count($sentPr),
  308. ],
  309. 'pr' => $sentPr,
  310. ];
  311. }
  312. }
  313. }
  314. return $this->ok(['rows' => $output, 'count' => count($output)]);
  315. }
  316. /**
  317. * 发表讨论 / 批注或回复
  318. *
  319. * 必须登录,未登录返回 401。传了 parent 即为回复:res_id 与 res_type 继承自父节点,
  320. * 无需再传,父节点不存在返回 no record,保存后父节点 children_count 自增;
  321. * 不传 parent 为顶级节点,此时 res_id、res_type、title 均为必填。
  322. * 锚点字段(pos_start/pos_end/quote_exact/quote_prefix/quote_suffix)两种情况都可选。
  323. * 默认会向消息队列 discussion 推送通知,可用 notification=false 关闭。
  324. *
  325. * @bodyParam parent string 父讨论 id,传入表示这是一条回复
  326. * @bodyParam res_id string required 关联资源 uid(有 parent 时忽略并继承父节点)
  327. * @bodyParam res_type string required 关联资源类型(有 parent 时忽略并继承父节点)。Enum: sentence,wbw,article
  328. * @bodyParam title string 标题(可选)
  329. * @bodyParam content string 正文内容
  330. * @bodyParam content_type string 正文格式。Default: markdown
  331. * @bodyParam type string 讨论类型。Enum: discussion,qa,help,note,commentary Default: discussion
  332. * @bodyParam tpl_id string 模板 id
  333. * @bodyParam pos_start integer 锚点在资源文本中的起始偏移,非负整数
  334. * @bodyParam pos_end integer 锚点在资源文本中的结束偏移,非负整数
  335. * @bodyParam quote_exact string 锚定的原文片段
  336. * @bodyParam quote_prefix string 锚定片段前的上下文,用于重新定位
  337. * @bodyParam quote_suffix string 锚定片段后的上下文,用于重新定位
  338. * @bodyParam notification boolean 是否推送消息队列通知。Default: true
  339. */
  340. public function store(Request $request)
  341. {
  342. $user = AuthService::current($request);
  343. if (! $user) {
  344. return $this->error(__('auth.failed'), [401], 401);
  345. }
  346. //
  347. // validate
  348. // read more on validation at http://laravel.com/docs/validation
  349. $annotationRules = [
  350. 'pos_start' => 'nullable|integer|min:0',
  351. 'pos_end' => 'nullable|integer|min:0',
  352. 'quote_exact' => 'nullable|string',
  353. 'quote_prefix' => 'nullable|string',
  354. 'quote_suffix' => 'nullable|string',
  355. ];
  356. if ($request->has('parent')) {
  357. $rules = $annotationRules;
  358. $parentInfo = Discussion::find($request->input('parent'));
  359. if (! $parentInfo) {
  360. return $this->error('no record');
  361. }
  362. } else {
  363. $rules = array_merge([
  364. 'res_id' => 'required',
  365. 'res_type' => 'required',
  366. ], $annotationRules);
  367. }
  368. $validated = $request->validate($rules);
  369. $discussion = new Discussion;
  370. if ($request->has('parent')) {
  371. $discussion->res_id = $parentInfo->res_id;
  372. $discussion->res_type = $parentInfo->res_type;
  373. } else {
  374. $discussion->res_id = $request->input('res_id');
  375. $discussion->res_type = $request->input('res_type');
  376. }
  377. $discussion->type = $request->input('type', 'discussion');
  378. $discussion->tpl_id = $request->input('tpl_id');
  379. $discussion->title = $request->input('title', null);
  380. $discussion->content = $request->input('content', null);
  381. $discussion->content_type = $request->input('content_type', 'markdown');
  382. $discussion->parent = $request->input('parent', null);
  383. $discussion->editor_uid = $user['user_uid'];
  384. $discussion->pos_start = $request->input('pos_start');
  385. $discussion->pos_end = $request->input('pos_end');
  386. $discussion->quote_exact = $request->input('quote_exact');
  387. $discussion->quote_prefix = $request->input('quote_prefix');
  388. $discussion->quote_suffix = $request->input('quote_suffix');
  389. $discussion->save();
  390. // 更新parent children_count
  391. if ($request->has('parent')) {
  392. $parentInfo->increment('children_count', 1);
  393. $parentInfo->save();
  394. }
  395. if ($request->input('notification', true)) {
  396. Mq::publish('discussion', new DiscussionResource($discussion));
  397. }
  398. return $this->ok(new DiscussionResource($discussion));
  399. }
  400. /**
  401. * 查询单条讨论
  402. *
  403. * 按路径参数做路由模型绑定并直接返回,不做登录与权限校验。
  404. *
  405. * @urlParam discussion string required 讨论 id
  406. */
  407. public function show(Discussion $discussion)
  408. {
  409. //
  410. return $this->ok(new DiscussionResource($discussion));
  411. }
  412. /**
  413. * 获取 discussion 锚点的数据
  414. *
  415. * 以句子为最小单位返回锚点上下文:res_type=wbw 时,由单词定位到所属 wbw block 与巴利句子,
  416. * 再用该 block 的 channel 渲染出句子内容的 markdown。其他 res_type 暂返回空字符串。
  417. * 不做登录与权限校验;wbw、wbw block 或句子任一查不到时分别返回 no wbw data / no wbwBlock data / no sent data。
  418. *
  419. * @urlParam id string required 讨论 id
  420. */
  421. public function anchor($id)
  422. {
  423. //
  424. $discussion = Discussion::find($id);
  425. $content = '';
  426. switch ($discussion->res_type) {
  427. case 'wbw':
  428. // 从逐词解析表获取逐词解析数据
  429. $wbw = Wbw::where('uid', $discussion->res_id)->first();
  430. if (! $wbw) {
  431. return $this->error('no wbw data');
  432. }
  433. $wbwBlock = WbwBlock::where('uid', $wbw->block_uid)->first();
  434. if (! $wbwBlock) {
  435. return $this->error('no wbwBlock data');
  436. }
  437. $sent = PaliSentence::where('book', $wbw->book_id)
  438. ->where('paragraph', $wbw->paragraph)
  439. ->where('word_begin', '<=', $wbw->wid)
  440. ->where('word_end', '>=', $wbw->wid)
  441. ->first();
  442. if (! $sent) {
  443. return $this->error('no sent data');
  444. }
  445. $sentId = "{$sent['book']}-{$sent['paragraph']}-{$sent['word_begin']}-{$sent['word_end']}";
  446. $channel = $wbwBlock->channel_uid;
  447. $content = MdRender::render('{{'.$sentId.'}}', [$channel]);
  448. break;
  449. default:
  450. // code...
  451. break;
  452. }
  453. return $this->ok($content);
  454. }
  455. /**
  456. * 修改讨论 / 批注
  457. *
  458. * 必须登录,未登录返回 403。作者本人可改;非作者则要求是资源所属 channel 的可编辑者
  459. * (res_type=sentence 取 sentence.channel_uid,res_type=wbw 经 wbw block 取 channel_uid),
  460. * 两者都不满足返回 403。
  461. * title、content、status 按请求覆盖,未提交时分别回落为 null、null、active;
  462. * type 与五个锚点字段为增量更新,只改请求里出现的字段,未提交的原样保留。
  463. *
  464. * @urlParam discussion string required 讨论 id
  465. *
  466. * @bodyParam title string 标题,未提交会被清空
  467. * @bodyParam content string 正文内容,未提交会被清空
  468. * @bodyParam status string 状态,未提交会重置为 active。Enum: active,close Default: active
  469. * @bodyParam type string 讨论类型,仅在提交时更新。Enum: discussion,qa,help,note,commentary
  470. * @bodyParam pos_start integer 锚点起始偏移,仅在提交时更新
  471. * @bodyParam pos_end integer 锚点结束偏移,仅在提交时更新
  472. * @bodyParam quote_exact string 锚定的原文片段,仅在提交时更新
  473. * @bodyParam quote_prefix string 锚定片段前的上下文,仅在提交时更新
  474. * @bodyParam quote_suffix string 锚定片段后的上下文,仅在提交时更新
  475. */
  476. public function update(Request $request, Discussion $discussion)
  477. {
  478. //
  479. $user = AuthService::current($request);
  480. if (! $user) {
  481. return $this->error(__('auth.failed'), [403], 403);
  482. }
  483. //
  484. $isManager = false;
  485. $isResManager = false;
  486. if ($discussion->editor_uid === $user['user_uid']) {
  487. $isManager = true;
  488. } else {
  489. // 查看是否是资源拥有者
  490. if ($discussion->res_type === 'sentence') {
  491. $res = Sentence::find($discussion->res_id);
  492. if ($res) {
  493. $channelId = $res->channel_uid;
  494. }
  495. } elseif ($discussion->res_type === 'wbw') {
  496. $res = Wbw::where('uid', $discussion->res_id)->first();
  497. if ($res) {
  498. $block = WbwBlock::where('uid', $res->block_uid)->first();
  499. if ($block) {
  500. $channelId = $block->channel_uid;
  501. }
  502. }
  503. }
  504. if (isset($channelId)) {
  505. $channel = Channel::find($channelId);
  506. if ($channel) {
  507. $isResManager = ChannelApi::userCanEdit($user['user_uid'], $channelId);
  508. }
  509. }
  510. }
  511. if (! $isManager && ! $isResManager) {
  512. return $this->error(__('auth.failed'), [403], 403);
  513. }
  514. // FIXME: 未提交 title/content/status 时会被覆盖成 null/active,与下方锚点字段的增量更新策略不一致,
  515. // 前端只想改单个字段时容易误清内容。建议统一改为 $request->has() 判断后再赋值。
  516. $discussion->title = $request->input('title', null);
  517. $discussion->content = $request->input('content', null);
  518. $discussion->status = $request->input('status', 'active');
  519. if ($request->has('type')) {
  520. $discussion->type = $request->input('type');
  521. }
  522. // 注释锚点字段:增量更新,只改请求里出现的字段,好让前端能显式清空;
  523. // 未提交的字段必须原样保留。
  524. foreach (['pos_start', 'pos_end', 'quote_exact', 'quote_prefix', 'quote_suffix'] as $field) {
  525. if ($request->has($field)) {
  526. $discussion->{$field} = $request->input($field);
  527. }
  528. }
  529. // $discussion->editor_uid = $user['user_uid'];
  530. $discussion->save();
  531. return $this->ok(new DiscussionResource($discussion));
  532. }
  533. /**
  534. * 删除讨论 / 批注
  535. *
  536. * 必须登录,未登录返回 401。目前只允许作者本人删除,其他人返回 403。
  537. * 不会级联删除子回复,也不会回退父节点的 children_count。
  538. *
  539. * @urlParam discussion string required 讨论 id
  540. */
  541. public function destroy(Request $request, Discussion $discussion)
  542. {
  543. //
  544. $user = AuthService::current($request);
  545. if (! $user) {
  546. return $this->error(__('auth.failed'), [401], 401);
  547. }
  548. // TODO 其他有权限的人也可以删除
  549. if ($discussion->editor_uid !== $user['user_uid']) {
  550. return $this->error(__('auth.failed'), [403], 403);
  551. }
  552. // FIXME: 删除时既不级联删除子回复(产生孤儿节点),也不回退父节点的 children_count,计数会漂移。
  553. // 建议在事务里一并删除子节点并 decrement 父节点计数。
  554. $delete = $discussion->delete();
  555. return $this->ok($delete);
  556. }
  557. }