长耗时接口异步改造总结

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

📋 目录


🎬 背景问题

问题场景 😱

长耗时接口(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! 🎊

相关推荐
Elieal2 小时前
Git 面试题全面汇总
git
风华同学2 小时前
【系统移植篇】系统烧写
java·开发语言·前端
by__csdn2 小时前
JavaScript性能优化实战:异步与延迟加载全方位攻略
开发语言·前端·javascript·vue.js·react.js·typescript·ecmascript
rchmin2 小时前
Git撤销命令revert与reset区别
git
牛三金2 小时前
魔改-隐语PSI通信,支持外部通信自定义
服务器·前端·算法
杨超越luckly2 小时前
HTML应用指南:利用GET请求获取全国瑞思教育门店位置信息
前端·python·arcgis·html·门店数据
尘缘浮梦2 小时前
chrome英文翻译插件
前端·chrome
HIT_Weston2 小时前
58、【Ubuntu】【Gitlab】拉出内网 Web 服务:Gitlab 配置审视(二)
前端·ubuntu·gitlab
diudiu96282 小时前
Logback使用指南
java·开发语言·spring boot·后端·spring·logback