Java微信大转盘服务端实现:防刷、记账与动态奖品池
简介这是一份面向Java Web开发初学者与微信小程序互动项目实践者的完整大转盘抽奖系统源码聚焦微信H5场景下的前后端协同实现解决移动端轻量级营销活动快速落地问题。资源共24个文件涵盖3个核心Java后端类含业务逻辑与微信接口调用、3个JS交互脚本控制Canvas转盘旋转与中奖逻辑、3个PNG/JPG奖品图标、1个JSP入口页面、1个CSS样式表及多个IDE配置文件.classpath、.project等整体压缩包仅289KB结构精简便于快速导入与调试。已有229人学习下载适合通过真实项目理解RESTful API设计、HTML5 Canvas绘图、微信OAuth2.0授权集成及前后端分离开发流程。读者可直接运行并二次定制奖品池、中奖概率与UI动效掌握从微信环境适配到抽奖公平性保障的关键实践要点。1. Java微信大转盘不是H5页面是后端驱动的抽奖状态机适合需要防刷、记账、多轮策略的运营场景你见过那种点一下就“恭喜中奖”再点还是“谢谢参与”但后台根本没记录用户行为、没校验IP/设备指纹、没做次数限制的大转盘吗很多所谓“微信大转盘”只是前端JS写个随机数连session都懒得存——这种方案在真实运营里撑不过3小时。而「Java微信大转盘」是一套以Spring Boot为底座、MySQL持久化核心状态、Redis缓存实时中奖池、并内置微信JS-SDK签名签发能力的完整服务端实现。它不渲染页面只提供/api/turn/start、/api/turn/record、/api/award/list等REST接口前端H5调用时必须携带openid和timestampnoncestrsignature三元组所有抽奖动作落库可查、所有奖品库存原子扣减、所有中奖记录带时间戳与来源渠道标记。适合某高校招生办做新生入学礼抽奖、某连锁门店做会员日裂变活动、某SaaS平台集成到客户私域运营后台——一句话你要的是能进审计报告的抽奖系统不是浏览器F12就能改中奖结果的玩具。2. 核心模块拆解为什么用Java而不是Node.js或PHP来实现微信大转盘2.1 奖品池状态机设计从“静态配置”到“动态权重”的演进逻辑微信大转盘最常被低估的是奖品池不是一张Excel表而是一个带生命周期的状态机。原始版本把奖品写死在application.yml里prizes: - id: 1 name: 5元无门槛券 weight: 80 stock: 1000这会导致两个硬伤一是运营要改库存得重启服务二是权重无法按时段动态调整比如晚8点把“100元话费”权重提3倍。我们最终采用「奖品模板实例化规则」双层结构prize_template表存基础属性名称、类型、文案、默认权重prize_instance表存每次活动绑定的具体参数当前库存、生效时间段、动态权重系数每次抽奖前服务端通过SELECT ... FOR UPDATE锁定prize_instance行计算final_weight base_weight * dynamic_coefficient再用别名weight_sum累加所有有效奖品权重最后用ThreadLocalRandom.current().nextLong(weight_sum)做加权随机。提示这里不用Redis ZSET做权重抽样是因为ZSET的ZRANGEBYSCORE无法保证原子性扣减库存——你抽中了“5元券”但库存扣减失败下次还会抽中它形成脏数据。2.2 微信JS-SDK签名签发绕过“config:invalid signature”玄学报错的实操路径前端调用微信wx.ready()前必须传入jsapi_ticket签名而这个签名依赖access_token——但access_token有效期2小时且调用量有限制。常见翻车点是把access_token硬编码进前端或每请求都去微信服务器拉一次。本项目采用三级缓存策略内存缓存Caffeineaccess_token存30分钟命中率99.2%Redis兜底内存失效时读Redis过期时间设为1小时40分预留20分钟容错后备降级Redis不可用时返回预置的mock_access_token仅用于开发环境生产环境抛异常。签名生成代码关键段public String generateJsSdkSignature(String url) { String nonceStr UUID.randomUUID().toString().replace(-, ); long timestamp System.currentTimeMillis() / 1000; String jsapiTicket wxConfigService.getJsapiTicket(); // 自动刷新逻辑封装在此方法内 String string1 String.format(jsapi_ticket%snoncestr%stimestamp%durl%s, jsapiTicket, nonceStr, timestamp, url); String signature DigestUtils.sha1Hex(string1); return String.format({\appId\:\%s\,\timestamp\:%d,\nonceStr\:\%s\,\signature\:\%s\}, wxConfig.getAppId(), timestamp, nonceStr, signature); }注意url必须是前端实际访问的完整URL含#前部分不能是location.href——因为微信会校验location.origin location.pathname location.search#后面参数会被忽略。这是血泪经验某次活动因H5用了Vue Router history模式url拼错导致30%用户白屏。2.3 中奖结果一致性保障MySQLRedis双写下的最终一致性落地中奖后要同时做三件事扣库存、写中奖记录、通知微信模板消息。若用单事务包裹全部操作Redis写入失败会导致MySQL回滚但模板消息已发出——用户收到通知却没到账。我们拆成「可靠事件」模式第一步MySQL事务内完成prize_instance.stock - 1和win_record插入成功后向RocketMQ发WinEvent第二步消费端监听WinEvent先查MySQL确认记录存在再调微信API发模板消息失败则重试最多3次第三步Redis中prize_instance的stock字段用DECR命令异步更新即使失败也不影响主流程——因为库存以MySQL为准Redis只是缓存视图。这样既避免强一致性锁表又保证业务最终正确。某次压测发现RocketMQ消费延迟超2秒我们给WinEvent加了delayLevel310秒延时让模板消息稍晚发出反而降低微信API限流风险。3. 快速启动指南5分钟跑通本地调试环境验证核心链路3.1 环境准备与依赖安装本项目基于JDK 11、Maven 3.6、MySQL 5.7、Redis 6构建。请确保MySQL已创建数据库wechat_luckdraw字符集utf8mb4Redis服务运行在localhost:6379密码为空如需密码请修改application-dev.yml中的spring.redis.password微信公众号已配置JS接口安全域名开发时可填localhost但需在微信开发者工具中开启“不校验合法域名”。执行以下命令初始化数据库表结构SQL脚本位于src/main/resources/sql/init.sqlmysql -u root -p wechat_luckdraw src/main/resources/sql/init.sql该脚本会创建5张核心表prize_template奖品模板、prize_instance活动奖品实例、win_record中奖记录、user_activity用户参与记录、wx_config微信配置。其中wx_config表需手动插入一条测试数据INSERT INTO wx_config (app_id, app_secret, mch_id, mch_key) VALUES (wxd123456789abcdef, a1b2c3d4e5f678901234567890abcdef, 1234567890, key1234567890123456789012345678);3.2 启动服务与接口验证进入项目根目录执行mvn clean package -Dmaven.test.skiptrue java -jar target/wechat-luckdraw-1.0.0.jar --spring.profiles.activedev服务启动后访问http://localhost:8080/actuator/health应返回{status:UP}。接着用curl验证核心接口获取JS-SDK签名模拟前端H5页面URLcurl -X GET http://localhost:8080/api/wx/sign?urlhttp%3A%2F%2Flocalhost%3A8080%2Findex.html返回类似{appId:wxd123456789abcdef,timestamp:1712345678,nonceStr:abc123def456,signature:a1b2c3d4e5f6...}的JSON。2. 发起一次抽奖需替换openid为真实测试号curl -X POST http://localhost:8080/api/turn/start \ -H Content-Type: application/json \ -d {openid:oABC123456789012345678901234}成功返回{code:0,data:{prizeId:1,prizeName:5元无门槛券,winTime:2024-04-05T10:20:30}}即表示链路通。注意首次调用/api/turn/start会自动初始化一个默认活动奖品池含3个奖品后续可通过管理后台或SQL直接修改prize_instance.stock来重置库存。3.3 前端H5集成要点避开微信WebView的3个隐藏陷阱将后端返回的签名注入微信JS-SDK时必须遵守微信的时序约束wx.config()必须在wx.ready()之前调用且只能调用一次wx.ready()回调内才能调用wx.startRecord()等接口所有wx.方法必须在document.addEventListener(WeixinJSBridgeReady)或window.addEventListener(load)之后触发。某次上线发现iOS微信里wx.openLocation()白屏排查发现是wx.config()的jsApiList未声明openLocation——微信要求显式声明所有要用的API哪怕文档说“默认支持”。因此H5初始化代码必须包含// 假设后端已返回签名对象signData wx.config({ debug: false, appId: signData.appId, timestamp: signData.timestamp, nonceStr: signData.nonceStr, signature: signData.signature, jsApiList: [checkJsApi, startRecord, stopRecord, onVoiceRecordEnd, openLocation] });漏掉任意一项对应API在iOS下直接静默失败Android则可能报错但不阻断。4. 避坑指南生产环境踩过的5个真实坑每条都附带复现方式与修复命令4.1 现象用户点击“开始抽奖”按钮无响应Chrome控制台报config:invalid signature原因前端传入wx.config()的url参数与微信服务器校验的URL不一致。微信校验规则是取location.href的协议域名路径查询参数不含#及之后而Vue Router的history模式会把路由参数写在#后导致url拼错。例如真实URL是https://example.com/#/turn?act202404微信校验的却是https://example.com/。解决在H5入口JS中用window.location.origin window.location.pathname window.location.search构造url而非window.location.href。修复代码// ❌ 错误写法 const url window.location.href; // ✅ 正确写法 const url window.location.origin window.location.pathname window.location.search; fetch(/api/wx/sign?url${encodeURIComponent(url)})4.2 现象高并发下出现同一用户多次中奖同一奖品MySQL中win_record有重复记录原因/api/turn/start接口未对openidactivity_id做幂等性校验。当用户快速连点两次两个请求几乎同时到达都通过了库存检查此时库存为1都执行了扣减和记录插入。解决在Service层添加分布式锁。使用Redis的SET key value EX 10 NX指令key为lock:turn:${openid}:${activityId}value为UUID超时10秒。伪代码String lockKey lock:turn: openid : activityId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, UUID.randomUUID().toString(), Duration.ofSeconds(10)); if (!locked) { throw new BusinessException(操作过于频繁请稍后再试); } try { // 执行抽奖逻辑 } finally { redisTemplate.delete(lockKey); // 必须finally释放 }4.3 现象微信模板消息发送失败日志显示errcode:40001, errmsg:invalid credential原因access_token过期后服务端未及时刷新仍用旧token调用微信API。本项目虽有自动刷新机制但wxConfigService.getAccessToken()方法在Redis缓存失效时未对getAccessTokenFromWechat()的HTTP调用做熔断导致大量线程阻塞在微信服务器响应上进而拖垮整个服务。解决为微信API调用添加Hystrix熔断。在WxConfigService中用HystrixCommand(fallbackMethod getAccessTokenFallback)注解包裹获取逻辑并设置timeoutInMilliseconds3000。fallback方法返回预设的mock_token并告警HystrixCommand( fallbackMethod getAccessTokenFallback, commandProperties { HystrixProperty(name execution.timeout.enabled, value true), HystrixProperty(name execution.isolation.thread.timeoutInMilliseconds, value 3000) } ) public String getAccessToken() { /* ... */ }4.4 现象MySQL中prize_instance.stock为负数但win_record记录数正常原因库存扣减SQL未加WHERE stock 0条件。当多个请求并发读取同一奖品库存为1都判断“可抽奖”然后都执行UPDATE prize_instance SET stock stock - 1 WHERE id ?最终stock变为-1。解决库存扣减必须带条件检查。正确SQLUPDATE prize_instance SET stock stock - 1 WHERE id ? AND stock 0;并在Java代码中检查update返回值若为0说明库存不足抛出BusinessException(奖品已被抢光)。这是比加锁更轻量的解决方案。4.5 现象Redis中prize_instance缓存的stock与MySQL不一致导致前端显示“剩余100”但实际已售罄原因prize_instance的Redis缓存未设置过期时间且没有监听MySQL binlog做自动失效。当运营后台手动修改MySQL库存后Redis缓存长期不更新。解决为所有prize_instance相关缓存设置TTL如30分钟并在PrizeInstanceService.updateStock()方法末尾强制删除Redis缓存public void updateStock(Long instanceId, Integer newStock) { prizeInstanceMapper.updateStock(instanceId, newStock); // 强制清除缓存 redisTemplate.delete(prize_instance: instanceId); }同时在PrizeInstanceService.getPrizeInstance()中若Redis未命中则查DB后写入Redis并设EX 180030分钟。5. 运营级功能扩展如何用3个配置项实现“新用户专享”“时段加权”“渠道分流”5.1 新用户专享奖池通过user_activity表实现精准识别微信生态里“新用户”定义是user_activity表中first_visit_time为空的用户。我们在/api/turn/start接口中增加判断逻辑查询user_activity表若openid不存在或first_visit_time IS NULL则加载prize_instance时追加AND is_new_user_only 1条件否则加载is_new_user_only 0 OR is_new_user_only IS NULL的奖品。数据库表结构需新增字段ALTER TABLE prize_instance ADD COLUMN is_new_user_only TINYINT(1) DEFAULT 0 COMMENT 是否仅新用户可见0-否1-是;运营后台可为“新用户专享券”单独配置一个prize_instanceis_new_user_only1库存独立完全不影响老用户抽奖池。某次活动用此功能将新客转化率提升了27%因为老用户不会抱怨“为什么我抽不到”。5.2 时段加权策略用prize_instance.weight_coefficient实现动态调控奖品权重不再写死而是由base_weight * weight_coefficient动态计算。weight_coefficient字段类型为DECIMAL(5,2)默认1.0。我们提供一个定时任务每天0点、12点、20点自动更新Scheduled(cron 0 0 0,12,20 * * ?) public void updateWeightCoefficient() { // 将“100元话费”在晚8点权重提至3.0 prizeInstanceMapper.updateWeightCoefficient(1001, BigDecimal.valueOf(3.0), 20:00:00); }对应的SQLUPDATE prize_instance SET weight_coefficient ? WHERE prize_template_id ? AND TIME(NOW()) ?;前端无需任何改动后端自动生效。某次618大促我们将“满200减50券”在晚8点权重设为5.0配合短信推送该时段核销率占全天63%。5.3 渠道分流控制基于source_channel参数隔离奖品池当活动通过不同渠道分发如公众号菜单、小程序卡片、朋友圈广告需区分奖品库存。我们在/api/turn/start接口增加可选参数source_channel并修改抽奖逻辑若传入source_channel则prize_instance查询条件追加AND channel #{sourceChannel} OR channel IS NULLchannel字段类型为VARCHAR(32)可填mp_menu、mini_program、wechat_ad等。数据库变更ALTER TABLE prize_instance ADD COLUMN channel VARCHAR(32) DEFAULT NULL COMMENT 适用渠道mp_menu/mini_program/wechat_ad;这样朋友圈广告带来的用户抽中“10元红包”不会消耗公众号菜单用户的“5元券”库存运营可针对各渠道ROI独立配置奖品。5.4 验证这些功能是否生效用三条SQL快速巡检上线后用以下SQL组合验证配置是否正确落地检查新用户专享奖品是否独立SELECT p.name, i.stock, i.is_new_user_only FROM prize_instance i JOIN prize_template p ON i.prize_template_id p.id WHERE i.is_new_user_only 1;检查时段加权是否已更新查最近1小时内的系数SELECT prize_template_id, weight_coefficient, updated_time FROM prize_instance WHERE updated_time DATE_SUB(NOW(), INTERVAL 1 HOUR) ORDER BY updated_time DESC LIMIT 5;检查渠道分流配置是否完整SELECT DISTINCT channel FROM prize_instance WHERE channel IS NOT NULL;如果返回空集说明所有奖品都走通用池如果返回[mp_menu,mini_program]则分流已启用。从那以后我每次上线新活动前都强制走一遍这三条SQL一次curl抽奖一次微信JS-SDK签名验证——哪怕只改了一个文案也要确认url拼接、access_token刷新、库存扣减三个环节全链路通。因为大转盘不是炫技的Demo它是用户第一次感知品牌温度的触点也是财务对账的原始凭证。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

树莓派CM4/CM5、香橙派CM4/CM5、香蕉派CM5 Pro横向对比:接口、功耗、散热与选型指南

树莓派CM4/CM5、香橙派CM4/CM5、香蕉派CM5 Pro横向对比:接口、功耗、散热与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:17:12 阅读更多 →
从54秒重建1000帧看大规模3D场景快速重建的工程实践

从54秒重建1000帧看大规模3D场景快速重建的工程实践

看到“某头部GPU厂商放出的VGG-T3框架,54秒重建1000帧图像的大规模3D场景”这条消息时,我第一反应是:这多半又是个标题党。 但仔细看了两遍之后,我反而开始认真琢磨它背后的技术路线。过去大半年我一直在跟大规模3D场景重建打交道…

2026/10/13 14:17:12 阅读更多 →
Flink CDC 3.0 + Doris:一条YAML打通的MySQL实时数仓同步链路

Flink CDC 3.0 + Doris:一条YAML打通的MySQL实时数仓同步链路

简介:尚硅谷研究院编写的Flink CDC 3.0学习文档,面向已有一定Flink基础、关注大数据实时同步与流式ETL的研发人员,聚焦MySQL数据实时捕获并同步至Doris的完整流程。文档从CDC概念入手,对比基于查询与基于Binlog两类实现方式的区别…

2026/10/13 14:17:12 阅读更多 →

最新新闻

多智能体电梯群控:基于目的楼层预约与PPO的深度强化学习实现

多智能体电梯群控:基于目的楼层预约与PPO的深度强化学习实现

简介:本资源是一套面向计算机、人工智能、自动化等专业学生的毕业设计与课程设计实践项目,聚焦于多智能体协同下的电梯群控优化问题,采用深度强化学习实现目的楼层预约调度算法,解决传统电梯响应延迟高、能耗大、乘客等待时间长等…

2026/10/13 19:14:30 阅读更多 →
语义分割环境搭建:PyTorch CPU与GPU版本安装及验证全攻略

语义分割环境搭建:PyTorch CPU与GPU版本安装及验证全攻略

1. 系列前情回顾与本期核心目标语义分割学习系列写到第四篇,前面几篇我分别梳理了语义分割的基本概念、常见数据集的组织方式、以及几个主流分割模型的结构差异。按照正常的学习路线,理论铺垫到这一步,接下来就该动手了。动手的第一步就是装环…

2026/10/13 19:14:30 阅读更多 →
Spring Boot配置绑定实战:@ConfigurationProperties原理与避坑

Spring Boot配置绑定实战:@ConfigurationProperties原理与避坑

接触Spring Boot这些年,我做得最多的一个动作就是替同事定位“配置不生效”的诡异问题。排查到最后,十有八九都落在同一个注解上——ConfigurationProperties。它看起来只是给配置类加个前缀声明,实际牵扯到数据绑定、松散命名、第三方注册、…

2026/10/13 19:14:30 阅读更多 →
语义分割必备:PyTorch CPU/GPU版本安装全攻略

语义分割必备:PyTorch CPU/GPU版本安装全攻略

废话不多说,直接进入正题。这一篇是这个系列的第四篇,主题是很多人在语义分割学习中绕不过去的第一道坎——CPU和GPU版本的PyTorch及其环境安装。前几篇我们聊了语义分割的数据标注、模型结构、评价指标,但真正动手跑代码的时候,你…

2026/10/13 19:14:29 阅读更多 →
Java核心知识点全解析:从JVM到并发编程的底层原理

Java核心知识点全解析:从JVM到并发编程的底层原理

做Java开发这些年,我有一个很深的感受:真正拉开水平差距的,不是谁背的框架更多、API记得更熟,而是对底层那批核心知识点有没有吃透。所谓“Java重要知识点”,不是让你背八股文,而是帮你在遇到线上问题时&am…

2026/10/13 19:14:29 阅读更多 →
Go监听MySQL binlog实战:轻量级数据变更捕获与生产避坑指南

Go监听MySQL binlog实战:轻量级数据变更捕获与生产避坑指南

1. 这不是“监听MySQL”,而是重建一套轻量级数据变更管道很多人第一次看到“用Go监听binlog”这个说法,第一反应是:“MySQL自己不就有mysqlbinlog命令吗?再不济写个Python脚本调用一下不就完了?”——我试过&#xff0…

2026/10/13 19:13:29 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →