☰
基于Spring Boot与Android的垃圾分类系统设计与实现指南
2026/10/15 6:54:43 网站建设 项目流程

1. 先把需求做厚:垃圾分类系统不只是"输名字查分类"

说到"基于Android的环境保护生活垃圾分类系统"这个题目,我先讲个大多数同学的共同经历:拿到题目,满网找源码,找到一份新鲜热乎的,解压之后报错三连——MySQL版本对不上、JDK多打了小数点、模拟器里一片白屏。好不容易跑起来了,发现自己根本讲不清表为什么这么建、接口为什么这么设计,答辩时老师随便一问就露馅。

所以这篇不谈虚的。我把这套系统从需求拆解、后端Spring Boot接口、Android客户端实现,到最后的文档、运行视频和讲解视频都用一遍后的经验盘出来。文章适合两类人看:一是正在找课设/毕设参考的同学,二是已经拿到类似源码、想把它变成"自己真正做过"的项目的人。

1.1 核心场景拆解:谁会用、在什么时候用、解决什么问题

很多同学拿到题目第一反应就是"做增删改查",然后照着网上的后台管理模板套一套。这样做不是不行,但老师看了开头就知道你是在交作业。想把这个题目做出区分度,得先回到真实场景去问三个问题。

第一个问题:谁会用?日常扔垃圾的是居民,他们不会对着长篇大论看,只想知道"手上的东西扔哪个桶"。第二个问题:在什么场景下用?最常见的是做饭时候,菜叶子、骨头、塑料袋混在一起,手机掏出来一搜就知道怎么分;其次是出门丢垃圾时站在垃圾桶前不确定,打开App查一下。第三个问题:系统要为谁的管理工作服务?垃圾分类不能只靠自觉,社区需要有平台去宣传教育、展示积分奖励,让居民愿意参与。

把这几个问题落到功能上,用户端核心就是垃圾搜索、分类库浏览、环保资讯、签到积分,而看不见的管理端,才是真正让项目结构饱满的部分:后台需要能维护垃圾词条、管理资讯、查看用户积分记录、统计搜索热词。整套系统就是从"居民查分类"延伸到"平台促分类"的逻辑链。

1.2 功能清单怎么收敛:七个模块砍到五个

我在做需求分析时,一开始列了七个模块:登录注册、垃圾搜索、分类库、资讯、积分商城、地图回收站定位、拍照识别。后来砍掉了两个,原因很现实。

拍照识别看着很炫,但如果接第三方OCR平台或图像识别服务,需要申请密钥,而且识别的准确率没法保证。课设阶段,评委很可能现场丢一张图让你识别,识别错了整个演示效果就崩了。更关键的是,垃圾分类本身是文本知识匹配,搜"电池"比拍一张电池照片更稳定可靠。

地图回收站定位依赖地图SDK,申请密钥、签名配置都是坑,而且这个功能跟"分类"的核心场景关系不大,属于锦上添花,不属于雪中送炭。

最后保留五个模块:

模块用户端能力管理端能力
登录注册账号密码注册/登录用户列表、状态管理
垃圾搜索输入关键词返回分类结果垃圾词条维护、别名管理
分类知识库按四大类浏览投放说明分类信息维护
环保资讯列表查看资讯详情资讯发布、上下架
积分体系每日签到、积分记录积分流水查询、签到规则配置

这样的好处非常明显:每个模块都能讲清楚"为什么存在"、数据从哪来、由谁维护。答辩时老师问任何一个功能,你都能从场景出发解释清楚,而不是说"网上模板自带的功能"。

1.3 角色与权限:普通用户和管理员的分界线

权限边界是很多课设忽略的地方,但老师特别爱问。这套系统实际拆下来就两种角色:

  • 普通用户(App端):注册登录后能搜索垃圾、查看分类库、签到、浏览资讯、查看自己的积分流水。
  • 管理员(后台接口):登录后能维护垃圾词条和分类、发布资讯、管理用户、查看积分记录。

客户端我只做了用户端,管理端通过后端接口加一个简单的Web页面或者直接用接口调用演示,两种方式我都试过。如果你的题目要求"系统"的完整性,我建议管理端单独做一个很轻的Thymeleaf页面,只是列表和管理表单,工作量不会增加太多,但演示起来比Postman调接口直观得多;如果时间紧,接口加Swagger文档也足够。

权限的实现方式,我用的JWT,后端用一个WebMvcConfigurer注册拦截器,拦截/api/admin/**路径,校验请求头里的Token,顺便校验角色字段。后续第3章会专门讲这块。

2. 技术选型:为什么是Spring Boot + Android,以及老师最爱问的三个细节

技术选型这部分,很多人的做法是"题目写着Spring Boot就用Spring Boot,写着Android就用Android",根本原因说不清楚。我建议你从"为什么是这套组合"和"为什么不是其他方案"两个角度准备,这比背十页八股文有用。

2.1 后端为什么选Spring Boot而不是更轻的方案

Spring Boot的核心价值是把Spring应用的装配成本压到最低。以前用Spring写一个Web项目,要配XML、配数据源、配事务管理器,一不留神版本冲突,课设时间全耗在配置上了。Spring Boot用自动配置和内嵌Tomcat,一个mvn spring-boot:run就能把服务拉起来,最终打成一个可执行Jar包,评委换一台电脑也能直接运行。

我用的版本组合是Spring Boot 2.7.x + JDK 1.8 + MyBatis-Plus 3.5.x + MySQL 8.0。选2.7而不是3.x,主要是稳定性和资料量考虑,2.7的资料在社区里几乎全覆盖,遇到问题一搜就有答案。MyBatis-Plus负责单表CRUD,不用写XML,实体类加几个注解就能直接用BaseMapper提供的方法,核心代码量能省30%以上。

至于为什么不选Node.js或Python Flask:不是不行,而是这个课题既然限定Java方向,用Spring Boot是最稳妥的选择——生态成熟、部署简单、面试和答辩的认可度都高。Flask写起来快,但"为什么不用Java"这个问题会让你在答辩时陷入被动。

2.2 客户端为什么坚持Android原生而不是小程序/H5

Android端选Java原生,不是因为我不会别的,而是这套方案的可解释性最强。课程设计阶段,老师希望看到你对Android基础组件的掌握:Activity、RecyclerView、Adapter、网络请求、数据解析。用Java写原生App,这些知识点都能落到代码里,一张架构图就能把你的技术栈讲清楚。

用H5打包成WebApp确实快,但答辩时老师会问"你的Android体现在哪里",那时候就很难圆场。用小程序同理,和题目要求的"Android"存在偏差。原生虽然多写一些代码,但每一步都是可控的。

依赖层面我的清单很克制:Retrofit 2.9.0做网络请求,OkHttp做底层拦截器,Gson做JSON解析,RecyclerView做列表,Glide做图片加载,ViewBinding代替findViewById。像EventBus、RxJava这种重武器我都没上,加了反而会让代码看起来像"堆依赖",而且自己说不清楚。

2.3 三个必须提前想清楚的选型细节

这套组合跑起来之后,我被追问过三个问题,都是脚手架之外、但又不超纲的细节,提前想清楚特别加分。

第一,JWT和Session的区别是什么?移动端场景下,用户登录后每次请求都要带上身份信息。传统Session把用户状态存在服务器内存里,手机端靠Cookie传SessionId;而JWT把用户ID和过期时间签名后直接发给客户端,服务端不需要存状态,天然适合前后端分离和水平扩展。缺点是服务端无法主动把Token作废,只能等它过期,所以我把过期时间设成7天,并在用户改密码时额外加一个token版本号校验。

第二,为什么存密码用MD5加盐不够,要用BCrypt?很多课设代码还是MD5(password),一查彩虹表就暴露明文。我的做法是Spring Security的BCryptPasswordEncoder,每次加密自动生成随机盐,同一个密码两次加密的结果不同,数据库泄漏也不担心撞库。演示时可以拿这个细节主动讲,老师会觉得你关注过安全问题。

第三,图片识别到底做了没有?我的答复是:预留了扩展接口,但核心业务用文本搜索更可靠。垃圾分类的知识本质上是"名称到类别"的映射,文本搜索就能准确解决;图像识别依赖大量标注数据,而且对光线、角度敏感,作为课设强行接入第三方服务,效果不可控。这个回答既诚实,又体现了你在做技术取舍,而不是不会做。

3. 后端核心设计:从数据库表到接口的一整套可落地方案

后端是整个系统的心脏。这一章我把数据库表结构、统一返回体、拦截器、搜索逻辑和积分设计全部摊开讲,都是能直接抄进项目里的方案。

3.1 六张表如何支撑整套业务

我的数据库设计非常简单,一共六张核心表加两张辅助表。这里贴出核心DDL:

CREATE TABLE category ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT '垃圾大类:可回收/厨余/有害/其他', description TEXT COMMENT '分类投放说明', icon VARCHAR(255) COMMENT '图标路径', sort INT DEFAULT 0, create_time DATETIME, update_time DATETIME ); CREATE TABLE garbage ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL COMMENT '垃圾名称', category_id BIGINT NOT NULL COMMENT '所属大类', keywords VARCHAR(255) COMMENT '别名,逗号分隔', detail TEXT COMMENT '投放提示,如沥干水分、拆除包装', create_time DATETIME, update_time DATETIME ); CREATE TABLE `user` ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), points INT DEFAULT 0 COMMENT '总积分', role TINYINT DEFAULT 1 COMMENT '1用户 2管理员', status TINYINT DEFAULT 1, create_time DATETIME ); CREATE TABLE article ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, content TEXT, cover VARCHAR(255), status TINYINT DEFAULT 1 COMMENT '1发布 0下架', create_time DATETIME ); CREATE TABLE points_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, type TINYINT COMMENT '1签到 2兑换 3后台调整', points INT COMMENT '正负值', remark VARCHAR(255), create_time DATETIME ); CREATE TABLE search_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, keyword VARCHAR(100) NOT NULL, user_id BIGINT, result_type VARCHAR(20), create_time DATETIME );

看一眼就会发现,这套表没有外键约束。为什么?我故意不建物理外键,因为MyBatis-Plus关联查询麻烦,而且课设阶段单表查询配合逻辑外键完全够用,还能少踩一堆外键约束报错的坑。garbage.keywords字段是我比较得意的设计,比如"废旧电池"的别名可以存"电池、干电池、充电电池",搜索"干电池"也能准确命中"有害垃圾",不需要额外建关联表。

search_log这张表很多人会忽略,但它直接支撑了后台的"热门搜索词统计",演示的时候做一个TOP10排行榜,效果远好过普通列表。

3.2 统一返回体、接口路径设计与JWT拦截器

前后端分离的项目,最怕每个接口返回格式不一样。我定义了一个通用的Result<T>类:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "success"; r.data = data; return r; } public static <T> Result<T> error(int code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }

调用方只要约定code == 200就处理data,否则弹错误提示,逻辑非常清晰。

接口路径我遵循REST风格:

  • POST /api/user/register、POST /api/user/login
  • GET /api/garbage/search?keyword=xx
  • GET /api/garbage/list?page=1&size=10&categoryId=xx
  • GET /api/category/list
  • GET /api/article/list、GET /api/article/detail/{id}
  • POST /api/points/sign
  • GET /api/points/records
  • GET /api/admin/stats/hotKeywords(管理端)

JWT拦截器是后端的看门狗。我写了一个TokenInterceptor,在preHandle里从请求头取Authorization,解析Token并放入ThreadLocal,后续代码直接拿当前用户:

@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } try { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); UserContext.set(claims); return true; } catch (Exception e) { response.setStatus(401); return false; } }

注意一个容易忽略的配置:拦截器要排除注册、登录和垃圾查询这些公开接口。不然用户没登录就连垃圾都查不了,那就闹笑话了。

3.3 垃圾分类搜索的核心逻辑与边界处理

垃圾搜索是这个系统的门面,也是答辩时必问的模块。最朴素的实现是WHERE name LIKE '%keyword%' OR keywords LIKE '%keyword%',数据量几百条时性能完全没问题,SQL一眼看懂,我建议第一版就这么做。

但"能搜出来"只是及格线,我的代码里额外做了三个边界处理。

去掉干扰词。用户搜"大骨头"和"猪骨头",本质都是厨余垃圾,但LIKE匹配的命中文案不一样。我的做法是维护一个小型同义词字典,把"骨头、排骨、大骨"归为同一组,搜索时先在同义词表里做归一化。这个字典不需要做得多豪华,几十个高频别名就能覆盖大多数演示场景。

兜底结果。搜索不到时不能让界面弹"无结果"就完事。我返回一条提示记录:"未找到明确分类,请参考分类知识库或咨询当地管理部门",同时在知识库页面给出四大类的指导性说明。这样既诚实又避免了用户的无助感。

记录搜索日志。每次搜索都往search_log插一条记录,这一步看似简单,却让后台有了活力。我做了热门关键词TOP10接口:

@GetMapping("/api/admin/stats/hotKeywords") public Result<List<Map<String, Object>>> hotKeywords() { return Result.success(searchLogMapper.selectHotKeywords()); }

SQL就是GROUP BY keyword ORDER BY COUNT(*) DESC LIMIT 10。演示时先搜几个词,再切到后台看排行榜,那种"前后端数据打通"的感觉,比任何华丽的图表都更能说明系统是完整的。

3.4 签到积分与管理员统计接口的设计

积分模块的设计重点是防止重复签到。我在user表加了last_sign_date字段,每次签到先比对日期:

String today = LocalDate.now().toString(); if (today.equals(user.getLastSignDate())) { return Result.error(400, "今天已经签过到了"); } user.setPoints(user.getPoints() + 2); user.setLastSignDate(today); userService.updateById(user);

规则很简单:每天签到加2分,连续签到7天额外加10分,兑换商品时扣分并插入积分流水。这里的关键不是规则本身,而是积分流水要留痕。不管加分扣分都要在points_record里落一条,后台能查每个人的历史记录。这样答辩时如果老师问"有人恶意刷积分怎么办",你可以回答:流水可回溯,后台可以人工调整分值。

管理员统计接口我做了三个就够用:每日新增用户数、每日搜索次数、热门搜索词TOP10。统计SQL都基于MySQL的DATE_FORMAT(create_time, '%Y-%m-%d')分组,工作量不大,但能让后台不再是干巴巴的CRUD列表。

4. Android客户端实战:网络层封装、状态缓存与联调天坑

后端做得再完整,如果Android端不配合,演示起来照样拉胯。这一章讲客户端我怎么分包、怎么封网络层、搜索页怎么写,最后重点说模拟器和真机联调时必然踩的三个坑。

4.1 工程分包与依赖管理:小项目也要有清晰结构

Android端的包结构我按功能拆:

com.example.garbage ├── activity/ // 登录、注册、主页、搜索、资讯、个人中心 ├── adapter/ // RecyclerView适配器 ├── network/ // Retrofit接口、OkHttp拦截器 ├── model/ // 实体类 ├── ui/ // 自定义View、空态组件 └── utils/ // SharedPreferences工具、防抖工具

依赖文件里我只保留必要的库:

implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' implementation 'com.github.bumptech.glide:glide:4.15.1'

也许你会问为什么不加ViewModel和LiveData?我实测下来,课设项目用Activity + 回调完全能驾驭,引入MVVM全家桶反而要多解释概念。当然,如果项目文档里写了MVVM,那另说;如果没有,就别自找麻烦。

4.2 基于Retrofit + OkHttp的网络层封装

Retrofit定义接口的方式非常直观:

public interface ApiService { @POST("api/user/login") Call<Result<UserInfo>> login(@Body LoginRequest request); @GET("api/garbage/search") Call<Result<List<Garbage>>> search(@Query("keyword") String keyword); @POST("api/points/sign") Call<Result<String>> sign(); }

网络客户端我封装成一个单例,发布到Release时,把BuildConfig.DEBUG的日志拦截器关闭,防止Log信息泄漏:

public class ApiClient { private static final String BASE_URL = "http://10.0.2.2:8080/"; private static ApiService apiService; public static ApiService getInstance() { if (apiService == null) { synchronized (ApiClient.class) { if (apiService == null) { OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new TokenInterceptor()) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .build(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); apiService = retrofit.create(ApiService.class); } } } return apiService; } }

TokenInterceptor是核心,每次请求前从SharedPreferences里取Token塞进请求头:

public class TokenInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); String token = TokenManager.getToken(); if (!TextUtils.isEmpty(token)) { original = original.newBuilder() .header("Authorization", token) .build(); } return chain.proceed(original); } }

注意统一处理401:一旦接口返回401,说明Token过期,直接跳回登录页并清空本地缓存。这个逻辑写在BaseActivity里,所有页面自动生效。

4.3 垃圾搜索页的实现:防抖、空态与分类着色

搜索页是演示中停留时间最长的页面,我做了三个细节让体验接近真实App。

输入防抖。用户每敲一个字就请求一次,很容易造成请求风暴。我用了RxJava里的debounce思路,但为了不引入重依赖,用Handler手动实现:onTextChanged时remove掉之前的回调,500毫秒后重新发起搜索。效果是停顿半秒才发请求,体验顺畅,代码也只有十几行。

空态设计。搜索无结果、列表为空、网络异常,三种状态要分别处理。我专门写了一个EmptyView组件,支持切换图片和提示文案,例如网络异常提示"请检查网络后重试",并带重试按钮。这一眼看上去很细节,其实是Android开发的基础素养,也经常被评委注意到。

分类着色。可回收垃圾用蓝色标签、厨余垃圾用绿色、有害垃圾用红色、其他垃圾用灰色。颜色表达是用户最快理解分类结果的方式。我在GarbageAdapter里根据categoryId设置标签颜色,代码短,视觉效果好。

搜索接口的响应体和后端Result<T>对应,Android端解析时我加了容错:data为null时返回空列表而不是崩溃。

4.4 模拟器与真机联调:三个必然踩到的坑

这三个坑我敢说90%的人第一次跑这个项目都会遇到,提前写在这里,能帮你省一晚上。

坑一:模拟器访问后端不能用localhost。Android模拟器里的localhost指的是模拟器自己,不是你的电脑。宿主机要用10.0.2.2访问。所以后端的application.yml里监听0.0.0.0:8080,Android端BASE_URL写http://10.0.2.2:8080/。如果是真机调试,要把10.0.2.2换成电脑的局域网IP,且手机和电脑连同一个WiFi。

坑二:Android 9及以上默认禁止HTTP明文流量。我第一版跑起来一直报"Cleartext HTTP traffic to 10.0.2.2 not permitted",就是因为没开明文流量。最简单的处理是在AndroidManifest.xml的application标签加:

android:usesCleartextTraffic="true"

坑三:Gson解析Long类型精度丢失。如果后端的ID是雪花ID或超过Java的Integer.MAX_VALUE,Gson默认转成int会溢出。我统一把实体类的ID字段声明为Long,并在GsonBuilder上注册Long的类型适配器,保证后端返回的Long在Android端不丢精度。

联调建议的顺序是:先在模拟器里用10.0.2.2跑通一遍,再换真机用局域网IP跑一遍。两条链路都通,说明网络层没问题,后面演示才稳。

5. 代码之外的交付:文档、运行视频与讲解视频怎么弄

源码能跑只是第一步。我见过太多人源码是能跑,但文档是敷衍的,视频是糊的,讲的时候自己都紧张。这个题目的最终交付物既然是"源码+文档+运行视频+讲解视频",那这几样东西就得当成产品来做,各有各的套路。

5.1 文档目录与写作节奏

文档我不建议一次性从头写到尾,而是按阶段推进,这样内容更真实更扎实:

  • 需求分析文档:在动工前写,内容包括项目背景、用户画像、功能列表、用例图。哪怕用文字表达,也要把"用户能做什么、管理员能做什么"分清楚。
  • 数据库设计文档:表结构确定后立刻写,每个字段的注释写清楚,附ER图和建表SQL。
  • 接口文档:用Knife4j或者Swagger自动生成,再手动补充每个接口的调用示例和错误码含义。
  • 测试文档:最后补,写核心功能的手工测试用例:注册、登录、搜索、签到、管理员操作,每条标注预期结果和实际结果。

5.2 运行视频的录制脚本

运行视频的标准是"一个完全没接触过这个项目的人,照着视频能把系统跑起来"。我录过一版糟糕的,镜头怼着代码一顿操作,看不清点的是什么。后来我固定了脚本,总共4分钟:

时间段内容
0:00-0:30说明环境:JDK版本、MySQL版本、Android Studio版本
0:30-1:30MySQL导入SQL文件,启动后端Spring Boot,确认日志无报错
1:30-2:30Android Studio打开项目,等待Gradle同步,编译安装到模拟器
2:30-4:00演示注册登录、搜索垃圾、分类浏览、签到积分全流程

录屏时把系统的鼠标指针调大,Android Studio的日志窗口稍微拉大一点,让观众清楚看到没有红色的报错堆栈。我还会在视频里把"10.0.2.2"这个地址念一遍,因为这是初学者最容易卡住的地方。

5.3 讲解视频的结构设计

讲解视频和运行视频的最大区别:运行视频是"操作手册",讲解视频是"项目汇报"。我的结构是:

  • 第一段说背景和需求,控制在1分钟,讲清楚"谁用、解决什么问题"。
  • 第二段讲技术架构,用一张自己画的前后端交互图,说明Android端通过网络请求Spring Boot接口,接口读取MySQL数据。这张图上标清楚JWT的流通路径。
  • 第三段做功能演示,挑搜索和签到两个亮点,分别演示正常流程和边界情况(比如搜索无结果、重复签到)。
  • 第四段说不足与展望,例如"当前图像识别功能暂未接入,后续可以扩展"。

讲解视频最忌讳念PPT。哪怕你对着稿子念,也要保持语气像在和同学解释代码,而不是播报新闻。我每次录之前会自己对着摄像头讲两遍烂稿,第三遍才开始正式录,这样语气自然多了。

5.4 答辩前的自我提问清单

最后,把整个项目当成答辩作品来审视。我把评委可能问的问题整理了一份清单,每道题都准备了两层答案:

常见问题回答思路
为什么选Spring Boot?生态成熟、部署方便、Java课设的合理选择
JWT和Session的区别?服务端不存状态、适合前后端分离;缺点无法主动失效
密码怎么存储?BCrypt加盐,不用MD5
垃圾分类结果的数据来源?初期人工整理国家标准分类表,后期用户可以提交纠错
搜索功能在大数据量下会怎样?当前LIKE可满足百级数据量;后续可引入倒排索引或缓存
签到积分怎么防止刷分?按天去重 + 积分流水可追溯

准备这些不是为了背稿,而是强迫自己把项目的每个决策都想明白。想明白之后,评委问什么你都能从容回答。

这个项目我前前后后跑了一个多月,最有价值的体会是:它的技术难度不算高,但"完整性"很磨人——从需求、数据库、接口到Android端再到交付视频,每一环都可能掉链子。如果还想在这个基础上加亮点,我建议优先加离线词库缓存(断网也能查分类)和搜索热词可视化,这两个方向成本低、演示效果好。等你把这条链路完整走一遍,这个题目就不只是"源码+文档+视频",而是你真正能拿得出手的一个作品了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询