[{"data":1,"prerenderedAt":1763},["ShallowReactive",2],{"notes-flutter-project-interview":3},{"id":4,"title":5,"body":6,"date":1752,"description":1753,"extension":1754,"meta":1755,"navigation":466,"path":1756,"seo":1757,"seoDescription":1758,"seoTitle":1759,"slug":1760,"stem":1761,"__hash__":1762},"notes\u002Fnotes\u002F2026-05-03-flutter-project-interview.md","Flutter 项目面试准备 — 重点与难点",{"type":7,"value":8,"toc":1723},"minimark",[9,25,28,33,38,60,64,87,89,93,97,102,113,118,200,205,217,219,223,227,233,250,255,388,393,411,416,421,423,427,432,443,447,540,544,561,563,567,572,583,587,664,668,673,675,679,684,701,705,977,981,994,996,1000,1004,1018,1022,1184,1188,1201,1203,1207,1212,1218,1221,1238,1242,1250,1252,1256,1261,1272,1276,1294,1298,1306,1308,1312,1315,1458,1460,1464,1468,1480,1484,1489,1493,1498,1500,1504,1553,1555,1559,1662,1664,1668,1679,1685,1690,1719],[10,11,12],"blockquote",{},[13,14,15,16,20,21,24],"p",{},"基于 ",[17,18,19],"strong",{},"AIRBS Wholesale","（C端批发采购App）和 ",[17,22,23],{},"YesShop Business HD","（B端iPad批发管理系统）两个真实项目的技术总结。",[26,27],"hr",{},[29,30,32],"h2",{"id":31},"一项目概览开场介绍用","一、项目概览（开场介绍用）",[34,35,37],"h3",{"id":36},"airbs-wholesalev340","AIRBS Wholesale（v3.4.0）",[39,40,41,48,54],"ul",{},[42,43,44,47],"li",{},[17,45,46],{},"定位","：B2B 批发电商移动端，面向采购商",[42,49,50,53],{},[17,51,52],{},"核心功能","：商品浏览\u002F搜索、购物车、销售订单、报价单、优惠券、QR扫码、多语言（英\u002F中\u002F西\u002F意）",[42,55,56,59],{},[17,57,58],{},"技术栈","：Flutter + GetX + Dio + SharedPreferences + json_serializable",[34,61,63],{"id":62},"yesshop-business-hdv700","YesShop Business HD（v7.0.0）",[39,65,66,71,76,81],{},[42,67,68,70],{},[17,69,46],{},"：iPad 端批发业务管理系统，面向业务员\u002F仓管",[42,72,73,75],{},[17,74,52],{},"：商品管理、采购\u002F销售订单、报价单、WMS仓储、ESL电子价签、PDA扫码枪、相机拍照",[42,77,78,80],{},[17,79,58],{},"：Flutter + GetX + Dio + Drift(SQLite) + SharedPreferences + json_serializable",[42,82,83,86],{},[17,84,85],{},"亮点","：同一套代码适配手机和 iPad 两种尺寸",[26,88],{},[29,90,92],{"id":91},"二架构设计面试高频问题","二、架构设计（面试高频问题）",[34,94,96],{"id":95},"q-项目整体架构是怎么设计的","Q: 项目整体架构是怎么设计的？",[13,98,99],{},[17,100,101],{},"回答要点：",[103,104,109],"pre",{"className":105,"code":107,"language":108},[106],"language-text","lib\u002F\n├── common\u002F          # 全局状态、路由、事件总线\n├── runtime\u002F         # 基础设施：HTTP、Auth、Language、枚举\n├── config\u002F          # 环境配置（develop\u002Ftest\u002Frelease）\n├── domains\u002F         # 业务层：Service + Model + Command\n│   ├── service\u002F     # 18个领域服务（ProductService, CartService...）\n│   ├── model\u002F       # 数据模型（38+ @JsonSerializable 类）\n│   └── command\u002F     # 命令模式处理写操作\n├── modules\u002F         # 功能页面（19个模块，每个含 Controller + View）\n├── components\u002F      # 可复用业务组件\n└── ui\u002F              # 基础 UI 组件库（按钮、表单、对话框、骨架屏...）\n","text",[110,111,107],"code",{"__ignoreMap":112},"",[13,114,115],{},[17,116,117],{},"关键设计决策：",[119,120,121,137],"table",{},[122,123,124],"thead",{},[125,126,127,131,134],"tr",{},[128,129,130],"th",{},"层级",[128,132,133],{},"职责",[128,135,136],{},"说明",[138,139,140,154,171,184],"tbody",{},[125,141,142,148,151],{},[143,144,145],"td",{},[17,146,147],{},"UI 层",[143,149,150],{},"纯展示",[143,152,153],{},"Widget 只负责渲染，不含业务逻辑",[125,155,156,161,164],{},[143,157,158],{},[17,159,160],{},"Controller 层",[143,162,163],{},"页面逻辑",[143,165,166,167,170],{},"继承 ",[110,168,169],{},"GetxController","，管理页面状态和交互",[125,172,173,178,181],{},[143,174,175],{},[17,176,177],{},"Service 层",[143,179,180],{},"业务逻辑",[143,182,183],{},"封装 API 调用和数据处理，与 UI 解耦",[125,185,186,191,194],{},[143,187,188],{},[17,189,190],{},"Model 层",[143,192,193],{},"数据定义",[143,195,196,199],{},[110,197,198],{},"@JsonSerializable"," + code gen，保证序列化一致性",[13,201,202],{},[17,203,204],{},"可以深入展开的点：",[39,206,207,210],{},[42,208,209],{},"为什么选 GetX 而不是 BLoC\u002FRiverpod → 项目偏 CRUD 型业务，GetX 的路由+状态+DI 一体化开发效率高",[42,211,212,213,216],{},"Service 层的设计 → 每个业务领域一个 Service，通过 ",[110,214,215],{},"Get.lazyPut()"," 懒加载注入，按需初始化",[26,218],{},[29,220,222],{"id":221},"三重点难点及解决方案","三、重点难点及解决方案",[34,224,226],{"id":225},"难点-1购物车状态管理与多端同步","难点 1：购物车状态管理与多端同步",[13,228,229,232],{},[17,230,231],{},"问题描述：","\n购物车是核心模块，涉及多种复杂计算和状态同步：",[39,234,235,238,241,244,247],{},[42,236,237],{},"商品有多种包装规格（箱\u002F包\u002F个），切换规格时需要合并或拆分",[42,239,240],{},"数量变化触发实时价格计算（单价 × 数量 × 包装倍率）",[42,242,243],{},"重量\u002F体积需要单位换算后聚合",[42,245,246],{},"优惠券折扣叠加计算",[42,248,249],{},"用户快速操作时防止频繁请求后端",[13,251,252],{},[17,253,254],{},"解决方案：",[103,256,260],{"className":257,"code":258,"language":259,"meta":112,"style":112},"language-dart shiki shiki-themes github-light github-dark","\u002F\u002F 1. CartState 单例 + Rx 响应式\nclass CartState {\n  final cartItems = RxList\u003CCartItemVdto>([]);\n  final totalAmount = 0.0.obs;\n  final totalWeight = 0.0.obs;\n  \n  \u002F\u002F 2. 防抖同步：250ms 内的多次数量变更只触发一次后端请求\n  void onQuantityChanged(String itemId, int qty) {\n    _updateLocalState(itemId, qty);      \u002F\u002F 立即更新本地\n    _debouncer.run(() => _syncToServer()); \u002F\u002F 防抖同步后端\n  }\n  \n  \u002F\u002F 3. 聚合计算：单位换算 + 折扣\n  void calcSumInfo() {\n    totalWeight.value = cartItems.sumBy((x) =>\n      UnitUtils.weightConvert(targetUnit, x.weightUnit, x.weight * x.quantity));\n    \n    final discounted = amount * (100 - discountRate) \u002F 100;\n    totalAmount.value = discounted;\n  }\n}\n","dart",[110,261,262,270,276,282,288,294,300,306,312,318,324,330,335,341,347,353,359,365,371,377,382],{"__ignoreMap":112},[263,264,267],"span",{"class":265,"line":266},"line",1,[263,268,269],{},"\u002F\u002F 1. CartState 单例 + Rx 响应式\n",[263,271,273],{"class":265,"line":272},2,[263,274,275],{},"class CartState {\n",[263,277,279],{"class":265,"line":278},3,[263,280,281],{},"  final cartItems = RxList\u003CCartItemVdto>([]);\n",[263,283,285],{"class":265,"line":284},4,[263,286,287],{},"  final totalAmount = 0.0.obs;\n",[263,289,291],{"class":265,"line":290},5,[263,292,293],{},"  final totalWeight = 0.0.obs;\n",[263,295,297],{"class":265,"line":296},6,[263,298,299],{},"  \n",[263,301,303],{"class":265,"line":302},7,[263,304,305],{},"  \u002F\u002F 2. 防抖同步：250ms 内的多次数量变更只触发一次后端请求\n",[263,307,309],{"class":265,"line":308},8,[263,310,311],{},"  void onQuantityChanged(String itemId, int qty) {\n",[263,313,315],{"class":265,"line":314},9,[263,316,317],{},"    _updateLocalState(itemId, qty);      \u002F\u002F 立即更新本地\n",[263,319,321],{"class":265,"line":320},10,[263,322,323],{},"    _debouncer.run(() => _syncToServer()); \u002F\u002F 防抖同步后端\n",[263,325,327],{"class":265,"line":326},11,[263,328,329],{},"  }\n",[263,331,333],{"class":265,"line":332},12,[263,334,299],{},[263,336,338],{"class":265,"line":337},13,[263,339,340],{},"  \u002F\u002F 3. 聚合计算：单位换算 + 折扣\n",[263,342,344],{"class":265,"line":343},14,[263,345,346],{},"  void calcSumInfo() {\n",[263,348,350],{"class":265,"line":349},15,[263,351,352],{},"    totalWeight.value = cartItems.sumBy((x) =>\n",[263,354,356],{"class":265,"line":355},16,[263,357,358],{},"      UnitUtils.weightConvert(targetUnit, x.weightUnit, x.weight * x.quantity));\n",[263,360,362],{"class":265,"line":361},17,[263,363,364],{},"    \n",[263,366,368],{"class":265,"line":367},18,[263,369,370],{},"    final discounted = amount * (100 - discountRate) \u002F 100;\n",[263,372,374],{"class":265,"line":373},19,[263,375,376],{},"    totalAmount.value = discounted;\n",[263,378,380],{"class":265,"line":379},20,[263,381,329],{},[263,383,385],{"class":265,"line":384},21,[263,386,387],{},"}\n",[13,389,390],{},[17,391,392],{},"YesShop HD 额外难点 — 离线购物车：",[39,394,395,402,408],{},[42,396,397,398,401],{},"使用 ",[17,399,400],{},"Drift ORM (SQLite)"," 做本地持久化，断网时也能操作购物车",[42,403,404,407],{},[110,405,406],{},"CartSubTable"," 存储明细，重新联网后批量同步",[42,409,410],{},"去重逻辑：相同商品+相同规格 → 合并数量而非新增行",[13,412,413],{},[17,414,415],{},"面试话术：",[10,417,418],{},[13,419,420],{},"\"购物车是整个系统最复杂的状态模块。难点在于实时计算（价格、重量、体积的单位换算）和前后端同步。我用 GetX 的 Rx 响应式做本地即时更新，配合 250ms 防抖避免频繁请求。YesShop 项目因为有离线场景，额外用 Drift 做了 SQLite 持久化，保证断网可用。\"",[26,422],{},[34,424,426],{"id":425},"难点-2ipad-手机双端适配yesshop-hd","难点 2：iPad + 手机双端适配（YesShop HD）",[13,428,429,431],{},[17,430,231],{},"\n同一套代码需要适配 iPad（1024×1366）和手机（375×812），布局差异很大：",[39,433,434,437,440],{},[42,435,436],{},"iPad 用多列布局，手机用单列",[42,438,439],{},"字体大小、间距、图标尺寸都不同",[42,441,442],{},"部分页面 iPad 有侧边栏，手机没有",[13,444,445],{},[17,446,254],{},[103,448,450],{"className":257,"code":449,"language":259,"meta":112,"style":112},"\u002F\u002F 1. 基于 flutter_screenutil 的双端适配\nScreenUtils.isPad  \u002F\u002F 运行时判断设备类型\n\n\u002F\u002F 2. 封装适配函数\ndouble fitW(double phoneValue, double padValue) {\n  return ScreenUtils.isPad ? padValue.w : phoneValue.w;\n}\n\n\u002F\u002F 3. 不同设计稿尺寸初始化\nScreenUtil.init(context,\n  designSize: ScreenUtils.isPad \n    ? const Size(1024, 1366)  \u002F\u002F iPad\n    : const Size(375, 812),    \u002F\u002F iPhone\n);\n\n\u002F\u002F 4. 资源文件分目录管理\n\u002F\u002F assets\u002Fphone\u002F  → 手机端图片\n\u002F\u002F assets\u002Fpad\u002F    → iPad端图片\n",[110,451,452,457,462,468,473,478,483,487,491,496,501,506,511,516,521,525,530,535],{"__ignoreMap":112},[263,453,454],{"class":265,"line":266},[263,455,456],{},"\u002F\u002F 1. 基于 flutter_screenutil 的双端适配\n",[263,458,459],{"class":265,"line":272},[263,460,461],{},"ScreenUtils.isPad  \u002F\u002F 运行时判断设备类型\n",[263,463,464],{"class":265,"line":278},[263,465,467],{"emptyLinePlaceholder":466},true,"\n",[263,469,470],{"class":265,"line":284},[263,471,472],{},"\u002F\u002F 2. 封装适配函数\n",[263,474,475],{"class":265,"line":290},[263,476,477],{},"double fitW(double phoneValue, double padValue) {\n",[263,479,480],{"class":265,"line":296},[263,481,482],{},"  return ScreenUtils.isPad ? padValue.w : phoneValue.w;\n",[263,484,485],{"class":265,"line":302},[263,486,387],{},[263,488,489],{"class":265,"line":308},[263,490,467],{"emptyLinePlaceholder":466},[263,492,493],{"class":265,"line":314},[263,494,495],{},"\u002F\u002F 3. 不同设计稿尺寸初始化\n",[263,497,498],{"class":265,"line":320},[263,499,500],{},"ScreenUtil.init(context,\n",[263,502,503],{"class":265,"line":326},[263,504,505],{},"  designSize: ScreenUtils.isPad \n",[263,507,508],{"class":265,"line":332},[263,509,510],{},"    ? const Size(1024, 1366)  \u002F\u002F iPad\n",[263,512,513],{"class":265,"line":337},[263,514,515],{},"    : const Size(375, 812),    \u002F\u002F iPhone\n",[263,517,518],{"class":265,"line":343},[263,519,520],{},");\n",[263,522,523],{"class":265,"line":349},[263,524,467],{"emptyLinePlaceholder":466},[263,526,527],{"class":265,"line":355},[263,528,529],{},"\u002F\u002F 4. 资源文件分目录管理\n",[263,531,532],{"class":265,"line":361},[263,533,534],{},"\u002F\u002F assets\u002Fphone\u002F  → 手机端图片\n",[263,536,537],{"class":265,"line":367},[263,538,539],{},"\u002F\u002F assets\u002Fpad\u002F    → iPad端图片\n",[13,541,542],{},[17,543,415],{},[10,545,546],{},[13,547,548,549,552,553,556,557,560],{},"\"YesShop 最大的挑战之一是一套代码同时跑在 iPad 和手机上。我们用 ",[110,550,551],{},"flutter_screenutil"," 配合自定义的 ",[110,554,555],{},"fitW()"," 函数做尺寸适配，通过 ",[110,558,559],{},"ScreenUtils.isPad"," 在运行时决定布局策略。对于布局差异大的页面，用条件渲染切换多列\u002F单列。图片资源也按设备类型分目录管理，避免 iPad 上图片模糊。\"",[26,562],{},[34,564,566],{"id":565},"难点-3pda-扫码枪硬件集成yesshop-hd","难点 3：PDA 扫码枪硬件集成（YesShop HD）",[13,568,569,571],{},[17,570,231],{},"\n业务员使用 PDA 手持终端（带物理扫码键），需要：",[39,573,574,577,580],{},[42,575,576],{},"识别多种 PDA 品牌（CRUISE、AUTOID、MT5）",[42,578,579],{},"监听硬件扫码按键事件",[42,581,582],{},"扫码结果要根据当前所在页面路由到不同处理逻辑",[13,584,585],{},[17,586,254],{},[103,588,590],{"className":257,"code":589,"language":259,"meta":112,"style":112},"\u002F\u002F 1. Platform Channel 与原生通信\nstatic const pdaScanChannel = MethodChannel('pdaScanChannel');\n\n\u002F\u002F 2. 设备识别 — 通过 manufacturer 字段判断 PDA 型号\nbool isPdaDevice = ['CRUISE', 'AUTOID', 'MT5']\n    .contains(deviceInfo.manufacturer.toUpperCase());\n\n\u002F\u002F 3. Android 端 BroadcastReceiver 接收扫码数据\n\u002F\u002F 原生层注册广播接收器，扫到条码后通过 Channel 传给 Flutter\n\n\u002F\u002F 4. 路由感知的扫码订阅\n\u002F\u002F 根据当前页面路由，决定扫码结果的处理方式：\n\u002F\u002F - 商品页 → 查询商品\n\u002F\u002F - 库存页 → 查找库位\n\u002F\u002F - 订单页 → 匹配订单号\n",[110,591,592,597,602,606,611,616,621,625,630,635,639,644,649,654,659],{"__ignoreMap":112},[263,593,594],{"class":265,"line":266},[263,595,596],{},"\u002F\u002F 1. Platform Channel 与原生通信\n",[263,598,599],{"class":265,"line":272},[263,600,601],{},"static const pdaScanChannel = MethodChannel('pdaScanChannel');\n",[263,603,604],{"class":265,"line":278},[263,605,467],{"emptyLinePlaceholder":466},[263,607,608],{"class":265,"line":284},[263,609,610],{},"\u002F\u002F 2. 设备识别 — 通过 manufacturer 字段判断 PDA 型号\n",[263,612,613],{"class":265,"line":290},[263,614,615],{},"bool isPdaDevice = ['CRUISE', 'AUTOID', 'MT5']\n",[263,617,618],{"class":265,"line":296},[263,619,620],{},"    .contains(deviceInfo.manufacturer.toUpperCase());\n",[263,622,623],{"class":265,"line":302},[263,624,467],{"emptyLinePlaceholder":466},[263,626,627],{"class":265,"line":308},[263,628,629],{},"\u002F\u002F 3. Android 端 BroadcastReceiver 接收扫码数据\n",[263,631,632],{"class":265,"line":314},[263,633,634],{},"\u002F\u002F 原生层注册广播接收器，扫到条码后通过 Channel 传给 Flutter\n",[263,636,637],{"class":265,"line":320},[263,638,467],{"emptyLinePlaceholder":466},[263,640,641],{"class":265,"line":326},[263,642,643],{},"\u002F\u002F 4. 路由感知的扫码订阅\n",[263,645,646],{"class":265,"line":332},[263,647,648],{},"\u002F\u002F 根据当前页面路由，决定扫码结果的处理方式：\n",[263,650,651],{"class":265,"line":337},[263,652,653],{},"\u002F\u002F - 商品页 → 查询商品\n",[263,655,656],{"class":265,"line":343},[263,657,658],{},"\u002F\u002F - 库存页 → 查找库位\n",[263,660,661],{"class":265,"line":349},[263,662,663],{},"\u002F\u002F - 订单页 → 匹配订单号\n",[13,665,666],{},[17,667,415],{},[10,669,670],{},[13,671,672],{},"\"YesShop 需要对接 PDA 扫码枪，这是典型的 Platform Channel 实战场景。Android 端通过 BroadcastReceiver 监听硬件扫码事件，再通过 MethodChannel 传给 Flutter。难点是同一个扫码入口要根据当前页面路由分发到不同业务逻辑，我设计了一个基于路由的订阅者模式，每个页面注册自己的扫码回调，离开页面时自动取消。\"",[26,674],{},[34,676,678],{"id":677},"难点-4网络层封装与统一错误处理","难点 4：网络层封装与统一错误处理",[13,680,681,683],{},[17,682,231],{},"\n两个项目共 30+ 个 Service，几百个 API 接口，需要统一管理：",[39,685,686,689,692,695,698],{},[42,687,688],{},"请求头动态注入（token、语言、时区、公司ID、区域ID）",[42,690,691],{},"登录过期自动跳转",[42,693,694],{},"Loading 弹窗统一控制",[42,696,697],{},"分页请求的通用封装",[42,699,700],{},"文件上传（单张\u002F多张图片）",[13,702,703],{},[17,704,254],{},[103,706,708],{"className":257,"code":707,"language":259,"meta":112,"style":112},"class Http {\n  static final Dio _dio = Dio(BaseOptions(\n    connectTimeout: Duration(seconds: 20),\n    receiveTimeout: Duration(seconds: 25),\n  ));\n\n  \u002F\u002F 动态请求头\n  static Map\u003CString, String> get _headers => {\n    'ide': Auth.identity,          \u002F\u002F 登录 token\n    'lang': Lang.current.apiCode,   \u002F\u002F 语言代码\n    'corp': AppContext.corpId,      \u002F\u002F 公司ID\n    'area': AppContext.areaCorpId,  \u002F\u002F 区域ID\n    'offset_hours': timezone,       \u002F\u002F 时区偏移\n    'type': '3',                    \u002F\u002F 终端类型\n  };\n\n  \u002F\u002F 统一响应处理\n  static Future\u003CHttpResult\u003CT>> post\u003CT>(String url, {\n    Map\u003CString, dynamic>? data,\n    bool showLoading = false,\n    T Function(dynamic)? fromJson,\n  }) async {\n    if (showLoading) DialogUtils.showLoading();\n    try {\n      final response = await _dio.post(url, data: data);\n      final result = HttpResult\u003CT>.fromJson(response.data, fromJson);\n      \n      \u002F\u002F 登录过期统一拦截\n      if (result.errorCode == HttpErrorCode.loginExpired) {\n        _redirectToLogin();\n      }\n      return result;\n    } on DioException catch (e) {\n      return HttpResult.error(e.message);\n    } finally {\n      if (showLoading) DialogUtils.dismiss();\n    }\n  }\n\n  \u002F\u002F 分页请求封装\n  static Future\u003CPagedResult\u003CT>> paged\u003CT>(...) { ... }\n  \n  \u002F\u002F 图片上传\n  static Future\u003CHttpResult\u003CT>> uploadImage(File file) { ... }\n  static Future\u003CHttpResult\u003CList\u003CT>>> multiUploadImage(List\u003CFile> files) { ... }\n}\n",[110,709,710,715,720,725,730,735,739,744,749,757,765,773,781,789,797,802,806,811,816,821,826,831,837,843,849,855,861,867,873,879,885,891,897,903,909,915,921,927,932,937,943,949,954,960,966,972],{"__ignoreMap":112},[263,711,712],{"class":265,"line":266},[263,713,714],{},"class Http {\n",[263,716,717],{"class":265,"line":272},[263,718,719],{},"  static final Dio _dio = Dio(BaseOptions(\n",[263,721,722],{"class":265,"line":278},[263,723,724],{},"    connectTimeout: Duration(seconds: 20),\n",[263,726,727],{"class":265,"line":284},[263,728,729],{},"    receiveTimeout: Duration(seconds: 25),\n",[263,731,732],{"class":265,"line":290},[263,733,734],{},"  ));\n",[263,736,737],{"class":265,"line":296},[263,738,467],{"emptyLinePlaceholder":466},[263,740,741],{"class":265,"line":302},[263,742,743],{},"  \u002F\u002F 动态请求头\n",[263,745,746],{"class":265,"line":308},[263,747,748],{},"  static Map\u003CString, String> get _headers => {\n",[263,750,751,754],{"class":265,"line":314},[263,752,753],{},"    'ide': Auth.identity,",[263,755,756],{},"          \u002F\u002F 登录 token\n",[263,758,759,762],{"class":265,"line":320},[263,760,761],{},"    'lang': Lang.current.apiCode,",[263,763,764],{},"   \u002F\u002F 语言代码\n",[263,766,767,770],{"class":265,"line":326},[263,768,769],{},"    'corp': AppContext.corpId,",[263,771,772],{},"      \u002F\u002F 公司ID\n",[263,774,775,778],{"class":265,"line":332},[263,776,777],{},"    'area': AppContext.areaCorpId,",[263,779,780],{},"  \u002F\u002F 区域ID\n",[263,782,783,786],{"class":265,"line":337},[263,784,785],{},"    'offset_hours': timezone,",[263,787,788],{},"       \u002F\u002F 时区偏移\n",[263,790,791,794],{"class":265,"line":343},[263,792,793],{},"    'type': '3',",[263,795,796],{},"                    \u002F\u002F 终端类型\n",[263,798,799],{"class":265,"line":349},[263,800,801],{},"  };\n",[263,803,804],{"class":265,"line":355},[263,805,467],{"emptyLinePlaceholder":466},[263,807,808],{"class":265,"line":361},[263,809,810],{},"  \u002F\u002F 统一响应处理\n",[263,812,813],{"class":265,"line":367},[263,814,815],{},"  static Future\u003CHttpResult\u003CT>> post\u003CT>(String url, {\n",[263,817,818],{"class":265,"line":373},[263,819,820],{},"    Map\u003CString, dynamic>? data,\n",[263,822,823],{"class":265,"line":379},[263,824,825],{},"    bool showLoading = false,\n",[263,827,828],{"class":265,"line":384},[263,829,830],{},"    T Function(dynamic)? fromJson,\n",[263,832,834],{"class":265,"line":833},22,[263,835,836],{},"  }) async {\n",[263,838,840],{"class":265,"line":839},23,[263,841,842],{},"    if (showLoading) DialogUtils.showLoading();\n",[263,844,846],{"class":265,"line":845},24,[263,847,848],{},"    try {\n",[263,850,852],{"class":265,"line":851},25,[263,853,854],{},"      final response = await _dio.post(url, data: data);\n",[263,856,858],{"class":265,"line":857},26,[263,859,860],{},"      final result = HttpResult\u003CT>.fromJson(response.data, fromJson);\n",[263,862,864],{"class":265,"line":863},27,[263,865,866],{},"      \n",[263,868,870],{"class":265,"line":869},28,[263,871,872],{},"      \u002F\u002F 登录过期统一拦截\n",[263,874,876],{"class":265,"line":875},29,[263,877,878],{},"      if (result.errorCode == HttpErrorCode.loginExpired) {\n",[263,880,882],{"class":265,"line":881},30,[263,883,884],{},"        _redirectToLogin();\n",[263,886,888],{"class":265,"line":887},31,[263,889,890],{},"      }\n",[263,892,894],{"class":265,"line":893},32,[263,895,896],{},"      return result;\n",[263,898,900],{"class":265,"line":899},33,[263,901,902],{},"    } on DioException catch (e) {\n",[263,904,906],{"class":265,"line":905},34,[263,907,908],{},"      return HttpResult.error(e.message);\n",[263,910,912],{"class":265,"line":911},35,[263,913,914],{},"    } finally {\n",[263,916,918],{"class":265,"line":917},36,[263,919,920],{},"      if (showLoading) DialogUtils.dismiss();\n",[263,922,924],{"class":265,"line":923},37,[263,925,926],{},"    }\n",[263,928,930],{"class":265,"line":929},38,[263,931,329],{},[263,933,935],{"class":265,"line":934},39,[263,936,467],{"emptyLinePlaceholder":466},[263,938,940],{"class":265,"line":939},40,[263,941,942],{},"  \u002F\u002F 分页请求封装\n",[263,944,946],{"class":265,"line":945},41,[263,947,948],{},"  static Future\u003CPagedResult\u003CT>> paged\u003CT>(...) { ... }\n",[263,950,952],{"class":265,"line":951},42,[263,953,299],{},[263,955,957],{"class":265,"line":956},43,[263,958,959],{},"  \u002F\u002F 图片上传\n",[263,961,963],{"class":265,"line":962},44,[263,964,965],{},"  static Future\u003CHttpResult\u003CT>> uploadImage(File file) { ... }\n",[263,967,969],{"class":265,"line":968},45,[263,970,971],{},"  static Future\u003CHttpResult\u003CList\u003CT>>> multiUploadImage(List\u003CFile> files) { ... }\n",[263,973,975],{"class":265,"line":974},46,[263,976,387],{},[13,978,979],{},[17,980,415],{},[10,982,983],{},[13,984,985,986,989,990,993],{},"\"我封装了一个统一的 Http 工具类，基于 Dio，所有请求自动注入动态 header（token、语言、时区等）。核心设计是 ",[110,987,988],{},"HttpResult\u003CT>"," 泛型响应包装，配合 fromJson 工厂方法实现类型安全的反序列化。对于登录过期（errorCode 1003\u002F1004）做了全局拦截自动跳转登录页。还封装了 ",[110,991,992],{},"paged\u003CT>()"," 方法统一处理分页逻辑，避免每个列表页重复写分页代码。\"",[26,995],{},[34,997,999],{"id":998},"难点-5多语言方案设计与维护","难点 5：多语言方案设计与维护",[13,1001,1002],{},[17,1003,231],{},[39,1005,1006,1009,1012,1015],{},[42,1007,1008],{},"AIRBS 支持 4 种语言（英\u002F中\u002F西\u002F意），YesShop 支持 3 种（英\u002F中\u002F西）",[42,1010,1011],{},"语言需要运行时切换，且持久化到本地",[42,1013,1014],{},"API 请求头也要同步当前语言",[42,1016,1017],{},"枚举值、单位名称等也需要国际化",[13,1019,1020],{},[17,1021,254],{},[103,1023,1025],{"className":257,"code":1024,"language":259,"meta":112,"style":112},"\u002F\u002F 1. 自定义 L 类 — 编译时类型安全\nclass L {\n  final String en;\n  final String zh;\n  final String es;\n  final String it;\n  \n  const L({required this.en, required this.zh, required this.es, required this.it});\n  \n  \u002F\u002F 根据当前语言返回对应文本\n  String get tr => switch (Lang.current) {\n    Language.en => en,\n    Language.zh => zh,\n    Language.es => es,\n    Language.it => it,\n  };\n}\n\n\u002F\u002F 2. 每个模块定义自己的 locals\nfinal _locals = (\n  msgConfirmClear: L(en: 'Clear cart?', zh: '清空购物车？', es: '¿Vaciar carrito?', it: 'Svuotare?'),\n  btnCheckout: L(en: 'Checkout', zh: '结算', es: 'Pagar', it: 'Pagare'),\n);\n\n\u002F\u002F 3. 使用\nText(_locals.msgConfirmClear.tr)\n\n\u002F\u002F 4. 语言切换 — 通知全局\nLang.changeTo(Language.en);\n\u002F\u002F → 持久化到 SharedPreferences\n\u002F\u002F → Get.updateLocale() 更新 Material 组件语言\n\u002F\u002F → EventBus 发送 LanguageChangeEvent\n\u002F\u002F → API 请求头自动切换\n",[110,1026,1027,1032,1037,1042,1047,1052,1057,1061,1066,1070,1075,1080,1085,1090,1095,1100,1104,1108,1112,1117,1122,1127,1132,1136,1140,1145,1150,1154,1159,1164,1169,1174,1179],{"__ignoreMap":112},[263,1028,1029],{"class":265,"line":266},[263,1030,1031],{},"\u002F\u002F 1. 自定义 L 类 — 编译时类型安全\n",[263,1033,1034],{"class":265,"line":272},[263,1035,1036],{},"class L {\n",[263,1038,1039],{"class":265,"line":278},[263,1040,1041],{},"  final String en;\n",[263,1043,1044],{"class":265,"line":284},[263,1045,1046],{},"  final String zh;\n",[263,1048,1049],{"class":265,"line":290},[263,1050,1051],{},"  final String es;\n",[263,1053,1054],{"class":265,"line":296},[263,1055,1056],{},"  final String it;\n",[263,1058,1059],{"class":265,"line":302},[263,1060,299],{},[263,1062,1063],{"class":265,"line":308},[263,1064,1065],{},"  const L({required this.en, required this.zh, required this.es, required this.it});\n",[263,1067,1068],{"class":265,"line":314},[263,1069,299],{},[263,1071,1072],{"class":265,"line":320},[263,1073,1074],{},"  \u002F\u002F 根据当前语言返回对应文本\n",[263,1076,1077],{"class":265,"line":326},[263,1078,1079],{},"  String get tr => switch (Lang.current) {\n",[263,1081,1082],{"class":265,"line":332},[263,1083,1084],{},"    Language.en => en,\n",[263,1086,1087],{"class":265,"line":337},[263,1088,1089],{},"    Language.zh => zh,\n",[263,1091,1092],{"class":265,"line":343},[263,1093,1094],{},"    Language.es => es,\n",[263,1096,1097],{"class":265,"line":349},[263,1098,1099],{},"    Language.it => it,\n",[263,1101,1102],{"class":265,"line":355},[263,1103,801],{},[263,1105,1106],{"class":265,"line":361},[263,1107,387],{},[263,1109,1110],{"class":265,"line":367},[263,1111,467],{"emptyLinePlaceholder":466},[263,1113,1114],{"class":265,"line":373},[263,1115,1116],{},"\u002F\u002F 2. 每个模块定义自己的 locals\n",[263,1118,1119],{"class":265,"line":379},[263,1120,1121],{},"final _locals = (\n",[263,1123,1124],{"class":265,"line":384},[263,1125,1126],{},"  msgConfirmClear: L(en: 'Clear cart?', zh: '清空购物车？', es: '¿Vaciar carrito?', it: 'Svuotare?'),\n",[263,1128,1129],{"class":265,"line":833},[263,1130,1131],{},"  btnCheckout: L(en: 'Checkout', zh: '结算', es: 'Pagar', it: 'Pagare'),\n",[263,1133,1134],{"class":265,"line":839},[263,1135,520],{},[263,1137,1138],{"class":265,"line":845},[263,1139,467],{"emptyLinePlaceholder":466},[263,1141,1142],{"class":265,"line":851},[263,1143,1144],{},"\u002F\u002F 3. 使用\n",[263,1146,1147],{"class":265,"line":857},[263,1148,1149],{},"Text(_locals.msgConfirmClear.tr)\n",[263,1151,1152],{"class":265,"line":863},[263,1153,467],{"emptyLinePlaceholder":466},[263,1155,1156],{"class":265,"line":869},[263,1157,1158],{},"\u002F\u002F 4. 语言切换 — 通知全局\n",[263,1160,1161],{"class":265,"line":875},[263,1162,1163],{},"Lang.changeTo(Language.en);\n",[263,1165,1166],{"class":265,"line":881},[263,1167,1168],{},"\u002F\u002F → 持久化到 SharedPreferences\n",[263,1170,1171],{"class":265,"line":887},[263,1172,1173],{},"\u002F\u002F → Get.updateLocale() 更新 Material 组件语言\n",[263,1175,1176],{"class":265,"line":893},[263,1177,1178],{},"\u002F\u002F → EventBus 发送 LanguageChangeEvent\n",[263,1180,1181],{"class":265,"line":899},[263,1182,1183],{},"\u002F\u002F → API 请求头自动切换\n",[13,1185,1186],{},[17,1187,415],{},[10,1189,1190],{},[13,1191,1192,1193,1196,1197,1200],{},"\"我们没用 ARB 文件或第三方国际化包，而是自定义了一个 ",[110,1194,1195],{},"L"," 类，每个翻译项是编译时常量，通过 ",[110,1198,1199],{},".tr"," getter 根据当前语言返回对应文本。好处是类型安全、IDE 自动补全、重构友好。切换语言时通过 EventBus 广播，同时更新 SharedPreferences 持久化、GetX locale、和 API 请求头，保证全链路一致。\"",[26,1202],{},[34,1204,1206],{"id":1205},"难点-6商品可售性判断的复杂业务逻辑","难点 6：商品可售性判断的复杂业务逻辑",[13,1208,1209,1211],{},[17,1210,231],{},"\n一个商品是否可以售卖，取决于 5 个条件同时满足：",[103,1213,1216],{"className":1214,"code":1215,"language":108},[106],"1. 商品启用状态 (enabled = true)\n2. 至少有一个可售包装规格\n3. 平台销售标志开启\n4. 库存数量 > 0\n5. 售价 > 0\n",[110,1217,1215],{"__ignoreMap":112},[13,1219,1220],{},"还有更复杂的定价策略：",[39,1222,1223,1226,1229,1232,1235],{},[42,1224,1225],{},"基础价 + 数量阶梯价",[42,1227,1228],{},"特价商品（isSpecial + specialPrice）",[42,1230,1231],{},"清仓价 \u002F 返场价",[42,1233,1234],{},"优惠券折扣叠加",[42,1236,1237],{},"按包装规格的倍率计算（1箱=12个，price × rate）",[13,1239,1240],{},[17,1241,415],{},[10,1243,1244],{},[13,1245,1246,1247,1249],{},"\"批发场景的定价逻辑比 C 端复杂很多。一个商品能不能卖要过 5 个校验，价格计算涉及包装倍率、阶梯价、特价、优惠券叠加。我在 Service 层统一封装了定价计算方法，Model 层用 ",[110,1248,198],{}," 保证后端数据映射准确，避免前端自己拼凑计算逻辑导致金额不一致。\"",[26,1251],{},[34,1253,1255],{"id":1254},"难点-7自定义数字键盘与表单交互yesshop-hd","难点 7：自定义数字键盘与表单交互（YesShop HD）",[13,1257,1258,1260],{},[17,1259,231],{},"\n业务员在 iPad 上录入大量数字（数量、价格、重量），系统键盘体验差：",[39,1262,1263,1266,1269],{},[42,1264,1265],{},"需要支持上一项\u002F下一项快速跳转",[42,1267,1268],{},"小数点可选（数量不需要，价格需要）",[42,1270,1271],{},"输入时顶部实时显示当前值",[13,1273,1274],{},[17,1275,254],{},[39,1277,1278,1285,1291],{},[42,1279,1280,1281,1284],{},"自定义 ",[110,1282,1283],{},"NumericKeyboard"," 组件，替代系统键盘",[42,1286,1287,1290],{},[110,1288,1289],{},"NumericTextFieldManage"," 管理焦点链，支持 Previous\u002FNext\u002FDone",[42,1292,1293],{},"路由感知 — 离开页面自动关闭键盘、清理焦点",[13,1295,1296],{},[17,1297,415],{},[10,1299,1300],{},[13,1301,1302,1303,1305],{},"\"iPad 上录入大量数字时系统键盘体验很差，所以我们做了自定义数字键盘。难点不在键盘本身，而是焦点管理——多个输入框之间的 Previous\u002FNext 跳转、页面切换时的自动清理。我用了一个 ",[110,1304,1289],{}," 统一管理焦点链，配合路由监听做生命周期清理。\"",[26,1307],{},[29,1309,1311],{"id":1310},"四性能优化面试加分项","四、性能优化（面试加分项）",[34,1313,1314],{"id":1314},"可以聊的优化点",[119,1316,1317,1330],{},[122,1318,1319],{},[125,1320,1321,1324,1327],{},[128,1322,1323],{},"优化项",[128,1325,1326],{},"做法",[128,1328,1329],{},"效果",[138,1331,1332,1352,1365,1381,1399,1412,1427,1443],{},[125,1333,1334,1339,1349],{},[143,1335,1336],{},[17,1337,1338],{},"图片加载",[143,1340,1341,1344,1345,1348],{},[110,1342,1343],{},"cached_network_image"," + ",[110,1346,1347],{},"flutter_cache_manager"," 二级缓存",[143,1350,1351],{},"减少重复网络请求，列表滑动流畅",[125,1353,1354,1359,1362],{},[143,1355,1356],{},[17,1357,1358],{},"图片上传",[143,1360,1361],{},"上传前压缩（quality 90%，min 800×800）",[143,1363,1364],{},"减少上传时间和带宽",[125,1366,1367,1372,1378],{},[143,1368,1369],{},[17,1370,1371],{},"列表分页",[143,1373,1374,1377],{},[110,1375,1376],{},"RefreshPagingListController"," 封装分页+下拉刷新+上拉加载",[143,1379,1380],{},"首屏快，按需加载",[125,1382,1383,1388,1396],{},[143,1384,1385],{},[17,1386,1387],{},"骨架屏",[143,1389,1280,1390,1344,1393],{},[110,1391,1392],{},"ShimmerWidget",[110,1394,1395],{},"AnimationController.unbounded()",[143,1397,1398],{},"提升感知性能，替代白屏等待",[125,1400,1401,1406,1409],{},[143,1402,1403],{},[17,1404,1405],{},"防抖",[143,1407,1408],{},"购物车数量变更 250ms 防抖",[143,1410,1411],{},"避免频繁 API 调用",[125,1413,1414,1419,1424],{},[143,1415,1416],{},[17,1417,1418],{},"懒加载",[143,1420,1421,1423],{},[110,1422,215],{}," 按需初始化 Service\u002FController",[143,1425,1426],{},"减少启动时间",[125,1428,1429,1434,1440],{},[143,1430,1431],{},[17,1432,1433],{},"文字缩放",[143,1435,1436,1439],{},[110,1437,1438],{},"TextScaler.linear(1.0)"," 固定缩放比",[143,1441,1442],{},"防止系统字体放大导致布局错乱",[125,1444,1445,1450,1455],{},[143,1446,1447],{},[17,1448,1449],{},"屏幕适配",[143,1451,1452,1454],{},[110,1453,551],{}," + splitScreenMode",[143,1456,1457],{},"不同设备一致体验",[26,1459],{},[29,1461,1463],{"id":1462},"五技术选型问题面试常见追问","五、技术选型问题（面试常见追问）",[34,1465,1467],{"id":1466},"q-为什么选-getx-而不是-bloc","Q: 为什么选 GetX 而不是 BLoC？",[10,1469,1470],{},[13,1471,1472,1473,1344,1476,1479],{},"\"两个项目都是业务驱动的 CRUD 应用，页面多、迭代快。GetX 的优势是路由、状态管理、依赖注入三合一，减少样板代码。BLoC 更适合状态流转复杂的场景，但对于我们这种以表单和列表为主的业务系统，GetX 的 ",[110,1474,1475],{},"Obx",[110,1477,1478],{},"RxList"," 开发效率更高。缺点是 GetX 的隐式依赖查找在大型项目中需要注意管理生命周期。\"",[34,1481,1483],{"id":1482},"q-为什么-yesshop-要用-drift-而-airbs-不用","Q: 为什么 YesShop 要用 Drift 而 AIRBS 不用？",[10,1485,1486],{},[13,1487,1488],{},"\"YesShop 是业务员使用的工具，有离线操作购物车的场景，需要本地持久化。Drift 提供类型安全的 SQL 操作和 schema migration，比直接用 sqflite 写 raw SQL 更可靠。AIRBS 是 C 端采购 App，网络环境稳定，用 SharedPreferences 存简单配置就够了。\"",[34,1490,1492],{"id":1491},"q-网络层为什么选-dio-而不是-http-包","Q: 网络层为什么选 Dio 而不是 http 包？",[10,1494,1495],{},[13,1496,1497],{},"\"Dio 支持拦截器、请求取消、文件上传、超时配置，这些在业务项目中都是必须的。特别是拦截器机制，让我可以统一处理 token 注入、登录过期跳转、Loading 弹窗，不用在每个 API 调用处重复写。\"",[26,1499],{},[29,1501,1503],{"id":1502},"六可以主动提的亮点","六、可以主动提的亮点",[1505,1506,1507,1513,1519,1525,1531,1537,1547],"ol",{},[42,1508,1509,1512],{},[17,1510,1511],{},"EventBus 跨模块通信"," — 登录\u002F登出、语言切换、区域切换等全局事件，用 EventBus 解耦模块间依赖",[42,1514,1515,1518],{},[17,1516,1517],{},"Deep Linking"," — AIRBS 支持 App Links，扫码\u002F分享链接可直接跳转到商品详情页",[42,1520,1521,1524],{},[17,1522,1523],{},"TabViewController 基类"," — 封装了底部 Tab 页面的生命周期（激活\u002F失活\u002F首次加载\u002F登录状态），子类只需关注业务",[42,1526,1527,1530],{},[17,1528,1529],{},"RxFilter 响应式过滤器"," — 支持变更追踪、默认值保存\u002F恢复，用于复杂筛选场景",[42,1532,1533,1536],{},[17,1534,1535],{},"多环境配置"," — develop\u002Ftest\u002Frelease 三套环境，Host 配置从服务端动态获取，避免硬编码",[42,1538,1539,1542,1543,1546],{},[17,1540,1541],{},"PDF 预览"," — YesShop 支持在 App 内查看 PDF 文档（",[110,1544,1545],{},"flutter_pdfview","）",[42,1548,1549,1552],{},[17,1550,1551],{},"Syncfusion 图表"," — YesShop Dashboard 使用 Syncfusion Charts 展示业务数据可视化",[26,1554],{},[29,1556,1558],{"id":1557},"七项目数据量化展示能力","七、项目数据（量化展示能力）",[119,1560,1561,1572],{},[122,1562,1563],{},[125,1564,1565,1568,1570],{},[128,1566,1567],{},"指标",[128,1569,19],{},[128,1571,23],{},[138,1573,1574,1585,1596,1607,1618,1629,1640,1651],{},[125,1575,1576,1579,1582],{},[143,1577,1578],{},"功能模块数",[143,1580,1581],{},"19",[143,1583,1584],{},"60+ 路由页面",[125,1586,1587,1590,1593],{},[143,1588,1589],{},"Service 数",[143,1591,1592],{},"18",[143,1594,1595],{},"20+",[125,1597,1598,1601,1604],{},[143,1599,1600],{},"数据模型数",[143,1602,1603],{},"38+",[143,1605,1606],{},"50+",[125,1608,1609,1612,1615],{},[143,1610,1611],{},"支持语言",[143,1613,1614],{},"4（英\u002F中\u002F西\u002F意）",[143,1616,1617],{},"3（英\u002F中\u002F西）",[125,1619,1620,1623,1626],{},[143,1621,1622],{},"设备适配",[143,1624,1625],{},"手机",[143,1627,1628],{},"手机 + iPad",[125,1630,1631,1634,1637],{},[143,1632,1633],{},"本地数据库",[143,1635,1636],{},"无",[143,1638,1639],{},"Drift (SQLite)",[125,1641,1642,1645,1648],{},[143,1643,1644],{},"硬件对接",[143,1646,1647],{},"QR 扫码",[143,1649,1650],{},"QR 扫码 + PDA 扫码枪 + 相机",[125,1652,1653,1656,1659],{},[143,1654,1655],{},"当前版本",[143,1657,1658],{},"v3.4.0",[143,1660,1661],{},"v7.0.0",[26,1663],{},[29,1665,1667],{"id":1666},"八面试万能回答模板","八、面试万能回答模板",[13,1669,1670,1671,1674,1675,1678],{},"当面试官问 ",[17,1672,1673],{},"\"讲一个你遇到的难点，是怎么解决的\""," 时，用 ",[17,1676,1677],{},"STAR 法则","：",[103,1680,1683],{"className":1681,"code":1682,"language":108},[106],"Situation（背景）: 在 XXX 项目中，我们需要实现 XXX 功能...\nTask（任务）:      难点在于 XXX，因为 XXX...\nAction（行动）:    我的解决方案是 XXX，具体做了以下几步...\nResult（结果）:    最终实现了 XXX，效果是 XXX...\n",[110,1684,1682],{"__ignoreMap":112},[13,1686,1687],{},[17,1688,1689],{},"示例：",[10,1691,1692],{},[13,1693,1694,1697,1698,1701,1702,1705,1706,1708,1709,1711,1712,1714,1715,1718],{},[17,1695,1696],{},"S",": 在 YesShop Business HD 项目中，我们需要同一套 Flutter 代码同时运行在 iPad 和手机上。\n",[17,1699,1700],{},"T",": 难点在于两种设备的屏幕尺寸差异巨大，iPad 适合多列布局，手机适合单列，而且字体、间距、图片都需要差异化。\n",[17,1703,1704],{},"A",": 我基于 ",[110,1707,551],{}," 封装了双端适配方案：用不同的设计稿尺寸初始化，写了 ",[110,1710,555],{}," 等适配函数，通过 ",[110,1713,559],{}," 在运行时切换布局策略，图片资源按设备分目录管理。\n",[17,1716,1717],{},"R",": 最终一套代码成功适配两种设备，没有维护两个分支的成本，iPad 上的多列布局充分利用了大屏空间，用户体验良好。",[1720,1721,1722],"style",{},"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":112,"searchDepth":272,"depth":272,"links":1724},[1725,1729,1732,1741,1744,1749,1750,1751],{"id":31,"depth":272,"text":32,"children":1726},[1727,1728],{"id":36,"depth":278,"text":37},{"id":62,"depth":278,"text":63},{"id":91,"depth":272,"text":92,"children":1730},[1731],{"id":95,"depth":278,"text":96},{"id":221,"depth":272,"text":222,"children":1733},[1734,1735,1736,1737,1738,1739,1740],{"id":225,"depth":278,"text":226},{"id":425,"depth":278,"text":426},{"id":565,"depth":278,"text":566},{"id":677,"depth":278,"text":678},{"id":998,"depth":278,"text":999},{"id":1205,"depth":278,"text":1206},{"id":1254,"depth":278,"text":1255},{"id":1310,"depth":272,"text":1311,"children":1742},[1743],{"id":1314,"depth":278,"text":1314},{"id":1462,"depth":272,"text":1463,"children":1745},[1746,1747,1748],{"id":1466,"depth":278,"text":1467},{"id":1482,"depth":278,"text":1483},{"id":1491,"depth":278,"text":1492},{"id":1502,"depth":272,"text":1503},{"id":1557,"depth":272,"text":1558},{"id":1666,"depth":272,"text":1667},"2026-05-03 10:20:00 CST","基于真实跨境电商和 iPad 批发管理系统项目的技术总结与面试准备。","md",{},"\u002Fnotes\u002F2026-05-03-flutter-project-interview",{"title":5,"description":1753},"基于 AIRBS Wholesale 和 YesShop Business HD 两个真实 Flutter 项目的技术重点、难点和面试准备总结。","Flutter 项目面试准备 — 重点与难点｜个人笔记","flutter-project-interview","notes\u002F2026-05-03-flutter-project-interview","-SDbHktSA2YiYx6L5bWmC_5OVcZK3S2qNHU8v6Z_0qY",1777744305315]