一、 为什么电商系统需要独立的API层
电商系统通常服务多个前端:Web端、H5、小程序、APP,未来还可能接入第三方开发者。传统的"前端直接调用后端Service"模式会带来三个问题:
-
接口分散:每个前端独立对接后端,接口逻辑散落各处
-
无法复用:同样的功能在不同端重复开发
-
无法开放:第三方接入时,需要重新梳理所有接口
因此需要建设独立的API层------对内统一多端调用,对外提供商业化能力。
分层架构:
┌─────────────────────────────────────────────┐
│ 前端层:Web │ H5 │ 小程序 │ APP │ 第三方 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ API网关层 │
│ 鉴权 │ 限流 │ 路由 │ 日志 │ 灰度 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ API服务层 │
│ 商品API │ 订单API │ 用户API │ 支付API │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 业务服务层(微服务) │
│ 商品中心 │ 订单中心 │ 用户中心 │ 支付中心 │
└─────────────────────────────────────────────┘
二、 API设计规范
2.1 RESTful路径规范
GET /api/v1/products → 商品列表
GET /api/v1/products/{id} → 商品详情
POST /api/v1/orders → 创建订单
GET /api/v1/orders/{id} → 订单详情
PUT /api/v1/orders/{id}/pay → 支付订单
DELETE /api/v1/cart/{skuId} → 删除购物车
版本管理:/api/v1/ → 大版本迭代时升级至 /api/v2/
2.2 统一响应格式
json
{
"code": 0,
"msg": "success",
"data": {
"orderId": "ORD20240101001",
"status": "PAID"
},
"timestamp": 1704000000000,
"requestId": "req-abc-123-def"
}
状态码设计:
| code范围 | 含义 | 示例 |
|---|---|---|
| 0 | 成功 | 正常返回 |
| 1xxx | 参数错误 | 1001=参数缺失 1002=参数格式错误 |
| 2xxx | 业务错误 | 2001=库存不足 2002=订单不存在 |
| 3xxx | 权限错误 | 3001=未登录 3002=无权限 |
| 5xxx | 系统错误 | 5001=系统繁忙 5002=服务不可用 |
2.3 字段命名规范
| 规范 | 示例 |
|---|---|
| 使用小驼峰(camelCase) | orderId, userName, createTime |
| 时间统一使用时间戳(毫秒) | 1704000000000 |
| 金额使用整数(分) | 9900 表示 99.00 元 |
| 布尔使用 is 前缀 | isPaid, isDeleted |
三、 统一鉴权
3.1 三种认证方式
| 方式 | 适用端 | 特点 |
|---|---|---|
| JWT Token | APP、Web端 | 无状态,携带用户信息,有效期7天 |
| Session Cookie | Web端 | 有状态,依赖Redis存储 |
| AppKey + AppSecret | 第三方开发者 | 用于API调用,可设置不同权限 |
3.2 JWT认证实现
java
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String token = extractToken(request);
if (token != null) {
try {
Claims claims = JwtUtils.parseToken(token);
String userId = claims.getSubject();
String tenantId = claims.get("tenantId", String.class);
// 注入上下文
UserContext.setUserId(userId);
TenantContext.setTenantId(tenantId);
} catch (ExpiredJwtException e) {
response.setStatus(401);
response.getWriter().write("{\"code\":3001,\"msg\":\"token已过期\"}");
return;
}
}
chain.doFilter(request, response);
}
// Token生成
public String generateToken(String userId, String tenantId) {
return Jwts.builder()
.setSubject(userId)
.claim("tenantId", tenantId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
}
四、 开放平台:API商业化
4.1 开放API的使用场景
┌─────────────────────────────────────────────────────────────┐
│ 外部开发者/第三方商家 │
│ ├─ ERP系统:同步订单、商品、库存 │
│ ├─ 物流服务商:获取发货信息、回传物流单号 │
│ ├─ 财务系统:拉取对账单、交易流水 │
│ └─ 第三方应用:扩展平台功能 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 开放平台网关 │
│ 认证 │ 限流 │ 计费 │ 监控 │ 日志 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 开放API服务层 │
│ 订单开放API │ 商品开放API │ 物流开放API │
└─────────────────────────────────────────────────────────────┘
4.2 AppKey认证
第三方开发者调用开放API需要持有AppKey + AppSecret:
请求签名流程:
1. 开发者申请AppKey、AppSecret
2. 请求时携带:appKey + timestamp + sign
3. sign = MD5(appKey + timestamp + 请求参数 + appSecret)
4. 服务端校验:timestamp有效期、sign是否匹配
5. 通过后按AppKey关联的权限返回数据
java
@Component
public class OpenApiAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, ...) {
String appKey = request.getHeader("X-App-Key");
String timestamp = request.getHeader("X-Timestamp");
String sign = request.getHeader("X-Sign");
// 1. 校验timestamp(防重放攻击)
long now = System.currentTimeMillis();
if (Math.abs(now - Long.parseLong(timestamp)) > 60000) {
throw new BusinessException("请求已过期");
}
// 2. 查询AppKey关联的AppSecret和权限
OpenApp app = openAppService.getByAppKey(appKey);
if (app == null || !app.isEnabled()) {
throw new BusinessException("AppKey无效或已禁用");
}
// 3. 计算签名并比对
String expectedSign = calculateSign(request, app.getAppSecret());
if (!expectedSign.equals(sign)) {
throw new BusinessException("签名错误");
}
// 4. 注入上下文
OpenApiContext.setAppKey(appKey);
OpenApiContext.setPermissions(app.getPermissions());
OpenApiContext.setTenantId(app.getTenantId());
}
}
4.3 开放API限流
第三方调用需要独立限流,防止单个应用拖垮系统:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenApiLimit {
int value() default 100; // 每分钟最大请求数
}
@Aspect
@Component
public class OpenApiLimitAspect {
@Around("@annotation(openApiLimit)")
public Object checkLimit(ProceedingJoinPoint joinPoint,
OpenApiLimit openApiLimit) throws Throwable {
String appKey = OpenApiContext.getAppKey();
String apiName = joinPoint.getSignature().getName();
String key = "openapi:limit:" + appKey + ":" + apiName;
Long count = redis.incr(key);
if (count == 1) {
redis.expire(key, 60);
}
if (count > openApiLimit.value()) {
throw new RateLimitException("API调用次数超限,请升级套餐");
}
return joinPoint.proceed();
}
}
4.4 开放API商业化定价
| 套餐 | 月调用次数 | 同时在线 | 价格 |
|---|---|---|---|
| 免费版 | 1000次 | 1个应用 | ¥0 |
| 基础版 | 1万次 | 3个应用 | ¥199/月 |
| 专业版 | 10万次 | 10个应用 | ¥999/月 |
| 企业版 | 100万次 | 不限 | ¥4999/月 |
五、 接口版本管理
API版本向后兼容是开放平台的底线能力------不能因为升级导致第三方应用挂掉。
5.1 版本策略
java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderApiV1 {
@GetMapping("/{id}")
public Result getOrder(@PathVariable Long id) {
// v1返回数据结构
OrderDTO order = orderService.getById(id);
return Result.success(order);
}
}
@RestController
@RequestMapping("/api/v2/orders")
public class OrderApiV2 {
@GetMapping("/{id}")
public Result getOrder(@PathVariable Long id) {
// v2返回扩展数据结构(新增了售后状态)
OrderDTOV2 order = orderService.getByIdV2(id);
return Result.success(order);
}
}
5.2 版本过渡策略
┌─────────────────────────────────────────────────────────────┐
│ 版本生命周期 │
│ │
│ v1.0 ──→ v1.1 ──→ v1.2 ──→ v2.0 ──→ v2.1 │
│ 发布 兼容 兼容 发布 │
│ ↓ ↓ │
│ 维护期内 开始迁移 │
│ │
│ 策略: │
│ 1. 新功能只在最新版本中提供 │
│ 2. 旧版本在最新版本发布后维护6个月 │
│ 3. 6个月后旧版本下线,提前通知开发者迁移 │
└─────────────────────────────────────────────────────────────┘
六、 API监控与SLA
| 监控维度 | 指标 | 告警阈值 |
|---|---|---|
| 请求量 | 总QPS、各接口QPS | 环比增长 > 100% |
| 响应时间 | 平均RT、TP99 | TP99 > 1000ms |
| 错误率 | HTTP 4xx/5xx占比 | 错误率 > 1% |
| 限流触发 | 限流拦截次数 | 超过100次/分钟 |
| 第三方调用 | 各AppKey调用统计 | 单应用流量异常 |
对外SLA承诺(商业化API需要明确的契约):
| SLA项 | 承诺值 |
|---|---|
| 可用性 | 99.9%(月度不可用不超过43分钟) |
| TP99响应时间 | < 500ms |
| 错误率 | < 0.5% |
| 赔偿标准 | 月度费用 × 不可用时长比例 |
七、 踩坑实录
坑1:版本升级导致第三方应用崩溃
现象:升级到v2版本后,v1版本的响应体中删除一个废弃字段,导致第三方应用解析失败。
教训:v1版本必须完全向后兼容,不能删除字段、不能修改字段类型,只能新增字段。
修复 :恢复废弃字段,标记为@Deprecated,在文档中说明将在v3版本移除。
坑2:签名算法被暴力枚举
现象:某恶意开发者用低强度签名算法的弱密钥暴力枚举AppSecret,成功伪造了一个有效签名。
修复:升级签名算法,增加时效性(请求有效期为60秒),且加入随机nonce防重放。
java
public String calculateSign(Map<String, Object> params, String appSecret) {
// 排序 + 拼接参数 + timestamp + nonce + appSecret
// 使用SHA-256代替MD5
return DigestUtils.sha256Hex(sb.toString());
}
坑3:限流误伤正常请求
现象:某商家的ERP系统定时同步订单,每分钟批量请求100次,刚好触发限流阈值(100次/分钟),导致部分请求被拒绝。
解决方案:
-
限流改为滑动窗口,避免分钟边界时的瞬时高峰
-
对批量操作接口单独设置更高的限流阈值
-
提供"获取配额"接口,批量操作前预先获取配额
java
// 滑动窗口限流(Redis + Lua)
String luaScript =
"local current = redis.call('get', KEYS[1]) or 0 " +
"if current + 1 > tonumber(ARGV[1]) then return 0 end " +
"redis.call('incr', KEYS[1]) " +
"redis.call('expire', KEYS[1], ARGV[2]) " +
"return 1";
八、 总结
| 设计维度 | 核心原则 | 实现方式 |
|---|---|---|
| 统一规范 | 所有端使用同一套API契约 | RESTful规范 + 统一响应格式 |
| 统一鉴权 | 多端共用一套认证体系 | JWT + AppKey双层认证 |
| 版本管理 | 向后兼容,平滑升级 | URL版本号 + 过渡期策略 |
| 安全防护 | 防重放、防枚举、防超限 | 签名校验 + 滑动窗口限流 |
| 商业化能力 | 面向第三方开发者提供增值API | AppKey + 套餐计费 + SLA承诺 |
| 可观测 | 全链路可追踪、可计量 | 统一requestId + 监控大盘 |
文末思考:
API是电商系统的"外交界面"。对内统一多端接入,对外提供商业化能力。好的API设计应该让调用方感觉"自然"------命名直观、错误信息清晰、文档详尽、升级平滑。
建议API设计遵循**"先有契约,后有实现"**的原则,用OpenAPI/Swagger定义好接口规范,前后端、多方开发者在同一份契约下并行开发。