1. 先把需求拆开看:太阳码到底难在哪
做小程序这一行,只要涉及到"分享给用户"的需求,绕不开太阳码与二维码生成这件事。太阳码指的是平台开放接口生成的那种圆形小程序码,中间有个小图标,看上去比方形二维码干净;而普通二维码就是标准的方形黑白格子码,任何扫码工具都能识别。我这几年接手过好几个带线下物料、地推、分销、邀请注册的项目,太阳码与二维码生成几乎每次都要重写一遍,原因不是接口难调,是"配置"和"存储"这两段太容易埋雷。
这篇文章讲的就是从接口配置、参数调优、图片生成,到最后落到存储桶、拼出访问地址、落库归档的完整实现。适合三类人看:一是第一次接这个接口、被 40164 和 41030 打懵的后端同学;二是做增长、地推活动,需要批量生成几十万张码的运营侧开发;三是做小程序全栈、需要把这块能力沉淀成公共服务的独立开发者。我尽量把每一步为什么这么做讲透,而不是丢一段代码让你自己悟。
先说说它到底能做什么。核心能力就一条:给定一个场景参数和一个目标页面,平台返回一张图片。听起来简单,但往下拆就复杂了——场景参数有长度上限,页面路径必须在线上存在,码有尺寸和底色的取舍,接口有调用频率限制,图片生成后要有地方存、要有能长期访问的地址、要能追溯哪张码对应哪个业务对象。太阳码与二维码生成这件事,真正的工作量有两成在调接口,八成在接口之外。
一个很常见的误区是把"能生成"当成"做完了"。我见过一个项目,开发环境调通了就上了线,结果活动当天用户量一冲,token 被并发刷新互相顶掉,一半的请求返回无效凭证;还有一次是码能生成,但存到了公有读的存储桶里,被人遍历了 key 把所有码扒了一遍。这些都不是接口本身的问题,是工程化没做全。所以下面我会按照"设计思路、配置鉴权、参数实操、存储落地、工程化、排查"这个顺序往下走。
1.1 圆形码与方形码的能力差异决定了你的选型
很多人上来就问"我该用哪个",其实答案取决于三件事:你要不要带页面路径、你的场景参数有多长、你的物料是什么材质。
方形二维码接口的限制更严,路径参数有明确的字节长度上限,适合那种"扫了直接进某个固定页"的场景;圆形码接口在场景参数上给了三十来个可见字符的空间,页面路径的灵活性也更好,还能控制是否校验页面是否已发布,做灰度的时候特别好用。真要做业务,圆形码是主力,方形码是补充——比如你需要把码印在很小的位置、或者需要极简风格的物料时,方形码的体积和容错更好压。
还有一点容易被忽略:圆形码中间那个图标是平台固定样式,你没法换成自己的 logo。有些人拿到需求说"要把我们品牌 logo 放码中间",这个用官方接口做不到,得走"官方码 + 外围自己合成一张海报"的路子。我一般建议客户别在这上面纠结,官方样式反而更容易被用户识别为"官方的小程序入口",信任度更高。
另外,透明底的选项只在圆形码的 PNG 输出里有效。如果你想把这个码合成到海报上,透明底是刚需;但如果你的海报背景是深色的,透明底生成出来的圆形码会因为本身是深色而"糊"在背景里,扫不出来。这个坑我在一个线下活动的物料上踩过,后面会细说补救方案。
1.2 三个典型场景,技术方案完全不同
第一个场景:每个用户一张专属码。比如分销、邀请注册,用户 A 分享出去的码里带着 A 的标识,别人扫了要能落到 A 的账上。这个场景的特点是量大、实时性要求中等、码本身要长期有效。技术上要解决的是参数编码和去重,不能每次打开页面都重新调接口生成一张,必须要有一层映射缓存。
第二个场景:每个商品/门店/活动一张码。这种量级小很多,但要求码必须长期稳定,不能今天能扫明天扫不了。技术上重点是存储的持久性和路径的不可变性——同一张码反复生成必须是同一张图,不能覆盖,否则印出去的物料就废了。
第三个场景:一次性临时码。比如会议签到、临时核销,用完就失效。这种要看清楚,码本身在平台侧是长期有效的,业务上的"失效"必须由你自己的服务端来控制——扫码进来先查这个场景参数对应的记录状态,失效了就提示。指望码自然过期是不现实的。
这三个场景对应三种存储策略:高频复用的走缓存加 CDN,长期稳定的走不可变路径加长缓存,一次性的走短生命周期加定时清理。选错了不会立刻出问题,但成本会很不一样。
1.3 整条链路的分层设计
我把太阳码与二维码生成拆成五层,从上到下依次是:业务层(谁要码、要什么码)、编排层(缓存查、是否已存在、是否需要新生成)、接口层(换凭证、调生成接口、处理错误)、存储层(上传、拼地址、落库)、分发层(CDN、签名、防盗链)。
层与层之间用明确的入参出参隔开,好处是任何一层出问题都能单独替换。比如后来平台换了接口版本,我只改了接口层,上面四层一行没动;再比如早期用的存储方案成本太高,我换了一家,只改了存储层。
这里有个设计上的取舍值得说:编排层要不要做同步阻塞。如果用户点"生成我的码",同步走完五层大概几百毫秒到一秒多,体验能接受;如果是批量生成十万张,就必须异步,否则接口频率限制和存储上传的耗时会把请求队列堵死。我的做法是统一走异步,前端拿一个任务 ID 轮询,小批量的时候其实也一样快,代码还省了一套。
2. 接口配置与鉴权:第一公里最容易翻车
配置阶段看着枯燥,但九成"线上突然全挂"的事故都埋在这一步。我按实际操作顺序说:先在平台后台开通接口权限、配置好服务器出口 IP 白名单、拿到应用凭证,然后才是代码里的凭证管理。
IP 白名单这个事特别值得强调。平台的生成接口只允许白名单内的 IP 调用,很多人在本机开发时把家里的宽带 IP 加进去了,上线后忘了加服务器出口 IP,结果测试环境一切正常、生产环境全量失败。更麻烦的是容器化部署,出口 IP 可能是 NAT 网关的,也可能随扩缩容变化,必须找运维确认固定出口,把这些地址一次性配全。我现在的习惯是在项目文档里专门留一栏"平台侧配置清单",每次新环境部署照着对一遍。
凭证管理上,应用凭证(那个密钥字符串)绝对不能写进代码仓库,也不能塞进前端。我一般放在配置中心或者环境变量里,并且做一次启动时的校验——如果读不到就直接启动失败,而不是等到第一次调用才报错。宁可起不来,也不要带着残缺配置跑起来,那样排查成本高得多。
2.1 凭证缓存:一个进程内缓存是不够的
接口凭证有有效期,通常两小时。最简单粗暴的做法是每次调用前都去换一次,这样肯定不出错,但你的调用量直接翻倍,而且平台侧对换取凭证本身也有频率限制,高并发下会被限流。
进一步的做法是进程内缓存,用一个全局变量存凭证和过期时间,快过期了再换。单体应用这么干没问题,但只要你的服务扩到两个实例以上,问题就来了:两个实例各自持有凭证,A 刷新之后,B 手里的旧凭证可能立刻失效,于是 B 又去刷新,把 A 刚换的新凭证顶掉,来回踢皮球,日志里全是一片凭证无效。
正确的做法是把凭证放到一个所有实例共享的地方,比如 Redis,加一把分布式锁,只有抢到锁的实例去换,换完写回共享缓存,其他实例读缓存。锁的过期时间要设短一点,比凭证有效期短得多,避免某个实例挂了把锁一直占着。再叠一层本地的兜底缓存,减少对共享缓存的读压力,本地缓存的有效期设得比共享缓存短,比如共享缓存两小时、本地缓存五分钟,这样即使共享缓存被清空,也能撑过一小段时间。
我用过的一个具体参数是这样:共享缓存 TTL 设 7000 秒(比凭证实际的 7200 秒提前一点,留出安全边界),本地缓存 TTL 设 300 秒,分布式锁 TTL 设 20 秒。实测下来高并发下几乎没有出现过凭证互踢。
2.2 优先用稳定版凭证接口
平台后来提供了稳定版的凭证获取接口,跟我们自己在中控缓存里做的那套逻辑思路一致,好处是它内部保证了多次调用拿到的是同一个值(除非强制刷新),不会因为你多调几次就把前面的顶掉。如果你的项目还在用老接口配自己的缓存,建议迁到稳定版,代码能少一大截。
迁移时要注意两点:一是强制刷新参数要慎用,它会让旧凭证立刻失效,只适合在明确检测到凭证异常时用一次;二是稳定版接口返回的字段名和老接口不完全一样,过期时间的字段要重新对齐,别直接复制粘贴。
# 凭证获取的骨架写法,参数名以实际文档为准 import time import requests def fetch_stable_token(app_id, app_secret, force=False): url = "https://api.example-platform.com/cgi-bin/stable_token" payload = { "grant_type": "client_credential", "appid": app_id, "secret": app_secret, "force_refresh": force, } resp = requests.post(url, json=payload, timeout=5) data = resp.json() if "access_token" not in data: raise RuntimeError(f"换取凭证失败: {data}") # 提前 200 秒视为过期,避免临界点上失效 return data["access_token"], int(time.time()) + data["expires_in"] - 200这段代码本身不复杂,关键在调用它的地方要包一层"读缓存 → 未命中 → 抢锁 → 双检 → 换凭证 → 写缓存"的流程。双检这一步别省,很多人抢到锁之后直接就换了,结果排队等锁的实例进来又换一次,白换。
2.3 出口网络与超时策略
调用生成接口的服务,网络出口最好单独配一个稳定的通道,别跟大流量的业务接口挤在一起。有一次我们把生成任务放在一个跑批量导出的机器上,那台机器出口带宽被占满,生成接口的响应时间从 200 毫秒涨到 8 秒,超时重试叠加上去直接把接口频率打满。
超时设置上,连接超时和读超时分开设,我一般连接设 2 秒、读设 8 秒。生成接口是个相对重的操作,读超时设太短会大量误判为失败;但也不能太长,否则慢请求会堆积把连接池占满。重试策略上,只对明确的"系统繁忙"类错误重试,参数错误类的重试毫无意义,只会白白消耗额度。重试次数两次足够,且必须带退避,间隔加一点随机抖动。
3. 生成环节:参数错一个字,码就废了
接口调通之后,真正决定成败的是参数。太阳码与二维码生成的参数不多,但每一个都有讲究,我按踩坑频率从高到低讲。
3.1 场景参数的长度红线与编码设计
场景参数是整张码的灵魂,扫码进来后你的服务端靠它还原业务上下文。它有明确的可见字符长度上限,超了要么报错要么被截断——被截断最坑,因为你拿到了一张能扫但参数不对的码,线上表现就是"用户扫了进错页面"。
三十来个字符看起来不少,实际一放业务字段就紧张。我见过直接把 JSON 塞进去的,一个括号一个引号就吃掉好几个字符。合理的做法有两种:一是短 ID 映射,生成一个短码(比如几位随机串)当 key,把完整上下文存到你自己的表里,码里只放这个短码;二是紧凑编码,把数字 ID 转成高进制字符串,两个字段拼起来用固定分隔符隔开,一般能控制在二十个字符以内。
短 ID 映射的好处是灵活,业务字段怎么加都不影响码;坏处是多一次查询,且映射表要长期维护。紧凑编码的好处是无状态、查询省一次;坏处是业务一变就得重新设计。我的选择标准是:如果一个码的有效期超过半年,用短 ID 映射;如果是短周期活动,用紧凑编码。
还有一点,场景参数里不要放敏感信息。这张码是公开的,任何人都能扫、能截图、能反解出参数内容。曾经有人把用户手机号编码进去,虽然做了变形处理,但只要样本够多就能推出规律。参数里只放无意义的 ID,所有敏感判断都放到服务端做。
# 紧凑编码示例:两个数字字段压进 32 字符以内 ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" def to_base62(num: int) -> str: if num == 0: return ALPHABET[0] out = [] while num: num, rem = divmod(num, 62) out.append(ALPHABET[rem]) return "".join(reversed(out)) def build_scene(uid: int, channel: int) -> str: scene = f"{to_base62(uid)}_{to_base62(channel)}" if len(scene) > 32: raise ValueError("场景参数超长,需要改用短 ID 映射方案") return scene这段逻辑上线前一定要加长度断言,别指望上游会传合法值。我在生产环境见过因为一个渠道 ID 是负数,编码出来带了个减号,扫出来参数解析直接崩掉。
3.2 页面路径与版本控制,灰度期的关键开关
页面路径必须是小程序里真实存在的页面。接口默认会校验,如果你传的路径在线上版本里没找到,直接报"页面不存在"。这个校验在正式发布前是个好东西,能提前发现路径写错;但在开发阶段很烦,因为页面还在开发版里。
所以有个开关叫"是否校验页面",开发阶段关掉,上线前打开。关掉的时候一定要配合版本参数使用——版本参数可以指定生成的是正式版、体验版还是开发版的码。这里有个高频 misunderstanding:开发版的码只有这个小程序的开发者扫码才能打开,普通用户扫了会提示"无法打开"。我见过有人在测试阶段把开发版码发给客户看效果,客户扫了半天打不开,以为项目黄了。
另一个坑是路径带了查询参数。这个接口的页面路径里允许带参数,但要注意参数会被原样拼进小程序的启动路径,如果你的参数里有需要转义的字符,扫码后解析会出问题。我的建议是页面路径只写到页面名,所有业务参数一律走场景参数,两条线不要混。
分享出去的太阳码与二维码,扫码后的落地页要处理好"参数缺失"的情况。用户可能扫的是别人转发时被裁掉一半的图,也可能扫的是一个已下线的活动。落地页拿到参数先做一次完整的合法性校验,不合法就跳到一个兜底页,别让白屏成为用户看到的第一印象。
3.3 尺寸、底色与清晰度:印刷和屏幕是两回事
尺寸参数有下限和上限,默认值在屏幕上够用,但印刷就差远了。地推物料我一般直接拉到上限或者接近上限,生成的图分辨率高,印出来边缘才锐利。屏幕上展示的话,默认值加一定的压缩就足够,图太大反而拖慢首屏。
透明底这个选项要慎重。它的输出是带透明通道的图,合成到海报上很自然,但有两个副作用:一是体积比普通底大不少,二是深色背景上扫不出来。补救方案很简单——在合成海报的时候,先给码垫一层白色圆角底,再合成,视觉上也更像一个"卡片"。
还有个小细节:生成的图是位图,放大到印刷尺寸会有锯齿边缘。如果物料尺寸很大,别指望单靠接口返回的原图,最好在本地做一次矢量重绘或者至少做锐化处理。我一般是在设计稿里直接给码留一个足够大的位置,用高分辨率原图等比缩放,不放大。
4. 存储落地:从字节流到能用的地址
码生成出来只是拿到了一段字节流,真正要交付给业务的是一个能长期访问的地址。这一段我按"放哪、叫什么名、怎么发出去、怎么记"来说。
4.1 路径规划:让同一张码永远是同一个地址
存储路径的设计决定了后续所有事情。我的原则是:内容寻址。先算图片内容的摘要值(比如 MD5 或者 SHA256 的前若干位),用摘要作为文件名的一部分。这样只要内容一样,路径就一定一样,天然去重;内容有一点点不同,路径就不同,不会互相覆盖。
具体结构我用的是"业务类型/年月/摘要前两位/摘要.扩展名"。中间加一层摘要前两位的目录,是为了避免同一个目录下文件过多——对象存储的单目录文件数太多时,某些操作系统的列举性能会下降。年月这一层纯属运维友好,方便按时间做生命周期管理和成本核算。
改了底色、改了尺寸、改了页面,内容都会变,路径跟着变,这就是我要的效果:任何一张曾经发出去的码,它的地址永远有效,永远不会被新内容覆盖。曾经有人用"用户 ID 作为文件名"来存,看着更直观,但用户换了个邀请页面重新生成,就把旧码覆盖了,之前印出去的物料全部失效。这个教训我记了很久。
import hashlib def build_storage_key(biz_type: str, img_bytes: bytes, ext: str = "png") -> str: digest = hashlib.sha256(img_bytes).hexdigest() year_month = time.strftime("%Y%m") return f"{biz_type}/{year_month}/{digest[:2]}/{digest}.{ext}"用 SHA256 而不是 MD5,是因为有些存储平台在做内容校验时会用更强的摘要,顺手统一了。截取前两位做目录、完整摘要做文件名,长度可控。
4.2 公有读还是私有桶签名:这是一道安全题
从存储对外怎么发出去看,有两条路。公有读最简单,拼一个固定域名加路径就能访问,CDN 缓存也最省心;代价是任何人拿到地址都能访问,而且如果路径有规律,可能被批量遍历。
私有桶加临时签名地址安全,但地址带过期时间,意味着你不能直接把地址印在物料上,也不能长期缓存在客户端。对于太阳码这种要长期稳定可访问的图片,私有桶其实不太合适——难道用户扫码前还要先换一次签名?那就得在扫码链路里再插一跳,得不偿失。
我的做法是:码图本身放公有读的桶,但用两层保护。第一层是路径不可枚举——因为文件名是内容摘要,没有规律,遍历不出来;第二层是在分发层配防盗链和频率限制,限制来源域名和单 IP 的请求速率。这样既保住了长期可访问,又把被刷的风险压下去。
如果业务上确实要求"码不能被人随便拿到",那就别把码当图片存,改成扫码后由服务端校验再跳转。码里只放一个短 ID,短 ID 到真实业务的映射放在服务端,服务端做权限判断。这个思路比在图片层面做防护靠谱得多。
4.3 数据落库与状态机
每生成一张码,我都要落一条记录,字段大概包括:业务类型、业务 ID、场景参数、页面路径、版本、尺寸、内容摘要、存储 Key、访问地址、状态、创建时间、失效时间。这份记录是所有后续排查的依据,缺一个字段后面就要靠猜。
状态机我设成三个状态:生成中、可用、失效。"生成中"是为了解决并发重复生成——两个请求同时判断出"没有这张码",都会去调接口。用业务维度做一个唯一索引,插入"生成中"记录时如果冲突,就说明有人在做,第二个请求直接等结果或者返回处理中,避免重复调用浪费额度。
"失效"这个状态不是码本身过期,是业务上不再使用了。比如某个活动下线,对应的码记录标成失效,扫码进来提示活动已结束。存储上的图片先别急着删,保留一段时间以应对可能的复查,之后交给生命周期规则自动清理。我一般设 90 天,足够覆盖绝大部分售后追溯需求。
| 字段 | 类型 | 说明 |
|---|---|---|
| biz_type | 字符串 | 业务类型,如 invite、store、activity |
| biz_id | 字符串 | 业务主键,与 biz_type 组成唯一索引 |
| scene | 字符串 | 场景参数,长度受接口限制 |
| page | 字符串 | 落地页路径 |
| env_version | 字符串 | 正式版、体验版、开发版 |
| digest | 字符串 | 图片内容摘要 |
| storage_key | 字符串 | 存储对象路径 |
| url | 字符串 | 对外访问地址 |
| state | 枚举 | 生成中、可用、失效 |
| expire_at | 时间 | 业务失效时间 |
5. 工程化:让这条链路能扛量
单张生成谁都会写,难的是批量和高并发下不出事。这一节讲三件我在实际项目里反复调整过的事。
5.1 同步、异步与批量预生成怎么分工
用户主动点"生成我的码"这种请求,如果缓存未命中,可以同步调接口,几百毫秒能接受,但要加一个总超时保护,超了就直接返回"生成中,稍后查看",把结果写进异步任务。这样用户不会看到转圈转十秒。
批量场景必须异步。我一般的结构是:业务侧把要生成的记录批量写进队列,消费端按固定速率消费,每次处理一条,处理完把结果写回业务表。消费速率要按接口的调用额度来定,别按机器的处理能力来定——接口额度的上限是硬的,超了就是一片报错。
预生成是个很划算的优化。商品上架、门店创建、活动发布这些动作发生时,顺手丢一个生成任务进队列,等用户真正要分享的时候,码已经在库里了,直接用。这个改造让我们的分享点击到出图的耗时从平均 800 毫秒降到了 30 毫秒左右。
5.2 缓存分层:别让同一张码被生成两次
缓存我分了三级。第一级是业务表的唯一索引,保证同一个业务对象只会有一条"可用"记录;第二级是内容缓存,用业务类型加业务 ID 拼 key,存图片的地址,TTL 设得比业务失效时间稍长;第三级是分发展面的 CDN 缓存,因为路径是内容寻址的,可以直接设成永久缓存,不用担心更新问题。
这里的关键是"内容不变"这个前提。一旦你允许同一张码被重新生成覆盖,CDN 缓存立刻变成灾难。所以路径设计那一步做对了,缓存这块就基本没有心智负担。
缓存击穿也要防。某个热点码的缓存过期瞬间,可能有几十个请求同时穿透到存储层。用互斥锁把第一个请求放进去重建,其他请求短暂等待或者返回旧值。这个套路很经典,但在太阳码这个场景里特别值得做,因为重建意味着调接口、上传、落库,成本比读一次缓存高好几个数量级。
5.3 监控指标:出问题之前就能看见
我在这个链路上固定埋了几个指标:接口调用成功率、平均耗时、凭证刷新次数、存储上传失败率、缓存命中率、平均每张码的生成成本。
凭证刷新次数这个指标特别有用。正常情况下每小时每实例刷新不超过两次,如果某段时间突然飙升,多半是缓存出了问题或者有人在强制刷新,早发现能省一次事故。存储上传失败率和缓存命中率是配套看的,命中率掉下来意味着成本在涨。
告警阈值我给的是:接口成功率低于 98% 持续五分钟告警,凭证刷新次数十分钟内超过二十次告警,上传失败率超过 1% 告警。这几个数不是拍脑袋,是跑了半年之后按实际波动区间定的,你们上线后可以先用更宽松的阈值,跑一段时间再收紧。
6. 排查实录:这些坑我基本都踩过
前面讲的是"应该怎么做",这一节讲"做错了会看到什么"。我把这几年遇到的高频问题整理成一张表,配合几个典型案例。
6.1 错误码与现象速查
| 现象 | 常见原因 | 处理方向 |
|---|---|---|
| 提示 IP 不在白名单 | 服务器出口 IP 未配置,或容器化后出口变化 | 找运维确认固定出口,全部加入白名单 |
| 凭证无效或频繁失效 | 多实例各自刷新互相顶掉,或用了强刷参数 | 改中控缓存加分布式锁,慎用强制刷新 |
| 页面不存在 | 路径写错,或页面未发布,或校验开关开着但页面在开发版 | 核对路径,开发阶段关校验并指定版本 |
| 场景参数非法 | 长度超限,或含不允许的字符 | 加长度断言,改用短 ID 映射或紧凑编码 |
| 尺寸不合法 | 传了超出范围的宽 | 限制在允许区间内,默认值附近最稳 |
| 调用频率超限 | 批量任务没限速,或重试风暴 | 消费端按额度限速,重试加退避和抖动 |
| 系统繁忙 | 平台侧瞬时压力 | 退避重试两次即可,别无限重试 |
6.2 扫不出来的五类原因,按概率排序
第一类,版本不对。开发版或体验版的码,普通用户扫不开。这个占比最高,尤其是发给客户预览的时候。
第二类,路径对但参数丢了。页面路径里带了查询参数,扫码后解析失败,落到白屏。检查一下启动参数的实际值,别只看文档。
第三类,图被压缩糊了。物料印刷时把图缩得太小,或者为了省流量压得很狠,码的定位图案糊掉。太阳码对清晰度的要求比想象中高,最小印刷尺寸我一般控制在两厘米以上,且四周留足空白。
第四类,透明底叠深色背景。前面说过,垫一层白色底能解决九成问题。这个坑的成本是重印物料,很痛。
第五类,落地页逻辑把用户拦住了。码是好的,扫进去了,但落地页判断用户未登录直接跳登录,登录完没带回来,看起来像"扫码没反应"。这种要在登录跳转里做好参数透传。
6.3 存储侧的三个典型坑
第一个坑是覆盖。前面说过,用业务 ID 当文件名,重新生成就覆盖,旧物料全废。改成内容寻址之后彻底解决。
第二个坑是权限配错。桶设成了公有读且开了目录列举,等于把全站码公开。检查一下列举权限和目录索引,这两个默认是关的,但有人为了调试方便打开后忘了关。
第三个坑是缓存不一致。图片更新了但 CDN 还是旧的,原因是用固定路径加短缓存。内容寻址加长缓存之后,这个问题从根上消失。
注意:改存储路径规则属于不可逆操作。老路径的图还会被访问,新规则只对新生成的生效,切换前先确认老路径的访问量,必要时保留一段时间双写。
7. 上线前我自己会过一遍的检查项
这套清单是我贴在项目文档第一页的,每次上线前对着走一遍,能挡掉绝大多数低级问题。
配置侧:生产环境的出口 IP 全部在白名单里;应用凭证走的是配置中心不是代码;凭证缓存接的是共享存储不是进程内变量;强制刷新参数在正常链路里没有出现。
接口侧:场景参数有长度断言;页面路径有单测覆盖;校验页面开关在生产是打开的;版本参数生产环境固定为正式版;超时和重试策略配了,重试有退避。
存储侧:路径是内容寻址的,不随业务 ID 变化;桶的列举权限是关的;CDN 缓存策略是按路径不可变设的;失效记录有清理规则。
业务侧:落地页对参数缺失有兜底;登录跳转会回传原始参数;失效的码有明确的提示文案;数据库有唯一索引防止并发重复生成。
监控侧:成功率、耗时、凭证刷新次数、上传失败率四个指标都在看板上;告警阈值配了;有至少一条根据业务 ID 查码记录的排查路径。
我自己踩过最深的一个坑,是一个急需上线的活动,所有检查项都过了,唯独忘了确认"出口 IP 是固定还是弹性"。上线当天扩了三个实例,新实例的出口 IP 不在白名单里,三分之一的请求直接失败,日志里一片报错。后来加了出口固定,还在启动脚本里加了一次自检调用,起不来就报警。这个自检的逻辑很简单,就是启动时用最小参数生成一张测试码,成功就继续,失败就重试三次然后退出。加上之后,这类配置问题再没在线上出现过。
另外分享一个我常用的小技巧:给每张生成的码在角落叠加一个极小的版本水印,肉眼几乎看不见,但你自己知道哪个批次对应哪次改版。排查"为什么这张码扫出来的行为和预期不一样"的时候,看一眼水印就知道是不是老版本生成的,比翻数据库快得多。这个水印要放在不影响识别的位置,且透明度压到很低,具体参数按实际出图效果调。