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 } }