feat: 优化顺运宝同步反馈与记录刷新 (#61)
This commit is contained in:
+20
-14
@@ -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, "登录成功,同步已开始,请稍后刷新页面查看结果")
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -441,7 +441,9 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
||||
```
|
||||
|
||||
有跳过/失败会在后面列出具体原因,不是只给个数字。同步中再次点「同步」会提示
|
||||
"已经有一个同步任务在跑",不会并发跑两个。
|
||||
“同步正在进行,请耐心等待,请勿重复点击”,不会并发跑两个。表单有效提交后,
|
||||
按钮立即变成禁用的“同步中…”;页面检测到当前进程仍在同步时也保持这个状态。
|
||||
同步成功启动使用状态条反馈,不弹出需要操作员手工关闭的成功确认框。
|
||||
|
||||
- 两端都必填,结束日期不得晚于 UTC+8 下的今天,闭区间最多 31 天。
|
||||
- 日期范围经过自动 OCR 或手工验证码登录后必须原样保留。
|
||||
@@ -453,6 +455,10 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
||||
操作账号、开始/完成时间、同步统计、覆盖游标结果和失败原因。状态至少区分同步中、
|
||||
成功、失败、已中断;不能只靠颜色表达。刷新或重启 Admin 后记录仍可查看。
|
||||
|
||||
弹窗底部右侧依次放“刷新记录”、当前页、翻页组件和“关闭”。点击刷新后按钮显示
|
||||
“刷新中…”并暂时禁用;通过普通 GET 保留当前历史页码、订单号和日期范围,页面
|
||||
返回后自动重开弹窗。本阶段不自动轮询,避免阅读历史记录时内容突然移动。
|
||||
|
||||
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
|
||||
|
||||
### 6.2 表格列
|
||||
|
||||
Reference in New Issue
Block a user