做了5年后端,我整理了10个最让我怀疑人生的奇葩需求
参与奇葩需求大赏话题征文
做了5年后端开发,我见过凌晨四点的月亮,也见过比月亮还离谱的需求文档。
有时候真的怀疑产品经理是不是偷偷在测试我们的忍耐极限。今天就来盘一盘那些让我在工位上直接怀疑人生的需求,看看你能撑到第几个不掀桌子。
1. "这个接口要同时支持分页和不分页,但代码只能写一份"
产品经理的原话是:"前端有时候需要分页,有时候又想要全部数据,你能不能写一个接口,传个参数控制一下?"
我心想这不就是加个 pageSize 参数的事嘛,简单。
然后他补了一句:"但是不分页的时候,数据格式要和分页的不一样,而且要能兼容老版本。"
我当时的表情:🤯
// 于是就有了这种"优雅"的代码
@GetMapping("/api/orders")
public ResponseEntity<?> getOrders(
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer pageSize) {
if (page != null && pageSize != null) {
// 分页模式
Page<Order> orderPage = orderService.findAll(PageRequest.of(page - 1, pageSize));
return ResponseEntity.ok(Map.of(
"code", 200,
"data", Map.of(
"list", orderPage.getContent(),
"total", orderPage.getTotalElements(),
"page", page,
"pageSize", pageSize
),
"message", "success"
));
} else {
// 不分页模式 - 老版本返回格式完全不同
List<Order> allOrders = orderService.findAll();
return ResponseEntity.ok(Map.of(
"success", true,
"result", allOrders,
"msg", "查询成功"
));
}
}
后来这个接口在三个版本之后被废弃了,因为产品经理离职了,新来的产品经理说:"干嘛搞这么复杂,统一分页不就行了?"
我:😶
2. "删除功能要有,但数据不能真的删"
这是我在一家电商公司遇到的真实需求。
产品经理:"用户点删除订单,订单要从列表里消失,但后台要能查到。"
我:"那不就是软删除吗?加个 is_deleted 字段就行。"
产品经理:"对,但是用户的回收站里也要能看到,而且30天后自动清空,而且清空的时候有些订单要保留,而且保留的规则是——"
我赶紧打断:"等等,让我拿个本子记一下。"
最终的需求矩阵是:
| 订单类型 | 删除后用户可见 | 30天后 | 特殊规则 |
|---|---|---|---|
| 普通订单 | ✅ 回收站 | 永久删除 | 无 |
| 拼团订单 | ✅ 回收站 | 不删除 | 拼团失败可退款 |
| 秒杀订单 | ❌ 直接消失 | 永久删除 | 不可恢复 |
| 会员订单 | ✅ 回收站 | 永久删除 | 保留90天 |
-- 光删除逻辑就写了这么一坨
UPDATE orders
SET
is_deleted = CASE
WHEN order_type = 'flash_sale' THEN 1 -- 秒杀直接删
WHEN order_type = 'member' THEN 2 -- 会员标记待清理
ELSE 1
END,
deleted_at = CASE
WHEN order_type = 'flash_sale' THEN NOW()
WHEN order_type = 'group_buy' THEN NULL -- 拼团不设置删除时间
ELSE NOW()
END,
recycle_days = CASE
WHEN order_type = 'member' THEN 90
WHEN order_type = 'group_buy' THEN 0
ELSE 30
END
WHERE order_id = #{orderId};
后来运维告诉我,那个定时清理的 cron job 跑了半年从来没成功过,因为每次要清理的时候数据库就锁表了。
3. "这个导出功能要支持Excel,但数据量可能有100万行"
产品经理:"客户说需要导出订单数据。"
我:"好的,导出Excel,没什么问题。"
产品经理:"嗯,客户说他们有时候一年有100多万条订单,要全部导出。"
我:"……你说的是Excel对吧?Excel最大行数是1048576行。"
产品经理:"对啊,所以100万行刚好吧?"
我说不是行数的问题,而是——100万行Excel文件大概200MB,你确定客户打得开?
最后折中方案是:超过10万行走异步导出,生成多个文件并打包成zip,发邮件通知下载链接。
@Service
public class ExportService {
private static final int MAX_ROWS_PER_FILE = 100_000;
private static final int SYNC_THRESHOLD = 10_000;
public void exportOrders(ExportRequest request) {
long totalCount = orderRepository.count(request.getFilter());
if (totalCount <= SYNC_THRESHOLD) {
// 同步导出,直接返回
byte[] excel = generateExcel(request, 0, (int) totalCount);
saveAndNotify(request.getUserId(), excel);
} else {
// 异步导出,拆分成多个文件
int fileCount = (int) Math.ceil((double) totalCount / MAX_ROWS_PER_FILE);
asyncExport(request, fileCount, totalCount);
}
}
private void asyncExport(ExportRequest request, int fileCount, long totalCount) {
// 创建导出任务
ExportTask task = exportTaskRepository.save(ExportTask.builder()
.userId(request.getUserId())
.status("PROCESSING")
.totalFiles(fileCount)
.progress(0)
.build());
// 分批处理
for (int i = 0; i < fileCount; i++) {
int offset = i * MAX_ROWS_PER_FILE;
byte[] part = generateExcel(request, offset, MAX_ROWS_PER_FILE);
// 上传到OSS
String url = ossService.upload(part, "export/" + task.getId() + "/part_" + i + ".xlsx");
task.addFileUrl(url);
task.setProgress((i + 1) * 100 / fileCount);
}
// 打包成zip
byte[] zip = zipService.compress(task.getFileUrls());
String downloadUrl = ossService.upload(zip, "export/" + task.getId() + "/all.zip");
// 发邮件通知
emailService.send(request.getUserId(), "导出完成", "下载链接:" + downloadUrl);
}
}
后来客户说:"其实我们只需要最近3个月的数据,之前都是随口一说。"
4. "接口响应时间不能超过200ms,但数据来自三个微服务"
这是最经典的一个。
产品经理拿着竞品分析报告来找我:"你看,竞品的这个页面加载只要200ms,我们的为什么不行?"
我打开DevTools一看,这个页面后端调了三个微服务:用户服务、订单服务、商品服务。
"这三个服务之间需要串行调用,用户服务100ms,订单服务150ms,商品服务80ms,加起来就330ms了。"
产品经理:"那你能不能并行调用?"
我:"……可以,但这改动量不小。"
产品经理:"那不能超过200ms,你看着办。"
行,我改。
// 改之前:串行调用,330ms
public PageData getPageData(Long userId) {
UserInfo user = userService.getUser(userId); // 100ms
List<Order> orders = orderService.getOrders(userId); // 150ms
List<Product> products = productService.getHot(); // 80ms
return new PageData(user, orders, products); // 总计 330ms
}
// 改之后:并行调用 + 缓存
public PageData getPageData(Long userId) {
CompletableFuture<UserInfo> userFuture =
CompletableFuture.supplyAsync(() ->
userCacheService.getOrLoad(userId)); // 命中缓存: 5ms
CompletableFuture<List<Order>> orderFuture =
CompletableFuture.supplyAsync(() ->
orderService.getRecentOrders(userId, 10)); // 限制10条: 50ms
CompletableFuture<List<Product>> productFuture =
CompletableFuture.supplyAsync(() ->
productCacheService.getHotProducts()); // 本地缓存: 2ms
CompletableFuture.allOf(userFuture, orderFuture, productFuture).join();
// 总计约 50ms,满足要求!
return new PageData(userFuture.get(), orderFuture.get(), productFuture.get());
}
上线后产品经理很满意:"你看,我就说可以做到200ms以内吧。"
我指着监控面板上的缓存命中率曲线:"你看,这个99%的命中率,是用Redis集群和本地缓存双层扛着的,机器成本涨了30%。"
产品经理:"那没事,反正不是我付钱。"
5. "这个接口要同时支持GET和POST,而且参数名还不一样"
不要笑,这是真实发生的。
某个前端同事离职了,交接文档里写的是用GET请求调用某个接口,新来的前端同事接手后,发现代码里已经是POST了。
然后产品经理的解决方案是:"你的接口同时支持GET和POST不就行了?"
// 产品经理眼中的"简单兼容"
@RequestMapping(value = "/api/user/profile", method = {RequestMethod.GET, RequestMethod.POST})
public Result getUserProfile(
@RequestParam(required = false) String userId, // GET用
@RequestBody(required = false) UserProfileRequest request // POST用
) {
String uid;
if (request != null && request.getUserId() != null) {
uid = request.getUserId(); // POST: body里取
} else if (userId != null) {
uid = userId; // GET: query参数
} else {
throw new BadRequestException("userId is required");
}
return Result.success(userService.getProfile(uid));
}
后来有一天,前端同事在群里问:"为什么这个接口POST请求的userId有时候取不到?"
我一看日志,他把参数放在了 @RequestParam 里,但 Content-Type 是 application/json。
这口锅,后端背了。
6. "日志要打印所有请求参数,但是敏感信息不能打"
数据安全部门的要求,合情合理。但问题是——
"哪些字段是敏感信息?"
"手机号、身份证、银行卡号、密码、地址、真实姓名、邮箱……"
"那基本上所有字段都是敏感信息了?"
"也不是,你可以脱敏嘛,手机号中间四位打星号,身份证中间几位打星号……"
于是我写了一个脱敏工具类,对接了二十多个接口。
public class SensitiveDataMasker {
private static final Set<String> SENSITIVE_FIELDS = Set.of(
"phone", "mobile", "idCard", "bankCard", "password",
"realName", "email", "address", "idNumber"
);
public static String mask(String fieldName, String value) {
if (value == null || value.isEmpty()) return value;
return switch (fieldName) {
case "phone", "mobile" ->
value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
case "idCard", "idNumber" ->
value.replaceAll("(\\d{4})\\d{10}(\\d{4})", "$1**********$2");
case "bankCard" ->
value.replaceAll("(\\d{4})\\d+(\\d{4})", "$1****$2");
case "email" ->
value.replaceAll("(.)(.*)(@.*)", "$1***$3");
case "password" -> "******";
default -> "***";
};
}
}
然后有一天,测试提了个Bug:"订单详情页的用户姓名显示三个星号,用户投诉了。"
我一看,realName 字段映射错误,脱敏规则把 "张三" 变成了 "***"。
但用户说的是:"我叫张三,不是三颗星!"
7. "这个需求很简单,就是加个按钮"
所有开发者的噩梦词:"这个需求很简单"。
产品经理指着原型图:"这里加个按钮,点击后弹一个确认框,确认后调用后端接口,然后刷新页面。"
我:"嗯,听起来确实简单。"
产品经理:"对,但是——"
我就知道有"但是"。
"——但是这个按钮要在用户首次登录后7天内显示,且用户必须是VIP,且用户的订单数超过3单,且用户的账户余额大于100,且用户没有点击过'不再提醒',且这个按钮的文案要根据用户性别动态变化,女用户显示'领取专属优惠',男用户显示'查看权益',且点击后的弹窗样式要跟品牌色一致,且弹窗里要展示用户当前可用的优惠券列表,且优惠券按过期时间排序,且已经过期的要用灰色显示但不要隐藏,且——"
"停!!!"我打断了他,"这叫'加个按钮'?"
// 这就是"加个按钮"背后的逻辑
public ButtonConfig getButtonConfig(Long userId) {
User user = userService.getById(userId);
LocalDate now = LocalDate.now();
// 首次登录后7天内
if (user.getFirstLoginDate().plusDays(7).isBefore(now)) {
return ButtonConfig.hidden();
}
// 必须是VIP
if (!user.isVip()) {
return ButtonConfig.hidden();
}
// 订单数超过3单
if (orderService.countByUserId(userId) <= 3) {
return ButtonConfig.hidden();
}
// 账户余额大于100
if (user.getBalance().compareTo(new BigDecimal("100")) <= 0) {
return ButtonConfig.hidden();
}
// 没有点击过"不再提醒"
if (userPreferenceService.hasDismissed(userId, "VIP_BUTTON")) {
return ButtonConfig.hidden();
}
// 动态文案
String text = "女".equals(user.getGender())
? "领取专属优惠"
: "查看权益";
return ButtonConfig.builder()
.visible(true)
.text(text)
.coupons(couponService.getAvailableCoupons(userId))
.build();
}
这个"简单"需求,我写了300多行代码,提测了3次,因为产品经理每次测试都发现新的"但是"。
8. "数据库查询太慢了,能不能优化一下?"
DBA找我:"这个SQL跑了3秒,用户投诉了。"
SELECT * FROM orders
WHERE status IN ('PAID', 'SHIPPED', 'DELIVERED')
AND created_at BETWEEN '2025-01-01' AND '2025-12-31'
AND (user_id IN (SELECT id FROM users WHERE level >= 3)
OR total_amount > 1000)
ORDER BY created_at DESC;
我:"这个查询确实慢,主要问题是 IN 子查询和 OR 条件。"
DBA:"那能不能优化成10ms以内?"
我:"……5000万数据量的表,10ms?你认真的?"
最后我做了这些优化:
-- 1. 加索引
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
CREATE INDEX idx_orders_user_amount ON orders(user_id, total_amount);
-- 2. 用 UNION 替代 OR(MySQL 5.7 对 OR 优化不佳)
SELECT * FROM orders
WHERE status IN ('PAID', 'SHIPPED', 'DELIVERED')
AND created_at BETWEEN '2025-01-01' AND '2025-12-31'
AND user_id IN (SELECT id FROM users WHERE level >= 3)
UNION
SELECT * FROM orders
WHERE status IN ('PAID', 'SHIPPED', 'DELIVERED')
AND created_at BETWEEN '2025-01-01' AND '2025-12-31'
AND total_amount > 1000;
优化后从 3 秒降到 200ms,DBA 说:"还是不够,产品说要 10ms。"
最后方案是加 Elasticsearch,把订单数据同步到 ES 里,查询走 ES。响应时间降到了 30ms。
但引入 ES 后又带来了数据一致性问题,于是又上了 Canal + MQ 做数据同步。
本来一个"慢查询"的优化,最后变成了一个微服务架构升级项目。
9. "这个接口要给第三方用,但不想让他们看到全部数据"
这是一个开放平台的需求。
产品经理:"我们要把订单查询接口开放给合作商家。"
我:"好的,做权限控制,商家只能查自己的订单。"
产品经理:"对,但是每个商家能看到的字段还不一样。A商家能看到价格,B商家不能看价格但能看到用户手机号,C商家不能看价格也不能看手机号但能看到订单备注……"
我:"……一共有多少商家?"
产品经理:"目前20个,但以后会更多。"
我硬着头皮设计了一个字段级别的权限控制:
@Service
public class FieldPermissionService {
// 商家-字段权限映射
private static final Map<String, Set<String>> MERCHANT_FIELD_PERMISSIONS = Map.of(
"MERCHANT_A", Set.of("orderId", "productName", "price", "status", "createdAt"),
"MERCHANT_B", Set.of("orderId", "productName", "userPhone", "status", "createdAt"),
"MERCHANT_C", Set.of("orderId", "productName", "remark", "status", "createdAt"),
"MERCHANT_D", Set.of("orderId", "productName", "status", "createdAt")
// ... 20个商家
);
public Map<String, Object> filterFields(String merchantId, Order order) {
Set<String> allowedFields = MERCHANT_FIELD_PERMISSIONS
.getOrDefault(merchantId, Set.of("orderId", "productName", "status"));
Map<String, Object> original = objectMapper.convertValue(order, Map.class);
Map<String, Object> filtered = new HashMap<>();
for (String field : allowedFields) {
if (original.containsKey(field)) {
filtered.put(field, original.get(field));
}
}
return filtered;
}
}
后来产品经理说:"这个字段权限改成了配置化,商家可以在后台自己勾选哪些字段可见。"
我默默地把硬编码的 Map 改成了查数据库,然后又加了一个审批流程:商家选字段 → 平台审核 → 生效。
原本一个简单的接口开放,变成了一个完整的权限管理系统。
10. "把这个系统从MySQL迁移到MongoDB,但不要停服"
最后一个暴击。
CTO在技术分享会上听说了MongoDB,觉得我们电商系统的订单数据很适合用文档数据库存储。
CTO:"订单数据本身就是嵌套结构,用MongoDB比MySQL更合适。"
我:"确实,但迁移成本很高,而且——"
CTO:"这个季度完成,不能停服。"
我:"……"
两个月,我做了双写方案:
┌─────────┐ ┌──────────┐ ┌─────────┐
│ App │────▶│ Router │────▶│ MySQL │ (主)
└─────────┘ └──────────┘ └─────────┘
│
│ 异步双写
▼
┌──────────┐
│ MongoDB │ (影子库)
└──────────┘
@Transactional
public Order createOrder(OrderRequest request) {
// 1. 写 MySQL(主库)
Order order = orderMapper.insert(request);
// 2. 异步写 MongoDB
mongoTemplate.insertAsync(order);
// 3. 记录双写日志
syncLogService.record(order.getId(), "MYSQL", "SUCCESS");
syncLogService.record(order.getId(), "MONGO", "PENDING");
return order;
}
// 定时对账任务
@Scheduled(cron = "0 0/30 * * * ?")
public void reconciliation() {
List<Long> diffIds = syncLogService.findDiff();
for (Long id : diffIds) {
Order mysqlOrder = orderMapper.findById(id);
mongoTemplate.save(mysqlOrder); // 补偿写入
syncLogService.markSynced(id);
}
}
上线后一切正常,直到有一天,一个读接口从MongoDB切换过去后,响应时间从不稳定到超时。
排查发现:MongoDB的某个分片挂了,但因为双写方案没有做 MongoDB 的故障转移,导致部分请求超时。
最后紧急切回了 MySQL,CTO 说:"那还是先用 MySQL 吧,MongoDB 的事以后再说。"
我:😐
写在最后
做了这么多年后端,我发现一个规律:越简单的需求描述,背后越复杂。
"加个按钮" = 权限控制 + 动态文案 + 生命周期管理 + 用户偏好 + AB测试
"优化一下" = 索引优化 + 缓存设计 + 数据同步 + 架构升级
"兼容一下" = 接口版本管理 + 数据格式转换 + 回归测试
但吐槽归吐槽,这些"奇葩需求"某种程度上也逼着我们成长。没有那个"简单"的导出需求,我可能永远不会去学异步任务和消息队列。没有那个"200ms"的要求,我也不会去深入研究缓存策略。
当然,如果产品经理能先把需求想清楚再提,那就更好了。 🫠
你遇到过哪些让你怀疑人生的奇葩需求?评论区聊聊,看看谁的经历更离谱。
🏷️ 本文参与奇葩需求大赏话题征文