Java实现点击文字与滑动图片验证码:原理、代码与单元测试

发布时间:2026/10/1 8:54:22
Java实现点击文字与滑动图片验证码:原理、代码与单元测试 简介本资源面向Java后端与全栈开发者提供一套可直接用于生产环境的用户行为验证码方案涵盖点击中文文字图片验证码与拖动/滑动图片验证码两种形态适合需要提升登录、注册等场景防刷能力的中高级开发者参考落地。压缩包共178个文件约8.92MB以68个Java源码为核心辅以21张png、11张jpg、6个gif等图片素材以及18个xml、15个css、13个js等前端与配置资源另有yml、ftl、ttf等字体模板文件结构完整。目前已有3078人学习下载。资源内含完整demo、单元测试与实现思路基于JDK1.8、Spring Boot 2.1.17与Redis利用BufferedImage、Graphics2D、Font实现随机中文文字、随机抠图与拼图前端结合点击坐标、图片位置与滚动位置计算相对坐标坐标传输采用AES或DES加密并附有部署步骤与运行说明便于快速搭建与二次开发。1. 从一次登录页改造说起Java 验证码为什么值得自己写一遍上个月帮一个做后台管理系统的团队做登录页改造需求很朴素把原来那个静态四位数字的图形验证码换掉换成点击文字验证码加拖动图片验证码的组合。听起来不难但真动手才发现市面上能直接拿来用的 Java 验证码方案要么是纯图形字符的要么是第三方 SDK 绑死某个云服务点击文字和滑动拼图这两类行为式验证码开源且带单元测试的完整实现并不多。这就是我决定自己写一套的原因也是这篇笔记要讲清楚的事。标题里的「Java 实现点击文字验证码与拖动/滑动图片验证码」核心是两件事一是点击文字验证码服务端给出一张带若干汉字的底图用户按提示顺序点击对应文字前端把点击坐标回传服务端校验坐标是否落在正确文字区域内二是拖动/滑动图片验证码服务端从背景图裁出一块拼图用户拖动滑块把拼图对齐到缺口服务端校验拖动轨迹和最终位置。这两类都属于行为式验证码比传统字符验证码更难被 OCR 批量识别对php ocr识别验证码这类自动化脚本的抵抗力明显更强。这套东西适合谁如果你正在做 Java Web 项目的登录、注册、短信发送、评论提交等需要防刷的入口又不想引入外部依赖那自己实现一套是划算的。源码、demo、单元测试三件套齐全意味着你能直接跑起来看效果也能改参数适配自己的业务。下面我按「先讲清两类验证码怎么设计再落到代码和参数最后说坑」的顺序展开中间会给出可抄作业的代码块和参数表。2. 点击文字验证码坐标校验模型与最小可跑实现2.1 为什么点击文字比字符验证码更难被批量破解传统字符验证码的破解路径很清晰截图、OCR 识别、回填。php ocr识别验证码和burpsuite json生成的验证码怎么识别这类搜索背后就是大量自动化工具在批量处理字符型验证码。点击文字验证码把「识别」和「交互」绑在一起攻击者不仅要识别出图上有哪些字还要知道每个字在图片里的精确坐标并且按正确顺序点击。这个坐标信息只存在于服务端生成时的那份数据里前端拿到的只是一张图和一个提示语。从工程角度看点击文字验证码的校验模型是这样的服务端生成底图时随机选 4 到 6 个汉字记录每个字在图片上的中心坐标和允许的点击半径把这些数据存进一个带过期时间的存储常见做法是 Redis单机 demo 可以用 ConcurrentHashMap。前端展示图片和提示「请依次点击春、风、得、意」用户点击后把坐标序列回传。服务端按顺序比对每个点击坐标与对应文字中心点的距离全部落在半径内才算通过。这个模型有两个关键参数点击半径和过期时间。半径太小用户点不准体验差半径太大攻击者随便点都能中。我一般设成文字字号的一半左右比如字号 28px半径设 14px。过期时间设 60 到 120 秒太短用户来不及操作太长给暴力尝试留了窗口。2.2 服务端生成底图与坐标数据的代码实现下面这段是生成点击文字验证码的核心逻辑用 Java 标准库的 BufferedImage 和 Graphics2D 画图不依赖任何第三方图形库。汉字从预设字库里随机取坐标在画的时候同步记录。public class ClickCaptchaGenerator { // 预设汉字库实际项目可以换成更丰富的字库 private static final String WORD_POOL 春风得意花好月圆山清水秀鸟语花香; // 点击半径单位像素约为字号一半 private static final int CLICK_RADIUS 14; // 验证码有效期单位秒 private static final int EXPIRE_SECONDS 90; public ClickCaptcha generate() { int width 300, height 180; BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g image.createGraphics(); // 抗锯齿让文字边缘更平滑 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(new Color(245, 245, 245)); g.fillRect(0, 0, width, height); Random random new Random(); ListWordPoint points new ArrayList(); SetInteger usedIndex new HashSet(); // 随机选 4 个字记录坐标 while (points.size() 4) { int idx random.nextInt(WORD_POOL.length()); if (usedIndex.contains(idx)) continue; usedIndex.add(idx); String word String.valueOf(WORD_POOL.charAt(idx)); int x 40 random.nextInt(width - 80); int y 50 random.nextInt(height - 100); g.setFont(new Font(SimHei, Font.BOLD, 28)); g.setColor(new Color(60 random.nextInt(80), 60 random.nextInt(80), 60 random.nextInt(80))); g.drawString(word, x, y); points.add(new WordPoint(word, x 14, y - 14)); } // 加一些干扰线增加 OCR 识别难度 for (int i 0; i 6; i) { g.setColor(new Color(180 random.nextInt(60), 180 random.nextInt(60), 180 random.nextInt(60))); g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height)); } g.dispose(); String captchaId UUID.randomUUID().toString(); // 存储坐标数据生产环境换成 Redis 并设置 TTL CaptchaStore.put(captchaId, points, EXPIRE_SECONDS); return new ClickCaptcha(captchaId, toBase64(image), points.stream().map(WordPoint::getWord).collect(Collectors.toList())); } }这段代码有几个点值得说明。WORD_POOL是字库实际项目里可以放几十上百个常用字避免每次都是那几个字被猜到。坐标记录时我用了x 14, y - 14因为drawString的坐标是文字左下角基线位置而用户点击的是文字视觉中心需要做偏移修正这个偏移量约等于字号的一半。干扰线的作用是干扰 OCR但不要画太多否则影响用户辨认。CaptchaStore是存储抽象demo 里用 ConcurrentHashMap 加定时清理生产环境换成 Redis 的SETEX。校验逻辑如下public boolean verify(String captchaId, Listint[] clickPoints) { ListWordPoint expected CaptchaStore.get(captchaId); if (expected null || expected.size() ! clickPoints.size()) { return false; } for (int i 0; i expected.size(); i) { WordPoint wp expected.get(i); int[] cp clickPoints.get(i); double dist Math.sqrt(Math.pow(wp.getX() - cp[0], 2) Math.pow(wp.getY() - cp[1], 2)); if (dist CLICK_RADIUS) { return false; } } // 校验通过后立即删除防止重放 CaptchaStore.remove(captchaId); return true; }校验时按顺序比对任何一次点击超出半径就失败。通过后立刻删除存储这是防重放的关键否则同一个 captchaId 可以被反复提交尝试。CLICK_RADIUS这个参数我建议做成可配置不同屏幕尺寸和字号下最优值不一样移动端可以适当放大到 18 到 20px。2.3 前端坐标采集与回传格式约定前端要做的事很简单把图片按原始尺寸展示监听点击事件把点击位置换算成图片坐标系下的坐标。注意不要用 CSS 缩放后的坐标否则和服务端记录的坐标对不上。常见做法是给 img 设固定宽高或者用 canvas 绘制后监听 canvas 坐标。// 假设 img 元素按原始尺寸 300x180 展示 const img document.getElementById(captchaImg); const clickPoints []; img.addEventListener(click, (e) { const rect img.getBoundingClientRect(); // 换算成图片原始坐标系 const x Math.round((e.clientX - rect.left) * (img.naturalWidth / rect.width)); const y Math.round((e.clientY - rect.top) * (img.naturalHeight / rect.height)); clickPoints.push([x, y]); // 画个标记给用户反馈 drawMarker(x, y); if (clickPoints.length 4) { submit(captchaId, clickPoints); } });回传格式用 JSON{captchaId: xxx, points: [[x1,y1],[x2,y2],...]}。这里有个容易翻车的点如果图片被 CSS 拉伸过naturalWidth / rect.width这个比例必须算对否则坐标整体偏移用户怎么点都过不了。我一般会在联调时先打印服务端记录的坐标和前端回传的坐标肉眼比对一下。3. 拖动/滑动图片验证码拼图缺口定位与轨迹校验3.1 拼图缺口生成随机位置与边缘留白滑动验证码的视觉部分是「背景图 缺口 拼图块」。服务端从背景图随机位置裁出一块拼图形状的小图作为滑块上的拼图块同时在背景图上把该位置挖空或画上半透明遮罩形成缺口。用户拖动滑块拼图块跟着移动对齐缺口后提交。生成缺口位置时有两个约束一是缺口不能太靠边否则拼图块拖到边缘时可能超出画布二是缺口位置要随机但每次生成的随机范围要留出滑块轨道的宽度。常见做法是缺口 x 坐标在[背景图宽度 * 0.3, 背景图宽度 - 拼图块宽度 - 10]之间随机y 坐标在[10, 背景图高度 - 拼图块高度 - 10]之间随机。public SlideCaptcha generate() { int bgWidth 320, bgHeight 180; int puzzleSize 50; BufferedImage bg loadRandomBackground(bgWidth, bgHeight); Random random new Random(); // 缺口位置x 留出左侧 30% 给滑块初始位置右侧留边距 int gapX (int) (bgWidth * 0.3) random.nextInt((int) (bgWidth * 0.6) - puzzleSize); int gapY 10 random.nextInt(bgHeight - puzzleSize - 20); // 裁出拼图块 BufferedImage puzzle bg.getSubimage(gapX, gapY, puzzleSize, puzzleSize); // 在背景图上画缺口遮罩 Graphics2D g bg.createGraphics(); g.setColor(new Color(0, 0, 0, 120)); g.fillRect(gapX, gapY, puzzleSize, puzzleSize); g.dispose(); String captchaId UUID.randomUUID().toString(); // 存储正确的 x 偏移量y 固定为 gapY SlideStore.put(captchaId, gapX, gapY, 90); return new SlideCaptcha(captchaId, toBase64(bg), toBase64(puzzle), gapY); }这里gapX就是用户需要把拼图块拖到的目标 x 位置。前端拿到gapY后把拼图块初始 y 设成gapY这样用户只需要水平拖动降低操作难度。SlideStore存的是gapX和gapY以及 90 秒过期时间。3.2 轨迹校验为什么只看最终位置不够如果只校验最终拖动位置攻击者用脚本直接算出gapX然后一次性拖过去就能绕过。所以滑动验证码的校验要加上轨迹特征拖动过程中的速度变化、是否有加速减速、总耗时是否合理。真人拖动通常是先快后慢有微调总耗时在 800ms 到 3000ms 之间脚本往往匀速或者瞬间到位。我一般会收集前端上报的轨迹点数组每个点包含x, y, timestamp服务端做三件事一是最终位置与gapX的偏差在允许范围内比如 ±5px二是轨迹点数量不少于 10 个且时间跨度在合理区间三是计算速度序列看是否有明显的匀速段。下面是一个简化的校验实现public boolean verify(String captchaId, ListTrackPoint track) { SlideData data SlideStore.get(captchaId); if (data null || track.size() 10) { return false; } TrackPoint last track.get(track.size() - 1); // 最终位置偏差校验 if (Math.abs(last.getX() - data.getGapX()) 5) { return false; } // 总耗时校验 long duration last.getTimestamp() - track.get(0).getTimestamp(); if (duration 500 || duration 5000) { return false; } // 速度变化校验统计速度标准差匀速脚本标准差接近 0 ListDouble speeds new ArrayList(); for (int i 1; i track.size(); i) { long dt track.get(i).getTimestamp() - track.get(i - 1).getTimestamp(); if (dt 0) continue; double dx track.get(i).getX() - track.get(i - 1).getX(); speeds.add(dx / dt); } double std standardDeviation(speeds); // 标准差过小说明太匀速判定为脚本 if (std 0.05) { return false; } SlideStore.remove(captchaId); return true; }standardDeviation是标准差的实现std 0.05这个阈值是我在几个项目里调出来的经验值不同前端采样频率下需要微调。轨迹点采集频率建议 20 到 50ms 一次太密浪费带宽太疏特征不明显。3.3 前端拖动交互与轨迹上报前端用 mousedown、mousemove、mouseup 三个事件实现拖动移动端对应 touchstart、touchmove、touchend。关键是把每次移动的坐标和时间戳记录下来提交时一起发给服务端。let dragging false; let track []; const slider document.getElementById(slider); const puzzle document.getElementById(puzzle); slider.addEventListener(mousedown, (e) { dragging true; track [{ x: 0, y: 0, t: Date.now() }]; }); document.addEventListener(mousemove, (e) { if (!dragging) return; const offsetX e.clientX - sliderStartX; // 限制不能拖出背景图范围 const clampedX Math.max(0, Math.min(offsetX, maxDragWidth)); puzzle.style.left clampedX px; track.push({ x: clampedX, y: 0, t: Date.now() }); }); document.addEventListener(mouseup, () { if (!dragging) return; dragging false; submit(captchaId, track); });maxDragWidth是背景图宽度减去拼图块宽度防止拖出边界。轨迹上报时 x 用相对初始位置的偏移量服务端比对时也用同样的基准。这里有个坑如果前端做了节流比如每 50ms 才记录一个点那轨迹点数量会偏少可能触发track.size() 10的失败。我一般不在 mousemove 里做节流直接记录提交前如果点太多可以抽样压缩。4. 单元测试怎么写覆盖生成、校验、过期与重放4.1 点击文字验证码的测试用例设计单元测试要覆盖四类场景正常通过、坐标偏差过大、过期失效、重复提交。用 JUnit 5 写不依赖 Spring 容器直接测生成器和校验器。class ClickCaptchaTest { Test void testVerifySuccess() { ClickCaptchaGenerator generator new ClickCaptchaGenerator(); ClickCaptcha captcha generator.generate(); // 从存储里取出正确坐标模拟用户精确点击 ListWordPoint points CaptchaStore.get(captcha.getCaptchaId()); Listint[] clicks points.stream() .map(p - new int[]{p.getX(), p.getY()}) .collect(Collectors.toList()); assertTrue(generator.verify(captcha.getCaptchaId(), clicks)); } Test void testVerifyFailWhenOffsetTooLarge() { ClickCaptchaGenerator generator new ClickCaptchaGenerator(); ClickCaptcha captcha generator.generate(); ListWordPoint points CaptchaStore.get(captcha.getCaptchaId()); // 故意把第一个点偏移 30px超过半径 14px Listint[] clicks points.stream() .map(p - new int[]{p.getX() 30, p.getY()}) .collect(Collectors.toList()); assertFalse(generator.verify(captcha.getCaptchaId(), clicks)); } Test void testVerifyFailAfterExpire() throws InterruptedException { ClickCaptchaGenerator generator new ClickCaptchaGenerator(); ClickCaptcha captcha generator.generate(); // 把过期时间改短或者直接操作存储模拟过期 CaptchaStore.expireNow(captcha.getCaptchaId()); Listint[] clicks List.of(new int[]{100, 100}); assertFalse(generator.verify(captcha.getCaptchaId(), clicks)); } Test void testVerifyFailOnReplay() { ClickCaptchaGenerator generator new ClickCaptchaGenerator(); ClickCaptcha captcha generator.generate(); ListWordPoint points CaptchaStore.get(captcha.getCaptchaId()); Listint[] clicks points.stream() .map(p - new int[]{p.getX(), p.getY()}) .collect(Collectors.toList()); assertTrue(generator.verify(captcha.getCaptchaId(), clicks)); // 第二次提交同一个 captchaId 应该失败 assertFalse(generator.verify(captcha.getCaptchaId(), clicks)); } }testVerifyFailAfterExpire里我用了CaptchaStore.expireNow这个测试辅助方法生产代码里不需要测试时用来模拟过期。如果你用 Redis可以注入一个可控制的时钟或者用Thread.sleep等过期但 sleep 会让测试变慢不推荐。testVerifyFailOnReplay验证的是校验通过后删除存储的逻辑这是防重放的核心。4.2 滑动验证码的轨迹测试与边界用例滑动验证码的测试重点是轨迹校验的边界刚好在偏差范围内、刚好超出、轨迹点太少、耗时过短、速度太匀速。class SlideCaptchaTest { Test void testVerifySuccessWithHumanLikeTrack() { SlideCaptchaGenerator generator new SlideCaptchaGenerator(); SlideCaptcha captcha generator.generate(); int gapX SlideStore.get(captcha.getCaptchaId()).getGapX(); // 构造一条先快后慢的轨迹 ListTrackPoint track new ArrayList(); long start System.currentTimeMillis(); for (int i 0; i 20; i) { double progress 1 - Math.pow(1 - i / 20.0, 2); // 缓出曲线 int x (int) (gapX * progress); track.add(new TrackPoint(x, 0, start i * 50L)); } assertTrue(generator.verify(captcha.getCaptchaId(), track)); } Test void testVerifyFailWhenTooFast() { SlideCaptchaGenerator generator new SlideCaptchaGenerator(); SlideCaptcha captcha generator.generate(); int gapX SlideStore.get(captcha.getCaptchaId()).getGapX(); ListTrackPoint track new ArrayList(); long start System.currentTimeMillis(); // 总耗时 200ms低于 500ms 下限 for (int i 0; i 10; i) { track.add(new TrackPoint(gapX * i / 10, 0, start i * 20L)); } assertFalse(generator.verify(captcha.getCaptchaId(), track)); } Test void testVerifyFailWhenUniformSpeed() { SlideCaptchaGenerator generator new SlideCaptchaGenerator(); SlideCaptcha captcha generator.generate(); int gapX SlideStore.get(captcha.getCaptchaId()).getGapX(); ListTrackPoint track new ArrayList(); long start System.currentTimeMillis(); // 匀速轨迹速度标准差接近 0 for (int i 0; i 20; i) { track.add(new TrackPoint(gapX * i / 20, 0, start i * 50L)); } assertFalse(generator.verify(captcha.getCaptchaId(), track)); } }testVerifySuccessWithHumanLikeTrack用缓出曲线模拟真人拖动progress 1 - (1 - t)^2这个公式让前期移动快、后期慢符合真人操作习惯。testVerifyFailWhenUniformSpeed构造匀速轨迹速度标准差为 0应该被判定为脚本。这三个用例覆盖了滑动验证码校验的主要分支跑通它们基本能保证核心逻辑正确。4.3 测试环境搭建与常见报错处理如果你用 MavenJUnit 5 的依赖这样加dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.10.0/version scopetest/scope /dependencyIDEA 里跑 JUnit 测试常见报错是「No tests found」或者「Test class should have exactly one public constructor」。前者通常是测试类没放在src/test/java下或者方法没加Test后者是测试类构造函数不是无参的。idea怎么写junit单元测试这个搜索背后很多人卡在依赖没加对或者目录结构不对。我一般会先确认pom.xml里junit-jupiter的 scope 是 test然后确认测试类在正确的源目录下最后确认方法签名是void且无参。另一个坑是vue单元测试报错这类前后端联调问题。如果前端用 Vue 写单元测试跑的是前端逻辑和后端 Java 测试是两套东西。后端测试只测生成和校验逻辑前端测试测坐标换算和轨迹采集两边通过接口约定对齐。不要试图在一个测试里同时测前后端那样只会让问题定位变难。5. 避坑与排查那些让我加班到凌晨的细节5.1 坐标偏移前端缩放导致点击永远不中现象用户按提示点击了正确的字但服务端校验一直失败日志里显示点击坐标和记录坐标差了几十像素。原因前端图片被 CSS 缩放展示比如原图 300x180CSS 设成 200x120用户点击的是缩放后的坐标直接回传就和原图坐标对不上。解决前端回传前必须做坐标换算用naturalWidth / rect.width的比例还原。联调时先打印服务端记录坐标和前端回传坐标肉眼比对。我一般会在前端加一个 debug 模式把点击位置画在图上方便确认。5.2 存储未设过期验证码数据越积越多现象服务跑几天后内存占用持续上涨或者 Redis 里 key 数量异常多。原因生成验证码时往存储里写数据但校验失败或用户放弃操作时没有清理数据一直堆积。解决存储必须设 TTL。用 Redis 就SETEX用本地 Map 就加定时清理线程或者用ConcurrentHashMap配合时间戳判断。我一般会在存储层封装一个put方法强制要求传过期时间不传就抛异常从代码层面杜绝忘记设过期。5.3 轨迹校验阈值太严真人用户被误判为脚本现象部分用户反馈「怎么拖都过不了」日志显示轨迹校验失败。原因速度标准差阈值设得太高或者耗时下限设得太长导致一些操作较快的真人用户被误判。解决阈值要基于真实用户数据调。我一般会先上线一个只记录不拦截的版本收集几百条真人轨迹统计速度标准差和耗时的分布取 5% 分位数作为阈值。不要拍脑袋定阈值不同设备、不同网络下差异很大。5.4 拼图块边缘锯齿视觉上像没对齐现象用户觉得拼图块已经对齐了缺口但视觉上总差一点反复微调。原因拼图块是从背景图裁出来的边缘没有做羽化或描边和缺口遮罩叠加时边缘不清晰。解决给拼图块加一圈半透明描边或者对缺口遮罩做羽化处理。我一般会在拼图块边缘画 1px 的白色半透明边框让用户更容易判断对齐位置。这个纯属体验优化不影响校验逻辑但能明显降低用户放弃率。5.5 并发提交同一个 captchaId 被多次校验现象日志里同一个 captchaId 出现多次校验请求有时第一次失败第二次成功。原因前端提交按钮没做防抖用户快速点击多次或者网络重试导致重复提交。解决服务端校验通过后立即删除存储第二次提交会因为找不到数据而失败。前端提交后禁用按钮直到收到响应。另外可以在校验入口加一个基于 captchaId 的分布式锁防止并发校验同一份数据。我一般用 Redis 的SETNX做锁锁的 key 就是 captchaId过期时间设短一点比如 5 秒。6. 进阶技巧把两类验证码组合成风控入口单独用点击文字或滑动验证码能挡住大部分自动化脚本但如果遇到针对性攻击比如有人专门写了识别点击文字坐标的模型或者用模拟真人轨迹的工具单一验证码的防御力会下降。我一般会把两类组合起来用形成一个简单的风控入口。组合策略是这样的第一次请求登录接口时返回滑动验证码如果滑动校验失败或者同一 IP 短时间内请求次数超过阈值下一次返回点击文字验证码如果点击文字也失败就触发短信验证或者直接拒绝。这个策略的核心是「提高攻击成本」让攻击者需要同时破解两类验证码而且失败后惩罚升级。实现上我会在服务端维护一个基于 IP 和用户标识的计数器记录最近一段时间内的验证失败次数。下面是一个简化的风控决策逻辑public CaptchaType decideCaptchaType(String clientId) { int failCount RiskStore.getFailCount(clientId); if (failCount 5) { // 失败次数过多触发短信验证 return CaptchaType.SMS; } else if (failCount 2) { // 失败 2 次以上用点击文字 return CaptchaType.CLICK_WORD; } else { // 默认用滑动 return CaptchaType.SLIDE; } }RiskStore用 Redis 的INCR加EXPIRE实现key 是risk:fail:{clientId}过期时间设 10 分钟。失败时INCR成功时DEL。这个逻辑不复杂但能明显提升防御效果。验证组合策略是否有效我一般会做两件事一是用脚本模拟攻击看连续失败后是否触发升级二是看线上数据统计各类验证码的通过率和失败率如果滑动验证码的失败率突然升高可能是有人在针对性攻击需要进一步分析轨迹特征。最后说个我自己的习惯每次上线新的验证码策略我都会先在小流量上跑一周观察通过率和用户反馈再全量。验证码这东西安全性和体验是一对矛盾阈值调太严用户骂调太松等于没防。我踩过最惨的一次坑是把滑动验证码的耗时下限设成 1000ms结果一批用高性能设备的老用户怎么拖都过不了客服电话被打爆。后来改成 500ms问题才解决。所以阈值一定要基于真实数据调不要凭感觉。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询