package service import ( "errors" "fmt" "strconv" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "laic-backend/client" "laic-backend/common" "laic-backend/logger" "laic-backend/model" "laic-backend/tool" ) type simbossAdapter struct { client *client.SimbossClient } func InitSimboss(conf common.Simboss) error { if conf.AppID == "" && conf.Secret == "" { return nil } instance, err := client.NewSimbossClient(conf.AppID, conf.Secret, conf.APIBase) if err != nil { return err } DefaultSimRechargeService.simboss = &simbossAdapter{client: instance} return nil } type SimRechargeService struct { simboss *simbossAdapter } var DefaultSimRechargeService = &SimRechargeService{} func (s *SimRechargeService) GetCard(userID, cardID int64, refresh bool) (*model.SimCard, *common.BusiError) { var card model.SimCard if err := common.DB.Where("id = ? AND user_id = ?", cardID, userID).First(&card).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrSimCardNotFound } return nil, common.ErrInternal } if refresh { if s.simboss == nil { return nil, common.ErrCarrierUnavailable } if err := s.refreshCard(&card); err != nil { logger.WARN("刷新 SIM 卡信息失败", card.ID, err) return nil, common.ErrCarrierUnavailable } } return &card, nil } func (s *SimRechargeService) ListPackages() ([]model.SimPackage, *common.BusiError) { var packages []model.SimPackage if err := common.DB.Where("status = ?", "active").Order("sale_fee_fen ASC, id ASC").Find(&packages).Error; err != nil { return nil, common.ErrInternal } return packages, nil } func (s *SimRechargeService) CreateOrder(userID, cardID, packageID int64) (*model.SimRechargeOrder, *common.BusiError) { var order model.SimRechargeOrder now := time.Now() var busiErr *common.BusiError err := common.DB.Transaction(func(tx *gorm.DB) error { var card model.SimCard if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND user_id = ?", cardID, userID).First(&card).Error; err != nil { return err } var pending model.SimRechargeOrder if err := tx.Where("sim_card_id = ? AND payment_status IN ?", cardID, []string{"unpaid", "processing"}).First(&pending).Error; err == nil { busiErr = common.ErrPendingOrderExists return busiErr } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } if card.Status != "active" || card.CarrierStatus == "cancelled" || card.CarrierStatus == "suspended" || card.Iccid == "" { busiErr = common.ErrParam return busiErr } var pkg model.SimPackage if err := tx.Where("id = ? AND status = ?", packageID, "active").First(&pkg).Error; err != nil { return err } if pkg.ProviderRatePlanID <= 0 || pkg.AmountGb <= 0 || pkg.ValidityMonths <= 0 || pkg.SaleFeeFen <= 0 { busiErr = common.ErrParam return busiErr } if card.Carrier != "" && pkg.Carrier != "" && card.Carrier != pkg.Carrier { busiErr = common.ErrParam return busiErr } id, err := tool.NextID() if err != nil { return err } order = model.SimRechargeOrder{ID: id, UserID: userID, SimCardID: card.ID, PackageID: pkg.ID, PackageCode: pkg.Code, ProviderRatePlanID: pkg.ProviderRatePlanID, IccidSnapshot: card.Iccid, AmountGb: pkg.AmountGb, ValidityMonths: pkg.ValidityMonths, TotalFeeFen: pkg.SaleFeeFen, PaymentStatus: "unpaid", FulfillmentStatus: "pending", ExternalOrderNo: "sim-" + strconv.FormatInt(id, 10), CreatedAt: now, UpdatedAt: now} return tx.Create(&order).Error }) if err != nil { if busiErr != nil { return nil, busiErr } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrSimCardNotFound } if code, _ := common.ParseError(err); code == 1062 { return nil, common.ErrPendingOrderExists } logger.ERROR("创建 SIM 续费订单失败", err) return nil, common.ErrInternal } DefaultOperationLogService.RecordEvent(userID, "充值与履约", "创建SIM续费订单", fmt.Sprintf("orderId=%d simCardId=%d feeFen=%d", order.ID, order.SimCardID, order.TotalFeeFen), "success", "user_api") return &order, nil } func (s *SimRechargeService) CloseExpiredOrders(cutoff time.Time) { var orders []model.SimRechargeOrder if err := common.DB.Where("payment_status IN ? AND created_at <= ?", []string{"unpaid", "processing"}, cutoff).Limit(100).Find(&orders).Error; err != nil { logger.ERROR("扫描超时 SIM 订单失败", err) return } for i := range orders { now := time.Now() closed := false err := common.DB.Transaction(func(tx *gorm.DB) error { result := tx.Model(&model.SimRechargeOrder{}).Where("id = ? AND payment_status IN ? AND created_at <= ?", orders[i].ID, []string{"unpaid", "processing"}, cutoff).Updates(map[string]any{"payment_status": "closed", "closed_at": now, "updated_at": now}) if result.Error != nil { return result.Error } if result.RowsAffected != 1 { return nil } closed = true return tx.Model(&model.PaymentTransaction{}).Where("business_type = ? AND business_order_id = ? AND status IN ?", "sim_recharge_order", orders[i].ID, []string{"unpaid", "processing"}).Updates(map[string]any{"status": "closed", "updated_at": now}).Error }) if err != nil { logger.ERROR("关闭超时 SIM 订单失败", err) continue } if closed { DefaultOperationLogService.RecordEvent(orders[i].UserID, "充值与履约", "自动关闭SIM续费订单", fmt.Sprintf("orderId=%d status=closed", orders[i].ID), "success", "system") } } } func (s *SimRechargeService) CancelOrder(userID, orderID int64) *common.BusiError { now := time.Now() var busiErr *common.BusiError err := common.DB.Transaction(func(tx *gorm.DB) error { var order model.SimRechargeOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil { return err } if order.PaymentStatus != "unpaid" && order.PaymentStatus != "processing" { busiErr = common.ErrOrderNotCancellable return busiErr } result := tx.Model(&order).Where("id = ? AND payment_status IN ?", orderID, []string{"unpaid", "processing"}).Updates(map[string]any{"payment_status": "cancelled", "closed_at": now, "updated_at": now}) if result.Error != nil { return result.Error } if result.RowsAffected != 1 { busiErr = common.ErrOrderNotCancellable return busiErr } return tx.Model(&model.PaymentTransaction{}).Where("business_type = ? AND business_order_id = ? AND status IN ?", "sim_recharge_order", orderID, []string{"unpaid", "processing"}).Updates(map[string]any{"status": "cancelled", "updated_at": now}).Error }) if err != nil { if busiErr != nil { return busiErr } if errors.Is(err, gorm.ErrRecordNotFound) { return common.ErrOrderNotFound } logger.ERROR("取消 SIM 订单失败", err) return common.ErrInternal } DefaultOperationLogService.RecordEvent(userID, "充值与履约", "取消SIM续费订单", fmt.Sprintf("orderId=%d status=cancelled", orderID), "success", "user_api") return nil } func (s *SimRechargeService) GetOrder(userID, orderID int64) (*model.SimRechargeOrder, *common.BusiError) { var order model.SimRechargeOrder if err := common.DB.Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, common.ErrOrderNotFound } return nil, common.ErrInternal } return &order, nil } func (s *SimRechargeService) SubmitPaidOrderOnce(order *model.SimRechargeOrder) { const supportMessage = "续费处理异常,请联系管理员。" DefaultOperationLogService.RecordEvent(order.UserID, "充值与履约", "提交SIMBOSS履约", fmt.Sprintf("orderId=%d attempt=1", order.ID), "success", "simboss") if s.simboss == nil { DefaultOperationLogService.RecordEvent(order.UserID, "充值与履约", "SIMBOSS履约转人工处理", fmt.Sprintf("orderId=%d errorCode=simboss_unavailable", order.ID), "failed", "simboss") s.markManualReview(order.ID, "simboss_unavailable", supportMessage, errors.New("SIMBOSS is not configured")) return } providerOrderNo, err := s.simboss.client.Recharge(order.IccidSnapshot, order.ProviderRatePlanID, order.ValidityMonths, order.ExternalOrderNo) if err != nil { logger.ERROR("SIM 续费提交 SIMBOSS 失败", err) DefaultOperationLogService.RecordEvent(order.UserID, "充值与履约", "SIMBOSS履约转人工处理", fmt.Sprintf("orderId=%d errorCode=simboss_request_failed", order.ID), "failed", "simboss") s.markManualReview(order.ID, "simboss_request_failed", supportMessage, err) return } if providerOrderNo == "" { err = errors.New("SIMBOSS returned an empty order number") logger.ERROR("SIMBOSS 返回空订单号", err) DefaultOperationLogService.RecordEvent(order.UserID, "充值与履约", "SIMBOSS履约转人工处理", fmt.Sprintf("orderId=%d errorCode=simboss_empty_order_no", order.ID), "failed", "simboss") s.markManualReview(order.ID, "simboss_empty_order_no", supportMessage, err) return } now := time.Now() result := common.DB.Model(&model.SimRechargeOrder{}).Where("id = ? AND payment_status = ? AND fulfillment_status = ? AND attempt_count = ?", order.ID, "paid", "submitting", 1).Updates(map[string]any{ "fulfillment_status": "success", "provider_order_no": providerOrderNo, "fulfilled_at": now, "fulfillment_message": "", "last_error_code": "", "last_error_message": "", "updated_at": now, }) if result.Error != nil { logger.ERROR("更新 SIM 续费履约结果失败", result.Error) return } if result.RowsAffected != 1 { logger.WARN("SIM 续费履约状态已变化", order.ID, errors.New("stale fulfillment state")) } } func (s *SimRechargeService) markManualReview(orderID int64, code, message string, cause error) { now := time.Now() result := common.DB.Model(&model.SimRechargeOrder{}).Where("id = ? AND payment_status = ? AND fulfillment_status = ? AND attempt_count = ?", orderID, "paid", "submitting", 1).Updates(map[string]any{ "fulfillment_status": "manual_review", "fulfillment_message": message, "last_error_code": code, "last_error_message": cause.Error(), "next_attempt_at": nil, "fulfillment_lease_token": nil, "fulfillment_lease_until": nil, "updated_at": now, }) if result.Error != nil { logger.ERROR("标记 SIM 续费人工处理失败", result.Error) return } if result.RowsAffected != 1 { logger.WARN("SIM 续费人工处理状态已变化", orderID, errors.New("stale fulfillment state")) } } func (s *SimRechargeService) refreshCard(card *model.SimCard) error { detail, err := s.simboss.client.GetDeviceDetail(card.Iccid) if err != nil { return err } now := time.Now() updates := map[string]any{ "carrier": detail.Carrier, "carrier_status": normalizeCarrierStatus(detail.Status, detail.DeviceStatus), "used_gb": detail.DataUsage / 1024, "last_sync_at": now, "updated_at": now, } if detail.RatePlanExpirationDate != "" { if expiresAt, err := parseSimbossDate(detail.RatePlanExpirationDate); err == nil { updates["expired_at"] = expiresAt } } if updates["carrier_status"] == "cancelled" { updates["status"] = "expired" } if err := common.DB.Model(card).Updates(updates).Error; err != nil { return err } return common.DB.First(card, card.ID).Error } func parseSimbossDate(value string) (time.Time, error) { for _, layout := range []string{"2006-01-02", time.RFC3339, "2006-01-02 15:04:05"} { if parsed, err := time.ParseInLocation(layout, value, time.Local); err == nil { return parsed, nil } } return time.Time{}, errors.New("invalid SIMBOSS date") } func normalizeCarrierStatus(status, deviceStatus string) string { value := status + " " + deviceStatus switch { case containsFold(value, "cancel"): return "cancelled" case containsFold(value, "arrear"): return "arrears" case containsFold(value, "suspend"): return "suspended" default: return "normal" } } func containsFold(value, fragment string) bool { for i := 0; i+len(fragment) <= len(value); i++ { if equalFoldASCII(value[i:i+len(fragment)], fragment) { return true } } return false } func equalFoldASCII(value, fragment string) bool { for i := range value { left, right := value[i], fragment[i] if left >= 'A' && left <= 'Z' { left += 'a' - 'A' } if right >= 'A' && right <= 'Z' { right += 'a' - 'A' } if left != right { return false } } return true }