Spark外卖数据分析平台实战:从订单表建模到指标可视化

发布时间:2026/10/28 9:17:36
Spark外卖数据分析平台实战:从订单表建模到指标可视化 简介这是基于Spark的外卖大数据平台分析系统毕业设计/课程设计项目面向计算机专业学生及大数据入门学习者旨在解决理论与实践脱节的问题。项目以外卖平台业务为场景系统覆盖该框架的架构组成、内存计算模型、弹性分布式数据集、结构化数据接口、结构化查询引擎、实时流处理以及机器学习库等核心知识点并提供从数据采集、数据清洗、特征工程、模型训练到结果评估的完整开发链路。资源共40个文件以14个Scala源代码文件为主体用于业务分析与算法实现搭配6份Markdown说明文档、4张架构示意图、3个HQL脚本与2个SQL脚本、2个JSON和1个CSV及1个TSV示例数据文件、1个Python辅助脚本及配置脚本等压缩包整体仅645KBMaven工程结构清晰便于按模块查阅和二次开发。目前已有168人学习适合正在筹备毕业设计或课程设计希望快速上手大数据分析、获得完整项目代码与学习路径的读者。1. 一个基于 Spark 的外卖分析平台毕设到底在做什么“基于spark的外卖大数据平台分析系统.zip”这个标题在计算机课程设计里几乎是一个固定搭配外卖订单数据、Spark 离线计算、指标可视化。剥开“大数据平台”这个外壳它真正解决的事只有一件——把一堆每天产生的订单明细整理成“哪个门店卖得多、哪个时段单量高、骑手平均送多久”这类能直接回答业务问题的数字。难点不在于算法多深而在于数据怎么建模、口径怎么统一、任务怎么跑起来不翻车。适合两拨人正在选毕设/课程设计题目、想用 Spark 写一个有真实业务感的项目的学生以及已经会 Spark 基础 API、想补一个完整离线数仓项目的初级工程师。这篇就按“数据结构 → 本地复现 → 指标计算 → 可视化 → 避坑”的顺序把这个 zip 里的项目拆到能直接照做。2. 核心链路外卖订单数据要经过哪几道加工2.1 外卖数据的典型形态订单事实表和维表怎么设计外卖平台离线分析的数据模型最常见做法是星型模型中间一张大订单事实表周围挂用户、门店、骑手、地区维表。订单表核心字段长这样字段类型说明order_idstring订单唯一IDuser_idstring用户IDshop_idstring门店IDrider_idstring骑手IDorder_timetimestamp下单时间finish_timetimestamp送达时间amountdecimal(10,2)订单金额statusint订单状态1完成 2取消 3配送中region_idstring区域IDdtstring按天分区yyyy-MM-dd维表就是 user_info、shop_info、rider_info一般只放 ID、名称、分类这类稳定属性。这个数据模型的价值在于事实表只记“发生了一次交易”维表负责解释“是谁、在哪、哪类门店”。分析需求变化时不需要反复改事实表join 维表就能得到新维度。数据量不需要很大。真正影响面试和答辩观感的是有没有分区概念dt 分区让每日增量任务只处理当天数据而不是每次全表扫描。有人把 dt 当普通字段存结果跑一个月全量数据时每一批任务都在扫 30 天的表集群再大也经不住这么烧。设计时我一般会把 order_time 和 finish_time 都存原生时间戳同时加 dt 冗余分区列。这样做既保留精确时间用于算配送时长又让分区裁剪生效两类需求都不耽误。真实外卖数仓还会把一些高频过滤维度如 status、region_id 做二级分区但课程设计有 dt 足够了。2.2 为什么选 Spark 而不是 Hive 或 Flink在课程设计里选型理由是要写进论文里的。离线 T1 指标的常见路线有三条Hive on MR、Flink 流处理、Spark 批处理。Hive 最贴近传统数仓但底层 MapReduce 每次 shuffle 都要落盘迭代计算性能不如 Spark。外卖平台日订单量在几十万到百万级Hive 能跑但“能不能跑”和“跑得爽不爽”是两回事。尤其做用户复购这类多阶段聚合时MR 的每个中间结果都写磁盘效率差距被放大。Flink 确实火但它主战场是实时指标——实时大屏、实时告警。课程设计如果只做离线报表用 Flink 会面临“实时数据源从哪来”的问题。没有 Kafka 和持续运行的 JobFlink 的优势根本体现不出来反而把自己搞进一个黑匣子里出了问题都不知道去哪看日志。Spark 的定位正好卡在中间RDD/DataFrame 把中间结果留在内存迭代效率高Spark SQL 的语法和 Hive 高度接近论文里写“兼容 Hive 数仓体系”是成立的再加上 Spark 天然吃 Parquet 列式存储统计类查询触发列裁剪后读的数据量是行式存储的三分之一不到。课程设计用 Spark性价比最高。有个常见误区是“Spark 比 Hive 快所以用了 Spark 就完事”。实际项目里跑得慢往往不是引擎的问题而是分区设计不合理、join 键有倾斜、小文件太多。引擎能救算法救不了烂表结构。这个结论在答辩时比单纯背“Spark 基于内存计算”要有说服力得多。2.3 分层加工从 ODS 到 ADS层次和口径一起定数仓分层在外卖场景下的常见做法是四层ODS原始订单层CSV/JSON 导入后不加工只加 dt 分区。DWD清洗层。过滤 status2 的取消订单把时间字段统一成 timestamp修正异常金额。DWS明细聚合层。按“小时 门店 区域”做预聚合算出单量、GMV、配送时长均值。ADS应用层。面向报表要什么出什么比如 Top10 门店日销榜、用户复购率。分层最大的收益不是“看起来专业”而是口径有地方落。很多翻车现场是这样的看板上的“订单量100万”论文里的“订单量98万”因为一个过滤了取消单一个没过滤。分层以后把“有效订单数 status1 且 amount0”这类口径写在 DWD 层的加工说明里下游所有指标都从 DWD 取数数字才能对得上。在 ADS 层我习惯把“口径说明”做成一张 metric_dict 表字段就是 metric_name、metric_definition、calculation_sql跟着数据一起输出。答辩或者交接时直接看表就知道每个数字怎么来的不用翻代码。这个习惯救过我很多次因为人很容易高估自己一星期后还能记住当时的计算逻辑。3. 本地复现最小集群、造数脚本和第一批指标3.1 最小环境Spark Standalone 的启动与内存参数拿到 zip 后第一件事不是翻代码而是把 Spark 跑起来。课程设计最常见的部署是 Spark Standalone不需要 Yarn 也不需要 Hadoop HDFS除非老师明确要求一台笔记本就能把 master 和 worker 都起在同一台机器上。如果你用的是 Spark 3.x解压后进入 sbin 目录最简启动是这样# 启动 master默认监听 7077RPC和 8080Web UI ./sbin/start-master.sh -h localhost -p 7077 # 启动一个 worker把资源挂到 master 上 ./sbin/start-worker.sh spark://localhost:7077 -c 2 -m 2g # 用 spark-shell 验证连通性 ./bin/spark-shell --master spark://localhost:7077 --executor-memory 1g这里有几个参数要说明。-c 2是给 worker 分配 2 个 CPU core-m 2g是 worker 最多使用 2GB 内存。spark-shell --executor-memory 1g设定 executor 的堆内内存。笔记本上跑小数据量时executor 内存设 1g~2g 就够不要一上来给 8g后面避坑章节会解释为什么内存并非越大越好。启动后打开 http://localhost:8080能看到 worker 的 CPU、内存和运行中 Application。如果 8080 打不开多数是 master 没起成功去 logs 目录看spark-org.apache.spark.deploy.master.Master-*.out的报错。shell 里能执行sc.parallelize(1 to 10).sum()说明环境已经通了。真实生产环境一般走 Yarn 或 K8s 调度但课程设计不必上。先把 Standalone 跑通把提交命令里的--master换成yarn就是集群模式这个演进思路写在论文里反而是加分项。3.2 订单造数脚本先造一份可复现的数据外卖订单数据涉及业务隐私课程设计基本拿不到真实数据所以造数脚本是这个 zip 里的关键一环。造数的目标不是“像真的”而是“口径可验证”——固定随机种子后每次生成的数完全一样指标才能被复现和核对。下面这个 Python 脚本生成 30 天、每天约 5000 条订单落成 CSVimport random import datetime import csv # 固定种子保证可复现 random.seed(42) start_date datetime.date(2024, 1, 1) status_pool [1, 1, 1, 2] # 25% 取消单测试过滤逻辑 with open(orders.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([order_id, user_id, shop_id, rider_id, order_time, finish_time, amount, status, dt]) for day_offset in range(30): day start_date datetime.timedelta(daysday_offset) for i in range(5000): order_id f{day:%Y%m%d}{i:05d} user_id fu{random.randint(1, 500):04d} shop_id fs{random.randint(1, 100):03d} rider_id fr{random.randint(1, 200):03d} order_time datetime.datetime( day.year, day.month, day.day, random.randint(8, 23), random.randint(0, 59)) amount round(random.uniform(15, 80), 2) status random.choice(status_pool) finish_time order_time datetime.timedelta( minutesrandom.randint(15, 60)) writer.writerow([order_id, user_id, shop_id, rider_id, order_time.strftime(%Y-%m-%d %H:%M:%S), finish_time.strftime(%Y-%m-%d %H:%M:%S), amount, status, f{day:%Y-%m-%d}]) print(orders.csv generated)参数说明random.seed(42)是整份数据可复现的关键没有它每次生成的结果都不一样后面任何指标都没法对账status_pool里故意放了 status2 的取消单是为了让 DWD 层的过滤逻辑有数据可验证amount 范围 15~80 模拟外卖客单价。如果想模拟数据倾斜把 shop_id 改成random.choices([s001, s002], weights[100, 1], k1)[0]这类写法后面调倾斜时候会有用。数据文件生成后用wc -l orders.csv确认总行数这个数字要和后续 Spark 统计结果对得上。3.3 第一批指标用 PySpark 计算“按小时订单量”环境通了、数据有了接下来就是第一个真正的分析任务。这里用 DataFrame API 做最经典的“按小时统计订单量”顺便演示分区裁剪。from pyspark.sql import SparkSession from pyspark.sql.functions import to_timestamp, hour, col, count spark SparkSession.builder \ .master(spark://localhost:7077) \ .appName(delivery_hourly_orders) \ .config(spark.sql.shuffle.partitions, 4) \ .getOrCreate() df spark.read.option(header, True).csv(orders.csv) df df.withColumn(order_ts, to_timestamp(col(order_time), yyyy-MM-dd HH:mm:ss)) df df.filter(col(dt) 2024-01-01) df df.filter(col(status) 1) hourly df.groupBy(hour(col(order_ts)).alias(hour)) \ .agg(count(*).alias(order_cnt)) \ .orderBy(hour) hourly.coalesce(1).write.mode(overwrite).parquet(output/hourly_orders) hourly.show() spark.stop()逻辑说明to_timestamp把字符串时间列解析成真正的 timestamp后续取小时、算配送时长都靠它filter(status 1)是 DWD 层的第一个清洗动作把取消单挡在指标外面groupBy(...).agg(count(...))做分组计数orderBy保证输出按小时排序最后coalesce(1)是因为数据量很小合并成一个分区方便看结果文件。如果订单数据是服务端埋点的 JSON 格式用spark.read.json(orders.json)即可schema 自动推断嵌套字段要用col(extra.user_id)这种点号路径取。真实项目里 JSON 比 CSV 更常见但 CSV 胜在轻量、便于检查课程设计够用。几个参数值得记住spark.sql.shuffle.partitions4控制 groupBy 之后 shuffle 出来的分区数默认 200 在本地小数据上会白白生成 200 个小文件mode(overwrite)表示重复跑任务时覆盖上次输出这是开发期的设定生产环境常用append加分区master(spark://localhost:7077)明确走 Standalone 集群而不是 local[*]这样 Web UI 才能看到任务进度答辩时也更有说服力。3.4 同样的指标用 Spark SQL 再做一遍DataFrame API 和 Spark SQL 是同一套 Catalyst 优化引擎只是写法不同。很多课程设计代码里两种混着用这没问题但要清楚各自适合的场景聚合逻辑复杂、join 多时SQL 更容易维护和讲清楚需要写动态过滤条件时DataFrame 更灵活。下面把上面的指标改成 SQLCREATE OR REPLACE TEMP VIEW orders_v AS SELECT *, to_timestamp(order_time, yyyy-MM-dd HH:mm:ss) AS order_ts FROM orders_csv; SELECT hour(order_ts) AS hour, count(*) AS order_cnt FROM orders_v WHERE status 1 AND dt 2024-01-01 GROUP BY hour(order_ts) ORDER BY hour;在 PySpark 里执行这段 SQL要先注册视图orders_csv.createOrReplaceTempView(orders_csv)然后spark.sql(...)直接跑。SQL 版的优势是 WHERE 条件一目了然数据清洗逻辑评审起来非常直观DataFrame 版的优势是写参数化过滤日期时不用拼 SQL 字符串。有一点坑要注意hour(order_ts)在 Spark SQL 里返回的是当前会话时区的小时数而不是数据里隐含的时区。如果造数脚本生成的是 UTC 时间本地会话默认 Asia/Shanghai统计出来的“凌晨时段”会整体偏移 8 小时。解法是显式设置spark.conf.set(spark.sql.sessionTimeZone, Asia/Shanghai)并且建议造数脚本直接生成东八区时间从源头消掉歧义。4. 从聚合结果到可视化Spark SQL 与数据大屏怎么接4.1 三类典型指标门店日销、骑手时效、用户复购大部分外卖分析系统的业务指标可以归成三类卖了多少、送得多快、客人是否再来。对应的计算逻辑各有各的坑我一个个说。门店日销按门店 日期分组求 GMV 和订单数。SQL 里需要 join shop_info 维表才能把“门店名”写进结果否则只输出 shop_id图表上没法看。CREATE OR REPLACE TEMP VIEW dwd_orders AS SELECT *, to_timestamp(order_time, yyyy-MM-dd HH:mm:ss) AS order_ts, to_timestamp(finish_time, yyyy-MM-dd HH:mm:ss) AS finish_ts FROM orders_csv WHERE status 1; SELECT t.shop_id, t.dt, sum(t.amount) AS gmv, count(*) AS order_cnt FROM dwd_orders t GROUP BY t.shop_id, t.dt ORDER BY gmv DESC;这段 SQL 的关键是先在视图层把 status1 过滤掉下游所有指标都不必再关心取消单。sum(t.amount)算 GMVcount(*)算订单量这两个口径必须和 metric_dict 表里的定义一致。骑手时效核心是“送达时间 - 下单时间”。注意要先过滤取消单和 finish_time 为空的异常单再用unix_timestamp(finish_ts) - unix_timestamp(order_ts)得到秒数最后换算成分钟。如果数据里出现负数说明造数脚本或真实数据里有“送达早于下单”的脏数据必须回到清洗层处理而不是在指标层临时加 abs()。用户复购稍微绕一点。先按 user_id dt 去重得到“每日活跃购买用户”再统计“在 7 天窗口内活跃天数 2 天”的用户数。用两级 groupBy 就能算出来SELECT count(*) AS repurchase_users FROM ( SELECT user_id FROM dwd_orders WHERE dt 2024-01-01 AND dt 2024-01-07 GROUP BY user_id HAVING count(DISTINCT dt) 2 ) t;逻辑说明内层先按“用户 日期”粒度做 groupBycount(DISTINCT dt)算的是活跃天数而不是购买次数同一天买两次只算一天外层再统计满足条件的用户数。这个口径要写在代码注释里否则答辩时被问到“为什么这个人买三次不算复购”很容易卡住。复购周期也可以做成参数比如 7 天、30 天不同的业务定义会得到完全不同的数字这正是数据项目里“口径比代码更重要”的典型例子。4.2 结果写到哪里Parquet、MySQL 还是 ClickHouse可视化层通常不直接读 Parquet而是走后端接口。最常见做法是把聚合结果写入 MySQL后端用 SQL 查询。小数据量项目这样最省事不需要引入额外 OLAP 引擎。df.write.mode(overwrite) \ .format(jdbc) \ .option(url, jdbc:mysql://localhost:3306/delivery_analysis) \ .option(dbtable, ads_shop_daily) \ .option(user, root) \ .option(password, your_password) \ .option(driver, com.mysql.cj.jdbc.Driver) \ .save()这段代码会把 Spark 聚合结果直接写进 MySQL 表。JDBC 写入比较慢数据量大时会成为瓶颈但课程设计到百万行以内没什么问题。参数里注意rewriteBatchedStatementstrue通常要拼到 JDBC URL 上否则 Spark 走单条 insert速度差一个数量级。真实外卖平台会用 ClickHouse 或 Doris 做 OLAP 加速课程设计里不必上。如果老师追问“为什么不用 ClickHouse”可以答当前数据量 MySQL 足够项目聚焦 Spark 离线加工链路引入更多组件会增加部署复杂度和答辩风险。这个回答比“还没学”成熟得多也显得你考虑过边界。4.3 数据大屏的最小实现FastAPI ECharts可视化层很多同学直接套一个现成后台模板把表格填进去。想加分的话做一个简单的数据大屏左边热销门店 Top10中间订单趋势折线右边骑手时效分布。这里的关键是后端把 Spark 结果包装成 JSON。用 FastAPI 读 Parquet 再返回核心代码很短from fastapi import FastAPI import pandas as pd app FastAPI() app.get(/api/shop_top10) def shop_top10(): df pd.read_parquet(output/shop_daily_top10.parquet) result df.to_dict(orientrecords) return {code: 0, data: result}前端用 ECharts 的折线图承接“每日订单量”接口fetch(/api/daily_orders) .then(res res.json()) .then(res { const dates res.data.map(d d.dt); const counts res.data.map(d d.order_cnt); myChart.setOption({ xAxis: { data: dates }, series: [{ type: line, data: counts }] }); });说明pd.read_parquet需要pyarrow或fastparquet库Parquet 目录里有 Spark 生成的_SUCCESS和.crc文件Pandas 读取时直接指向目录即可它会自动跳过。可视化层不要堆砌太多动画和地图面试官真正关心的是你从 Spark 算出来的数字有没有被正确展示出来。图表上最好标注“统计口径有效订单status1”这句话比任何炫酷特效都加分。5. 外卖场景的避坑清单五个反复踩的现场问题5.1 订单时间字符串导致按小时聚合结果对不上现象数字看着合理但凌晨单量整体偏大和业务常识不符。原因八成是时区问题。造数脚本按东八区生成时间Spark 会话默认spark.sql.sessionTimeZone跟随系统时区如果系统是 UTChour(order_ts)会把 08:00 变成 00:00 统计进凌晨。解决统一显式设置spark.conf.set(spark.sql.sessionTimeZone, Asia/Shanghai)并且在造数脚本里直接生成东八区时间字符串避免二次换算。开发机器和集群时区不一致时这个坑必然发生最好在提交脚本里固定时区不要依赖系统默认值。5.2 数据倾斜一个爆款门店把整个任务拖住现象任务跑到某个 stage 卡住日志里看到某个 executor 长时间 GC其他 executor 早早空闲。原因外卖订单天然集中——头部门店的订单可能占全量 10%。groupBy shop_id时同一个 key 的全部数据被 shuffle 到同一个 task单 task 处理量远超均值。解决常见做法是“两阶段聚合 加盐”。先给 shop_id 拼一个随机后缀分散到多个 task 做部分聚合再去掉后缀做最终聚合from pyspark.sql.functions import concat, lit, rand, substring, col, sum # 第一阶段加盐打散热点 key salted df.withColumn(salt, (rand() * 10).cast(int)) \ .withColumn(shop_salted, concat(col(shop_id), lit(_), col(salt))) partial salted.groupBy(shop_salted, dt).agg(sum(amount).alias(part_gmv)) # 第二阶段去掉盐合并结果 final partial.withColumn(shop_id, substring(col(shop_salted), 1, 4)) \ .groupBy(shop_id, dt).agg(sum(part_gmv).alias(gmv))注意加盐只解决热点 key 的 shuffle 倾斜。如果倾斜出在 join 上比如大表和小表 join 且 key 集中优先考虑broadcast join把小表广播到每个 executor直接跳过 shuffle。spark.sql.autoBroadcastJoinThreshold默认 10MB小维表默认就能广播别手动关掉它。5.3 Spark OOMExecutor 内存设得越大越容易翻车现象把--executor-memory提到 8g、16g任务反而 OOM 更频繁。原因Spark 内存里有默认spark.memory.fraction0.6的“执行/存储共享区”剩下 0.4 留给用户代码和元数据。内存调大了但分区数没变单个 task 拉进内存的数据量不变甚至更多GC 压力反而上升。解决先看数据量再定内存。本地课程设计百万行以内--executor-memory 2g足够集群模式下按“单分区数据量预估 executor 内存”的通用原则来配置。出现 OOM 时先查spark.sql.shuffle.partitions和spark.memory.fraction而不是直接加内存。加内存是玄学调分区是科学。5.4 本地能跑提交到集群后结果对不上现象本地程序统计的订单量是 98 万集群跑出来是 101 万再跑一次又变 102 万。原因常见有三类。一是造数脚本里用了random但没有固定种子每次生成的数据不一样二是 SQL 里用了rand()、current_date()这类非确定性函数导致多次执行结果不一致三是文件处理顺序不固定——CSV 读入后分区不保证有序某些 topN 临界值会随分区不同而变化。解决造数固定随机种子把current_date()这类函数换成任务参数传入的固定日期对需要稳定结果的指标先orderBy再limit。答辩前用同一份出厂数据跑两遍结果必须一致这是数据项目最基本也最容易被问到的点。5.5 答辩被问“指标为什么是这个数”答不上来现象代码能跑、图表能展示但评委随口问“这个 gmv 为什么是 80 万”当场卡壳。原因统计口径只存在于代码注释里没有沉淀成独立可查的文档造数脚本又不可复现数字无法对账。解决做两件事。第一ADS 层每个指标配一条口径记录写成 metric_dict 表订单量 状态为 1 的订单数GMV 状态为 1 且金额 0 之和。第二造数脚本固定种子并保留生成参数随时可以“从数据集到指标重跑一遍”。答辩时给出“口径表 可复现脚本”比现场看代码再拍脑袋解释可靠得多。6. 让它成为真正的毕设项目三件花小钱办大事的验证6.1 加一个调度脚本让任务“每天自动跑”课程设计交上去如果能“一键跑完”说服力其实不够强。给它加一层调度哪怕是最简单的 crontab就把批处理项目的核心逻辑补上了0 2 * * * cd /home/user/spark-delivery ./bin/spark-submit \ --master spark://localhost:7077 \ --executor-memory 2g \ daily_orders_job.py --date$(date -d yesterday \%Y-\%m-\%d)这段脚本说明“每天凌晨 2 点自动对前一天的数据做加工”。配合--date参数演示时还能现场回算任意一天的指标。这个“参数化日期 定时调度”的组合比把日期写死在代码里高级一个档次。6.2 做一次“回算验证”用同一天的数据对账我一般在答辩前做一次回算从 ODS 原始数据开始把某一天的数据重新跑全链路然后和 ADS 层已经落库的报表对比。两次数字一致说明数据链路是闭环的不一致说明中间某个环节有状态依赖或非确定性逻辑。把这个过程写进项目说明面试时可以直接说“我有数据校验的意识和实操”这句话比“我熟悉 Spark”值钱得多。6.3 把踩坑写进 README形成“过程文档”比“运行说明”更有价值的是把踩坑记录整理成“问题 - 原因 - 解决”小节。比如时区偏移、数据倾斜、OOM每个问题配一段现象描述和一段解决代码。这既是写给人看的文档也是自己复盘的过程。空机器上能不能按 README 跑通是检验文档质量的唯一标准。最后说个自己之前的教训第一次做这种项目时我把“取消订单”忘了过滤导致 GMV 虚高幸好回算时及时发现。所以我现在交任何数据项目前都会先问三个问题指标口径有没有文档数据能不能重跑复现拿到一台空机器能不能按 README 跑通这三件事做完项目才算真正收尾。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询