package admin import ( "reflect" "strconv" "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" distributionapp "github.com/break/junhong_cmp_fiber/internal/application/distributionwithdrawal" distributiondomain "github.com/break/junhong_cmp_fiber/internal/domain/distribution" "github.com/break/junhong_cmp_fiber/internal/model/dto" distributionquery "github.com/break/junhong_cmp_fiber/internal/query/distributionwithdrawal" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/break/junhong_cmp_fiber/pkg/response" ) // WithdrawalQualificationHandler 提现资料资格后台 Handler。 type WithdrawalQualificationHandler struct { service *distributionapp.QualificationService query *distributionquery.Query validator *validator.Validate } // NewWithdrawalQualificationHandler 创建提现资料资格后台 Handler。 func NewWithdrawalQualificationHandler( service *distributionapp.QualificationService, query *distributionquery.Query, validate *validator.Validate, ) *WithdrawalQualificationHandler { return &WithdrawalQualificationHandler{service: service, query: query, validator: validate} } // SubmitWithdrawalQualification 提交或替换提现资料资格 // POST /api/admin/shops/:shop_id/withdrawal-qualifications // 仅本人代理店铺;替换合同或法人身份证时同一事务新增版本并使旧有效版本失效。 func (h *WithdrawalQualificationHandler) SubmitWithdrawalQualification(c *fiber.Ctx) error { if h.service == nil { return errors.New(errors.CodeServiceUnavailable, "提现资料资格能力尚未配置") } var req dto.SubmitWithdrawalQualificationReq if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数解析失败") } shopID, err := strconv.ParseUint(c.Params("shop_id"), 10, 64) if err != nil || shopID == 0 { return errors.New(errors.CodeInvalidParam, "无效的店铺 ID") } // shop_id 只来自路径,必须在校验前回填,否则 ShopID 的 required 恒失败。 req.ShopID = uint(shopID) if h.validator == nil { return errors.New(errors.CodeInternalError, "提现资料资格校验器未配置") } if err := h.validator.Struct(&req); err != nil { return errors.New(errors.CodeInvalidParam, validationMessage("提现资料资格参数不合法", &req, err)) } result, err := h.service.Submit(c.UserContext(), uint(shopID), distributiondomain.QualificationInput{ SubjectType: req.SubjectType, SubjectCode: req.SubjectCode, LegalPersonIDCard: req.LegalPersonIDCard, ContractFileKey: req.ContractFileKey, IDCardFrontFileKey: req.IDCardFrontFileKey, IDCardBackFileKey: req.IDCardBackFileKey, BusinessLicenseFileKey: req.BusinessLicenseFileKey, ShopFrontFileKey: req.ShopFrontFileKey, InvoiceFileKey: req.InvoiceFileKey, InvoiceTitle: req.InvoiceTitle, InvoiceSubjectCode: req.InvoiceSubjectCode, }) if err != nil { return err } return response.Success(c, &dto.SubmitWithdrawalQualificationResp{ ID: result.QualificationID, Status: result.Status, StatusName: constants.GetWithdrawalQualificationStatusName(result.Status), }) } // VoidWithdrawalQualification 超级管理员作废有效提现资料资格 // POST /api/admin/withdrawal-qualifications/:id/void func (h *WithdrawalQualificationHandler) VoidWithdrawalQualification(c *fiber.Ctx) error { if h.service == nil { return errors.New(errors.CodeServiceUnavailable, "提现资料资格能力尚未配置") } id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil || id == 0 { return errors.New(errors.CodeInvalidParam, "无效的资格 ID") } var req dto.VoidWithdrawalQualificationReq if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数解析失败") } if h.validator == nil { return errors.New(errors.CodeInternalError, "提现资料资格校验器未配置") } if err := h.validator.Struct(&req); err != nil { return errors.New(errors.CodeInvalidParam, validationMessage("作废提现资料资格参数不合法", &req, err)) } if err := h.service.Void(c.UserContext(), uint(id), req.Reason); err != nil { return err } return response.Success(c, nil) } // validationMessage 把请求校验失败转换为可定位字段的中文提示。 // 只使用字段的 description 与校验规则,不拼接底层错误文本,也不回显字段值。 func validationMessage(prefix string, req any, err error) string { fieldErrs, ok := err.(validator.ValidationErrors) if !ok || len(fieldErrs) == 0 { return prefix } return prefix + ":" + describeFieldError(req, fieldErrs[0]) } // describeFieldError 用字段中文名与失败规则描述单个字段错误。 func describeFieldError(req any, fieldErr validator.FieldError) string { label := fieldDescription(req, fieldErr.StructField()) switch fieldErr.Tag() { case "required": // 数字字段的 required 只在零值失败;说“不能为空”会误导为缺字段。 if isNumericField(req, fieldErr.StructField()) { return label + "必须大于 0" } return label + "不能为空" case "min": if isNumericField(req, fieldErr.StructField()) { return label + "不能小于 " + fieldErr.Param() } return label + "长度不能小于 " + fieldErr.Param() case "max": if isNumericField(req, fieldErr.StructField()) { return label + "不能超过 " + fieldErr.Param() } return label + "长度不能超过 " + fieldErr.Param() case "oneof": return label + "必须为 " + strings.ReplaceAll(fieldErr.Param(), " ", "/") + " 之一" default: return label + "不合法(" + fieldErr.Tag() + ")" } } // isNumericField 判断字段是否为整数或浮点类型。 func isNumericField(req any, fieldName string) bool { field, ok := lookupField(req, fieldName) if !ok { return false } switch field.Type.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: return true default: return false } } // lookupField 在去指针的结构体类型上按名取字段。 func lookupField(req any, fieldName string) (reflect.StructField, bool) { typ := reflect.TypeOf(req) for typ != nil && typ.Kind() == reflect.Ptr { typ = typ.Elem() } if typ == nil || typ.Kind() != reflect.Struct { return reflect.StructField{}, false } return typ.FieldByName(fieldName) } // fieldDescription 取字段 description 的首个中文短语作为提示名,缺失时退回字段名。 func fieldDescription(req any, fieldName string) string { field, ok := lookupField(req, fieldName) if !ok { return fieldName } description := strings.TrimSpace(field.Tag.Get("description")) if description == "" { return fieldName } if cut := strings.IndexAny(description, "((::,,;;"); cut > 0 { description = strings.TrimSpace(description[:cut]) } if description == "" { return fieldName } // 提示名以拉丁字母/数字结尾时补一个空格,避免与后续中文粘连。 if last := description[len(description)-1]; last < 0x80 { description += " " } return description } // ListWithdrawalQualifications 查询提现资料资格版本 // GET /api/admin/shops/:shop_id/withdrawal-qualifications // 仅返回当前账号数据范围内的资料版本;证件号脱敏,附件只返回对象存储 Key。 func (h *WithdrawalQualificationHandler) ListWithdrawalQualifications(c *fiber.Ctx) error { if h.query == nil { return errors.New(errors.CodeInternalError, "提现资料资格查询能力未配置") } var req dto.WithdrawalQualificationListReq if err := c.QueryParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数解析失败") } // 路由路径必带 shop_id;数据范围由 CanManageShop 在业务边界强制。 shopID, err := strconv.ParseUint(c.Params("shop_id"), 10, 64) if err != nil || shopID == 0 { return errors.New(errors.CodeInvalidParam, "无效的店铺 ID") } if err := middleware.CanManageShop(c.UserContext(), uint(shopID)); err != nil { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } result, err := h.query.ListQualifications(c.UserContext(), []uint{uint(shopID)}, &req) if err != nil { return err } return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size) }