fix: 档口入库码按货运单号直查顺运宝 (#245)
This commit is contained in:
+117
-16
@@ -464,10 +464,8 @@ func (c *Client) CheckSession(ctx context.Context, userID int64, username string
|
||||
|
||||
// ---------- 货运单列表 ----------
|
||||
|
||||
// listPayload 组装 /am/stock/listTotal、/am/stock/list 共用的请求体,
|
||||
// 见 08 §4.1/§4.2:按日期范围查询(同步用这个),dvalue 是
|
||||
// "起始日期,结束日期",YYYY-MM-DD,逗号分隔。
|
||||
func listPayload(dateFrom, dateTo string, start, pageIndex, pageSize int) map[string]any {
|
||||
// stockListPayload 组装 /am/stock/listTotal、/am/stock/list 共用的请求体。
|
||||
func stockListPayload(query map[string]any, start, pageIndex, pageSize int) map[string]any {
|
||||
return map[string]any{
|
||||
"history": 0,
|
||||
"length": pageSize,
|
||||
@@ -476,20 +474,38 @@ func listPayload(dateFrom, dateTo string, start, pageIndex, pageSize int) map[st
|
||||
"pageIndex": pageIndex,
|
||||
"store": false,
|
||||
"columns": columnsPayload(),
|
||||
"queries": []map[string]any{
|
||||
{
|
||||
"dvalue": dateFrom + "," + dateTo,
|
||||
"tableName": "t_stock",
|
||||
"colName": "created",
|
||||
"op": 0,
|
||||
"type": 3,
|
||||
"tableAlias": "t",
|
||||
"optType": 0,
|
||||
},
|
||||
},
|
||||
"queries": []map[string]any{query},
|
||||
}
|
||||
}
|
||||
|
||||
// listPayload 组装同步使用的日期范围条件,dvalue 是
|
||||
// "起始日期,结束日期",YYYY-MM-DD,逗号分隔。
|
||||
func listPayload(dateFrom, dateTo string, start, pageIndex, pageSize int) map[string]any {
|
||||
return stockListPayload(map[string]any{
|
||||
"dvalue": dateFrom + "," + dateTo,
|
||||
"tableName": "t_stock",
|
||||
"colName": "created",
|
||||
"op": 0,
|
||||
"type": 3,
|
||||
"tableAlias": "t",
|
||||
"optType": 0,
|
||||
}, start, pageIndex, pageSize)
|
||||
}
|
||||
|
||||
// orderNumberListPayload 使用顺运宝页面“全部单号”的原始 HAR 条件。
|
||||
// allcode 会搜索多类单号,因此调用方还必须核对返回行的 code 与输入完全一致。
|
||||
func orderNumberListPayload(orderNumber string, start, pageIndex, pageSize int) map[string]any {
|
||||
return stockListPayload(map[string]any{
|
||||
"dvalue": orderNumber,
|
||||
"tableName": "t_stock",
|
||||
"colName": "allcode",
|
||||
"op": 6,
|
||||
"type": 0,
|
||||
"tableAlias": "t",
|
||||
"optType": 1,
|
||||
}, start, pageIndex, pageSize)
|
||||
}
|
||||
|
||||
// ListTotal 查某个日期范围内的货运单总数:POST /am/stock/listTotal。
|
||||
// `[必须]` data 是裸整数,不是对象,见 08 §2。
|
||||
func (c *Client) ListTotal(ctx context.Context, dateFrom, dateTo string, pageSize int) (int, error) {
|
||||
@@ -498,10 +514,17 @@ func (c *Client) ListTotal(ctx context.Context, dateFrom, dateTo string, pageSiz
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return parseListTotal(data)
|
||||
}
|
||||
|
||||
func parseListTotal(data json.RawMessage) (int, error) {
|
||||
var total int
|
||||
if err := json.Unmarshal(data, &total); err != nil {
|
||||
return 0, fmt.Errorf("listTotal 返回的总数格式错误: %s", string(data))
|
||||
}
|
||||
if total < 0 {
|
||||
return 0, fmt.Errorf("listTotal 返回了负数: %d", total)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
@@ -516,8 +539,12 @@ type StockRow struct {
|
||||
// ListPage 按日期范围翻一页货运单列表:POST /am/stock/list。
|
||||
// 返回响应内的 total;HAR 证明它是当前页条数,不是筛选范围总数。
|
||||
func (c *Client) ListPage(ctx context.Context, dateFrom, dateTo string, start, pageIndex, pageSize int) ([]StockRow, int, error) {
|
||||
return c.listPage(ctx, listPayload(dateFrom, dateTo, start, pageIndex, pageSize))
|
||||
}
|
||||
|
||||
func (c *Client) listPage(ctx context.Context, payload map[string]any) ([]StockRow, int, error) {
|
||||
data, err := c.do(ctx, http.MethodPost, "/am/stock/list", nil,
|
||||
listPayload(dateFrom, dateTo, start, pageIndex, pageSize))
|
||||
payload)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -548,6 +575,80 @@ func (c *Client) ListPage(ctx context.Context, dateFrom, dateTo string, start, p
|
||||
return rows, *wrap.Total, nil
|
||||
}
|
||||
|
||||
const (
|
||||
orderNumberPageSize = 20
|
||||
orderNumberMaxMatches = 100
|
||||
orderNumberMaxLength = 128
|
||||
)
|
||||
|
||||
// ListByOrderNumber 使用顺运宝页面“全部单号”条件直接查询货运单。
|
||||
// 返回值只包含 code 与输入完全相同的货运单;服务端返回不相关 code、重复 ID
|
||||
// 或不完整分页时按协议错误处理,避免后续把档口入库码写到错误货运单。
|
||||
func (c *Client) ListByOrderNumber(ctx context.Context, orderNumber string) ([]StockRow, error) {
|
||||
orderNumber = strings.TrimSpace(orderNumber)
|
||||
if orderNumber == "" {
|
||||
return nil, fmt.Errorf("货运单号不能为空")
|
||||
}
|
||||
if utf8.RuneCountInString(orderNumber) > orderNumberMaxLength {
|
||||
return nil, fmt.Errorf("货运单号不能超过 %d 个字符", orderNumberMaxLength)
|
||||
}
|
||||
for _, character := range orderNumber {
|
||||
if character < 32 || character == 127 {
|
||||
return nil, fmt.Errorf("货运单号不能包含控制字符")
|
||||
}
|
||||
}
|
||||
|
||||
data, err := c.do(ctx, http.MethodPost, "/am/stock/listTotal", nil,
|
||||
orderNumberListPayload(orderNumber, 0, 1, orderNumberPageSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total, err := parseListTotal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if total == 0 {
|
||||
return []StockRow{}, nil
|
||||
}
|
||||
if total > orderNumberMaxMatches {
|
||||
return nil, fmt.Errorf("货运单号 %q 查询返回 %d 条,超过安全上限 %d",
|
||||
orderNumber, total, orderNumberMaxMatches)
|
||||
}
|
||||
|
||||
rows := make([]StockRow, 0, total)
|
||||
seenIDs := make(map[int64]bool, total)
|
||||
for start := 0; start < total; start += orderNumberPageSize {
|
||||
pageIndex := start/orderNumberPageSize + 1
|
||||
pageRows, pageCount, pageErr := c.listPage(ctx,
|
||||
orderNumberListPayload(orderNumber, start, pageIndex, orderNumberPageSize))
|
||||
if pageErr != nil {
|
||||
return nil, pageErr
|
||||
}
|
||||
expected := orderNumberPageSize
|
||||
if remaining := total - start; remaining < expected {
|
||||
expected = remaining
|
||||
}
|
||||
if pageCount != expected {
|
||||
return nil, fmt.Errorf("货运单号 %q 第 %d 页不完整:预期 %d 行,实际 %d 行",
|
||||
orderNumber, pageIndex, expected, pageCount)
|
||||
}
|
||||
for _, row := range pageRows {
|
||||
if row.Code != orderNumber {
|
||||
return nil, fmt.Errorf("货运单号 %q 查询返回了不一致的 code %q", orderNumber, row.Code)
|
||||
}
|
||||
if seenIDs[row.ID] {
|
||||
return nil, fmt.Errorf("货运单号 %q 查询重复返回货运单 id=%d", orderNumber, row.ID)
|
||||
}
|
||||
seenIDs[row.ID] = true
|
||||
rows = append(rows, row)
|
||||
}
|
||||
}
|
||||
if len(rows) != total {
|
||||
return nil, fmt.Errorf("货运单号 %q 查询总数为 %d,实际读取 %d 行", orderNumber, total, len(rows))
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// ---------- 货运单明细 ----------
|
||||
|
||||
// DetailItem 是货运单明细里的一个商品(t_stock.details[] 的一项),
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -334,6 +335,143 @@ func TestClient_ListTotal和ListPage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_ListByOrderNumber_使用Allcode精确查询(t *testing.T) {
|
||||
const orderNumber = "260812711E49CU"
|
||||
var paths []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
paths = append(paths, r.URL.Path)
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("解析请求体失败: %v", err)
|
||||
}
|
||||
queries, _ := body["queries"].([]any)
|
||||
if len(queries) != 1 {
|
||||
t.Fatalf("queries=%v", body["queries"])
|
||||
}
|
||||
query, _ := queries[0].(map[string]any)
|
||||
if query["dvalue"] != orderNumber || query["tableName"] != "t_stock" ||
|
||||
query["colName"] != "allcode" || query["op"] != float64(6) ||
|
||||
query["type"] != float64(0) || query["tableAlias"] != "t" || query["optType"] != float64(1) {
|
||||
t.Fatalf("allcode 查询条件不正确: %v", query)
|
||||
}
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/am/stock/listTotal":
|
||||
w.Write(envelopeBody(t, true, "ok", 1, nil))
|
||||
case "/am/stock/list":
|
||||
w.Write(envelopeBody(t, true, "ok", map[string]any{
|
||||
"total": 1,
|
||||
"list": []map[string]any{{"id": 76000001, "code": orderNumber}},
|
||||
}, nil))
|
||||
default:
|
||||
t.Fatalf("意外路径: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, _ := New(srv.URL)
|
||||
rows, err := client.ListByOrderNumber(context.Background(), " "+orderNumber+" ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].ID != 76000001 || rows[0].Code != orderNumber {
|
||||
t.Fatalf("货运单查询结果不正确: %+v", rows)
|
||||
}
|
||||
if !reflect.DeepEqual(paths, []string{"/am/stock/listTotal", "/am/stock/list"}) {
|
||||
t.Fatalf("请求路径不正确: %v", paths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_ListByOrderNumber_按20条分页读取(t *testing.T) {
|
||||
const orderNumber = "ORDER-21"
|
||||
listRequests := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body map[string]any
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
if r.URL.Path == "/am/stock/listTotal" {
|
||||
w.Write(envelopeBody(t, true, "ok", 21, nil))
|
||||
return
|
||||
}
|
||||
listRequests++
|
||||
start := int(body["start"].(float64))
|
||||
count := 20
|
||||
if start == 20 {
|
||||
count = 1
|
||||
}
|
||||
rows := make([]map[string]any, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
rows = append(rows, map[string]any{"id": start + index + 1, "code": orderNumber})
|
||||
}
|
||||
w.Write(envelopeBody(t, true, "ok", map[string]any{"total": count, "list": rows}, nil))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, _ := New(srv.URL)
|
||||
rows, err := client.ListByOrderNumber(context.Background(), orderNumber)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 21 || listRequests != 2 || rows[20].ID != 21 {
|
||||
t.Fatalf("分页结果不正确 rows=%d requests=%d last=%+v", len(rows), listRequests, rows[len(rows)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_ListByOrderNumber_拒绝不一致Code和不完整页(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
total int
|
||||
pageTotal int
|
||||
rows []map[string]any
|
||||
}{
|
||||
{"code不一致", 1, 1, []map[string]any{{"id": 1, "code": "OTHER"}}},
|
||||
{"分页不完整", 2, 1, []map[string]any{{"id": 1, "code": "ORDER"}}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/am/stock/listTotal" {
|
||||
w.Write(envelopeBody(t, true, "ok", test.total, nil))
|
||||
return
|
||||
}
|
||||
w.Write(envelopeBody(t, true, "ok", map[string]any{
|
||||
"total": test.pageTotal, "list": test.rows,
|
||||
}, nil))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := New(srv.URL)
|
||||
if _, err := client.ListByOrderNumber(context.Background(), "ORDER"); err == nil {
|
||||
t.Fatal("非法查询结果必须报错")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_ListByOrderNumber_零命中不查列表且限制安全上限(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
total int
|
||||
wantError bool
|
||||
}{
|
||||
{"零命中", 0, false},
|
||||
{"超过安全上限", orderNumberMaxMatches + 1, true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
listRequests := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/am/stock/list" {
|
||||
listRequests++
|
||||
}
|
||||
w.Write(envelopeBody(t, true, "ok", test.total, nil))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := New(srv.URL)
|
||||
rows, err := client.ListByOrderNumber(context.Background(), "ORDER")
|
||||
if (err != nil) != test.wantError || (!test.wantError && len(rows) != 0) || listRequests != 0 {
|
||||
t.Fatalf("rows=%v err=%v listRequests=%d", rows, err, listRequests)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_ListPage要求合法Total(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user