[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"i-ri:search-line":3,"i-ri:moon-line":8,"i-ri:sun-line":10,"i-ri:menu-3-line":12,"i-ri:close-line":14,"notes-module-items-im":16,"notes-page-im-03-message-content\u002F01-file-download":66,"i-ri:arrow-left-s-line":246,"i-ri:menu-2-line":248,"i-ri:arrow-right-s-line":250,"i-ep:copy-document":252},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm6 7h12v2H9zm-6 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z\"\u002F>",[17,22,26,30,34,38,42,46,50,54,58,62],{"path":18,"title":19,"meta":20},"\u002Fnotes\u002Fim\u002F01-message-state\u002F01-offline-sync","连接恢复了，离线消息为什么还没补齐？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},"2026-11-05T09:47:15.000Z",{"path":23,"title":24,"meta":25},"\u002Fnotes\u002Fim\u002F01-message-state\u002F02-read-cursor","手机读过了，电脑为何又显示未读？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":27,"title":28,"meta":29},"\u002Fnotes\u002Fim\u002F01-message-state\u002F03-pagination-search","聊天搜索跳回旧消息后，怎样继续翻页？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":31,"title":32,"meta":33},"\u002Fnotes\u002Fim\u002F01-message-state\u002F04-unread-refresh","消息没停，未读角标就不更新？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":35,"title":36,"meta":37},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F01-stream-parsing","一次接收回调，为什么不是一条消息？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":39,"title":40,"meta":41},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F02-request-response","长连接上的请求，怎样等到自己的响应？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":43,"title":44,"meta":45},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F03-connection-lifecycle","登录一直在连接中，到底在等什么？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":47,"title":48,"meta":49},"\u002Fnotes\u002Fim\u002F03-message-content\u002F01-file-download","附件下载完了，为什么还是打不开？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":51,"title":52,"meta":53},"\u002Fnotes\u002Fim\u002F03-message-content\u002F02-image-send","图片上传到 100%，为什么还没发出去？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":55,"title":56,"meta":57},"\u002Fnotes\u002Fim\u002F03-message-content\u002F03-mentions","聊天 @提及怎样保存身份和位置？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":59,"title":60,"meta":61},"\u002Fnotes\u002Fim\u002F04-client-release\u002F01-flutter-build","Release 包为什么仍连着测试环境？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":63,"title":64,"meta":65},"\u002Fnotes\u002Fim","即时通信",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"id":67,"type":68,"sourcePath":69,"routePath":47,"path":47,"title":48,"description":70,"updatedAt":21,"meta":71,"body":80,"toc":239},"bm90ZTpub3Rlcy9pbS8wMy1tZXNzYWdlLWNvbnRlbnQvMDEtZmlsZS1kb3dubG9hZC5tZA","note","notes\u002Fim\u002F03-message-content\u002F01-file-download.md","用户点击聊天附件，应用从远端取文件，保存到设备，再把本地位置记回消息，下次打开就能复用。文件传完以后，命名、移动和数据库写回仍可能失败。",{"id":67,"type":68,"sourcePath":69,"slug":72,"title":48,"updatedAt":21,"description":70,"tags":73,"date":77,"comment":78,"summary":79,"lastUpdated":21},"im\u002F03-message-content\u002F01-file-download",[74,75,76],"flutter","dart","IM","2026-08-30",true,"下载只是附件落地的一步，还要处理重复任务、同名文件、平台引用和消息写回。按实际停下的位置区分网络失败与本地状态失败。",{"type":81,"children":82},"root",[83,91,96,101,108,113,118,123,129,134,155,169,183,189,194,207,212,218,223,228,233],{"type":84,"tag":85,"props":86,"children":88},"element","h1",{"id":87},"附件下载完了为什么还是打不开",[89],{"type":90,"value":48},"text",{"type":84,"tag":92,"props":93,"children":94},"p",{},[95],{"type":90,"value":70},{"type":84,"tag":92,"props":97,"children":98},{},[99],{"type":90,"value":100},"这个 Flutter App 同时支持 Android 和 iOS，还会遇到两个页面打开同一条附件、不同附件文件名相同等情况。界面上都可能表现为“下载过了，却打不开”，需要沿保存流程分清。",{"type":84,"tag":102,"props":103,"children":105},"h2",{"id":104},"同一条附件点了两次要下载两遍吗",[106],{"type":90,"value":107},"同一条附件点了两次，要下载两遍吗？",{"type":84,"tag":92,"props":109,"children":110},{},[111],{"type":90,"value":112},"可以给下载入口加锁，也可以让后来的调用共用进行中的 Future。前者主要安排执行顺序，后者能让多处直接等待同一个结果。",{"type":84,"tag":92,"props":114,"children":115},{},[116],{"type":90,"value":117},"项目按消息 ID 保存进行中的下载 Future，创建后先登记，再开始下载，结束时在 finally 中移除。后来的调用找到已有任务就返回它，不再启动第二份。",{"type":84,"tag":92,"props":119,"children":120},{},[121],{"type":90,"value":122},"这个范围是当前服务实例、同一条消息。两条消息引用同一文件仍可能各下载一次，重启或其他进程也不共享。命中已有任务时，后一次传入的进度回调、更新回调和 force 不会追加；若两个页面都要进度，需要订阅共享状态或补充监听。",{"type":84,"tag":102,"props":124,"children":126},{"id":125},"名字都叫报价单怎样避免互相覆盖",[127],{"type":90,"value":128},"名字都叫报价单，怎样避免互相覆盖？",{"type":84,"tag":92,"props":130,"children":131},{},[132],{"type":90,"value":133},"先检查不存在再写，会在两个任务同时执行时竞争。项目选择原始名或带编号的名字，并为约定的临时路径创建占位：",{"type":84,"tag":135,"props":136,"children":140},"pre",{"className":137,"code":138,"language":75,"meta":139,"style":139},"language-dart shiki shiki-themes github-light github-dark","await File(tmpPath).create(recursive: true, exclusive: true);\n","",[141],{"type":84,"tag":142,"props":143,"children":144},"code",{"__ignoreMap":139},[145],{"type":84,"tag":146,"props":147,"children":150},"span",{"class":148,"line":149},"line",1,[151],{"type":84,"tag":146,"props":152,"children":153},{},[154],{"type":90,"value":138},{"type":84,"tag":92,"props":156,"children":157},{},[158,167],{"type":84,"tag":159,"props":160,"children":164},"a",{"href":161,"rel":162},"https:\u002F\u002Fapi.dart.dev\u002Fdart-io\u002FFile\u002Fcreate.html",[163],"nofollow",[165],{"type":90,"value":166},"Dart File.create",{"type":90,"value":168}," 的排他创建会在目标已存在时失败，让遵守同一规则的任务先占到自己的临时位置。但它没有锁住最终文件名，其他程序仍可创建最终目标，失败后的占位也需要清理。",{"type":84,"tag":92,"props":170,"children":171},{},[172,174,181],{"type":90,"value":173},"文件先写临时位置，检查存在且非空，再尝试移动，失败时还有复制与删除临时文件的回退。这些步骤不能统称为原子提交；",{"type":84,"tag":159,"props":175,"children":178},{"href":176,"rel":177},"https:\u002F\u002Fapi.dart.dev\u002Fdart-io\u002FFile\u002Frename.html",[163],[179],{"type":90,"value":180},"Dart File.rename",{"type":90,"value":182}," 也可能替换已有目标，不能拿来保证不覆盖。force 分支先删旧文件后再移动，后面失败还可能失去旧版本。",{"type":84,"tag":102,"props":184,"children":186},{"id":185},"消息里有本地位置就一定能打开吗",[187],{"type":90,"value":188},"消息里有本地位置，就一定能打开吗？",{"type":84,"tag":92,"props":190,"children":191},{},[192],{"type":90,"value":193},"项目对普通文件会解析为绝对路径，检查存在且长度大于零再复用。但没有核对远端大小或内容，非空并不保证下载完整。",{"type":84,"tag":92,"props":195,"children":196},{},[197,199,205],{"type":90,"value":198},"Android 优先保存原生导出返回的内容 URI，其他路径作回退。",{"type":84,"tag":142,"props":200,"children":202},{"className":201},[],[203],{"type":90,"value":204},"content:\u002F\u002F",{"type":90,"value":206}," 引用的是内容提供者管理的资源，当前分支直接复用引用，没有实际打开检查，权限过期或资源消失要到读取时才能发现。",{"type":84,"tag":92,"props":208,"children":209},{},[210],{"type":90,"value":211},"iOS 放在应用 Documents 下，消息保存相对路径，访问时再拼当前目录。因此读取时要区分相对路径、绝对路径和内容 URI，不能全部直接交给 File。它们也只对当前设备有意义，其他设备仍需远端文件引用。",{"type":84,"tag":102,"props":213,"children":215},{"id":214},"文件已经落地消息为什么还记着旧路径",[216],{"type":90,"value":217},"文件已经落地，消息为什么还记着旧路径？",{"type":84,"tag":92,"props":219,"children":220},{},[221],{"type":90,"value":222},"这类流程可以把下载、移动与消息写回的结果分别记录，失败时才知道应该恢复哪一步，同时检查残留文件与平台引用的可读性。",{"type":84,"tag":92,"props":224,"children":225},{},[226],{"type":90,"value":227},"文件与数据库是两份状态。项目取得最终位置后先修改消息对象、通知界面，再写数据库。数据库抛错时，内部方法捕获并返回 null，但已经保存的文件不会随之撤销。",{"type":84,"tag":92,"props":229,"children":230},{},[231],{"type":90,"value":232},"于是可能出现 UI 曾收到新位置、数据库仍是旧值的情况。重新打开时，先对照实际文件与消息引用，才能区分文件未落地和写回未完成。",{"type":84,"tag":234,"props":235,"children":236},"style",{},[237],{"type":90,"value":238},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":139,"searchDepth":240,"depth":240,"links":241},2,[242,243,244,245],{"id":104,"depth":240,"text":107},{"id":125,"depth":240,"text":128},{"id":185,"depth":240,"text":188},{"id":214,"depth":240,"text":217},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":247},"\u003Cpath fill=\"currentColor\" d=\"m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":249},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":251},"\u003Cpath fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"\u002F>",{"left":4,"top":4,"width":253,"height":253,"rotate":4,"vFlip":6,"hFlip":6,"body":254},1024,"\u003Cpath fill=\"currentColor\" d=\"M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64z\"\u002F>\u003Cpath fill=\"currentColor\" d=\"M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64\"\u002F>"]