基于研究即代码理念的协同研究项目实践指南
2026/5/11 21:25:27
一次从同步到异步的华丽转身 ✨
长耗时接口(30-60秒)遇到网关超时(30秒)的问题:
异步处理 + Redis缓存= 完美解决!
// constant/redis.jsOP_TASK_AI:'task_ai'// 用于存储异步任务状态// router/xxx.jsrouter.get('/api/task/result_by_key',controller.task.getTaskResult,)| 接口 | 用途 | 参数 | 返回 |
|---|---|---|---|
create_task | 🚀创建任务 | input,type,related_id | 处理结果 or{status: 'processing'} |
get_task_result | 🔍查询结果 | taskKey | Redis中的完整数据 ornull |
关键代码:
// 创建任务接口(主动)asynccreateTask(ctx){const{input,type,related_id}=ctx.queryconstresult=awaitthis.ctx.service.task.createTask(input,type,related_id)// ...}// 查询结果接口(被动)asyncgetTaskResult(ctx){const{taskKey}=ctx.queryconstresult=awaitthis.ctx.service.task.getTaskResult(taskKey)// ...}createTask- 创建异步任务
asynccreateTask(input,type,related_id){// 1. 检查Redis是否有结果// 2. 如果有 → 直接返回// 3. 如果没有 → 创建任务,异步处理// 4. 返回 { status: 'processing' }}processTaskAsync- 异步处理任务
asyncprocessTaskAsync(input,type,redisKey,related_id){// 1. 调用长耗时接口// 2. 处理成功 → 更新Redis状态为 'completed'// 3. 发送通知(包含跳转链接)}getTaskResult- 查询任务结果
asyncgetTaskResult(taskKey){// 直接从Redis查询,不创建任务constredisKey=`${REDIS_KEYS.OP_TASK_AI}:${taskKey}`constredisValue=awaitthis.ctx.getRedisValue(redisKey)returnredisValue?JSON.parse(redisValue):null}Redis数据结构:
// 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}// service/xxx/index.tsasyncfunctiongetTaskResult(params:{taskKey:string}){const{data}=awaithttpGet('/api/task/result_by_key',params)returndata}// 解析URL参数const{relatedId}=querystring.parse(window.location.search)// 通过URL打开详情constopenDetailByUrl=async()=>{if(relatedId){constdetail=data.list.find((item)=>`${item.id}`===relatedId)if(detail){dispatchUpdate(detail)}}}// 监听数据加载完成,自动打开useEffect(()=>{if(data.list.length>0){openDetailByUrl()}},[relatedId,data])关闭时清除URL参数:
consthandleClose=()=>{dispatchClose()onReset()// 重置搜索参数,URL会自动更新}// 解析URL参数const{taskKey}=querystring.parse(window.location.search)as{taskKey:string}// 查询Redis并显示结果constloadTaskResult=async()=>{if(taskKey){consttaskResult=awaitgetTaskResult({taskKey})if(taskResult){const{status,result,type}=taskResult// 设置类型if(type){setTaskType(type)}// 根据状态显示结果if(status==='completed'&&result){setShowResult(true)setResultValue(result)}elseif(status==='processing'){message.info('处理中,请稍候...')}elseif(status==='failed'){message.warning('处理失败,请重试')}}}}// 页面打开时自动查询useEffect(()=>{if(taskKey&&visible){loadTaskResult()}},[taskKey,visible])重要:Form和State的区别
// ❌ 错误:组件不在Form.Item中,不能用form管理form.setFieldsValue({field:value})// 没用!// ✅ 正确:直接用state管理const[field,setField]=useState<string>()<Select value={field}// 绑定stateonChange={setField}// 更新state>规则:${REDIS_KEYS.OP_TASK_AI}:${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任务结果前端解析:
importquerystringfrom'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参数:
// 方法1:重置defaultParams(推荐)onReset()// 会设置所有参数为默认值,空值会被过滤// 方法2:手动清除特定参数constclearUrlExtraParams=()=>{constcurrentParams=querystring.parse(location.search)const{relatedId,taskKey,...restParams}=currentParams// 只保留 restParams}问题:
// Select不在Form.Item中<Select onChange={onVendorTypeChange}>{/* ... */}</Select>// 但是代码中用了form.setFieldsValueform.setFieldsValue({vendorType:value})// ❌ 没用!原因:Form只能管理Form.Item中的字段
解决:
// ✅ 直接用state管理const[vendorType,setVendorType]=useState<string>()constonVendorTypeChange=(value:string)=>{setVendorType(value)// 只更新state}<Select value={vendorType}onChange={onVendorTypeChange}>问题:
relatedId,taskKey)useSyncParamshook 会重写URL,只保留defaultParams中的参数原因:
// useSyncParams 的 onParamSync 会重写URLconstonParamSync=()=>{history.push(`${pathname}?${querystring.stringify(params,{sort:false})}`)// params 只包含 defaultParams 中的字段}解决:
// 方案1:在defaultParams中添加这些字段(推荐)constdefaultParams={// ...其他字段taskKey:'',relatedId:'',}// 方案2:修改useSyncParams hook,保留额外参数// (但可能影响其他页面,需谨慎)问题:前端用window.location.host,后端用什么?
答案:
// 方式1:配置文件(当前使用)constbaseUrl=this.app.config.baseUrl||''// 方式2:从请求获取(更灵活)constbaseUrl=`${this.ctx.request.protocol}://${this.ctx.request.host}`// 或constbaseUrl=this.ctx.request.origin问题:异步函数中this可能为undefined
解决:
// ✅ 项目风格:直接调用,不捕获thisthis.processRecognitionTask(imageUrl,vendorType,redisKey,maintain_id)// 如果出错,会在processRecognitionTask内部用this.ctx.throwValidateWarn处理Redis Key设计要简单
taskKey),不要组合多个参数异步处理要"fire and forget"
两个接口职责要清晰
create_task:创建任务(主动,会创建新任务)get_task_result:查询结果(被动,只查询不创建)Redis数据结构要完整
URL参数处理要统一
querystring.parse(window.location.search)string | string[] | null)Form和State要分清
useSyncParams要小心
页面打开时机要准确
代码清理很重要
异步改造的核心
Redis的妙用
URL参数传递
代码风格一致性
✅适合:
❌不适合:
Happy Coding! 🎊