Files
junhong_cmp_fiber/internal/service/agent_open_api/service.go
2026-06-02 16:56:01 +08:00

626 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent_open_api
import (
"context"
"fmt"
"math/rand"
"strings"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
deviceSvc "github.com/break/junhong_cmp_fiber/internal/service/device"
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
orderSvc "github.com/break/junhong_cmp_fiber/internal/service/order"
packageSvc "github.com/break/junhong_cmp_fiber/internal/service/package"
shopCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_commission"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"gorm.io/gorm"
)
// Service 代理开放接口业务编排服务
type Service struct {
assetService *assetSvc.Service
packageService *packageSvc.Service
orderService *orderSvc.Service
shopCommissionService *shopCommissionSvc.Service
stopResumeService *iotCardSvc.StopResumeService
deviceService *deviceSvc.Service
iotCardStore *postgres.IotCardStore
packageUsageStore *postgres.PackageUsageStore
packageStore *postgres.PackageStore
packageSeriesStore *postgres.PackageSeriesStore
agentWalletStore *postgres.AgentWalletStore
}
// New 创建代理开放接口业务编排服务
func New(
assetService *assetSvc.Service,
packageService *packageSvc.Service,
orderService *orderSvc.Service,
shopCommissionService *shopCommissionSvc.Service,
stopResumeService *iotCardSvc.StopResumeService,
deviceService *deviceSvc.Service,
iotCardStore *postgres.IotCardStore,
packageUsageStore *postgres.PackageUsageStore,
packageStore *postgres.PackageStore,
packageSeriesStore *postgres.PackageSeriesStore,
agentWalletStore *postgres.AgentWalletStore,
) *Service {
return &Service{
assetService: assetService,
packageService: packageService,
orderService: orderService,
shopCommissionService: shopCommissionService,
stopResumeService: stopResumeService,
deviceService: deviceService,
iotCardStore: iotCardStore,
packageUsageStore: packageUsageStore,
packageStore: packageStore,
packageSeriesStore: packageSeriesStore,
agentWalletStore: agentWalletStore,
}
}
// GetCardTraffic 查询开放接口单卡流量
func (s *Service) GetCardTraffic(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPICardTrafficResponse, error) {
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
if err != nil {
return nil, err
}
activeStatus := constants.PackageUsageStatusActive
pendingStatus := constants.PackageUsageStatusPending
activeUsages, err := s.packageUsageStore.ListByCarrier(ctx, "iot_card", card.ID, &activeStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询生效套餐失败")
}
pendingUsages, err := s.packageUsageStore.ListByCarrier(ctx, "iot_card", card.ID, &pendingStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询待生效套餐失败")
}
packageMap, seriesMap, err := s.loadUsagePackageContext(ctx, append(activeUsages, pendingUsages...))
if err != nil {
return nil, err
}
resp := &dto.AgentOpenAPICardTrafficResponse{
CardNo: req.CardNo,
ActivePackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(activeUsages)),
PendingPackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(pendingUsages)),
}
for _, usage := range activeUsages {
item := s.buildTrafficItem(usage, packageMap, seriesMap, true)
resp.ActivePackages = append(resp.ActivePackages, item)
resp.ActiveTotalFlowMB += item.TotalFlowMB
resp.ActiveUsedFlowMB += item.UsedFlowMB
resp.ActiveRemainingFlowMB += item.RemainingFlowMB
if item.ExpiresAt != nil && (resp.ActiveExpiresAt == nil || item.ExpiresAt.After(*resp.ActiveExpiresAt)) {
resp.ActiveExpiresAt = item.ExpiresAt
}
}
for _, usage := range pendingUsages {
resp.PendingPackages = append(resp.PendingPackages, s.buildTrafficItem(usage, packageMap, seriesMap, false))
}
return resp, nil
}
// GetCardStatus 查询开放接口单卡状态
func (s *Service) GetCardStatus(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPICardStatusResponse, error) {
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
if err != nil {
return nil, err
}
cardStatus := constants.AgentOpenAPICardStatusStopped
stopReason := card.StopReason
if card.NetworkStatus == constants.NetworkStatusOnline {
cardStatus = constants.AgentOpenAPICardStatusNormal
stopReason = ""
}
return &dto.AgentOpenAPICardStatusResponse{
CardNo: req.CardNo,
CardStatus: cardStatus,
CardStatusName: constants.AgentOpenAPICardStatusName(cardStatus),
GatewayExtend: card.GatewayExtend,
StopReason: stopReason,
StopReasonName: constants.AgentOpenAPIStopReasonName(stopReason),
}, nil
}
// GetRealnameStatus 查询开放接口单卡实名状态
func (s *Service) GetRealnameStatus(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPIRealnameStatusResponse, error) {
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
if err != nil {
return nil, err
}
return &dto.AgentOpenAPIRealnameStatusResponse{
CardNo: req.CardNo,
IsRealnamed: card.RealNameStatus == constants.RealNameStatusVerified,
}, nil
}
// ResumeCard 调用上游复机机卡分离停机卡
func (s *Service) ResumeCard(ctx context.Context, req *dto.AgentOpenAPICardResumeRequest) (*dto.AgentOpenAPICardResumeResponse, error) {
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
if err != nil {
return nil, err
}
if s.stopResumeService == nil {
return nil, errors.New(errors.CodeInternalError, "开放接口复机依赖未配置")
}
if err := s.stopResumeService.StartMachineSeparatedCard(ctx, card); err != nil {
return nil, err
}
return &dto.AgentOpenAPICardResumeResponse{
CardNo: req.CardNo,
CardStatus: constants.AgentOpenAPICardStatusNormal,
CardStatusName: constants.AgentOpenAPICardStatusName(constants.AgentOpenAPICardStatusNormal),
GatewayExtend: "",
}, nil
}
// ListPackages 查询开放接口代理可购买套餐列表
func (s *Service) ListPackages(ctx context.Context, req *dto.AgentOpenAPIPackageListRequest) ([]*dto.AgentOpenAPIPackageItem, int64, int, int, error) {
status := constants.StatusEnabled
shelfStatus := constants.ShelfStatusOn
packageReq := &dto.PackageListRequest{
Page: req.Page,
PageSize: req.PageSize,
PackageName: req.PackageName,
SeriesID: req.SeriesID,
Status: &status,
ShelfStatus: &shelfStatus,
PackageType: req.PackageType,
}
packages, total, err := s.packageService.List(ctx, packageReq)
if err != nil {
return nil, 0, 0, 0, err
}
items := make([]*dto.AgentOpenAPIPackageItem, 0, len(packages))
for _, pkg := range packages {
items = append(items, &dto.AgentOpenAPIPackageItem{
PackageID: pkg.ID,
PackageCode: pkg.PackageCode,
PackageName: pkg.PackageName,
SeriesName: stringValue(pkg.SeriesName),
PackageType: pkg.PackageType,
PackageTypeName: constants.AgentOpenAPIPackageTypeName(pkg.PackageType),
CostPrice: pkg.CostPrice,
RetailPrice: int64Value(pkg.EffectiveRetailPrice),
TotalFlowMB: pkg.RealDataMB,
ValidDays: packageValidDays(pkg.DurationDays, pkg.DurationMonths),
})
}
page, pageSize := normalizePage(req.Page, req.PageSize)
return items, total, page, pageSize, nil
}
// GetWalletBalance 查询开放接口预充值主钱包余额
func (s *Service) GetWalletBalance(ctx context.Context) (*dto.AgentOpenAPIWalletBalanceResponse, error) {
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return nil, errors.New(errors.CodeUnauthorized)
}
wallet, err := s.agentWalletStore.GetMainWallet(ctx, shopID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return &dto.AgentOpenAPIWalletBalanceResponse{
Balance: 0,
FrozenBalance: 0,
AvailableBalance: 0,
Currency: constants.AgentOpenAPIWalletCurrencyCNY,
}, nil
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询主钱包余额失败")
}
return &dto.AgentOpenAPIWalletBalanceResponse{
Balance: wallet.Balance,
FrozenBalance: wallet.FrozenBalance,
AvailableBalance: wallet.GetAvailableBalance(),
Currency: walletCurrency(wallet.Currency),
}, nil
}
// ListWalletTransactions 查询开放接口预充值主钱包流水
func (s *Service) ListWalletTransactions(ctx context.Context, req *dto.AgentOpenAPIWalletTransactionListRequest) (*dto.MainWalletTransactionListResponse, error) {
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return nil, errors.New(errors.CodeUnauthorized)
}
return s.shopCommissionService.ListMainWalletTransactions(ctx, shopID, &dto.MainWalletTransactionListRequest{
Page: req.Page,
PageSize: req.PageSize,
TransactionType: req.TransactionType,
StartDate: req.StartDate,
EndDate: req.EndDate,
})
}
// CreateWalletPackageOrders 使用代理预充值钱包按卡购买套餐
func (s *Service) CreateWalletPackageOrders(ctx context.Context, req *dto.AgentOpenAPIWalletPackageOrderRequest) (*dto.AgentOpenAPIWalletPackageOrderResponse, error) {
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return nil, errors.New(errors.CodeUnauthorized)
}
pkg, err := s.packageStore.GetByCode(ctx, strings.TrimSpace(req.PackageCode))
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询套餐失败")
}
resp := &dto.AgentOpenAPIWalletPackageOrderResponse{
BatchNo: generateAgentOpenAPIBatchNo(),
PackageCode: pkg.PackageCode,
Orders: []dto.AgentOpenAPIWalletPackageOrderItem{},
FailedCards: []dto.AgentOpenAPIWalletPackageOrderFailure{},
}
for _, rawCardNo := range req.CardNos {
cardNo := strings.TrimSpace(rawCardNo)
if cardNo == "" {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(rawCardNo, errors.New(errors.CodeInvalidParam, "卡标识不能为空")))
continue
}
card, err := s.resolveOpenAPICard(ctx, cardNo)
if err != nil {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, err))
continue
}
orderResp, err := s.orderService.CreateAdminOrder(ctx, &dto.CreateAdminOrderRequest{
Identifier: card.ICCID,
PackageIDs: []uint{pkg.ID},
PaymentMethod: model.PaymentMethodWallet,
}, model.BuyerTypeAgent, shopID)
if err != nil {
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, err))
continue
}
resp.Orders = append(resp.Orders, dto.AgentOpenAPIWalletPackageOrderItem{
CardNo: cardNo,
OrderNo: orderResp.OrderNo,
})
}
resp.SuccessCount = len(resp.Orders)
resp.FailedCount = len(resp.FailedCards)
return resp, nil
}
// resolveOpenAPIDevice 将开放接口设备标识解析为当前代理可见的设备
// 设备不存在或不属于代理管辖店铺时统一返回 CodeForbidden避免信息泄露
func (s *Service) resolveOpenAPIDevice(ctx context.Context, deviceNo string) (*model.Device, error) {
identifier := strings.TrimSpace(deviceNo)
if identifier == "" {
return nil, errors.New(errors.CodeInvalidParam)
}
if s.deviceService == nil {
return nil, errors.New(errors.CodeInternalError, "开放接口设备解析依赖未配置")
}
device, err := s.deviceService.GetDeviceByIdentifier(ctx, identifier)
if err != nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
if device.ShopID == nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
subordinateIDs := middleware.GetSubordinateShopIDs(ctx)
found := false
for _, id := range subordinateIDs {
if id == *device.ShopID {
found = true
break
}
}
if !found {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return device, nil
}
// GetDeviceTraffic 查询开放接口设备套餐内流量
func (s *Service) GetDeviceTraffic(ctx context.Context, req *dto.AgentOpenAPIDeviceQueryRequest) (*dto.AgentOpenAPIDeviceTrafficResponse, error) {
device, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo)
if err != nil {
return nil, err
}
activeStatus := constants.PackageUsageStatusActive
pendingStatus := constants.PackageUsageStatusPending
activeUsages, err := s.packageUsageStore.ListByCarrier(ctx, "device", device.ID, &activeStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询生效套餐失败")
}
pendingUsages, err := s.packageUsageStore.ListByCarrier(ctx, "device", device.ID, &pendingStatus)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询待生效套餐失败")
}
packageMap, seriesMap, err := s.loadUsagePackageContext(ctx, append(activeUsages, pendingUsages...))
if err != nil {
return nil, err
}
resp := &dto.AgentOpenAPIDeviceTrafficResponse{
DeviceNo: req.DeviceNo,
ActivePackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(activeUsages)),
PendingPackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(pendingUsages)),
}
for _, usage := range activeUsages {
item := s.buildTrafficItem(usage, packageMap, seriesMap, true)
resp.ActivePackages = append(resp.ActivePackages, item)
resp.ActiveTotalFlowMB += item.TotalFlowMB
resp.ActiveUsedFlowMB += item.UsedFlowMB
resp.ActiveRemainingFlowMB += item.RemainingFlowMB
if item.ExpiresAt != nil && (resp.ActiveExpiresAt == nil || item.ExpiresAt.After(*resp.ActiveExpiresAt)) {
resp.ActiveExpiresAt = item.ExpiresAt
}
}
for _, usage := range pendingUsages {
resp.PendingPackages = append(resp.PendingPackages, s.buildTrafficItem(usage, packageMap, seriesMap, false))
}
return resp, nil
}
// SwitchDeviceCard 开放接口切网(多卡设备切换到指定 ICCID
func (s *Service) SwitchDeviceCard(ctx context.Context, req *dto.AgentOpenAPIDeviceSwitchCardRequest) error {
if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil {
return err
}
return s.deviceService.GatewaySwitchCard(ctx, req.DeviceNo, &dto.SwitchCardRequest{
TargetICCID: req.ICCID,
})
}
// RebootDevice 开放接口重启设备
func (s *Service) RebootDevice(ctx context.Context, req *dto.AgentOpenAPIDeviceOperationRequest) error {
if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil {
return err
}
return s.deviceService.GatewayRebootDevice(ctx, req.DeviceNo)
}
// ResetDevice 开放接口恢复出厂设置
func (s *Service) ResetDevice(ctx context.Context, req *dto.AgentOpenAPIDeviceOperationRequest) error {
if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil {
return err
}
return s.deviceService.GatewayResetDevice(ctx, req.DeviceNo)
}
// resolveOpenAPICard 将开放接口卡标识解析为当前代理可见的 IoT 卡
func (s *Service) resolveOpenAPICard(ctx context.Context, cardNo string) (*model.IotCard, error) {
identifier := strings.TrimSpace(cardNo)
if identifier == "" {
return nil, errors.New(errors.CodeInvalidParam)
}
if s.assetService == nil || s.iotCardStore == nil {
return nil, errors.New(errors.CodeInternalError, "开放接口资产解析依赖未配置")
}
resolved, err := s.assetService.Resolve(ctx, identifier)
if err != nil {
if appErr, ok := err.(*errors.AppError); ok && appErr.Code == errors.CodeNotFound {
return s.resolveOpenAPICardByIdentifier(ctx, identifier)
}
return nil, err
}
if resolved.AssetType != "card" {
return nil, errors.New(errors.CodeInvalidParam, "开放接口只支持单卡标识")
}
card, err := s.iotCardStore.GetByID(ctx, resolved.AssetID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
}
return card, nil
}
// resolveOpenAPICardByIdentifier 兜底支持 ICCID 双列、虚拟号和 MSISDN 精确解析
func (s *Service) resolveOpenAPICardByIdentifier(ctx context.Context, identifier string) (*model.IotCard, error) {
card, err := s.iotCardStore.GetByIdentifier(ctx, identifier)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
}
return card, nil
}
// loadUsagePackageContext 批量加载套餐和系列信息,避免单卡流量响应出现 N+1 查询
func (s *Service) loadUsagePackageContext(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, map[uint]string, error) {
packageIDs := make([]uint, 0)
packageIDSeen := make(map[uint]struct{})
for _, usage := range usages {
if usage.PackageID == 0 {
continue
}
if _, ok := packageIDSeen[usage.PackageID]; !ok {
packageIDSeen[usage.PackageID] = struct{}{}
packageIDs = append(packageIDs, usage.PackageID)
}
}
packages, err := s.packageStore.GetByIDsUnscoped(ctx, packageIDs)
if err != nil {
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "批量查询套餐失败")
}
packageMap := make(map[uint]*model.Package, len(packages))
seriesIDSeen := make(map[uint]struct{})
seriesIDs := make([]uint, 0)
for _, pkg := range packages {
packageMap[pkg.ID] = pkg
if pkg.SeriesID > 0 {
if _, ok := seriesIDSeen[pkg.SeriesID]; !ok {
seriesIDSeen[pkg.SeriesID] = struct{}{}
seriesIDs = append(seriesIDs, pkg.SeriesID)
}
}
}
seriesList, err := s.packageSeriesStore.GetByIDs(ctx, seriesIDs)
if err != nil {
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "批量查询套餐系列失败")
}
seriesMap := make(map[uint]string, len(seriesList))
for _, series := range seriesList {
seriesMap[series.ID] = series.SeriesName
}
return packageMap, seriesMap, nil
}
// buildTrafficItem 按开放接口口径构造套餐流量项,主动隐藏虚流量内部字段
func (s *Service) buildTrafficItem(usage *model.PackageUsage, packageMap map[uint]*model.Package, seriesMap map[uint]string, active bool) dto.AgentOpenAPIPackageTrafficItem {
pkg := packageMap[usage.PackageID]
packageCode := ""
packageName := usage.PackageName
packageType := ""
validDays := 0
seriesName := ""
if pkg != nil {
packageCode = pkg.PackageCode
if packageName == "" {
packageName = pkg.PackageName
}
packageType = pkg.PackageType
validDays = packageValidDays(nil, pkg.DurationMonths)
if pkg.DurationDays > 0 {
validDays = pkg.DurationDays
}
seriesName = seriesMap[pkg.SeriesID]
}
usedFlow := int64(0)
remainingFlow := int64(0)
if active {
usedFlow = visibleUsedFlowMB(usage)
remainingFlow = usage.DataLimitMB - usedFlow
if remainingFlow < 0 {
remainingFlow = 0
}
}
return dto.AgentOpenAPIPackageTrafficItem{
PackageCode: packageCode,
PackageName: packageName,
SeriesName: seriesName,
PackageType: packageType,
PackageTypeName: constants.AgentOpenAPIPackageTypeName(packageType),
TotalFlowMB: usage.DataLimitMB,
UsedFlowMB: usedFlow,
RemainingFlowMB: remainingFlow,
StartAt: usage.ActivatedAt,
ExpiresAt: usage.ExpiresAt,
ValidDays: validDays,
Priority: usage.Priority,
}
}
// visibleUsedFlowMB 计算对代理展示的已用流量,并限制不超过真实总量
func visibleUsedFlowMB(usage *model.PackageUsage) int64 {
ratio := usage.DisplayGainRatioSnapshot
if ratio <= 0 {
ratio = 1
}
used := int64(float64(usage.DataUsageMB) * ratio)
if used > usage.DataLimitMB {
return usage.DataLimitMB
}
if used < 0 {
return 0
}
return used
}
// packageValidDays 将套餐有效期统一折算为开放接口展示天数
func packageValidDays(durationDays *int, durationMonths int) int {
if durationDays != nil && *durationDays > 0 {
return *durationDays
}
if durationMonths > 0 {
return durationMonths * 30
}
return 0
}
func normalizePage(page, pageSize int) (int, int) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = constants.DefaultPageSize
}
if pageSize > constants.MaxPageSize {
pageSize = constants.MaxPageSize
}
return page, pageSize
}
func stringValue(value *string) string {
if value == nil {
return ""
}
return *value
}
func int64Value(value *int64) int64 {
if value == nil {
return 0
}
return *value
}
func walletCurrency(currency string) string {
if currency == "" {
return constants.AgentOpenAPIWalletCurrencyCNY
}
return currency
}
func buildFailedCard(cardNo string, err error) dto.AgentOpenAPIWalletPackageOrderFailure {
code := errors.CodeInternalError
message := errors.GetMessage(code, "zh-CN")
if appErr, ok := err.(*errors.AppError); ok {
code = appErr.Code
message = appErr.Message
}
return dto.AgentOpenAPIWalletPackageOrderFailure{
CardNo: cardNo,
Code: code,
Message: message,
}
}
func generateAgentOpenAPIBatchNo() string {
return fmt.Sprintf("%s%s%06d", constants.AgentOpenAPIBatchNoPrefix, time.Now().Format("20060102150405"), rand.Intn(1000000))
}