Files
junhong_cmp_fiber/internal/service/agent_open_api/service.go
huang 98ff88d5c3
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
开放接口,修复上游同步不对的问题
2026-05-11 11:20:48 +08:00

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))
}