feat: 统一顺运宝日期同步并记录历史 (#59)
This commit is contained in:
+97
-19
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -59,6 +60,12 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
|
||||
"读取顺运宝货运单列表失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
|
||||
return
|
||||
}
|
||||
history, err := service.ListSybSyncHistory(h.db, service.ParsePage(c.Query("history_page")))
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError,
|
||||
"读取顺运宝同步记录失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
|
||||
return
|
||||
}
|
||||
|
||||
status := msg
|
||||
if status == "" {
|
||||
@@ -88,15 +95,25 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
|
||||
// 真正会登录的只有「同步」这一个按钮。这与 Client 侧
|
||||
// 「『获取任务』是唯一会产生外部后果的命令」是同一条原则。
|
||||
needLogin := c.Query("login") == "1"
|
||||
today := service.SybToday(time.Now())
|
||||
now := time.Now()
|
||||
today := service.SybToday(now)
|
||||
defaults, err := service.DefaultSybSyncRange(h.db, now)
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError,
|
||||
"计算默认同步日期失败,数据没有被改动。刷新页面重试;一直失败请把这句话报给维护者。")
|
||||
return
|
||||
}
|
||||
dateFrom := c.Query("date_from")
|
||||
dateTo := c.Query("date_to")
|
||||
hasSyncRange := dateFrom != "" || dateTo != ""
|
||||
rangeWarning := ""
|
||||
if dateFrom == "" {
|
||||
dateFrom = today
|
||||
dateFrom = defaults.From
|
||||
}
|
||||
if dateTo == "" {
|
||||
dateTo = today
|
||||
dateTo = defaults.To
|
||||
}
|
||||
if c.Query("date_from") == "" && c.Query("date_to") == "" {
|
||||
rangeWarning = defaults.Warning
|
||||
}
|
||||
|
||||
cfg, cfgErr := config.Load()
|
||||
@@ -133,13 +150,39 @@ func (h *Handler) renderSybListWithLoginReason(c *gin.Context, keyword, pageRaw,
|
||||
"Today": today,
|
||||
"SyncDateFrom": dateFrom,
|
||||
"SyncDateTo": dateTo,
|
||||
"HasSyncRange": hasSyncRange,
|
||||
"NeedRangeSync": c.Query("range_sync") == "1",
|
||||
"RangeError": c.Query("range_error"),
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
||||
"RangeWarning": rangeWarning,
|
||||
"SyncHistory": history,
|
||||
"NeedSyncHistory": c.Query("history") == "1",
|
||||
"HistoryPagination": sybHistoryPagination(history.Page, history.TotalPages,
|
||||
keyword, dateFrom, dateTo),
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
||||
}))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
view := service.PaginationView{
|
||||
Page: pageNum, TotalPages: totalPages, HasPrev: pageNum > 1, HasNext: pageNum < totalPages,
|
||||
FirstURL: link(1), LastURL: link(totalPages),
|
||||
}
|
||||
if view.HasPrev {
|
||||
view.PrevURL = link(pageNum - 1)
|
||||
}
|
||||
if view.HasNext {
|
||||
view.NextURL = link(pageNum + 1)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// sybStatusLine 组装底部状态条的默认文案(没有 msg 覆盖时)。
|
||||
// `[必须]` 显示筛选后的**全量**总数,不是本页行数,见工单 #43 定下的规则。
|
||||
func sybStatusLine(result *service.SybListResult) string {
|
||||
@@ -189,19 +232,29 @@ func (h *Handler) SybSync(c *gin.Context) {
|
||||
h.sybRedirectLogin(c, reason)
|
||||
return
|
||||
}
|
||||
if !h.startSybSync(client, *cfg, options) {
|
||||
started, startErr := h.startSybSync(client, *cfg, options, currentUser(c))
|
||||
if startErr != nil {
|
||||
h.sybRedirect(c, "同步没有启动:"+startErr.Error())
|
||||
return
|
||||
}
|
||||
if !started {
|
||||
h.sybRedirect(c, "验证码自动识别成功,已自动登录;但已经有一个同步任务在跑,请稍后刷新页面查看结果")
|
||||
return
|
||||
}
|
||||
h.sybRedirect(c, "验证码自动识别成功,已自动登录,同步已开始,请稍后刷新页面查看结果")
|
||||
h.sybRedirectAfterStart(c, "验证码自动识别成功,已自动登录,同步已开始,请稍后刷新页面查看结果")
|
||||
return
|
||||
}
|
||||
|
||||
if !h.startSybSync(client, *cfg, options) {
|
||||
started, startErr := h.startSybSync(client, *cfg, options, currentUser(c))
|
||||
if startErr != nil {
|
||||
h.sybRedirect(c, "同步没有启动:"+startErr.Error())
|
||||
return
|
||||
}
|
||||
if !started {
|
||||
h.sybRedirect(c, "已经有一个同步任务在跑,请稍后刷新页面查看结果,不要重复点击")
|
||||
return
|
||||
}
|
||||
h.sybRedirect(c, "同步已开始,请稍后刷新页面查看结果")
|
||||
h.sybRedirectAfterStart(c, "同步已开始,请稍后刷新页面查看结果")
|
||||
}
|
||||
|
||||
// attemptSybAutoLogin 尝试用配置的 OCR 服务自动识别验证码并登录。
|
||||
@@ -304,11 +357,16 @@ func (h *Handler) SybLoginAndSync(c *gin.Context) {
|
||||
log.Printf("syb_session_save_failed username=%s err=%v", result.User.Username, err)
|
||||
}
|
||||
|
||||
if !h.startSybSync(client, *cfg, options) {
|
||||
started, startErr := h.startSybSync(client, *cfg, options, currentUser(c))
|
||||
if startErr != nil {
|
||||
h.sybRedirect(c, "登录成功,但同步没有启动:"+startErr.Error())
|
||||
return
|
||||
}
|
||||
if !started {
|
||||
h.sybRedirect(c, "登录成功,但已经有一个同步任务在跑,请稍后刷新页面查看结果")
|
||||
return
|
||||
}
|
||||
h.sybRedirect(c, "登录成功,同步已开始,请稍后刷新页面查看结果")
|
||||
h.sybRedirectAfterStart(c, "登录成功,同步已开始,请稍后刷新页面查看结果")
|
||||
}
|
||||
|
||||
// startSybSync 尝试拿互斥标志并在后台协程里跑同步;已经在跑时返回 false。
|
||||
@@ -319,16 +377,27 @@ func (h *Handler) SybLoginAndSync(c *gin.Context) {
|
||||
//
|
||||
// `[必须]` 后台协程用 context.Background(),不能用 c.Request.Context()——
|
||||
// 那个请求上下文会在这次 HTTP 请求返回后就被取消,同步跑到一半会被打断。
|
||||
func (h *Handler) startSybSync(client *syb.Client, cfg config.Config, options service.SybSyncOptions) bool {
|
||||
func (h *Handler) startSybSync(client *syb.Client, cfg config.Config, options service.SybSyncOptions, actor *model.User) (bool, error) {
|
||||
if !service.TryStartSybSync() {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
runID, err := service.CreateSybSyncRun(h.db, actor, options, time.Now())
|
||||
if err != nil {
|
||||
service.FinishSybSync(service.SyncReport{Err: err, FinishedAt: time.Now().UTC()})
|
||||
return false, err
|
||||
}
|
||||
db := h.db
|
||||
go func() {
|
||||
report := service.RunSybSyncWithOptions(context.Background(), db, client, cfg.Syb, time.Now(), options)
|
||||
if err := service.FinishSybSyncRun(db, runID, report); err != nil {
|
||||
log.Printf("syb_sync_run_finish_failed run_id=%s err=%v", runID, err)
|
||||
if report.Err == nil {
|
||||
report.Err = fmt.Errorf("同步数据已处理,但保存同步记录失败,请联系维护者核对:%w", err)
|
||||
}
|
||||
}
|
||||
service.FinishSybSync(report)
|
||||
}()
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// sybRedirect 处理完写操作后跳回列表页,带上当前筛选和提示信息。
|
||||
@@ -346,6 +415,16 @@ func (h *Handler) sybRedirect(c *gin.Context, msg string) {
|
||||
c.Redirect(http.StatusSeeOther, target)
|
||||
}
|
||||
|
||||
// sybRedirectAfterStart 不保留本次日期。后台任务结束后刷新干净的列表 URL,
|
||||
// 页面就会按新的覆盖游标给出下一段默认范围,避免误点后重复上一段。
|
||||
func (h *Handler) sybRedirectAfterStart(c *gin.Context, msg string) {
|
||||
params := url.Values{"msg": {msg}}
|
||||
if keyword := strings.TrimSpace(c.PostForm("order_no")); keyword != "" {
|
||||
params.Set("order_no", keyword)
|
||||
}
|
||||
c.Redirect(http.StatusSeeOther, "/syb?"+params.Encode())
|
||||
}
|
||||
|
||||
// sybRedirectLogin 同 sybRedirect,但把 reason 放进 login_reason 参数,
|
||||
// 渲染时会显示在**登录弹窗里面**(工单 #47 要求:自动识别失败/降级要
|
||||
// 说明原因,不能弹一个不知道为什么弹出来的空表单)。reason 为空时
|
||||
@@ -364,12 +443,11 @@ func (h *Handler) sybRedirectLogin(c *gin.Context, reason string) {
|
||||
c.Redirect(http.StatusSeeOther, target)
|
||||
}
|
||||
|
||||
// sybRedirectRange 把服务端日期校验错误带回指定日期弹窗。输入值一并保留,
|
||||
// 操作员只需改错的那一端,不必重新填整张表单。
|
||||
// sybRedirectRange 把服务端日期校验错误带回工具栏日期表单。输入值一并保留,
|
||||
// 操作员只需改错的那一端,不必重新填写。
|
||||
func (h *Handler) sybRedirectRange(c *gin.Context, message string) {
|
||||
params := url.Values{}
|
||||
appendSybFormState(params, c)
|
||||
params.Set("range_sync", "1")
|
||||
params.Set("range_error", message)
|
||||
c.Redirect(http.StatusSeeOther, "/syb?"+params.Encode())
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestSybRedirectLogin_保留指定日期并打开登录弹窗(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSybRedirectRange_保留输入并重新打开日期弹窗(t *testing.T) {
|
||||
func TestSybRedirectRange_保留输入并显示日期错误(t *testing.T) {
|
||||
context, recorder := sybPostContext(t, url.Values{
|
||||
"date_from": {"2026-08-09"},
|
||||
"date_to": {"2026-08-01"},
|
||||
@@ -59,10 +59,43 @@ func TestSybRedirectRange_保留输入并重新打开日期弹窗(t *testing.T)
|
||||
t.Fatalf("解析 Location 失败: %v", err)
|
||||
}
|
||||
query := location.Query()
|
||||
if query.Get("range_sync") != "1" || query.Get("range_error") == "" {
|
||||
t.Fatalf("日期错误后应该重开弹窗并显示错误,Location=%s", location.String())
|
||||
if query.Get("range_error") == "" {
|
||||
t.Fatalf("日期错误后应该显示错误,Location=%s", location.String())
|
||||
}
|
||||
if query.Get("date_from") != "2026-08-09" || query.Get("date_to") != "2026-08-01" {
|
||||
t.Fatalf("日期错误后应该保留两端输入,Location=%s", location.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSybRedirectAfterStart_清除本次日期范围(t *testing.T) {
|
||||
context, recorder := sybPostContext(t, url.Values{
|
||||
"order_no": {"ORDER-1"}, "date_from": {"2026-08-01"}, "date_to": {"2026-08-09"},
|
||||
})
|
||||
(&Handler{}).sybRedirectAfterStart(context, "同步已开始")
|
||||
location, err := url.Parse(recorder.Header().Get("Location"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
query := location.Query()
|
||||
if query.Get("order_no") != "ORDER-1" || query.Get("msg") != "同步已开始" {
|
||||
t.Fatalf("跳转应保留搜索和提示: %s", location.String())
|
||||
}
|
||||
if query.Has("date_from") || query.Has("date_to") {
|
||||
t.Fatalf("启动后不应保留旧日期,否则下一段会误重复: %s", location.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSybHistoryPagination_保留筛选并重开弹窗(t *testing.T) {
|
||||
view := sybHistoryPagination(2, 3, "ORDER-1", "2026-08-01", "2026-08-09")
|
||||
for _, raw := range []string{view.FirstURL, view.PrevURL, view.NextURL, view.LastURL} {
|
||||
location, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
query := location.Query()
|
||||
if query.Get("history") != "1" || query.Get("order_no") != "ORDER-1" ||
|
||||
query.Get("date_from") != "2026-08-01" || query.Get("date_to") != "2026-08-09" {
|
||||
t.Fatalf("同步记录翻页链接丢失页面状态: %s", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user