2
0

DhammaTermController.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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 = $this->editorId($user);
  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. * editor_id 存的是人类用户的自增 sn,模型没有。模型 token 里的 id 恒为 0,
  297. * 而 0 同时也是「缺省/未知」的值,落库后分不清是模型写的还是数据有问题,
  298. * 故模型一律记 -1;模型的真实身份看 editor_uid。
  299. *
  300. * @param array<string, mixed> $user
  301. */
  302. private function editorId(array $user): int
  303. {
  304. return $this->isAiModel($user['user_uid']) ? -1 : (int) $user['user_id'];
  305. }
  306. /**
  307. * 当前身份是不是 AI 模型。模型 token 的 user_id 恒为 0,但人类的旧 cookie
  308. * 鉴权也可能给出奇怪的值,故直接查表判定,不靠 id。
  309. */
  310. private function isAiModel(string $userUid): bool
  311. {
  312. return AiModel::where('uid', $userUid)->exists();
  313. }
  314. private function deleteCache($term)
  315. {
  316. if (empty($term->channal)) {
  317. // 通用 查询studio所有channel
  318. $channels = Channel::where('owner_uid', $term->owner)->select('uid')->get();
  319. foreach ($channels as $channel) {
  320. Cache::forget("/term/{$channel}/{$term->word}");
  321. }
  322. } else {
  323. Cache::forget("/term/{$term->channal}/{$term->word}");
  324. }
  325. }
  326. /**
  327. * Display the specified resource.
  328. *
  329. * @param string $id
  330. * @return Response
  331. */
  332. public function show(Request $request, $id)
  333. {
  334. //
  335. $result = DhammaTerm::where('guid', $id)->first();
  336. if ($result) {
  337. return $this->ok(new TermResource($result));
  338. } else {
  339. return $this->error('没有查询到数据');
  340. }
  341. }
  342. /**
  343. * Update the specified resource in storage.
  344. *
  345. * @param DhammaTerm $dhammaTerm
  346. * @return Response
  347. */
  348. public function update(Request $request, string $id)
  349. {
  350. //
  351. $user = AuthService::current($request);
  352. if (! $user) {
  353. return $this->error(__('auth.failed'), [], 401);
  354. }
  355. $dhammaTerm = DhammaTerm::find($id);
  356. if (! $dhammaTerm) {
  357. return $this->error('404');
  358. }
  359. if (empty($dhammaTerm->channal)) {
  360. // studio 级术语(不属于任何 channel)只有 owner 本人能改:
  361. // access token 是 channel 级的,代持不了 studio 权限。
  362. if ($this->isAiModel($user['user_uid'])) {
  363. return $this->error('ai model cannot edit a term outside a channel', [], 403);
  364. }
  365. if ($user['user_uid'] !== $dhammaTerm->owner) {
  366. return $this->error(__('auth.failed'), [], 403);
  367. }
  368. } else {
  369. // 查看有没有channel权限(owner / 协作者 / access token)
  370. if (! $this->userCanEditChannel(
  371. $user['user_uid'],
  372. $dhammaTerm->channal,
  373. 0,
  374. $request->input('access_token')
  375. )) {
  376. return $this->error(__('auth.failed'), [], 403);
  377. }
  378. }
  379. // 增量更新:只改提交上来的字段。此前这里无条件赋值,客户端漏提一个
  380. // 字段就会把库里的 note/tag 等清成 null。
  381. if ($request->has('word')) {
  382. $dhammaTerm->word = $request->input('word');
  383. $dhammaTerm->word_en = Tools::getWordEn($request->input('word'));
  384. }
  385. foreach (['meaning', 'other_meaning', 'note', 'tag', 'language'] as $field) {
  386. if ($request->has($field)) {
  387. $dhammaTerm->$field = $request->input($field);
  388. }
  389. }
  390. $dhammaTerm->editor_id = $this->editorId($user);
  391. $dhammaTerm->editor_uid = $user['user_uid'];
  392. // create_time 是创建时刻,改动时不该被刷新
  393. $dhammaTerm->modify_time = time() * 1000;
  394. $dhammaTerm->save();
  395. // 删除cache
  396. $this->deleteCache($dhammaTerm);
  397. return $this->ok(new TermResource($dhammaTerm));
  398. }
  399. /**
  400. * Remove the specified resource from storage.
  401. *
  402. * @return Response
  403. */
  404. public function destroy(DhammaTerm $dhammaTerm, Request $request)
  405. {
  406. /**
  407. * 一次删除多个单词
  408. */
  409. $user = AuthService::current($request);
  410. if (! $user) {
  411. return $this->error(__('auth.failed'));
  412. }
  413. $count = 0;
  414. if ($request->has('uuid')) {
  415. // 查看是否有删除权限
  416. foreach ($request->input('id') as $key => $uuid) {
  417. $term = DhammaTerm::find($uuid);
  418. if (! $term) {
  419. continue;
  420. }
  421. if ($term->owner !== $user['user_uid']) {
  422. if (! empty($term->channal)) {
  423. // 看是否为协作
  424. $power = ShareApi::getResPower($user['user_uid'], $term->channal);
  425. if ($power < 20) {
  426. continue;
  427. }
  428. } else {
  429. continue;
  430. }
  431. }
  432. $count += $term->delete();
  433. // 删除cache
  434. $this->deleteCache($term);
  435. }
  436. } else {
  437. $arrId = json_decode($request->input('id'), true);
  438. foreach ($arrId as $key => $id) {
  439. // code...
  440. $term = DhammaTerm::where('id', $id)
  441. ->where('owner', $user['user_uid'])
  442. ->first();
  443. if (! $term) {
  444. continue;
  445. }
  446. // 先取到模型再删:此前这里把 query builder 传给 deleteCache,
  447. // 拿不到 word/channal,缓存根本没被清掉。
  448. $result = $term->delete();
  449. if ($result) {
  450. $this->deleteCache($term);
  451. $count++;
  452. }
  453. }
  454. }
  455. return $this->ok($count);
  456. }
  457. }