feat: 档口入库码后台批量回写 (#246)
This commit is contained in:
@@ -2,7 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -30,51 +32,110 @@ type InnerCodeApplyResult struct {
|
||||
NeedsCheck int
|
||||
}
|
||||
|
||||
// InnerCodeApplyBatch 是一次已经落库的后台回写请求。
|
||||
type InnerCodeApplyBatch struct {
|
||||
ID string
|
||||
Count int
|
||||
}
|
||||
|
||||
const innerCodeApplyChunkSize = 20
|
||||
|
||||
type innerCodeApplyOutcome struct {
|
||||
Status model.InnerCodeStatus
|
||||
Message string
|
||||
RemoteCode string
|
||||
}
|
||||
|
||||
// ApplyInnerCodes 逐条原子领取并回写。每条写请求只发送一次,单条失败不阻断后续行。
|
||||
func ApplyInnerCodes(ctx context.Context, db *sql.DB, writer InnerCodeWriter, ids []int64, actorUserID string) (*InnerCodeApplyResult, error) {
|
||||
// QueueInnerCodeApplyBatch 把任意数量的已选记录原子排队,远端请求由后台执行器处理。
|
||||
func QueueInnerCodeApplyBatch(db *sql.DB, ids []int64, actorUserID string) (*InnerCodeApplyBatch, error) {
|
||||
ids = uniquePositiveInnerCodeIDs(ids)
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("没有选择可回写记录")
|
||||
}
|
||||
if len(ids) > InnerCodeApplyBatchLimit {
|
||||
return nil, fmt.Errorf("单次最多回写 %d 条记录", InnerCodeApplyBatchLimit)
|
||||
batchID, err := newInnerCodeApplyBatchID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &InnerCodeApplyResult{Requested: len(ids)}
|
||||
for _, id := range ids {
|
||||
now := model.NowISO()
|
||||
record, claimed, err := repository.ClaimInnerCodeForApply(db, id, actorUserID, now)
|
||||
count, err := repository.QueueInnerCodeApplyBatch(db, ids, batchID, actorUserID, model.NowISO())
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrInnerCodeApplyConflict) {
|
||||
return nil, fmt.Errorf("所选记录中有记录已不再可回写;本批没有部分入队,请刷新后重新选择")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &InnerCodeApplyBatch{ID: batchID, Count: count}, nil
|
||||
}
|
||||
|
||||
// RunInnerCodeApplyBatch 分组读取后台队列,再逐条执行原有安全回写门禁。
|
||||
// 分组大小只用于控制一次数据库读取,远端请求始终逐条发送且不会自动重试。
|
||||
func RunInnerCodeApplyBatch(ctx context.Context, db *sql.DB, writer InnerCodeWriter, batchID, actorUserID string) (*InnerCodeApplyResult, error) {
|
||||
result := &InnerCodeApplyResult{}
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
ids, err := repository.ListQueuedInnerCodeIDs(db, batchID, innerCodeApplyChunkSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return result, err
|
||||
}
|
||||
if !claimed {
|
||||
result.Skipped++
|
||||
continue
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
outcome := applyClaimedInnerCode(ctx, writer, *record)
|
||||
finishedAt := model.NowISO()
|
||||
if err := repository.FinishInnerCodeApply(db, id, outcome.Status, compactInnerCodeMessage(outcome.Message), outcome.RemoteCode, finishedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch outcome.Status {
|
||||
case model.InnerCodeUpdated:
|
||||
result.Updated++
|
||||
case model.InnerCodeAlreadyFilled:
|
||||
result.AlreadyFilled++
|
||||
case model.InnerCodeNeedsCheck:
|
||||
result.NeedsCheck++
|
||||
case model.InnerCodeFailed:
|
||||
result.Failed++
|
||||
default:
|
||||
result.Skipped++
|
||||
result.Requested += len(ids)
|
||||
for _, id := range ids {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
now := model.NowISO()
|
||||
record, claimed, err := repository.ClaimQueuedInnerCodeForApply(db, id, batchID, actorUserID, now)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if !claimed {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
outcome := applyClaimedInnerCode(ctx, writer, *record)
|
||||
finishedAt := model.NowISO()
|
||||
if err := repository.FinishInnerCodeApply(db, id, outcome.Status, compactInnerCodeMessage(outcome.Message), outcome.RemoteCode, finishedAt); err != nil {
|
||||
return result, err
|
||||
}
|
||||
switch outcome.Status {
|
||||
case model.InnerCodeUpdated:
|
||||
result.Updated++
|
||||
case model.InnerCodeAlreadyFilled:
|
||||
result.AlreadyFilled++
|
||||
case model.InnerCodeNeedsCheck:
|
||||
result.NeedsCheck++
|
||||
case model.InnerCodeFailed:
|
||||
result.Failed++
|
||||
default:
|
||||
result.Skipped++
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// InterruptInnerCodeApplyBatch 在后台异常时收敛当前批次,不自动重试任何远端请求。
|
||||
func InterruptInnerCodeApplyBatch(db *sql.DB, batchID string) (int, int, error) {
|
||||
return repository.InterruptInnerCodeApplyBatch(db, strings.TrimSpace(batchID), model.NowISO())
|
||||
}
|
||||
|
||||
// GetInnerCodeApplyBatchProgress 返回页面使用的单表聚合进度。
|
||||
func GetInnerCodeApplyBatchProgress(db *sql.DB, batchID string) (*repository.InnerCodeApplyBatchProgress, error) {
|
||||
batchID = strings.TrimSpace(batchID)
|
||||
if batchID == "" || len(batchID) > 191 {
|
||||
return nil, nil
|
||||
}
|
||||
return repository.GetInnerCodeApplyBatchProgress(db, batchID)
|
||||
}
|
||||
|
||||
func newInnerCodeApplyBatchID() (string, error) {
|
||||
random := make([]byte, 8)
|
||||
if _, err := rand.Read(random); err != nil {
|
||||
return "", fmt.Errorf("生成档口入库码后台批次失败: %w", err)
|
||||
}
|
||||
return "ICB-" + time.Now().UTC().Format("20060102T150405") + "-" + hex.EncodeToString(random), nil
|
||||
}
|
||||
|
||||
func applyClaimedInnerCode(ctx context.Context, writer InnerCodeWriter, record model.InnerCodeRecord) innerCodeApplyOutcome {
|
||||
|
||||
Reference in New Issue
Block a user