
最近在开发漫画阅读类应用时发现很多开发者对多端适配和前后端分离架构的实现存在困惑。本文将以漫小天漫画阅读平台为例完整分享基于SpringBoot3Vue3的全栈开发方案涵盖Web端、微信小程序端的统一架构设计。这套方案采用前后端分离模式后端使用SpringBoot3提供统一的REST API前端通过Vue3实现Web管理端微信小程序端则基于uni-app框架开发。学完本文后你将掌握多端漫画平台的核心开发技能能够独立完成从数据库设计到前后端联调的完整流程。1. 项目架构与技术选型1.1 整体架构设计漫小天漫画平台采用典型的前后端分离架构后端API服务统一为Web端和小程序端提供数据支持。这种架构的优势在于业务逻辑统一、便于维护和扩展。前端层用户界面 ├── Web管理端Vue3 Element Plus - 供管理员使用 └── 微信小程序端uni-app Vue3 - 供终端用户使用 后端层业务逻辑 └── SpringBoot3 API服务 ├── 用户管理模块 ├── 漫画管理模块 ├── 阅读记录模块 └── 文件存储模块 数据层数据持久化 └── MySQL Redis缓存1.2 技术栈说明后端技术栈SpringBoot 3.x现代Spring框架支持JDK17MyBatis Plus 3.5简化数据库操作MySQL 8.0主数据库Redis 7.0缓存和会话管理JWT身份认证Maven依赖管理前端技术栈Vue 3.2组合式API更好的TypeScript支持Vite 4.0快速构建工具Element PlusUI组件库AxiosHTTP客户端Vue Router路由管理小程序技术栈uni-app 3.0跨端开发框架Vue 3语法一致性uni-ui小程序UI组件2. 开发环境准备2.1 基础环境配置在开始项目开发前需要确保本地环境满足以下要求操作系统要求Windows 10/11 或 macOS 10.15 或 Ubuntu 18.04至少8GB内存推荐16GB至少20GB可用磁盘空间开发工具安装# 安装Node.js前端开发 node -v # 要求版本16.0 npm -v # 要求版本8.0 # 安装Java开发环境 java -version # 要求JDK17 mvn -v # 要求Maven 3.6 # 安装数据库 mysql --version # 要求MySQL 8.0 redis-server --version # 要求Redis 7.02.2 IDE和工具配置推荐使用以下开发工具组合后端开发IntelliJ IDEA Ultimate强大的Java IDE安装插件Lombok、MyBatisX、Spring Assistant前端开发VS Code轻量级代码编辑器安装扩展Volar、TypeScript Vue Plugin、Element Plus Helper小程序开发HBuilder Xuni-app官方IDE微信开发者工具小程序调试和发布3. 数据库设计与建模3.1 核心表结构设计漫画平台的核心业务涉及用户、漫画、章节、阅读记录等实体以下是关键表的设计-- 用户表 CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(200) DEFAULT NULL COMMENT 头像, phone varchar(20) DEFAULT NULL COMMENT 手机号, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 漫画表 CREATE TABLE comic ( id bigint NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 漫画标题, author varchar(50) DEFAULT NULL COMMENT 作者, cover_image varchar(200) DEFAULT NULL COMMENT 封面图, description text COMMENT 描述, category_id bigint DEFAULT NULL COMMENT 分类ID, status tinyint DEFAULT 1 COMMENT 状态1-连载中 2-已完结, view_count int DEFAULT 0 COMMENT 浏览量, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 漫画章节表 CREATE TABLE comic_chapter ( id bigint NOT NULL AUTO_INCREMENT, comic_id bigint NOT NULL COMMENT 漫画ID, chapter_number int NOT NULL COMMENT 章节编号, title varchar(100) NOT NULL COMMENT 章节标题, page_count int DEFAULT 0 COMMENT 页数, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_comic_id (comic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 阅读记录表 CREATE TABLE reading_history ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 用户ID, comic_id bigint NOT NULL COMMENT 漫画ID, chapter_id bigint NOT NULL COMMENT 章节ID, current_page int DEFAULT 1 COMMENT 当前阅读页数, read_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 阅读时间, PRIMARY KEY (id), KEY idx_user_comic (user_id,comic_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 索引优化策略为了提高查询性能需要为常用查询字段添加合适的索引-- 为漫画表添加分类索引 ALTER TABLE comic ADD INDEX idx_category_status (category_id, status); -- 为章节表添加漫画和章节号索引 ALTER TABLE comic_chapter ADD INDEX idx_comic_chapter (comic_id, chapter_number); -- 为阅读记录表添加时间索引 ALTER TABLE reading_history ADD INDEX idx_user_time (user_id, read_time);4. SpringBoot3后端实现4.1 项目结构规划采用标准的分层架构确保代码的可维护性src/main/java/com/mantian/comic/ ├── config/ # 配置类 ├── controller/ # 控制层 ├── service/ # 业务层 ├── mapper/ # 数据访问层 ├── entity/ # 实体类 ├── dto/ # 数据传输对象 ├── common/ # 通用组件 └── Application.java # 启动类4.2 核心依赖配置在pom.xml中配置SpringBoot3和必要依赖?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.0.0/version relativePath/ /parent groupIdcom.mantian/groupId artifactIdcomic-platform/artifactId version1.0.0/version properties java.version17/java.version mybatis-plus.version3.5.3/mybatis-plus.version /properties dependencies !-- Spring Boot Starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Redis -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- JWT -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency /dependencies /project4.3 数据层实现使用MyBatis Plus简化数据库操作首先配置实体类// 漫画实体类 Data TableName(comic) public class Comic { TableId(type IdType.AUTO) private Long id; private String title; private String author; private String coverImage; private String description; private Long categoryId; private Integer status; private Integer viewCount; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; } // Mapper接口 public interface ComicMapper extends BaseMapperComic { Select(SELECT * FROM comic WHERE category_id #{categoryId} ORDER BY view_count DESC LIMIT #{limit}) ListComic selectHotComicsByCategory(Param(categoryId) Long categoryId, Param(limit) Integer limit); }4.4 业务层实现实现漫画相关的业务逻辑Service public class ComicService { Autowired private ComicMapper comicMapper; Autowired private RedisTemplateString, Object redisTemplate; public PageComic getComicList(ComicQueryDTO queryDTO) { PageComic page new Page(queryDTO.getPageNum(), queryDTO.getPageSize()); LambdaQueryWrapperComic wrapper new LambdaQueryWrapper(); if (StringUtils.isNotBlank(queryDTO.getKeyword())) { wrapper.like(Comic::getTitle, queryDTO.getKeyword()); } if (queryDTO.getCategoryId() ! null) { wrapper.eq(Comic::getCategoryId, queryDTO.getCategoryId()); } wrapper.orderByDesc(Comic::getViewCount); return comicMapper.selectPage(page, wrapper); } Cacheable(value comic, key #id) public Comic getComicDetail(Long id) { Comic comic comicMapper.selectById(id); if (comic ! null) { // 增加浏览量 comicMapper.updateViewCount(id); } return comic; } }4.5 控制层实现提供REST API接口RestController RequestMapping(/api/comic) Validated public class ComicController { Autowired private ComicService comicService; GetMapping(/list) public ResultPageComic getComicList(Valid ComicQueryDTO queryDTO) { PageComic page comicService.getComicList(queryDTO); return Result.success(page); } GetMapping(/detail/{id}) public ResultComic getComicDetail(PathVariable Long id) { Comic comic comicService.getComicDetail(id); return Result.success(comic); } PostMapping(/{id}/view) public ResultVoid increaseViewCount(PathVariable Long id) { comicService.increaseViewCount(id); return Result.success(); } } // 统一返回结果封装 Data public class ResultT { private Integer code; private String message; private T data; private Long timestamp; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); result.setTimestamp(System.currentTimeMillis()); return result; } }5. Vue3前端管理端实现5.1 项目初始化使用Vite创建Vue3项目npm create vuelatest comic-admin cd comic-admin npm install安装必要依赖npm install element-plus element-plus/icons-vue npm install axios vue-router4 pinia npm install sass -D5.2 路由配置配置前端路由// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Dashboard, component: () import(/views/Dashboard.vue), meta: { title: 仪表板 } }, { path: /comic, name: Comic, component: () import(/views/comic/ComicList.vue), meta: { title: 漫画管理 } }, { path: /comic/add, name: ComicAdd, component: () import(/views/comic/ComicAdd.vue), meta: { title: 添加漫画 } }, { path: /comic/edit/:id, name: ComicEdit, component: () import(/views/comic/ComicEdit.vue), meta: { title: 编辑漫画 } } ] const router createRouter({ history: createWebHistory(), routes }) export default router5.3 状态管理使用Pinia进行状态管理// stores/comic.js import { defineStore } from pinia export const useComicStore defineStore(comic, { state: () ({ comicList: [], currentComic: null, loading: false, pagination: { page: 1, pageSize: 10, total: 0 } }), actions: { async fetchComicList(params {}) { this.loading true try { const response await api.getComicList({ page: this.pagination.page, pageSize: this.pagination.pageSize, ...params }) this.comicList response.data.records this.pagination.total response.data.total } catch (error) { console.error(获取漫画列表失败:, error) } finally { this.loading false } }, async fetchComicDetail(id) { try { const response await api.getComicDetail(id) this.currentComic response.data } catch (error) { console.error(获取漫画详情失败:, error) } } } })5.4 漫画列表组件实现漫画管理界面template div classcomic-list el-card template #header div classcard-header span漫画管理/span el-button typeprimary clickhandleAdd添加漫画/el-button /div /template !-- 搜索条件 -- el-form :modelqueryParams inline el-form-item label关键词 el-input v-modelqueryParams.keyword placeholder请输入漫画标题 / /el-form-item el-form-item label分类 el-select v-modelqueryParams.categoryId placeholder请选择分类 el-option label全部 value / el-option v-forcategory in categoryList :keycategory.id :labelcategory.name :valuecategory.id / /el-select /el-form-item el-form-item el-button typeprimary clickhandleSearch搜索/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form !-- 数据表格 -- el-table :datacomicStore.comicList v-loadingcomicStore.loading el-table-column propid labelID width80 / el-table-column propcoverImage label封面 width100 template #default{ row } el-image :srcrow.coverImage :preview-src-list[row.coverImage] fitcover stylewidth: 60px; height: 80px; / /template /el-table-column el-table-column proptitle label标题 min-width200 / el-table-column propauthor label作者 width120 / el-table-column propviewCount label浏览量 width100 / el-table-column propstatus label状态 width100 template #default{ row } el-tag :typerow.status 1 ? success : info {{ row.status 1 ? 连载中 : 已完结 }} /el-tag /template /el-table-column el-table-column propcreateTime label创建时间 width180 / el-table-column label操作 width200 fixedright template #default{ row } el-button sizesmall clickhandleEdit(row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(row)删除/el-button /template /el-table-column /el-table !-- 分页 -- div classpagination el-pagination v-model:current-pagecomicStore.pagination.page v-model:page-sizecomicStore.pagination.pageSize :totalcomicStore.pagination.total current-changehandlePageChange layouttotal, sizes, prev, pager, next, jumper / /div /el-card /div /template script setup import { onMounted, reactive } from vue import { useRouter } from vue-router import { useComicStore } from /stores/comic import { ElMessage, ElMessageBox } from element-plus const router useRouter() const comicStore useComicStore() const queryParams reactive({ keyword: , categoryId: }) onMounted(() { comicStore.fetchComicList() }) const handleSearch () { comicStore.pagination.page 1 comicStore.fetchComicList(queryParams) } const handleReset () { Object.keys(queryParams).forEach(key { queryParams[key] }) handleSearch() } const handleAdd () { router.push(/comic/add) } const handleEdit (row) { router.push(/comic/edit/${row.id}) } const handleDelete async (row) { try { await ElMessageBox.confirm(确定删除该漫画吗, 提示, { type: warning }) // 调用删除API await api.deleteComic(row.id) ElMessage.success(删除成功) comicStore.fetchComicList() } catch (error) { if (error ! cancel) { ElMessage.error(删除失败) } } } const handlePageChange (page) { comicStore.pagination.page page comicStore.fetchComicList(queryParams) } /script6. uni-app微信小程序端开发6.1 项目创建与配置使用HBuilder X创建uni-app项目// manifest.json 配置文件 { name: 漫小天漫画, appid: __UNI__XXXXXX, description: 漫画阅读小程序, versionName: 1.0.0, versionCode: 100, transformPx: false, app-plus: { usingComponents: true }, mp-weixin: { appid: wxxxxxxxxxxxxxxx, setting: { urlCheck: false }, usingComponents: true, permission: { scope.userLocation: { desc: 你的位置信息将用于小程序位置接口的效果展示 } } } }6.2 小程序页面结构实现小程序首页template view classcontainer !-- 搜索栏 -- view classsearch-bar u-search v-modelsearchKeyword placeholder搜索漫画 searchhandleSearch clearhandleClearSearch / /view !-- 轮播图 -- swiper classbanner-swiper indicator-dots autoplay circular swiper-item v-forbanner in bannerList :keybanner.id image :srcbanner.image modeaspectFill clickhandleBannerClick(banner)/ /swiper-item /swiper !-- 分类导航 -- view classcategory-nav scroll-view classnav-scroll scroll-x view v-forcategory in categoryList :keycategory.id :class[nav-item, activeCategory category.id ? active : ] clickhandleCategoryChange(category.id) {{ category.name }} /view /scroll-view /view !-- 漫画列表 -- view classcomic-list view classsection-title热门推荐/view view classcomic-grid view v-forcomic in comicList :keycomic.id classcomic-item clickhandleComicClick(comic) image classcomic-cover :srccomic.coverImage modeaspectFill / view classcomic-info text classcomic-title{{ comic.title }}/text text classcomic-author{{ comic.author }}/text view classcomic-stats text classview-count {{ comic.viewCount }}/text text classstatus{{ comic.status 1 ? 连载中 : 完结 }}/text /view /view /view /view /view !-- 加载更多 -- view classload-more v-ifhasMore u-loadmore statusloading / /view view classno-more v-else text没有更多数据了/text /view /view /template script setup import { ref, onMounted } from vue import { onReachBottom, onPullDownRefresh } from dcloudio/uni-app const searchKeyword ref() const activeCategory ref(0) const comicList ref([]) const bannerList ref([]) const categoryList ref([]) const currentPage ref(1) const hasMore ref(true) const loading ref(false) onMounted(() { loadInitialData() }) // 加载初始数据 const loadInitialData async () { await Promise.all([ loadBanners(), loadCategories(), loadComicList() ]) } // 加载轮播图 const loadBanners async () { try { const res await uni.request({ url: /api/banner/list, method: GET }) bannerList.value res.data.data } catch (error) { console.error(加载轮播图失败:, error) } } // 加载分类 const loadCategories async () { try { const res await uni.request({ url: /api/category/list, method: GET }) categoryList.value res.data.data } catch (error) { console.error(加载分类失败:, error) } } // 加载漫画列表 const loadComicList async (reset false) { if (loading.value) return loading.value true try { const page reset ? 1 : currentPage.value const res await uni.request({ url: /api/comic/list, method: GET, data: { page, pageSize: 10, categoryId: activeCategory.value || undefined, keyword: searchKeyword.value || undefined } }) const newList res.data.data.records if (reset) { comicList.value newList } else { comicList.value [...comicList.value, ...newList] } hasMore.value res.data.data.current res.data.data.pages currentPage.value page 1 } catch (error) { console.error(加载漫画列表失败:, error) } finally { loading.value false uni.stopPullDownRefresh() } } // 搜索处理 const handleSearch () { currentPage.value 1 loadComicList(true) } const handleClearSearch () { searchKeyword.value handleSearch() } // 分类切换 const handleCategoryChange (categoryId) { activeCategory.value categoryId currentPage.value 1 loadComicList(true) } // 漫画点击 const handleComicClick (comic) { uni.navigateTo({ url: /pages/comic/detail?id${comic.id} }) } // 上拉加载更多 onReachBottom(() { if (hasMore.value !loading.value) { loadComicList() } }) // 下拉刷新 onPullDownRefresh(() { currentPage.value 1 loadComicList(true) }) /script style scoped .container { padding: 20rpx; background-color: #f5f5f5; } .search-bar { margin-bottom: 20rpx; } .banner-swiper { height: 300rpx; border-radius: 16rpx; overflow: hidden; margin-bottom: 30rpx; } .banner-swiper image { width: 100%; height: 100%; } .category-nav { margin-bottom: 30rpx; } .nav-scroll { white-space: nowrap; } .nav-item { display: inline-block; padding: 16rpx 32rpx; margin-right: 20rpx; background: #fff; border-radius: 32rpx; font-size: 28rpx; } .nav-item.active { background: #007aff; color: #fff; } .section-title { font-size: 32rpx; font-weight: bold; margin-bottom: 20rpx; } .comic-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; } .comic-item { background: #fff; border-radius: 16rpx; overflow: hidden; } .comic-cover { width: 100%; height: 300rpx; } .comic-info { padding: 20rpx; } .comic-title { display: block; font-size: 28rpx; font-weight: bold; margin-bottom: 8rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .comic-author { font-size: 24rpx; color: #666; margin-bottom: 12rpx; } .comic-stats { display: flex; justify-content: space-between; font-size: 22rpx; color: #999; } .load-more, .no-more { text-align: center; padding: 40rpx; color: #999; } /style7. 文件上传与存储方案7.1 后端文件上传接口实现漫画封面和章节图片的上传功能RestController RequestMapping(/api/upload) public class FileUploadController { Value(${file.upload.path}) private String uploadPath; Value(${file.access.url}) private String accessUrl; PostMapping(/image) public ResultString uploadImage(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } // 验证文件类型 String contentType file.getContentType(); if (!contentType.startsWith(image/)) { return Result.error(只支持图片文件); } // 生成文件名 String originalFilename file.getOriginalFilename(); String fileExtension originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString() fileExtension; // 创建目录 File destDir new File(uploadPath); if (!destDir.exists()) { destDir.mkdirs(); } // 保存文件 File destFile new File(destDir, fileName); try { file.transferTo(destFile); String fileUrl accessUrl / fileName; return Result.success(fileUrl); } catch (IOException e) { return Result.error(文件上传失败); } } }7.2 前端文件上传组件实现通用的文件上传组件template div classupload-component el-upload classavatar-uploader action/api/upload/image :show-file-listfalse :before-uploadbeforeUpload :on-successhandleSuccess :on-errorhandleError img v-ifimageUrl :srcimageUrl classavatar / el-icon v-else classavatar-uploader-iconPlus //el-icon /el-upload div classupload-tips支持 JPG、PNG 格式大小不超过 2MB/div /div /template script setup import { ref } from vue import { ElMessage } from element-plus import { Plus } from element-plus/icons-vue const props defineProps({ modelValue: String }) const emit defineEmits([update:modelValue]) const imageUrl ref(props.modelValue) const beforeUpload (file) { const isJPGOrPNG file.type image/jpeg || file.type image/png const isLt2M file.size / 1024 / 1024 2 if (!isJPGOrPNG) { ElMessage.error(只能上传 JPG/PNG 格式的图片!) return false } if (!isLt2M) { ElMessage.error(图片大小不能超过 2MB!) return false } return true } const handleSuccess (response) { imageUrl.value response.data emit(update:modelValue, response.data) ElMessage.success(上传成功) } const handleError () { ElMessage.error(上传失败请重试) } /script style scoped .avatar-uploader { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: border-color 0.3s; width: 178px; height: 178px; display: flex; align-items: center; justify-content: center; } .avatar-uploader:hover { border-color: #409eff; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; } .avatar { width: 100%; height: 100%; object-fit: cover; } .upload-tips { margin-top: 8px; color: #909399; font-size: 12px; } /style8. 性能优化与最佳实践8.1 后端性能优化数据库查询优化Service public class ComicService { // 使用Redis缓存热门数据 Cacheable(value hotComics, key #categoryId : #limit) public ListComic getHotComics(Long categoryId, Integer limit) { return comicMapper.selectHotComicsByCategory(categoryId, limit); } // 批量操作优化 Transactional public void batchUpdateViewCount(ListLong comicIds) { comicMapper.batchUpdateViewCount(comicIds); } } // MyBatis批量更新配置 Mapper public interface ComicMapper { void batchUpdateViewCount(Param(comicIds) ListLong comicIds); }!-- batchUpdateViewCount SQL -- update idbatchUpdateViewCount UPDATE comic SET view_count view_count 1 WHERE id IN foreach collectioncomicIds itemid open( separator, close) #{id} /foreach /update8.2 前端性能优化图片懒加载优化template img v-lazyimageUrl :altaltText classlazy-image / /template script setup // 自定义懒加载指令 const vLazy { mounted(el, binding) { const observer new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { el.src binding.value observer.unobserve(el) } }) }) observer.observe(el) } } /script路由懒加载优化// 使用动态导入实现路由懒加载 const routes [ { path: /comic/detail, component: () import(/* webpackChunkName: comic-detail */ /views/ComicDetail.vue) } ]8.3 小程序优化技巧图片优化策略template image :srcimageUrl modeaspectFill lazy-load :webpsupportWebp errorhandleImageError / /template script setup import { ref