feat: 优化顺运宝同步反馈与记录刷新 (#61)
This commit is contained in:
+20
-14
@@ -67,12 +67,13 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncStatus := service.GetSybSyncStatus()
|
||||||
status := msg
|
status := msg
|
||||||
if status == "" {
|
if status == "" {
|
||||||
status = sybStatusLine(result)
|
status = sybStatusLine(result)
|
||||||
}
|
}
|
||||||
if service.GetSybSyncStatus().Running {
|
if syncStatus.Running && (msg == "" || strings.Contains(msg, "同步已开始")) {
|
||||||
status = "同步进行中,请稍后刷新页面查看结果 · " + status
|
status = "同步正在进行,请耐心等待;可打开同步记录刷新查看结果"
|
||||||
}
|
}
|
||||||
|
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
@@ -152,8 +153,11 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
|
|||||||
"SyncDateTo": dateTo,
|
"SyncDateTo": dateTo,
|
||||||
"RangeError": c.Query("range_error"),
|
"RangeError": c.Query("range_error"),
|
||||||
"RangeWarning": rangeWarning,
|
"RangeWarning": rangeWarning,
|
||||||
|
"SyncRunning": syncStatus.Running,
|
||||||
"SyncHistory": history,
|
"SyncHistory": history,
|
||||||
"NeedSyncHistory": c.Query("history") == "1",
|
"NeedSyncHistory": c.Query("history") == "1",
|
||||||
|
"HistoryRefreshURL": sybHistoryURL(history.Page,
|
||||||
|
keyword, dateFrom, dateTo),
|
||||||
"HistoryPagination": sybHistoryPagination(history.Page, history.TotalPages,
|
"HistoryPagination": sybHistoryPagination(history.Page, history.TotalPages,
|
||||||
keyword, dateFrom, dateTo),
|
keyword, dateFrom, dateTo),
|
||||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
"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 {
|
func sybHistoryPagination(pageNum, totalPages int, keyword, dateFrom, dateTo string) service.PaginationView {
|
||||||
link := func(target int) string {
|
link := func(target int) string { return sybHistoryURL(target, keyword, dateFrom, dateTo) }
|
||||||
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()
|
|
||||||
}
|
|
||||||
view := service.PaginationView{
|
view := service.PaginationView{
|
||||||
Page: pageNum, TotalPages: totalPages, HasPrev: pageNum > 1, HasNext: pageNum < totalPages,
|
Page: pageNum, TotalPages: totalPages, HasPrev: pageNum > 1, HasNext: pageNum < totalPages,
|
||||||
FirstURL: link(1), LastURL: link(totalPages),
|
FirstURL: link(1), LastURL: link(totalPages),
|
||||||
@@ -183,6 +179,16 @@ func sybHistoryPagination(pageNum, totalPages int, keyword, dateFrom, dateTo str
|
|||||||
return view
|
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 覆盖时)。
|
// sybStatusLine 组装底部状态条的默认文案(没有 msg 覆盖时)。
|
||||||
// `[必须]` 显示筛选后的**全量**总数,不是本页行数,见工单 #43 定下的规则。
|
// `[必须]` 显示筛选后的**全量**总数,不是本页行数,见工单 #43 定下的规则。
|
||||||
func sybStatusLine(result *service.SybListResult) string {
|
func sybStatusLine(result *service.SybListResult) string {
|
||||||
@@ -238,7 +244,7 @@ func (h *Handler) SybSync(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !started {
|
if !started {
|
||||||
h.sybRedirect(c, "验证码自动识别成功,已自动登录;但已经有一个同步任务在跑,请稍后刷新页面查看结果")
|
h.sybRedirect(c, "同步正在进行,请耐心等待,请勿重复点击")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.sybRedirectAfterStart(c, "验证码自动识别成功,已自动登录,同步已开始,请稍后刷新页面查看结果")
|
h.sybRedirectAfterStart(c, "验证码自动识别成功,已自动登录,同步已开始,请稍后刷新页面查看结果")
|
||||||
@@ -251,7 +257,7 @@ func (h *Handler) SybSync(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !started {
|
if !started {
|
||||||
h.sybRedirect(c, "已经有一个同步任务在跑,请稍后刷新页面查看结果,不要重复点击")
|
h.sybRedirect(c, "同步正在进行,请耐心等待,请勿重复点击")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.sybRedirectAfterStart(c, "同步已开始,请稍后刷新页面查看结果")
|
h.sybRedirectAfterStart(c, "同步已开始,请稍后刷新页面查看结果")
|
||||||
@@ -363,7 +369,7 @@ func (h *Handler) SybLoginAndSync(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !started {
|
if !started {
|
||||||
h.sybRedirect(c, "登录成功,但已经有一个同步任务在跑,请稍后刷新页面查看结果")
|
h.sybRedirect(c, "同步正在进行,请耐心等待,请勿重复点击")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.sybRedirectAfterStart(c, "登录成功,同步已开始,请稍后刷新页面查看结果")
|
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()
|
sybResponse := httptest.NewRecorder()
|
||||||
router.ServeHTTP(sybResponse, sybRequest)
|
router.ServeHTTP(sybResponse, sybRequest)
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"同步记录", `name="date_from"`, `name="date_to"`,
|
"同步记录", "刷新记录", `data-history-refresh-url=`,
|
||||||
|
`name="date_from"`, `name="date_to"`,
|
||||||
"按顺运宝货运单创建日期(UTC+8)同步",
|
"按顺运宝货运单创建日期(UTC+8)同步",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(sybResponse.Body.String(), want) {
|
if !strings.Contains(sybResponse.Body.String(), want) {
|
||||||
@@ -78,6 +79,21 @@ func TestMainPagesReturnOK(t *testing.T) {
|
|||||||
t.Error("顺运宝页面不应再显示旧的指定日期同步入口")
|
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(
|
pdd, err := repository.EnsurePddProduct(
|
||||||
db, "737116531267",
|
db, "737116531267",
|
||||||
"https://mobile.yangkeduo.com/goods.html?goods_id=737116531267")
|
"https://mobile.yangkeduo.com/goods.html?goods_id=737116531267")
|
||||||
|
|||||||
@@ -288,7 +288,8 @@ tr.empty small { color: #aaa; }
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
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 {
|
.sync-status {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 52px;
|
min-width: 52px;
|
||||||
|
|||||||
@@ -251,6 +251,34 @@
|
|||||||
if (btn) btn.addEventListener("click", refresh);
|
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.addEventListener("DOMContentLoaded", function () {
|
||||||
document.querySelectorAll("table").forEach(setupCheckAll);
|
document.querySelectorAll("table").forEach(setupCheckAll);
|
||||||
setupConfirmDelete();
|
setupConfirmDelete();
|
||||||
@@ -260,6 +288,8 @@
|
|||||||
setupModals();
|
setupModals();
|
||||||
setupRowDetail();
|
setupRowDetail();
|
||||||
setupCaptchaRefresh();
|
setupCaptchaRefresh();
|
||||||
|
setupSybSyncFeedback();
|
||||||
|
setupSybHistoryRefresh();
|
||||||
syncButtons();
|
syncButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
|
||||||
<div class="toolbar">
|
<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="csrf_token" value="{{.CSRFToken}}">
|
||||||
<input type="hidden" name="order_no" value="{{.Keyword}}">
|
<input type="hidden" name="order_no" value="{{.Keyword}}">
|
||||||
<label for="sync-date-from">开始日期</label>
|
<label for="sync-date-from">开始日期</label>
|
||||||
@@ -11,7 +11,10 @@
|
|||||||
<label for="sync-date-to">结束日期</label>
|
<label for="sync-date-to">结束日期</label>
|
||||||
<input id="sync-date-to" type="date" name="date_to"
|
<input id="sync-date-to" type="date" name="date_to"
|
||||||
value="{{.SyncDateTo}}" max="{{.Today}}" required>
|
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>
|
</form>
|
||||||
|
|
||||||
<button type="button" data-modal-open="sync-history-modal">同步记录</button>
|
<button type="button" data-modal-open="sync-history-modal">同步记录</button>
|
||||||
@@ -79,8 +82,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot sync-history-foot">
|
<div class="modal-foot sync-history-foot">
|
||||||
<span>第 {{.SyncHistory.Page}} / {{.SyncHistory.TotalPages}} 页</span>
|
|
||||||
<span class="grow"></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}}
|
{{if .HistoryPagination.HasPrev}}
|
||||||
<a class="button-link" href="{{.HistoryPagination.FirstURL}}">首页</a>
|
<a class="button-link" href="{{.HistoryPagination.FirstURL}}">首页</a>
|
||||||
<a class="button-link" href="{{.HistoryPagination.PrevURL}}">上一页</a>
|
<a class="button-link" href="{{.HistoryPagination.PrevURL}}">上一页</a>
|
||||||
|
|||||||
@@ -441,7 +441,9 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
|||||||
```
|
```
|
||||||
|
|
||||||
有跳过/失败会在后面列出具体原因,不是只给个数字。同步中再次点「同步」会提示
|
有跳过/失败会在后面列出具体原因,不是只给个数字。同步中再次点「同步」会提示
|
||||||
"已经有一个同步任务在跑",不会并发跑两个。
|
“同步正在进行,请耐心等待,请勿重复点击”,不会并发跑两个。表单有效提交后,
|
||||||
|
按钮立即变成禁用的“同步中…”;页面检测到当前进程仍在同步时也保持这个状态。
|
||||||
|
同步成功启动使用状态条反馈,不弹出需要操作员手工关闭的成功确认框。
|
||||||
|
|
||||||
- 两端都必填,结束日期不得晚于 UTC+8 下的今天,闭区间最多 31 天。
|
- 两端都必填,结束日期不得晚于 UTC+8 下的今天,闭区间最多 31 天。
|
||||||
- 日期范围经过自动 OCR 或手工验证码登录后必须原样保留。
|
- 日期范围经过自动 OCR 或手工验证码登录后必须原样保留。
|
||||||
@@ -453,6 +455,10 @@ Go 的 map 是无序的,不靠它定顺序的话,同一个商品每次刷新
|
|||||||
操作账号、开始/完成时间、同步统计、覆盖游标结果和失败原因。状态至少区分同步中、
|
操作账号、开始/完成时间、同步统计、覆盖游标结果和失败原因。状态至少区分同步中、
|
||||||
成功、失败、已中断;不能只靠颜色表达。刷新或重启 Admin 后记录仍可查看。
|
成功、失败、已中断;不能只靠颜色表达。刷新或重启 Admin 后记录仍可查看。
|
||||||
|
|
||||||
|
弹窗底部右侧依次放“刷新记录”、当前页、翻页组件和“关闭”。点击刷新后按钮显示
|
||||||
|
“刷新中…”并暂时禁用;通过普通 GET 保留当前历史页码、订单号和日期范围,页面
|
||||||
|
返回后自动重开弹窗。本阶段不自动轮询,避免阅读历史记录时内容突然移动。
|
||||||
|
|
||||||
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
|
`[必须]` 搜索框宽度见 [§3.1](#31-搜索框宽度)。
|
||||||
|
|
||||||
### 6.2 表格列
|
### 6.2 表格列
|
||||||
|
|||||||
Reference in New Issue
Block a user