package service import ( "encoding/json" "fmt" "math" "net/http" "strconv" "strings" "time" ) type Service struct { store *StoreManager operationLogs *OperationLogStore runtimeLogs *RuntimeLogStore } func NewService(dataPath string, runtimeLogStores ...*RuntimeLogStore) (*Service, error) { store, err := NewStoreManager(dataPath) if err != nil { return nil, err } runtimeLogs := NewRuntimeLogStore() if len(runtimeLogStores) > 0 && runtimeLogStores[0] != nil { runtimeLogs = runtimeLogStores[0] } return &Service{store: store, operationLogs: NewOperationLogStore(), runtimeLogs: runtimeLogs}, nil } func (s *Service) Router() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/healthz", s.health) mux.HandleFunc("/api/state", s.adminState) mux.HandleFunc("/api/appids", s.adminAppIDs) mux.HandleFunc("/api/cards", s.adminCards) mux.HandleFunc("/api/card-groups", s.adminCardGroups) mux.HandleFunc("/api/cards/export", s.adminCardsExport) mux.HandleFunc("/api/settings", s.adminSettings) mux.HandleFunc("/api/orders", s.adminOrders) mux.HandleFunc("/api/operation-logs", s.adminOperationLogs) mux.HandleFunc("/api/runtime-logs", s.adminRuntimeLogs) mux.HandleFunc("/api/sync", s.adminSync) mux.HandleFunc("/api/generate", s.adminGenerate) mux.HandleFunc("/sim/", s.chanAPI) mux.HandleFunc("/v5/", s.oneLink) mux.HandleFunc("/ec/", s.oneLink) return withCORS(s.withOperationLogging(mux)) } func withCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (s *Service) health(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Service) oneLink(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/v5/") path = strings.TrimPrefix(path, "/") switch path { case "ec/get/token": s.getToken(w, r) case "ec/query/sim-basic-info": s.withToken(w, r, s.queryBasicInfo) case "ec/query/sim-session": s.withToken(w, r, s.querySession) case "ec/query/sim-status": s.withToken(w, r, s.queryStatus) case "ec/query/sim-stop-reason": s.withToken(w, r, s.queryStopReason) case "ec/query/sim-frozen-state-info": s.withToken(w, r, s.queryFrozenState) case "ec/query/sim-apn-session-info": s.withToken(w, r, s.queryAPNSession) case "ec/query/sim-activated-features-info": s.withToken(w, r, s.queryActivatedFeatures) case "ec/query/sim-operating-status": s.withToken(w, r, s.queryOperatingStatus) case "ec/query/sim-data-diagnosis": s.withToken(w, r, s.queryDataDiagnosis) case "ec/query/sim-imei": s.withToken(w, r, s.queryIMEI) case "ec/query/sim-data-usage-inpool": s.withToken(w, r, s.queryDataUsageInPool) case "ec/query/sim-data-usage": s.withToken(w, r, s.queryDataUsage) case "ec/query/sim-data-usage-monthly/batch": s.withToken(w, r, s.queryDataUsageMonthlyBatch) case "ec/query/sim-data-usage-daily/batch": s.withToken(w, r, s.queryDataUsageDailyBatch) case "ec/query/apn-info": s.withToken(w, r, s.queryAPNInfo) case "ec/query/card-bind-status": s.withToken(w, r, s.queryCardBindStatus) case "ec/query/on-off-status": s.withToken(w, r, s.queryOnOffStatus) case "ec/query/region-limit-status": s.withToken(w, r, s.queryRegionLimitStatus) case "ec/change/sim-status": s.withToken(w, r, s.changeSIMStatus) case "ec/operate/sim-apn-function": s.withToken(w, r, s.operateSIMAPNFunction) default: writeOneLink(w, Response{Status: "14008", Message: "查询不到信息", Result: []interface{}{}}) } } func (s *Service) getToken(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() appid := q.Get("appid") password := q.Get("password") refresh := q.Get("refresh") var result Response err := s.store.SaveMutation(func(st *Store) error { for i := range st.AppIDs { cred := &st.AppIDs[i] if cred.AppID != appid || !cred.Enabled { continue } if cred.Password != password { result = Response{Status: "12022", Message: "PASSWORD 鉴权不通过", Result: []interface{}{}} return nil } now := time.Now() if refresh == "0" { if token, ok := st.Tokens[appid]; ok && token.ExpiresAt.After(now) { ttl := int(time.Until(token.ExpiresAt).Seconds()) result = okResp(map[string]string{"token": token.Value, "ttl": strconv.Itoa(ttl)}) return nil } } token := Token{ AppID: appid, Value: randomHex(32), ExpiresAt: now.Add(time.Hour), } st.Tokens[appid] = token cred.LastToken = token.Value cred.TokenExpires = token.ExpiresAt.Format("2006-01-02 15:04:05") result = okResp(map[string]string{"token": token.Value, "ttl": "3600"}) return nil } result = Response{Status: "12021", Message: "认证无效", Result: []interface{}{}} return nil }) if err != nil { writeOneLink(w, Response{Status: "19000", Message: err.Error(), Result: []interface{}{}}) return } writeOneLink(w, result) } func (s *Service) withToken(w http.ResponseWriter, r *http.Request, next func(http.ResponseWriter, *http.Request, Store)) { token := r.URL.Query().Get("token") st := s.store.Snapshot() for _, t := range st.Tokens { if t.Value == token && t.ExpiresAt.After(time.Now()) { next(w, r, st) return } } writeOneLink(w, Response{Status: "12021", Message: "TOKEN 不存在或已过期,请重新获取", Result: []interface{}{}}) } func (s *Service) queryBasicInfo(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]string{ "msisdn": card.MSISDN, "imsi": card.IMSI, "iccid": card.ICCID, "activeDate": card.ActiveDate, "openDate": card.OpenDate, "remark": card.Remark, })) } func (s *Service) querySession(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]interface{}{ "simSessionList": []map[string]string{{ "apnId": "CMIOT", "status": card.SessionStatus, "ip": card.IP, "ipv6Prefix": card.IPv6Prefix, "ipv6": card.IPv6, "createDate": card.LastChangeDate, "rat": card.RAT, }}, })) } func (s *Service) queryStatus(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]string{"cardStatus": card.CardStatus, "lastChangeDate": card.LastChangeDate})) } func (s *Service) queryStopReason(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } stopReason := card.StopReason if stopReason == "" { stopReason = defaultStopReason(card.CardStatus) } writeOneLink(w, okResp(map[string]string{"platformType": "OneLink-CT", "stopReason": stopReason})) } func (s *Service) queryFrozenState(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } frozen := card.FrozenStatus if frozen == "" { frozen = "0" } operTime := card.FrozenOperTime if operTime == "" { operTime = diagnosticTime(parseCardTime(card.LastChangeDate)) } data := map[string]string{"frozenStatus": frozen, "operTime": operTime} if frozen == "1" && card.FrozenReason != "" { data["frozenReason"] = card.FrozenReason } writeOneLink(w, okResp(data)) } func (s *Service) queryAPNSession(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } items := make([]map[string]string, 0, len(card.APNs)) for _, apn := range card.APNs { status := card.SessionStatus if status != "00" && status != "01" { status = "00" } createTime := card.SessionCreateDate if createTime == "" { createTime = card.LastChangeDate } items = append(items, map[string]string{"apn": apn.Name, "sessionStatus": status, "ipv4": card.IP, "ipv6Prefix": card.IPv6Prefix, "ipv6": card.IPv6, "createTime": createTime, "accessType": accessType(card.RAT)}) } if len(items) == 0 { status := card.SessionStatus if status != "00" && status != "01" { status = "00" } createTime := card.SessionCreateDate if createTime == "" { createTime = card.LastChangeDate } items = append(items, map[string]string{"apn": "CMIOT", "sessionStatus": status, "ipv4": card.IP, "ipv6Prefix": card.IPv6Prefix, "ipv6": card.IPv6, "createTime": createTime, "accessType": accessType(card.RAT)}) } writeOneLink(w, okResp(map[string]interface{}{"infoList": items})) } func (s *Service) queryActivatedFeatures(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } items := make([]map[string]string, 0, len(card.APNs)) for _, apn := range card.APNs { openStatus := "0" if apn.Status == "1" { openStatus = "1" } changeTime := apn.ModifyTime if changeTime == "" { changeTime = diagnosticTime(parseCardTime(card.LastChangeDate)) } items = append(items, map[string]string{"functionType": "11", "openStatus": openStatus, "apn": apn.Name, "changeTime": changeTime}) } if len(items) == 0 { items = append(items, map[string]string{"functionType": "11", "openStatus": "1", "apn": "CMIOT", "changeTime": diagnosticTime(parseCardTime(card.LastChangeDate))}) } writeOneLink(w, okResp(map[string]interface{}{"communicationFunctionList": items})) } func (s *Service) queryOperatingStatus(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]string{"operatingStatus": card.OnOffStatus})) } func (s *Service) queryDataDiagnosis(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } apnInfo := make([]map[string]interface{}, 0, len(card.APNs)) for _, apn := range card.APNs { status := apn.DiagnosisStatus if status == "" { status = "2" } modifyTime := apn.ModifyTime if modifyTime == "" { modifyTime = diagnosticTime(parseCardTime(card.LastChangeDate)) } usageState := apn.ServiceUsageState if usageState == "" { usageState = "1" } item := map[string]interface{}{"apnName": apn.Name, "apnStatus": status, "modifyTime": modifyTime, "apnServiceUsageState": usageState} if status == "4" && apn.OverUseDetails != "" { item["overUseDetails"] = apn.OverUseDetails } pccInfo := make([]map[string]string, 0, len(apn.PCCs)) for _, pcc := range apn.PCCs { pccTime := pcc.ModifyTime if pccTime == "" { pccTime = modifyTime } pccInfo = append(pccInfo, map[string]string{"pccServiceCode": pcc.ServiceCode, "pccName": pcc.Name, "pccPolicyStatus": pcc.PolicyStatus, "pccServiceUsageState": pcc.ServiceUsageState, "modifyTime": pccTime}) } item["pccList"] = map[string]interface{}{"pccInfo": pccInfo} apnInfo = append(apnInfo, item) } if len(apnInfo) == 0 { apnInfo = append(apnInfo, map[string]interface{}{"apnName": "CMIOT", "apnStatus": "2", "modifyTime": diagnosticTime(parseCardTime(card.LastChangeDate)), "apnServiceUsageState": "1", "pccList": map[string]interface{}{"pccInfo": []map[string]string{}}}) } writeOneLink(w, okResp(map[string]interface{}{"apnList": map[string]interface{}{"apnInfo": apnInfo}})) } func accessType(rat string) string { switch strings.ToUpper(rat) { case "5G", "51", "SA": return "51" case "NSA", "10": return "10" case "REDCAP", "58": return "58" case "NB", "8": return "8" case "2G", "2": return "2" case "3G", "3": return "1" default: return "6" } } func (s *Service) queryIMEI(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]string{"imei": card.IMEI})) } func (s *Service) queryDataUsageInPool(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } apns := make([]map[string]string, 0, len(card.APNs)) for _, apn := range card.APNs { apns = append(apns, map[string]string{"apnName": apn.Name, "apnUseAmount": fmtAmount(apn.UsageBytes)}) } writeOneLink(w, okResp(map[string]interface{}{"apnList": apns})) } func (s *Service) queryDataUsage(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]interface{}{ "dataAmount": fmt.Sprintf("%.2f", card.MonthUsageKB), "apnUseAmountList": apnUseAmountList(card), })) } func (s *Service) queryDataUsageMonthlyBatch(w http.ResponseWriter, r *http.Request, st Store) { cards, key, err := findBatchCards(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } items := make([]map[string]interface{}, 0, len(cards)) for _, card := range cards { amount := card.MonthUsageKB if q := r.URL.Query().Get("queryDate"); len(q) == 6 && q != time.Now().Format("200601") { amount = card.MonthlyBaseKB } items = append(items, batchUsageItem(card, key, amount)) } writeOneLink(w, okResp(map[string]interface{}{ "deadLine": time.Now().AddDate(0, 0, -1).Format("20060102"), "dataAmountList": items, })) } func (s *Service) queryDataUsageDailyBatch(w http.ResponseWriter, r *http.Request, st Store) { cards, key, err := findBatchCards(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } items := make([]map[string]interface{}, 0, len(cards)) for _, card := range cards { items = append(items, batchUsageItem(card, key, card.DailyBaseKB)) } writeOneLink(w, okResp(map[string]interface{}{"dataAmountList": items})) } func (s *Service) queryAPNInfo(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } apns := make([]map[string]string, 0, len(card.APNs)) for _, apn := range card.APNs { apns = append(apns, map[string]string{"apnName": apn.Name, "status": apn.Status}) } writeOneLink(w, okResp(map[string]interface{}{"apnList": apns})) } func (s *Service) queryCardBindStatus(w http.ResponseWriter, r *http.Request, st Store) { if r.URL.Query().Get("msisdn") == "" || r.URL.Query().Get("iccid") != "" || r.URL.Query().Get("imsi") != "" { writeOneLink(w, paramErr("card-bind-status requires msisdn only")) return } testType := r.URL.Query().Get("testType") if testType != "0" && testType != "1" { writeOneLink(w, paramErr("testType must be 0 or 1")) return } card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } result := card.CardBindStatus if result != "0" && result != "1" && result != "2" { result = "1" } data := map[string]string{"result": result} if result == "2" { data["errorCode"] = "MOCK_BIND_CHECK_FAILED" data["errorDes"] = "mock bind check failed" } writeOneLink(w, okResp(data)) } func (s *Service) queryOnOffStatus(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]string{"status": card.OnOffStatus})) } func (s *Service) queryRegionLimitStatus(w http.ResponseWriter, r *http.Request, st Store) { card, err := findSingleCard(r, st) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(map[string]string{"regionLimitStatus": card.RegionLimitStatus})) } func (s *Service) changeSIMStatus(w http.ResponseWriter, r *http.Request, st Store) { q := r.URL.Query() idKey, idVal, err := singleIdentifier(q) if err != nil { writeOneLink(w, paramErr(err.Error())) return } operType := q.Get("operType") orderNum := "ORD" + time.Now().Format("20060102150405") + randomHex(3) var result map[string]string err = s.store.SaveMutation(func(store *Store) error { idx := findCardIndex(*store, idKey, idVal) if idx < 0 { return fmt.Errorf("查询不到信息") } card := &store.Cards[idx] card.CardStatus = statusByOperType(operType, card.CardStatus) card.LastChangeDate = time.Now().Format("2006-01-02 15:04:05") store.Orders = append(store.Orders, Order{ OrderNum: orderNum, MSISDN: card.MSISDN, ICCID: card.ICCID, IMSI: card.IMSI, Type: "sim-status", OperType: operType, Status: "completed", CreatedAt: card.LastChangeDate, }) result = map[string]string{idKey: idVal, "orderNum": orderNum} return nil }) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(result)) } func (s *Service) operateSIMAPNFunction(w http.ResponseWriter, r *http.Request, st Store) { q := r.URL.Query() idKey, idVal, err := singleIdentifier(q) if err != nil { writeOneLink(w, paramErr(err.Error())) return } apnName := q.Get("apnName") operType := q.Get("operType") if apnName == "" || (operType != "0" && operType != "1") { writeOneLink(w, paramErr("apnName 与 operType 必填")) return } orderNum := "APN" + time.Now().Format("20060102150405") + randomHex(3) var result map[string]string err = s.store.SaveMutation(func(store *Store) error { idx := findCardIndex(*store, idKey, idVal) if idx < 0 { return fmt.Errorf("查询不到信息") } card := &store.Cards[idx] status := "0" if operType == "0" { status = "1" } found := false for i := range card.APNs { if strings.EqualFold(card.APNs[i].Name, apnName) { card.APNs[i].Status = status found = true } } if !found { card.APNs = append(card.APNs, APN{Name: apnName, Status: status}) } now := time.Now().Format("2006-01-02 15:04:05") store.Orders = append(store.Orders, Order{ OrderNum: orderNum, MSISDN: card.MSISDN, ICCID: card.ICCID, IMSI: card.IMSI, Type: "sim-apn-function", OperType: operType, APNName: apnName, Status: "completed", CreatedAt: now, }) result = map[string]string{idKey: idVal, "orderNum": orderNum} return nil }) if err != nil { writeOneLink(w, paramErr(err.Error())) return } writeOneLink(w, okResp(result)) } func okResp(item interface{}) Response { return Response{Status: "0", Message: "正确", Result: []interface{}{item}} } func paramErr(message string) Response { if strings.Contains(message, "查询不到") { return Response{Status: "14008", Message: "查询不到信息", Result: []interface{}{}} } return Response{Status: "12001", Message: message, Result: []interface{}{}} } func writeOneLink(w http.ResponseWriter, resp Response) { writeJSON(w, http.StatusOK, resp) } func writeJSON(w http.ResponseWriter, code int, v interface{}) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } func singleIdentifier(q mapLike) (string, string, error) { found := [][2]string{} for _, key := range []string{"msisdn", "iccid", "imsi"} { if v := q.Get(key); v != "" { found = append(found, [2]string{key, v}) } } if len(found) != 1 { return "", "", fmt.Errorf("msisdn、iccid、imsi 必须有且只有一项") } return found[0][0], found[0][1], nil } type mapLike interface { Get(string) string } func findSingleCard(r *http.Request, st Store) (SIMCard, error) { key, value, err := singleIdentifier(r.URL.Query()) if err != nil { return SIMCard{}, err } for _, card := range st.Cards { if matches(card, key, value) { return card, nil } } return SIMCard{}, fmt.Errorf("查询不到信息") } func findBatchCards(r *http.Request, st Store) ([]SIMCard, string, error) { q := r.URL.Query() keys := []string{"msisdns", "iccids", "imsis"} foundKey := "" foundValue := "" for _, key := range keys { if v := q.Get(key); v != "" { if foundKey != "" { return nil, "", fmt.Errorf("msisdns、iccids、imsis 必须有且只有一项") } foundKey = key foundValue = v } } if foundKey == "" { return nil, "", fmt.Errorf("msisdns、iccids、imsis 必须有且只有一项") } singular := strings.TrimSuffix(foundKey, "s") ids := strings.Split(foundValue, "_") var cards []SIMCard for _, id := range ids { id = strings.TrimSpace(id) for _, card := range st.Cards { if matches(card, singular, id) { cards = append(cards, card) break } } } if len(cards) == 0 { return nil, "", fmt.Errorf("查询不到信息") } return cards, singular, nil } func findCardIndex(st Store, key, value string) int { for i, card := range st.Cards { if matches(card, key, value) { return i } } return -1 } func matches(card SIMCard, key, value string) bool { switch key { case "msisdn": return card.MSISDN == value case "iccid": return card.ICCID == value case "imsi": return card.IMSI == value } return false } func apnUseAmountList(card SIMCard) []map[string]interface{} { out := make([]map[string]interface{}, 0, len(card.APNs)) for _, apn := range card.APNs { out = append(out, map[string]interface{}{ "apnName": apn.Name, "apnUseAmount": fmtAmount(apn.UsageBytes / 1024), "pccCodeUseAmountList": []map[string]string{{ "pccCode": "default", "pccCodeUseAmount": fmtAmount(apn.UsageBytes / 1024), }}, }) } return out } func batchUsageItem(card SIMCard, key string, amount float64) map[string]interface{} { item := map[string]interface{}{ key: identifierValue(card, key), "dataAmount": fmtAmount(amount), } if amount > 0 { item["apnDataAmountList"] = []map[string]string{ {"apnName": "CMIOT", "apnDataAmount": fmtAmount(amount * 0.7)}, {"apnName": "CMMTM", "apnDataAmount": fmtAmount(amount * 0.3)}, } } return item } func identifierValue(card SIMCard, key string) string { switch key { case "iccid": return card.ICCID case "imsi": return card.IMSI default: return card.MSISDN } } func fmtAmount(v float64) string { if math.Abs(v-math.Round(v)) < 0.001 { return strconv.FormatInt(int64(math.Round(v)), 10) } return fmt.Sprintf("%.2f", v) } func statusByOperType(operType, current string) string { switch operType { case "0": return "4" case "1", "2", "5", "6": return "2" case "3", "8": return "7" case "4", "9": return "1" default: return current } }