package imei import ( "context" "fmt" "regexp" "strings" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/os/gtime" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do" ) // imeiValuePattern bounds what may be written into t_sim_card_info.imei. // // The column is varchar(32) and the carrier value is digits only (15-17 for a // real IMEI). This pattern is the primary defence for the CASE WHEN statement // below, which is assembled as SQL text and therefore cannot rely on bound // parameters for the value itself. // // The separator is intentionally absent: this service never concatenates // multiple IMEIs (see the handler's multi-value note), so allowing ';' would // widen the column contract for a case that cannot occur. var imeiValuePattern = regexp.MustCompile(`^[0-9]{1,32}$`) // maxImeiSyncBatch bounds one ApplyBatch statement. The caller (the ZSet // consumer) claims in batches of claimBatch (default 100), so this is a guard // rather than a working limit: it keeps a single UPDATE statement from growing // without bound if a future caller raises the claim size. const maxImeiSyncBatch = 1000 // Result is the database projection of one IMEI sync outcome. // // Two update kinds are represented by one struct: // // - Synced: the carrier answered successfully (Code == 0). imei_sync_time is // written because the card was covered by this polling pass. // - ValueChanged: the carrier returned a non-empty IMEI that differs from the // stored value. Only then are imei and imei_update_time written. // // A Synced-but-empty result (carrier answered, no IMEI available) is therefore // a heartbeat-only update: it records coverage without touching the value. That // asymmetry is what makes the whole chain "additive": an upstream outage that // returns empty values can never clear a stored IMEI. type Result struct { CardID int64 Synced bool ValueChanged bool Imei string } type Repository struct{} func NewRepository() *Repository { return &Repository{} } // ApplyBatch writes a batch of IMEI sync results in as few statements as // possible. // // Two mutually exclusive groups are emitted, because they update different // column sets and mixing them in one CASE expression would force every card to // write every column: // // 1. heartbeat only (synced, value unchanged or empty) → imei_sync_time // 2. value changed → imei + imei_update_time + imei_sync_time // // A statement is only issued when its group is non-empty, so an all-empty // upstream pass still records coverage (group 1) without writing any value. func (r *Repository) ApplyBatch(ctx context.Context, results []Result) error { if len(results) == 0 { return nil } if len(results) > maxImeiSyncBatch { return fmt.Errorf("imei sync batch too large: %d > %d", len(results), maxImeiSyncBatch) } columns := dao.TSimCardInfo.Columns() heartbeatIDs := make([]int64, 0, len(results)) changedIDs := make([]int64, 0, len(results)) var changedCase strings.Builder changedCase.WriteString("CASE " + columns.Id + " ") for _, result := range results { if result.CardID <= 0 { return fmt.Errorf("imei result has invalid cardId %d", result.CardID) } if !result.Synced { // Only successfully synchronized cards get a heartbeat; a failed // query says nothing about coverage. continue } if !result.ValueChanged { heartbeatIDs = append(heartbeatIDs, result.CardID) continue } if !imeiValuePattern.MatchString(result.Imei) { // Reject the whole batch rather than write a value that violates // the column contract. The caller logs the message; the card is // retried in a later round. return fmt.Errorf("imei result for card %d has invalid value %q", result.CardID, result.Imei) } fmt.Fprintf(&changedCase, "WHEN %d THEN '%s' ", result.CardID, result.Imei) changedIDs = append(changedIDs, result.CardID) } now := gtime.Now() // Value changes first: if the second statement fails, the value and its // change timestamp are already consistent, and the heartbeat is only a // coverage marker that the next pass will re-write anyway. if len(changedIDs) > 0 { changedCase.WriteString("END") if _, err := dao.TSimCardInfo.Ctx(ctx). WhereIn(columns.Id, changedIDs). Data(do.TSimCardInfo{ Imei: gdb.Raw(changedCase.String()), ImeiUpdateTime: now, ImeiSyncTime: now, }).Update(); err != nil { return err } } if len(heartbeatIDs) > 0 { if _, err := dao.TSimCardInfo.Ctx(ctx). WhereIn(columns.Id, heartbeatIDs). Data(do.TSimCardInfo{ ImeiSyncTime: now, }).Update(); err != nil { return err } } return nil }