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 }