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:
MiFaZhan
2026-10-22 15:08:27 +08:00
commit 4e2cd43a55
61 changed files with 10417 additions and 0 deletions
+285
View File
@@ -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)
}