4e2cd43a55
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
562 lines
15 KiB
Go
562 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (s *Service) adminState(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
}
|
|
|
|
func (s *Service) adminAppIDs(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
var cred Credential
|
|
if err := json.NewDecoder(r.Body).Decode(&cred); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if cred.AppID == "" || cred.Password == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "appid and password are required"})
|
|
return
|
|
}
|
|
if cred.Name == "" {
|
|
cred.Name = cred.AppID
|
|
}
|
|
cred.Enabled = true
|
|
cred.CreatedAt = time.Now().Format("2006-01-02 15:04:05")
|
|
err := s.store.SaveMutation(func(st *Store) error {
|
|
for i := range st.AppIDs {
|
|
if st.AppIDs[i].AppID == cred.AppID {
|
|
st.AppIDs[i].Password = cred.Password
|
|
st.AppIDs[i].Name = cred.Name
|
|
st.AppIDs[i].Enabled = cred.Enabled
|
|
st.AppIDs[i].LastToken = ""
|
|
st.AppIDs[i].TokenExpires = ""
|
|
delete(st.Tokens, cred.AppID)
|
|
return nil
|
|
}
|
|
}
|
|
st.AppIDs = append(st.AppIDs, cred)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
case http.MethodDelete:
|
|
appid := r.URL.Query().Get("appid")
|
|
_ = s.store.SaveMutation(func(st *Store) error {
|
|
next := st.AppIDs[:0]
|
|
for _, c := range st.AppIDs {
|
|
if c.AppID != appid {
|
|
next = append(next, c)
|
|
}
|
|
}
|
|
st.AppIDs = next
|
|
delete(st.Tokens, appid)
|
|
return nil
|
|
})
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Service) adminCards(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
page, pageSize := pagination(r)
|
|
filter, err := cardFilterFromRequest(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, s.store.QueryCardsByGroup(filter.keyword, filter.exportStatus, filter.groupID, filter.filterGroup, page, pageSize))
|
|
case http.MethodPost:
|
|
var card SIMCard
|
|
if err := json.NewDecoder(r.Body).Decode(&card); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
normalizeCard(&card)
|
|
err := s.store.SaveMutation(func(st *Store) error {
|
|
for i := range st.Cards {
|
|
if st.Cards[i].MSISDN == card.MSISDN {
|
|
st.Cards[i] = card
|
|
return nil
|
|
}
|
|
}
|
|
st.Cards = append(st.Cards, card)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
case http.MethodDelete:
|
|
msisdn := r.URL.Query().Get("msisdn")
|
|
_ = s.store.SaveMutation(func(st *Store) error {
|
|
next := st.Cards[:0]
|
|
for _, c := range st.Cards {
|
|
if c.MSISDN != msisdn {
|
|
next = append(next, c)
|
|
}
|
|
}
|
|
st.Cards = next
|
|
return nil
|
|
})
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Service) adminCardGroups(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
st := s.store.Snapshot()
|
|
counts := map[int64]int{}
|
|
for _, card := range st.Cards {
|
|
counts[card.GroupID]++
|
|
}
|
|
for i := range st.Groups {
|
|
st.Groups[i].CardCount = counts[st.Groups[i].ID]
|
|
}
|
|
writeJSON(w, http.StatusOK, st.Groups)
|
|
case http.MethodPost:
|
|
var input struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
input.Name = strings.TrimSpace(input.Name)
|
|
if input.Name == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "group name is required"})
|
|
return
|
|
}
|
|
var created CardGroup
|
|
err := s.store.SaveMutation(func(st *Store) error {
|
|
for _, group := range st.Groups {
|
|
if strings.EqualFold(group.Name, input.Name) {
|
|
return fmt.Errorf("group name already exists")
|
|
}
|
|
}
|
|
id := int64(1)
|
|
for _, group := range st.Groups {
|
|
if group.ID >= id {
|
|
id = group.ID + 1
|
|
}
|
|
}
|
|
now := time.Now().Format("2006-01-02 15:04:05")
|
|
created = CardGroup{ID: id, Name: input.Name, CreatedAt: now, UpdatedAt: now}
|
|
st.Groups = append(st.Groups, created)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, created)
|
|
case http.MethodDelete:
|
|
id, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
|
|
if err != nil || id <= 0 {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid group id"})
|
|
return
|
|
}
|
|
err = s.store.SaveMutation(func(st *Store) error {
|
|
found := false
|
|
next := st.Groups[:0]
|
|
for _, group := range st.Groups {
|
|
if group.ID == id {
|
|
found = true
|
|
} else {
|
|
next = append(next, group)
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("card group not found")
|
|
}
|
|
st.Groups = next
|
|
for i := range st.Cards {
|
|
if st.Cards[i].GroupID == id {
|
|
st.Cards[i].GroupID = 0
|
|
st.Cards[i].GroupName = ""
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *Service) adminSettings(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var settings SimulationSettings
|
|
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
_ = s.store.SaveMutation(func(st *Store) error {
|
|
st.Settings = settings
|
|
return nil
|
|
})
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
}
|
|
|
|
func (s *Service) adminOrders(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodGet {
|
|
page, pageSize := pagination(r)
|
|
writeJSON(w, http.StatusOK, s.store.QueryOrders(page, pageSize))
|
|
return
|
|
}
|
|
if r.Method != http.MethodDelete {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
_ = s.store.SaveMutation(func(st *Store) error {
|
|
st.Orders = []Order{}
|
|
return nil
|
|
})
|
|
writeJSON(w, http.StatusOK, s.store.Snapshot())
|
|
}
|
|
|
|
func pagination(r *http.Request) (int, int) {
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
pageSize, _ := strconv.Atoi(r.URL.Query().Get("pageSize"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
return page, pageSize
|
|
}
|
|
|
|
type cardFilter struct {
|
|
keyword string
|
|
exportStatus string
|
|
groupID int64
|
|
filterGroup bool
|
|
}
|
|
|
|
func cardFilterFromRequest(r *http.Request) (cardFilter, error) {
|
|
filter := cardFilter{
|
|
keyword: r.URL.Query().Get("keyword"),
|
|
exportStatus: strings.TrimSpace(r.URL.Query().Get("exportStatus")),
|
|
}
|
|
if filter.exportStatus != "" && filter.exportStatus != "exported" && filter.exportStatus != "not_exported" {
|
|
return cardFilter{}, fmt.Errorf("invalid export status")
|
|
}
|
|
groupRaw := strings.TrimSpace(r.URL.Query().Get("groupId"))
|
|
if groupRaw == "" {
|
|
return filter, nil
|
|
}
|
|
groupID, err := strconv.ParseInt(groupRaw, 10, 64)
|
|
if err != nil || groupID < 0 {
|
|
return cardFilter{}, fmt.Errorf("invalid group id")
|
|
}
|
|
filter.groupID = groupID
|
|
filter.filterGroup = true
|
|
return filter, nil
|
|
}
|
|
|
|
func (s *Service) adminGenerate(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
count, groupID, err := generationRequest(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
result, err := s.store.GenerateCardsInGroup(count, groupID)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (s *Service) adminSync(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
count, _, err := generationRequest(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
result, err := s.store.GenerateCards(count)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (s *Service) adminCardsExport(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
var cards []SIMCard
|
|
if r.URL.Query().Get("selection") == "filtered" {
|
|
filter, err := cardFilterFromRequest(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
cards = s.store.QueryAllCardsByGroup(filter.keyword, filter.exportStatus, filter.groupID, filter.filterGroup)
|
|
} else {
|
|
cards = exportCardsByMSISDN(s.store.Snapshot().Cards, r.URL.Query().Get("msisdns"))
|
|
}
|
|
raw, err := exportInboundCardsXLSX(cards)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
exportedAt := time.Now().Format("2006-01-02 15:04:05")
|
|
exportedMSISDNs := make(map[string]struct{}, len(cards))
|
|
for _, card := range cards {
|
|
exportedMSISDNs[card.MSISDN] = struct{}{}
|
|
}
|
|
if err := s.store.SaveMutation(func(st *Store) error {
|
|
for i := range st.Cards {
|
|
if _, ok := exportedMSISDNs[st.Cards[i].MSISDN]; ok {
|
|
st.Cards[i].Exported = true
|
|
st.Cards[i].ExportCheckedAt = exportedAt
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="sim_card_export.xlsx"`)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(raw)
|
|
}
|
|
|
|
func exportCardsByMSISDN(cards []SIMCard, rawMSISDNs string) []SIMCard {
|
|
rawMSISDNs = strings.TrimSpace(rawMSISDNs)
|
|
if rawMSISDNs == "" {
|
|
return cards
|
|
}
|
|
selected := map[string]bool{}
|
|
for _, item := range strings.FieldsFunc(rawMSISDNs, func(r rune) bool {
|
|
return r == ',' || r == '_' || r == ' '
|
|
}) {
|
|
item = strings.TrimSpace(item)
|
|
if item != "" {
|
|
selected[item] = true
|
|
}
|
|
}
|
|
if len(selected) == 0 {
|
|
return cards
|
|
}
|
|
out := make([]SIMCard, 0, len(selected))
|
|
for _, card := range cards {
|
|
if selected[card.MSISDN] {
|
|
out = append(out, card)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func generationCount(r *http.Request) (int, error) {
|
|
count := 100
|
|
if r.Body != nil {
|
|
var payload struct {
|
|
Count int `json:"count"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err == nil && payload.Count > 0 {
|
|
count = payload.Count
|
|
}
|
|
}
|
|
if value := r.URL.Query().Get("count"); value != "" {
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
count = parsed
|
|
}
|
|
if count <= 0 {
|
|
return 0, strconv.ErrSyntax
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func generationRequest(r *http.Request) (int, int64, error) {
|
|
var input struct {
|
|
Count int `json:"count"`
|
|
GroupID *int64 `json:"groupId"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
if err == io.EOF {
|
|
count, countErr := generationCount(r)
|
|
return count, 0, countErr
|
|
}
|
|
return 0, 0, err
|
|
}
|
|
if input.Count <= 0 {
|
|
return 0, 0, fmt.Errorf("count must be greater than 0")
|
|
}
|
|
if input.GroupID == nil || *input.GroupID < 0 {
|
|
return input.Count, 0, nil
|
|
}
|
|
return input.Count, *input.GroupID, nil
|
|
}
|
|
|
|
func normalizeCard(card *SIMCard) {
|
|
now := time.Now()
|
|
card.MSISDN = strings.TrimSpace(card.MSISDN)
|
|
card.ICCID = strings.TrimSpace(card.ICCID)
|
|
card.IMSI = strings.TrimSpace(card.IMSI)
|
|
if card.CardStatus == "" {
|
|
card.CardStatus = "2"
|
|
}
|
|
if card.StopReason == "" {
|
|
card.StopReason = defaultStopReason(card.CardStatus)
|
|
}
|
|
if card.FrozenStatus == "" {
|
|
card.FrozenStatus = "0"
|
|
}
|
|
if card.FrozenOperTime == "" {
|
|
card.FrozenOperTime = diagnosticTime(now)
|
|
}
|
|
if card.LastChangeDate == "" {
|
|
card.LastChangeDate = now.Format("2006-01-02 15:04:05")
|
|
}
|
|
if card.OpenDate == "" {
|
|
card.OpenDate = now.Format("2006-01-02 15:04:05")
|
|
}
|
|
if card.ActiveDate == "" {
|
|
card.ActiveDate = now.Format("2006-01-02 15:04:05")
|
|
}
|
|
if card.OnOffStatus == "" {
|
|
card.OnOffStatus = "1"
|
|
}
|
|
if card.SessionStatus == "" {
|
|
card.SessionStatus = "01"
|
|
}
|
|
if card.SessionStatus != "00" && card.SessionStatus != "01" {
|
|
card.SessionStatus = "00"
|
|
}
|
|
if card.SessionCreateDate == "" {
|
|
card.SessionCreateDate = card.LastChangeDate
|
|
}
|
|
if card.SessionCreateDate == "" {
|
|
card.SessionCreateDate = card.LastChangeDate
|
|
}
|
|
if card.RAT == "" {
|
|
card.RAT = "6"
|
|
}
|
|
if card.CardBindStatus == "" {
|
|
card.CardBindStatus = "1"
|
|
}
|
|
if card.RegionLimitStatus == "" {
|
|
card.RegionLimitStatus = "0"
|
|
}
|
|
if card.AreaRestrictType == "" {
|
|
card.AreaRestrictType = "1"
|
|
}
|
|
if card.RestrictedArea == "" {
|
|
card.RestrictedArea = "200"
|
|
}
|
|
if card.LastUsageUpdateTime == "" {
|
|
card.LastUsageUpdateTime = now.Format(time.RFC3339)
|
|
}
|
|
card.GrowthPerMinuteKB = 0
|
|
if len(card.APNs) == 0 {
|
|
card.APNs = []APN{{Name: "CMIOT", Status: "1", UsageBytes: card.MonthUsageKB * 1024}}
|
|
}
|
|
for i := range card.APNs {
|
|
apn := &card.APNs[i]
|
|
if apn.Name == "" {
|
|
apn.Name = "CMIOT"
|
|
}
|
|
if apn.Status == "" {
|
|
apn.Status = "1"
|
|
}
|
|
if apn.DiagnosisStatus == "" {
|
|
apn.DiagnosisStatus = diagnosisStatus(apn.Status)
|
|
}
|
|
if apn.ModifyTime == "" {
|
|
apn.ModifyTime = diagnosticTime(now)
|
|
}
|
|
if apn.ServiceUsageState == "" {
|
|
apn.ServiceUsageState = "1"
|
|
}
|
|
if len(apn.PCCs) == 0 {
|
|
apn.PCCs = []PCC{{ServiceCode: "default", Name: "APN 缺省流量", PolicyStatus: "2", ServiceUsageState: "1", ModifyTime: apn.ModifyTime}}
|
|
}
|
|
for j := range apn.PCCs {
|
|
if apn.PCCs[j].ServiceCode == "" {
|
|
apn.PCCs[j].ServiceCode = "default"
|
|
}
|
|
if apn.PCCs[j].Name == "" {
|
|
apn.PCCs[j].Name = "APN 缺省流量"
|
|
}
|
|
if apn.PCCs[j].PolicyStatus == "" {
|
|
apn.PCCs[j].PolicyStatus = "2"
|
|
}
|
|
if apn.PCCs[j].ServiceUsageState == "" {
|
|
apn.PCCs[j].ServiceUsageState = "1"
|
|
}
|
|
if apn.PCCs[j].ModifyTime == "" {
|
|
apn.PCCs[j].ModifyTime = apn.ModifyTime
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func diagnosticTime(value time.Time) string { return value.Format("20060102150405") }
|
|
|
|
func defaultStopReason(cardStatus string) string {
|
|
if cardStatus == "4" {
|
|
return "000020000000"
|
|
}
|
|
return "000000000000"
|
|
}
|
|
|
|
func diagnosisStatus(status string) string {
|
|
switch status {
|
|
case "0":
|
|
return "4"
|
|
case "1":
|
|
return "2"
|
|
default:
|
|
return status
|
|
}
|
|
}
|