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

feat(annotation): discussions 锚定选择器字段接入 controller 读写与校验,补充测试

visuddhinanda 2 дней назад
Родитель
Сommit
efa156cc8c

+ 22 - 3
api-v13/app/Http/Controllers/DiscussionController.php

@@ -305,18 +305,25 @@ class DiscussionController extends Controller
         // validate
         // read more on validation at http://laravel.com/docs/validation
 
+        $annotationRules = [
+            'pos_start' => 'nullable|integer|min:0',
+            'pos_end' => 'nullable|integer|min:0',
+            'quote_exact' => 'nullable|string',
+            'quote_prefix' => 'nullable|string',
+            'quote_suffix' => 'nullable|string',
+        ];
         if ($request->has('parent')) {
-            $rules = [];
+            $rules = $annotationRules;
             $parentInfo = Discussion::find($request->input('parent'));
             if (! $parentInfo) {
                 return $this->error('no record');
             }
         } else {
-            $rules = [
+            $rules = array_merge([
                 'res_id' => 'required',
                 'res_type' => 'required',
                 'title' => 'required',
-            ];
+            ], $annotationRules);
         }
 
         $validated = $request->validate($rules);
@@ -336,6 +343,11 @@ class DiscussionController extends Controller
         $discussion->content_type = $request->input('content_type', 'markdown');
         $discussion->parent = $request->input('parent', null);
         $discussion->editor_uid = $user['user_uid'];
+        $discussion->pos_start = $request->input('pos_start');
+        $discussion->pos_end = $request->input('pos_end');
+        $discussion->quote_exact = $request->input('quote_exact');
+        $discussion->quote_prefix = $request->input('quote_prefix');
+        $discussion->quote_suffix = $request->input('quote_suffix');
         $discussion->save();
         // 更新parent children_count
         if ($request->has('parent')) {
@@ -453,6 +465,13 @@ class DiscussionController extends Controller
         if ($request->has('type')) {
             $discussion->type = $request->input('type');
         }
+        // 注释锚点字段:增量更新,只改请求里出现的字段,好让前端能显式清空;
+        // 未提交的字段必须原样保留。
+        foreach (['pos_start', 'pos_end', 'quote_exact', 'quote_prefix', 'quote_suffix'] as $field) {
+            if ($request->has($field)) {
+                $discussion->{$field} = $request->input($field);
+            }
+        }
         // $discussion->editor_uid = $user['user_uid'];
         $discussion->save();
 

+ 5 - 0
api-v13/app/Http/Resources/DiscussionResource.php

@@ -37,6 +37,11 @@ class DiscussionResource extends JsonResource
             'children_count' => Discussion::where('parent', $this->id)->count(),
             'created_at' => $this->created_at,
             'updated_at' => $this->updated_at,
+            'pos_start' => $this->pos_start,
+            'pos_end' => $this->pos_end,
+            'quote_exact' => $this->quote_exact,
+            'quote_prefix' => $this->quote_prefix,
+            'quote_suffix' => $this->quote_suffix,
         ];
         $channels = [];
         switch ($this->res_type) {

+ 7 - 0
api-v13/app/Models/Discussion.php

@@ -13,6 +13,8 @@ class Discussion extends Model
 
     protected $casts = [
         'id' => 'string',
+        'pos_start' => 'integer',
+        'pos_end' => 'integer',
     ];
 
     // 批量填充
@@ -26,6 +28,11 @@ class Discussion extends Model
         'content_type',
         'parent',
         'editor_uid',
+        'pos_start',
+        'pos_end',
+        'quote_exact',
+        'quote_prefix',
+        'quote_suffix',
     ];
 
     // 设置默认值

+ 128 - 0
api-v13/tests/Feature/DiscussionControllerTest.php

@@ -0,0 +1,128 @@
+<?php
+
+use App\Models\Discussion;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Str;
+
+uses(RefreshDatabase::class);
+
+it('stores the annotation selector fields on create', function () {
+    $userUid = makeStudio('annotator');
+    $resId = (string) Str::uuid();
+
+    $response = $this->postJson('/api/v2/discussion', [
+        'res_id' => $resId,
+        'res_type' => 'sentence',
+        'type' => 'note',
+        'title' => '义注',
+        'content' => '{{1-2-3-4}}',
+        'pos_start' => 10,
+        'pos_end' => 25,
+        'quote_exact' => '被锚定文本',
+        'quote_prefix' => '前缀上下文',
+        'quote_suffix' => '后缀上下文',
+        'notification' => false,
+    ], authHeader($userUid))->assertOk();
+
+    $saved = Discussion::where('editor_uid', $userUid)->first();
+
+    expect($saved->pos_start)->toBe(10);
+    expect($saved->pos_end)->toBe(25);
+    expect($saved->quote_exact)->toBe('被锚定文本');
+    expect($saved->quote_prefix)->toBe('前缀上下文');
+    expect($saved->quote_suffix)->toBe('后缀上下文');
+
+    // 资源输出同样暴露这些字段
+    expect($response->json('data.pos_start'))->toBe(10);
+    expect($response->json('data.pos_end'))->toBe(25);
+    expect($response->json('data.quote_exact'))->toBe('被锚定文本');
+    expect($response->json('data.quote_prefix'))->toBe('前缀上下文');
+    expect($response->json('data.quote_suffix'))->toBe('后缀上下文');
+});
+
+it('leaves annotation selector fields null when not provided', function () {
+    $userUid = makeStudio('annotator');
+
+    $this->postJson('/api/v2/discussion', [
+        'res_id' => (string) Str::uuid(),
+        'res_type' => 'sentence',
+        'type' => 'note',
+        'title' => '无锚点',
+        'content' => '{{1-2-3-4}}',
+        'notification' => false,
+    ], authHeader($userUid))->assertOk();
+
+    $saved = Discussion::where('editor_uid', $userUid)->first();
+
+    expect($saved->pos_start)->toBeNull();
+    expect($saved->pos_end)->toBeNull();
+    expect($saved->quote_exact)->toBeNull();
+    expect($saved->quote_prefix)->toBeNull();
+    expect($saved->quote_suffix)->toBeNull();
+});
+
+it('rejects an invalid annotation selector', function () {
+    $userUid = makeStudio('annotator');
+
+    $this->postJson('/api/v2/discussion', [
+        'res_id' => (string) Str::uuid(),
+        'res_type' => 'sentence',
+        'title' => 'x',
+        'pos_start' => -1,
+        'notification' => false,
+    ], authHeader($userUid))->assertStatus(422);
+
+    expect(Discussion::count())->toBe(0);
+});
+
+it('updates only the annotation fields present in the request', function () {
+    $userUid = makeStudio('annotator');
+    $discussion = new Discussion;
+    $discussion->forceFill([
+        'res_id' => (string) Str::uuid(),
+        'res_type' => 'sentence',
+        'type' => 'note',
+        'title' => '旧标题',
+        'content' => '{{1-2-3-4}}',
+        'editor_uid' => $userUid,
+        'pos_start' => 1,
+        'pos_end' => 2,
+        'quote_exact' => '保留我',
+        'quote_prefix' => '保留前缀',
+        'quote_suffix' => '保留后缀',
+    ])->save();
+
+    $this->putJson("/api/v2/discussion/{$discussion->id}", [
+        'pos_start' => 5,
+        'quote_exact' => '新摘录',
+    ], authHeader($userUid))->assertOk();
+
+    $discussion->refresh();
+
+    // 只改提交的字段
+    expect($discussion->pos_start)->toBe(5);
+    expect($discussion->quote_exact)->toBe('新摘录');
+    // 未提交的字段原样保留
+    expect($discussion->pos_end)->toBe(2);
+    expect($discussion->quote_prefix)->toBe('保留前缀');
+    expect($discussion->quote_suffix)->toBe('保留后缀');
+});
+
+it('allows clearing an annotation field explicitly', function () {
+    $userUid = makeStudio('annotator');
+    $discussion = new Discussion;
+    $discussion->forceFill([
+        'res_id' => (string) Str::uuid(),
+        'res_type' => 'sentence',
+        'type' => 'note',
+        'title' => '旧标题',
+        'editor_uid' => $userUid,
+        'quote_exact' => '旧摘录',
+    ])->save();
+
+    $this->putJson("/api/v2/discussion/{$discussion->id}", [
+        'quote_exact' => null,
+    ], authHeader($userUid))->assertOk();
+
+    expect($discussion->refresh()->quote_exact)->toBeNull();
+});