低空智控平台 后端go
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

324 lines
12 KiB

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
}