feat: 优化顺运宝同步反馈与记录刷新 (#61)

This commit is contained in:
chengma
2026-08-09 20:41:24 +08:00
parent 59d70b2d64
commit 3fd863da64
7 changed files with 99 additions and 20 deletions
+20 -14
View File
@@ -67,12 +67,13 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
return
}
syncStatus := service.GetSybSyncStatus()
status := msg
if status == "" {
status = sybStatusLine(result)
}
if service.GetSybSyncStatus().Running {
status = "同步进行中,请稍后刷新页面查看结果 · " + status
if syncStatus.Running && (msg == "" || strings.Contains(msg, "同步已开始")) {
status = "同步正在进行,请耐心等待;可打开同步记录刷新查看结果"
}
values := url.Values{}
@@ -152,8 +153,11 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
"SyncDateTo": dateTo,
"RangeError": c.Query("range_error"),
"RangeWarning": rangeWarning,
"SyncRunning": syncStatus.Running,
"SyncHistory": history,
"NeedSyncHistory": c.Query("history") == "1",
"HistoryRefreshURL": sybHistoryURL(history.Page,
keyword, dateFrom, dateTo),
"HistoryPagination": sybHistoryPagination(history.Page, history.TotalPages,
keyword, dateFrom, dateTo),
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
@@ -161,15 +165,7 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
}
func sybHistoryPagination(pageNum, totalPages int, keyword, dateFrom, dateTo string) service.PaginationView {
link := func(target int) string {
values := url.Values{"history": {"1"}, "history_page": {strconv.Itoa(target)}}
if keyword != "" {
values.Set("order_no", keyword)
}
values.Set("date_from", dateFrom)
values.Set("date_to", dateTo)
return "/syb?" + values.Encode()
}
link := func(target int) string { return sybHistoryURL(target, keyword, dateFrom, dateTo) }
view := service.PaginationView{
Page: pageNum, TotalPages: totalPages, HasPrev: pageNum > 1, HasNext: pageNum < totalPages,
FirstURL: link(1), LastURL: link(totalPages),
@@ -183,6 +179,16 @@ func sybHistoryPagination(pageNum, totalPages int, keyword, dateFrom, dateTo str
return view
}
func sybHistoryURL(pageNum int, keyword, dateFrom, dateTo string) string {
values := url.Values{"history": {"1"}, "history_page": {strconv.Itoa(pageNum)}}
if keyword != "" {
values.Set("order_no", keyword)
}
values.Set("date_from", dateFrom)
values.Set("date_to", dateTo)
return "/syb?" + values.Encode()
}
// sybStatusLine 组装底部状态条的默认文案(没有 msg 覆盖时)。
// `[必须]` 显示筛选后的**全量**总数,不是本页行数,见工单 #43 定下的规则。
func sybStatusLine(result *service.SybListResult) string {
@@ -238,7 +244,7 @@ func (h *Handler) SybSync(c *gin.Context) {
return
}
if !started {
h.sybRedirect(c, "验证码自动识别成功,已自动登录;但已经有一个同步任务在跑,请稍后刷新页面查看结果")
h.sybRedirect(c, "同步正在进行,请耐心等待,请勿重复点击")
return
}
h.sybRedirectAfterStart(c, "验证码自动识别成功,已自动登录,同步已开始,请稍后刷新页面查看结果")
@@ -251,7 +257,7 @@ func (h *Handler) SybSync(c *gin.Context) {
return
}
if !started {
h.sybRedirect(c, "已经有一个同步任务在跑,请稍后刷新页面查看结果,不要重复点击")
h.sybRedirect(c, "同步正在进行,请耐心等待,请勿重复点击")
return
}
h.sybRedirectAfterStart(c, "同步已开始,请稍后刷新页面查看结果")
@@ -363,7 +369,7 @@ func (h *Handler) SybLoginAndSync(c *gin.Context) {
return
}
if !started {
h.sybRedirect(c, "登录成功,但已经有一个同步任务在跑,请稍后刷新页面查看结果")
h.sybRedirect(c, "同步正在进行,请耐心等待,请勿重复点击")
return
}
h.sybRedirectAfterStart(c, "登录成功,同步已开始,请稍后刷新页面查看结果")
+16
View File
@@ -99,3 +99,19 @@ func TestSybHistoryPagination_保留筛选并重开弹窗(t *testing.T) {
}
}
}
func TestSybHistoryURL_刷新时保留当前页和筛选(t *testing.T) {
location, err := url.Parse(sybHistoryURL(3, "ORDER-1", "2026-08-01", "2026-08-09"))
if err != nil {
t.Fatal(err)
}
query := location.Query()
for name, want := range map[string]string{
"history": "1", "history_page": "3", "order_no": "ORDER-1",
"date_from": "2026-08-01", "date_to": "2026-08-09",
} {
if got := query.Get(name); got != want {
t.Fatalf("刷新链接 %s=%q,期望 %q;URL=%s", name, got, want, location.String())
}
}
}
+17 -1
View File
@@ -67,7 +67,8 @@ func TestMainPagesReturnOK(t *testing.T) {
sybResponse := httptest.NewRecorder()
router.ServeHTTP(sybResponse, sybRequest)
for _, want := range []string{
"同步记录", `name="date_from"`, `name="date_to"`,
"同步记录", "刷新记录", `data-history-refresh-url=`,
`name="date_from"`, `name="date_to"`,
"按顺运宝货运单创建日期(UTC+8)同步",
} {
if !strings.Contains(sybResponse.Body.String(), want) {
@@ -78,6 +79,21 @@ func TestMainPagesReturnOK(t *testing.T) {
t.Error("顺运宝页面不应再显示旧的指定日期同步入口")
}
if !service.TryStartSybSync() {
t.Fatal("准备同步中页面状态失败")
}
runningRequest := httptest.NewRequest(http.MethodGet, "/syb", nil)
addAuth(runningRequest)
runningResponse := httptest.NewRecorder()
router.ServeHTTP(runningResponse, runningRequest)
service.FinishSybSync(service.SyncReport{})
if runningResponse.Code != http.StatusOK ||
!strings.Contains(runningResponse.Body.String(), "同步中…") ||
!strings.Contains(runningResponse.Body.String(), `disabled aria-busy="true"`) ||
!strings.Contains(runningResponse.Body.String(), "同步正在进行,请耐心等待") {
t.Fatalf("同步运行时页面应禁用按钮并显示等待提示,响应:%s", runningResponse.Body.String())
}
pdd, err := repository.EnsurePddProduct(
db, "737116531267",
"https://mobile.yangkeduo.com/goods.html?goods_id=737116531267")
+2 -1
View File
@@ -288,7 +288,8 @@ tr.empty small { color: #aaa; }
white-space: normal;
word-break: break-word;
}
.sync-history-foot > span:first-child { color: #555; }
.sync-history-page { color: #555; white-space: nowrap; }
.sync-history-foot { flex-wrap: wrap; }
.sync-status {
display: inline-block;
min-width: 52px;
+30
View File
@@ -251,6 +251,34 @@
if (btn) btn.addEventListener("click", refresh);
}
/* 顺运宝同步是后台长任务。表单通过浏览器校验并提交后立即锁住按钮,
让操作员在页面跳转前就看到反馈;服务端互斥仍是最终保护。 */
function setupSybSyncFeedback() {
var form = document.querySelector("[data-syb-sync-form]");
if (!form) return;
var button = form.querySelector("[data-syb-sync-submit]");
if (!button) return;
form.addEventListener("submit", function () {
button.disabled = true;
button.setAttribute("aria-busy", "true");
button.textContent = "同步中…";
});
}
/* 同步记录继续由服务端渲染。刷新按钮带着 history=1 和当前页码重新加载,
返回后弹窗会自动打开;点击后先显示加载态,避免连续刷新。 */
function setupSybHistoryRefresh() {
var button = document.querySelector("[data-history-refresh-url]");
if (!button) return;
button.addEventListener("click", function () {
if (button.disabled) return;
button.disabled = true;
button.setAttribute("aria-busy", "true");
button.textContent = "刷新中…";
window.location.assign(button.getAttribute("data-history-refresh-url"));
});
}
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("table").forEach(setupCheckAll);
setupConfirmDelete();
@@ -260,6 +288,8 @@
setupModals();
setupRowDetail();
setupCaptchaRefresh();
setupSybSyncFeedback();
setupSybHistoryRefresh();
syncButtons();
});
+7 -3
View File
@@ -2,7 +2,7 @@
{{template "header" .}}
<div class="toolbar">
<form class="inline sync-range-form" method="post" action="/syb/sync">
<form class="inline sync-range-form" method="post" action="/syb/sync" data-syb-sync-form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<label for="sync-date-from">开始日期</label>
@@ -11,7 +11,10 @@
<label for="sync-date-to">结束日期</label>
<input id="sync-date-to" type="date" name="date_to"
value="{{.SyncDateTo}}" max="{{.Today}}" required>
<button type="submit" class="primary">同步</button>
<button type="submit" class="primary" data-syb-sync-submit
{{if .SyncRunning}}disabled aria-busy="true" title="同步正在进行,请耐心等待"{{end}}>
{{if .SyncRunning}}同步中…{{else}}同步{{end}}
</button>
</form>
<button type="button" data-modal-open="sync-history-modal">同步记录</button>
@@ -79,8 +82,9 @@
</div>
</div>
<div class="modal-foot sync-history-foot">
<span>第 {{.SyncHistory.Page}} / {{.SyncHistory.TotalPages}} 页</span>
<span class="grow"></span>
<button type="button" data-history-refresh-url="{{.HistoryRefreshURL}}">刷新记录</button>
<span class="sync-history-page">第 {{.SyncHistory.Page}} / {{.SyncHistory.TotalPages}} 页</span>
{{if .HistoryPagination.HasPrev}}
<a class="button-link" href="{{.HistoryPagination.FirstURL}}">首页</a>
<a class="button-link" href="{{.HistoryPagination.PrevURL}}">上一页</a>
+7 -1
View File
@@ -441,7 +441,9 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
```
有跳过/失败会在后面列出具体原因,不是只给个数字。同步中再次点「同步」会提示
"已经有一个同步任务在跑",不会并发跑两个。
“同步正在进行,请耐心等待,请勿重复点击”,不会并发跑两个。表单有效提交后,
按钮立即变成禁用的“同步中…”;页面检测到当前进程仍在同步时也保持这个状态。
同步成功启动使用状态条反馈,不弹出需要操作员手工关闭的成功确认框。
- 两端都必填,结束日期不得晚于 UTC+8 下的今天,闭区间最多 31 天。
- 日期范围经过自动 OCR 或手工验证码登录后必须原样保留。
@@ -453,6 +455,10 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
操作账号、开始/完成时间、同步统计、覆盖游标结果和失败原因。状态至少区分同步中、
成功、失败、已中断;不能只靠颜色表达。刷新或重启 Admin 后记录仍可查看。
弹窗底部右侧依次放“刷新记录”、当前页、翻页组件和“关闭”。点击刷新后按钮显示
“刷新中…”并暂时禁用;通过普通 GET 保留当前历史页码、订单号和日期范围,页面
返回后自动重开弹窗。本阶段不自动轮询,避免阅读历史记录时内容突然移动。
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
### 6.2 表格列