《从零开发校园墙微信小程序》进阶篇之一。前面说过,「墙」建在微博之上。这篇讲后端到底是怎么和微博开放平台打交道的:令牌从哪来、怎么发一条微博、怎么把别人 @ 墙号的微博转发上墙,以及我为了不被微博风控踩过的坑。

一、先理清:OAuth2.0 在这里扮演什么角色

微博开放平台的所有接口都靠 OAuth2.0 的access_token 鉴权。标准的授权码流程是三步:

1. 引导用户(这里是墙号的账号主人)打开授权页,同意授权
2. 授权通过后,微博把 code 回调给你的应用
3. 应用用 code(加上 App Key / App Secret)去换 access_token

之后调用任何接口,都把 access_token 作为参数带上。
本项目的做法要说清楚 :后端不做 授权跳转和回调,它只是消费 已经拿到的令牌。令牌被配置成一个列表(pzforest.access-token),后端在调用时从里面轮询取用:

@Component
@ConfigurationProperties(prefix = "pzforest")
public class WeiboConfig {
    private static List<String> accessToken = Collections.emptyList();
    ...
    public void setAccessToken(List<String> accessToken) {
        WeiboConfig.accessToken = accessToken == null ? Collections.emptyList() : accessToken;
    }
}

为什么是一个列表 而不是一个令牌?因为微博的调用额度是按「应用 × 接口」 计算的,单个应用的额度很容易被打满。项目里一共配置了 5 个来自不同应用 的令牌,额度彼此独立,轮着用、互相兜底。这个设计是下一篇(第 10 篇)的主角,这里先只讲接口调用本身。

部署提示:令牌属于密钥,和数据库密码一样不能入库 。它通过 application-local.yaml 或环境变量注入,漏配的现象是:服务正常启动、健康检查通过,但发帖和评论全部报错(见第 8 篇)。

二、项目用到的接口

作用域接口用途
写statuses/share.json发微博(文字 / 带图)
写comments/create、comments/reply评论、回复评论
写statuses/destroy、comments/destroy管理端删帖、删评论
读statuses/mentions.json拉取 @ 墙号的微博
读comments/show、statuses/count、statuses/show读评论、计数、单条详情

注意「读 / 写」这个分类,它不是随便分的:后面做限流退避时,读和写的额度要分开记 。

三、发一条微博:statuses/share.json

文字微博

return weiboShareClient().post()
        .uri("https://api.weibo.com/2/statuses/share.json")
        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
        .body(BodyInserters.fromFormData("status", safeText + " http://your-safe-domain.example")
                .with("access_token", accessToken)
                .with("rip", "<你的服务器出口 IP>"))
        .retrieve()
        ...

有几个参数值得解释:

  • status 末尾要追加应用的「安全域名」链接。 微博的分享接口要求发布内容里必须含有一个属于该应用有效安全域名的网址,否则会被拒绝。所以代码统一在正文后面补上这个链接。
  • rip :调用方的真实 IP,微博用它做风控判断。填的是服务器的出口 IP。
  • 文字内容有长度限制(约 140 字),所以后面「@ 转发」里会对超长内容直接跳过。

带图微博:改用 multipart

MultipartBodyBuilder mb = new MultipartBodyBuilder();
mb.part("access_token", accessToken);
mb.part("rip", "<你的服务器出口 IP>");
mb.part("pic", pic, MediaType.parseMediaType(contentType));
mb.part("status", (mode != null && mode == 1) || safeText.isEmpty()
        ? "http://your-safe-domain.example" : safeText + " http://your-safe-domain.example");

这里的 mode == 1 就是第 5 篇说的「文字已经画进图片里」:这时微博正文只放安全域名链接,不重复文字。
读取图片字节、上传 MinIO 这些阻塞步骤 放在 boundedElastic 里,真正调微博再用 WebClient 响应式发出。

成功之后:镜像落库

Status temp = JSONUtil.toBean(responseBody, Status.class);
temp.setUserId(userObj.get("id").toString());
temp.setCreatedTime(LocalDateTime.now());
statusService.saveOrUpdate(temp);                       // 微博镜像
statusUserService.AddStatusUser(openId, "1", weiboId);  // 记录真实提交人

微博返回的 JSON 直接映射成 Status 实体写进 weibos_status;同时用 status_user 记下「这条微博是哪个 openid 发的」。AddStatusUser 的第二个参数用来区分来源(小程序投稿,还是 @ 转发进来的),后台才能分别统计。

四、一个很容易写错的重试策略

调外部接口都会想加重试。但发帖接口不能乱重试 ,因为微博的 share.json 没有幂等键 ——同一条内容请求两次,就会发两条微博。项目里的策略是:

/** 发微博重试策略:连接失败时最多重试 2 次,指数退避 1s->2s->4s;其他异常不重试直接抛 */
private static Retry weiboShareRetry() {
    return Retry.backoff(2, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(4))
            .filter(WeiboServiceImpl::isRetryableConnectError);
}

private static boolean isRetryableConnectError(Throwable t) {
    // 只认【连接层】失败:ConnectException / UnknownHostException / ConnectTimeoutException
    ...
}

关键区别:

  • 连接失败 (连不上、DNS 解析失败):请求根本没有发出去 ,重试是安全的;
  • 读超时 :请求可能已经到了微博并且处理成功了 ,只是响应没回来。这时重试就会重复发帖,所以不重试 ,而是把明确的错误告诉用户「可能已发出」。
通用原则:对没有幂等保护的写操作,只重试「确定没有副作用」的失败。

五、@ 转发:把别人 @ 墙号的微博转上墙

墙号除了接收小程序投稿,还会被微博用户直接 @。这些 @ 也应该被转发到墙上,这是一个定时任务 handleAt:

每隔一段时间:
  拉取 statuses/mentions.json(最近 @ 墙号的微博)
  逐条判断 → 清洗文案 → 转发到墙 → 记录

逐条处理:forwardMention

private boolean forwardMention(JSONObject mention) {
    String text = mention.getStr("text");
    if (text.contains("转发微博")) return false;           // 本身是转发,跳过,避免循环转发
    String cleanedText = text.replace("@", "")...;         // 去掉 @ 墙号、安全域名链接等
    String weiboId = mention.get("idstr").toString();
    if (!ObjectUtils.isEmpty(statusService.selectByWeiboId(weiboId))
            || cleanedText.length() >= 140) {
        return false;                                       // 已转发过 / 文案超长
    }
    // 有原图就下载图片后带图转发,否则转发文字
    ...
}

三个判断,各有各的原因:

  1. 跳过「转发微博」 :否则转发出来的内容又被 @ 到,无限循环;
  2. 按本地镜像去重 :selectByWeiboId 查一下这条微博是不是已经转过了。这就是前面「镜像到自己库」的另一个好处——去重不需要额外存状态;
  3. 跳过超长文案 :超过 140 字发不出去。

反风控:每轮只转一条

这是这篇最有「教训」价值的部分。最初的实现是 for 循环里零间隔 逐条转发。某天凌晨积压了一批 @,一口气连发了十几条,44 秒内连转 9 条,微博随即判定「账号行为异常」,要求验证账号 。
改进后的策略写在代码注释里,值得完整抄下来:

// 节流方式:每轮只转 maxPerRound 条(默认 1),拉取间隔由动态配置
// weibo.handle-at.interval-seconds 决定(默认 600 秒)。
// 于是稳态节奏 = 每轮 N 条 / 每 10 分钟,积压时也是 10 分钟一条地匀速补。
int maxPerRound = dynamicConfigService.getInt("weibo.handle-at.max-per-round");
int forwarded = 0;
for (int i = 0; i < statuses.size(); i++) {
    if (forwarded >= maxPerRound) break;
    if (forwardMention(statuses.getJSONObject(i))) forwarded++;
}

注意三个细节:

  • 没转完的不丢 :它们仍在微博的 mentions 列表里,下一轮还会拉到,靠本地镜像去重,所以不需要自己存「待转发队列」 ;
  • 绝对不能用Thread.sleep 拉开相邻两条:Spring 的调度线程池默认只有 1 个线程,在上面睡几分钟,会把同一个池里的教务登录队列、反馈汇总邮件一起卡死(第 2 篇配置里把调度池调成 4 的原因就在这)。要放慢,靠「每轮上限 × 拉取间隔」,而不是在线程里等;
  • 节流参数做成动态配置 ,管理后台可调,不用发版。

防止任务重叠

public void handleAt() {
    // 上一轮还没跑完就跳过本轮:转发要逐条下载图片 + 发微博,落后时
    // fixedRate 会把任务不断堆积,既抢微博配额也拖垮调度线程。
    if (!HANDLE_AT_RUNNING.compareAndSet(false, true)) {
        log.warn("handleAt 上一轮尚未结束,跳过本轮");
        return;
    }
    try { doHandleAt(); } finally { HANDLE_AT_RUNNING.set(false); }
}

用一个 AtomicBoolean 做「正在运行」标志:上一轮没结束就跳过。finally 里一定要复位,否则一次异常之后任务永远不再运行。

熔断时不去拉

if (upstreamHealth.isCoolingDown(WeiboConfig.SCOPE_READ)) {
    log.info("微博上游处于熔断期,跳过本轮 @ 拉取");
    return;
}

@ 转发不急 ,而额度很宝贵。当微博正在限流时,应该把配额优先留给用户发帖和评论,这条后台任务就主动让路。

下载图片必须设超时

转发带图微博要先把原图下载下来:

URLConnection connection = new URL(url).openConnection();
connection.setConnectTimeout(5_000);
connection.setReadTimeout(15_000);

这两行超时是事故换来的 。微博图床偶尔只建连不回包,裸的 openStream() 没有超时,会把调度线程永久挂住,连带同一线程上的教务登录队列、反馈汇总一起停摆。凡是在后台线程里发起的网络调用,一律显式设置连接和读取超时。

六、管理端删除:对「已经不存在」宽容

管理员删微博时,先调微博的 statuses/destroy,成功后再清理本地镜像:

  • 微博返回「微博不存在」(错误码 20101)→ 视为已删除 ,继续清本地;
  • 其他平台错误 → 直接返回 502,不 清本地,避免出现「微博上还在、本地已经没了」的不一致;
  • 确实只想清本地时,提供 localOnly 选项。

删除接口的幂等性 很重要:重复点击删除、网络重试,都不应该报错。

小结

  1. 微博 OAuth2.0:本项目后端只消费 access_token,令牌是列表 ,来自多个应用以分摊额度;
  2. share.json 要带应用安全域名,带图用 multipart;
  3. 无幂等键的写接口,只重试确定没发出的失败(连接层),读超时不重试 ;
  4. 成功后立刻镜像落库,并记录真实提交人;
  5. @ 转发:跳过转发微博、本地去重、限制每轮条数 、不在调度线程 sleep、防任务重叠;
  6. 后台线程里的网络调用必须有超时;
  7. 删除要幂等,对「已不存在」宽容。

下一篇讲多个令牌之间怎么分摊额度、被限流后怎么退避,这是整个微博接入里最精巧的一块。

最后修改:2026 年 11 月 12 日
如果觉得我的文章对你有用,请随意赞赏