indexDB

1. 在项目中使用IndexDB缓存多类型数据(对话、题目、会话状态)时,你是如何设计对象存储空间(Object Store)的?为什么不采用单-store存储所有类型数据?请结合数据特点说明设计思路。

参考答案

设计思路是按数据类型拆分3个独立的Object Store ,分别为conversations(对话)、questions(题目)、sessionStates(会话状态),核心设计逻辑如下:

  1. 主键策略差异化

    • 对话数据(conversations):需唯一标识单条对话,且需按时间排序,故主键设为复合结构{ sessionId: string, timestamp: number }(会话ID+时间戳),避免同一会话内重复时间戳的冲突;
    • 题目数据(questions):题目ID全局唯一(如后端生成的UUID),故主键直接设为questionId: string,便于快速查询单题详情;
    • 会话状态(sessionStates):一个会话对应一个状态,主键设为sessionId: string,保证"会话-状态"的1:1映射。
  2. 索引需求不同

    • conversations需支持"按会话ID筛选所有对话",故基于sessionId单独建立索引;
    • questions需支持"按题目类型(如单选/多选)筛选",故基于questionType建立索引;
    • sessionStates仅需按sessionId查询,无需额外索引。

不采用单-store的原因:

  • 单-store存储会导致主键冲突(如对话的timestamp与题目的questionId格式不同,无法共用主键);
  • 索引冗余:为满足多类型数据的查询需求,单-store需建立大量无关索引,降低读写性能;
  • 数据隔离性差:增删改某类数据时(如删除过期对话),需额外过滤类型字段,易误操作其他数据,且事务无法保证"仅操作目标类型"的原子性。

2. 处理JSON序列化的深拷贝兼容问题时,你遇到了哪些具体场景(比如特殊数据类型、循环引用等)?分别采用了什么解决方案?如何验证序列化/反序列化后的完整性?

参考答案

一、核心问题场景与解决方案

  1. 特殊数据类型丢失/变形

    • 场景1:Date对象 :JSON序列化会将new Date()转为"2024-05-20T12:00:00.000Z"字符串,反序列化后无法恢复为Date类型;
      方案 :自定义JSON.stringifyreplacer函数,将Date对象标记为{ __type: 'Date', value: date.toISOString() };反序列化时通过reviver函数识别__type: 'Date',调用new Date(value)重建。
    • 场景2:RegExp对象 :序列化后会丢失flags(如/abc/g变为{});
      方案 :replacer阶段记录source(正则表达式体)和flags(如g/i),格式为{ __type: 'RegExp', source: 'abc', flags: 'g' };reviver阶段通过new RegExp(source, flags)重建。
    • 场景3:BigInt精度丢失 :超过2^53的BigInt(如后端返回的19位ID)会被转为Number导致精度丢失;
      方案 :replacer将BigInt转为{ __type: 'BigInt', value: bigInt.toString() },reviver阶段通过BigInt(value)恢复。
  2. 循环引用报错

    • 场景 :会话状态中可能包含"当前对话引用",导致JSON.stringify直接抛出TypeError: Converting circular structure to JSON
      方案 :用WeakMap记录已遍历的对象,replacer函数中检测到循环引用时,标记为{ __type: 'Circular', ref: 唯一标识 },反序列化时通过唯一标识重新建立引用;或使用成熟库(如lodash.clonedeep)的循环引用处理逻辑,但需二次封装适配项目数据类型。

二、完整性验证方案

  1. 单元测试覆盖 :针对每种特殊类型(Date/RegExp/BigInt)和循环引用场景,编写"序列化-反序列化"对比用例,断言:
    • 数据类型一致(如反序列化后instanceof Datetrue);
    • 核心属性一致(如RegExp的sourceflags、BigInt的数值);
    • 引用关系一致(如循环引用对象的内存地址指向正确)。
  2. 生产环境日志监控:在反序列化失败时,记录原始JSON字符串和错误栈,排查未覆盖的异常类型(如后端新增的特殊字段)。

3. IndexDB是异步操作,在缓存多类型数据时,如何保证"同时写入对话和会话状态"这类关联操作的一致性?如果中途失败(如浏览器崩溃),如何做数据恢复?

参考答案

一、关联操作一致性保证:基于IndexDB事务(Transaction)

IndexDB的事务是"原子性"的核心,具体实现如下:

  1. 事务创建 :调用db.transaction(['conversations', 'sessionStates'], 'readwrite'),同时指定两个需要操作的store,模式为readwrite(读写模式);
  2. 批量操作 :在事务内执行"写入对话"和"更新会话状态"的操作(如conversations.add(convData)sessionStates.put(stateData));
  3. 结果监听
    • 事务的oncomplete事件:所有操作成功时触发,确认数据一致性;
    • 事务的onerror事件:任意一步失败时触发,事务自动回滚(所有已执行的操作都会撤销,不会出现"对话写入成功、状态更新失败"的中间态)。

二、中途失败的数据恢复:版本控制+操作日志表

  1. 设计操作日志表 :新增syncLogs Object Store,记录每笔关联操作的元数据,格式为:

    javascript 复制代码
    {
      logId: string, // 唯一日志ID(UUID)
      operationType: 'createConvWithState', // 操作类型,标识关联操作
      status: 'pending'|'completed'|'failed', // 操作状态
      data: { convData, stateData }, // 操作的原始数据
      timestamp: number // 操作时间
    }
  2. 崩溃后恢复流程

    • 页面重新初始化时,通过IndexDB的versionchange事件检测数据库连接,优先查询syncLogsstatus: 'pending'的日志;
    • 对每笔pending日志,重新执行对应的关联操作(基于日志中的data),并更新日志状态为completed
    • 若重试失败(如数据格式已变更),将日志状态改为failed,并触发前端告警,由用户手动触发同步或后端介入。

4. 项目中"重构同步逻辑覆盖所有增删改场景",具体是如何设计同步策略的?比如本地修改与远程数据冲突时(如A在本地修改,B在远程删除同一条数据),采用什么冲突解决规则?

参考答案

一、全场景同步策略:基于"版本号+操作标记"的增量同步

  1. 数据元信息设计:为所有本地数据(对话/题目/会话状态)增加3个核心字段:

    • version: number // 数据版本号(初始为1,每次修改+1);
    • lastModifiedTime: number // 最后修改时间戳;
    • operation: 'none'|'create'|'update'|'delete' // 本地操作标记(默认none,增删改后更新为对应类型)。
  2. 同步流程(本地→远程+远程→本地)

    • 步骤1:本地增量上报
      筛选本地operation !== 'none'的数据,按类型批量上报给后端,携带versionlastModifiedTime
      后端验证版本号(如仅接受比远程当前版本高的数据),处理完成后返回"处理结果+远程最新版本号";
      本地收到结果后,将对应数据的operation改为none,并更新version为远程最新版本。
    • 步骤2:远程增量拉取
      本地携带"各类型数据的最大version"请求后端,后端返回"版本号高于本地的数据"(包含增删改记录);
      本地按operation类型处理:create→新增、update→覆盖(基于version)、delete→标记本地数据为删除。

二、冲突解决规则:分层优先级+业务场景适配

针对"本地修改vs远程删除"等冲突,采用"业务优先级+时间戳兜底"的规则:

  1. 冲突场景1:本地修改 vs 远程删除

    • 若数据为"会话状态"(强关联当前用户):优先保留本地修改,同步时告知后端"恢复删除并应用本地修改",后端更新数据并记录冲突日志;
    • 若数据为"公共题目"(多用户共享):优先遵循远程删除,本地标记数据为isDeleted: true,并提示用户"该题目已被管理员删除,本地修改未同步"。
  2. 冲突场景2:本地删除 vs 远程修改

    • 对比本地删除时间戳与远程修改时间戳:
      • 若本地删除时间更新(后于远程修改):保留本地删除,同步时请求后端删除远程数据;
      • 若远程修改时间更新:放弃本地删除,拉取远程最新数据并覆盖本地,提示用户"该数据已被更新,删除操作已取消"。
  3. 冲突场景3:本地修改 vs 远程修改

    • 基于version号:仅接受版本号更高的修改(避免旧数据覆盖新数据);
    • 若版本号相同(并发修改):按lastModifiedTime兜底,保留时间更新的修改,并合并非冲突字段(如对话内容修改 vs 对话标签修改,合并两者字段)。

5. 对于IndexDB中的"会话状态"这类高频读写的数据,你做了哪些性能优化?如何避免频繁读写导致的性能瓶颈?

参考答案

针对高频读写的"会话状态",核心优化思路是"减少IndexDB直接操作次数+优化读写链路",具体方案如下:

  1. 增加内存缓存层(Map暂存)

    • 初始化时,将所有会话状态从IndexDB加载到内存sessionStateCache: Map<string, StateData>(key为sessionId);
    • 高频读写操作(如切换会话、更新状态)优先操作内存Map,避免每次都调用IndexDB的get/put
    • 批量同步到IndexDB:设置"防抖同步"(如300ms防抖),或在"会话切换完成""页面隐藏"等时机,将内存中变更的状态批量写入IndexDB,减少IO次数。
  2. 优化IndexDB读写链路

    • 避免长事务:会话状态读写单独用短事务(仅操作sessionStates store),不与其他慢操作(如批量写入对话)共用事务,防止事务阻塞;
    • 禁用不必要的索引:sessionStates仅需按sessionId查询,无需额外索引,减少写入时的索引更新开销;
    • 使用put而非delete+add:更新状态时直接用put(存在则更新,不存在则新增),避免先删除再新增的两次IO。
  3. 控制数据规模

    • 过期数据清理:定期(如每周)删除"30天未活跃会话"的状态数据,减少IndexDB存储量;
    • 状态字段精简:会话状态仅保留核心字段(如currentStepisActivelastAccessTime),避免存储大体积数据(如完整对话列表),降低读写数据量。
  4. 监控与兜底

    • 监听IndexDB的onblocked事件:若其他页面占用数据库连接导致当前操作阻塞,降级为"直接操作内存+页面刷新后重试同步";
    • 性能监控:通过performance.mark记录"内存缓存命中耗时""IndexDB同步耗时",若某环节耗时超阈值(如同步超500ms),触发告警优化。

6. JSON序列化无法处理BigInt类型,但项目中如果有大数字ID(如超过2^53的ID),会导致精度丢失,你是如何解决这个问题的?

参考答案

针对BigInt精度丢失问题,项目采用"类型标记+自定义序列化/反序列化"方案,兼顾兼容性和易用性,具体实现如下:

一、核心解决方案:结构化标记转换

  1. 序列化阶段(JSON.stringify增强)

    自定义stringifyWithBigInt函数,通过replacer识别BigInt类型,转为包含类型标记的对象:

    javascript 复制代码
    function stringifyWithBigInt(data) {
      return JSON.stringify(data, (key, value) => {
        if (typeof value === 'bigint') {
          // 标记BigInt类型,value转为字符串避免精度丢失
          return { __type: 'BigInt', value: value.toString() };
        }
        return value;
      });
    }

    示例:12345678901234567890123n会被序列化为{"__type":"BigInt","value":"12345678901234567890123"}

  2. 反序列化阶段(JSON.parse增强)

    自定义parseWithBigInt函数,通过reviver识别标记对象,重建BigInt类型:

    javascript 复制代码
    function parseWithBigInt(jsonStr) {
      return JSON.parse(jsonStr, (key, value) => {
        if (value?.__type === 'BigInt' && typeof value.value === 'string') {
          return BigInt(value.value);
        }
        return value;
      });
    }

    示例:上述序列化结果反序列化后,会恢复为12345678901234567890123n

二、方案优化与兼容处理

  1. 与后端协同 :约定后端返回的BigInt类型字段(如userIdconversationId),在JSON中统一按"字符串+类型标记"格式返回,避免前端单独处理;
  2. 边界情况适配
    • 若BigInt嵌套在数组中(如{ ids: [123n, 456n] }),上述函数可递归处理,无需额外修改;
    • 若遇到未标记的Number类型大数字(如后端误传为Number),在反序列化后检测"数值是否超过2^53",若超过则提示后端修正数据格式;
  3. 性能优化 :对超大规模数据(如上万条含BigInt的列表),通过JSONStream等流式处理库,避免一次性序列化导致的内存占用过高。

7. 在覆盖"删除场景"的同步逻辑中,如何处理"本地删除后,远程又新增同ID数据"的情况?是否需要设计墓碑(Tombstone)机制?为什么?

参考答案

一、核心处理方案:必须设计墓碑(Tombstone)机制

针对"本地删除+远程新增同ID"的冲突,直接物理删除本地数据会导致"远程新增数据无法同步到本地"(因为本地无该ID数据,同步时会误判为"新数据",但实际ID已存在),因此必须通过墓碑机制实现"逻辑删除+状态追踪"。

二、墓碑机制的具体设计

  1. 数据层面:增加逻辑删除标记

    为所有可删除的数据(对话/题目/会话状态)增加isDeleted: boolean字段(默认false),本地删除时不执行store.delete(id),而是执行store.put({ id, isDeleted: true, ...otherFields }),保留数据的ID和元信息(如versionlastModifiedTime)。

  2. 同步层面:区分"逻辑删除"与"物理删除"

    • 本地同步删除操作时,上报数据的isDeleted: trueversion,后端标记该ID数据为"逻辑删除",但不物理删除;
    • 远程新增同ID数据时,后端会检测到"该ID已存在逻辑删除记录",返回"冲突提示"和"远程新增数据的完整信息";
    • 本地收到冲突后,按规则处理:
      • 若本地删除时间戳 < 远程新增时间戳:说明远程新增是"后操作",本地将isDeleted改为false,并覆盖为远程新增数据;
      • 若本地删除时间戳 > 远程新增时间戳:说明本地删除是"后操作",保留isDeleted: true,并请求后端删除远程新增数据。
  3. 墓碑清理策略

    • 定期清理:每周执行一次"墓碑清理任务",筛选"isDeleted: truelastModifiedTime距当前超30天"的数据,执行store.delete(id)物理删除,减少存储占用;
    • 同步确认后清理:若本地逻辑删除同步到后端,且后端返回"已确认删除",则立即物理删除本地墓碑(避免冗余)。

三、不设计墓碑机制的风险

  • 数据一致性丢失:本地物理删除后,远程新增同ID数据同步到本地时,会被当作"全新数据"插入,导致同一ID存在两条数据(本地旧删除记录已消失,无法冲突检测);
  • 同步链路断裂:后端无法区分"本地未同步过该ID"和"本地已删除该ID",可能重复推送已删除数据,导致前端数据冗余。

8. 对比LocalStorage,为什么选择IndexDB存储多类型数据?在实际使用中,IndexDB的哪些特性对你的场景至关重要?

参考答案

选择IndexDB而非LocalStorage的核心原因是LocalStorage的局限性无法满足"多类型数据+复杂操作"的需求,具体对比及IndexDB关键特性如下:

一、LocalStorage与IndexDB的核心差异对比

对比维度 LocalStorage IndexDB
存储容量 约5MB,容量固定且较小 无严格上限(依赖浏览器/设备,通常GB级)
数据类型 仅支持字符串(需手动JSON序列化) 支持复杂对象(Array/Object/Blob等)
操作方式 同步操作(阻塞UI线程,高频读写卡顿) 异步操作(非阻塞,适合大量数据)
查询能力 仅支持"键-值"精准查询(无索引) 支持索引、游标、范围查询(如按会话ID筛选)
事务支持 无事务,无法保证多操作原子性 支持事务,保证关联操作一致性

二、IndexDB对项目场景至关重要的特性

  1. 大容量存储:项目需缓存"对话记录(可能上万条)+题目库(上千题)+会话状态",总数据量易超5MB,LocalStorage容量不足会导致数据丢失,IndexDB的GB级容量可满足需求。

  2. 复杂查询能力

    • 场景需求:需"按会话ID查询所有对话""按题目类型筛选题目",LocalStorage需遍历所有数据手动过滤(性能差),IndexDB可通过索引快速定位(如conversations基于sessionId建索引,查询耗时从"秒级"降至"毫秒级")。
  3. 异步非阻塞:会话状态是高频读写数据(如每切换一次会话就更新状态),LocalStorage的同步操作会阻塞UI线程(导致页面卡顿),IndexDB的异步操作可避免此问题,保证用户交互流畅。

  4. 事务与数据一致性:项目需"同时写入对话和更新会话状态",LocalStorage无事务支持,易出现"对话写入成功、状态更新失败"的中间态,IndexDB的事务可确保两者要么同时成功,要么同时回滚,避免数据不一致。

  5. Blob类型支持:若后续扩展"缓存对话中的图片/文件",IndexDB可直接存储Blob类型,无需转为Base64字符串(LocalStorage需转码,增加数据体积和性能开销)。

9. 当数据量较大(如上万条对话记录)时,从IndexDB查询特定会话的历史数据,如何保证查询性能?你是如何设计索引的?

参考答案

针对"大数据量下查询特定会话的历史数据",核心优化思路是"精准索引设计+高效查询方式",具体方案如下:

一、索引设计:按"查询场景+数据特点"建立分层索引

  1. 核心索引:会话ID单字段索引(满足基础查询)

    • 场景需求:"查询某会话(如sessionId: 's123')的所有历史对话";

    • 索引创建:在conversations store中,基于sessionId字段建立单字段索引:

      javascript 复制代码
      // 初始化Object Store时创建索引
      const convStore = db.createObjectStore('conversations', { keyPath: ['sessionId', 'timestamp'] });
      convStore.createIndex('idx_sessionId', 'sessionId'); // 索引名:idx_sessionId,索引字段:sessionId
    • 查询方式:通过索引打开游标,直接筛选目标会话的对话:

      javascript 复制代码
      const transaction = db.transaction('conversations', 'readonly');
      const convStore = transaction.objectStore('conversations');
      const sessionIndex = convStore.index('idx_sessionId');
      // 仅查询sessionId为's123'的对话
      const cursorRequest = sessionIndex.openCursor(IDBKeyRange.only('s123'));
  2. 优化索引:会话ID+时间戳复合索引(满足排序+分页)

    • 场景需求:"查询某会话的历史对话,并按时间戳倒序(最新对话在前)+分页加载(每次加载20条)";

    • 复合索引创建:基于[sessionId, timestamp]建立复合索引(利用IndexDB的"复合索引按字段顺序排序"特性):

      javascript 复制代码
      convStore.createIndex('idx_sessionId_timestamp', ['sessionId', 'timestamp'], { descending: true });
      // descending: true:按时间戳倒序(最新对话在前)
    • 分页查询实现:通过游标范围查询,指定"起始时间戳"和"数量限制",避免全量加载:

      javascript 复制代码
      // 加载sessionId='s123'、时间戳<1716000000000的前20条对话(分页第二页)
      const range = IDBKeyRange.bound(
        ['s123', 1716000000000], // 下界:会话ID+起始时间戳
        ['s123', 0], // 上界:会话ID+最小时间戳
        false, // 不包含下界
        true  // 包含上界
      );
      const cursorRequest = sessionIndex.openCursor(range, 'prev'); // 'prev':倒序
      let count = 0;
      cursorRequest.onsuccess = (e) => {
        const cursor = e.target.result;
        if (cursor && count < 20) {
          pushToConversationList(cursor.value); // 加入结果列表
          count++;
          cursor.continue(); // 继续下一条
        }
      };

二、其他性能优化手段

  1. 避免全表扫描 :禁用"先获取所有对话再过滤"的方式(如convStore.getAll()),必须通过索引查询,减少IO次数(全表扫描需读取所有数据,索引查询仅读取目标会话数据)。

  2. 游标复用与批量处理 :使用cursor.continue()而非多次get(),减少数据库连接开销;若需处理大量数据(如导出对话),通过cursor.advance(100)批量读取(每次读100条),避免一次性加载导致内存溢出。

  3. 索引维护 :定期清理"过期对话"(如删除3个月前的记录),减少索引数据量;避免建立无关索引(如不为conversationContent(对话内容)建索引,因其不用于查询,仅增加写入时的索引更新开销)。

  4. 预加载与缓存:对"用户频繁访问的会话",将其历史对话缓存到内存Map中,后续查询优先读取内存(如用户连续查看某会话的对话,无需重复查询IndexDB),缓存失效时间设为1小时(平衡性能与数据新鲜度)。

10. 在测试同步逻辑时,你是如何模拟"网络中断""部分数据同步失败"等异常场景的?如何保证同步逻辑的健壮性?

参考答案

一、异常场景模拟方案:分层模拟(网络层+数据层+业务层)

  1. 网络中断模拟

    • 工具层面:使用Chrome DevTools的Network面板,切换到"Offline"模式,模拟完全断网;或使用Throttling功能(如"3G""高延迟"),模拟弱网环境下的同步超时。

    • 代码层面:封装fetch/axios请求拦截器,在测试环境中手动抛出NetworkError,或延迟返回响应(模拟超时):

      javascript 复制代码
      // 测试环境请求拦截器
      if (process.env.NODE_ENV === 'test') {
        axios.interceptors.request.use(config => {
          if (config.url.includes('/sync/data')) {
            // 模拟10%概率网络中断
            if (Math.random() < 0.1) {
              throw new Error('Network Error: Failed to connect');
            }
            // 模拟500ms延迟(弱网)
            return new Promise(resolve => setTimeout(() => resolve(config), 500));
          }
          return config;
        });
      }
  2. 部分数据同步失败模拟

    • 后端配合:在测试环境的同步接口中,增加"随机失败"逻辑(如对10%的同步数据返回{ code: 500, msg: '部分数据处理失败' }),并携带"失败数据ID列表"。
    • 前端构造:手动构造"格式错误的数据"(如缺失version字段的对话),调用同步接口,模拟后端因数据非法导致的部分失败。
  3. 并发冲突模拟

    • 双页面测试:打开两个浏览器标签页,同时对同一条对话进行修改,然后分别触发同步,模拟"本地A修改+本地B修改"的并发冲突;
    • 时间戳篡改:在测试代码中手动修改数据的lastModifiedTime(如将本地时间戳改为1小时前),模拟"本地旧数据同步到远程新数据"的冲突。

二、保证同步逻辑健壮性的核心手段

  1. 操作幂等设计

    • 为每笔同步操作生成唯一operationId(UUID),后端基于operationId去重(即使前端重试,也不会重复处理);
    • 同步接口支持"重复调用"(如本地重试时,后端若已处理过该operationId,直接返回成功结果,不重复执行逻辑)。
  2. 失败重试与退避策略

    • 基础重试:网络中断或部分失败时,触发"立即重试1次"(解决临时网络波动);
    • 退避重试:连续失败时,采用"指数退避"策略(如第1次重试间隔1s,第2次2s,第3次4s,最多重试5次),避免频繁请求压垮后端。
  3. 同步状态可视化与告警

    • 前端增加"同步状态指示器"(如右上角小图标:绿色=同步完成,黄色=同步中,红色=同步失败),让用户感知状态;
    • 同步失败(重试5次仍失败)时,触发前端告警(如弹窗提示"同步失败,请检查网络或联系客服"),并记录详细日志(失败operationId、数据内容、错误栈),便于排查问题。
  4. 数据备份与回滚

    • 同步前备份本地数据(如将待同步的数据复制到backup store),若同步失败且无法重试,可从备份恢复本地数据,避免同步过程中数据损坏;
    • 远程同步到本地时,先将数据写入"临时store",验证数据完整性(如字段校验、版本号检查)后,再覆盖正式store,避免脏数据写入。
  5. 单元测试与E2E测试覆盖

    • 单元测试:针对"冲突解决逻辑""序列化/反序列化"编写测试用例(如模拟10种冲突场景,断言处理结果符合预期);
    • E2E测试:使用Cypress/Puppeteer模拟"大数据量同步""网络中断恢复"等场景,验证端到端流程的正确性(如上万条对话同步后,查询性能仍达标)。
相关推荐
xlp666hub7 分钟前
Linux 设备模型学习笔记(1)
面试·嵌入式
南囝coding1 小时前
CSS终于能做瀑布流了!三行代码搞定,告别JavaScript布局
前端·后端·面试
踏浪无痕2 小时前
Go 的协程是线程吗?别被"轻量级线程"骗了
后端·面试·go
一只叫煤球的猫3 小时前
为什么Java里面,Service 层不直接返回 Result 对象?
java·spring boot·面试
求梦8203 小时前
字节前端面试复盘
面试·职场和发展
C雨后彩虹4 小时前
书籍叠放问题
java·数据结构·算法·华为·面试
码农水水4 小时前
中国电网Java面试被问:流批一体架构的实现和状态管理
java·c语言·开发语言·面试·职场和发展·架构·kafka
程序员清风5 小时前
猿辅导二面:线上出现的OOM是如何排查的?
java·后端·面试
CCPC不拿奖不改名5 小时前
数据处理与分析:pandas基础+面试习题
开发语言·数据结构·python·面试·职场和发展·pandas
小徐不徐说5 小时前
避坑指南:Qt 中 Lambda 表达式崩溃原因与高效使用实践
数据库·c++·qt·面试