All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
490 lines
16 KiB
Go
490 lines
16 KiB
Go
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"
|
|
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
|
|
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,
|
|
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,
|
|
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),
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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))
|
|
}
|