Fix e-paper subscriber import getting stuck on duplicate emails

This commit is contained in:
rulingcom 2026-07-09 15:45:37 +08:00
parent f4cf48409e
commit 3b8317e6c0
5 changed files with 133 additions and 23 deletions

View File

@ -106,20 +106,42 @@ class Admin::EPaperSubscribersController < OrbitAdminController
:status=>'Importing',
'all_count'=>all_count,
'current_count'=>current_count,
'finish_percent'=>finish_percent
'finish_percent'=>finish_percent,
'pid'=>Process.pid
}
)
# 修改點:迴圈外先準備一個陣列,收集匯入失敗的列,最後要回報給使用者
failed_rows = []
existing_subscribers = {}
EPaperSubscriber.all.each { |s| existing_subscribers[s.email] = s }
if subscribe_sheet
subscribe_sheet.each_with_index do |row, i|
next if i < 1
next if row.nil?
email = row.cells[0]&.value
next if email.blank?
subscriber = EPaperSubscriber.where(:email=>email).first || EPaperSubscriber.new(:email=>email)
subscriber.subscribed = true
subscriber.language = row.cells[1]&.value.presence || I18n.locale.to_s
subscriber.save
# 修改點:查詢前先用跟 model 一樣的清洗規則處理 email避免「查詢用原始字串、儲存用清洗後字串」不一致造成誤判成新資料
fixed_email = email.to_s.gsub(/[  ]/,'').sub(/;$/, '').downcase
if fixed_email.count('@') > 1
failed_rows << { row: i + 1, email: email.to_s, reason: '一個欄位包含多個信箱,請拆成單獨一列後重新匯入' }
current_count += 1
next
end
begin
# 修改點:改用記憶體 Hash 查詢,取代資料庫查詢
subscriber = existing_subscribers[fixed_email] || EPaperSubscriber.new(:email=>fixed_email)
subscriber.subscribed = true
subscriber.language = row.cells[1]&.value.presence || I18n.locale.to_s
subscriber.save!
# 修改點:存檔成功後更新 Hash避免同一份檔案裡後面重複出現的同一個email又找不到
existing_subscribers[subscriber.email] = subscriber
rescue => e
failed_rows << { row: i + 1, email: email.to_s, reason: e.message }
end
current_count += 1
if current_count % puts_every_count == 0
@ -132,13 +154,32 @@ class Admin::EPaperSubscribersController < OrbitAdminController
if unsubscribe_sheet
unsubscribe_sheet.each_with_index do |row, i|
next if i < 1
# 修改點對齊Subscribe那段整列空白時row本身是nil要先擋掉
next if row.nil?
email = row.cells[0]&.value
next if email.blank?
subscriber = EPaperSubscriber.where(:email=>email).first || EPaperSubscriber.new(:email=>email)
subscriber.subscribed = false
subscriber.language = row.cells[1]&.value.presence || I18n.locale.to_s
subscriber.save
# 修改點對齊Subscribe那段查詢前先用同一套清洗規則處理email
fixed_email = email.to_s.gsub(/[  ]/,'').sub(/;$/, '').downcase
# 修改點對齊Subscribe那段擋下一格塞多個信箱的資料
if fixed_email.count('@') > 1
failed_rows << { row: i + 1, email: email.to_s, reason: '一個欄位包含多個信箱,請拆成單獨一列後重新匯入' }
current_count += 1
next
end
# 修改點對齊Subscribe那段改用記憶體Hash查詢取代資料庫查詢
# 修改點save改save!並包begin/rescue例外不再讓整條Thread死掉
begin
subscriber = existing_subscribers[fixed_email] || EPaperSubscriber.new(:email=>fixed_email)
subscriber.subscribed = false
subscriber.language = row.cells[1]&.value.presence || I18n.locale.to_s
subscriber.save!
existing_subscribers[subscriber.email] = subscriber
rescue => e
failed_rows << { row: i + 1, email: email.to_s, reason: e.message }
end
current_count += 1
if current_count % puts_every_count == 0
@ -153,7 +194,8 @@ class Admin::EPaperSubscribersController < OrbitAdminController
:status=>'finish',
'all_count'=>all_count,
'current_count'=>current_count,
'finish_percent'=>100
'finish_percent'=>100,
'failed_rows'=>failed_rows
}
)
end
@ -161,6 +203,19 @@ class Admin::EPaperSubscribersController < OrbitAdminController
redirect_to admin_e_paper_subscribers_path(thread_id: thread.id)
end
def last_import_result
thread = Multithread.where(:key=>'import_epaper_subscribers').first
stalled = false
if thread && thread.status['status'] == 'Importing' && thread.status['pid']
begin
Process.kill(0, thread.status['pid'])
rescue Errno::ESRCH
stalled = true
end
end
render :partial => 'last_import_result', locals: { thread: thread, stalled: stalled }
end
def download_excel_format
@subscribers = []
@unsubscribers = []

View File

@ -7,14 +7,19 @@ class EPaperSubscriber
field :subscribed, type: Boolean, :default => true
field :language
field :last_paper_sent
before_save do
self.fix_email
if self.class.where(:email=>self.email, :language=>self.language,:subscribed=>true).count != 0
return false
# 修改點:用 throw(:abort) 取代 return falseRails 5+ block 內不可用 return
# 修改點:排除自己這筆(self.id.ne=>self.id),避免更新既有紀錄時把自己算成重複
if self.class.where(:id.ne=>self.id, :email=>self.email, :language=>self.language, :subscribed=>true).count != 0
throw(:abort)
end
end
def fix_email(save_flag=false)
self.email = self.email.to_s.gsub(/[  ]/,'').sub(/;$/, '')
# 修改點:加上 .downcase統一大小寫避免 ABC@Gmail.com 跟 abc@gmail.com 被當成兩筆
self.email = self.email.to_s.gsub(/[ ]/,'').sub(/;$/, '').downcase
self.save if save_flag
end
end
end

View File

@ -53,9 +53,7 @@
content_tag(:div, link_to(t("e_paper.export"), admin_e_paper_subscribers_export_excel_path + '.xlsx', :class=>"btn btn-success"), class: "pull-right")
%>
<div class="dropup upload-button pull-right">
<% if @thread %>
<button class="show_progress btn btn-primary" type="button"><%= t("e_paper.show_progress") %></button>
<% end %>
<button class="btn btn-default" id="show-last-import-result" type="button">查看最近一次匯入</button>
<button class="btn btn-info dropdown-toggle" type="button" data-toggle="dropdown">
<i class="icon-upload-alt icon-white"></i><%= t('personal_journal.upload') %>
<span class="caret"></span>
@ -94,7 +92,20 @@
window.clearTimeout(window.time_out_id);
window.setTimeout(function(){
$("#threadModal").modal("hide");
alert(data["status"]);
// 修改點is_finish後檢查failed_rows有內容才組成清單提醒取代原本單純alert(status)
var failed_rows = data["failed_rows"];
if(failed_rows && failed_rows.length > 0){
var msg = "匯入完成,但有 " + failed_rows.length + " 筆資料匯入失敗:\n\n";
failed_rows.slice(0, 20).forEach(function(r){
msg += "第" + r["row"] + "列 (" + r["email"] + ")" + r["reason"] + "\n";
});
if(failed_rows.length > 20){
msg += "\n...等共 " + failed_rows.length + " 筆僅顯示前20筆";
}
alert(msg);
}else{
alert(data["status"]);
}
}, 3000);
return;
}
@ -107,10 +118,15 @@
$("#threadModal").on('shown.bs.modal',function(){
window.time_out_id = window.setTimeout(update_thread, 1000);
})
$("#threadModal").modal("show");
$(".show_progress").click(function(){
$("#threadModal").modal("show");
})
}
$("#show-last-import-result").click(function(){
$.get("<%=admin_e_paper_subscribers_last_import_result_path%>", function(html){
if($("#lastImportResultModal").length == 0){
$("body").append('<div id="lastImportResultModal" class="modal hide fade" tabindex="-1"><div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h3>上次匯入結果</h3></div><div id="lastImportResultBody"></div><div class="modal-footer"><button class="btn" data-dismiss="modal">關閉</button></div></div></div></div>');
}
$("#lastImportResultBody").html(html);
$("#lastImportResultModal").modal("show");
});
});
})
</script>
</script>

View File

@ -0,0 +1,33 @@
<div class="modal-body">
<p style="color:#888;font-size:0.9em;">此畫面顯示的是查詢當下的狀態,若匯入仍在進行中,請關閉後重新點擊按鈕以取得最新進度。</p>
<% if thread.nil? %>
<p>目前沒有匯入紀錄。</p>
<% elsif stalled %>
<p style="color:#c00;font-weight:bold;">匯入似乎已意外中斷(原本處理這次匯入的伺服器程序已經不存在,可能是伺服器重啟或未預期的錯誤所致)。</p>
<p>中斷前已處理:<%= thread.status['current_count'] %> / <%= thread.status['all_count'] %><%= thread.status['finish_percent'] %>%),這些資料已寫入資料庫,不會消失。如需完成剩餘資料,請重新上傳檔案。</p>
<% else %>
<p>狀態:<%= thread.status['status'] %>/已處理 <%= thread.status['current_count'] %> / <%= thread.status['all_count'] %><%= thread.status['finish_percent'] %>%</p>
<% end %>
<% if thread %>
<% failed_rows = thread.status['failed_rows'] || [] %>
<% if failed_rows.empty? %>
<p>沒有失敗紀錄。</p>
<% else %>
<p>共 <%= failed_rows.count %> 筆匯入失敗:</p>
<table class="table">
<thead><tr><th>列號</th><th>Email</th><th>原因</th></tr></thead>
<tbody>
<% failed_rows.each do |r| %>
<tr>
<td><%= r['row'] %></td>
<td><%= r['email'] %></td>
<td><%= r['reason'] %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% end %>
</div>

View File

@ -33,6 +33,7 @@ Rails.application.routes.draw do
get 'e_paper_subscribers/export_excel', to: 'e_paper_subscribers#export_excel'
get 'e_paper_subscribers/download_excel_format', to: 'e_paper_subscribers#download_excel_format'
post 'e_paper_subscribers/import_from_excel', to: 'e_paper_subscribers#import_from_excel'
get 'e_paper_subscribers/last_import_result', to: 'e_paper_subscribers#last_import_result'
get 'e_paper_subscribers/get_subscribers_modal', to: 'e_paper_subscribers#get_subscribers_modal'
get 'e_paper_subscribers/batch_delete_subscribers', to: 'e_paper_subscribers#batch_delete_subscribers'
post 'e_paper_subscribers/delete_subscribers', to: 'e_paper_subscribers#delete_subscribers'