微信群发消息API接口对接中Java后端的请求参数校验与异常反馈优化技巧
1. 群发接口典型参数结构与校验需求
微信群发(含企微/个微)通常接收如下结构:
json
{
"receiver_type": "tag|dept|user",
"receiver_ids": ["id1", "id2"],
"msg_type": "text|image|link",
"content": { ... },
"corp_id": "ww1234567890ab"
}
需校验:
receiver_type与receiver_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. 动态校验消息内容结构
使用 @AssertTrue 在 MessageContent 内部校验:
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 接口,显著降低无效请求与排查成本。