Skip to content

Commit 52932c7

Browse files
committed
feat(导入): 添加加密文档验证和文档冲突检查
添加对加密文档的验证,禁止导入包含正文检索字段的加密文档 增加文档冲突检查,避免导入重复文档(相同标题+日期) 在导入时跳过已存在的文档并返回警告信息
1 parent 87336c2 commit 52932c7

2 files changed

Lines changed: 307 additions & 0 deletions

File tree

app/services/user_import_service.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def validate
3131
validate_schema_version
3232
validate_structure
3333
validate_unique_names
34+
validate_encrypted_documents
3435
rescue SchemaVersionError => e
3536
@errors << e.message
3637
rescue ImportError => e
@@ -147,6 +148,19 @@ def validate_unique_names
147148
check_existing_conflicts(folder_names, tag_names, person_names, state_names)
148149
end
149150

151+
def validate_encrypted_documents
152+
data = @json_data['data']
153+
154+
data['documents'].each_with_index do |doc, idx|
155+
next unless doc['encrypted_flag']
156+
157+
if doc.key?('document_text') && doc['document_text'].present?
158+
raise ImportError,
159+
"Document #{idx} ('#{doc['title']}'): 加密文档禁止导入正文检索字段"
160+
end
161+
end
162+
end
163+
150164
def check_existing_conflicts(folder_names, tag_names, person_names, state_names)
151165
existing_folders = @user.folders.where(name: folder_names).pluck(:name)
152166
if existing_folders.any?
@@ -167,6 +181,59 @@ def check_existing_conflicts(folder_names, tag_names, person_names, state_names)
167181
if existing_states.any?
168182
@warnings << "States already exist and will be skipped: #{existing_states.join(', ')}"
169183
end
184+
185+
check_document_conflicts
186+
end
187+
188+
def check_document_conflicts
189+
data = @json_data['data']
190+
imported_docs = data['documents']
191+
192+
doc_keys = Set.new
193+
duplicate_in_import = []
194+
195+
imported_docs.each do |doc|
196+
key = "#{doc['title']}|||#{doc['document_date']}"
197+
if doc_keys.include?(key)
198+
duplicate_in_import << doc['title']
199+
end
200+
doc_keys.add(key)
201+
end
202+
203+
if duplicate_in_import.any?
204+
raise ImportError,
205+
"Duplicate documents in import (same title + document_date): #{duplicate_in_import.uniq.join(', ')}"
206+
end
207+
208+
existing_doc_keys = Set.new
209+
@user.documents.each do |doc|
210+
key = "#{doc.title}|||#{doc.document_date}"
211+
existing_doc_keys.add(key)
212+
end
213+
214+
import_dates = imported_docs.pluck('document_date').compact.uniq
215+
existing_docs_by_date = {}
216+
217+
import_dates.each do |date_str|
218+
date = parse_date(date_str)
219+
docs = @user.documents.where(document_date: date)
220+
docs.each do |doc|
221+
key = "#{doc.title}|||#{doc.document_date}"
222+
existing_docs_by_date[key] = doc
223+
end
224+
end
225+
226+
conflicting_titles = []
227+
imported_docs.each do |doc|
228+
key = "#{doc['title']}|||#{doc['document_date']}"
229+
if existing_docs_by_date.key?(key)
230+
conflicting_titles << doc['title']
231+
end
232+
end
233+
234+
if conflicting_titles.any?
235+
@warnings << "Documents already exist (same title + document_date) and will be skipped: #{conflicting_titles.join(', ')}"
236+
end
170237
end
171238

172239
def import_with_transaction
@@ -320,8 +387,28 @@ def import_states(states_data)
320387

321388
def import_documents(documents_data)
322389
imported = 0
390+
skipped = 0
391+
392+
import_dates = documents_data.pluck('document_date').compact.uniq
393+
existing_docs_by_key = {}
394+
395+
import_dates.each do |date_str|
396+
date = parse_date(date_str)
397+
docs = @user.documents.where(document_date: date)
398+
docs.each do |doc|
399+
key = "#{doc.title}|||#{doc.document_date}"
400+
existing_docs_by_key[key] = doc
401+
end
402+
end
323403

324404
documents_data.each do |doc_data|
405+
doc_key = "#{doc_data['title']}|||#{doc_data['document_date']}"
406+
407+
if existing_docs_by_key.key?(doc_key)
408+
skipped += 1
409+
next
410+
end
411+
325412
folder_id = @name_to_id_map[:folders][doc_data['folder_name']]
326413
state_id = @name_to_id_map[:states][doc_data['state_name']]
327414
person_id = @name_to_id_map[:people][doc_data['person_name']]
@@ -363,6 +450,7 @@ def import_documents(documents_data)
363450
end
364451

365452
@imported_data[:documents] = imported
453+
@imported_data[:documents_skipped] = skipped if skipped > 0
366454
end
367455

368456
def parse_time(value)

test/controllers/imports_controller_test.rb

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,36 @@ class ImportsControllerTest < ActionController::TestCase
9191
documents: []
9292
}
9393
}
94+
95+
@encrypted_with_document_text_data = {
96+
schema_version: 1,
97+
exported_at: Time.current.iso8601,
98+
user: { name: 'Encrypted User', email: 'encrypted@example.com' },
99+
data: {
100+
folders: [
101+
{ name: 'Encrypted Folder', parent_folder_name: nil, created_at: 1.day.ago.iso8601, updated_at: 1.day.ago.iso8601 }
102+
],
103+
tags: [],
104+
people: [],
105+
states: [],
106+
documents: [
107+
{
108+
title: 'Encrypted With Text',
109+
description: 'Should be rejected',
110+
document_date: Date.today.iso8601,
111+
version: '1.0',
112+
encrypted_flag: true,
113+
folder_name: 'Encrypted Folder',
114+
state_name: nil,
115+
person_name: nil,
116+
tag_names: [],
117+
document_text: 'This should not be here for encrypted docs',
118+
created_at: 1.day.ago.iso8601,
119+
updated_at: 1.day.ago.iso8601
120+
}
121+
]
122+
}
123+
}
94124
end
95125

96126
test 'should preview import data without modifying database' do
@@ -299,4 +329,193 @@ class ImportsControllerTest < ActionController::TestCase
299329

300330
assert_equal 1, json_response['preview']['encrypted_documents']
301331
end
332+
333+
test 'should reject encrypted document with document_text' do
334+
original_folder_count = @user.folders.count
335+
original_doc_count = @user.documents.count
336+
337+
post :create, params: { import: @encrypted_with_document_text_data }, format: :json
338+
339+
assert_response :unprocessable_entity
340+
json_response = JSON.parse(response.body)
341+
342+
assert_not json_response['success']
343+
assert_not_empty json_response['errors']
344+
assert_includes json_response['errors'].first, '加密文档禁止导入正文检索字段'
345+
assert_includes json_response['errors'].first, 'Encrypted With Text'
346+
347+
assert_equal original_folder_count, @user.folders.count
348+
assert_equal original_doc_count, @user.documents.count
349+
end
350+
351+
test 'should reject encrypted document with document_text in preview' do
352+
post :preview, params: { import: @encrypted_with_document_text_data }, format: :json
353+
354+
assert_response :success
355+
json_response = JSON.parse(response.body)
356+
357+
assert_not_empty json_response['errors']
358+
assert_includes json_response['errors'].first, '加密文档禁止导入正文检索字段'
359+
end
360+
361+
test 'conflict: existing folder should be skipped with warning' do
362+
existing_folder = @user.folders.create!(name: 'Imported Work')
363+
364+
original_folder_count = @user.folders.count
365+
366+
post :create, params: { import: @valid_export_data }, format: :json
367+
368+
assert_response :success
369+
json_response = JSON.parse(response.body)
370+
371+
assert json_response['success']
372+
assert_not_empty json_response['warnings']
373+
assert_includes json_response['warnings'].first, 'Folders already exist'
374+
assert_includes json_response['warnings'].first, 'Imported Work'
375+
376+
assert_equal original_folder_count + 1, @user.folders.count
377+
assert @user.folders.exists?(id: existing_folder.id)
378+
end
379+
380+
test 'conflict: existing tag should be skipped with warning' do
381+
existing_tag = @user.tags.create!(name: 'Imported Tag', color: '#00ff00')
382+
383+
original_tag_count = @user.tags.count
384+
385+
post :create, params: { import: @valid_export_data }, format: :json
386+
387+
assert_response :success
388+
json_response = JSON.parse(response.body)
389+
390+
assert json_response['success']
391+
assert_not_empty json_response['warnings']
392+
assert_includes json_response['warnings'].first, 'Tags already exist'
393+
assert_includes json_response['warnings'].first, 'Imported Tag'
394+
395+
assert_equal original_tag_count, @user.tags.count
396+
assert @user.tags.exists?(id: existing_tag.id)
397+
assert_equal '#00ff00', existing_tag.reload.color
398+
end
399+
400+
test 'conflict: existing person should be skipped with warning' do
401+
existing_person = @user.people.create!(name: 'Imported Person')
402+
403+
original_person_count = @user.people.count
404+
405+
post :create, params: { import: @valid_export_data }, format: :json
406+
407+
assert_response :success
408+
json_response = JSON.parse(response.body)
409+
410+
assert json_response['success']
411+
assert_not_empty json_response['warnings']
412+
assert_includes json_response['warnings'].to_s, 'People already exist'
413+
assert_includes json_response['warnings'].to_s, 'Imported Person'
414+
415+
assert_equal original_person_count, @user.people.count
416+
assert @user.people.exists?(id: existing_person.id)
417+
end
418+
419+
test 'conflict: existing document (same title + date) should be skipped with warning' do
420+
test_date = Date.today
421+
existing_doc = @user.documents.create!(
422+
title: 'Imported Document',
423+
document_date: test_date,
424+
document_url: 'existing.pdf',
425+
document_text: 'existing text'
426+
)
427+
428+
original_doc_count = @user.documents.count
429+
430+
post :create, params: { import: @valid_export_data }, format: :json
431+
432+
assert_response :success
433+
json_response = JSON.parse(response.body)
434+
435+
assert json_response['success']
436+
assert_not_empty json_response['warnings']
437+
assert_includes json_response['warnings'].to_s, 'Documents already exist'
438+
assert_includes json_response['warnings'].to_s, 'Imported Document'
439+
440+
assert_equal 0, json_response['imported_data']['documents']
441+
assert_equal 1, json_response['imported_data']['documents_skipped']
442+
443+
assert_equal original_doc_count, @user.documents.count
444+
assert @user.documents.exists?(id: existing_doc.id)
445+
assert_equal 'existing text', existing_doc.reload.document_text
446+
end
447+
448+
test 'should reject import with duplicate documents in same data (same title + date)' do
449+
duplicate_data = @valid_export_data.deep_dup
450+
duplicate_data[:data][:documents] << {
451+
title: 'Imported Document',
452+
description: 'Duplicate',
453+
document_date: Date.today.iso8601,
454+
version: '2.0',
455+
encrypted_flag: false,
456+
folder_name: nil,
457+
state_name: nil,
458+
person_name: nil,
459+
tag_names: [],
460+
document_text: 'duplicate text',
461+
created_at: 1.day.ago.iso8601,
462+
updated_at: 1.day.ago.iso8601
463+
}
464+
465+
original_doc_count = @user.documents.count
466+
467+
post :create, params: { import: duplicate_data }, format: :json
468+
469+
assert_response :unprocessable_entity
470+
json_response = JSON.parse(response.body)
471+
472+
assert_not json_response['success']
473+
assert_not_empty json_response['errors']
474+
assert_includes json_response['errors'].first, 'Duplicate documents in import'
475+
assert_includes json_response['errors'].first, 'Imported Document'
476+
477+
assert_equal original_doc_count, @user.documents.count
478+
end
479+
480+
test 'conflict: mixed existing and new documents should import new ones' do
481+
test_date = Date.today
482+
@user.documents.create!(
483+
title: 'Imported Document',
484+
document_date: test_date,
485+
document_url: 'existing.pdf'
486+
)
487+
488+
data_with_new_doc = @valid_export_data.deep_dup
489+
data_with_new_doc[:data][:documents] << {
490+
title: 'New Document',
491+
description: 'Brand new',
492+
document_date: 1.day.ago.iso8601,
493+
version: '1.0',
494+
encrypted_flag: false,
495+
folder_name: 'Imported Work',
496+
state_name: nil,
497+
person_name: nil,
498+
tag_names: [],
499+
document_text: 'new document text',
500+
created_at: 1.day.ago.iso8601,
501+
updated_at: 1.day.ago.iso8601
502+
}
503+
504+
original_doc_count = @user.documents.count
505+
506+
assert_difference -> { @user.documents.count }, 1 do
507+
post :create, params: { import: data_with_new_doc }, format: :json
508+
end
509+
510+
assert_response :success
511+
json_response = JSON.parse(response.body)
512+
513+
assert json_response['success']
514+
assert_equal 1, json_response['imported_data']['documents']
515+
assert_equal 1, json_response['imported_data']['documents_skipped']
516+
assert_not_empty json_response['warnings']
517+
518+
assert @user.documents.find_by(title: 'Imported Document').present?
519+
assert @user.documents.find_by(title: 'New Document').present?
520+
end
302521
end

0 commit comments

Comments
 (0)