Просмотр исходного кода

fix: 术语查重按 channel,channel 信息按 channal 列取

两处都是被 AI 写入暴露出来的既有 bug:

- store() 的查重按 `owner = 当前身份`,而 channel 内术语落库的 owner 是
  channel 所属 studio。人类操作时两者恰好相等所以看不出来;模型的 uid 与
  owner 永远不等,查重必然落空,同一个 word+tag 会被反复插入。channel 内的
  唯一性本来就只该看 channel,去掉这个多余又有害的条件。
- TermResource 读的是 $this->channel_id,而列名是 channal,模型上根本没有
  这个属性。结果所有 channel 内的术语都拿不到 channel 信息,还会被当成社区
  术语去找 _community_translation_ 频道来渲染 note。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 1 неделя назад
Родитель
Сommit
8f3b1fdc1a

+ 6 - 3
api-v13/app/Http/Controllers/DhammaTermController.php

@@ -226,14 +226,17 @@ class DhammaTermController extends Controller
          * 查询重复的
          * 一个channel下面word+tag+language 唯一
          */
-        $table = DhammaTerm::where('owner', $user['user_uid'])
-            ->where('word', $request->input('word'))
+        $table = DhammaTerm::where('word', $request->input('word'))
             ->where('tag', $request->input('tag'));
         if (! empty($request->input('channel'))) {
+            // channel 内的唯一性只看 channel。此前这里还按 owner 过滤,而
+            // owner 取的是当前身份——AI 模型的 uid 与落库的 owner(channel
+            // 所属 studio)永远不等,查重必然落空,同一个词会被反复插入。
             $isDoesntExist = $table->where('channal', $request->input('channel'))
                 ->doesntExist();
         } else {
-            $isDoesntExist = $table->whereNull('channal')->where('language', $request->input('language'))
+            $isDoesntExist = $table->where('owner', $user['user_uid'])
+                ->whereNull('channal')->where('language', $request->input('language'))
                 ->doesntExist();
         }
 

+ 5 - 3
api-v13/app/Http/Resources/TermResource.php

@@ -51,9 +51,11 @@ class TermResource extends JsonResource
         if ($request->has('channel') && ! empty($request->input('channel'))) {
             $channels = explode('_', $request->input('channel'));
         } else {
-            if (! empty($this->channel_id) && Str::isUuid($this->channel_id)) {
-                $channelId = $this->channel_id;
-                $data['channel'] = ChannelApi::getById($this->channel_id);
+            // 列名是 channal;channel_id 只存在于本资源的输出里,模型上取不到,
+            // 于是所有 channel 内的术语都拿不到 channel 信息、还被当成社区术语渲染。
+            if (! empty($this->channal) && Str::isUuid($this->channal)) {
+                $channelId = $this->channal;
+                $data['channel'] = ChannelApi::getById($this->channal);
             } else {
                 $channelId = ChannelApi::getSysChannel('_community_translation_'.$this->language.'_');
                 if (empty($channelId)) {

+ 45 - 0
api-v13/tests/Feature/TermWriteAsAiModelTest.php

@@ -225,3 +225,48 @@ it('refuses to edit a studio-level term as an ai model', function () {
 
     expect(DhammaTerm::find($guid)->meaning)->toBe('念处');
 });
+
+it('rejects a duplicate word+tag in the same channel written by an ai model', function () {
+    $human = makeStudio('tester');
+    $channel = makeChannel($human);
+    $model = AiModel::factory()->ownedBy($human)->create();
+    [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
+
+    $payload = [
+        'word' => 'satipaṭṭhāna',
+        'meaning' => '念处',
+        'tag' => 'abhidhamma',
+        'channel' => $channel,
+        'access_token' => $accessToken,
+    ];
+
+    $this->postJson('/api/v2/terms', $payload, ['Authorization' => 'Bearer '.$modelToken])
+        ->assertOk();
+
+    // 查重按 channel,不按 owner——模型的 uid 与落库的 owner 不等,
+    // 按 owner 查会永远查不到重复,同一个词被反复插入。
+    $this->postJson('/api/v2/terms', $payload, ['Authorization' => 'Bearer '.$modelToken])
+        ->assertOk()
+        ->assertJsonPath('message', 'word existed');
+
+    expect(DhammaTerm::where('word', 'satipaṭṭhāna')->count())->toBe(1);
+});
+
+it('exposes the channel of a channel term', function () {
+    $human = makeStudio('tester');
+    $channel = makeChannel($human, 'my channel');
+    $model = AiModel::factory()->ownedBy($human)->create();
+    [$accessToken, $modelToken] = termWriteTokens($this, $human, $channel, $model);
+
+    $guid = $this->postJson('/api/v2/terms', [
+        'word' => 'satipaṭṭhāna',
+        'meaning' => '念处',
+        'channel' => $channel,
+        'access_token' => $accessToken,
+    ], ['Authorization' => 'Bearer '.$modelToken])->json('data.guid');
+
+    // channel 信息取自 channal 列;此前读的是模型上并不存在的 channel_id
+    $this->getJson("/api/v2/terms/{$guid}", authHeader($human))
+        ->assertOk()
+        ->assertJsonPath('data.channel.name', 'my channel');
+});