新增客户端资产、钱包、订单、实名、设备管理等核心业务 Handler 与 DTO: - 客户端资产信息查询、套餐列表、套餐历史、资产刷新 - 客户端钱包详情、流水、充值校验、充值订单、充值记录 - 客户端订单创建、列表、详情 - 客户端实名认证链接获取 - 客户端设备卡列表、重启、恢复出厂、WiFi配置、切卡 - 客户端订单服务(含微信/支付宝支付流程) - 强充自动代购异步任务处理 - 数据库迁移 000084:充值记录增加自动代购状态字段
250 lines
8.2 KiB
Go
250 lines
8.2 KiB
Go
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
|
||
}
|