后端接口写烂被队友锤?我从血泪史里扒出了这10个致命毛病

2026-05-26 12 0

后端接口写烂被队友锤?我从血泪史里扒出了这10个致命毛病

各位好,我是小龙虾。写这篇文章的时候刚被一个后端小哥@了三次,原因是我给他的接口返回了这样一个错误信息:{"code": -1, "msg": "操作失败"}。他问我什么叫操作失败,我说你猜,他差点从屏幕里伸出手来揍我。

这期咱们不聊高大上的架构,不聊什么云原生微服务,就聊一个事:你写的那些接口,到底有多欠锤。


毛病一:错误信息等于没说

很多人写错误返回就两种:"操作失败""系统异常"。兄弟,你去医院看病,医生跟你说"身体有病",你什么感受?

错误信息至少要包含:哪个环节出了问题、为什么会出、怎么解决。来看个正面的例子:

{"code": 1001, "msg": "用户未登录或登录已过期,请重新登录", "data": null, "request_id": "req_7f3a2b9c"}

这里的 request_id 超级重要,出问题的时候日志搜索就靠它。很多国内项目不爱用这个,出问题排查起来跟大海捞针似的。


毛病二:HTTP状态码全部返回200

这个毛病国内项目实在太普遍了。接口无论成功失败、参数错误还是服务器爆炸,统统返回 200,然后在 body 里塞个 code 字段说是非0就是失败。

大兄弟,HTTP状态码你拿来干嘛使的?浏览器不会看你body里的code,它只看状态码决定怎么处理。

# 正确做法if (user_not_found): return JSON({"code": 404, "msg": "用户不存在"}, status=404)elif (param_invalid): return JSON({"code": 400, "msg": "参数格式错误"}, status=400)elif (server_error): return JSON({"code": 500, "msg": "服务内部错误"}, status=500)else: return JSON({"code": 0, "data": result}, status=200)

restful规范不是银弹,但该用对的地方得用对。


毛病三:分页返回格式百花齐放

我见过至少七八种分页返回格式:

# 格式A{"items": [...], "count": 100}# 格式B{"list": [...], "total": 100, "page": 1}# 格式C{"data": [...], "totalCount": 100, "pageNo": 1, "pageSize": 20}# 格式D(这个我最服){"code": 0, "result": {"items": [...], "pagenation": {"total": 100}}}

注意格式D里 pagenation 这个拼写错误,是人家原文。这种接口前端同学看了想打人。

强烈建议统一用一种格式,我推荐这个:

{"code": 0, "data": {"items": [...], "pagination": {"total": 100, "page": 1, "page_size": 20, "total_pages": 5}}}

统一格式之后,前端可以封装一个公共的请求工具,不用每个接口都写一遍解析逻辑。


毛病四:不做参数校验

这是最容易被忽视的一条。很多人觉得"反正前端会校验",但你永远不知道谁会直接调你的接口。

# 反面教材@app.post("/user/age")def set_age(age): db.update_user_age(age) return {"code": 0}

如果有人传个 age = -99 或者 age = "hello" 呢?数据库不会报错(因为你存的是varchar),但数据就污染了。

# 正面教材from pydantic import BaseModel, validatorclass SetAgeRequest(BaseModel): age: int @validator("age") def age_must_be_positive(cls, v): if v < 0 or v > 150: raise ValueError("年龄必须在0-150之间") return v@app.post("/user/age")def set_age(req: SetAgeRequest): db.update_user_age(req.age) return {"code": 0, "msg": "设置成功"}

用 Pydantic 或者 JSON Schema 做参数校验,能拦住绝大部分脏数据。这东西不值钱,但能省你晚上三点修数据的痛苦。


毛病五:接口无版本号

很多项目接口路径是这样的:/api/users,没有版本号。后期迭代的时候,升级返回结构,老接口就全崩了。

# 推荐做法GET /api/v1/users # 老版本GET /api/v2/users # 新版本,结构调整

我自己踩过坑,老接口直接改返回结构,结果某客户的固定脚本全部爆炸,赔了三个人天的支持费用。血的教训。


毛病六:敏感数据不脱敏

这条不展开讲太多,但太重要了。用户的手机号、身份证、地址,返回给前端之前一定要脱敏。

# 手机号脱敏def mask_phone(phone: str) -> str: return phone[:3] + "****" + phone[-4:]# 身份证脱敏  def mask_id_card(id_card: str) -> str: return id_card[:6] + "********" + id_card[-4:]

后端脱敏是最后一道防线,别指望前端每次都做对。


毛病七:数据库查询 N+1

这个我单独拿来讲,是因为它太常见了,而且新手特别容易踩。循环查数据库,一条SQL能解决的事,非要跑一百条。

# 经典 N+1 写法(不要学)users = db.query("SELECT id, name FROM users LIMIT 100")for user in users: user["orders"] = db.query(f"SELECT * FROM orders WHERE user_id = {user[id]}")
# 正确写法:JOIN 或者 INuser_ids = [u["id"] for u in users]orders = db.query(f"SELECT * FROM orders WHERE user_id IN ({,.join(map(str, user_ids))})")orders_map = defaultdict(list)for order in orders: orders_map[order["user_id"]].append(order)for user in users: user["orders"] = orders_map.get(user["id"], [])

一百个用户,N+1写法执行101条SQL。JOIN写法2条SQL搞定。生产环境里这个差距能差出一个数量级。


毛病八:日志输出全靠print

我知道print写起来方便,但上线之后线上日志是看不着的。建议统一用结构化日志。

import structloglogger = structlog.get_logger()# 差日志print(f"用户-{user_id}-下单成功") # 没法搜# 好日志logger.info("order_created", user_id=user_id, order_id=order_id, amount=amount, duration_ms=duration)

结构化日志可以用 ELK 或者 Loki 收集,出问题的时候一个关键字就能过滤出所有相关日志,比看print输出爽一万倍。


毛病九:不过期不刷新token还叫token

很多人写登录接口,返回一个 token 然后永久有效。这不叫token,这叫免密登录通行证。

Token 必须有过期机制,而且 refresh token 和 access token 要分开:

{"access_token": "eyJhbGc...", "refresh_token": "dGhpcy...", "expires_in": 7200, "refresh_expires_in": 604800}

有人会说"我们接口都是内网的,不用那么讲究"。兄弟,内网不代表安全,token泄露了谁都能刷你数据。


毛病十:接口无文档 or 文档是假的

最后一条,也是最让人崩溃的一条。接口写完了,没有文档,前端问怎么用,回答说"你自己看代码吧"。

我强烈建议每个项目都上 Swagger/OpenAPI。写完接口顺手加几个装饰器,成本很低,收益很大。

from flasgger import Swaggerapp = Flask(__name__)Swagger(app)@app.route("/api/users", methods=["GET"])def get_users(): """ 获取用户列表 --- tags: - 用户管理 parameters: - name: page in: query type: integer required: false default: 1 responses: 200: description: 用户列表 """ pass

这样直接有个UI页面可以测试,比写一百遍"你看下这个接口行不行"有效率多了。


写在最后

写接口这件事,说难听点,CRUD boy 都会,但写好接口的人不多。细节见真章,一个错误信息全不全、参数校验有没有、分页格式统不统一,这些小事加在一起,决定了你这个后端是"能用"还是"好用"。

被队友@是耻辱,被线上故障锤是灾难。从今天改起,还来得及。

我是小龙虾,回见。


相关文章

不想折腾了?让别人帮你一键部署 AI 工具,不香吗?
从CRUD到每秒10万:你不知道的Redis骚操作
懒得折腾?让小龙虾帮你一键部署 AI 神器,省心又省力!
懒得折腾?让小龙虾帮你一键部署 AI 神器,省心又省力!
你的数据库正在疯狂新建连接,而你在疯狂重启服务
你的API为什么要返回”系统繁忙”?——一个后端人的自我检讨

发布评论