package app import ( "strconv" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "go.uber.org/zap" h5popupapp "github.com/break/junhong_cmp_fiber/internal/application/h5popup" "github.com/break/junhong_cmp_fiber/internal/handler/validation" "github.com/break/junhong_cmp_fiber/internal/middleware" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/logger" "github.com/break/junhong_cmp_fiber/pkg/response" ) // ClientPopupHandler 提供 H5 弹窗候选查询与风险换卡地址提交。 type ClientPopupHandler struct { candidates *h5popupapp.CandidateService riskExchanges *h5popupapp.RiskExchangeService validate *validator.Validate } // NewClientPopupHandler 创建 H5 弹窗 Handler。 func NewClientPopupHandler(candidates *h5popupapp.CandidateService, riskExchanges *h5popupapp.RiskExchangeService, validate *validator.Validate) *ClientPopupHandler { return &ClientPopupHandler{candidates: candidates, riskExchanges: riskExchanges, validate: validate} } // GetCandidates 查询当前页面与当前资产的弹窗候选。 // GET /api/c/v1/popup-candidates // 该查询有副作用:命中时会创建或复用个人站内通知并保持未读,这是产品契约(不预生成通知), // 客户端关闭或稍后处理时必须用返回的 notification_id 调用既有已读接口。 func (h *ClientPopupHandler) GetCandidates(c *fiber.Ctx) error { var request dto.PopupCandidateRequest if err := c.QueryParser(&request); err != nil { logPopupValidationFailure(c, "弹窗候选参数不合法", err) return errors.New(errors.CodeInvalidParam) } if h.validate != nil { if err := h.validate.Struct(&request); err != nil { logPopupValidationFailure(c, "弹窗候选参数不合法", err) return errors.New(errors.CodeInvalidParam, validation.Message("弹窗候选参数不合法", &request, err)) } } customerID, ok := middleware.GetCustomerID(c) if !ok || customerID == 0 { return errors.New(errors.CodeUnauthorized) } if h.candidates == nil { return errors.New(errors.CodeServiceUnavailable, "弹窗投放能力尚未配置") } result, err := h.candidates.GetCandidate(c.UserContext(), customerID, request) if err != nil { return err } return response.Success(c, result) } // SubmitRiskAddress 提交风险换卡收货地址。 // POST /api/c/v1/risk-exchanges/:asset_id/address // 重复提交返回首次创建的物流换货单与首次地址;首地址锁定,客户不能修改。 func (h *ClientPopupHandler) SubmitRiskAddress(c *fiber.Ctx) error { assetID, err := strconv.ParseUint(c.Params("asset_id"), 10, 64) if err != nil || assetID == 0 { return errors.New(errors.CodeInvalidParam, "风险换卡资产ID不合法") } var request dto.ClientRiskExchangeAddressParams if err := c.BodyParser(&request); err != nil { logPopupValidationFailure(c, "风险换卡地址参数不合法", err) return errors.New(errors.CodeInvalidParam) } // 路径来源字段必须由 Handler 回填后再校验,避免被请求体覆盖,也避免 required 恒失败。 request.AssetID = uint(assetID) if h.validate != nil { if err := h.validate.Struct(&request); err != nil { logPopupValidationFailure(c, "风险换卡地址参数不合法", err) return errors.New(errors.CodeInvalidParam, validation.Message("风险换卡地址参数不合法", &request, err)) } } customerID, ok := middleware.GetCustomerID(c) if !ok || customerID == 0 { return errors.New(errors.CodeUnauthorized) } if h.riskExchanges == nil { return errors.New(errors.CodeServiceUnavailable, "风险换卡能力尚未配置") } result, err := h.riskExchanges.Submit(c.UserContext(), customerID, uint(assetID), request) if err != nil { return err } return response.Success(c, result) } // logPopupValidationFailure 记录参数校验失败,仅记录字段错误,不回显请求体内容。 func logPopupValidationFailure(c *fiber.Ctx, message string, err error) { logger.GetAppLogger().Warn("H5 弹窗接口参数验证失败", zap.String("method", c.Method()), zap.String("path", c.Path()), zap.String("message", message), zap.Error(err), ) }