package service import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) const ( chanSuccessCode = 0 chanErrorCode = 9999 ) type chanSimKey struct { ChanKey string `json:"chanKey"` ICCID string `json:"iccid"` IMSI string `json:"imsi"` MSISDN string `json:"msisdn"` } type chanRequest struct { SimKeys []chanSimKey `json:"simKeys"` ReqData json.RawMessage `json:"reqData"` } type chanResponse[T any] struct { Code int `json:"code"` Message string `json:"message"` SimKey chanSimKey `json:"simKey"` ResData T `json:"resData"` } type chanBasicInfo struct { ActiveDate string `json:"activeDate"` OpenDate string `json:"openDate"` Remark string `json:"remark"` } type chanSession struct { ApnID string `json:"apnId"` Rat string `json:"rat"` IPv4 string `json:"ipv4"` IPv6 string `json:"ipv6"` IPv6Prefix string `json:"ipv6Prefix"` Status string `json:"status"` CreateTime string `json:"createTime"` } type chanCardStatus struct { CardStatus int `json:"cardStatus"` LastChangeDate string `json:"lastChangeDate"` } type chanIMEI struct { IMEI string `json:"imei"` } type chanUsage struct { DataUsage float64 `json:"dataUsage"` } type chanMonthlyUsage struct { DeadLine string `json:"deadLine"` DataAmount float64 `json:"dataAmount"` } type chanDailyUsage struct { QueryDate string `json:"queryDate"` DataAmount float64 `json:"dataAmount"` } type chanAPN struct { APNName string `json:"apnName"` Status string `json:"status"` } type chanAPNList struct { APNList []chanAPN `json:"apnList"` } type chanOrder struct { OrderNum string `json:"orderNum"` } type chanBindStatus struct { BindStatus string `json:"bindStatus"` } type chanDeviceStatus struct { DeviceStatus string `json:"deviceStatus"` } type chanRegionLimitStatus struct { RegionLimitStatus string `json:"regionLimitStatus"` } type chanReqData struct { QueryDate string `json:"queryDate"` OperType string `json:"operType"` APNName string `json:"apnName"` TestType *int `json:"testType"` } func (s *Service) chanAPI(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/sim/") switch path { case "query/sim-basic-info": s.chanQueryBasicInfo(w, r) case "query/sim-session": s.chanQuerySession(w, r) case "query/sim-status", "query/sim-status-batch": s.chanQueryStatus(w, r) case "query/sim-card-bind-status": s.chanQueryBindStatus(w, r) case "query/sim-device-status": s.chanQueryDeviceStatus(w, r) case "query/sim-region-limit-status": s.chanQueryRegionLimitStatus(w, r) case "query/sim-imei": s.chanQueryIMEI(w, r) case "query/sim-datausage": s.chanQueryDataUsage(w, r) case "query/sim-datausage-monthly", "query/sim-datausage-monthly-batch": s.chanQueryMonthly(w, r) case "query/sim-datausage-daily", "query/sim-datausage-daily-batch": s.chanQueryDaily(w, r) case "query/sim-apn-info": s.chanQueryAPN(w, r) case "change/sim-status": s.chanChangeStatus(w, r) case "change/sim-apn": s.chanChangeAPN(w, r) default: writeJSON(w, http.StatusNotFound, map[string]string{"message": "unknown sim endpoint"}) } } func parseChanRequest(r *http.Request) (chanRequest, error) { var req chanRequest if r.Method != http.MethodGet && r.Body != nil { body, err := io.ReadAll(io.LimitReader(r.Body, 2<<20)) if err != nil { return req, err } if len(strings.TrimSpace(string(body))) > 0 { if err := json.Unmarshal(body, &req); err != nil { return req, fmt.Errorf("invalid JSON body: %w", err) } } } if len(req.SimKeys) == 0 { req.SimKeys = simKeysFromQuery(r.URL.Query()) } if len(req.ReqData) == 0 { data := chanReqData{ QueryDate: r.URL.Query().Get("queryDate"), OperType: r.URL.Query().Get("operType"), APNName: r.URL.Query().Get("apnName"), } if value := r.URL.Query().Get("testType"); value != "" { if parsed, err := strconv.Atoi(value); err == nil { data.TestType = &parsed } } if data.QueryDate != "" || data.OperType != "" || data.APNName != "" || data.TestType != nil { req.ReqData, _ = json.Marshal(data) } } return req, nil } func simKeysFromQuery(q url.Values) []chanSimKey { if raw := q.Get("simKeys"); raw != "" { var keys []chanSimKey if json.Unmarshal([]byte(raw), &keys) == nil { return keys } } var values []chanSimKey for _, item := range []struct { key string name string }{ {"iccids", "iccid"}, {"imsis", "imsi"}, {"msisdns", "msisdn"}, } { for _, value := range splitIDs(q.Get(item.key)) { key := chanSimKey{} switch item.name { case "iccid": key.ICCID = value case "imsi": key.IMSI = value case "msisdn": key.MSISDN = value } values = append(values, key) } } if len(values) == 0 { key := chanSimKey{ICCID: q.Get("iccid"), IMSI: q.Get("imsi"), MSISDN: q.Get("msisdn")} if key.ICCID != "" || key.IMSI != "" || key.MSISDN != "" { values = append(values, key) } } return values } func splitIDs(raw string) []string { if raw == "" { return nil } raw = strings.ReplaceAll(raw, ",", "_") parts := strings.Split(raw, "_") values := make([]string, 0, len(parts)) for _, part := range parts { if value := strings.TrimSpace(part); value != "" { values = append(values, value) } } return values } func decodeChanReqData(req chanRequest) chanReqData { var data chanReqData _ = json.Unmarshal(req.ReqData, &data) return data } func chanSuccess[T any](key chanSimKey, data T) chanResponse[T] { return chanResponse[T]{Code: chanSuccessCode, Message: "success", SimKey: key, ResData: data} } func chanFailure[T any](key chanSimKey, message string) chanResponse[T] { return chanResponse[T]{Code: chanErrorCode, Message: message, SimKey: key, ResData: *new(T)} } func writeChanResponses[T any](w http.ResponseWriter, responses []chanResponse[T]) { writeJSON(w, http.StatusOK, responses) } func (s *Service) chanCards(r *http.Request) (chanRequest, Store, error) { req, err := parseChanRequest(r) if err != nil { return req, Store{}, err } return req, s.store.Snapshot(), nil } func findChanCard(st Store, key chanSimKey) (SIMCard, bool) { if key.ICCID != "" { for _, card := range st.Cards { if card.ICCID == key.ICCID { return card, true } } } if key.IMSI != "" { for _, card := range st.Cards { if card.IMSI == key.IMSI { return card, true } } } if key.MSISDN != "" { for _, card := range st.Cards { if card.MSISDN == key.MSISDN { return card, true } } } return SIMCard{}, false } func chanRequestError[T any](w http.ResponseWriter, err error) { writeJSON(w, http.StatusBadRequest, []chanResponse[T]{{Code: chanErrorCode, Message: err.Error()}}) } func (s *Service) chanQueryBasicInfo(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanBasicInfo](w, err) return } responses := make([]chanResponse[chanBasicInfo], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanBasicInfo](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanBasicInfo{ActiveDate: card.ActiveDate, OpenDate: card.OpenDate, Remark: card.Remark})) } writeChanResponses(w, responses) } func (s *Service) chanQuerySession(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanSession](w, err) return } responses := make([]chanResponse[chanSession], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanSession](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanSession{ApnID: primaryAPN(card).Name, Rat: card.RAT, IPv4: card.IP, IPv6: card.IPv6, IPv6Prefix: card.IPv6Prefix, Status: chanSessionStatus(card.SessionStatus), CreateTime: card.LastChangeDate})) } writeChanResponses(w, responses) } func (s *Service) chanQueryStatus(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanCardStatus](w, err) return } responses := make([]chanResponse[chanCardStatus], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanCardStatus](key, "card not found")) continue } status, _ := strconv.Atoi(card.CardStatus) responses = append(responses, chanSuccess(key, chanCardStatus{CardStatus: status, LastChangeDate: card.LastChangeDate})) } writeChanResponses(w, responses) } func (s *Service) chanQueryBindStatus(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanBindStatus](w, err) return } responses := make([]chanResponse[chanBindStatus], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanBindStatus](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanBindStatus{BindStatus: card.CardBindStatus})) } writeChanResponses(w, responses) } func (s *Service) chanQueryDeviceStatus(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanDeviceStatus](w, err) return } responses := make([]chanResponse[chanDeviceStatus], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanDeviceStatus](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanDeviceStatus{DeviceStatus: chanSessionStatus(card.OnOffStatus)})) } writeChanResponses(w, responses) } func (s *Service) chanQueryRegionLimitStatus(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanRegionLimitStatus](w, err) return } responses := make([]chanResponse[chanRegionLimitStatus], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanRegionLimitStatus](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanRegionLimitStatus{RegionLimitStatus: card.RegionLimitStatus})) } writeChanResponses(w, responses) } func (s *Service) chanQueryIMEI(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanIMEI](w, err) return } responses := make([]chanResponse[chanIMEI], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanIMEI](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanIMEI{IMEI: card.IMEI})) } writeChanResponses(w, responses) } func (s *Service) chanQueryDataUsage(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanUsage](w, err) return } responses := make([]chanResponse[chanUsage], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanUsage](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanUsage{DataUsage: card.MonthUsageKB / 1024})) } writeChanResponses(w, responses) } func (s *Service) chanQueryMonthly(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanMonthlyUsage](w, err) return } data := decodeChanReqData(req) responses := make([]chanResponse[chanMonthlyUsage], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanMonthlyUsage](key, "card not found")) continue } amount := card.MonthUsageKB if data.QueryDate != "" && len(data.QueryDate) == 6 && data.QueryDate != time.Now().Format("200601") { amount = card.MonthlyBaseKB } responses = append(responses, chanSuccess(key, chanMonthlyUsage{DeadLine: time.Now().AddDate(0, 0, -1).Format("20060102"), DataAmount: amount / 1024})) } writeChanResponses(w, responses) } func (s *Service) chanQueryDaily(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanDailyUsage](w, err) return } data := decodeChanReqData(req) queryDate := data.QueryDate if queryDate == "" { queryDate = r.URL.Query().Get("queryDate") } if queryDate == "" { queryDate = time.Now().Format("20060102") } responses := make([]chanResponse[chanDailyUsage], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanDailyUsage](key, "card not found")) continue } responses = append(responses, chanSuccess(key, chanDailyUsage{QueryDate: queryDate, DataAmount: card.DailyBaseKB / 1024})) } writeChanResponses(w, responses) } func (s *Service) chanQueryAPN(w http.ResponseWriter, r *http.Request) { req, st, err := s.chanCards(r) if err != nil { chanRequestError[chanAPNList](w, err) return } responses := make([]chanResponse[chanAPNList], 0, len(req.SimKeys)) for _, key := range req.SimKeys { card, ok := findChanCard(st, key) if !ok { responses = append(responses, chanFailure[chanAPNList](key, "card not found")) continue } apns := make([]chanAPN, 0, len(card.APNs)) for _, apn := range card.APNs { apns = append(apns, chanAPN{APNName: apn.Name, Status: apn.Status}) } responses = append(responses, chanSuccess(key, chanAPNList{APNList: apns})) } writeChanResponses(w, responses) } func (s *Service) chanChangeStatus(w http.ResponseWriter, r *http.Request) { req, err := parseChanRequest(r) if err != nil { chanRequestError[chanOrder](w, err) return } data := decodeChanReqData(req) if data.OperType != "0" && data.OperType != "1" { responses := make([]chanResponse[chanOrder], 0, len(req.SimKeys)) for _, key := range req.SimKeys { responses = append(responses, chanFailure[chanOrder](key, "unsupported operType, expected 0(stop) or 1(resume)")) } writeChanResponses(w, responses) return } responses := make([]chanResponse[chanOrder], 0, len(req.SimKeys)) err = s.store.SaveMutation(func(st *Store) error { for _, key := range req.SimKeys { idx := findChanCardIndex(*st, key) if idx < 0 { responses = append(responses, chanFailure[chanOrder](key, "card not found")) continue } card := &st.Cards[idx] if data.OperType == "0" { card.CardStatus = "4" } else { card.CardStatus = "2" } card.LastChangeDate = time.Now().Format("2006-01-02 15:04:05") orderNum := "ORD" + time.Now().Format("20060102150405") + randomHex(3) st.Orders = append(st.Orders, Order{OrderNum: orderNum, MSISDN: card.MSISDN, ICCID: card.ICCID, IMSI: card.IMSI, Type: "sim-status", OperType: data.OperType, Status: "completed", CreatedAt: card.LastChangeDate}) responses = append(responses, chanSuccess(key, chanOrder{OrderNum: orderNum})) } return nil }) if err != nil { chanRequestError[chanOrder](w, err) return } writeChanResponses(w, responses) } func (s *Service) chanChangeAPN(w http.ResponseWriter, r *http.Request) { req, err := parseChanRequest(r) if err != nil { chanRequestError[chanOrder](w, err) return } data := decodeChanReqData(req) if data.APNName == "" || (data.OperType != "0" && data.OperType != "1") { responses := make([]chanResponse[chanOrder], 0, len(req.SimKeys)) for _, key := range req.SimKeys { responses = append(responses, chanFailure[chanOrder](key, "apnName and operType are required")) } writeChanResponses(w, responses) return } responses := make([]chanResponse[chanOrder], 0, len(req.SimKeys)) err = s.store.SaveMutation(func(st *Store) error { for _, key := range req.SimKeys { idx := findChanCardIndex(*st, key) if idx < 0 { responses = append(responses, chanFailure[chanOrder](key, "card not found")) continue } card := &st.Cards[idx] status := "0" if data.OperType == "0" { status = "1" } found := false for i := range card.APNs { if strings.EqualFold(card.APNs[i].Name, data.APNName) { card.APNs[i].Status = status found = true } } if !found { card.APNs = append(card.APNs, APN{Name: data.APNName, Status: status}) } orderNum := "APN" + time.Now().Format("20060102150405") + randomHex(3) now := time.Now().Format("2006-01-02 15:04:05") st.Orders = append(st.Orders, Order{OrderNum: orderNum, MSISDN: card.MSISDN, ICCID: card.ICCID, IMSI: card.IMSI, Type: "sim-apn-function", OperType: data.OperType, APNName: data.APNName, Status: "completed", CreatedAt: now}) responses = append(responses, chanSuccess(key, chanOrder{OrderNum: orderNum})) } return nil }) if err != nil { chanRequestError[chanOrder](w, err) return } writeChanResponses(w, responses) } func findChanCardIndex(st Store, key chanSimKey) int { for i, card := range st.Cards { if (key.ICCID != "" && card.ICCID == key.ICCID) || (key.IMSI != "" && card.IMSI == key.IMSI) || (key.MSISDN != "" && card.MSISDN == key.MSISDN) { return i } } return -1 } func chanSessionStatus(status string) string { switch strings.ToUpper(strings.TrimSpace(status)) { case "01", "1", "0", "ONLINE": return "0" case "00", "-1", "OFFLINE": return "-1" default: return "-2" } }