微信群发消息API接口对接中Java后端的请求参数校验与异常反馈优化技巧

微信群发消息API接口对接中Java后端的请求参数校验与异常反馈优化技巧

1. 群发接口典型参数结构与校验需求

微信群发(含企微/个微)通常接收如下结构:

json 复制代码
{
  "receiver_type": "tag|dept|user",
  "receiver_ids": ["id1", "id2"],
  "msg_type": "text|image|link",
  "content": { ... },
  "corp_id": "ww1234567890ab"
}

需校验:

  • receiver_typereceiver_ids 非空且匹配;
  • receiver_ids 长度不超过平台限制(如企微标签最多1000人);
  • content 结构随 msg_type 动态变化;
  • corp_id 必须为已授权企业。

传统 if 判断冗长,应采用 JSR-380(Bean Validation 2.0) + 自定义约束

2. 定义带级联校验的消息 DTO

java 复制代码
package wlkankan.cn.wechat.mass.dto;

import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;

public class MassMessageRequest {

    @NotNull(message = "接收者类型不能为空")
    private ReceiverType receiverType;

    @NotEmpty(message = "接收者ID列表不能为空")
    @ValidReceiverIds
    private List<String> receiverIds;

    @NotNull(message = "消息类型不能为空")
    private MessageType msgType;

    @Valid
    @NotNull(message = "消息内容不能为空")
    private MessageContent content;

    @NotEmpty(message = "企业ID不能为空")
    private String corpId;

    // getters/setters
}

枚举定义:

java 复制代码
public enum ReceiverType {
    TAG, DEPT, USER
}

public enum MessageType {
    TEXT, IMAGE, LINK
}

3. 实现自定义校验注解:@ValidReceiverIds

java 复制代码
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ReceiverIdsValidator.class)
public @interface ValidReceiverIds {
    String message() default "接收者ID格式或数量不合法";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

校验器实现:

java 复制代码
package wlkankan.cn.wechat.mass.validator;

import wlkankan.cn.wechat.mass.dto.MassMessageRequest;
import wlkankan.cn.wechat.mass.dto.ReceiverType;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;
import java.util.regex.Pattern;

public class ReceiverIdsValidator implements ConstraintValidator<ValidReceiverIds, List<String>> {

    private static final Pattern WXID_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\-]{6,64}$");
    private static final int MAX_TAG_USERS = 1000;
    private static final int MAX_DEPT_COUNT = 100;

    @Override
    public boolean isValid(List<String> value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) return true; // 非空由 @NotEmpty 保证

        MassMessageRequest request = (MassMessageRequest) context.getRootBean();
        ReceiverType type = request.getReceiverType();

        if (type == null) return false;

        switch (type) {
            case USER:
                return value.size() <= MAX_TAG_USERS && value.stream().allMatch(WXID_PATTERN.asMatchPredicate());
            case TAG:
                return value.size() <= 10 && value.stream().allMatch(id -> id.matches("^\\d+$"));
            case DEPT:
                return value.size() <= MAX_DEPT_COUNT && value.stream().allMatch(id -> id.matches("^\\d+$"));
            default:
                return false;
        }
    }
}

4. 动态校验消息内容结构

使用 @AssertTrueMessageContent 内部校验:

java 复制代码
public class MessageContent {

    private String text;
    private String mediaId;
    private String title;
    private String url;

    @AssertTrue(message = "文本消息必须包含text字段")
    public boolean isTextValid() {
        if (MessageType.TEXT.equals(getMessageTypeFromParent())) {
            return text != null && !text.trim().isEmpty();
        }
        return true;
    }

    @AssertTrue(message = "图文消息必须包含title和url")
    public boolean isLinkValid() {
        if (MessageType.LINK.equals(getMessageTypeFromParent())) {
            return title != null && url != null && URLValidator.isValid(url);
        }
        return true;
    }

    // 注意:需通过 ThreadLocal 或上下文传递 msgType,此处简化
    private MessageType getMessageTypeFromParent() {
        // 实际项目中可通过自定义 ValidatingGroup 或嵌套验证上下文实现
        return MessageType.TEXT; // 示例
    }
}

更健壮的方式是使用 分组校验自定义 ValidatingVisitor,但为简洁起见,也可在 Controller 层手动触发二次校验。

5. 统一异常处理与结构化错误返回

捕获 MethodArgumentNotValidException 并格式化:

java 复制代码
@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
            .collect(Collectors.toList());

        ApiError error = new ApiError(400, "参数校验失败", errors);
        return ResponseEntity.badRequest().body(error);
    }
}

ApiError 定义:

java 复制代码
package wlkankan.cn.wechat.common;

public class ApiError {
    private int code;
    private String message;
    private List<String> details;

    public ApiError(int code, String message, List<String> details) {
        this.code = code;
        this.message = message;
        this.details = details;
    }

    // getters
}

6. 业务层校验:企业授权与额度检查

参数合法 ≠ 业务可执行,需在 Service 层补充校验:

java 复制代码
@Service
public class MassMessageService {

    public void send(MassMessageRequest request) {
        // 校验企业是否授权
        if (!corpAuthService.isAuthorized(request.getCorpId())) {
            throw new BusinessException(ErrorCode.CORP_NOT_AUTHORIZED, "企业未授权");
        }

        // 校验今日群发额度
        int used = quotaService.getUsedCount(request.getCorpId(), LocalDate.now());
        if (used >= 100) {
            throw new BusinessException(ErrorCode.QUOTA_EXCEEDED, "今日群发次数已达上限");
        }

        // 执行发送...
    }
}

自定义异常:

java 复制代码
public class BusinessException extends RuntimeException {
    private final ErrorCode code;

    public BusinessException(ErrorCode code, String message) {
        super(message);
        this.code = code;
    }
}

全局捕获:

java 复制代码
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiError> handleBusiness(BusinessException ex) {
    ApiError error = new ApiError(ex.getCode().getValue(), ex.getMessage(), null);
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}

7. 调用方友好性:提供 OpenAPI 校验规则示例

在 Swagger 中标注枚举与长度限制:

java 复制代码
@Schema(description = "接收者类型", allowableValues = {"TAG", "DEPT", "USER"})
private ReceiverType receiverType;

@Schema(description = "用户ID列表(USER类型时为wxid,最多1000个)", maxLength = 1000)
private List<String> receiverIds;

通过 Bean Validation + 自定义约束 + 分层异常处理 + OpenAPI 注解,可构建高鲁棒性、易调试、调用方友好的微信群发 API 接口,显著降低无效请求与排查成本。

相关推荐
麦兜*2 小时前
Spring Boot整合Swagger 3.0:自动生成API文档并在线调试
java·spring boot·后端
Main. 242 小时前
从0到1学习Qt -- Qt3D入门
开发语言·qt·学习
接着奏乐接着舞。2 小时前
Go 一小时上手指南:从零到运行第一个程序
开发语言·后端·golang
三少爷的鞋2 小时前
架构避坑:为什么 UseCase 不该启动协程,也不该切线程?
android
飞机和胖和黄2 小时前
王道C语言第一周作业
c语言·开发语言
lly2024062 小时前
SQLite 安装指南
开发语言
星火开发设计2 小时前
C++ deque 全面解析与实战指南
java·开发语言·数据结构·c++·学习·知识
独自破碎E2 小时前
什么是RabbitMQ中的死信队列?
java·rabbitmq·java-rabbitmq
码界奇点2 小时前
基于Spring与Netty的分布式配置管理系统设计与实现
java·分布式·spring·毕业设计·源代码管理