|
@@ -14,7 +14,6 @@ use App\Services\AuthService;
|
|
|
use App\Services\TermIndexService;
|
|
use App\Services\TermIndexService;
|
|
|
use App\Tools\Tools;
|
|
use App\Tools\Tools;
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Request;
|
|
|
-use Illuminate\Http\Response;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Log;
|
|
@@ -29,9 +28,35 @@ class DhammaTermController extends Controller
|
|
|
) {}
|
|
) {}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Display a listing of the resource.
|
|
|
|
|
|
|
+ * 列出术语(dhamma term)
|
|
|
*
|
|
*
|
|
|
- * @return Response
|
|
|
|
|
|
|
+ * 按 view 指定的口径返回术语列表。studio、channel、user 三个口径需要登录;
|
|
|
|
|
+ * studio 要求 name 指向的 studio 属于当前用户,否则 403;channel 要求当前用户是
|
|
|
|
|
+ * channel owner 或协作者(ShareApi 权限值非 0),否则 403。
|
|
|
|
|
+ * create-by-channel、show、hot-meaning 三个口径直接返回各自的数据结构,
|
|
|
|
|
+ * 不走后面的搜索/排序/分页;其余口径统一返回 {rows, count}。
|
|
|
|
|
+ * hot-meaning 结果会按 config('mint.cache.expire') 缓存在 term/hot_meaning 键下。
|
|
|
|
|
+ * 未指定 view(或给了无法识别的值)时没有任何基础查询,会直接报错,调用方必须传 view。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @queryParam view string required 查询口径。Enum: create-by-channel,studio,channel,show,user,word,tag,hot-meaning
|
|
|
|
|
+ * @queryParam channel string view=create-by-channel 时必填,目标 channel 的 uid,
|
|
|
|
|
+ * 用于确定语言、同 studio 的 channel 列表与 studio 信息
|
|
|
|
|
+ * @queryParam word string view=create-by-channel 时为要新建的术语词形;
|
|
|
|
|
+ * view=word 时为逗号分隔的词或义项列表,按 word 或 meaning 命中
|
|
|
|
|
+ * @queryParam name string view=studio 时必填,studio 名称,须与当前用户的 studio 一致
|
|
|
|
|
+ * @queryParam id string view=channel 时为 channel id(uid);view=show 时为术语主键 id
|
|
|
|
|
+ * @queryParam tag string view=tag 时必填,逗号分隔的 tag 列表
|
|
|
|
|
+ * @queryParam language string view=hot-meaning 时必填,统计该语言下每个词出现次数最多的义项
|
|
|
|
|
+ * @queryParam search string 关键字,按 word、word_en 前缀匹配或 meaning 模糊匹配
|
|
|
|
|
+ * @queryParam order string 排序字段。Default: updated_at
|
|
|
|
|
+ * @queryParam dir string 排序方向。Enum: asc,desc Default: desc
|
|
|
|
|
+ * @queryParam offset integer 分页偏移量。Default: 0
|
|
|
|
|
+ * @queryParam limit integer 每页条数。Default: 1000
|
|
|
|
|
+ * @queryParam mode string 渲染 note/summary 的模式,透传给 Markdown 渲染器。Default: read
|
|
|
|
|
+ * @queryParam format string note 的渲染输出格式。Enum: react,html,text Default: react
|
|
|
|
|
+ * @queryParam community_summary boolean 传入该参数时,自身没有 note 的术语会回落到社区
|
|
|
|
|
+ * channel 的解释作为 summary
|
|
|
|
|
+ * @queryParam exp boolean 传入该参数时附带编辑者截至本条更新时间的累计经验值(秒)
|
|
|
*/
|
|
*/
|
|
|
public function index(Request $request)
|
|
public function index(Request $request)
|
|
|
{
|
|
{
|
|
@@ -53,6 +78,8 @@ class DhammaTermController extends Controller
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
switch ($request->input('view')) {
|
|
switch ($request->input('view')) {
|
|
|
|
|
+ // FIXME: create-by-channel 口径完全不做登录/权限校验,任何人都能枚举任意
|
|
|
|
|
+ // channel 所属 studio 的全部 channel 列表;应补上与 channel 口径一致的鉴权。
|
|
|
case 'create-by-channel':
|
|
case 'create-by-channel':
|
|
|
// 新建术语时。根据术语所在channel 给出新建术语所需数据。如语言,备选意思等。
|
|
// 新建术语时。根据术语所在channel 给出新建术语所需数据。如语言,备选意思等。
|
|
|
// 获取channel信息
|
|
// 获取channel信息
|
|
@@ -122,6 +149,8 @@ class DhammaTermController extends Controller
|
|
|
return $this->error(__('auth.failed'));
|
|
return $this->error(__('auth.failed'));
|
|
|
}
|
|
}
|
|
|
// 判断当前用户是否有指定的 channel 的权限
|
|
// 判断当前用户是否有指定的 channel 的权限
|
|
|
|
|
+ // FIXME: find() 按主键查(其余地方 channel 都按 uid 查),且结果未判空就访问
|
|
|
|
|
+ // ->owner_uid,id 不存在时直接 500;应改为 where('uid', ...)->first() 并判空返回 404。
|
|
|
$channel = Channel::find($request->input('id'));
|
|
$channel = Channel::find($request->input('id'));
|
|
|
if ($user['user_uid'] !== $channel->owner_uid) {
|
|
if ($user['user_uid'] !== $channel->owner_uid) {
|
|
|
// 看是否为协作
|
|
// 看是否为协作
|
|
@@ -134,6 +163,8 @@ class DhammaTermController extends Controller
|
|
|
->where('channal', $request->input('id'));
|
|
->where('channal', $request->input('id'));
|
|
|
break;
|
|
break;
|
|
|
case 'show':
|
|
case 'show':
|
|
|
|
|
+ // FIXME: 不做登录校验且直接返回原始模型(未过 TermResource),会把 owner、
|
|
|
|
|
+ // editor_id 等非公开字段暴露出去;应改用 TermResource 并补权限判定。
|
|
|
return $this->ok(DhammaTerm::find($request->input('id')));
|
|
return $this->ok(DhammaTerm::find($request->input('id')));
|
|
|
break;
|
|
break;
|
|
|
case 'user':
|
|
case 'user':
|
|
@@ -143,11 +174,14 @@ class DhammaTermController extends Controller
|
|
|
return $this->error(__('auth.failed'));
|
|
return $this->error(__('auth.failed'));
|
|
|
}
|
|
}
|
|
|
$userUid = $user['user_uid'];
|
|
$userUid = $user['user_uid'];
|
|
|
|
|
+ // FIXME: 这里的 $search 是死代码,会被方法末尾同名变量重新赋值覆盖;应直接删除。
|
|
|
$search = $request->input('search');
|
|
$search = $request->input('search');
|
|
|
$table = DhammaTerm::select($indexCol)
|
|
$table = DhammaTerm::select($indexCol)
|
|
|
->where('owner', $userUid);
|
|
->where('owner', $userUid);
|
|
|
break;
|
|
break;
|
|
|
case 'word':
|
|
case 'word':
|
|
|
|
|
+ // FIXME: whereIn / orWhereIn 未用闭包分组,后面追加的 search 条件会与这个 or
|
|
|
|
|
+ // 错误结合导致越界结果;应包成 ->where(fn ($q) => $q->whereIn(...)->orWhereIn(...))。
|
|
|
$table = DhammaTerm::select($indexCol)
|
|
$table = DhammaTerm::select($indexCol)
|
|
|
->whereIn('word', explode(',', $request->input('word')))
|
|
->whereIn('word', explode(',', $request->input('word')))
|
|
|
->orWhereIn('meaning', explode(',', $request->input('word')));
|
|
->orWhereIn('meaning', explode(',', $request->input('word')));
|
|
@@ -165,6 +199,8 @@ class DhammaTermController extends Controller
|
|
|
->groupby('word')
|
|
->groupby('word')
|
|
|
->get();
|
|
->get();
|
|
|
|
|
|
|
|
|
|
+ // FIXME: 典型 N+1,每个 word 单独发一次聚合查询;应改成一次
|
|
|
|
|
+ // group by word, meaning 的查询再在 PHP 里取每个词的 top 1。
|
|
|
foreach ($words as $key => $word) {
|
|
foreach ($words as $key => $word) {
|
|
|
// code...
|
|
// code...
|
|
|
$result = DhammaTerm::select(DB::raw('count(*) as word_count, meaning'))
|
|
$result = DhammaTerm::select(DB::raw('count(*) as word_count, meaning'))
|
|
@@ -182,6 +218,8 @@ class DhammaTermController extends Controller
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ // FIXME: 外层 Cache::remember 已负责写缓存,这里重复写入且 TTL(3600)
|
|
|
|
|
+ // 与 config('mint.cache.expire') 不一致;应删除这一行。
|
|
|
Cache::put($key, $hotMeaning, 3600);
|
|
Cache::put($key, $hotMeaning, 3600);
|
|
|
|
|
|
|
|
return $hotMeaning;
|
|
return $hotMeaning;
|
|
@@ -189,6 +227,8 @@ class DhammaTermController extends Controller
|
|
|
|
|
|
|
|
return $this->ok(['rows' => $value, 'count' => count($value)]);
|
|
return $this->ok(['rows' => $value, 'count' => count($value)]);
|
|
|
break;
|
|
break;
|
|
|
|
|
+ // FIXME: 未传 view 或传了无法识别的 view 时 $table 未定义,会一路走到下面的
|
|
|
|
|
+ // $table->count() 直接抛错;应在 default 里返回明确的参数错误。
|
|
|
default:
|
|
default:
|
|
|
// code...
|
|
// code...
|
|
|
break;
|
|
break;
|
|
@@ -197,9 +237,9 @@ class DhammaTermController extends Controller
|
|
|
$search = $request->input('search');
|
|
$search = $request->input('search');
|
|
|
if (! empty($search)) {
|
|
if (! empty($search)) {
|
|
|
$table = $table->where(function ($query) use ($search) {
|
|
$table = $table->where(function ($query) use ($search) {
|
|
|
- $query->where('word', 'like', $search . '%')
|
|
|
|
|
- ->orWhere('word_en', 'like', $search . '%')
|
|
|
|
|
- ->orWhere('meaning', 'like', '%' . $search . '%');
|
|
|
|
|
|
|
+ $query->where('word', 'like', $search.'%')
|
|
|
|
|
+ ->orWhere('word_en', 'like', $search.'%')
|
|
|
|
|
+ ->orWhere('meaning', 'like', '%'.$search.'%');
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
$count = $table->count();
|
|
$count = $table->count();
|
|
@@ -212,9 +252,28 @@ class DhammaTermController extends Controller
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Store a newly created resource in storage.
|
|
|
|
|
|
|
+ * 新建术语
|
|
|
|
|
+ *
|
|
|
|
|
+ * 需要登录。传了 channel 则建 channel 级术语,owner 取该 channel 所属 studio、
|
|
|
|
|
+ * language 取该 channel 的语言(请求里的 language 被覆盖),并要求当前身份对该
|
|
|
|
|
+ * channel 有编辑权(owner / 协作者 / channel access token),否则 403。
|
|
|
|
|
+ * 不传 channel 则建 studio 级术语,只允许 studio 本人创建(studioId 必须等于当前
|
|
|
|
|
+ * 用户 uid,否则 403);AI 模型身份一律禁止(403),因为 access token 只能代持
|
|
|
|
|
+ * channel 级权限。
|
|
|
|
|
+ * 唯一性:channel 级按 channel+word+tag 查重,studio 级按 owner+word+tag+language
|
|
|
|
|
+ * 且 channal 为空查重;重复时返回 'word existed',HTTP 状态仍是 200。
|
|
|
|
|
+ * 写入成功后清除该词的 term 缓存并重建 OpenSearch 索引(索引失败只记日志)。
|
|
|
*
|
|
*
|
|
|
- * @return Response
|
|
|
|
|
|
|
+ * @bodyParam word string required 术语词形,同时用于生成 word_en
|
|
|
|
|
+ * @bodyParam meaning string required 主要义项
|
|
|
|
|
+ * @bodyParam other_meaning string 其他义项,逗号分隔
|
|
|
|
|
+ * @bodyParam note string 术语解释,Markdown
|
|
|
|
|
+ * @bodyParam tag string 标签,参与查重
|
|
|
|
|
+ * @bodyParam channel string channel uid。给出则建 channel 级术语,否则建 studio 级术语
|
|
|
|
|
+ * @bodyParam language string 语言。仅 studio 级术语生效,channel 级会被 channel 语言覆盖
|
|
|
|
|
+ * @bodyParam studioId string studio 的 uuid,不传 channel 时必填(或改用 studioName)
|
|
|
|
|
+ * @bodyParam studioName string studio 名称,未给 studioId 时用它换取 studioId
|
|
|
|
|
+ * @bodyParam access_token string channel access token,供 AI 模型等非 owner 身份验证 channel 编辑权
|
|
|
*/
|
|
*/
|
|
|
public function store(Request $request)
|
|
public function store(Request $request)
|
|
|
{
|
|
{
|
|
@@ -343,6 +402,8 @@ class DhammaTermController extends Controller
|
|
|
// 通用 查询studio所有channel
|
|
// 通用 查询studio所有channel
|
|
|
$channels = Channel::where('owner_uid', $term->owner)->select('uid')->get();
|
|
$channels = Channel::where('owner_uid', $term->owner)->select('uid')->get();
|
|
|
foreach ($channels as $channel) {
|
|
foreach ($channels as $channel) {
|
|
|
|
|
+ // FIXME: $channel 是 Eloquent 模型对象,插值后得到的是 JSON 而非 uid,
|
|
|
|
|
+ // 缓存键与写入时的键对不上,studio 级术语的缓存实际没被清掉;应用 $channel->uid。
|
|
|
Cache::forget("/term/{$channel}/{$term->word}");
|
|
Cache::forget("/term/{$channel}/{$term->word}");
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
@@ -369,14 +430,21 @@ class DhammaTermController extends Controller
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Display the specified resource.
|
|
|
|
|
|
|
+ * 查询单条术语
|
|
|
|
|
+ *
|
|
|
|
|
+ * 按 guid(不是自增 id)查找,公开接口,不需要登录。查不到返回错误 '没有查询到数据'。
|
|
|
*
|
|
*
|
|
|
- * @param string $id
|
|
|
|
|
- * @return Response
|
|
|
|
|
|
|
+ * @urlParam id string required 术语的 guid
|
|
|
|
|
+ *
|
|
|
|
|
+ * @queryParam channel string 渲染 note 时使用的 channel 列表,多个用下划线分隔;
|
|
|
|
|
+ * 不传则按术语自身的 channel 或对应语言的社区 channel 渲染
|
|
|
|
|
+ * @queryParam mode string 渲染模式。Default: read
|
|
|
|
|
+ * @queryParam format string note 的渲染输出格式。Enum: react,html,text Default: react
|
|
|
|
|
+ * @queryParam community_summary boolean 传入该参数时,没有 note 的术语回落到社区解释作为 summary
|
|
|
|
|
+ * @queryParam exp boolean 传入该参数时附带编辑者累计经验值(秒)
|
|
|
*/
|
|
*/
|
|
|
public function show(Request $request, $id)
|
|
public function show(Request $request, $id)
|
|
|
{
|
|
{
|
|
|
- //
|
|
|
|
|
$result = DhammaTerm::where('guid', $id)->first();
|
|
$result = DhammaTerm::where('guid', $id)->first();
|
|
|
if ($result) {
|
|
if ($result) {
|
|
|
return $this->ok(new TermResource($result));
|
|
return $this->ok(new TermResource($result));
|
|
@@ -386,14 +454,27 @@ class DhammaTermController extends Controller
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Update the specified resource in storage.
|
|
|
|
|
|
|
+ * 修改术语
|
|
|
|
|
+ *
|
|
|
|
|
+ * 需要登录(未登录 401),按主键 id 查找,不存在返回 '404'。
|
|
|
|
|
+ * 权限:studio 级术语(channal 为空)只有 owner 本人能改,AI 模型一律 403;
|
|
|
|
|
+ * channel 级术语要求对该 channel 有编辑权(owner / 协作者 / access token),否则 403。
|
|
|
|
|
+ * 增量更新——只有请求里出现的字段才会被写入,未提交的字段保持原值;
|
|
|
|
|
+ * 提交 word 时会同步重算 word_en。create_time 不变,只刷新 modify_time。
|
|
|
|
|
+ * 保存后清除该词的 term 缓存并重建 OpenSearch 索引(索引失败只记日志)。
|
|
|
*
|
|
*
|
|
|
- * @param DhammaTerm $dhammaTerm
|
|
|
|
|
- * @return Response
|
|
|
|
|
|
|
+ * @urlParam id string required 术语主键 id
|
|
|
|
|
+ *
|
|
|
|
|
+ * @bodyParam word string 术语词形,提交时同步重算 word_en
|
|
|
|
|
+ * @bodyParam meaning string 主要义项
|
|
|
|
|
+ * @bodyParam other_meaning string 其他义项,逗号分隔
|
|
|
|
|
+ * @bodyParam note string 术语解释,Markdown
|
|
|
|
|
+ * @bodyParam tag string 标签
|
|
|
|
|
+ * @bodyParam language string 语言
|
|
|
|
|
+ * @bodyParam access_token string channel access token,供非 owner 身份验证 channel 编辑权
|
|
|
*/
|
|
*/
|
|
|
public function update(Request $request, string $id)
|
|
public function update(Request $request, string $id)
|
|
|
{
|
|
{
|
|
|
- //
|
|
|
|
|
$user = AuthService::current($request);
|
|
$user = AuthService::current($request);
|
|
|
if (! $user) {
|
|
if (! $user) {
|
|
|
return $this->error(__('auth.failed'), [], 401);
|
|
return $this->error(__('auth.failed'), [], 401);
|
|
@@ -449,22 +530,32 @@ class DhammaTermController extends Controller
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Remove the specified resource from storage.
|
|
|
|
|
|
|
+ * 批量删除术语
|
|
|
|
|
+ *
|
|
|
|
|
+ * 需要登录。一次可以删除多条,返回实际删除的条数;无权限的条目被跳过而不报错。
|
|
|
|
|
+ * 传了 uuid 参数时,id 按数组处理:owner 本人可删,非 owner 且术语属于某个 channel 时
|
|
|
|
|
+ * 需要该 channel 的协作权限达到 20(编辑)才可删,studio 级术语非 owner 一律跳过。
|
|
|
|
|
+ * 未传 uuid 时,id 按 JSON 字符串数组处理,且只能删除 owner 是当前用户的术语。
|
|
|
|
|
+ * 每删一条都会清除该词对应的 term 缓存。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @urlParam dhammaTerm string required 路由模型绑定占位的术语 id,实际删除对象由 id 参数决定
|
|
|
*
|
|
*
|
|
|
- * @return Response
|
|
|
|
|
|
|
+ * @queryParam uuid string 存在该参数即表示 id 以数组形式提交,并启用 channel 协作者删除权限判定
|
|
|
|
|
+ * @queryParam id array required 要删除的术语 id 列表。带 uuid 时为数组,否则为 JSON 数组字符串
|
|
|
*/
|
|
*/
|
|
|
public function destroy(DhammaTerm $dhammaTerm, Request $request)
|
|
public function destroy(DhammaTerm $dhammaTerm, Request $request)
|
|
|
{
|
|
{
|
|
|
- /**
|
|
|
|
|
- * 一次删除多个单词
|
|
|
|
|
- */
|
|
|
|
|
$user = AuthService::current($request);
|
|
$user = AuthService::current($request);
|
|
|
if (! $user) {
|
|
if (! $user) {
|
|
|
return $this->error(__('auth.failed'));
|
|
return $this->error(__('auth.failed'));
|
|
|
}
|
|
}
|
|
|
$count = 0;
|
|
$count = 0;
|
|
|
|
|
+ // FIXME: 判断用的是 uuid 参数、实际读的却是 id 参数,uuid 纯当开关用,语义混乱
|
|
|
|
|
+ // 极易误用;应改为按 id 的实际类型分支,或统一成一个明确的参数格式。
|
|
|
if ($request->has('uuid')) {
|
|
if ($request->has('uuid')) {
|
|
|
// 查看是否有删除权限
|
|
// 查看是否有删除权限
|
|
|
|
|
+ // FIXME: 未校验 input('id') 是数组,客户端传字符串会导致 foreach 直接 fatal;
|
|
|
|
|
+ // 应先 validate(['id' => 'required|array'])。
|
|
|
foreach ($request->input('id') as $key => $uuid) {
|
|
foreach ($request->input('id') as $key => $uuid) {
|
|
|
$term = DhammaTerm::find($uuid);
|
|
$term = DhammaTerm::find($uuid);
|
|
|
if (! $term) {
|
|
if (! $term) {
|
|
@@ -486,6 +577,8 @@ class DhammaTermController extends Controller
|
|
|
$this->deleteCache($term);
|
|
$this->deleteCache($term);
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ // FIXME: json_decode 结果未判空,非法 JSON 时得到 null 会让下面的 foreach 报错;
|
|
|
|
|
+ // 应判断解析结果是数组,否则返回参数错误。
|
|
|
$arrId = json_decode($request->input('id'), true);
|
|
$arrId = json_decode($request->input('id'), true);
|
|
|
foreach ($arrId as $key => $id) {
|
|
foreach ($arrId as $key => $id) {
|
|
|
// code...
|
|
// code...
|