DhammaTermController.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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('word', $request->input('word'))
  216. ->where('tag', $request->input('tag'));
  217. if (! empty($request->input('channel'))) {
  218. // channel 内的唯一性只看 channel。此前这里还按 owner 过滤,而
  219. // owner 取的是当前身份——AI 模型的 uid 与落库的 owner(channel
  220. // 所属 studio)永远不等,查重必然落空,同一个词会被反复插入。
  221. $isDoesntExist = $table->where('channal', $request->input('channel'))
  222. ->doesntExist();
  223. } else {
  224. $isDoesntExist = $table->where('owner', $user['user_uid'])
  225. ->whereNull('channal')->where('language', $request->input('language'))
  226. ->doesntExist();
  227. }
  228. if ($isDoesntExist) {
  229. // 没有重复的 插入数据
  230. $term = new DhammaTerm;
  231. $term->id = app('snowflake')->id();
  232. $term->guid = Str::uuid();
  233. $term->word = $request->input('word');
  234. $term->word_en = Tools::getWordEn($request->input('word'));
  235. $term->meaning = $request->input('meaning');
  236. $term->other_meaning = $request->input('other_meaning');
  237. $term->note = $request->input('note');
  238. $term->tag = $request->input('tag');
  239. $term->channal = $request->input('channel');
  240. $term->language = $request->input('language');
  241. if (! empty($request->input('channel'))) {
  242. $channelInfo = ChannelApi::getById($request->input('channel'));
  243. if (! $channelInfo) {
  244. return $this->error('channel id failed');
  245. } else {
  246. // 查看有没有channel权限。术语没有 book 概念,access token 的
  247. // book 一律按 0(不限)判定。
  248. if (! $this->userCanEditChannel(
  249. $user['user_uid'],
  250. $request->input('channel'),
  251. 0,
  252. $request->input('access_token')
  253. )) {
  254. return $this->error(__('auth.failed'), [], 403);
  255. }
  256. $term->owner = $channelInfo['studio_id'];
  257. $term->language = $channelInfo['lang'];
  258. }
  259. } else {
  260. // AI 模型只能在 channel 里建术语。它的权限全部由人类签出的
  261. // channel access token 代持,而 access token 是 channel 级的,
  262. // 代持不了 studio 权限——没有 channel 就没有任何可核验的授权。
  263. if ($this->isAiModel($user['user_uid'])) {
  264. return $this->error('ai model must specify a channel', [], 403);
  265. }
  266. if ($request->has('studioId')) {
  267. $studioId = $request->input('studioId');
  268. } elseif ($request->has('studioName')) {
  269. $studioId = StudioApi::getIdByName($request->input('studioName'));
  270. }
  271. if (! isset($studioId) || ! Str::isUuid($studioId)) {
  272. return $this->error('not valid studioId');
  273. }
  274. // studio 级术语(不属于任何 channel)只能由 studio 本人建。
  275. // 此前这里不校验归属,任何登录用户都能往别人 studio 名下写。
  276. // access token 是 channel 级的,代持不了 studio 权限,所以
  277. // AI 模型建 studio 级术语必然走到这里被拒——这是有意的。
  278. if ($studioId !== $user['user_uid']) {
  279. return $this->error(__('auth.failed'), [], 403);
  280. }
  281. $term->owner = $studioId;
  282. }
  283. $term->editor_id = $user['user_id'];
  284. $term->editor_uid = $user['user_uid'];
  285. $term->create_time = time() * 1000;
  286. $term->modify_time = time() * 1000;
  287. $term->save();
  288. // 删除cache
  289. $this->deleteCache($term);
  290. return $this->ok(new TermResource($term));
  291. } else {
  292. return $this->error('word existed', [], 200);
  293. }
  294. }
  295. /**
  296. * 当前身份是不是 AI 模型。模型 token 的 user_id 恒为 0,但人类的旧 cookie
  297. * 鉴权也可能给出奇怪的值,故直接查表判定,不靠 id。
  298. */
  299. private function isAiModel(string $userUid): bool
  300. {
  301. return AiModel::where('uid', $userUid)->exists();
  302. }
  303. private function deleteCache($term)
  304. {
  305. if (empty($term->channal)) {
  306. // 通用 查询studio所有channel
  307. $channels = Channel::where('owner_uid', $term->owner)->select('uid')->get();
  308. foreach ($channels as $channel) {
  309. Cache::forget("/term/{$channel}/{$term->word}");
  310. }
  311. } else {
  312. Cache::forget("/term/{$term->channal}/{$term->word}");
  313. }
  314. }
  315. /**
  316. * Display the specified resource.
  317. *
  318. * @param string $id
  319. * @return Response
  320. */
  321. public function show(Request $request, $id)
  322. {
  323. //
  324. $result = DhammaTerm::where('guid', $id)->first();
  325. if ($result) {
  326. return $this->ok(new TermResource($result));
  327. } else {
  328. return $this->error('没有查询到数据');
  329. }
  330. }
  331. /**
  332. * Update the specified resource in storage.
  333. *
  334. * @param DhammaTerm $dhammaTerm
  335. * @return Response
  336. */
  337. public function update(Request $request, string $id)
  338. {
  339. //
  340. $user = AuthService::current($request);
  341. if (! $user) {
  342. return $this->error(__('auth.failed'), [], 401);
  343. }
  344. $dhammaTerm = DhammaTerm::find($id);
  345. if (! $dhammaTerm) {
  346. return $this->error('404');
  347. }
  348. if (empty($dhammaTerm->channal)) {
  349. // studio 级术语(不属于任何 channel)只有 owner 本人能改:
  350. // access token 是 channel 级的,代持不了 studio 权限。
  351. if ($this->isAiModel($user['user_uid'])) {
  352. return $this->error('ai model cannot edit a term outside a channel', [], 403);
  353. }
  354. if ($user['user_uid'] !== $dhammaTerm->owner) {
  355. return $this->error(__('auth.failed'), [], 403);
  356. }
  357. } else {
  358. // 查看有没有channel权限(owner / 协作者 / access token)
  359. if (! $this->userCanEditChannel(
  360. $user['user_uid'],
  361. $dhammaTerm->channal,
  362. 0,
  363. $request->input('access_token')
  364. )) {
  365. return $this->error(__('auth.failed'), [], 403);
  366. }
  367. }
  368. // 增量更新:只改提交上来的字段。此前这里无条件赋值,客户端漏提一个
  369. // 字段就会把库里的 note/tag 等清成 null。
  370. if ($request->has('word')) {
  371. $dhammaTerm->word = $request->input('word');
  372. $dhammaTerm->word_en = Tools::getWordEn($request->input('word'));
  373. }
  374. foreach (['meaning', 'other_meaning', 'note', 'tag', 'language'] as $field) {
  375. if ($request->has($field)) {
  376. $dhammaTerm->$field = $request->input($field);
  377. }
  378. }
  379. $dhammaTerm->editor_id = $user['user_id'];
  380. $dhammaTerm->editor_uid = $user['user_uid'];
  381. // create_time 是创建时刻,改动时不该被刷新
  382. $dhammaTerm->modify_time = time() * 1000;
  383. $dhammaTerm->save();
  384. // 删除cache
  385. $this->deleteCache($dhammaTerm);
  386. return $this->ok(new TermResource($dhammaTerm));
  387. }
  388. /**
  389. * Remove the specified resource from storage.
  390. *
  391. * @return Response
  392. */
  393. public function destroy(DhammaTerm $dhammaTerm, Request $request)
  394. {
  395. /**
  396. * 一次删除多个单词
  397. */
  398. $user = AuthService::current($request);
  399. if (! $user) {
  400. return $this->error(__('auth.failed'));
  401. }
  402. $count = 0;
  403. if ($request->has('uuid')) {
  404. // 查看是否有删除权限
  405. foreach ($request->input('id') as $key => $uuid) {
  406. $term = DhammaTerm::find($uuid);
  407. if (! $term) {
  408. continue;
  409. }
  410. if ($term->owner !== $user['user_uid']) {
  411. if (! empty($term->channal)) {
  412. // 看是否为协作
  413. $power = ShareApi::getResPower($user['user_uid'], $term->channal);
  414. if ($power < 20) {
  415. continue;
  416. }
  417. } else {
  418. continue;
  419. }
  420. }
  421. $count += $term->delete();
  422. // 删除cache
  423. $this->deleteCache($term);
  424. }
  425. } else {
  426. $arrId = json_decode($request->input('id'), true);
  427. foreach ($arrId as $key => $id) {
  428. // code...
  429. $term = DhammaTerm::where('id', $id)
  430. ->where('owner', $user['user_uid'])
  431. ->first();
  432. if (! $term) {
  433. continue;
  434. }
  435. // 先取到模型再删:此前这里把 query builder 传给 deleteCache,
  436. // 拿不到 word/channal,缓存根本没被清掉。
  437. $result = $term->delete();
  438. if ($result) {
  439. $this->deleteCache($term);
  440. $count++;
  441. }
  442. }
  443. }
  444. return $this->ok($count);
  445. }
  446. }