长耗时接口异步改造总结

一次从同步到异步的华丽转身 ✨

📋 目录


🎬 背景问题

问题场景 😱

长耗时接口(30-60秒)遇到网关超时(30秒)的问题:

  • 接口调用需要 30-60秒 才能返回
  • 网关设置了 30秒超时重试
  • 结果:接口超时 → 网关重试 → 重复调用 → 用户体验差 😭

解决方案 💡

异步处理 + Redis缓存 = 完美解决!

  • 用户发起请求 → 立即返回"处理中" → 后台异步处理
  • 处理完成后 → Redis存储结果 → 发送通知
  • 用户点击通知链接 → 自动打开页面 → 显示处理结果

🎯 核心改动

后端改动

1️⃣ 新增 Redis Key
javascript 复制代码
// constant/redis.js
OP_TASK_AI: 'task_ai'  // 用于存储异步任务状态
2️⃣ 新增查询路由
javascript 复制代码
// router/xxx.js
router.get(
  '/api/task/result_by_key',
  controller.task.getTaskResult,
)
3️⃣ Controller层:两个接口的职责分离
接口 用途 参数 返回
create_task 🚀 创建任务 input, type, related_id 处理结果 or {status: 'processing'}
get_task_result 🔍 查询结果 taskKey Redis中的完整数据 or null

关键代码

javascript 复制代码
// 创建任务接口(主动)
async createTask(ctx) {
  const { input, type, related_id } = ctx.query
  const result = await this.ctx.service.task.createTask(
    input, type, related_id
  )
  // ...
}

// 查询结果接口(被动)
async getTaskResult(ctx) {
  const { taskKey } = ctx.query
  const result = await this.ctx.service.task.getTaskResult(taskKey)
  // ...
}
4️⃣ Service层:核心方法

createTask - 创建异步任务

javascript 复制代码
async createTask(input, type, related_id) {
  // 1. 检查Redis是否有结果
  // 2. 如果有 → 直接返回
  // 3. 如果没有 → 创建任务,异步处理
  // 4. 返回 { status: 'processing' }
}

processTaskAsync - 异步处理任务

javascript 复制代码
async processTaskAsync(input, type, redisKey, related_id) {
  // 1. 调用长耗时接口
  // 2. 处理成功 → 更新Redis状态为 'completed'
  // 3. 发送通知(包含跳转链接)
}

getTaskResult - 查询任务结果

javascript 复制代码
async getTaskResult(taskKey) {
  // 直接从Redis查询,不创建任务
  const redisKey = `${REDIS_KEYS.OP_TASK_AI}:${taskKey}`
  const redisValue = await this.ctx.getRedisValue(redisKey)
  return redisValue ? JSON.parse(redisValue) : null
}

Redis数据结构

javascript 复制代码
// processing 状态
{
  status: 'processing',
  type: 'task_type',
  input: 'input_data',
  related_id: 123,
  createdAt: 1234567890
}

// completed 状态
{
  status: 'completed',
  result: '处理结果',
  type: 'task_type',
  input: 'input_data',
  related_id: 123,
  completedAt: 1234567890
}

前端改动

1️⃣ Service层:新增查询接口
typescript 复制代码
// service/xxx/index.ts
async function getTaskResult(params: { taskKey: string }) {
  const { data } = await httpGet('/api/task/result_by_key', params)
  return data
}
2️⃣ 列表页:URL参数处理
typescript 复制代码
// 解析URL参数
const { relatedId } = querystring.parse(window.location.search)

// 通过URL打开详情
const openDetailByUrl = async () => {
  if (relatedId) {
    const detail = data.list.find((item) => `${item.id}` === relatedId)
    if (detail) {
      dispatchUpdate(detail)
    }
  }
}

// 监听数据加载完成,自动打开
useEffect(() => {
  if (data.list.length > 0) {
    openDetailByUrl()
  }
}, [relatedId, data])

关闭时清除URL参数

typescript 复制代码
const handleClose = () => {
  dispatchClose()
  onReset()  // 重置搜索参数,URL会自动更新
}
3️⃣ 详情页:从URL查询结果
typescript 复制代码
// 解析URL参数
const { taskKey } = querystring.parse(window.location.search) as { taskKey: string }

// 查询Redis并显示结果
const loadTaskResult = async () => {
  if (taskKey) {
    const taskResult = await getTaskResult({ taskKey })
    if (taskResult) {
      const { status, result, type } = taskResult
      
      // 设置类型
      if (type) {
        setTaskType(type)
      }
      
      // 根据状态显示结果
      if (status === 'completed' && result) {
        setShowResult(true)
        setResultValue(result)
      } else if (status === 'processing') {
        message.info('处理中,请稍候...')
      } else if (status === 'failed') {
        message.warning('处理失败,请重试')
      }
    }
  }
}

// 页面打开时自动查询
useEffect(() => {
  if (taskKey && visible) {
    loadTaskResult()
  }
}, [taskKey, visible])

重要:Form和State的区别

typescript 复制代码
// ❌ 错误:组件不在Form.Item中,不能用form管理
form.setFieldsValue({ field: value })  // 没用!

// ✅ 正确:直接用state管理
const [field, setField] = useState<string>()

<Select
  value={field}  // 绑定state
  onChange={setField}  // 更新state
>

🛠️ 技术实现

Redis Key设计

规则${REDIS_KEYS.OP_TASK_AI}:${taskKey}

为什么只用单个唯一标识?

  • ✅ 简单:不需要组合多个参数
  • ✅ 唯一:taskKey本身就是唯一的
  • ✅ 易查:前端只需要提取唯一标识

示例

复制代码
input: https://example.com/file/1765423642667_169.jpeg
taskKey: 1765423642667_169.jpeg
redisKey: task_ai:1765423642667_169.jpeg

异步处理流程

复制代码
用户点击识别
    ↓
检查Redis是否有结果
    ↓
有结果 → 直接返回
    ↓
无结果 → 设置Redis状态为 'processing'
    ↓
异步调用 processRecognitionTask
    ↓
调用AI识别接口(30-60秒)
    ↓
识别成功 → 更新Redis状态为 'completed'
    ↓
发送通知(包含跳转链接)

通知链接格式

复制代码
https://example.com/page/detail?relatedId=${related_id}&taskKey=${taskKey}

为什么这样设计?

  • relatedId:用于获取关联数据详情,打开页面
  • taskKey:用于查询Redis任务结果

URL参数处理

前端解析

typescript 复制代码
import querystring from 'query-string'

// 解析URL参数
const { relatedId, taskKey } = querystring.parse(window.location.search)

// ⚠️ 注意:querystring.parse 返回的类型可能是 string | string[] | null
// 需要类型断言或类型守卫
const { taskKey } = querystring.parse(window.location.search) as { taskKey: string }

清除URL参数

typescript 复制代码
// 方法1:重置defaultParams(推荐)
onReset()  // 会设置所有参数为默认值,空值会被过滤

// 方法2:手动清除特定参数
const clearUrlExtraParams = () => {
  const currentParams = querystring.parse(location.search)
  const { relatedId, taskKey, ...restParams } = currentParams
  // 只保留 restParams
}

🐛 踩坑记录

坑1:Select组件不在Form.Item中

问题

typescript 复制代码
// Select不在Form.Item中
<Select onChange={onVendorTypeChange}>
  {/* ... */}
</Select>

// 但是代码中用了form.setFieldsValue
form.setFieldsValue({ vendorType: value })  // ❌ 没用!

原因:Form只能管理Form.Item中的字段

解决

typescript 复制代码
// ✅ 直接用state管理
const [vendorType, setVendorType] = useState<string>()
const onVendorTypeChange = (value: string) => {
  setVendorType(value)  // 只更新state
}

<Select value={vendorType} onChange={onVendorTypeChange}>

坑2:URL参数被useSyncParams重置

问题

  • URL中有额外参数(如 relatedId, taskKey
  • 但是 useSyncParams hook 会重写URL,只保留 defaultParams 中的参数
  • 结果:额外参数被清除了 😱

原因

typescript 复制代码
// useSyncParams 的 onParamSync 会重写URL
const onParamSync = () => {
  history.push(`${pathname}?${querystring.stringify(params, { sort: false })}`)
  // params 只包含 defaultParams 中的字段
}

解决

typescript 复制代码
// 方案1:在defaultParams中添加这些字段(推荐)
const defaultParams = {
  // ...其他字段
  taskKey: '',
  relatedId: '',
}

// 方案2:修改useSyncParams hook,保留额外参数
// (但可能影响其他页面,需谨慎)

坑3:后端获取域名的方式

问题 :前端用 window.location.host,后端用什么?

答案

javascript 复制代码
// 方式1:配置文件(当前使用)
const baseUrl = this.app.config.baseUrl || ''

// 方式2:从请求获取(更灵活)
const baseUrl = `${this.ctx.request.protocol}://${this.ctx.request.host}`
// 或
const baseUrl = this.ctx.request.origin

坑4:异步处理中的this上下文

问题 :异步函数中 this 可能为 undefined

解决

javascript 复制代码
// ✅ 项目风格:直接调用,不捕获this
this.processRecognitionTask(imageUrl, vendorType, redisKey, maintain_id)
// 如果出错,会在processRecognitionTask内部用this.ctx.throwValidateWarn处理

💡 经验总结

后端经验

  1. Redis Key设计要简单

    • ✅ 只用单个唯一标识(如 taskKey),不要组合多个参数
    • ✅ 便于前端查询,不需要复杂的key生成逻辑
    • ✅ 从输入中提取唯一标识(如文件名、ID等)
  2. 异步处理要"fire and forget"

    • ✅ 不等待异步任务完成
    • ✅ 错误处理在异步函数内部完成
    • ✅ 符合项目现有代码风格
  3. 两个接口职责要清晰

    • create_task:创建任务(主动,会创建新任务)
    • get_task_result:查询结果(被动,只查询不创建)
  4. Redis数据结构要完整

    • 存储任务相关的完整信息(type、input、related_id等)
    • 便于前端直接使用,不需要额外查询
    • 包含状态、结果、时间戳等元数据

前端经验

  1. URL参数处理要统一

    • ✅ 使用 querystring.parse(window.location.search)
    • ✅ 注意类型处理(可能是 string | string[] | null
    • ✅ 使用类型断言或类型守卫
  2. Form和State要分清

    • ✅ Form只能管理Form.Item中的字段
    • ✅ 不在Form.Item中的组件用state管理
    • ✅ 不要混用,避免无效操作
  3. useSyncParams要小心

    • ✅ 会重写URL,只保留defaultParams中的参数
    • ✅ 额外参数需要在defaultParams中声明(即使为空)
    • ✅ 避免URL参数被意外清除
  4. 页面打开时机要准确

    • ✅ 等数据加载完成后再打开
    • ✅ 通过useEffect监听数据变化
    • ✅ 避免在数据未加载时操作
  5. 代码清理很重要

    • ✅ 删除无意义的form操作
    • ✅ 删除console.log和调试代码
    • ✅ 保持代码整洁和可维护

通用经验

  1. 异步改造的核心

    • 立即返回 → 后台处理 → 结果存储 → 通知用户
  2. Redis的妙用

    • 临时存储任务状态
    • 跨请求共享数据
    • 设置TTL自动清理
  3. URL参数传递

    • 简单直接,便于分享
    • 刷新页面不丢失状态
    • 但要处理好清除逻辑
  4. 代码风格一致性

    • 参考项目现有代码
    • 保持风格统一
    • 便于团队协作

🎉 最终效果

用户体验提升

  1. 发起请求 → 立即返回"处理中",可以关闭页面
  2. 处理完成 → 收到通知,点击链接
  3. 自动打开 → 页面自动打开,处理结果自动填充
  4. 完美!

技术指标

  • ✅ 接口响应时间:从30-60秒 → <1秒
  • ✅ 用户体验:从等待 → 异步处理
  • ✅ 代码质量:逻辑清晰,职责分明
  • ✅ 可维护性:,符合项目风格

🚀 适用场景

什么时候用这个模式?

适合

  • 长耗时接口(>10秒)
  • 网关有超时限制
  • 用户不需要立即看到结果
  • 可以通过通知告知用户

不适合

  • 短耗时接口(<5秒)
  • 需要实时反馈
  • 用户必须等待结果

📝 核心要点总结

  1. 两个接口分离:创建任务 vs 查询结果
  2. Redis存储状态:processing → completed/failed
  3. 异步处理:fire and forget模式
  4. URL参数传递:通过链接跳转并自动加载结果
  5. 前端状态管理:Form vs State要分清

Happy Coding! 🎊

相关推荐
恋猫de小郭8 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
牛奔9 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌14 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅14 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606115 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX15 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了15 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅15 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅16 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法16 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate