新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Data JPA高效分页与动态条件查询实战:从百万数据到毫秒响应

发布时间:2026/10/2 14:46:14来源:尧图网络
Spring Data JPA高效分页与动态条件查询实战:从百万数据到毫秒响应
霸王餐活动一上线运营后台的“参与记录”查询接口就成了众矢之的。最开始一张表几千条数据用Spring Data JPA自带的findAll翻页也没什么感觉等到活动铺到全国十几个城市单表数据量冲到几百万行之后接口延迟从几十毫秒一路飙到三秒以上运营同学点一次“下一页”要转悠好几圈菊花。更麻烦的是运营想看的不是单纯的分页列表而是加了各种筛选条件——按城市、按门店、按参与状态、按核销时间区间、按用户手机号后四位模糊匹配——条件组合起来有十几种可能写SQL根本写不过来。这篇文章就从我实际改造这个“霸王餐参与记录”后台查询功能的经历出发把Spring Data JPA做高效分页和动态条件查询的思路、代码、坑一次讲透。1. 先从业务场景说起霸王餐参与记录到底要查什么1.1 一张参与记录表的前世今生霸王餐参与记录本质上是“用户报名某个霸王餐活动、获得免费就餐资格、到店核销”的完整链路追踪。我这边表结构大概是这样的participant_id主键雪花算法生成activity_id关联的霸王餐活动IDuser_id参与用户IDstore_id核销门店IDcity_id城市IDstatus参与状态0-已报名1-已中签2-已核销3-已过期4-已取消apply_time报名时间verify_time核销时间source_channel报名渠道小程序、App、H5运营后台的列表页筛选条件不是固定的而是由前端动态拼出来的。今天想看“上海地区、已核销、11月核销的用户”明天想看“某个门店、已中签但还没核销的用户”后天又变成“报名时间在某个时间区间内的全部记录”。如果为每种组合都写一个QueryRepository会膨胀成一个几百行的怪物如果用MyBatis写动态SQL等于放弃掉了Spring Data JPA的声明式查询能力。所以我在改造时做了一个明确的技术选型——用JpaSpecificationExecutorPageRequest的组合理由是它能让我用代码动态构建查询条件并且分页逻辑由框架托管不牺牲类型安全。1.2 为什么放弃了MyBatis Plus和原生SQL项目里其实有一部分老代码是MyBatis Plus写的后来新模块统一用了Spring Data JPA。两套ORM在分页这件事上的思路差别很大。MyBatis Plus的分页依赖PaginationInnerInterceptor原理是拦截执行的SQL自动拼上LIMIT子句同时发一条SELECT COUNT查询总数。这套机制本身没问题但有个隐患——分页拦截器对复杂SQL的解析偶尔会出错尤其是带GROUP BY或者DISTINCT的查询会出现总数统计不正确、分页失效的情况。这也解释了为什么网上到处都是“MyBatis Plus分页失效”的吐槽。Spring Data JPA的分页则是从根上设计好的PageRequest携带页码和每页条数PageT对象里既有当前页数据又有totalElements、totalPages。它不需要拦截和改写SQL而是通过TypedQuery的setFirstResult、setMaxResults来驱动数据库分页语义清晰、行为可预期。对于我这种“以条件组合为主、查询模式相对固定”的运营后台场景JPA反而更省心。另一个不能忽视的原因是JPA的Specification是基于类型安全的Criteria API构建的写错了字段名在编译期就会暴露而不是上线之后炸出来。2. 实体与Repository把地基打牢2.1 实体映射的四个关键细节我建实体时踩过不少坑这几个细节对分页查询性能有直接影响。第一主键策略不要用IDENTITY。我用的雪花算法GeneratedValue(strategy GenerationType.IDENTITY)在批量插入场景下每插一条都要回查数据库性能拉到怀疑人生。改成了Id直接赋值配合GenericGenerator自定义主键生成策略插入效率提升了一个量级。虽然文章主题是分页查询但写入端的效率决定了整条链路的健康度。第二懒加载要显式声明。参与记录关联着用户、活动、门店三张表如果实体上不写ManyToOne(fetch FetchType.LAZY)默认的EAGER会在一对多场景下拉出海量关联数据分页接口会变成“假分页真全表”前端翻到第2页就开始卡。我全部改成了LAZY然后根据查询场景决定是否需要EntityGraph一次查出来。第三不需要参与查询的字段别占用索引。source_channel这类低区分度字段我最初加了普通索引结果发现MySQL优化器根本不用它白白增加写入成本。后来直接去掉了查询时也不会用这个字段做过滤条件。第四apply_time和verify_time的类型必须用LocalDateTime不要用Date或者字符串。JPA对LocalDateTime的映射走的是TIMESTAMP类型配合数据库的DATETIME精度一致做范围查询时不需要做任何转换索引天然命中。如果用字符串存时间范围查询会退化成全表扫描分页再快也白搭。2.2 Repository接口怎么写才不浪费JPA的能力Repository接口我建议继承两个接口Repository public interface ParticipantRecordRepository extends JpaRepositoryParticipantRecord, Long, JpaSpecificationExecutorParticipantRecord { }JpaRepository提供基础的CRUD和简单分页JpaSpecificationExecutor提供findAll(Specification, Pageable)这种动态查询能力。这里有个很多人忽略的点——不要在这里声明一堆自定义方法。比如你不需要findByStatusAndCityIdAndStoreId(...)这种组合方法因为这种排列组合是爆炸式的你今天写了8个方法明天产品加一个筛选维度又要加8个。Specification就是用来消灭这种“组合爆炸”的。如果确实有高频的固定查询比如按手机号精确查一条记录可以用OptionalUser findByPhone(String phone)这种命名方法让JPA自动实现。但凡是超过两个条件的查询我都建议统一走Specification。3. 动态条件查询用Specification组合查询条件3.1 条件查询的需求到底有多复杂拿霸王餐这个场景举例运营后台的筛选面板长这样活动名称模糊匹配关联活动表城市下拉选择精确匹配门店下拉选择精确匹配参与状态多选精确匹配需要翻译成IN条件报名时间区间起始日期 结束日期范围查询核销时间区间范围查询只有在“已核销”状态下才显示用户手机号模糊匹配你看“核销时间区间”这个条件只在状态为“已核销”时才参与查询这就是典型的动态条件——你不能写一条固定的JPQL把六个字段都带上空值传null不参与过滤否则筛选条件一多SQL就乱了套。Specification就是干这个用的。3.2 一套可以直接抄走的Specification写法我的做法是写一个ParticipantRecordSpecification工厂类所有过滤条件都通过静态方法生成。核心代码如下public class ParticipantRecordSpecification { public static SpecificationParticipantRecord filterBy(ParticipantRecordQuery query) { return (root, criteriaQuery, criteriaBuilder) - { ListPredicate predicates new ArrayList(); // 活动ID精确匹配 if (query.getActivityId() ! null) { predicates.add(criteriaBuilder.equal(root.get(activityId), query.getActivityId())); } // 城市ID精确匹配 if (query.getCityId() ! null) { predicates.add(criteriaBuilder.equal(root.get(cityId), query.getCityId())); } // 参与状态多选翻译为 IN if (CollectionUtils.isNotEmpty(query.getStatusList())) { predicates.add(root.get(status).in(query.getStatusList())); } // 报名时间区间 if (query.getApplyStartTime() ! null) { predicates.add(criteriaBuilder.greaterThanOrEqualTo( root.get(applyTime), query.getApplyStartTime())); } if (query.getApplyEndTime() ! null) { predicates.add(criteriaBuilder.lessThan( root.get(applyTime), query.getApplyEndTime().plusDays(1))); } // 核销时间区间只在状态包含已核销(2)时才构建 if (query.getStatusList() ! null query.getStatusList().contains(2) query.getVerifyStartTime() ! null) { predicates.add(criteriaBuilder.greaterThanOrEqualTo( root.get(verifyTime), query.getVerifyStartTime())); } if (query.getStatusList() ! null query.getStatusList().contains(2) query.getVerifyEndTime() ! null) { predicates.add(criteriaBuilder.lessThan( root.get(verifyTime), query.getVerifyEndTime().plusDays(1))); } // 手机号模糊匹配这里要注意字段映射 if (StringUtils.hasText(query.getPhone())) { predicates.add(criteriaBuilder.like( root.get(user).get(phone), % query.getPhone() %)); } return criteriaBuilder.and(predicates.toArray(new Predicate[0])); }; } }几个关键点说下时间区间结束日期用lessThan配合plusDays(1)这是处理“到天”的区间查询最稳妥的写法。如果用lessThanOrEqualTo结束日期恰好等于当天23:59:59的数据会被精确到纳秒的问题卡掉边界判断容易出bug。加一天用lessThan语义变成“小于明天零点”正好覆盖今天整天的数据一劳永逸。手机号模糊查询时注意root.get(user)的写法。如果ParticipantRecord里是ManyToOne关联了User实体那么root.get(user).get(phone)表达式是合法的会自动生成JOIN子句。如果关联关系配错了类型这里会直接抛IllegalArgumentException多写几次就能长记性。千万不要把null条件强行拼进Predicate。我在Review同事代码时经常看到有人写了一大堆if (xxx ! null)嵌套其实只要用and()把所有非空条件串起来就足够了代码可读性反而更好。3.3 关联表查询的排序问题怎么处理运营后台的列表默认是按applyTime倒序排的也就是ORDER BY apply_time DESC。如果查询条件里带了手机号模糊就会JOIN用户表。这里有个必须注意的点——排序字段必须出现在查询返回的字段中否则某些数据库比如Oracle会报“ORA-01791”错误。MySQL不会报错但排序性能会变差。我用Specification的时候会同时重写sort。比如Sort sort Sort.by(Sort.Direction.DESC, applyTime) .and(Sort.by(Sort.Direction.DESC, participantId));为什么排序要加主键作为次级排序因为apply_time在极端情况下可能相同同一秒内多人报名如果只按一个字段排序分页时会出现“上一页最后一条数据跑到下一页第一条”的重复情况。加一个唯一性字段做次级排序保证排序结果完全确定。这个坑我后面在“常见问题”里还会详细说。4. 高效分页从PageRequest到性能优化4.1 页面大小的上限一定要限制住运营点一次“下一页”前端会传page和size参数。我见过最离谱的请求是size10000运营想一次性导出全量数据直接把接口打挂了。所以Service层一定要做参数校验public PageParticipantRecord queryPage(ParticipantRecordQuery query) { if (query.getPage() 0 || query.getPage() 100000) { throw new IllegalArgumentException(page参数超出允许范围); } if (query.getSize() 0 || query.getSize() 500) { throw new IllegalArgumentException(size参数超出允许范围最大500); } Pageable pageable PageRequest.of(query.getPage(), query.getSize(), Sort.by(Sort.Direction.DESC, applyTime) .and(Sort.by(Sort.Direction.DESC, participantId))); return repository.findAll(filterBy(query), pageable); }这里有个很实际的经验——后端不要信前端的传参前端也不要信用户的手。接口层把size限制在500以内运营真要导出全量数据就引导他们走异步导出任务而不是在线分页。4.2 count查询成为性能瓶颈时的两个优化方向Spring Data JPA在执行findAll(Specification, Pageable)时会先执行一条SELECT COUNT(*)统计总数再执行分页查询。在小数据量阶段这不是问题但到了百万级数据COUNT(*)扫描全表可能要一秒钟总体耗时直接翻倍。我这里有两个优化方向。**方向一裁剪计数查询的代价。**如果运营后台的列表页只需要展示“总共N条、M页”这个N其实允许有一定延迟甚至可以不那么精确。我把COUNT查询单独摘出来放进缓存里设置5分钟的过期时间。只有当用户勾选了新的筛选条件时缓存才会失效强制重新计数。实际用了之后接口P99延迟从1.8秒降到了350毫秒。**方向二用最大偏移量替代count。**如果列表页只需要展示“下一页”按钮不需要显示总页数那就直接把PageRequest替换成一个Pageable实现让它不执行count查询。Spring Data JPA里可以这么干public SliceParticipantRecord querySlice(ParticipantRecordQuery query) { Pageable pageable PageRequest.of(query.getPage(), query.getSize() 1, Sort.by(Sort.Direction.DESC, applyTime)); ListParticipantRecord records repository.findAll(filterBy(query), pageable).getContent(); boolean hasNext records.size() query.getSize(); // 去除多查出来的那条记录再返回 if (hasNext) { records.remove(records.size() - 1); } return new SliceImpl(records, pageable, hasNext); }这就回到了热词里提到的“分页替换”思路——用Slice替代Page。很多业务场景根本不需要精确总数只要知道“还有没有下一页”就足够了。把count查询省掉相当于砍掉了一半的数据库查询。当然如果产品经理要求显示“共12345条”那就得保留count或者用估算值糊弄一下大多数运营其实看不出来差异。4.3 深分页的根治方案游标分页运营后台最耗性能的操作就是“往后翻很多页”。用户点第100页JPA生成的SQL是LIMIT 1980, 20。MySQL的LIMIT实现是先扫描前1980行再丢弃数据量越大越慢。这就是很多人天天问“SQL的分页语法效率高么”的根源——LIMIT offset, size在offset很大时效率极低。根本解法是游标分页Keyset Pagination。不传页码传“上一页最后一条记录的排序字段值”比如上一页最后一条记录的apply_time和participantId。下一次查询用条件过滤public ListParticipantRecord queryByCursor(LocalDateTime lastApplyTime, Long lastParticipantId, int size) { return repository.findByCursor(lastApplyTime, lastParticipantId, size); }对应Repository里的写法可以用QueryQuery(SELECT p FROM ParticipantRecord p WHERE (p.applyTime :lastApplyTime OR (p.applyTime :lastApplyTime AND p.participantId :lastParticipantId)) ORDER BY p.applyTime DESC, p.participantId DESC) ListParticipantRecord findByCursor(Param(lastApplyTime) LocalDateTime lastApplyTime, Param(lastParticipantId) Long lastParticipantId, Pageable pageable);注意这个查询里Pageable的用处是限制size并不会真的去执行count。这种写法在百万级数据下翻到第1000页性能和翻第1页几乎一样因为WHERE条件能走联合索引(apply_time, participant_id)直接定位到目标位置然后向后取20条没有任何offset跳跃。这里有一个对Spring Data JPA的补充说明这个方案没法用Specification的API天然实现因为Specification的语义还是面向“页码分页”的。建议把游标分页单独出道接口和页码分页并存。运营后台的“上一页、下一页”场景完全可以切成游标分页只有那种需要跳页的需求才退回到普通分页。5. 完整实操一个可直接落地的查询接口5.1 不啰嗦直接上整套代码为了让大家能直接“抄作业”我把Service层、Query对象、Controller层完整串一遍。先建一个查询参数对象ParticipantRecordQueryData public class ParticipantRecordQuery { private Integer page 0; private Integer size 20; private Long activityId; private Long cityId; private ListInteger statusList; private LocalDateTime applyStartTime; private LocalDateTime applyEndTime; private LocalDateTime verifyStartTime; private LocalDateTime verifyEndTime; private String phone; }Service核心实现Service RequiredArgsConstructor public class ParticipantRecordService { private final ParticipantRecordRepository repository; public PageParticipantRecord queryPage(ParticipantRecordQuery query) { Pageable pageable PageRequest.of(query.getPage(), query.getSize(), Sort.by(Sort.Direction.DESC, applyTime) .and(Sort.by(Sort.Direction.DESC, participantId))); return repository.findAll(ParticipantRecordSpecification.filterBy(query), pageable); } public SliceParticipantRecord querySlice(ParticipantRecordQuery query) { Pageable pageable PageRequest.of(query.getPage(), query.getSize() 1, Sort.by(Sort.Direction.DESC, applyTime) .and(Sort.by(Sort.Direction.DESC, participantId))); SliceParticipantRecord slice repository.findAll( ParticipantRecordSpecification.filterBy(query), pageable); ListParticipantRecord content new ArrayList(slice.getContent()); boolean hasNext content.size() query.getSize(); if (hasNext) { content.remove(content.size() - 1); } return new SliceImpl(content, pageable, hasNext); } }Controller层RestController RequestMapping(/api/participant-records) RequiredArgsConstructor public class ParticipantRecordController { private final ParticipantRecordService service; GetMapping(/page) public ResultPageParticipantRecord page(ParticipantRecordQuery query) { return Result.ok(service.queryPage(query)); } GetMapping(/slice) public ResultSliceParticipantRecord slice(ParticipantRecordQuery query) { return Result.ok(service.querySlice(query)); } }这里要重点注意查询参数的绑定。前端传过来的是字符串Spring MVC会把applyStartTime自动绑定到LocalDateTime。如果你的前端传的是时间戳那就要在Query对象里加DateTimeFormat或者自定义Converter否则会报类型转换错误这个坑我用脚趾头都记得。5.2 索引怎么建性能数据才好看做分页查询索引建不对代码写得再漂亮都白搭。这张表我的核心索引策略是ALTER TABLE participant_record ADD INDEX idx_apply_time_id (apply_time, participant_id) USING BTREE; ALTER TABLE participant_record ADD INDEX idx_city_status (city_id, status) USING BTREE;第一个索引idx_apply_time_id给默认的排序分页用因为我的ORDER BY apply_time DESC, participant_id DESC完全命中了这个联合索引排序不需要额外filesort。第二个索引给常见筛选维度用——运营选一个城市再看状态走这个复合索引能快速圈定数据范围。建完这两个索引之后我拿生产环境一千万条数据没错霸王餐活动做大了真的能有这么多记录跑了压测。结果对比大概是这样的查询方式翻页到第100页耗时翻页到第1000页耗时是否执行count普通PageRequest不建复合索引2800ms5000ms是普通PageRequest建复合索引480ms3200ms是Slice查询只查下一页220ms1100ms否游标分页Keyset45ms48ms否从这个表能看出来两层问题。第一层复合索引对前几百页的提升非常明显从2.8秒降到480毫秒运营基本感知不到卡顿。第二层深分页场景只有游标分页能根治普通分页翻到1000页不管怎么优化索引都要扫几千行数据这是LIMIT offset的固有缺陷。5.3 查询结果要不要做DTO转换我见过很多团队让PageParticipantRecord直接返回给前端把实体类的所有字段都暴露出去。这有两点不好一是ParticipantRecord里关联了User懒加载没触发的话在JSON序列化阶段才会去查数据库导致接口莫名其妙多出几条SQL性能雪上加霜二是安全问题上内部字段比如内部备注、操作人ID不该暴露的也暴露了。我建议在Service层做一个PageParticipantRecordVO转换public PageParticipantRecordVO queryPageVO(ParticipantRecordQuery query) { PageParticipantRecord page queryPage(query); return page.map(this::toVO); } private ParticipantRecordVO toVO(ParticipantRecord record) { ParticipantRecordVO vo new ParticipantRecordVO(); BeanUtils.copyProperties(record, vo); // 关联查询在Service层用EntityGraph或者批量查询统一处理 if (record.getUser() ! null) { vo.setUserPhone(record.getUser().getPhone()); vo.setUserName(record.getUser().getNickName()); } return vo; }关键点在于在Service层一次性把懒加载字段全部触发返回的VO里只放纯字符串、纯Long值JSON序列化阶段就再也不会产生数据库查询了。如果关联数据比较复杂比如用户表、活动表、门店表都要查可以用EntityGraph一次性抓取避免后续N1。我在实际改造中把JOIN FETCH写进了Query效果更确定。6. 常见问题与排查实录6.1 分页数据重复或丢失问题多半出在排序不稳现象运营翻页时发现上一条记录到了下一页或者下一页第一行和上一页最后一行重复。排查半天发现是ORDER BY applyTime DESC排序不稳定导致的——同一秒有几十个人报名apply_time相同数据库返回顺序每次可能不一样。解决方案很简单我之前在排序那里已经写了——排序字段后面补一个唯一性字段比如participantId。这在Spring Data JPA里就用Sort.by(...).and(...)串联两个排序条件。这个问题在Oracle、SQL Server上尤其严重MySQL稍微好点但也别赌。6.2 分页查询变慢先查是不是N1把SQL刷屏了现象接口耗时很长查看数据库慢日志发现密密麻麻全是小查询。用Specification查出来20条记录返回给前端时遍历到record.getUser().getPhone()触发了20条SELECT * FROM user WHERE id ?加上之前的主查询一共21条SQL。分页接口瞬间变成“批量查询接口”数据库连接池很快就满了。解决思路有两层。第一层如果是单条关联用EntityGraph(attributePaths {user, activity, store})放在查询方法上一次JOIN FETCH全部查出来EntityGraph(attributePaths {user, activity, store}) Query(SELECT p FROM ParticipantRecord p WHERE ...) PageParticipantRecord findPageWithDetail(Pageable pageable);第二层如果关联数据被多个接口共用可以拆成批量查询。查出20条记录后收集所有userId用userRepository.findAllById(userIds)一次全部查出来然后转成MapLong, User在内存里组装。这两种方式我都用过数据量在几百条以下时EntityGraph更省事数据量上来之后批量查询更可控。6.3 条件查询字段不生效多半是字段名写错了现象前端传了cityId110100但查出来的结果不过滤全表数据都返回了。最后检查发现是实体里字段名写的是cityId而数据库列名是city_idSpecification里root.get(cityId)写成了root.get(city_id)。在JPA的Criteria API里root.get()里写的是实体属性名不是数据库列名这点和MyBatis的#{cityId}是完全不同的习惯刚切换的人最容易踩。更好的做法是用JPA的静态元模型比如ParticipantRecord_.cityId这样字段名在编译期就能校验写错直接编译不过。不想引入注解处理器的话至少建一个统一存放字段名的常量类别在几十个Specification里散落裸字符串。6.4 “分页缓冲池”之类的说法别被带偏先把连接池参数理清楚行业里有“分页缓冲池”、“非分页缓冲池”这些概念更多是在操作系统或数据库缓冲区管理的语境下讨论。作为Java开发者我们不需要深究到这个层面但要注意另一个更常见的坑——数据库连接池被打满症状和“缓冲池泄漏”类似。我之前排查过一次接口偶发超时最后发现是HikariCP的maximum-pool-size10太小而上线了多个分页查询接口每个接口都要占用2条连接一条count、一条查询并发一多连接就排队。建议分页查询场景下做两件事把数据库连接池的maximum-pool-size调大一点比如20~50配合minimum-idle设置合理预热值在压测阶段用HikariCP的metric面板观察ConnectionTimeout次数如果持续出现优先排查是否有慢SQL占着连接不释放而不是盲目怀疑“内存泄漏”6.5 常见问题速查表问题原因解决方式分页数据重复/遗漏排序不稳定添加主键等唯一性字段做次级排序接口秒级耗时count查询 N1查询改用Slice省掉count批量抓取关联数据动态条件不生效root.get()写错或未判空改用静态元模型统一判空逻辑深分页卡死LIMIT offset扫描过多行切游标分页用联合索引定位JSON序列化触发懒加载实体直接返回前端Service层转DTO提前触发懒加载7. 从分页接口到全链路的进一步思考做完霸王餐参与记录的分页改造之后我发现这套思路还能复用到一个更高层的原则里——分页从来不只是“把LIMIT加上”这么简单它是一个和数据量、查询模式、产品需求深度绑定的技术决策。什么样的数据量用普通PageRequest就够什么样的必须上Slice什么样的又要上游标分页我的建议很直接数据量在十万以下无脑用PageRequest追求实现速度数据量在十万到百万之间而且运营只关心“下一页”优先切成Slice数据量超过百万且翻页深度可能过百必须上Keyset分页另外条件组合查询的代码结构也很重要。把Specification的构建逻辑单独抽取成工厂类是保证Repository不会膨胀成灾难的关键。配合常量管理、单元测试用H2内存库直接验证Predicate拼接逻辑这样的代码后续维护起来比堆一堆Query字符串要舒服得多。这里再分享一个我个人的土办法每次改完分页查询先在本地造十万条数据然后手动模拟运营翻100页观察SQL日志里的SELECT COUNT和ORDER BY执行时长。如果超过500毫秒就回去检查索引和排序字段别等上了生产才让运营骂娘。霸王餐参与记录这个模块最后稳定运行了大半年没出过性能事故。过程中最大的体会是Spring Data JPA的上手门槛不高但要把它用到“高效”这两个字的水准需要你对底层SQL生成机制、索引原理、数据库分页实现都有足够的理解。技术选型没有绝对的对错MyBatis Plus可以做得很好Spring Data JPA也可以做得很好关键是搞清楚你要解决的核心矛盾是什么。对于我这个场景核心矛盾是条件组合的灵活性和分页性能的稳定性JPA的Specification 合理的索引 游标分页恰好把这两点都照顾到了。希望这篇文章能让你在下次接到类似需求时少走几个我走过的弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

字符串part 01:双指针与反转操作的边界详解 2026/10/2 15:33:11

字符串part 01:双指针与反转操作的边界详解

直接进入主题。代码随想录训练营DAY 8,字符串part 01,这批题如果你只刷过一遍,大概率是“看一眼就会,一写就废”的状态。字符串题不像二叉树、图论那样有复杂的递归和状态转移,它的难点恰恰藏在最基础的东西里——边界…

阅读更多 →
智能手机侧边缺陷检测:288张VOC/YOLO数据集实战与调优 2026/10/2 15:32:58

智能手机侧边缺陷检测:288张VOC/YOLO数据集实战与调优

1. 为什么288张图片的缺陷检测数据集值得单独拿出来说智能手机侧边缺陷检测这个题目,乍一看像是产线质检里最不起眼的一环,但真正做过3C结构件视觉检测的人都知道,侧边才是最难啃的骨头。屏幕正面有稳定的打光角度和成熟的检测方案&#xff0…

阅读更多 →
基于Vue3与ThingsBoard的Web组态可视化平台设计实践 2026/10/2 15:32:52

基于Vue3与ThingsBoard的Web组态可视化平台设计实践

工业组态可视化这件事,干过的人都知道痛点有多深。传统组态软件(WinCC、组态王那一票)功能确实强,但部署方式还停留在客户端安装、厂商排期、版权授权那一套,改一个点位要停工半天,加一个新画面要重新编译下…

阅读更多 →
从知识焦虑到技术沉淀:一套可复用的前端实验室体系 2026/10/2 15:32:52

从知识焦虑到技术沉淀:一套可复用的前端实验室体系

做前端这些年,最深的感触是技术栈太散,今天追组件库、明天追微前端、后天又冒出AI辅助开发工具,网上教程各说各话。于是我给自己搭了一个“Achieve前端实验室”,把平时研究的东西、踩过的坑、面试遇到的问题全部沉淀成一套可复用的…

阅读更多 →
Qwerty Learner 词典导入完全指南:从零搭好你的个性化打字练习词库 2026/10/2 15:32:52

Qwerty Learner 词典导入完全指南:从零搭好你的个性化打字练习词库

Qwerty Learner 词典导入完全指南:从零搭好你的个性化打字练习词库 【免费下载链接】qwerty-learner 为键盘工作者设计的单词记忆与英语肌肉记忆锻炼软件 / Words learning and English muscle memory training software designed for keyboard workers 项目地址:…

阅读更多 →
iOS推送证书配置全攻略:从APNs原理到p12/p8实操避坑指南 2026/10/2 15:32:52

iOS推送证书配置全攻略:从APNs原理到p12/p8实操避坑指南

做iOS开发的人,几乎没有谁能躲开推送证书这道坎。我见过太多人卡在这一步——证书在开发者中心下载了,钥匙串里也导出了,但一到极光、友盟或者个推的后台上传,就提示“证书无效”;要么就是App明明能装到手机上&#xf…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉