feat: 实现客户端核心业务接口(client-core-business-api)
新增客户端资产、钱包、订单、实名、设备管理等核心业务 Handler 与 DTO: - 客户端资产信息查询、套餐列表、套餐历史、资产刷新 - 客户端钱包详情、流水、充值校验、充值订单、充值记录 - 客户端订单创建、列表、详情 - 客户端实名认证链接获取 - 客户端设备卡列表、重启、恢复出厂、WiFi配置、切卡 - 客户端订单服务(含微信/支付宝支付流程) - 强充自动代购异步任务处理 - 数据库迁移 000084:充值记录增加自动代购状态字段
This commit is contained in:
556
internal/handler/app/client_asset.go
Normal file
556
internal/handler/app/client_asset.go
Normal file
@@ -0,0 +1,556 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"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/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ClientAssetHandler C 端资产信息处理器
|
||||
// 提供 B1~B4 资产信息、可购套餐、套餐历史、手动刷新接口
|
||||
type ClientAssetHandler struct {
|
||||
assetService *asset.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
assetWalletStore *postgres.AssetWalletStore
|
||||
packageStore *postgres.PackageStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewClientAssetHandler 创建 C 端资产信息处理器
|
||||
func NewClientAssetHandler(
|
||||
assetService *asset.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
assetWalletStore *postgres.AssetWalletStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
db *gorm.DB,
|
||||
logger *zap.Logger,
|
||||
) *ClientAssetHandler {
|
||||
return &ClientAssetHandler{
|
||||
assetService: assetService,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
assetWalletStore: assetWalletStore,
|
||||
packageStore: packageStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type resolvedAssetContext struct {
|
||||
CustomerID uint
|
||||
Identifier string
|
||||
Asset *dto.AssetResolveResponse
|
||||
Generation int
|
||||
WalletBalance int64
|
||||
SkipPermissionCtx context.Context
|
||||
IsAgentChannel bool
|
||||
SellerShopID uint
|
||||
MainPackageActived bool
|
||||
}
|
||||
|
||||
// resolveAssetFromIdentifier 统一执行资产解析与归属校验
|
||||
// 处理流程:客户鉴权 -> 标识符解析 -> 资产解析 -> 归属校验 -> 世代与钱包信息补齐
|
||||
func (h *ClientAssetHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier string) (*resolvedAssetContext, error) {
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
identifier = strings.TrimSpace(identifier)
|
||||
if identifier == "" {
|
||||
identifier = strings.TrimSpace(c.Query("identifier"))
|
||||
}
|
||||
if identifier == "" {
|
||||
var req dto.AssetRefreshRequest
|
||||
if err := c.BodyParser(&req); err == nil {
|
||||
identifier = strings.TrimSpace(req.Identifier)
|
||||
}
|
||||
}
|
||||
if identifier == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
skipPermissionCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
assetInfo, err := h.assetService.Resolve(skipPermissionCtx, identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owned, ownErr := h.isCustomerOwnAsset(skipPermissionCtx, customerID, assetInfo.VirtualNo)
|
||||
if ownErr != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
}
|
||||
if !owned {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||
}
|
||||
|
||||
generation, genErr := h.getAssetGeneration(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||||
if genErr != nil {
|
||||
return nil, genErr
|
||||
}
|
||||
|
||||
walletBalance, walletErr := h.getAssetWalletBalance(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||||
if walletErr != nil {
|
||||
return nil, walletErr
|
||||
}
|
||||
|
||||
ctxInfo := &resolvedAssetContext{
|
||||
CustomerID: customerID,
|
||||
Identifier: identifier,
|
||||
Asset: assetInfo,
|
||||
Generation: generation,
|
||||
WalletBalance: walletBalance,
|
||||
SkipPermissionCtx: skipPermissionCtx,
|
||||
}
|
||||
|
||||
if assetInfo.ShopID != nil && *assetInfo.ShopID > 0 {
|
||||
ctxInfo.IsAgentChannel = true
|
||||
ctxInfo.SellerShopID = *assetInfo.ShopID
|
||||
}
|
||||
|
||||
return ctxInfo, nil
|
||||
}
|
||||
|
||||
// GetAssetInfo B1 资产信息
|
||||
// GET /api/c/v1/asset/info
|
||||
func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
|
||||
var req dto.AssetInfoRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &dto.AssetInfoResponse{
|
||||
AssetType: resolved.Asset.AssetType,
|
||||
AssetID: resolved.Asset.AssetID,
|
||||
Identifier: resolved.Identifier,
|
||||
VirtualNo: resolved.Asset.VirtualNo,
|
||||
Status: resolved.Asset.Status,
|
||||
RealNameStatus: resolved.Asset.RealNameStatus,
|
||||
CarrierName: resolved.Asset.CarrierName,
|
||||
Generation: strconv.Itoa(resolved.Generation),
|
||||
WalletBalance: resolved.WalletBalance,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetAvailablePackages B2 资产可购套餐列表
|
||||
// GET /api/c/v1/asset/packages
|
||||
func (h *ClientAssetHandler) GetAvailablePackages(c *fiber.Ctx) error {
|
||||
var req dto.AssetPackageListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resolved.Asset.SeriesID == nil || *resolved.Asset.SeriesID == 0 {
|
||||
return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
|
||||
}
|
||||
|
||||
allUsages, err := h.assetService.GetPackages(resolved.SkipPermissionCtx, resolved.Asset.AssetType, resolved.Asset.AssetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolved.MainPackageActived = hasActiveMainPackage(allUsages)
|
||||
|
||||
listCtx := resolved.SkipPermissionCtx
|
||||
if resolved.IsAgentChannel {
|
||||
listCtx = context.WithValue(listCtx, constants.ContextKeyUserType, constants.UserTypeAgent)
|
||||
listCtx = context.WithValue(listCtx, constants.ContextKeyShopID, resolved.SellerShopID)
|
||||
}
|
||||
|
||||
pkgs, _, err := h.packageStore.List(listCtx, &store.QueryOptions{
|
||||
Page: 1,
|
||||
PageSize: constants.MaxPageSize,
|
||||
OrderBy: "id DESC",
|
||||
}, map[string]any{
|
||||
"series_id": *resolved.Asset.SeriesID,
|
||||
"status": constants.StatusEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询可购套餐失败")
|
||||
}
|
||||
|
||||
allocationMap := make(map[uint]*model.ShopPackageAllocation)
|
||||
if resolved.IsAgentChannel {
|
||||
packageIDs := collectPackageIDs(pkgs)
|
||||
allocations, allocErr := h.shopPackageAllocationStore.GetByShopAndPackages(
|
||||
resolved.SkipPermissionCtx,
|
||||
resolved.SellerShopID,
|
||||
packageIDs,
|
||||
)
|
||||
if allocErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, allocErr, "查询套餐分配记录失败")
|
||||
}
|
||||
for _, allocation := range allocations {
|
||||
allocationMap[allocation.PackageID] = allocation
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]dto.ClientPackageItem, 0, len(pkgs))
|
||||
for _, pkg := range pkgs {
|
||||
item, ok := buildClientPackageItem(pkg, resolved, allocationMap)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return errors.New(errors.CodeNoAvailablePackage, "当前无可购买套餐")
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].RetailPrice < items[j].RetailPrice
|
||||
})
|
||||
|
||||
return response.Success(c, &dto.AssetPackageListResponse{Packages: items})
|
||||
}
|
||||
|
||||
// GetPackageHistory B3 资产套餐历史
|
||||
// GET /api/c/v1/asset/package-history
|
||||
func (h *ClientAssetHandler) GetPackageHistory(c *fiber.Ctx) error {
|
||||
var req dto.AssetPackageHistoryRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if req.PageSize > constants.MaxPageSize {
|
||||
req.PageSize = constants.MaxPageSize
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := h.db.WithContext(resolved.SkipPermissionCtx).Model(&model.PackageUsage{}).
|
||||
Where("generation = ?", resolved.Generation)
|
||||
if resolved.Asset.AssetType == "card" {
|
||||
query = query.Where("iot_card_id = ?", resolved.Asset.AssetID)
|
||||
} else {
|
||||
query = query.Where("device_id = ?", resolved.Asset.AssetID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史总数失败")
|
||||
}
|
||||
|
||||
var usages []*model.PackageUsage
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&usages).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐历史失败")
|
||||
}
|
||||
|
||||
packageMap, err := h.loadPackageMap(resolved.SkipPermissionCtx, usages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list := make([]dto.AssetPackageResponse, 0, len(usages))
|
||||
for _, usage := range usages {
|
||||
pkg := packageMap[usage.PackageID]
|
||||
ratio := 1.0
|
||||
pkgName := ""
|
||||
pkgType := ""
|
||||
if pkg != nil {
|
||||
ratio = safeVirtualRatio(pkg.VirtualRatio)
|
||||
pkgName = pkg.PackageName
|
||||
pkgType = pkg.PackageType
|
||||
}
|
||||
|
||||
list = append(list, dto.AssetPackageResponse{
|
||||
PackageUsageID: usage.ID,
|
||||
PackageID: usage.PackageID,
|
||||
PackageName: pkgName,
|
||||
PackageType: pkgType,
|
||||
UsageType: usage.UsageType,
|
||||
Status: usage.Status,
|
||||
StatusName: packageStatusName(usage.Status),
|
||||
DataLimitMB: usage.DataLimitMB,
|
||||
VirtualLimitMB: int64(float64(usage.DataLimitMB) / ratio),
|
||||
DataUsageMB: usage.DataUsageMB,
|
||||
VirtualUsedMB: float64(usage.DataUsageMB) / ratio,
|
||||
VirtualRemainMB: float64(usage.DataLimitMB-usage.DataUsageMB) / ratio,
|
||||
VirtualRatio: ratio,
|
||||
ActivatedAt: nonZeroTimePtr(usage.ActivatedAt),
|
||||
ExpiresAt: nonZeroTimePtr(usage.ExpiresAt),
|
||||
MasterUsageID: usage.MasterUsageID,
|
||||
Priority: usage.Priority,
|
||||
CreatedAt: usage.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// RefreshAsset B4 资产刷新
|
||||
// POST /api/c/v1/asset/refresh
|
||||
func (h *ClientAssetHandler) RefreshAsset(c *fiber.Ctx) error {
|
||||
var req dto.AssetRefreshRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := h.assetService.Refresh(
|
||||
resolved.SkipPermissionCtx,
|
||||
resolved.Asset.AssetType,
|
||||
resolved.Asset.AssetID,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &dto.AssetRefreshResponse{
|
||||
RefreshType: resolved.Asset.AssetType,
|
||||
Accepted: true,
|
||||
CooldownSeconds: 0,
|
||||
}
|
||||
if resolved.Asset.AssetType == constants.ResourceTypeDevice {
|
||||
resp.CooldownSeconds = int(constants.DeviceRefreshCooldownDuration / time.Second)
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) isCustomerOwnAsset(ctx context.Context, customerID uint, virtualNo string) (bool, error) {
|
||||
records, err := h.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||
switch assetType {
|
||||
case "card":
|
||||
card, err := h.iotCardStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败")
|
||||
}
|
||||
return card.Generation, nil
|
||||
case "device":
|
||||
device, err := h.deviceStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败")
|
||||
}
|
||||
return device.Generation, nil
|
||||
default:
|
||||
return 0, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) getAssetWalletBalance(ctx context.Context, assetType string, assetID uint) (int64, error) {
|
||||
resourceType := constants.AssetWalletResourceTypeIotCard
|
||||
if assetType == constants.ResourceTypeDevice {
|
||||
resourceType = constants.AssetWalletResourceTypeDevice
|
||||
}
|
||||
|
||||
wallet, err := h.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
|
||||
}
|
||||
|
||||
return wallet.Balance, nil
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) loadPackageMap(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, error) {
|
||||
ids := make([]uint, 0, len(usages))
|
||||
seen := make(map[uint]struct{}, len(usages))
|
||||
for _, usage := range usages {
|
||||
if usage == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[usage.PackageID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[usage.PackageID] = struct{}{}
|
||||
ids = append(ids, usage.PackageID)
|
||||
}
|
||||
|
||||
packages, err := h.packageStore.GetByIDsUnscoped(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
result := make(map[uint]*model.Package, len(packages))
|
||||
for _, pkg := range packages {
|
||||
result[pkg.ID] = pkg
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func collectPackageIDs(pkgs []*model.Package) []uint {
|
||||
ids := make([]uint, 0, len(pkgs))
|
||||
for _, pkg := range pkgs {
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, pkg.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func hasActiveMainPackage(usages []*dto.AssetPackageResponse) bool {
|
||||
for _, usage := range usages {
|
||||
if usage == nil {
|
||||
continue
|
||||
}
|
||||
if usage.PackageType == constants.PackageTypeFormal && usage.Status == constants.PackageUsageStatusActive {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildClientPackageItem(
|
||||
pkg *model.Package,
|
||||
resolved *resolvedAssetContext,
|
||||
allocationMap map[uint]*model.ShopPackageAllocation,
|
||||
) (dto.ClientPackageItem, bool) {
|
||||
if pkg == nil || pkg.Status != constants.StatusEnabled {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
isAddon := pkg.PackageType == constants.PackageTypeAddon
|
||||
if isAddon && !resolved.MainPackageActived {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
retailPrice := pkg.SuggestedRetailPrice
|
||||
costPrice := pkg.CostPrice
|
||||
|
||||
if resolved.IsAgentChannel {
|
||||
allocation, ok := allocationMap[pkg.ID]
|
||||
if !ok || allocation == nil {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
if allocation.ShelfStatus != constants.ShelfStatusOn || allocation.Status != constants.StatusEnabled {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
retailPrice = allocation.RetailPrice
|
||||
costPrice = allocation.CostPrice
|
||||
} else if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
if retailPrice < costPrice {
|
||||
return dto.ClientPackageItem{}, false
|
||||
}
|
||||
|
||||
validityDays := pkg.DurationDays
|
||||
if validityDays <= 0 && pkg.DurationMonths > 0 {
|
||||
validityDays = pkg.DurationMonths * 30
|
||||
}
|
||||
|
||||
dataAllowance := pkg.VirtualDataMB
|
||||
if dataAllowance <= 0 {
|
||||
dataAllowance = pkg.RealDataMB
|
||||
}
|
||||
|
||||
return dto.ClientPackageItem{
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
PackageType: pkg.PackageType,
|
||||
RetailPrice: retailPrice,
|
||||
CostPrice: costPrice,
|
||||
ValidityDays: validityDays,
|
||||
IsAddon: isAddon,
|
||||
DataAllowance: dataAllowance,
|
||||
DataUnit: "MB",
|
||||
Description: pkg.PackageCode,
|
||||
}, true
|
||||
}
|
||||
|
||||
func nonZeroTimePtr(t time.Time) *time.Time {
|
||||
if t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &t
|
||||
}
|
||||
|
||||
func safeVirtualRatio(ratio float64) float64 {
|
||||
if ratio <= 0 {
|
||||
return 1.0
|
||||
}
|
||||
return ratio
|
||||
}
|
||||
|
||||
func packageStatusName(status int) string {
|
||||
switch status {
|
||||
case constants.PackageUsageStatusPending:
|
||||
return "待生效"
|
||||
case constants.PackageUsageStatusActive:
|
||||
return "生效中"
|
||||
case constants.PackageUsageStatusDepleted:
|
||||
return "已用完"
|
||||
case constants.PackageUsageStatusExpired:
|
||||
return "已过期"
|
||||
case constants.PackageUsageStatusInvalidated:
|
||||
return "已失效"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
317
internal/handler/app/client_device.go
Normal file
317
internal/handler/app/client_device.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var clientDeviceValidator = validator.New()
|
||||
|
||||
// deviceAssetInfo validateDeviceAsset 解析后的设备资产信息
|
||||
type deviceAssetInfo struct {
|
||||
DeviceID uint // 设备数据库 ID
|
||||
IMEI string // 设备 IMEI(用于 Gateway API 调用)
|
||||
VirtualNo string // 设备虚拟号(用于所有权校验)
|
||||
}
|
||||
|
||||
// ClientDeviceHandler C 端设备能力处理器
|
||||
// 提供设备卡列表、重启、恢复出厂、WiFi 配置、切卡等操作
|
||||
type ClientDeviceHandler struct {
|
||||
assetService *assetSvc.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewClientDeviceHandler 创建 C 端设备能力处理器
|
||||
func NewClientDeviceHandler(
|
||||
assetService *assetSvc.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
gatewayClient *gateway.Client,
|
||||
logger *zap.Logger,
|
||||
) *ClientDeviceHandler {
|
||||
return &ClientDeviceHandler{
|
||||
assetService: assetService,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
deviceStore: deviceStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
iotCardStore: iotCardStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// validateDeviceAsset 校验设备资产的所有权和有效性
|
||||
// 流程:认证 → 资产解析 → 类型校验(仅设备)→ 所有权校验 → IMEI 校验
|
||||
func (h *ClientDeviceHandler) validateDeviceAsset(c *fiber.Ctx, identifier string) (*deviceAssetInfo, error) {
|
||||
// 获取当前登录的个人客户 ID
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 通过标识符解析资产
|
||||
asset, err := h.assetService.Resolve(ctx, identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 仅设备资产支持设备能力操作
|
||||
if asset.AssetType != "device" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "仅设备资产支持该操作")
|
||||
}
|
||||
|
||||
// 校验个人客户对该设备的所有权
|
||||
owns, err := h.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, asset.VirtualNo)
|
||||
if err != nil {
|
||||
h.logger.Error("校验设备所有权失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("virtual_no", asset.VirtualNo),
|
||||
zap.Error(err))
|
||||
return nil, errors.New(errors.CodeInternalError)
|
||||
}
|
||||
if !owns {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
|
||||
// 校验设备 IMEI 是否存在(Gateway API 调用必需)
|
||||
if asset.IMEI == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "设备IMEI缺失")
|
||||
}
|
||||
|
||||
return &deviceAssetInfo{
|
||||
DeviceID: asset.AssetID,
|
||||
IMEI: asset.IMEI,
|
||||
VirtualNo: asset.VirtualNo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDeviceCards F1 获取设备卡列表
|
||||
// GET /api/c/v1/device/cards
|
||||
func (h *ClientDeviceHandler) GetDeviceCards(c *fiber.Ctx) error {
|
||||
var req dto.DeviceCardListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := clientDeviceValidator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("设备卡列表参数校验失败", zap.Error(err))
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
info, err := h.validateDeviceAsset(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 查询设备绑定的所有 SIM 卡
|
||||
bindings, err := h.deviceSimBindingStore.ListByDeviceID(ctx, info.DeviceID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询设备SIM绑定失败",
|
||||
zap.Uint("device_id", info.DeviceID),
|
||||
zap.Error(err))
|
||||
return errors.New(errors.CodeInternalError)
|
||||
}
|
||||
|
||||
// 无绑定卡时返回空列表
|
||||
if len(bindings) == 0 {
|
||||
return response.Success(c, &dto.DeviceCardListResponse{Cards: []dto.DeviceCardItem{}})
|
||||
}
|
||||
|
||||
// 收集卡 ID 并记录插槽位置映射
|
||||
cardIDs := make([]uint, 0, len(bindings))
|
||||
slotMap := make(map[uint]int, len(bindings))
|
||||
for _, b := range bindings {
|
||||
cardIDs = append(cardIDs, b.IotCardID)
|
||||
slotMap[b.IotCardID] = b.SlotPosition
|
||||
}
|
||||
|
||||
// 批量查询卡详情
|
||||
cards, err := h.iotCardStore.GetByIDs(ctx, cardIDs)
|
||||
if err != nil {
|
||||
h.logger.Error("批量查询IoT卡失败",
|
||||
zap.Uints("card_ids", cardIDs),
|
||||
zap.Error(err))
|
||||
return errors.New(errors.CodeInternalError)
|
||||
}
|
||||
|
||||
// 组装响应,slot_position == 1 视为当前激活卡
|
||||
items := make([]dto.DeviceCardItem, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
slot := slotMap[card.ID]
|
||||
items = append(items, dto.DeviceCardItem{
|
||||
CardID: card.ID,
|
||||
ICCID: card.ICCID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierName: card.CarrierName,
|
||||
NetworkStatus: networkStatusText(card.NetworkStatus),
|
||||
RealNameStatus: card.RealNameStatus,
|
||||
SlotPosition: slot,
|
||||
IsActive: slot == 1,
|
||||
})
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.DeviceCardListResponse{Cards: items})
|
||||
}
|
||||
|
||||
// RebootDevice F2 设备重启
|
||||
// POST /api/c/v1/device/reboot
|
||||
func (h *ClientDeviceHandler) RebootDevice(c *fiber.Ctx) error {
|
||||
var req dto.DeviceRebootRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := clientDeviceValidator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("设备重启参数校验失败", zap.Error(err))
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
info, err := h.validateDeviceAsset(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 重启设备
|
||||
if err := h.gatewayClient.RebootDevice(c.UserContext(), &gateway.DeviceOperationReq{
|
||||
DeviceID: info.IMEI,
|
||||
}); err != nil {
|
||||
h.logger.Error("Gateway重启设备失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
zap.Error(err))
|
||||
return errors.Wrap(errors.CodeGatewayError, err, "设备重启失败")
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.DeviceOperationResponse{Accepted: true})
|
||||
}
|
||||
|
||||
// FactoryResetDevice F3 恢复出厂设置
|
||||
// POST /api/c/v1/device/factory-reset
|
||||
func (h *ClientDeviceHandler) FactoryResetDevice(c *fiber.Ctx) error {
|
||||
var req dto.DeviceFactoryResetRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := clientDeviceValidator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("恢复出厂设置参数校验失败", zap.Error(err))
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
info, err := h.validateDeviceAsset(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 恢复出厂设置
|
||||
if err := h.gatewayClient.ResetDevice(c.UserContext(), &gateway.DeviceOperationReq{
|
||||
DeviceID: info.IMEI,
|
||||
}); err != nil {
|
||||
h.logger.Error("Gateway恢复出厂设置失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
zap.Error(err))
|
||||
return errors.Wrap(errors.CodeGatewayError, err, "恢复出厂设置失败")
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.DeviceOperationResponse{Accepted: true})
|
||||
}
|
||||
|
||||
// SetWiFi F4 设备WiFi配置
|
||||
// POST /api/c/v1/device/wifi
|
||||
// 注意:WiFiReq.CardNo 字段名具有误导性,实际传入的是设备 IMEI,而非卡号
|
||||
func (h *ClientDeviceHandler) SetWiFi(c *fiber.Ctx) error {
|
||||
var req dto.DeviceWifiRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := clientDeviceValidator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("WiFi配置参数校验失败", zap.Error(err))
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
info, err := h.validateDeviceAsset(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 配置 WiFi
|
||||
// CardNo 字段虽名为"卡号",但 Gateway 实际要求传入设备 IMEI
|
||||
if err := h.gatewayClient.SetWiFi(c.UserContext(), &gateway.WiFiReq{
|
||||
CardNo: info.IMEI,
|
||||
DeviceID: info.IMEI,
|
||||
SSID: req.SSID,
|
||||
Password: req.Password,
|
||||
Enabled: req.Enabled,
|
||||
}); err != nil {
|
||||
h.logger.Error("Gateway配置WiFi失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
zap.String("ssid", req.SSID),
|
||||
zap.Error(err))
|
||||
return errors.Wrap(errors.CodeGatewayError, err, "WiFi配置失败")
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.DeviceOperationResponse{Accepted: true})
|
||||
}
|
||||
|
||||
// SwitchCard F5 设备切卡
|
||||
// POST /api/c/v1/device/switch-card
|
||||
func (h *ClientDeviceHandler) SwitchCard(c *fiber.Ctx) error {
|
||||
var req dto.DeviceSwitchCardRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := clientDeviceValidator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("设备切卡参数校验失败", zap.Error(err))
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
info, err := h.validateDeviceAsset(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 切卡,CardNo 传设备 IMEI
|
||||
if err := h.gatewayClient.SwitchCard(c.UserContext(), &gateway.SwitchCardReq{
|
||||
CardNo: info.IMEI,
|
||||
ICCID: req.TargetICCID,
|
||||
}); err != nil {
|
||||
h.logger.Error("Gateway切卡失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
zap.String("target_iccid", req.TargetICCID),
|
||||
zap.Error(err))
|
||||
return errors.Wrap(errors.CodeGatewayError, err, "设备切卡失败")
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.DeviceSwitchCardResponse{
|
||||
Accepted: true,
|
||||
TargetICCID: req.TargetICCID,
|
||||
})
|
||||
}
|
||||
|
||||
// networkStatusText 将网络状态码转为文本描述
|
||||
func networkStatusText(status int) string {
|
||||
switch status {
|
||||
case 0:
|
||||
return "停机"
|
||||
case 1:
|
||||
return "开机"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
415
internal/handler/app/client_order.go
Normal file
415
internal/handler/app/client_order.go
Normal file
@@ -0,0 +1,415 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
clientorder "github.com/break/junhong_cmp_fiber/internal/service/client_order"
|
||||
"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/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ClientOrderHandler C 端订单处理器
|
||||
// 提供 D1~D3 下单、列表、详情接口。
|
||||
type ClientOrderHandler struct {
|
||||
clientOrderService *clientorder.Service
|
||||
assetService *asset.Service
|
||||
orderStore *postgres.OrderStore
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewClientOrderHandler 创建 C 端订单处理器。
|
||||
func NewClientOrderHandler(
|
||||
clientOrderService *clientorder.Service,
|
||||
assetService *asset.Service,
|
||||
orderStore *postgres.OrderStore,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
logger *zap.Logger,
|
||||
db *gorm.DB,
|
||||
) *ClientOrderHandler {
|
||||
return &ClientOrderHandler{
|
||||
clientOrderService: clientOrderService,
|
||||
assetService: assetService,
|
||||
orderStore: orderStore,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
logger: logger,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateOrder D1 创建订单。
|
||||
// POST /api/c/v1/orders/create
|
||||
func (h *ClientOrderHandler) CreateOrder(c *fiber.Ctx) error {
|
||||
var req dto.ClientCreateOrderRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
resp, err := h.clientOrderService.CreateOrder(c.UserContext(), customerID, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// ListOrders D2 订单列表。
|
||||
// GET /api/c/v1/orders
|
||||
func (h *ClientOrderHandler) ListOrders(c *fiber.Ctx) error {
|
||||
var req dto.ClientOrderListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if req.PageSize > constants.MaxPageSize {
|
||||
req.PageSize = constants.MaxPageSize
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := h.db.WithContext(resolved.SkipPermissionCtx).
|
||||
Model(&model.Order{}).
|
||||
Where("generation = ?", resolved.Generation)
|
||||
|
||||
if resolved.Asset.AssetType == constants.ResourceTypeDevice {
|
||||
query = query.Where("order_type = ? AND device_id = ?", model.OrderTypeDevice, resolved.Asset.AssetID)
|
||||
} else {
|
||||
query = query.Where("order_type = ? AND iot_card_id = ?", model.OrderTypeSingleCard, resolved.Asset.AssetID)
|
||||
}
|
||||
|
||||
if req.PaymentStatus != nil {
|
||||
paymentStatus, ok := clientPaymentStatusToOrderStatus(*req.PaymentStatus)
|
||||
if !ok {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
query = query.Where("payment_status = ?", paymentStatus)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单总数失败")
|
||||
}
|
||||
|
||||
var orders []*model.Order
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&orders).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单列表失败")
|
||||
}
|
||||
|
||||
orderIDs := make([]uint, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
if order == nil {
|
||||
continue
|
||||
}
|
||||
orderIDs = append(orderIDs, order.ID)
|
||||
}
|
||||
|
||||
itemMap, err := h.loadOrderItemMap(resolved.SkipPermissionCtx, orderIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list := make([]dto.ClientOrderListItem, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
if order == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
packageNames := make([]string, 0, len(itemMap[order.ID]))
|
||||
for _, item := range itemMap[order.ID] {
|
||||
if item == nil || item.PackageName == "" {
|
||||
continue
|
||||
}
|
||||
packageNames = append(packageNames, item.PackageName)
|
||||
}
|
||||
|
||||
list = append(list, dto.ClientOrderListItem{
|
||||
OrderID: order.ID,
|
||||
OrderNo: order.OrderNo,
|
||||
TotalAmount: order.TotalAmount,
|
||||
PaymentStatus: orderStatusToClientPaymentStatus(order.PaymentStatus),
|
||||
CreatedAt: formatClientOrderTime(order.CreatedAt),
|
||||
PackageNames: packageNames,
|
||||
})
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, list, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// GetOrderDetail D3 订单详情。
|
||||
// GET /api/c/v1/orders/:id
|
||||
func (h *ClientOrderHandler) GetOrderDetail(c *fiber.Ctx) error {
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
orderID, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || orderID == 0 {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
order, items, err := h.orderStore.GetByIDWithItems(c.UserContext(), uint(orderID))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单详情失败")
|
||||
}
|
||||
|
||||
virtualNo, err := h.getOrderVirtualNo(c.UserContext(), order)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
owned, ownErr := h.isCustomerOwnAsset(c.UserContext(), customerID, virtualNo)
|
||||
if ownErr != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
}
|
||||
if !owned {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||
}
|
||||
|
||||
packages := make([]dto.ClientOrderPackageItem, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
packages = append(packages, dto.ClientOrderPackageItem{
|
||||
PackageID: item.PackageID,
|
||||
PackageName: item.PackageName,
|
||||
Price: item.UnitPrice,
|
||||
Quantity: item.Quantity,
|
||||
})
|
||||
}
|
||||
|
||||
resp := &dto.ClientOrderDetailResponse{
|
||||
OrderID: order.ID,
|
||||
OrderNo: order.OrderNo,
|
||||
TotalAmount: order.TotalAmount,
|
||||
PaymentStatus: orderStatusToClientPaymentStatus(order.PaymentStatus),
|
||||
PaymentMethod: order.PaymentMethod,
|
||||
CreatedAt: formatClientOrderTime(order.CreatedAt),
|
||||
PaidAt: formatClientOrderTimePtr(order.PaidAt),
|
||||
CompletedAt: nil,
|
||||
Packages: packages,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *ClientOrderHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier string) (*resolvedAssetContext, error) {
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
identifier = strings.TrimSpace(identifier)
|
||||
if identifier == "" {
|
||||
identifier = strings.TrimSpace(c.Query("identifier"))
|
||||
}
|
||||
if identifier == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
skipPermissionCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
assetInfo, err := h.assetService.Resolve(skipPermissionCtx, identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owned, ownErr := h.isCustomerOwnAsset(skipPermissionCtx, customerID, assetInfo.VirtualNo)
|
||||
if ownErr != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
}
|
||||
if !owned {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||
}
|
||||
|
||||
generation, genErr := h.getAssetGeneration(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||||
if genErr != nil {
|
||||
return nil, genErr
|
||||
}
|
||||
|
||||
return &resolvedAssetContext{
|
||||
CustomerID: customerID,
|
||||
Identifier: identifier,
|
||||
Asset: assetInfo,
|
||||
Generation: generation,
|
||||
SkipPermissionCtx: skipPermissionCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ClientOrderHandler) isCustomerOwnAsset(ctx context.Context, customerID uint, virtualNo string) (bool, error) {
|
||||
records, err := h.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (h *ClientOrderHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||
switch assetType {
|
||||
case "card":
|
||||
card, err := h.iotCardStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败")
|
||||
}
|
||||
return card.Generation, nil
|
||||
case constants.ResourceTypeDevice:
|
||||
device, err := h.deviceStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败")
|
||||
}
|
||||
return device.Generation, nil
|
||||
default:
|
||||
return 0, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ClientOrderHandler) loadOrderItemMap(ctx context.Context, orderIDs []uint) (map[uint][]*model.OrderItem, error) {
|
||||
result := make(map[uint][]*model.OrderItem)
|
||||
if len(orderIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var items []*model.OrderItem
|
||||
if err := h.db.WithContext(ctx).Where("order_id IN ?", orderIDs).Order("id ASC").Find(&items).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询订单明细失败")
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
result[item.OrderID] = append(result[item.OrderID], item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *ClientOrderHandler) getOrderVirtualNo(ctx context.Context, order *model.Order) (string, error) {
|
||||
if order == nil {
|
||||
return "", errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
|
||||
switch order.OrderType {
|
||||
case model.OrderTypeSingleCard:
|
||||
if order.IotCardID == nil || *order.IotCardID == 0 {
|
||||
return "", errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
card, err := h.iotCardStore.GetByID(ctx, *order.IotCardID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return "", errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return "", errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败")
|
||||
}
|
||||
return card.VirtualNo, nil
|
||||
case model.OrderTypeDevice:
|
||||
if order.DeviceID == nil || *order.DeviceID == 0 {
|
||||
return "", errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
device, err := h.deviceStore.GetByID(ctx, *order.DeviceID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return "", errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return "", errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败")
|
||||
}
|
||||
return device.VirtualNo, nil
|
||||
default:
|
||||
return "", errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func orderStatusToClientPaymentStatus(status int) int {
|
||||
switch status {
|
||||
case model.PaymentStatusPending:
|
||||
return 0
|
||||
case model.PaymentStatusPaid:
|
||||
return 1
|
||||
case model.PaymentStatusCancelled:
|
||||
return 2
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
func clientPaymentStatusToOrderStatus(status int) (int, bool) {
|
||||
switch status {
|
||||
case 0:
|
||||
return model.PaymentStatusPending, true
|
||||
case 1:
|
||||
return model.PaymentStatusPaid, true
|
||||
case 2:
|
||||
return model.PaymentStatusCancelled, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func formatClientOrderTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func formatClientOrderTimePtr(t *time.Time) *string {
|
||||
if t == nil || t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
formatted := formatClientOrderTime(*t)
|
||||
return &formatted
|
||||
}
|
||||
249
internal/handler/app/client_realname.go
Normal file
249
internal/handler/app/client_realname.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
assetService "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
"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/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var clientRealnameValidator = validator.New()
|
||||
|
||||
// ClientRealnameHandler C 端实名认证处理器
|
||||
type ClientRealnameHandler struct {
|
||||
assetService *assetService.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
carrierStore *postgres.CarrierStore
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewClientRealnameHandler 创建 C 端实名认证处理器
|
||||
func NewClientRealnameHandler(
|
||||
assetSvc *assetService.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore,
|
||||
carrierStore *postgres.CarrierStore,
|
||||
gatewayClient *gateway.Client,
|
||||
logger *zap.Logger,
|
||||
) *ClientRealnameHandler {
|
||||
return &ClientRealnameHandler{
|
||||
assetService: assetSvc,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
carrierStore: carrierStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRealnameLink E1 获取实名认证链接
|
||||
// GET /api/c/v1/realname/link
|
||||
func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
||||
// 1. 获取当前登录客户
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
// 2. 解析请求参数
|
||||
var req dto.RealnimeLinkRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := clientRealnameValidator.Struct(&req); err != nil {
|
||||
logger.GetAppLogger().Warn("实名链接参数校验失败", zap.Error(err))
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 3. 通过标识符解析资产
|
||||
asset, err := h.assetService.Resolve(ctx, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. 验证资产归属(个人客户必须绑定过该资产)
|
||||
owned, err := h.personalDeviceStore.ExistsByCustomerAndDevice(ctx, customerID, asset.VirtualNo)
|
||||
if err != nil {
|
||||
logger.GetAppLogger().Error("查询资产归属失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("virtual_no", asset.VirtualNo),
|
||||
zap.Error(err))
|
||||
return errors.New(errors.CodeInternalError, "查询资产归属失败")
|
||||
}
|
||||
if !owned {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
|
||||
// 5. 定位目标卡(3 条路径)
|
||||
var targetCard *model.IotCard
|
||||
switch {
|
||||
case asset.AssetType == "card":
|
||||
// 路径 1:资产本身就是卡,直接使用
|
||||
card, cardErr := h.iotCardStore.GetByID(ctx, asset.AssetID)
|
||||
if cardErr != nil {
|
||||
return errors.New(errors.CodeIotCardNotFound, "卡信息查询失败")
|
||||
}
|
||||
targetCard = card
|
||||
|
||||
case asset.AssetType == "device" && req.ICCID != "":
|
||||
// 路径 2:资产是设备,指定了 ICCID,从设备绑定中查找该卡
|
||||
card, cardErr := h.findCardInDeviceBindings(c, asset.AssetID, req.ICCID)
|
||||
if cardErr != nil {
|
||||
return cardErr
|
||||
}
|
||||
targetCard = card
|
||||
|
||||
case asset.AssetType == "device":
|
||||
// 路径 3:资产是设备,未指定 ICCID,取第一张绑定卡(按插槽位置排序)
|
||||
card, cardErr := h.findFirstBoundCard(c, asset.AssetID)
|
||||
if cardErr != nil {
|
||||
return cardErr
|
||||
}
|
||||
targetCard = card
|
||||
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "不支持的资产类型")
|
||||
}
|
||||
|
||||
// 6. 检查实名状态
|
||||
if targetCard.RealNameStatus == 1 {
|
||||
return errors.New(errors.CodeInvalidStatus, "该卡已完成实名")
|
||||
}
|
||||
|
||||
// 7. 获取运营商信息,根据实名链接类型生成 URL
|
||||
carrier, err := h.carrierStore.GetByID(ctx, targetCard.CarrierID)
|
||||
if err != nil {
|
||||
logger.GetAppLogger().Error("查询运营商失败",
|
||||
zap.Uint("carrier_id", targetCard.CarrierID),
|
||||
zap.Error(err))
|
||||
return errors.New(errors.CodeCarrierNotFound, "运营商信息查询失败")
|
||||
}
|
||||
|
||||
resp := &dto.RealnimeLinkResponse{
|
||||
CardInfo: dto.CardInfoBrief{
|
||||
ICCID: targetCard.ICCID,
|
||||
MSISDN: targetCard.MSISDN,
|
||||
VirtualNo: targetCard.VirtualNo,
|
||||
},
|
||||
}
|
||||
|
||||
switch carrier.RealnameLinkType {
|
||||
case constants.RealnameLinkTypeNone:
|
||||
// 该运营商不支持在线实名
|
||||
return errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名")
|
||||
|
||||
case constants.RealnameLinkTypeTemplate:
|
||||
// 模板模式:替换占位符生成实名链接
|
||||
url := carrier.RealnameLinkTemplate
|
||||
url = strings.ReplaceAll(url, "{iccid}", targetCard.ICCID)
|
||||
url = strings.ReplaceAll(url, "{msisdn}", targetCard.MSISDN)
|
||||
url = strings.ReplaceAll(url, "{virtual_no}", targetCard.VirtualNo)
|
||||
resp.RealnameMode = constants.RealnameLinkTypeTemplate
|
||||
resp.RealnameURL = url
|
||||
|
||||
case constants.RealnameLinkTypeGateway:
|
||||
// 网关模式:调用 Gateway 接口获取实名链接
|
||||
linkResp, gwErr := h.gatewayClient.GetRealnameLink(ctx, &gateway.CardStatusReq{
|
||||
CardNo: targetCard.ICCID,
|
||||
})
|
||||
if gwErr != nil {
|
||||
logger.GetAppLogger().Error("Gateway 获取实名链接失败",
|
||||
zap.String("iccid", targetCard.ICCID),
|
||||
zap.Error(gwErr))
|
||||
return errors.Wrap(errors.CodeGatewayError, gwErr, "获取实名链接失败")
|
||||
}
|
||||
resp.RealnameMode = constants.RealnameLinkTypeGateway
|
||||
resp.RealnameURL = linkResp.URL
|
||||
|
||||
default:
|
||||
logger.GetAppLogger().Warn("未知的实名链接类型",
|
||||
zap.Uint("carrier_id", carrier.ID),
|
||||
zap.String("realname_link_type", carrier.RealnameLinkType))
|
||||
return errors.New(errors.CodeInvalidStatus, "该运营商暂不支持在线实名")
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// findCardInDeviceBindings 在设备绑定中查找指定 ICCID 的卡
|
||||
func (h *ClientRealnameHandler) findCardInDeviceBindings(c *fiber.Ctx, deviceID uint, iccid string) (*model.IotCard, error) {
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 查询设备的所有有效绑定
|
||||
bindings, err := h.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
|
||||
if err != nil {
|
||||
logger.GetAppLogger().Error("查询设备绑定失败",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Error(err))
|
||||
return nil, errors.New(errors.CodeInternalError, "查询设备绑定失败")
|
||||
}
|
||||
|
||||
// 收集所有绑定卡的 ID
|
||||
cardIDs := make([]uint, 0, len(bindings))
|
||||
for _, b := range bindings {
|
||||
cardIDs = append(cardIDs, b.IotCardID)
|
||||
}
|
||||
|
||||
if len(cardIDs) == 0 {
|
||||
return nil, errors.New(errors.CodeIotCardNotFound, "该设备未绑定任何卡")
|
||||
}
|
||||
|
||||
// 批量查询卡,匹配指定的 ICCID
|
||||
cards, err := h.iotCardStore.GetByIDs(ctx, cardIDs)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "查询卡信息失败")
|
||||
}
|
||||
|
||||
for _, card := range cards {
|
||||
if card.ICCID == iccid {
|
||||
return card, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New(errors.CodeIotCardNotFound, "该设备未绑定指定的 ICCID")
|
||||
}
|
||||
|
||||
// findFirstBoundCard 获取设备第一张绑定卡(按插槽位置排序,取第一张)
|
||||
func (h *ClientRealnameHandler) findFirstBoundCard(c *fiber.Ctx, deviceID uint) (*model.IotCard, error) {
|
||||
ctx := c.UserContext()
|
||||
|
||||
// ListByDeviceID 返回 bind_status=1 的绑定,按 slot_position ASC 排序
|
||||
bindings, err := h.deviceSimBindingStore.ListByDeviceID(ctx, deviceID)
|
||||
if err != nil {
|
||||
logger.GetAppLogger().Error("查询设备绑定失败",
|
||||
zap.Uint("device_id", deviceID),
|
||||
zap.Error(err))
|
||||
return nil, errors.New(errors.CodeInternalError, "查询设备绑定失败")
|
||||
}
|
||||
|
||||
if len(bindings) == 0 {
|
||||
return nil, errors.New(errors.CodeIotCardNotFound, "该设备未绑定任何卡")
|
||||
}
|
||||
|
||||
// 取第一张绑定卡(插槽位置最小的)
|
||||
card, err := h.iotCardStore.GetByID(ctx, bindings[0].IotCardID)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeIotCardNotFound, "卡信息查询失败")
|
||||
}
|
||||
|
||||
return card, nil
|
||||
}
|
||||
660
internal/handler/app/client_wallet.go
Normal file
660
internal/handler/app/client_wallet.go
Normal file
@@ -0,0 +1,660 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
rechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||
"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/response"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ClientWalletHandler C 端钱包处理器
|
||||
// 提供 C1~C5 钱包详情、流水、充值前校验、充值下单、充值记录接口
|
||||
type ClientWalletHandler struct {
|
||||
assetService *asset.Service
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore
|
||||
walletStore *postgres.AssetWalletStore
|
||||
transactionStore *postgres.AssetWalletTransactionStore
|
||||
rechargeStore *postgres.AssetRechargeStore
|
||||
rechargeService *rechargeSvc.Service
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore
|
||||
wechatConfigService *wechatConfigSvc.Service
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
}
|
||||
|
||||
// NewClientWalletHandler 创建 C 端钱包处理器
|
||||
func NewClientWalletHandler(
|
||||
assetService *asset.Service,
|
||||
personalDeviceStore *postgres.PersonalCustomerDeviceStore,
|
||||
walletStore *postgres.AssetWalletStore,
|
||||
transactionStore *postgres.AssetWalletTransactionStore,
|
||||
rechargeStore *postgres.AssetRechargeStore,
|
||||
rechargeService *rechargeSvc.Service,
|
||||
openIDStore *postgres.PersonalCustomerOpenIDStore,
|
||||
wechatConfigService *wechatConfigSvc.Service,
|
||||
redisClient *redis.Client,
|
||||
logger *zap.Logger,
|
||||
db *gorm.DB,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
) *ClientWalletHandler {
|
||||
return &ClientWalletHandler{
|
||||
assetService: assetService,
|
||||
personalDeviceStore: personalDeviceStore,
|
||||
walletStore: walletStore,
|
||||
transactionStore: transactionStore,
|
||||
rechargeStore: rechargeStore,
|
||||
rechargeService: rechargeService,
|
||||
openIDStore: openIDStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
redis: redisClient,
|
||||
logger: logger,
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
}
|
||||
}
|
||||
|
||||
type resolvedWalletAssetContext struct {
|
||||
CustomerID uint
|
||||
Identifier string
|
||||
Asset *dto.AssetResolveResponse
|
||||
Generation int
|
||||
ResourceType string
|
||||
SkipPermissionCtx context.Context
|
||||
}
|
||||
|
||||
// GetWalletDetail C1 钱包详情
|
||||
// GET /api/c/v1/wallet/detail
|
||||
func (h *ClientWalletHandler) GetWalletDetail(c *fiber.Ctx) error {
|
||||
var req dto.WalletDetailRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wallet, err := h.getOrCreateWallet(resolved)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &dto.WalletDetailResponse{
|
||||
WalletID: wallet.ID,
|
||||
ResourceType: wallet.ResourceType,
|
||||
ResourceID: wallet.ResourceID,
|
||||
Balance: wallet.Balance,
|
||||
FrozenBalance: wallet.FrozenBalance,
|
||||
UpdatedAt: wallet.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetWalletTransactions C2 钱包流水列表
|
||||
// GET /api/c/v1/wallet/transactions
|
||||
func (h *ClientWalletHandler) GetWalletTransactions(c *fiber.Ctx) error {
|
||||
var req dto.WalletTransactionListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if req.PageSize > constants.MaxPageSize {
|
||||
req.PageSize = constants.MaxPageSize
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wallet, err := h.walletStore.GetByResourceTypeAndID(resolved.SkipPermissionCtx, resolved.ResourceType, resolved.Asset.AssetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return response.SuccessWithPagination(c, []dto.WalletTransactionItem{}, 0, req.Page, req.PageSize)
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
|
||||
}
|
||||
|
||||
var txType *string
|
||||
if strings.TrimSpace(req.TransactionType) != "" {
|
||||
v := strings.TrimSpace(req.TransactionType)
|
||||
txType = &v
|
||||
}
|
||||
|
||||
startTime, err := parseOptionalTime(req.StartTime)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
endTime, err := parseOptionalTime(req.EndTime)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if startTime != nil && endTime != nil && endTime.Before(*startTime) {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
list, err := h.transactionStore.ListByResourceIDWithFilter(
|
||||
resolved.SkipPermissionCtx,
|
||||
wallet.ResourceType,
|
||||
wallet.ResourceID,
|
||||
txType,
|
||||
startTime,
|
||||
endTime,
|
||||
offset,
|
||||
req.PageSize,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包流水失败")
|
||||
}
|
||||
|
||||
total, err := h.transactionStore.CountByResourceIDWithFilter(
|
||||
resolved.SkipPermissionCtx,
|
||||
wallet.ResourceType,
|
||||
wallet.ResourceID,
|
||||
txType,
|
||||
startTime,
|
||||
endTime,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包流水总数失败")
|
||||
}
|
||||
|
||||
items := make([]dto.WalletTransactionItem, 0, len(list))
|
||||
for _, tx := range list {
|
||||
if tx == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
remark := ""
|
||||
if tx.Remark != nil {
|
||||
remark = *tx.Remark
|
||||
}
|
||||
|
||||
items = append(items, dto.WalletTransactionItem{
|
||||
TransactionID: tx.ID,
|
||||
Type: tx.TransactionType,
|
||||
Amount: tx.Amount,
|
||||
BalanceAfter: tx.BalanceAfter,
|
||||
CreatedAt: tx.CreatedAt.Format(time.RFC3339),
|
||||
Remark: remark,
|
||||
})
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, items, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// GetRechargeCheck C3 充值前校验
|
||||
// GET /api/c/v1/wallet/recharge-check
|
||||
func (h *ClientWalletHandler) GetRechargeCheck(c *fiber.Ctx) error {
|
||||
var req dto.ClientRechargeCheckRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
check, err := h.rechargeService.GetRechargeCheck(resolved.SkipPermissionCtx, resolved.ResourceType, resolved.Asset.AssetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := &dto.ClientRechargeCheckResponse{
|
||||
NeedForceRecharge: check.NeedForceRecharge,
|
||||
ForceRechargeAmount: check.ForceRechargeAmount,
|
||||
TriggerType: check.TriggerType,
|
||||
MinAmount: check.MinAmount,
|
||||
MaxAmount: check.MaxAmount,
|
||||
Message: check.Message,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// CreateRecharge C4 创建充值订单
|
||||
// POST /api/c/v1/wallet/recharge
|
||||
func (h *ClientWalletHandler) CreateRecharge(c *fiber.Ctx) error {
|
||||
var req dto.ClientCreateRechargeRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.PaymentMethod != constants.RechargeMethodWechat {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wallet, err := h.getOrCreateWallet(resolved)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := h.wechatConfigService.GetActiveConfig(resolved.SkipPermissionCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil {
|
||||
return errors.New(errors.CodeWechatConfigUnavailable)
|
||||
}
|
||||
|
||||
appID, err := pickAppIDByType(config, req.AppType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
openID, err := h.findOpenIDByCustomerAndAppID(resolved.SkipPermissionCtx, resolved.CustomerID, appID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rechargeNo := generateClientRechargeNo()
|
||||
recharge := &model.AssetRechargeRecord{
|
||||
UserID: resolved.CustomerID,
|
||||
AssetWalletID: wallet.ID,
|
||||
ResourceType: resolved.ResourceType,
|
||||
ResourceID: resolved.Asset.AssetID,
|
||||
RechargeNo: rechargeNo,
|
||||
Amount: req.Amount,
|
||||
PaymentMethod: constants.RechargeMethodWechat,
|
||||
PaymentConfigID: &config.ID,
|
||||
Status: constants.RechargeStatusPending,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
OperatorType: constants.OperatorTypePersonalCustomer,
|
||||
Generation: resolved.Generation,
|
||||
}
|
||||
if err := h.rechargeStore.Create(resolved.SkipPermissionCtx, recharge); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值记录失败")
|
||||
}
|
||||
|
||||
cache := wechat.NewRedisCache(h.redis)
|
||||
paymentApp, err := wechat.NewPaymentAppFromConfig(config, appID, cache, h.logger)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWechatPayFailed, err, "初始化微信支付实例失败")
|
||||
}
|
||||
paymentService := wechat.NewPaymentService(paymentApp, h.logger)
|
||||
payResult, err := paymentService.CreateJSAPIOrder(
|
||||
resolved.SkipPermissionCtx,
|
||||
recharge.RechargeNo,
|
||||
"资产钱包充值",
|
||||
openID,
|
||||
int(req.Amount),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payConfig := buildClientRechargePayConfig(appID, payResult)
|
||||
resp := &dto.ClientRechargeResponse{
|
||||
Recharge: dto.ClientRechargeResult{
|
||||
RechargeID: recharge.ID,
|
||||
RechargeNo: recharge.RechargeNo,
|
||||
Amount: recharge.Amount,
|
||||
Status: recharge.Status,
|
||||
},
|
||||
PayConfig: payConfig,
|
||||
}
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetRechargeList C5 充值记录列表
|
||||
// GET /api/c/v1/wallet/recharges
|
||||
func (h *ClientWalletHandler) GetRechargeList(c *fiber.Ctx) error {
|
||||
var req dto.ClientRechargeListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
if req.PageSize > constants.MaxPageSize {
|
||||
req.PageSize = constants.MaxPageSize
|
||||
}
|
||||
|
||||
resolved, err := h.resolveAssetFromIdentifier(c, req.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := h.db.WithContext(resolved.SkipPermissionCtx).
|
||||
Model(&model.AssetRechargeRecord{}).
|
||||
Where("resource_type = ? AND resource_id = ? AND generation = ?", resolved.ResourceType, resolved.Asset.AssetID, resolved.Generation)
|
||||
if req.Status != nil {
|
||||
query = query.Where("status = ?", *req.Status)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录总数失败")
|
||||
}
|
||||
|
||||
var records []*model.AssetRechargeRecord
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&records).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询充值记录失败")
|
||||
}
|
||||
|
||||
items := make([]dto.ClientRechargeListItem, 0, len(records))
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, dto.ClientRechargeListItem{
|
||||
RechargeID: record.ID,
|
||||
RechargeNo: record.RechargeNo,
|
||||
Amount: record.Amount,
|
||||
Status: record.Status,
|
||||
PaymentMethod: record.PaymentMethod,
|
||||
CreatedAt: record.CreatedAt.Format(time.RFC3339),
|
||||
AutoPurchaseStatus: record.AutoPurchaseStatus,
|
||||
})
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, items, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
// resolveAssetFromIdentifier 统一执行资产解析与归属校验
|
||||
func (h *ClientWalletHandler) resolveAssetFromIdentifier(c *fiber.Ctx, identifier string) (*resolvedWalletAssetContext, error) {
|
||||
customerID, ok := middleware.GetCustomerID(c)
|
||||
if !ok || customerID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
identifier = strings.TrimSpace(identifier)
|
||||
if identifier == "" {
|
||||
identifier = strings.TrimSpace(c.Query("identifier"))
|
||||
}
|
||||
if identifier == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
skipPermissionCtx := context.WithValue(c.UserContext(), constants.ContextKeySubordinateShopIDs, []uint{})
|
||||
assetInfo, err := h.assetService.Resolve(skipPermissionCtx, identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
owned, ownErr := h.isCustomerOwnAsset(skipPermissionCtx, customerID, assetInfo.VirtualNo)
|
||||
if ownErr != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, ownErr, "查询资产归属失败")
|
||||
}
|
||||
if !owned {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资产或资源不存在")
|
||||
}
|
||||
|
||||
resourceType, mapErr := mapAssetTypeToWalletResource(assetInfo.AssetType)
|
||||
if mapErr != nil {
|
||||
return nil, mapErr
|
||||
}
|
||||
|
||||
generation, genErr := h.getAssetGeneration(skipPermissionCtx, assetInfo.AssetType, assetInfo.AssetID)
|
||||
if genErr != nil {
|
||||
return nil, genErr
|
||||
}
|
||||
|
||||
return &resolvedWalletAssetContext{
|
||||
CustomerID: customerID,
|
||||
Identifier: identifier,
|
||||
Asset: assetInfo,
|
||||
Generation: generation,
|
||||
ResourceType: resourceType,
|
||||
SkipPermissionCtx: skipPermissionCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) isCustomerOwnAsset(ctx context.Context, customerID uint, virtualNo string) (bool, error) {
|
||||
records, err := h.personalDeviceStore.GetByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
if record.Status == constants.StatusEnabled && record.VirtualNo == virtualNo {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) getAssetGeneration(ctx context.Context, assetType string, assetID uint) (int, error) {
|
||||
switch assetType {
|
||||
case "card":
|
||||
card, err := h.iotCardStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询卡信息失败")
|
||||
}
|
||||
return card.Generation, nil
|
||||
case "device":
|
||||
device, err := h.deviceStore.GetByID(ctx, assetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, errors.New(errors.CodeAssetNotFound)
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询设备信息失败")
|
||||
}
|
||||
return device.Generation, nil
|
||||
default:
|
||||
return 0, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) getOrCreateWallet(resolved *resolvedWalletAssetContext) (*model.AssetWallet, error) {
|
||||
wallet, err := h.walletStore.GetByResourceTypeAndID(resolved.SkipPermissionCtx, resolved.ResourceType, resolved.Asset.AssetID)
|
||||
if err == nil {
|
||||
return wallet, nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
|
||||
}
|
||||
|
||||
shopIDTag := uint(0)
|
||||
if resolved.Asset.ShopID != nil {
|
||||
shopIDTag = *resolved.Asset.ShopID
|
||||
}
|
||||
|
||||
newWallet := &model.AssetWallet{
|
||||
ResourceType: resolved.ResourceType,
|
||||
ResourceID: resolved.Asset.AssetID,
|
||||
Balance: 0,
|
||||
FrozenBalance: 0,
|
||||
Currency: "CNY",
|
||||
Status: constants.AssetWalletStatusNormal,
|
||||
Version: 0,
|
||||
ShopIDTag: shopIDTag,
|
||||
}
|
||||
if createErr := h.walletStore.Create(resolved.SkipPermissionCtx, newWallet); createErr != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, createErr, "创建钱包失败")
|
||||
}
|
||||
|
||||
wallet, err = h.walletStore.GetByResourceTypeAndID(resolved.SkipPermissionCtx, resolved.ResourceType, resolved.Asset.AssetID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
|
||||
}
|
||||
return wallet, nil
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) findOpenIDByCustomerAndAppID(ctx context.Context, customerID uint, appID string) (string, error) {
|
||||
list, err := h.openIDStore.ListByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(errors.CodeDatabaseError, err, "查询微信授权信息失败")
|
||||
}
|
||||
for _, item := range list {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if item.AppID == appID && strings.TrimSpace(item.OpenID) != "" {
|
||||
return item.OpenID, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New(errors.CodeOpenIDNotFound)
|
||||
}
|
||||
|
||||
func mapAssetTypeToWalletResource(assetType string) (string, error) {
|
||||
switch assetType {
|
||||
case "card":
|
||||
return constants.AssetWalletResourceTypeIotCard, nil
|
||||
case "device":
|
||||
return constants.AssetWalletResourceTypeDevice, nil
|
||||
default:
|
||||
return "", errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func parseOptionalTime(value string) (*time.Time, error) {
|
||||
v := strings.TrimSpace(value)
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
|
||||
for _, layout := range layouts {
|
||||
t, err := time.Parse(layout, v)
|
||||
if err == nil {
|
||||
return &t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid time format")
|
||||
}
|
||||
|
||||
func pickAppIDByType(config *model.WechatConfig, appType string) (string, error) {
|
||||
switch appType {
|
||||
case "official_account":
|
||||
if strings.TrimSpace(config.OaAppID) == "" {
|
||||
return "", errors.New(errors.CodeWechatConfigUnavailable)
|
||||
}
|
||||
return config.OaAppID, nil
|
||||
case "miniapp":
|
||||
if strings.TrimSpace(config.MiniappAppID) == "" {
|
||||
return "", errors.New(errors.CodeWechatConfigUnavailable)
|
||||
}
|
||||
return config.MiniappAppID, nil
|
||||
default:
|
||||
return "", errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
}
|
||||
|
||||
func generateClientRechargeNo() string {
|
||||
timestamp := time.Now().Format("20060102150405")
|
||||
randomNum := rand.Intn(1000000)
|
||||
return fmt.Sprintf("%s%s%06d", constants.AssetRechargeOrderPrefix, timestamp, randomNum)
|
||||
}
|
||||
|
||||
func buildClientRechargePayConfig(appID string, result *wechat.JSAPIPayResult) dto.ClientRechargePayConfig {
|
||||
resp := dto.ClientRechargePayConfig{AppID: appID}
|
||||
if result == nil || result.PayConfig == nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
if cfg, ok := result.PayConfig.(map[string]any); ok {
|
||||
resp.Timestamp = getStringFromAnyMap(cfg, "timeStamp", "timestamp")
|
||||
resp.NonceStr = getStringFromAnyMap(cfg, "nonceStr", "nonce_str")
|
||||
resp.PackageVal = getStringFromAnyMap(cfg, "package")
|
||||
resp.SignType = getStringFromAnyMap(cfg, "signType", "sign_type")
|
||||
resp.PaySign = getStringFromAnyMap(cfg, "paySign", "pay_sign")
|
||||
if appIDVal := getStringFromAnyMap(cfg, "appId", "app_id"); appIDVal != "" {
|
||||
resp.AppID = appIDVal
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
if cfg, ok := result.PayConfig.(map[string]string); ok {
|
||||
resp.Timestamp = cfg["timeStamp"]
|
||||
if resp.Timestamp == "" {
|
||||
resp.Timestamp = cfg["timestamp"]
|
||||
}
|
||||
resp.NonceStr = cfg["nonceStr"]
|
||||
if resp.NonceStr == "" {
|
||||
resp.NonceStr = cfg["nonce_str"]
|
||||
}
|
||||
resp.PackageVal = cfg["package"]
|
||||
resp.SignType = cfg["signType"]
|
||||
if resp.SignType == "" {
|
||||
resp.SignType = cfg["sign_type"]
|
||||
}
|
||||
resp.PaySign = cfg["paySign"]
|
||||
if resp.PaySign == "" {
|
||||
resp.PaySign = cfg["pay_sign"]
|
||||
}
|
||||
if cfg["appId"] != "" {
|
||||
resp.AppID = cfg["appId"]
|
||||
} else if cfg["app_id"] != "" {
|
||||
resp.AppID = cfg["app_id"]
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func getStringFromAnyMap(m map[string]any, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
val, ok := m[key]
|
||||
if !ok || val == nil {
|
||||
continue
|
||||
}
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
case fmt.Stringer:
|
||||
text := v.String()
|
||||
if text != "" {
|
||||
return text
|
||||
}
|
||||
default:
|
||||
text := fmt.Sprintf("%v", v)
|
||||
if text != "" && text != "<nil>" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user