不用互关也能发私信?Spring Boot实现私信权限策略与幂等设计
2026/9/9 8:33:02 网站建设 项目流程

刚开始做社交产品私信功能时,很容易把“能不能发消息”当做一个简单的布尔值判断。产品提了一个需求:“不用互关也可以发消息”,不少后端第一时间会想:那直接把关注关系校验去掉不就行了?实际上,从真实业务看,什么都不做直接放开,很快会被广告、骚扰和恶意注册打爆。真正要做的,是把私信权限从“固定代码”改造成“可配置策略”,同时在发送链路上补齐黑名单、频控、幂等和内容安全。这篇文章以“不用互关也可以发消息”为场景,从一个最小可运行的 Spring Boot 示例开始,讲清楚私信权限判断的顺序、关系链查询方式、幂等设计、频控策略,以及接入生产环境前还需要补齐哪些能力。

1. 先理解“不用互关也可以发消息”背后的私信权限模型

1.1 为什么会出现“不用互关也可以发消息”的需求

社交产品里,私信天然有骚扰风险。最早的方案是把“关注关系”当作门槛:只有互相关注或对方关注了你,才能发私信。这样能挡住大量陌生人消息,但代价是沟通成本变高。比如一个用户想向另一个用户咨询问题,却因为对方没有关注自己而发不出消息,只能去公开评论区留言,转化链路更长。

“不用互关也可以发消息”的价值在于降低连接成本:客服通知、创作者联系粉丝、兴趣社群破冰、后台回复都依赖这种能力。但它不是简单把关注校验删掉,而是把“关系链判断”换成一套组合权限:接收者允许谁发、发送者是否在拉黑名单、单位时间内能发多少条、消息内容是否合规。只有这些条件都满足,消息才会进入发送流程。

1.2 一次私信发送背后要检查什么

从一次发送请求出发,私信系统至少需要按顺序处理以下检查点:

  1. 发送者是否已登录,账号是否被禁用。
  2. 接收者是否存在,账号是否注销。
  3. 请求是否重复提交,需要幂等处理。
  4. 发送者和接收者之间是否存在拉黑关系,注意要双向判断。
  5. 接收者配置的私信策略是否允许当前发送者发送。
  6. 发送频率是否超过阈值,包括全局频率、接收者维度频率和内容重复频率。
  7. 消息内容是否通过合规校验。

这七个检查点不是每次都要严格按同一顺序执行。顺序会影响成本和结果。比如幂等检查放在业务校验之前,可以提前拦截重复请求,避免多次查询关系链;黑名单检查放在隐私策略之前,是因为被拉黑的人没有资格触发后续判断;频控检查放在最后,可以避免因为关系链判断失败而白白消耗用户发送额度。

1.3 私信权限模型:从“固定判断”到“可配置策略”

如果把“能否发送”写死在业务代码里,比如只有一个if (isFollowed()) { send(); },产品每次调整策略都要改代码、发版,后面会很难维护。更好的方式是把权限抽象成策略枚举,配置在用户维度。

策略枚举允许谁发私信是否需要互关
ALL所有人不需要
FOLLOWERS关注接收者的用户不需要互关,但需要是接收者粉丝
MUTUAL互相关注的用户需要
NONE关闭私信无法发送

上面这个表里,ALLFOLLOWERS都属于“不用互关也可以发消息”的场景,区别在于范围限制。ALL连认证关系都不看,FOLLOWERS仍然要求发送者是接收者的粉丝。实际产品中,很多平台默认的策略不是ALL,而是FOLLOWERS或“关注者+粉丝”,这样既能降低陌生人骚扰,又保持了沟通链路。

还有人容易混淆“关注关系”和“粉丝关系”。FOLLOWERS策略判断的是“发送者是否关注了接收者”,而不是“接收者是否关注了发送者”。如果判定方向写反,会出现别人关注了你但你被拦下的奇怪问题。这块在设计表结构时就要用字段语义固定下来。

2. 环境准备与项目结构:先搭起一套最小可运行私信服务

2.1 技术选型与依赖版本

最小实现使用 Spring Boot 2.7、MyBatis-Plus 3.5、MySQL 8.0、Redis 6.x。Redis 用于频控和短时间关系缓存,MySQL 用于持久化消息、用户隐私设置和拉黑关系。实际项目可以换成任意 Web 框架,这里用 Java 生态是为了把校验逻辑写清楚。

需要准备的本地环境:

组件版本参考作用
JDK1.8 及以上运行 Spring Boot 服务
Maven3.6 及以上依赖管理和构建
MySQL5.7 或 8.0持久化业务数据
Redis6.x频控计数、缓存
curl / Postman任意接口验证

pom.xml 核心依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

依赖不是越新越好。示例里的版本在多数 Spring Boot 2.7 项目里可以直接使用,如果原始项目依赖冲突,落地前要确认 MyBatis-Plus 和 Spring Boot 的兼容性。

2.2 数据库表设计:把关系、隐私和消息分开

私信功能涉及的数据可以拆成五张表:用户表、用户隐私设置表、拉黑关系表、私信消息表、幂等表。不要把隐私策略和用户基本信息放在同一张表里也行,但建议单独建表,方便后续扩展多个策略字段。

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `nickname` varchar(64) NOT NULL, `status` tinyint NOT NULL DEFAULT '1' COMMENT '1正常 0禁用', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_privacy` ( `user_id` bigint NOT NULL, `allow_strategy` varchar(20) NOT NULL DEFAULT 'FOLLOWERS' COMMENT 'ALL/FOLLOWERS/MUTUAL/NONE', `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_relation` ( `id` bigint NOT NULL AUTO_INCREMENT, `follower_id` bigint NOT NULL COMMENT '关注者', `followed_id` bigint NOT NULL COMMENT '被关注者', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_follow` (`follower_id`, `followed_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `user_block` ( `id` bigint NOT NULL AUTO_INCREMENT, `blocker_id` bigint NOT NULL COMMENT '拉黑发起方', `blocked_id` bigint NOT NULL COMMENT '被拉黑方', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_block` (`blocker_id`, `blocked_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `private_message` ( `id` bigint NOT NULL AUTO_INCREMENT, `sender_id` bigint NOT NULL, `receiver_id` bigint NOT NULL, `content` text NOT NULL, `msg_type` tinyint NOT NULL DEFAULT '1' COMMENT '1文字 2图片', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_receiver_sender` (`receiver_id`, `sender_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `idempotency` ( `id` bigint NOT NULL AUTO_INCREMENT, `biz_key` varchar(64) NOT NULL COMMENT '全局幂等键', `biz_hash` varchar(64) NOT NULL COMMENT '请求内容摘要', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_biz_key` (`biz_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

user_privacy.allow_strategy的默认值设为FOLLOWERSALL安全。即使某张配置表漏写数据,也不会出现所有陌生人都能私信的情况。user_relationfollower_id表示关注者,followed_id表示被关注者,查询“发送者是否关注接收者”时用follower_id = 发送者 AND followed_id = 接收者

私信消息表在生产环境中通常不会只有一个 MySQL 表,量大后需要分表或者迁移到消息中间件。最小实现先用单表把链路跑通,生产环境再考虑冷热分离。

2.3 服务端目录结构与基础配置

示例项目目录可以按以下方式拆分:

src/main/java/com/example/im ├── ImApplication.java ├── controller │ └── MessageController.java ├── service │ └── MessageService.java ├── mapper │ ├── UserMapper.java │ ├── UserPrivacyMapper.java │ ├── RelationMapper.java │ ├── BlockMapper.java │ ├── PrivateMessageMapper.java │ └── IdempotencyMapper.java ├── entity │ ├── UserPrivacy.java │ └── PrivateMessage.java └── enums └── PrivateChatStrategy.java

application.yml中需要配置 Redis、MySQL 和 MyBatis-Plus:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/im_demo?useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: your_password redis: host: localhost port: 6379 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted

map-underscore-to-camel-case开启后,数据库字段created_at可以映射成 Java 属性createdAt,避免大量手工映射。私有消息表如果后续要支持撤回,需要增加status字段;最小示例先不做撤回逻辑。

3. 把“不用互关也可以发消息”的判定逻辑实现出来

3.1 用枚举统一私信策略

私信策略不应该散落在 if-else 里。先定义PrivateChatStrategy枚举,为每个策略提供是否要求关注关系的标记:

package com.example.im.enums; public enum PrivateChatStrategy { ALL(false, false), FOLLOWERS(true, false), MUTUAL(true, true), NONE(false, false); private final boolean requireFollow; private final boolean requireMutual; PrivateChatStrategy(boolean requireFollow, boolean requireMutual) { this.requireFollow = requireFollow; this.requireMutual = requireMutual; } public boolean isRequireFollow() { return requireFollow; } public boolean isRequireMutual() { return requireMutual; } }

判断逻辑是:ALL不要求任何关系,所以不用互关也能发消息;FOLLOWERS要求发送者关注接收者,但不需要接收者回关,所以仍然属于“不用互关也能发消息”;MUTUAL要求双方互相关注;NONE表示接收者关闭私信。

这里要注意:MUTUALNONE并不是“不用互关”场景,但它们必须和开放策略放在同一套枚举里,否则后续扩展配置时还要再写一组映射。

3.2 发送私信接口:权限校验顺序是关键

Controller 层只负责接收参数和返回结果,真正的判断放在 Service:

@RestController @RequestMapping("/api/message") public class MessageController { private final MessageService messageService; public MessageController(MessageService messageService) { this.messageService = messageService; } @PostMapping("/send") public String send(@RequestBody SendMessageRequest request) { return messageService.send( request.getSenderId(), request.getReceiverId(), request.getContent(), request.getIdempotentKey() ); } }

生产环境中,senderId应该从登录态的 token 或 Session 中解析,不能由客户端传入。这里为了最小示例,先从请求参数读取。

MessageService的核心发送逻辑:

@Service public class MessageService { private final UserMapper userMapper; private final UserPrivacyMapper privacyMapper; private final RelationMapper relationMapper; private final BlockMapper blockMapper; private final PrivateMessageMapper messageMapper; private final IdempotencyMapper idempotencyMapper; private final StringRedisTemplate redisTemplate; // 省略构造方法 public String send(Long senderId, Long receiverId, String content, String idempotentKey) { // 1. 发送者和接收者账号检查 if (senderId == null || receiverId == null || senderId.equals(receiverId)) { throw new BizException("发送者或接收者不合法"); } // 2. 幂等检查,防止重复提交 if (!trySaveIdempotency(idempotentKey, senderId, receiverId, content)) { throw new BizException("重复请求,请勿重复发送"); } // 3. 账号状态和拉黑关系检查 checkBlockRelation(senderId, receiverId); // 4. 查询接收者私信策略 UserPrivacy privacy = privacyMapper.selectById(receiverId); PrivateChatStrategy strategy = resolveStrategy(privacy); // 5. 按策略判断关系 if (strategy.isRequireFollow()) { boolean followed = relationMapper.existsFollow(senderId, receiverId); if (!followed) { throw new BizException("对方未开放陌生人私信,请先关注对方"); } } if (strategy.isRequireMutual()) { boolean followBack = relationMapper.existsFollow(receiverId, senderId); if (!followBack) { throw new BizException("需要互相关注后才能发送私信"); } } // 6. 频控检查 checkFrequency(senderId, receiverId); // 7. 保存消息 PrivateMessage message = new PrivateMessage(); message.setSenderId(senderId); message.setReceiverId(receiverId); message.setContent(content); messageMapper.insert(message); // 8. 发送事件,异步清理/通知 return "发送成功"; } }

这段代码的顺序很有讲究。幂等检查放在最前面,是因为一旦重复请求通过了,后续所有业务计算都会浪费,而且可能落多条消息。拉黑检查放在隐私策略之前,是因为被拉黑是“资格问题”,不需要再看隐私策略。频控放在允许发送之后,是为了不让关系判断失败的用户消耗限流额度。

resolveStrategy需要考虑隐私表数据缺失的情况。如果没有查到接收者的隐私配置,不要直接默认ALL,应该默认走最安全的策略,比如FOLLOWERSNONE。实际项目里可以用如下方式兜底:

private PrivateChatStrategy resolveStrategy(UserPrivacy privacy) { if (privacy == null || StringUtils.isBlank(privacy.getAllowStrategy())) { return PrivateChatStrategy.FOLLOWERS; } try { return PrivateChatStrategy.valueOf(privacy.getAllowStrategy()); } catch (IllegalArgumentException e) { return PrivateChatStrategy.FOLLOWERS; } }

BizException是自定义业务异常,实际项目可以配合全局异常处理器返回统一错误码。

3.3 黑名单判断要双向

拉黑检查不是只查接收者是否拉黑了发送者。如果发送者把接收者拉黑了,同样不应该继续发送消息,否则会出现“我拉黑了你,你还能给我发消息”的异常体验。

private void checkBlockRelation(Long senderId, Long receiverId) { boolean blockedByReceiver = blockMapper.existsBlock(receiverId, senderId); boolean blockedBySender = blockMapper.existsBlock(senderId, receiverId); if (blockedByReceiver || blockedBySender) { throw new BizException("由于拉黑关系,无法发送私信"); } }

existsBlock(blockerId, blockedId)在 Mapper 里对应一条简单 SQL:

SELECT COUNT(*) FROM user_block WHERE blocker_id = #{blockerId} AND blocked_id = #{blockedId}

拉黑关系表用唯一索引(blocker_id, blocked_id)防止重复数据。做双向判断时,把两个方向各自查询一次,不要试图用一条INOR拼出模糊结果。

3.4 幂等键与消息去重

客户端在网络抖动时可能会重试,用户也可能快速连点两次发送按钮。如果没有幂等,同一条消息会落库多次。幂等表只保存业务键和请求哈希,第一次插入成功后才继续发送,重复的biz_key会被唯一索引挡住。

private boolean trySaveIdempotency(String idempotentKey, Long senderId, Long receiverId, String content) { if (StringUtils.isBlank(idempotentKey)) { idempotentKey = UUID.randomUUID().toString(); } try { Idempotency record = new Idempotency(); record.setBizKey(idempotentKey); record.setBizHash(Hashing.sha256() .hashString(senderId + ":" + receiverId + ":" + content, StandardCharsets.UTF_8) .toString()); idempotencyMapper.insert(record); return true; } catch (DuplicateKeyException e) { return false; } }

一定要把幂等业务的生成规则定义清楚。最稳妥的方案是服务端根据“发送者 + 接收者 + 内容哈希 + 时间窗口”生成,客户端也可以传自己的clientMsgId,但服务端必须校验同一个幂等键只能对应一条不同的消息内容。如果客户端随便传一个随机串,幂等就会失效。

实际实现时,幂等记录不能无限增长。可以给idempotency表加一个过期时间字段,或者定期清理超过 7 天的记录。也可以直接使用 Redis 的SET NX EX做短时间幂等,但只依赖 Redis 会出现键过期后重复请求穿透的问题,MySQL 唯一索引更适合作为最终一致性保证。

4. 跑通最小闭环:准备数据和验证接口

4.1 准备三个测试用户

先插入三个用户和关系数据,便于验证不同策略:

INSERT INTO `user` (`id`, `nickname`) VALUES (1001, '普通用户A'), (1002, '创作者B'), (1003, '新人C'); INSERT INTO `user_relation` (`follower_id`, `followed_id`) VALUES (1001, 1002); INSERT INTO `user_privacy` (`user_id`, `allow_strategy`) VALUES (1002, 'FOLLOWERS'), (1003, 'ALL');

这里 1002 的私信策略是FOLLOWERS,1001 关注了 1002,所以 1001 可以给 1002 发消息;1003 的策略是ALL,即使没有人与它互关,也可以收到消息。

4.2 发送私信接口验证

启动 Spring Boot 项目后,用 curl 发送一条从 1001 到 1003 的消息:

curl -X POST http://localhost:8080/api/message/send \ -H "Content-Type: application/json" \ -d '{"senderId":1001,"receiverId":1003,"content":"你好,来自新人C的消息","idempotentKey":"key-001"}'

预期返回:

发送成功

再验证 1001 到 1003 的重复请求:

curl -X POST http://localhost:8080/api/message/send \ -H "Content-Type: application/json" \ -d '{"senderId":1001,"receiverId":1003,"content":"你好,来自新人C的消息","idempotentKey":"key-001"}'

预期返回:

重复请求,请勿重复发送

这说明幂等键生效。如果需要发一条新消息,要换新的idempotentKey

4.3 不同私信策略的预期结果

用表格整理测试场景,方便后续做接口回归:

测试用例接收者策略发送者与接收者关系预期结果
1001 -> 1002FOLLOWERS1001 关注了 1002发送成功
1003 -> 1002FOLLOWERS1003 未关注 1002发送失败,提示先关注
1001 -> 1003ALL无关系发送成功,不用互关
1001 -> 1002(1001被拉黑)FOLLOWERS1002 拉黑 1001发送失败,提示拉黑
相同 idempotentKey 重复提交任意任意发送失败,提示重复

ALL策略对应的就是标题里“不用互关也可以发消息”的最直接实现。FOLLOWERS更进一步,保留了一层关注门槛,但不需要互关。

4.4 用 Redis 验证频控生效

如果发送接口里使用了如下 Redis 频控:

private void checkFrequency(Long senderId, Long receiverId) { String key = "im:frequency:sender:" + senderId; Long count = redisTemplate.opsForValue() .increment(key); if (count == 1L) { redisTemplate.expire(key, Duration.ofMinutes(1)); } if (count > 5) { throw new BizException("发送太频繁,请稍后再试"); } }

第一次调用后,可以在 Redis 中看到键:

redis-cli 127.0.0.1:6379> GET im:frequency:sender:1001 "1"

连续发送 5 次以上后,接口会返回频控异常。这个极简频控只能用来演示思路,生产环境需要按时间窗口、接收者维度、内容重复度组合设计,否则一个用户给不同人发消息都会共享一个计数,体验不好。

5. 常见问题排查:私信发不出去时从哪查起

5.1 提示“对方未开放陌生人私信”

这个异常表示接收者策略不是ALL,且发送者没有满足关注条件。可以按下面顺序排查:

  1. 查询user_privacy表,确认接收者的allow_strategy
  2. 查询user_relation,确认发送者是否关注接收者。
  3. 检查代码里是否把FOLLOWERS判断成了“接收者关注发送者”,方向写反会导致真正关注了的人也无法发送。
SELECT allow_strategy FROM user_privacy WHERE user_id = 1002; SELECT COUNT(*) FROM user_relation WHERE follower_id = 1003 AND followed_id = 1002;

第一条 SQL 得到FOLLOWERS,第二条 SQL 返回 0,说明 1003 没有关注 1002,所以被拦截。

5.2 提示“发送太频繁,请稍后再试”

只要 Redis 里频控键存在,请求就会被拦截。常见原因是测试时把频控阈值设得过高或过低,或者频控 key 的维度不够。检查 Key 的名称和过期时间:

redis-cli TTL im:frequency:sender:1001

如果 TTL 是-1,说明 key 没有设置过期时间,内存里会一直保留,用户永远无法发送。修复办法是在第一次计数时主动设置过期时间,或者使用带有过期时间的 Lua 脚本。

5.3 同一消息被发送多次

核心原因基本集中在幂等:

  • 幂等键没有传,服务端自动生成新键,结果每次都被当成第一次请求。
  • 幂等键和消息内容没有绑定,同一键被不同内容复用。
  • 幂等表唯一索引缺失,重复插入没有报错。

检查接口日志和idempotency表:

SELECT biz_key, biz_hash, COUNT(*) FROM idempotency GROUP BY biz_key HAVING COUNT(*) > 1;

如果出现重复biz_key,说明唯一索引没建,或者插入逻辑没有捕获重复键异常。如果没有任何重复记录但消息重复落库,说明幂等检查根本没走,需要看日志确认发送流程顺序。

5.4 私信问题通用排查链路

问题现象优先检查项检查方式处理建议
所有用户都发不出私信接收者隐私策略默认值是否安全查询user_privacy默认值确保缺少配置时默认FOLLOWERSNONE
只有部分用户发不出关系表数据是否完整用 SQL 核对关注记录补偿关系数据,检查关注回调链路
批量账号提示频繁频控 key 是否按账号维度设计查看 Redis key调整计数维度,增加接收者维度限制
重复点击后消息出现多条幂等键生成和唯一索引查询idempotency服务端统一生成幂等键,建唯一索引
拉黑后仍能发送是否只判断了一个方向查询user_block双向判断,发送和被发送都要拦截
消息内容为空也发送成功接口参数校验缺失看 Controller 是否有@Validated增加参数校验和内容长度限制

当线上出现“发不出消息”时,不要一开始就怀疑 Redis 或数据库。先构造最小测试用例,把发送者、接收者、策略、关系四类数据列出来,再按权限流程走一遍,通常能很快定位到是数据问题还是逻辑问题。

6. 从最小实现到生产环境:反骚扰、安全与可观测清单

6.1 学习环境和生产环境的差异

最小实现里为了让流程简单,很多环节都是直接查库、单机计数。生产环境的私信系统还要考虑数据量、安全合规和用户体验。

维度学习环境/演示项目生产环境
关系链查询每次直查 MySQL使用 Redis 缓存关系,异步同步关注变更
频控单维度 Redis 计数多维限流,支持滑动窗口、多级阈值
消息落库单表 MySQL分表分库,或接入消息队列
登录态客户端传 senderId从 token/Session 解析用户身份
内容安全不处理敏感词过滤、图片审核、举报机制
日志普通日志traceId 串联,全链路日志
回滚消息撤回、自动清理异常账号

学习过程中可以先把单表链路跑通,但心里要清楚:真正上线前,私信不是“能发出去”就行,还要能控得住、查得清、撤得回。

6.2 反骚扰设计的最佳实践

“不用互关也可以发消息”容易带来两类问题:陌生人广告和批量骚扰。建议从以下几方面补强:

  1. 分层频控。不要只限制发送者单日总条数,还要限制“发送者 to 单个接收者”的频次,以及“接收者每日陌生人私信总数”。比如同一发送者 1 分钟内只能给同一个人发 3 条。
  2. 重复内容识别。对发送内容做 MinHash 或 SimHash,短时间相似内容过多时提示稍后发送。
  3. 用户反馈入口。接收者可以对陌生私信选择“不喜欢”或“拉黑”,连续被拉黑多次后自动触发发送者风险标记。
  4. 异步审核。文本和图片先落库,再进入异步审核队列,审核通过后展示;风险内容直接标记处理。
  5. 不要用无上限的ALL策略。即使产品要“不用互关也能发消息”,也建议默认使用FOLLOWERS或加一层新用户限制,比如新注册 24 小时内只能给互关用户发消息。

不要把策略和业务代码耦合在一起。上线后产品可能会把ALL改成FOLLOWERS,或者增加“仅允许已实名用户发送”,如果策略写在 if-else 里,每次调整都发版,风险很高。推荐把策略配置放到配置中心或数据库,通过枚举和参数组合控制。

6.3 发布和排错可复用清单

上线前检查清单:

  • [ ] 是否所有新用户都有默认隐私策略,默认策略是否偏保守。
  • [ ] 拉黑关系是否双向判断,拉黑后原有会话是否需要隐藏。
  • [ ] 幂等键是否由服务端统一生成,idempotency表是否有唯一索引。
  • [ ] 频控是否覆盖发送者、接收者双维度,Redis key 是否设置过期时间。
  • [ ] 接口是否从登录态获取用户身份,而不是信任客户端传入的 senderId。
  • [ ] 消息内容是否有长度限制和敏感词校验。
  • [ ] 日志中是否包含 traceId、发送者、接收者、策略明细,方便事后排查。
  • [ ] 是否具备消息撤回和举报处理流程。

线上排错清单:

  1. 先看异常信息是业务异常还是系统异常。
  2. 业务异常直接查用户隐私表、关系表、拉黑表,确认数据条件。
  3. 系统异常查日志和 Redis 连接、数据库连接。
  4. 频控问题用 Redis key 确认计数和过期时间。
  5. 消息重复问题优先查幂等表,而不是先查消息表。

回到“不用互关也可以发消息”,它不是一个简单的开关,而是一整套权限策略的组合。真正有价值的做法是把关注关系从硬编码中解耦,通过用户维度的策略配置、双向拉黑、幂等、频控和内容安全,实现“开放却不失控”。在这个最小示例基础上,下一步可以继续扩展多端会话、已读回执、消息撤回、会话列表未读数,以及把策略判断抽成独立的规则引擎。对于刚接触社交产品后端的开发者,先把权限判断顺序和幂等链路跑通,比直接追求高并发更值得投入时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询