news 2026/10/21 11:15:35

微信积分商城H5落地指南:授权、账务与并发防超卖全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信积分商城H5落地指南:授权、账务与并发防超卖全解析

简介:这是一套面向移动端微信积分兑换商城的H5项目源码,定位于帮助开发者快速搭建具备签到、游戏积分、代金券兑换等功能的积分商城页面。基于HTML5技术实现,适配手机微信环境,适用于电商运营、会员营销等场景,也适合前端学习者参考完整积分商城的前端实现。压缩包含112个文件,体积约1.19MB,其中20个html构成商城核心页面,9个js负责交互逻辑,3个css统一样式,另有80张png/jpg图片素材支撑界面视觉。目前已有623人浏览学习。资源内提供完整的积分商城网站模板,涵盖首页、签到、兑换、个人中心等多个页面的源码,开发者可直接运行预览,并在此基础上调整积分上限、兑换规则和页面文案,也可结合微信支付、分享等接口做深度集成。对希望低成本搭建积分体系或研究H5商城交互细节的读者,这套源码具有直接的参考与复用价值。

1. 手机微信积分兑换商城 H5:先拆开这套 zip 再决定要不要接手

运营同事丢过来一个「手机微信积分兑换商城H5.zip」,说这是现成的积分商城,解压就能用。做过微信生态的都清楚,这种压缩包最像黑匣子:首页 Demo 看着完整,等你配微信网页授权、配 JS-SDK 分享、接真库存的时候,每一步都可能卡住。这套 H5 积分商城的本质不复杂:前端是跑在微信浏览器里的移动商城页面,后端负责微信授权拿 openid、积分扣减、库存锁和订单生成。它解决的是「公众号或服务号用户手里有积分,却缺一个能直接兑换商品的落地页」这个具体问题。适合谁?手里有微信流量入口、又不想从零写商城后台的团队。接手前先确认三件事:授权链路是否闭环、积分是否走流水表、下单库存有没有并发控制,这三条不过关,后面全是血泪经验。

2. 跑起来之前先看懂三件事:微信授权、积分账务与商城表结构

拆开包先别急着配域名,先看数据层和授权链路。这一章是后面所有操作的地基,地基没打正,第 3 章的代码抄得再对也跑不通。很多团队拿到现成 H5 商城第一件事就是传服务器,结果授权回调失败、积分对不上账,回头再改表结构,比重新写还难受。

2.1 微信网页授权:用 openid 做用户主键,别自己造账号体系

微信内 H5 商城要做的第一件事是识别用户。小程序用 code 换 openid,H5 走的则是微信网页授权 OAuth2.0。常见做法是用户进入页面时先跳一次授权链接,微信回调带回 code,后端再用 code 换 access_token 和 openid。这里有个容易误判的点:商城场景下绝大多数积分系统都用 snsapi_base 静默授权,因为它只花一次跳转、用户无感知;只有你需要头像昵称做展示时,才改用 snsapi_userinfo,而且用户必须手动点同意。我一般建议积分商城默认用 snsapi_base,头像昵称等用户兑换时再补,换来的是更低的授权流失率。

下面这段是后端处理微信授权回调的常见写法,用 PHP 伪代码表示:

// wechat_auth.php 授权回调处理 $code = $_GET['code'] ?? ''; if (!$code) { // 拼授权链接:appid 换成你的服务号 appid $redirect = urlencode("https://你的域名/index.php/api/wechat/auth"); $url = "https://open.weixin.qq.com/connect/oauth2/authorize?" . "appid=APPID&redirect_uri={$redirect}" . "&response_type=code&scope=snsapi_base&state=123#wechat_redirect"; header("Location: $url"); exit; } // 用 code 换 openid,appid 与 secret 从配置文件读取 $tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?" . "appid=APPID&secret=SECRET&code={$code}&grant_type=authorization_code"; $resp = json_decode(file_get_contents($tokenUrl), true); $openid = $resp['openid'] ?? ''; if (!$openid) { // 常见失败:code 被重复使用、secret 错误、用户拒绝授权 exit("授权失败,请从商城入口重新进入"); } // 用 openid 查本地用户表,不存在则自动注册 $user = queryUserByOpenid($openid); if (!$user) { $user = createUserByOpenid($openid); } // 写入 session,后续接口靠 session 识别身份 session_start(); $_SESSION['openid'] = $openid;

这段代码的关键在最后三步:查用户、建用户、写 session。很多人图省事,每次请求都拿 code 换一次 openid,这既浪费接口配额,又会撞上 code 只能使用一次的限制——微信的 code 有效期五分钟且只认一次,重复拿去换 token 必然失败。参数上注意 scope 必须写 snsapi_base,state 随意但建议带随机值防 CSRF,这样回调回来能校验是不是自己发起的请求。

2.2 积分账务模型:流水表和冻结额度决定你后面对账累不累

多数积分商城翻车不是翻在前端页面,而是翻在账务模型。如果代码里直接 update users set points = points - 100,那就等于把账户余额当计数器用:兑换失败、退货、后台手动调账,全都没法追溯。我见过的靠谱实现都至少有一张积分流水表和一张积分冻结表,流水表只增不改,每一笔积分变动都插入一行,人工调账也走流水,负分允许出现但必须带业务单号。

流水表的另一层作用是给运营留后悔药:用户投诉「积分少了」时,你只需要按 openid 查流水,就能知道他每一分去了哪,而不是对着一个余额数字干瞪眼。下面是一张典型的设计:

CREATE TABLE points_ledger ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_openid VARCHAR(64) NOT NULL COMMENT '微信openid', change_type TINYINT NOT NULL COMMENT '1签到 2兑换扣减 3兑换退回 4后台调整 5过期扣除', points INT NOT NULL COMMENT '变动积分,扣减为负数', order_no VARCHAR(32) DEFAULT '' COMMENT '关联订单号,调账可空', remark VARCHAR(255) DEFAULT '' COMMENT '备注', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_openid, created_at), KEY idx_order (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE points_freeze ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_openid VARCHAR(64) NOT NULL, order_no VARCHAR(32) NOT NULL, points INT NOT NULL COMMENT '冻结的积分数', status TINYINT NOT NULL DEFAULT 0 COMMENT '0冻结中 1已扣减 2已退回', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

冻结表解决的是「用户下单后、订单完成前,这笔积分到底算谁的」这个模糊地带。下单先冻结,订单完成才正式扣减;订单取消则解冻退回。这样设计的好处很明显:用户一次性下了五单,五笔冻结都在,最后一单失败时不会把已经扣掉的分再算一遍。参数上注意 change_type 必须用注释写清楚,否则三个月后你自己看着 0、1、2 也想不起来是什么含义。

2.3 商城表结构:商品、库存、兑换记录怎么设计最省事

商品和订单表相对直白,但有几个字段最好一开始就加上,省得后面加需求时改表。商品表需要区分「积分价」和「现金价」:纯积分商品现金价为 0;如果做积分加现金混合支付,则需要同时记录两个值。很多二手代码只留一个 price 字段,活动搞积分抵现时才发现算不出应付金额,只能硬编码,这是最常见的返工点。

CREATE TABLE product ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, cover_image VARCHAR(255) NOT NULL, points_price INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '积分价,0表示不支持积分购买', cash_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '现金价,0表示纯积分', stock INT NOT NULL DEFAULT 0 COMMENT '库存,下单扣减', status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', sort INT NOT NULL DEFAULT 0, KEY idx_status (status, sort) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE exchange_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', user_openid VARCHAR(64) NOT NULL, product_id INT NOT NULL, points_cost INT NOT NULL COMMENT '实际扣减积分', cash_cost DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '实际支付现金', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待确认 1已完成 2已取消 3已退款', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_openid, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么积分价和现金价分开存?因为积分兑换商城经常做「积分抵现」活动:原价 100 元,支持 500 积分抵 20 元。两个值分字段存,后续算应付金额就是一行公式;塞在一个字段里,改活动规则时只能写死逻辑。另外订单号建议用后端生成的长单号,日期加随机数,不要用数据库自增 id 直接当单号给用户,原因在订单退款对账时你就会明白——自增 id 暴露订单量,而且不好做全局唯一。

3. 把 H5 商城本地跑起来:从解压到完成一次积分兑换

数据层看懂之后,开始动手。这一章按「解压、配环境、授权联调、做成一单」的顺序走,每一步都能直接照着做。如果解压后文件都认不全,先做 3.1 的目录识别,别把后端模板页面当静态文件传到 CDN 上,那是新手最容易犯的错。

3.1 先读配置文件:前后端分离还是模板渲染,决定本地怎么起

解压后第一件事不是双击 index.html,而是看目录结构。常见做法分两种:一种是 PHP 模板渲染,页面、接口、数据库都在一个项目里;另一种是前后端分离,H5 前端是纯静态资源,后端只出 JSON 接口。识别方法很简单:看根目录有没有 index.php 或 application 目录,有就是模板渲染;只有 static、assets 和 config.js 之类,就是前后端分离。前者本地要装全套运行环境,后者可以先把静态页随便放到静态服务器上看效果。

两种形态的配置文件分别写在 .env 或 config.php 里,内容大致是这些:

# .env 环境配置示例 APP_NAME=points-mall APP_ENV=local APP_DEBUG=true # 微信服务号配置 WX_APPID=wx1234567890abcdef WX_SECRET=abcdef1234567890abcdef1234567890 # 数据库连接 DB_HOST=127.0.0.1 DB_PORT=3306 DB_NAME=points_mall DB_USER=root DB_PASS=root # 站点域名,微信授权回调会用到,必须与微信后台一致 APP_URL=https://mp.example.com

这里最容易踩的坑是 APP_URL。微信授权回调要求域名和微信公众平台后台配置的「网页授权域名」完全一致,连端口都要一致,而且域名必须是公网可访问的。本地开发如果直接写 localhost,授权必然失败——微信服务器根本访问不到你的电脑,这一步卡住的人最多。

3.2 微信授权回调的本地联调:没有公网域名也能测

本地起服务以后,授权回调卡在「微信服务器要回调你的公网地址」这一步。常见做法有两种:一是准备一台测试服务器,把代码部署到测试服域名上;二是本地起一个内网穿透服务,把 80 端口映射到一个临时公网域名,再把微信后台的网页授权域名临时改成这个临时域名。第二种适合快速验证,但临时域名每次重启都会变,改一次微信后台至少等几分钟生效,别在授权流程里反复切。

映射起来之后,数据库里的 APP_URL 要同步改成临时域名,不然回调地址拼接出来还是 localhost。整个链路是:用户微信打开页面,302 跳微信授权,微信带 code 回调你的临时域名,后端换 openid,种 session,跳回商城首页。调试时重点看浏览器 Network 面板里 /connect/oauth2/authorize 的请求和回调请求:code 有没有带到、后端有没有报错,一目了然。如果回调请求根本没发出去,优先检查微信后台域名配置,而不是代码。

提示:授权回调调试最好用微信开发者工具,它内置了本地代理能力,可以自定义域名映射,比真机验证快得多。真机只留到最后验证分享和支付。

3.3 最小兑换闭环:商品详情 → 提交兑换 → 扣积分 → 生成订单

配置通了,就该验证核心链路。最小闭环是:前端展示商品详情 → 用户点立即兑换 → 后端校验积分和库存 → 扣积分、扣库存、生成订单 → 返回结果。下面这段是把兑换逻辑写成单事务的示范,重点看执行顺序:

// exchange.php 兑换接口核心逻辑 public function doExchange($productId, $openid) { // 开启事务,防止并发下库存和积分不一致 $pdo->beginTransaction(); try { // 1. 锁定商品行,防止库存超卖 // 注意必须用 FOR UPDATE,普通 SELECT 在高并发下会读到同一份库存 $stmt = $pdo->prepare( "SELECT id, title, points_price, stock FROM product WHERE id = ? AND status = 1 FOR UPDATE" ); $stmt->execute([$productId]); $product = $stmt->fetch(); if (!$product) throw new Exception("商品不存在或已下架"); if ($product['stock'] <= 0) throw new Exception("库存不足"); if ($product['points_price'] <= 0) throw new Exception("该商品不支持积分兑换"); // 2. 读取用户当前可用积分(余额=总积分-冻结中) $userPoints = getAvailablePoints($pdo, $openid); if ($userPoints < $product['points_price']) { throw new Exception("积分不足,当前可用 " . $userPoints); } // 3. 扣库存、写流水、冻结积分、生成订单,顺序不能乱 $pdo->prepare("UPDATE product SET stock = stock - 1 WHERE id = ?") ->execute([$productId]); $orderNo = date('YmdHis') . mt_rand(1000, 9999); $pdo->prepare( "INSERT INTO exchange_order (order_no, user_openid, product_id, points_cost, status) VALUES (?, ?, ?, ?, 0)" )->execute([$orderNo, $openid, $productId, $product['points_price']]); $pdo->prepare( "INSERT INTO points_ledger (user_openid, change_type, points, order_no, remark) VALUES (?, -2, ?, ?, '兑换商品')" )->execute([$openid, $product['points_price'], $orderNo]); $pdo->prepare( "INSERT INTO points_freeze (user_openid, order_no, points, status) VALUES (?, ?, ?, 0)" )->execute([$openid, $orderNo, $product['points_price']]); // 4. 全部成功才提交事务 $pdo->commit(); echo json_encode(['code' => 0, 'msg' => '兑换成功', 'order_no' => $orderNo]); } catch (Exception $e) { $pdo->rollBack(); echo json_encode(['code' => 1, 'msg' => $e->getMessage()]); } }

逻辑说明:第一步的 SELECT ... FOR UPDATE 是整个防超卖的核心,它把商品行锁住,第二个用户进来必须等第一个事务提交或回滚,库存变负数的情况从根上被堵住。第 2 步的可用积分计算用的是「总积分减冻结中积分」,这样同一个用户并发兑换多件商品也不会互相透支。参数说明里最值得注意的是流水表和冻结表的同步写入:这里的 change_type=-2 表示扣减,但在订单点击确认收货之前,这 100 积分只是被冻结,并没有真正从用户余额里减去。你要是现在不理解这句话,回 2.2 再看一眼冻结表的说明。

4. 微信积分商城避坑指南:授权、库存、支付三大翻车现场

到这里代码能跑了,但能跑和能上线中间隔着大量真实环境问题。这一章把最常见的四个坑摊开,每个都按「现象 → 原因 → 解决」讲清楚,全是上线现场真实发生过的问题。

4.1 授权回调报 redirect_uri 参数错误:微信后台域名没加或加了不同端口

现象:用户从公众号菜单点进商城,微信弹出一个「redirect_uri 参数错误」的空白错误页,后端看不到任何业务日志,排查半天不知道从哪下手。

原因:90% 是微信公众平台后台的「网页授权域名」没配,或者配的域名与代码里的回调地址不一致。这里有个冷门坑:后台配的是 mp.example.com,代码里拼接回调地址时用了 https://mp.example.com/api/wechat/auth,粗看一致;实际上微信在回调时会严格比对协议、域名和端口,https 和 http 是两个东西,带不带 80/443 端口也是两个东西,配了 www 和不配 www 又是两个东西。你以为的一致,微信不认。

解决:先到微信公众平台后台确认网页授权域名,再检查代码里 APP_URL 是否与之一致,尤其注意协议和端口。我一般会写一条启动日志,把当前应用实际拼接的回调 URL 打印出来,上线前三分钟盯一眼,能避免绝大多数「换个环境就授权失败」的乌龙。

4.2 积分扣了但库存是负数:上线第一天就被薅出超卖

现象:运营反馈后台商品库存显示 -3,但兑换订单只有 5 单,页面也没提示库存不足,商品却被换超了。

原因:下单逻辑没加锁。常见错误写法是先 SELECT 查库存,发现大于 0,再 UPDATE 扣库存;高并发下两个请求同时查到库存=1,各自往下走,库存就变负数了。更要命的是订单状态机不完整:用户取消订单只改订单状态、没把库存加回去,库存会越差越多,最后运营以为系统有 bug,实际是逻辑漏洞。

解决:库存扣减必须写成原子操作,把「判断库存大于 0 并扣减」合并成一条 SQL:UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0,然后检查受影响行数,为 0 就说明没抢到。如果用了 3.3 的 FOR UPDATE 事务方案,同样能达到目的,但要注意事务里锁的顺序必须全局一致,否则高并发下可能死锁。另外取消订单时,把回补库存和积分退回归到同一个事务里,别拆成两个接口。

4.3 JS-SDK 签名失败:分享朋友圈没有缩略图,接口报 invalid signature

现象:页面功能全部正常,唯独自定义「分享给好友、分享到朋友圈」的标题和缩略图不生效;后台日志里 wx.config 报 invalid signature,这个问题看起来很玄学,因为同样的代码在安卓上偶尔好、在 iOS 上必挂。

原因:H5 商城要自定义分享样式,前端必须先调 wx.config 注入 JS-SDK 配置,而后端要用「当前页面的完整 URL」去生成签名。经典翻车点有两个:一是拿配置的域名去生成签名,而不是拿用户浏览器地址栏里的完整 URL(包括问号参数和井号后面的部分);二是页面里有异步路由跳转,导致签名时用的 URL 和实际分享时页面 URL 不一致,iOS 对 URL 的比对比安卓严格得多。

解决:签名 URL 必须用前端location.href.split('#')[0]动态获取,再传给后端参与 SHA1 签名。更保险的做法是单页应用里每次路由变化后重新调一次 wx.config,放弃「一次配置全局生效」的假设。另外 access_token 有 7200 秒有效期,代码里要做缓存,否则频繁刷新 token 会把接口调用配额快速耗尽,影响其他业务。

4.4 纯积分和积分加现金混合支付:别把两套流程塞进同一个下单接口

现象:纯积分兑换一切正常,一旦开启「积分加微信支付」的混合支付,下单后经常出现积分扣了但支付没完成,订单卡在中间态,用户来投诉,后台又不敢乱改订单状态。

原因:混合支付的本质是两个独立动作:扣积分是积分系统的事,拉起微信支付是支付系统的事。把扣积分塞进支付回调里,一旦支付超时或用户中途退出,积分状态就悬空了;反过来,先把积分扣掉再拉起支付,用户放弃支付后还得写退款逻辑,哪个方向都不好收场。

解决:下单时先冻结积分并生成待支付订单,支付回调里把订单置为已支付,再真正扣减冻结积分;用户取消支付则解冻退回。纯积分订单走另一条路径:校验 → 扣积分 → 直接完成。两条路径可以共用订单表,但接口必须分开,别用一堆 if 分支把时序搅乱。这里没有后悔药可吃,状态机设计错了,上线后补数据比写代码痛苦得多。

5. 上线前最后一步:用一套仿真流程验证积分商城,再谈进阶

核心链路跑通后,我习惯先不急着加功能,而是把「一个真实用户从进店到拿货」的完整链路用测试账号过一遍,尤其是下面这张验证清单:

验证项操作预期结果
授权闭环未登录用户从分享链接进入自动授权、无感注册、回到原页面
积分余额后台给测试号加 500 分商城页显示可用 500 分
库存扣减库存 1 的商品并发下两单只有一单成功,另一单提示库存不足
积分扣减用 300 分兑换一件商品流水表出现一条 -300,可用积分变 200
取消订单下单后 5 分钟内取消冻结积分退回,库存回补 1
重复提交快速连点两次兑换按钮只生成一单,前端按钮要有防抖

这张表跑完,说明这套 H5 商城的核心是健康的,然后才谈进阶。我比较推荐三个方向:一是积分过期任务,每天凌晨跑定时脚本,把超过有效期的积分按流水扣除,注意逐条写流水而不是直接清零,否则用户查不到明细又来投诉;二是分享加积分,在 wx.config 的分享成功回调里给分享者加积分,这是拉新成本最低的玩法;三是模板消息通知,下单成功或发货后用微信模板消息推给用户,能明显降低「我换的东西怎么没消息」的客服压力。

说一个我自己的习惯:每次改动兑换相关逻辑,第一件事不是看前端效果,而是开两个终端窗口,一个 tail 后端日志,一个连着数据库盯 points_ledger 和 exchange_order 两张表的变化。积分商城这种系统,用户感知到的都是「分没到账」,而账务问题的根源几乎都在流水和订单的状态不一致上。这个习惯帮我挡掉了不少上线事故,也推荐给你。最后补一句:如果你接手积分商城 H5 时能少走两个弯路,那这些坑就没白踩。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 7:38:14

Swing中ActionListener与FocusListener实战避坑指南

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

作者头像 李华
网站建设 2026/10/22 4:24:47

PDF密码移除实战:权限密码、批量处理与命令行自动化

简介&#xff1a;VeryPDFPDFPasswordRemover3.1是一款专业的PDF密码移除工具&#xff0c;面向需要解除PDF文档密码及操作权限限制的用户&#xff0c;可有效去除打印、复制、编辑等安全设置&#xff0c;适用于共享受限文档、提取需要编辑的PDF内容等日常办公场景&#xff0c;无需…

作者头像 李华
网站建设 2026/10/21 13:39:12

MATLAB卡尔曼滤波实战:从数据生成到参数调优的完整指南

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

作者头像 李华
网站建设 2026/10/18 4:47:46

FreeSWITCH 对接云端 ASR:NlsSdkCpp 与 mod_asr 编译实践

简介&#xff1a;面向 FreeSWITCH 开发者的阿里云实时语音识别对接模块&#xff0c;内含 mod_asr_ali_3.x 主目录及完整源码包&#xff0c;致力于解决电话软交换平台接入云语音服务的集成问题。通过阿里云 NlsSdkCpp3.X 实时将通话语音转写为文本&#xff0c;可应用于呼叫中心对…

作者头像 李华
网站建设 2026/10/22 0:52:34

Flask项目MVC拆分实战:Blueprint模块化与工厂模式避坑指南

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

作者头像 李华
网站建设 2026/10/21 16:28:33

ML-Agents 2020.7环境配置:四层隔离与版本锚定实践

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

作者头像 李华