ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

金蝶云星空采购入库重复了怎么搞:接口重复推单的原因、排查和防重做法

金蝶云星空采购入库重复了怎么搞:接口重复推单的原因、排查和防重做法 目录一、先说结论二、重复单是怎么来的2.1 超时重试最常见的坑2.2 游标没有推进整批重拉重推2.3 人工重推2.4 单号由金蝶自动生成三、先确认到底是不是重复了四、防重三道锁怎么写4.1 第一道锁本地唯一索引4.2 第二道锁推之前先问金蝶一次4.3 第三道锁报文里带上本系统单据 ID五、错误分类duplicate 这类错误绝对不能重试六、已经重复了怎么清理七、小结一、先说结论接口推出来的采购入库单重复了九成不是金蝶的问题是推单程序没做幂等。「幂等」这个词听着玄说人话就是同一张单不管推一次、三次还是十次金蝶那边只能有一张。想做到这一点至少要三道锁缺一道都会漏锁在哪挡什么第一道程序本地数据库加唯一索引同一张单第二次入队直接被挡回第二道推之前先去金蝶查一次单号上次超时、其实已经推成功的第三道报文里带上本系统单据 ID超时重试时能认出「这个单我刚才发过」只做第一道超时重试照样会重复只做第二道人工重推照样会重复。下面逐条讲。二、重复单是怎么来的2.1 超时重试最常见的坑一次推单其实是「发请求 → 金蝶处理 → 返回结果」三步。问题出在中间你的程序 金蝶 │── 保存请求 ──────────────►│ │ │ 单据保存成功单号 CGRK00311 │ ✗ 网络超时/连接断 │ │◄───────────────────────────│ 响应没回到你这边 │ │ 程序认为「失败」按重试策略再发一次 │── 保存请求 ──────────────►│ │ │ 又保存一遍 → 两张 CGRK00311 / 00312你这边看到的是失败金蝶那边其实成功了。这就是为什么「按错误信息判断要不要重试」这件事必须做对见第五节。这不是猜测。金蝶的会话失效时返回的原话是会话信息已丢失请重新登录2026-09-28 实测用一个过期的会话去调ExecuteBillQuery。HTTP 状态码仍然是200错误信息包在返回体里。连「没登录」这种明确错误都返回 200那么**「请求发出去了但没收到响应」的情况程序根本无法从 HTTP 层看出来**——只能靠幂等兜。2.2 游标没有推进整批重拉重推拉单这头如果用的是「上次拉到哪个时间点」的游标一旦中途出错有两种写法✅ 一整轮全部成功之后才写回游标 → 出错重来会重复拉到单据但靠幂等挡住❌ 边拉边写游标 → 中间挂了这批单可能永远漏掉比重复更麻烦重复可以挡漏单挡不住所以宁可重复拉。前提是后面有幂等。2.3 人工重推失败列表里那张单其实上次已经成功了只是状态没更新成功。有人看到「失败」就又点了一次重推。这种情况第一道锁拦不住记录状态是failed不是done只能靠第二道锁推之前先去金蝶问一句。2.4 单号由金蝶自动生成如果对接时让金蝶自己编号用金蝶的单据编码规则程序手里就只有自己的单号没有金蝶单号。这种设计下不能靠「单号相同」判断重复因为两次推生成的号本来就不一样只能靠第三道锁报文里带上本系统的单据 ID写在金蝶单据的自定义字段或者备注里重试前先按这个 ID 反查对接时最好明确一件事单据编号到底谁生成。用程序方的单号推给金蝶金蝶的「单据编号」字段允许手工指定幂等会好做得多。三、先确认到底是不是重复了排查第一步是查不是猜。金蝶的采购入库单是STK_InStock用查询接口直接列出来。以下字段是我在金蝶官方公开体验环境实测过的账套「金蝶蓝海实业集团」用户 demo2026-09-25只调查询接口没有写入任何数据FormIdSTK_InStock✅单号形如CGRK00310体验环境里共 284 张、433 行明细表头字段FBillNo、FDate、FSupplierId.FName供应商、FPurchaseOrgId、FStockOrgId、FBillTypeIDRKD01_SYS是标准采购入库、FDocumentStatus明细实体是FInStockEntry写成FEntity会报元数据中标识为FEntity_FEntryID的字段不存在明细字段FMaterialId、FMustQty应收、FRealQty实收、FTaxPrice含税单价、FPrice不含税、FStockId、FAllAmount、FAmount税率用FEntryTaxRate。写成FTaxRate也不报错但FTaxRate不是明细税率实测 CGRK00310FEntryTaxRate13FTaxRate0按单号查只查表头不查明细{data:{FormId:STK_InStock,FieldKeys:FBillNo,FDate,FSupplierId.FName,FDocumentStatus,FilterString:FBillNoCGRK00310,OrderString:FBillNo,Limit:50}}按「同一天 同一供应商」查重复的单一般会挨在一起{data:{FormId:STK_InStock,FieldKeys:FBillNo,FDate,FSupplierId.FName,FAllAmount,FilterString:FDate2026-09-01 and FSupplierId.FName某某供应商,OrderString:FSupplierId.FName,FDate,Limit:200}}查明细时有个坑FieldKeys里一加明细字段一张单就变成多行而Limit是按行算的。在销售出库单上实测过一张有 4 条明细的单Limit:12时只返回了 3 条单据被从中间切断了。查单据列表时不要带明细字段要明细就单独用 View 查那一张。另外报错时 HTTP 状态码也是 200判断成败不能看状态码要看返回体里的ResponseStatus.IsSuccess// 看返回体不看 HTTP 状态码functionreadResult(body){constres(body(body.Result||body.result))||body;conststatusres(res.ResponseStatus||res.responseStatus);if(!status)returnnull;constokstatus.IsSuccesstrue;consterrsstatus.Errors||[];constmessages(Array.isArray(errs)?errs:[errs]).map(e(typeofestring?e:ee.Message)).filter(Boolean);return{ok,message:messages.join()||(ok?:对方返回失败但没给错误信息)};}四、防重三道锁怎么写下面这段代码来自我自己写的一个对接程序聚水潭 → 金蝶出库单那条线已经跑起来了采购入库用的是同一套机制。4.1 第一道锁本地唯一索引一张业务单据在一个账套里只允许有一条记录。用数据库的唯一索引来保证不要靠代码里的 if 判断——并发的时候 if 会失效。CREATETABLEpush_records(idINTEGERPRIMARYKEYAUTOINCREMENT,conn_idTEXTNOTNULL,-- 哪个账套doc_typeTEXTNOTNULL,-- salesOutbound / purchaseIn …doc_noTEXTNOTNULL,-- 本系统单号doc_idTEXT,-- 本系统单据 ID推给 ERP 做幂等回写idem_keyTEXTNOTNULL,stateTEXTNOTNULL,-- pending / done / skipped / failedremote_noTEXT,-- ERP 里的单号remote_idTEXT,-- ERP 里的内码noteTEXT,created_atINTEGERNOTNULL,updated_atINTEGERNOTNULL);-- 幂等的第一道锁CREATEUNIQUEINDEXidx_rec_docONpush_records(conn_id,doc_type,doc_no);幂等键用「账套 单据类型 单号」算不要带单据内容// 幂等键账套 单据类型 单号。// 不带内容哈希——单据改了内容还是同一张单改了也不该在 ERP 里变成两张。functionidemKey(connId,docType,docNo){returncrypto.createHash(sha256).update([connId,docType,docNo].join(\0)).digest(hex).slice(0,32);}想推一张单时先来登记返回created: true才允许入队claim(connId,docType,docNo,docId){constexistgetByDoc.get(connId,docType,docNo);if(exist)return{created:false,record:exist};// 已经有记录按 state 决定跳过还是重试insert.run(connId,docType,docNo,docId||null,idemKey(connId,docType,docNo),now,now);return{created:true,record:getByDoc.get(connId,docType,docNo)};}4.2 第二道锁推之前先问金蝶一次这道锁专门挡 2.1 那个超时场景——上一次其实推成功了本地状态却是失败。// 幂等第二道锁推之前先问一次金蝶「这张单号在不在」。asyncfunctionfindExisting({conn,api,doc,docTypepurchaseIn}){constmconn.mapping[docType];constbillNoField(m.lookupm.lookup.billNoField)||FBillNo;constidField(m.lookupm.lookup.idField)||FID;constnoString(doc.no).replace(//g,);// 单引号转义constrowsawaitapi.query({formId:m.formId,// STK_InStockfieldKeys:${idField},${billNoField},// FID,FBillNofilterString:${billNoField} ${no},limit:1,});if(!rows||!rows.length)returnnull;constrowrows[0];return{remoteId:String(Array.isArray(row)?row[0]:row[idField]||),remoteNo:String(Array.isArray(row)?row[1]:row[billNoField]||doc.no),};}查到就跳过不要报错除非配置里明确要求报错constexistawaitfindExisting({conn,api,doc,docType});if(exist){if(push.onDuplicatefail){thrownewApiError(duplicate,金蝶里已经有单号${doc.no}内码${exist.remoteId});}return{state:skipped,...exist,note:金蝶里已存在同号单据跳过幂等命中};}这个查询用FID,FBillNo就够了字段越少越快。4.3 第三道锁报文里带上本系统单据 ID前两道锁都在程序这一侧。第三道锁是在金蝶那一侧留个记号好处是人工在金蝶里就能看出来这张单是从哪来的对账、追溯都靠它。做法在映射文件里把这个字段写成常量取值值就是本系统的单据 ID。{purchaseIn:{formId:STK_InStock,header:{FBillNo:{from:no},FDate:{from:date},FSupplierId:{from:supplier_name,wrap:FNumber},FNote:{const:JST-PI-,concat:id}}}}FNote是备注字段写进单据 ID 之后金蝶里一眼能看出来源排查重复时直接按备注反查。⚠️ 用哪个字段存这个 ID要在客户的真实账套上确认很多客户会自己占用备注字段。有的客户有没被占用的自定义字段那更好。五、错误分类duplicate 这类错误绝对不能重试「要不要重试」判断错后果很直接该重试的不重试会丢单不该重试的一直重试就是拿错误报文刷屏还可能刷出一堆重复单。按错误性质分类比按错误码分类实用——金蝶的报错文案不统一没有稳定的错误码可用// 可重试的constRETRYABLEnewSet([network,timeout,locked,serverError]);constPATTERNS[{kind:auth,re:/(用户名或密码|密码错误|登录失败|未登录|会话已过期|session|licen[cs]e|许可)/i},{kind:locked,re:/(被锁定|正在被其他|占用|互斥|并发|deadlock|timeout expired)/i},{kind:duplicate,re:/(单据编号.*(重复|已存在)|已存在相同|duplicate)/i},// ← 不重试{kind:validation,re:/(不存在|必录|必填|校验|不合法|超出|禁用|未审核|反审核|不允许|字段)/i},];类别例子重试怎么办network连不上、ECONNREFUSED✅ 重试退避重试timeout超时、AbortError✅ 重试靠幂等兜底locked账套被锁、并发冲突✅ 重试等一会儿再来serverError对方 5xx✅ 重试退避重试auth密码错、会话失效❌停下等人改配置validation客户不存在、字段不合法❌停下等人处理单据duplicate单号重复 / 已存在❌按「跳过」处理不能当成失败重试unknown认不出来的❌停下先看一眼别闷头刷另外超时这一类要单独对待重试前必须先做第二道锁的查询因为超时的时候对方很可能已经成功了。六、已经重复了怎么清理第一步不是删是停。先把推单任务停掉否则你一边删、它一边又推回来。第二步在金蝶里处理重复的那张。思路是反审核 → 删除保留最早那张、删掉后面重复的。⚠️ 说明白这两步我没有在真实账套上操作过——我用的体验环境是只读的不能改数据所以这里只给思路具体按钮和权限以你们财务和金蝶服务商为准。删除单据通常还要求该单据未被下游引用比如还没生成凭证、没被其他单下推否则删不掉要先删下游有对应的删除权限账套可能还锁着期间删除动作要留痕谁删的、什么时候删的最好在群里说一声别让财务以为数据飞了第三步不要把重复的单据直接从数据库删掉。金蝶的单据在数据库里是表头 明细 关联关系好几张表直接DELETE会留下脏数据后面报表和结账都可能出错。要走金蝶的接口或者界面删。第四步把本地幂等记录的状态改回去。程序里要留一个口子人工确认金蝶里确实没有这张单之后允许把它重新推一次。// 人工处理完想重来一次时用把记录退回 pendingreopen(connId,docType,docNo){setState.run(pending,null,null,人工重置允许重新推送,Date.now(),connId,docType,docNo);}这个口子要加权限控制别让谁都能点。它是整个防重体系里唯一的「后门」。七、小结重复单的根因是幂等没做全不是金蝶的事三道锁缺一不可本地唯一索引、推前查一次、报文带本系统单据 ID超时是最危险的场景程序看到失败金蝶其实成功了判断成败看返回体不看 HTTP 状态码错误时也是 200duplicate 类错误永远不要重试清理重复单的顺序先停任务 → 反审核删除 → 人工改回幂等记录 → 重推对接开始前先定一件事单据编号由谁生成在做金蝶对接、被重复单或者漏单折腾的把报错原文写在评论区看到都会回。
返回列表