package store import ( "database/sql" "errors" "fmt" "strings" "daili/internal/domain" ) // Customer 客户。 type Customer struct { ID int64 Tail string Name string GameName string GameServer string Contact string Tags string Source string Note string CreatedAt string UpdatedAt string // 列表聚合 SubCount int Summary string Level string // ok | warn | today | expired | none } // TagList 拆分标签。 func (c *Customer) TagList() []string { var out []string for _, t := range strings.Split(c.Tags, ",") { t = strings.TrimSpace(t) if t != "" { out = append(out, t) } } return out } const customerCols = `id,tail,name,game_name,game_server,contact,tags,source,note,created_at,updated_at` func scanCustomer(row interface{ Scan(dest ...any) error }) (*Customer, error) { var c Customer err := row.Scan(&c.ID, &c.Tail, &c.Name, &c.GameName, &c.GameServer, &c.Contact, &c.Tags, &c.Source, &c.Note, &c.CreatedAt, &c.UpdatedAt) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, err } return &c, nil } // CreateCustomer 新建客户(尾号+姓名唯一)。 func (db *DB) CreateCustomer(c *Customer) (int64, error) { now := nowStamp() res, err := db.sql.Exec(`INSERT INTO customers(tail,name,game_name,game_server,contact,tags,source,note,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)`, strings.TrimSpace(c.Tail), strings.TrimSpace(c.Name), c.GameName, c.GameServer, c.Contact, c.Tags, c.Source, c.Note, now, now) if err != nil { if isUnique(err) { return 0, ErrDuplicate } return 0, err } return res.LastInsertId() } // UpdateCustomer 更新客户。 func (db *DB) UpdateCustomer(c *Customer) error { _, err := db.sql.Exec(`UPDATE customers SET tail=?,name=?,game_name=?,game_server=?,contact=?,tags=?,source=?,note=?,updated_at=? WHERE id=?`, strings.TrimSpace(c.Tail), strings.TrimSpace(c.Name), c.GameName, c.GameServer, c.Contact, c.Tags, c.Source, c.Note, nowStamp(), c.ID) if err != nil && isUnique(err) { return ErrDuplicate } return err } // Customer 取单个客户。 func (db *DB) Customer(id int64) (*Customer, error) { return scanCustomer(db.sql.QueryRow(`SELECT `+customerCols+` FROM customers WHERE id=?`, id)) } // ListCustomers 分页查询客户,并聚合订阅摘要。 func (db *DB) ListCustomers(q string, limit, offset int, today string, warnDays int) ([]Customer, int, error) { where := "1=1" args := []any{} if q = strings.TrimSpace(q); q != "" { like := "%" + q + "%" where = "(tail LIKE ? OR name LIKE ? OR game_name LIKE ? OR contact LIKE ? OR tags LIKE ?)" args = append(args, like, like, like, like, like) } var total int if err := db.sql.QueryRow(`SELECT COUNT(*) FROM customers WHERE `+where, args...).Scan(&total); err != nil { return nil, 0, err } rows, err := db.sql.Query(`SELECT `+customerCols+` FROM customers WHERE `+where+` ORDER BY id DESC LIMIT ? OFFSET ?`, append(args, limit, offset)...) if err != nil { return nil, 0, err } defer rows.Close() out := []Customer{} ids := []int64{} for rows.Next() { c, err := scanCustomer(rows) if err != nil { return nil, 0, err } out = append(out, *c) ids = append(ids, c.ID) } if err := rows.Err(); err != nil { return nil, 0, err } if len(ids) > 0 { subs, err := db.SubsByCustomers(ids) if err != nil { return nil, 0, err } byCustomer := map[int64][]Subscription{} for _, s := range subs { byCustomer[s.CustomerID] = append(byCustomer[s.CustomerID], s) } for i := range out { list := byCustomer[out[i].ID] out[i].SubCount = len(list) out[i].Summary, out[i].Level = summarizeSubs(list, today, warnDays) } } return out, total, nil } // summarizeSubs 生成“包月日常 剩12天 · 深渊 剩3次”式的摘要与最紧急等级。 func summarizeSubs(subs []Subscription, today string, warnDays int) (string, string) { if len(subs) == 0 { return "暂无订阅", domain.LevelNone } t := domain.ParseDateOr(today, domain.Today()) rank := map[string]int{domain.LevelExpired: 4, domain.LevelToday: 3, domain.LevelWarn: 2, domain.LevelOK: 1, domain.LevelNone: 0} worst := domain.LevelNone parts := []string{} for _, s := range subs { e := s.Eval(t, warnDays) if e.State == domain.StateClosed { continue } var text string if s.Unit == domain.UnitDay { switch { case e.State == domain.StateExpired: text = s.PackageName + " 已过期" case e.DaysLeft == 0: text = s.PackageName + " 今天到期" default: text = fmt.Sprintf("%s 剩%d天", s.PackageName, e.RemainingDays) } } else { switch { case e.RemainingCount <= 0: text = s.PackageName + " 已用尽" case e.State == domain.StateExpired: text = s.PackageName + " 已过期" default: text = fmt.Sprintf("%s 剩%d次", s.PackageName, e.RemainingCount) } } parts = append(parts, text) if rank[e.Level] > rank[worst] { worst = e.Level } } if len(parts) == 0 { return "仅历史订阅", domain.LevelNone } if len(parts) > 2 { parts = append(parts[:2], fmt.Sprintf("等%d项", len(parts))) } return strings.Join(parts, " · "), worst } // FindCustomerByTailName 精确查找(用于查重提示)。 func (db *DB) FindCustomerByTailName(tail, name string) (*Customer, error) { return scanCustomer(db.sql.QueryRow(`SELECT `+customerCols+` FROM customers WHERE tail=? AND name=?`, strings.TrimSpace(tail), strings.TrimSpace(name))) } // CustomerCount 客户总数。 func (db *DB) CustomerCount() (int, error) { var n int err := db.sql.QueryRow(`SELECT COUNT(*) FROM customers`).Scan(&n) return n, err } // CustomersByTail 按手机尾号精确匹配客户;没有精确命中时退回模糊匹配。 func (db *DB) CustomersByTail(tail string) ([]Customer, error) { tail = strings.TrimSpace(tail) if tail == "" { return nil, nil } out, err := db.queryCustomers(`SELECT `+customerCols+` FROM customers WHERE tail = ? ORDER BY id`, tail) if err != nil { return nil, err } if len(out) > 0 { return out, nil } return db.queryCustomers(`SELECT `+customerCols+` FROM customers WHERE tail LIKE ? ORDER BY id LIMIT 20`, "%"+tail+"%") } func (db *DB) queryCustomers(q string, args ...any) ([]Customer, error) { rows, err := db.sql.Query(q, args...) if err != nil { return nil, err } defer rows.Close() out := []Customer{} for rows.Next() { c, err := scanCustomer(rows) if err != nil { return nil, err } out = append(out, *c) } return out, rows.Err() }