chore: 初始开源发布
iotlcm-onelink-mock:OneLink 挡板服务(本地测试替身)。 - Go module 迁移至 git.5iot.com/iotlcm-public/iotlcm-onelink-mock - 修复同毫秒内多次生成卡导致 ICCID/IMSI 唯一约束冲突 - 补 Apache-2.0 LICENSE,README 重写并新增 docs/deploy.md - 移除内部部署资产(CI 触发器 / 镜像推送脚本 / 指定环境部署文档) - 测试凭据改为 demo_appid / demo_password,真实手机号与内网 IP 脱敏 - 统一 LF 行尾并加 .gitattributes
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StoreManager struct {
|
||||
mu sync.Mutex
|
||||
backend storeBackend
|
||||
data Store
|
||||
initialized bool
|
||||
}
|
||||
|
||||
type storeBackend interface {
|
||||
Load() (Store, error)
|
||||
Save(Store) error
|
||||
}
|
||||
|
||||
type generatedCardBackend interface {
|
||||
AppendCards([]SIMCard) error
|
||||
}
|
||||
|
||||
func NewStoreManager(path string) (*StoreManager, error) {
|
||||
backend, err := newStoreBackend(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sm := &StoreManager{backend: backend}
|
||||
if err := sm.load(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sm, nil
|
||||
}
|
||||
|
||||
func (s *StoreManager) Snapshot() Store {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return cloneStore(s.data)
|
||||
}
|
||||
|
||||
func (s *StoreManager) SaveMutation(fn func(*Store) error) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := fn(&s.data); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
func (s *StoreManager) WithStore(fn func(*Store) error) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return fn(&s.data)
|
||||
}
|
||||
|
||||
func (s *StoreManager) Persist() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
func (s *StoreManager) GenerateCards(count int) (GenerateResult, error) {
|
||||
return s.GenerateCardsInGroup(count, 0)
|
||||
}
|
||||
|
||||
func (s *StoreManager) GenerateCardsInGroup(count int, groupID int64) (GenerateResult, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if count <= 0 {
|
||||
return GenerateResult{Total: len(s.data.Cards)}, nil
|
||||
}
|
||||
generator, ok := s.backend.(generatedCardBackend)
|
||||
if !ok {
|
||||
return GenerateResult{}, fmt.Errorf("current store backend does not support card generation")
|
||||
}
|
||||
if groupID != 0 {
|
||||
found := false
|
||||
for _, group := range s.data.Groups {
|
||||
if group.ID == groupID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return GenerateResult{}, fmt.Errorf("card group not found")
|
||||
}
|
||||
}
|
||||
cards := GenerateSIMCards(time.Now(), count)
|
||||
for i := range cards {
|
||||
cards[i].GroupID = groupID
|
||||
for _, group := range s.data.Groups {
|
||||
if group.ID == groupID {
|
||||
cards[i].GroupName = group.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := generator.AppendCards(cards); err != nil {
|
||||
return GenerateResult{}, err
|
||||
}
|
||||
s.data.Cards = append(s.data.Cards, cards...)
|
||||
return GenerateResult{Generated: len(cards), Total: len(s.data.Cards)}, nil
|
||||
}
|
||||
|
||||
func (s *StoreManager) QueryCards(keyword, exportStatus string, page, pageSize int) CardPage {
|
||||
return s.QueryCardsByGroup(keyword, exportStatus, 0, false, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *StoreManager) QueryCardsByGroup(keyword, exportStatus string, groupID int64, filterGroup bool, page, pageSize int) CardPage {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
matched := filterCards(s.data.Cards, keyword, exportStatus, groupID, filterGroup)
|
||||
start := (page - 1) * pageSize
|
||||
if start > len(matched) {
|
||||
start = len(matched)
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(matched) {
|
||||
end = len(matched)
|
||||
}
|
||||
return CardPage{Items: append([]SIMCard{}, matched[start:end]...), Total: len(matched), Page: page, PageSize: pageSize}
|
||||
}
|
||||
|
||||
func (s *StoreManager) QueryAllCardsByGroup(keyword, exportStatus string, groupID int64, filterGroup bool) []SIMCard {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return append([]SIMCard{}, filterCards(s.data.Cards, keyword, exportStatus, groupID, filterGroup)...)
|
||||
}
|
||||
|
||||
func filterCards(cards []SIMCard, keyword, exportStatus string, groupID int64, filterGroup bool) []SIMCard {
|
||||
keyword = strings.TrimSpace(keyword)
|
||||
matched := make([]SIMCard, 0)
|
||||
for i := len(cards) - 1; i >= 0; i-- {
|
||||
card := cards[i]
|
||||
groupMatch := !filterGroup || card.GroupID == groupID
|
||||
exportMatch := exportStatus == "" || (exportStatus == "exported" && card.Exported) || (exportStatus == "not_exported" && !card.Exported)
|
||||
if groupMatch && exportMatch && (keyword == "" || card.MSISDN == keyword || card.ICCID == keyword || card.IMSI == keyword) {
|
||||
matched = append(matched, card)
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
func (s *StoreManager) QueryOrders(page, pageSize int) OrderPage {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
total := len(s.data.Orders)
|
||||
start := (page - 1) * pageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
items := make([]Order, 0, end-start)
|
||||
for i := total - 1 - start; i >= total-end; i-- {
|
||||
items = append(items, s.data.Orders[i])
|
||||
}
|
||||
return OrderPage{Items: items, Total: total, Page: page, PageSize: pageSize}
|
||||
}
|
||||
|
||||
func (s *StoreManager) load() error {
|
||||
return s.reloadLocked()
|
||||
}
|
||||
|
||||
func (s *StoreManager) reloadLocked() error {
|
||||
data, err := s.backend.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.initialized {
|
||||
data.AppIDs = s.data.AppIDs
|
||||
data.Tokens = s.data.Tokens
|
||||
data.Orders = s.data.Orders
|
||||
data.Settings = s.data.Settings
|
||||
}
|
||||
s.data = data
|
||||
if s.data.Tokens == nil {
|
||||
s.data.Tokens = map[string]Token{}
|
||||
}
|
||||
s.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreManager) saveLocked() error {
|
||||
return s.backend.Save(s.data)
|
||||
}
|
||||
|
||||
func defaultStore() Store {
|
||||
now := time.Now()
|
||||
return Store{
|
||||
AppIDs: []Credential{
|
||||
{
|
||||
AppID: "demo_appid",
|
||||
Password: "demo_password",
|
||||
Name: "默认测试应用",
|
||||
Enabled: true,
|
||||
CreatedAt: now.Format("2006-01-02 15:04:05"),
|
||||
},
|
||||
},
|
||||
Cards: []SIMCard{},
|
||||
Groups: []CardGroup{},
|
||||
Tokens: map[string]Token{},
|
||||
Orders: []Order{},
|
||||
Settings: SimulationSettings{
|
||||
AutoGrowthEnabled: false,
|
||||
DefaultGrowthKB: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func sampleCard(msisdn, iccid, imsi, imei, remark string, monthKB float64) SIMCard {
|
||||
now := time.Now()
|
||||
return SIMCard{
|
||||
MSISDN: msisdn,
|
||||
ICCID: iccid,
|
||||
IMSI: imsi,
|
||||
IMEI: imei,
|
||||
Remark: remark,
|
||||
CardStatus: "2",
|
||||
LastChangeDate: now.AddDate(0, -1, 0).Format("2006-01-02 15:04:05"),
|
||||
OpenDate: now.AddDate(-1, 0, 0).Format("2006-01-02 15:04:05"),
|
||||
ActiveDate: now.AddDate(-1, 0, 3).Format("2006-01-02 15:04:05"),
|
||||
OnOffStatus: "1",
|
||||
SessionStatus: "01",
|
||||
IP: fmt.Sprintf("10.8.%d.%d", len(msisdn), now.Second()+20),
|
||||
IPv6Prefix: "",
|
||||
IPv6: "",
|
||||
RAT: "6",
|
||||
CardBindStatus: "1",
|
||||
RegionLimitStatus: "0",
|
||||
AreaRestrictType: "1",
|
||||
RestrictedArea: "200",
|
||||
MonthUsageKB: monthKB,
|
||||
PoolUsageBytes: monthKB * 1024,
|
||||
DailyBaseKB: monthKB / 30,
|
||||
MonthlyBaseKB: monthKB,
|
||||
GrowthPerMinuteKB: 1.5,
|
||||
LastUsageUpdateTime: now.Format(time.RFC3339),
|
||||
APNs: []APN{
|
||||
{Name: "CMIOT", Status: "1", UsageBytes: monthKB * 1024 * 0.7},
|
||||
{Name: "CMMTM", Status: "1", UsageBytes: monthKB * 1024 * 0.3},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cloneStore(src Store) Store {
|
||||
b, _ := json.Marshal(src)
|
||||
var out Store
|
||||
_ = json.Unmarshal(b, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
func randomHex(n int) string {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
Reference in New Issue
Block a user