DhammaTermController.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Http\Api\ChannelApi;
  4. use App\Http\Api\ShareApi;
  5. use App\Http\Api\StudioApi;
  6. use App\Http\Controllers\Concerns\ChecksChannelEditPower;
  7. use App\Http\Resources\TermResource;
  8. use App\Models\AiModel;
  9. use App\Models\Channel;
  10. use App\Models\DhammaTerm;
  11. use App\Services\AuthService;
  12. use App\Tools\Tools;
  13. use Illuminate\Http\Request;
  14. use Illuminate\Http\Response;
  15. use Illuminate\Support\Facades\Cache;
  16. use Illuminate\Support\Facades\DB;
  17. use Illuminate\Support\Str;
  18. class DhammaTermController extends Controller
  19. {
  20. use ChecksChannelEditPower;
  21. /**
  22. * Display a listing of the resource.
  23. *
  24. * @return Response
  25. */
  26. public function index(Request $request)
  27. {
  28. $result = false;
  29. $indexCol = [
  30. 'id',
  31. 'guid',
  32. 'word',
  33. 'meaning',
  34. 'other_meaning',
  35. 'note',
  36. 'tag',
  37. 'language',
  38. 'channal',
  39. 'owner',
  40. 'editor_id',
  41. 'editor_uid',
  42. 'created_at',
  43. 'updated_at',
  44. ];
  45. switch ($request->input('view')) {
  46. case 'create-by-channel':
  47. // 新建术语时。根据术语所在channel 给出新建术语所需数据。如语言,备选意思等。
  48. // 获取channel信息
  49. $currChannel = Channel::where('uid', $request->input('channel'))->first();
  50. if (! $currChannel) {
  51. return $this->error(__('auth.failed'));
  52. }
  53. // TODO 查询studio信息
  54. // 获取同studio的channel列表
  55. $studioChannels = Channel::where('owner_uid', $currChannel->owner_uid)
  56. ->select(['name', 'uid'])
  57. ->get();
  58. // 获取全网意思列表
  59. $meanings = DhammaTerm::where('word', $request->input('word'))
  60. ->where('language', $currChannel->lang)
  61. ->select(['meaning', 'other_meaning'])
  62. ->get();
  63. $meaningList = [];
  64. foreach ($meanings as $key => $value) {
  65. // code...
  66. $meaning1 = [$value->meaning];
  67. if (! empty($value->other_meaning)) {
  68. $meaning2 = \explode(',', $value->other_meaning);
  69. $meaning1 = array_merge($meaning1, $meaning2);
  70. }
  71. foreach ($meaning1 as $key => $value) {
  72. // code...
  73. if (isset($meaningList[$value])) {
  74. $meaningList[$value]++;
  75. } else {
  76. $meaningList[$value] = 1;
  77. }
  78. }
  79. }
  80. $meaningCount = [];
  81. foreach ($meaningList as $key => $value) {
  82. // code...
  83. $meaningCount[] = ['meaning' => $key, 'count' => $value];
  84. }
  85. return $this->ok([
  86. 'word' => $request->input('word'),
  87. 'meaningCount' => $meaningCount,
  88. 'studioChannels' => $studioChannels,
  89. 'language' => $currChannel->lang,
  90. 'studio' => StudioApi::getById($currChannel->owner_uid),
  91. ]);
  92. break;
  93. case 'studio':
  94. // 获取 studio 内所有 term
  95. $user = AuthService::current($request);
  96. if (! $user) {
  97. return $this->error(__('auth.failed'), [], 401);
  98. }
  99. // 判断当前用户是否有指定的studio的权限
  100. if ($user['user_uid'] !== StudioApi::getIdByName($request->input('name'))) {
  101. return $this->error(__('auth.failed'), [], 403);
  102. }
  103. $table = DhammaTerm::select($indexCol)
  104. ->where('owner', $user['user_uid']);
  105. break;
  106. case 'channel':
  107. // 获取 studio 内所有 term
  108. $user = AuthService::current($request);
  109. if (! $user) {
  110. return $this->error(__('auth.failed'));
  111. }
  112. // 判断当前用户是否有指定的 channel 的权限
  113. $channel = Channel::find($request->input('id'));
  114. if ($user['user_uid'] !== $channel->owner_uid) {
  115. // 看是否为协作
  116. $power = ShareApi::getResPower($user['user_uid'], $request->input('id'));
  117. if ($power === 0) {
  118. return $this->error(__('auth.failed'), [], 403);
  119. }
  120. }
  121. $table = DhammaTerm::select($indexCol)
  122. ->where('channal', $request->input('id'));
  123. break;
  124. case 'show':
  125. return $this->ok(DhammaTerm::find($request->input('id')));
  126. break;
  127. case 'user':
  128. // code...
  129. $user = AuthService::current($request);
  130. if (! $user) {
  131. return $this->error(__('auth.failed'));
  132. }
  133. $userUid = $user['user_uid'];
  134. $search = $request->input('search');
  135. $table = DhammaTerm::select($indexCol)
  136. ->where('owner', $userUid);
  137. break;
  138. case 'word':
  139. $table = DhammaTerm::select($indexCol)
  140. ->whereIn('word', explode(',', $request->input('word')))
  141. ->orWhereIn('meaning', explode(',', $request->input('word')));
  142. break;
  143. case 'tag':
  144. $table = DhammaTerm::select($indexCol)
  145. ->whereIn('tag', explode(',', $request->input('tag')));
  146. break;
  147. case 'hot-meaning':
  148. $key = 'term/hot_meaning';
  149. $value = Cache::remember($key, config('mint.cache.expire'), function () use ($request) {
  150. $hotMeaning = [];
  151. $words = DhammaTerm::select('word')
  152. ->where('language', $request->input('language'))
  153. ->groupby('word')
  154. ->get();
  155. foreach ($words as $key => $word) {
  156. // code...
  157. $result = DhammaTerm::select(DB::raw('count(*) as word_count, meaning'))
  158. ->where('language', $request->input('language'))
  159. ->where('word', $word['word'])
  160. ->groupby('meaning')
  161. ->orderby('word_count', 'desc')
  162. ->first();
  163. if ($result) {
  164. $hotMeaning[] = [
  165. 'word' => $word['word'],
  166. 'meaning' => $result['meaning'],
  167. 'language' => $request->input('language'),
  168. 'owner' => '',
  169. ];
  170. }
  171. }
  172. Cache::put($key, $hotMeaning, 3600);
  173. return $hotMeaning;
  174. });
  175. return $this->ok(['rows' => $value, 'count' => count($value)]);
  176. break;
  177. default:
  178. // code...
  179. break;
  180. }
  181. $search = $request->input('search');
  182. if (! empty($search)) {
  183. $table = $table->where(function ($query) use ($search) {
  184. $query->where('word', 'like', $search.'%')
  185. ->orWhere('word_en', 'like', $search.'%')
  186. ->orWhere('meaning', 'like', '%'.$search.'%');
  187. });
  188. }
  189. $count = $table->count();
  190. $table = $table->orderBy($request->input('order', 'updated_at'), $request->input('dir', 'desc'));
  191. $table = $table->skip($request->input('offset', 0))
  192. ->take($request->input('limit', 1000));
  193. $result = $table->get();
  194. return $this->ok(['rows' => TermResource::collection($result), 'count' => $count]);
  195. }
  196. /**
  197. * Store a newly created resource in storage.
  198. *
  199. * @return Response
  200. */
  201. public function store(Request $request)
  202. {
  203. $user = AuthService::current($request);
  204. if (! $user) {
  205. return $this->error(__('auth.failed'));
  206. }
  207. $validated = $request->validate([
  208. 'word' => 'required',
  209. 'meaning' => 'required',
  210. ]);
  211. /**
  212. * 查询重复的
  213. * 一个channel下面word+tag+language 唯一
  214. */
  215. $table = DhammaTerm::where('owner', $user['user_uid'])
  216. ->where('word', $request->input('word'))
  217. ->where('tag', $request->input('tag'));
  218. if (! empty($request->input('channel'))) {
  219. $isDoesntExist = $table->where('channal', $request->input('channel'))
  220. ->doesntExist();
  221. } else {
  222. $isDoesntExist = $table->whereNull('channal')->where('language', $request->input('language'))
  223. ->doesntExist();
  224. }
  225. if ($isDoesntExist) {
  226. // 没有重复的 插入数据
  227. $term = new DhammaTerm;
  228. $term->id = app('snowflake')->id();
  229. $term->guid = Str::uuid();
  230. $term->word = $request->input('word');
  231. $term->word_en = Tools::getWordEn($request->input('word'));
  232. $term->meaning = $request->input('meaning');
  233. $term->other_meaning = $request->input('other_meaning');
  234. $term->note = $request->input('note');
  235. $term->tag = $request->input('tag');
  236. $term->channal = $request->input('channel');
  237. $term->language = $request->input('language');
  238. if (! empty($request->input('channel'))) {
  239. $channelInfo = ChannelApi::getById($request->input('channel'));
  240. if (! $channelInfo) {
  241. return $this->error('channel id failed');
  242. } else {
  243. // 查看有没有channel权限。术语没有 book 概念,access token 的
  244. // book 一律按 0(不限)判定。
  245. if (! $this->userCanEditChannel(
  246. $user['user_uid'],
  247. $request->input('channel'),
  248. 0,
  249. $request->input('access_token')
  250. )) {
  251. return $this->error(__('auth.failed'), [], 403);
  252. }
  253. $term->owner = $channelInfo['studio_id'];
  254. $term->language = $channelInfo['lang'];
  255. }
  256. } else {
  257. // AI 模型只能在 channel 里建术语。它的权限全部由人类签出的
  258. // channel access token 代持,而 access token 是 channel 级的,
  259. // 代持不了 studio 权限——没有 channel 就没有任何可核验的授权。
  260. if ($this->isAiModel($user['user_uid'])) {
  261. return $this->error('ai model must specify a channel', [], 403);
  262. }
  263. if ($request->has('studioId')) {
  264. $studioId = $request->input('studioId');
  265. } elseif ($request->has('studioName')) {
  266. $studioId = StudioApi::getIdByName($request->input('studioName'));
  267. }
  268. if (! isset($studioId) || ! Str::isUuid($studioId)) {
  269. return $this->error('not valid studioId');
  270. }
  271. // studio 级术语(不属于任何 channel)只能由 studio 本人建。
  272. // 此前这里不校验归属,任何登录用户都能往别人 studio 名下写。
  273. // access token 是 channel 级的,代持不了 studio 权限,所以
  274. // AI 模型建 studio 级术语必然走到这里被拒——这是有意的。
  275. if ($studioId !== $user['user_uid']) {
  276. return $this->error(__('auth.failed'), [], 403);
  277. }
  278. $term->owner = $studioId;
  279. }
  280. $term->editor_id = $user['user_id'];
  281. $term->editor_uid = $user['user_uid'];
  282. $term->create_time = time() * 1000;
  283. $term->modify_time = time() * 1000;
  284. $term->save();
  285. // 删除cache
  286. $this->deleteCache($term);
  287. return $this->ok(new TermResource($term));
  288. } else {
  289. return $this->error('word existed', [], 200);
  290. }
  291. }
  292. /**
  293. * 当前身份是不是 AI 模型。模型 token 的 user_id 恒为 0,但人类的旧 cookie
  294. * 鉴权也可能给出奇怪的值,故直接查表判定,不靠 id。
  295. */
  296. private function isAiModel(string $userUid): bool
  297. {
  298. return AiModel::where('uid', $userUid)->exists();
  299. }
  300. private function deleteCache($term)
  301. {
  302. if (empty($term->channal)) {
  303. // 通用 查询studio所有channel
  304. $channels = Channel::where('owner_uid', $term->owner)->select('uid')->get();
  305. foreach ($channels as $channel) {
  306. Cache::forget("/term/{$channel}/{$term->word}");
  307. }
  308. } else {
  309. Cache::forget("/term/{$term->channal}/{$term->word}");
  310. }
  311. }
  312. /**
  313. * Display the specified resource.
  314. *
  315. * @param string $id
  316. * @return Response
  317. */
  318. public function show(Request $request, $id)
  319. {
  320. //
  321. $result = DhammaTerm::where('guid', $id)->first();
  322. if ($result) {
  323. return $this->ok(new TermResource($result));
  324. } else {
  325. return $this->error('没有查询到数据');
  326. }
  327. }
  328. /**
  329. * Update the specified resource in storage.
  330. *
  331. * @param DhammaTerm $dhammaTerm
  332. * @return Response
  333. */
  334. public function update(Request $request, string $id)
  335. {
  336. //
  337. $user = AuthService::current($request);
  338. if (! $user) {
  339. return $this->error(__('auth.failed'), [], 401);
  340. }
  341. $dhammaTerm = DhammaTerm::find($id);
  342. if (! $dhammaTerm) {
  343. return $this->error('404');
  344. }
  345. if (empty($dhammaTerm->channal)) {
  346. // studio 级术语(不属于任何 channel)只有 owner 本人能改:
  347. // access token 是 channel 级的,代持不了 studio 权限。
  348. if ($this->isAiModel($user['user_uid'])) {
  349. return $this->error('ai model cannot edit a term outside a channel', [], 403);
  350. }
  351. if ($user['user_uid'] !== $dhammaTerm->owner) {
  352. return $this->error(__('auth.failed'), [], 403);
  353. }
  354. } else {
  355. // 查看有没有channel权限(owner / 协作者 / access token)
  356. if (! $this->userCanEditChannel(
  357. $user['user_uid'],
  358. $dhammaTerm->channal,
  359. 0,
  360. $request->input('access_token')
  361. )) {
  362. return $this->error(__('auth.failed'), [], 403);
  363. }
  364. }
  365. // 增量更新:只改提交上来的字段。此前这里无条件赋值,客户端漏提一个
  366. // 字段就会把库里的 note/tag 等清成 null。
  367. if ($request->has('word')) {
  368. $dhammaTerm->word = $request->input('word');
  369. $dhammaTerm->word_en = Tools::getWordEn($request->input('word'));
  370. }
  371. foreach (['meaning', 'other_meaning', 'note', 'tag', 'language'] as $field) {
  372. if ($request->has($field)) {
  373. $dhammaTerm->$field = $request->input($field);
  374. }
  375. }
  376. $dhammaTerm->editor_id = $user['user_id'];
  377. $dhammaTerm->editor_uid = $user['user_uid'];
  378. // create_time 是创建时刻,改动时不该被刷新
  379. $dhammaTerm->modify_time = time() * 1000;
  380. $dhammaTerm->save();
  381. // 删除cache
  382. $this->deleteCache($dhammaTerm);
  383. return $this->ok(new TermResource($dhammaTerm));
  384. }
  385. /**
  386. * Remove the specified resource from storage.
  387. *
  388. * @return Response
  389. */
  390. public function destroy(DhammaTerm $dhammaTerm, Request $request)
  391. {
  392. /**
  393. * 一次删除多个单词
  394. */
  395. $user = AuthService::current($request);
  396. if (! $user) {
  397. return $this->error(__('auth.failed'));
  398. }
  399. $count = 0;
  400. if ($request->has('uuid')) {
  401. // 查看是否有删除权限
  402. foreach ($request->input('id') as $key => $uuid) {
  403. $term = DhammaTerm::find($uuid);
  404. if (! $term) {
  405. continue;
  406. }
  407. if ($term->owner !== $user['user_uid']) {
  408. if (! empty($term->channal)) {
  409. // 看是否为协作
  410. $power = ShareApi::getResPower($user['user_uid'], $term->channal);
  411. if ($power < 20) {
  412. continue;
  413. }
  414. } else {
  415. continue;
  416. }
  417. }
  418. $count += $term->delete();
  419. // 删除cache
  420. $this->deleteCache($term);
  421. }
  422. } else {
  423. $arrId = json_decode($request->input('id'), true);
  424. foreach ($arrId as $key => $id) {
  425. // code...
  426. $term = DhammaTerm::where('id', $id)
  427. ->where('owner', $user['user_uid'])
  428. ->first();
  429. if (! $term) {
  430. continue;
  431. }
  432. // 先取到模型再删:此前这里把 query builder 传给 deleteCache,
  433. // 拿不到 word/channal,缓存根本没被清掉。
  434. $result = $term->delete();
  435. if ($result) {
  436. $this->deleteCache($term);
  437. $count++;
  438. }
  439. }
  440. }
  441. return $this->ok($count);
  442. }
  443. }