ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot+Vue+MyBatis:小说阅读管理系统全栈开发实战

Spring Boot+Vue+MyBatis:小说阅读管理系统全栈开发实战 简介一套基于Springboot、Vue和Mybatis构建的小说阅读管理系统源码包面向Java全栈初学者、课程设计或毕业设计人群帮助解决在线小说阅读平台的开发落地问题。资源共480个文件压缩包约5.1MB主要包含112个JavaScript脚本、82个Java后端类、44个HTML页面、39个CSS样式以及12个Mybatis相关XML映射等其中还附带SQL数据库脚本便于直接导入运行。项目完整实现了用户注册登录、小说信息管理、章节在线阅读、书签记录等核心功能并通过Spring Security控制权限前端使用Vue Router和Vuex管理路由与状态前后端采用RESTful API通信展示了主流前后端分离架构的工程化写法。已有663人学习下载适合需要参考完整实战项目、梳理SpringbootVueMybatis整合步骤的开发者可从中获得数据库设计、权限配置、接口封装及页面交互等可直接复用的思路与代码。1. 小说阅读管理系统Spring Boot Vue MyBatis 能拼出什么小说阅读管理系统本质上是一套内容型 Web 应用的标准骨架用户登录、作品上架、章节阅读、书架收藏、搜索分页每个模块单独拎出来都不复杂但拼在一起要处理的工程问题不少。基于 Spring Boot Vue MyBatis 这套组合做后端管数据和接口前端管页面交互MyBatis 把 SQL 写清楚正好覆盖了全栈开发最常碰到的场景。这篇文章按我自己做这类项目的习惯从建表、后端接口、前端阅读器一路讲到联调打包时容易翻车的几个坑适合打算把它当全栈入门项目、毕设原型或者公司内部内容系统脚手架的人照着走一遍。2. Spring Boot 后端与 MyBatis 数据层先建表再写查询2.1 核心表设计小说、章节、用户、书架的四张表字段怎么定小说阅读管理系统的数据模型不复杂核心就四个实体用户、小说、章节、书架。我一般先画表再写代码因为字段取舍直接决定后面 Mapper 好不好写。用户表负责登录和身份标识小说表存元数据章节表存正文书架表记录用户收藏了哪本书、读到哪一章。建表 SQL 如下CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(255) NOT NULL COMMENT BCrypt 加密后存储, nickname VARCHAR(50) DEFAULT , created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author VARCHAR(50) NOT NULL, category VARCHAR(30) DEFAULT , cover_url VARCHAR(255) DEFAULT , intro TEXT COMMENT 简介, status TINYINT DEFAULT 1 COMMENT 0下架 1连载 2完结, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE chapter ( id BIGINT NOT NULL AUTO_INCREMENT, book_id BIGINT NOT NULL, chapter_no INT NOT NULL COMMENT 章节序号, title VARCHAR(200) NOT NULL, content LONGTEXT NOT NULL COMMENT 章节正文, word_count INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_book_no (book_id, chapter_no), KEY idx_book_id (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE bookshelf ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, last_chapter_id BIGINT DEFAULT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_book (user_id, book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;四张表的关联关系是book 一对多 chapteruser 多对多 book通过 bookshelf 这个中间表连接last_chapter_id 记录阅读进度。字段命名统一用下划线MySQL 里可读性好Java 实体里用驼峰靠 MyBatis 的 map-underscore-to-camel-case 自动映射省掉大量手写 resultMap 的活。这个配置是 MyBatis 的必开项后面避坑章节会展开。章节表的 content 用 LONGTEXT而不是 TEXT。一本十万字的小说章节正文从三千字到两万字不等TEXT 最大 64KB在 utf8mb4 字符集下差不多只能装 1.6 万个中文字符章节稍长就截断报错。LONGTEXT 上限 4GB对小说正文完全够用。前后端交互时一次性返回整章 content是最简单也最省事的做法性能优化放到最后一章说。bookshelf 表的 last_chapter_id 是阅读进度的核心。很多新手把进度存在前端 localStorage换设备就丢。正确做法是用户翻到某一章时前端把 chapter_id 传给接口后端 update 到 bookshelf.last_chapter_id下次打开书详情页直接读取续读章节编号跳转。关于字段约束有一个容易被忽略的点chapter 表对 (book_id, chapter_no) 建联合唯一索引。小说导入章节时经常要重复执行同一批数据有这个唯一索引兜底重复插入直接报错而不是产生脏数据批量导入的幂等性会好处理很多后端代码里也能省掉先查一遍的步骤。2.2 Spring Boot 项目结构Controller、Service、Mapper 的边界怎么划Spring Boot 项目的标准分包方式我会按照 com.example.novel 下拆 controller、service、mapper、entity、config 五个包。很多人会纠结业务逻辑到底放 Service 还是 Controller我的习惯是 Controller 只做参数接收和结果包装所有 SQL 之外的判断、事务、组装都放 ServiceMapper 只做数据库读写。这样小说阅读管理系统的代码量虽然不大但后续加 Redis 缓存、加消息队列时不用大改。application.yml 是后端启动的关键配置里面有三段需要重点看spring: datasource: url: jdbc:mysql://localhost:3306/novel?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.novel.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpldatasource 里的 characterEncodingutf8mb4 是关键参数很多小说正文乱码都是这里漏了。serverTimezone 指定 Asia/Shanghai避免 MySQL 驱动 8.x 版本对本地时区的不识别报错。multipart 配置控制封面图片上传大小默认 1MB 在小说封面场景下经常不够用我会按实际需要调到 10MB 文件、50MB 请求。mybatis.mapper-locations 指定 XML 文件在 classpath:mapper/ 目录下这是 MyBatis 扫到 SQL 的前提。type-aliases-package 让 XML 里写 resultType 时不用拼全限定类名写 Book 即可。map-underscore-to-camel-case 是必须开的不开的话表里的 cover_url 字段映射不到实体的 coverUrl 属性上查询结果全是 null。log-impl 配成 StdOutImpl 会把 SQL 打印到控制台开发阶段排查问题时非常直观。启动类上还要加一个注解漏了它 Mapper 接口就扫不到package com.example.novel; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.example.novel.mapper) public class NovelApplication { public static void main(String[] args) { SpringApplication.run(NovelApplication.class, args); } }MapperScan 指定 mapper 接口所在的包路径Spring 容器启动时会把所有接口通过 JdkDynamicAopProxy 代理注册成 Bean。不用它的话每个 Mapper 接口上得单独加 Mapper 注解代码量多且容易漏。这里还有一个隐含的边界问题MapperScan 扫描的包如果层级太长会把不是 Mapper 的接口也注册进去报显式代理错误所以建议 mapper 包独立、不要和其他接口混放。2.3 MyBatis 动态 SQL 与分页写一个小说列表的多条件查询小说管理后台最常见的需求是筛选列表按书名模糊搜、按作者模糊搜、按分类精确搜、按状态过滤。这种多条件组合查询是 MyBatis 动态 SQL 的主场我习惯把条件封装成一个查询对象传给 Mapperpackage com.example.novel.entity.query; import lombok.Data; Data public class BookQuery { private String title; private String author; private String category; private Integer status; private Long bookId; }对应的 BookMapper.xml 核心片段select idselectBookPage resultTypecom.example.novel.entity.Book SELECT id, title, author, category, cover_url, intro, status, created_at FROM book where if testtitle ! null and title ! AND title LIKE CONCAT(%, #{title}, %) /if if testauthor ! null and author ! AND author LIKE CONCAT(%, #{author}, %) /if if testcategory ! null and category ! AND category #{category} /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC /select这一段的两个要点要记住。where 标签会自动去掉第一个条件前面的 AND避免手写 where 11 的写法可读性和效率都好一些。like 查询用 CONCAT(%, #{title}, %) 而不是直接在参数里拼 %因为 #{} 占位符是预编译参数能防 SQL 注入${} 直接拼接是不推荐的做法。分页我用 PageHelper 插件它的原理是基于拦截器改写 SQL在查询前自动拼上 limit 子句。用法是查询前调一行 startPagepackage com.example.novel.controller; import com.example.novel.common.Result; import com.example.novel.entity.Book; import com.example.novel.entity.query.BookQuery; import com.example.novel.mapper.BookMapper; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; RestController RequestMapping(/api/book) public class BookController { Resource private BookMapper bookMapper; GetMapping(/page) public ResultPageInfoBook page(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, BookQuery query) { PageHelper.startPage(pageNum, pageSize); ListBook bookList bookMapper.selectBookPage(query); PageInfoBook pageInfo new PageInfo(bookList); return Result.ok(pageInfo); } }PageHelper.startPage(pageNum, pageSize) 必须在紧跟着的 Mapper 查询方法之前调用中间不能穿插任何其他 SQL 查询。因为它用的是 ThreadLocal 存分页参数查询结束后 PageHelper 会自动清理但这个顺序一旦被打破分页参数就会残留或错位。PageInfo 会把总条数、总页数、当前页、每页大小都算好前端拿到这个对象直接就能渲染分页组件。这里还有一个值得养成的习惯分页默认值写在 RequestParam 里而不是在 Service 层判断空值。这样接口文档清晰前端少传两个参数也不会报错。PageInfo 的 list 字段就是当前页的数据pages 字段是总页数total 是总记录数前端 Vue 的 el-pagination 组件可以直接绑定不需要二次组装。3. Vue 前端从脚手架到阅读器页面的完整链路3.1 Vue 项目初始化与开发代理跨域问题在配置层解决后端接口写到一半就该开前端工程了。常见做法是直接用 Vue CLI 创建项目命令是 vue create novel-web选 Vue Router 和 Vuex 预设。如果用 Vite则是 npm create vuelatest交互方式类似二者对 Spring Boot 后端的对接方式没有本质差别。Vue 的前端项目结构天然适合小说阅读管理系统组件化拆页面、Vue Router 管跳转、状态管理存用户信息。前后端分离开发时第一个坎是跨域。前端跑在 3000 端口后端跑在 8080 端口浏览器同源策略会拦住所有带自定义 Header 的请求。开发环境我习惯在 vue.config.js 里配代理而不是在后端开全局 CORS因为代理配置对生产环境的 nginx 反向代理有直接参考价值// vue.config.js const { defineConfig } require(vue/cli-service) module.exports defineConfig({ devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这段配置的意思是前端所有以 /api 开头的请求开发服务器都会转发给 target 指向的后端地址浏览器看到的请求是同源的跨域问题从根源上消失。changeOrigin: true 会让后端收到的 Host 头变成 localhost:8080避免某些后端校验 Host 时误判。关于 /api 前缀要定一个规矩后端所有接口统一以 /api 开头Controller 里的 RequestMapping 写成 /api/book、/api/chapter 这种形式。这样前端代理规则只需匹配一个前缀Vue Router 的路由里也不需要纠结接口路径和页面路径的关系。3.2 Axios 封装拦截器统一处理 Token 与错误提示开发小说阅读管理系统用户登录后要带着 Token 访问收藏、阅读进度这些接口。如果不做封装每个页面组件里都写一遍 localStorage.getItem(token)代码会非常分散。我用 axios 实例集中管理把请求拦截、响应拦截、错误处理都收拢在一个文件里import axios from axios import router from /router import { ElMessage } from element-plus const service axios.create({ baseURL: process.env.NODE_ENV production ? : /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response response.data, error { const status error.response error.response.status if (status 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录状态已过期请重新登录) } else if (status 500) { ElMessage.error(服务器开小差了请稍后重试) } else if (error.code ECONNABORTED) { ElMessage.error(请求超时请检查网络) } return Promise.reject(error) } ) export default servicebaseURL 这里做了一个环境判断开发环境走 /api 让 devServer 代理转发生产环境用空字符串接口路径直接以 /api 开头发到同域。很多新手在生产环境忘了改 baseURL导致请求打到前端静态服务器的 /api 上返回 index.html 而不是 JSON。这个环境判断能省掉部署时改代码的步骤。响应拦截器的 return response.data 是一个很实用的约定。后端 Result 包装类的结构通常是 { code: 200, message: ok, data: ... }拦截器直接把 data 字段拆出来返回页面里调接口拿到的就是业务数据不用每个组件里都写 .data.data代码干净很多。拦截器里对 401 的处理要特别注意清除本地 Token 后跳转登录页同时给用户一个明确提示。小说阅读场景里用户可能长时间沉浸阅读Token 过期后第一次点加入书架就跳登录如果没有提示会以为页面出 bug 了。后端返回 401 时响应拦截器不要尝试静默刷新 Token这类系统的 Token 刷新逻辑应该放到后端网关层统一处理前端强行做只会增加复杂度。3.3 阅读器页面章节列表加载、内容渲染与进度自动保存阅读器是整个小说阅读管理系统前端最核心的页面。它要同时处理三个问题左侧或上方的章节列表怎么组织、内容区怎么渲染长文本、阅读进度怎么在翻章时自动保存。Vue 组件拆分上我倾向于把章节列表拆成子组件内容区和操作栏放主组件避免一个文件几百行。先看阅读器主组件的模板和核心逻辑template div classreader-container aside classchapter-panel div v-foritem in chapterList :keyitem.id :class[chapter-item, { active: item.id currentChapter.id }] clickswitchChapter(item.id) {{ item.chapterNo }}. {{ item.title }} /div /aside section classcontent-panel h1{{ currentChapter.title }}/h1 div classnovel-content :style{ fontSize: fontSize px, lineHeight: lineHeight px } {{ currentChapter.content }} /div div classreader-ops button clickprevChapter上一章/button select v-modelfontSize option :value14小/option option :value16中/option option :value18大/option /select button clicknextChapter下一章/button /div /section /div /templateexport default { data() { return { bookId: this.$route.params.bookId, chapterList: [], currentChapter: {}, fontSize: 16, lineHeight: 32 } }, created() { this.fetchChapterList() }, methods: { async fetchChapterList() { const res await service.get(/api/chapter/list?bookId${this.bookId}) this.chapterList res.data const lastId localStorage.getItem(last_${this.bookId}) || this.chapterList[0].id this.loadChapter(lastId) }, async loadChapter(chapterId) { const res await service.get(/api/chapter/${chapterId}) this.currentChapter res.data localStorage.setItem(last_${this.bookId}, chapterId) this.saveReadingProgress(chapterId) }, prevChapter() { const idx this.chapterList.findIndex(c c.id this.currentChapter.id) if (idx 0) this.loadChapter(this.chapterList[idx - 1].id) }, nextChapter() { const idx this.chapterList.findIndex(c c.id this.currentChapter.id) if (idx this.chapterList.length - 1) this.loadChapter(this.chapterList[idx 1].id) }, async saveReadingProgress(chapterId) { if (!localStorage.getItem(token)) return await service.post(/api/bookshelf/progress, { bookId: this.bookId, chapterId }) } } }这段逻辑里有三个地方值得说明。第一章节列表接口和章节内容接口拆成两个。列表只返回 id、title、chapterNo 这些轻量字段内容接口单独返回 content。如果一个接口把整本书所有章节的 content 都查出来几十万字直接卡爆浏览器。这是阅读器性能的第一道闸门。第二当前章节 id 同时存在 localStorage 和后端。localStorage 用于快速恢复上次阅读位置后端 bookshelf 用于换设备续读。未登录时只写 localStorage登录后同步到后端两边互补。很多系统只做前端存储用户换个手机登录就找不到进度这是体验上的大坑。第三prevChapter 和 nextChapter 通过 chapterList 的 index 定位。这里要求章节接口返回的列表必须按 chapter_no 排好序否则上一章下一章跳转就会错乱。排序逻辑应该放在后端 SQL 的 ORDER BY chapter_no ASC不要把排序交给前端处理。4. 避坑小说阅读管理系统开发中最容易翻车的 5 个问题4.1 查询结果实体字段全是 null下划线转驼峰配置没生效做小说列表接口时前端传回来的书名、作者都有值但 cover_url 字段对应到实体的 coverUrl 永远是 null排查很久发现是 application.yml 里漏了 map-underscore-to-camel-case: true。原因很直接MyBatis 默认关闭驼峰映射数据库的 cover_url 无法自动映射到 Java 实体的 coverUrl 属性其他字段一样受影响。解决方式是在 mybatis.configuration 下开启 map-underscore-to-camel-case: true。如果项目已经用了自定义 MybatisConfiguration 类需要在 Bean 里显式设置Bean public ConfigurationCustomizer configurationCustomizer() { return configuration - configuration.setMapUnderscoreToCamelCase(true); }还有一个连带坑如果某些 SQL 查询用了 SELECT cover_url AS coverUrl 这种手动别名开启驼峰映射后字段映射会走两遍逻辑结果仍然正确但没必要。统一开驼峰SQL 里保持表字段原名可读性最好。4.2 PageHelper 分页传染到后面的查询startPage 和查询之间插了别的 SQL系统中用户详情页要查用户信息、再查他书架里的书我在 Service 方法里先调了用户查询再调书架分页查询结果第一次查询也被自动加了 LIMIT。原因是 PageHelper.startPage 基于 ThreadLocal 存参数只要同一个线程里后续还有其他查询参数就会作用到下一个查询上。解决方法是把 startPage 紧贴在目标 Mapper 查询前一行中间不能有任何其他数据库操作。如果 Service 里确实要先查别的数据可以先把分页参数放到方法最后再启动。另外在 try-finally 里调用 PageHelper.clearPage() 也是保险做法但 PageHelper 在 5.x 版本查询后会自动清理手动清理反而多余。4.3 Vue 打包后放进 Spring Boot 的 static 目录刷新页面 404看到有人直接把 Vue 打包产物 vue dist 里的文件复制到 src/main/resources/static生产环境访问首页正常但点进 /book/3 再按 F5 刷新页面直接 404。原因是 Vue Router 用了 history 模式浏览器刷新时会真实请求 /book/3 路径而 Spring Boot 只映射了 index.html该路径没有对应的控制器。解决做法是配置一个资源映射或路由回退让所有非静态资源的路径都回到 index.htmlConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/book/{path:[^\\.]*}).setViewName(forward:/index.html); } }更通用的做法是自定义 ErrorController 转发到 index.html但要注意别把静态资源的 404 也转发进去。如果前端用了 history 模式另一个方案是把路由模式改成 hash 模式URL 带 # 号刷新不会发真实请求但不少产品经理接受不了 URL 里的 #所以后端转发还是得会配。4.4 章节内容 2 万字接口要 8 秒才返回小说网站阅读量上来后章节约两万字的接口响应越来越慢浏览器转圈很久。排查发现接口返回的 JSON 是全文 content 字段两万字的 JSON 序列化后超过 100KB还不算网络传输时间。再加上后端 SQL 没有优化LONGTEXT 字段全量查询慢是必然的。第一波优化是只查需要的字段列表页查询不查 content阅读页单独查 content。第二波优化是压缩传输Spring Boot 的 server.compression.enabled 开启后超过阈值的数据会 Gzip 压缩100KB 能压到 20KB 左右。第三波优化是缓存章节内容几乎不变热点书第一次查完放 Redis后续请求直接走缓存这个放到最后一章展开。4.5 批量导入小说章节时MySQL 报 max_allowed_packet 超限爬虫或手工整理的小说数据批量导入系统每本书几百章我用 MyBatis 的 foreach 一次性插入全部章节运行时报 Packets larger than max_allowed_packet are not allowed。原因是单条 SQL 过大超过了 MySQL 默认的 4MB 包大小限制foreach 拼接的 INSERT INTO VALUES 一条就几 MB。解决方式分为两档。最简单的是调大 MySQL 的 max_allowed_packet 参数但治标不治本。更稳妥的方案是分批插入比如每次限制 100 条public void batchInsert(ListChapter chapters) { if (chapters null || chapters.isEmpty()) return; for (int i 0; i chapters.size(); i 100) { ListChapter subList chapters.subList(i, Math.min(i 100, chapters.size())); chapterMapper.batchInsert(subList); } }分批插入还有一个好处单批失败时定位问题更准确不会因为 500 章里有一章数据异常就整批回滚。配合前面建表时的 uk_book_no 唯一索引重复数据直接跳过或捕获异常导入稳定性和可重试性都好了。5. 进阶从能跑到能用的三个必做优化5.1 给章节内容加缓存读多写少的接口先上 Redis章节内容访问量远高于修改量是典型的读多写少场景。参考 MyBatis 二级缓存的思路我在 Service 层用 Redis 做一层缓存key 为 chapter:idvalue 是章节的 JSON 结构。逻辑上用 Spring Cache 注解最省事Cacheable(cacheNames chapter, key #chapterId) public Chapter getById(Long chapterId) { return chapterMapper.selectById(chapterId); }这里要注意一个问题章节更新时必须记得调 CacheEvict 清掉对应 key否则用户看到的还是修改前的旧章节。我踩过这个坑上线一周才发现编辑后的章节一直没生效。后续可以再加上缓存预热书完结时把前 100 章提前灌入 Redis避免流量高峰时首次访问打穿数据库。5.2 搜索从 LIKE 到全文索引数据量超过 10 万条时换方式小说列表页的书名搜索用 LIKE %xx%在 10 万本书以下是能接受的。数据量涨上去之后全表扫 LIKE 会拖垮 MySQL。如果不想引入 Elasticsearch可以先用 MySQL 自带的中文全文索引把 title、author、intro 三个字段建成 FULLTEXTALTER TABLE book ADD FULLTEXT INDEX ft_book_search (title, author, intro);查询时用 MATCH AGAINST 替代 LIKE。但这个方案对中文分词的支持比较弱ngram 解析器下短词效果不如预期。真正长期可用的做法还是独立搜索服务项目早期可以在 Service 层预留一个 searchProvider 接口后面切换实现时不用动 Controller。5.3 用 Docker 组合部署后端镜像与前端静态资源分开Vue 前端打包后是纯静态文件Spring Boot 后端是独立进程。生产部署常见做法是两个容器nginx 承载前端页面并反向代理 /api 请求到后端容器jar 包跑后端服务。我习惯用 docker-compose 描述两个服务docker build -t novel-api . docker run -d -p 8080:8080 --name novel-api novel-api docker run -d -p 80:80 --name novel-nginx \ -v /home/novel/dist:/usr/share/nginx/html \ -v /home/novel/nginx.conf:/etc/nginx/conf.d/default.conf \ --link novel-api \ nginx:alpinenginx.conf 里需要把 /api/ 开头的请求转发到后端location /api/ { proxy_pass http://novel-api:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里有个容易犯的错误proxy_pass 后面的 URL 末尾是否带斜杠会直接影响转发路径。带斜杠转发会去掉 /api 前缀后端接口路径对不上不带斜杠则原样转发最稳妥。我自己第一次部署时就是在这里调了半个晚上后来养成了一个习惯所有网关层的 URL 重写规则先在本地 curl 验证一遍再上生产。小说阅读管理系统做到这里技术上已经闭环了。这类的系统支撑一家小型内容平台绰绰有余后续要做评论、打赏、推荐都能顺着这层结构扩展。希望帮到你。本文还有配套的精品资源点击获取
返回列表