1. 项目定位与需求破局
1.1 为什么做经方药食同源平台
先聊聊这个项目的来龙去脉。我在做这个“经方药食两用服务平台”之前,其实已经接触了不少医疗信息化的项目,但这个项目让我重新思考了技术选型与业务深度的关系。经方指的是汉代以前经典医籍中记载的方剂,核心来源即《伤寒论》《金匮要略》,张仲景的方子历经近两千年临床验证,组方严谨、配伍精当,是中医方剂学的基础。药食同源则是中医“药食一体”思想的实践延伸——生姜、大枣、山药、枸杞、陈皮、茯苓这些材料,既是日常食材,也是规范化收录的药食同源品种。
这个平台的痛点非常清晰:大众想查某个症状对应什么经方,或者想了解某味药材能否日常食疗使用,信息是碎片化的。纸本经典对于非专业用户门槛太高,普通网站内容又杂乱无章、缺乏权威来源。所以我要做的,是一个以经方为核心、以药食两用为特色检索维度的服务平台。用户可以浏览经方详情、按症状或药材反向检索、管理收藏笔记,管理员可以维护方剂与药材数据。这个定位不是做一个泛泛的“中医门户”,而是聚焦于经方与药食两用这两个高度可落地的知识域。
1.2 前后端分离架构的优势
选择前后端分离是这个项目的第一个关键决策。如果做传统服务端渲染,用Thymeleaf或JSP,开发速度确实快,但有两个问题:一是前端交互体验受限,尤其是经方详情、药食检索这类需要频繁异步请求数据的场景,整页刷新会让体验大打折扣;二是前后端代码耦合严重,后续如果想增加移动端适配或小程序端,后端逻辑难以复用。
前后端分离之后,后端只提供JSON接口,不关心页面渲染;前端通过HTTP请求消费接口,用Vue组件化开发页面。这样做的直接收益有三个:第一,团队协作解耦——一个人负责接口、一个人负责页面,互不阻塞;第二,接口可以复用于多端场景;第三,部署灵活,前端静态资源由Nginx托管,后端以独立服务运行,两边可以独立扩缩容。这个项目走了这条路,后续部署上线时的轻松感,是值得前期多花一点时间做分离架构规划来换取的。
2. 系统架构与关键技术选型
2.1 总体架构设计
整个系统的架构分为表现层、接入层、业务层、数据层四层。表现层就是Vue SPA(单页应用),运行在浏览器中;接入层用Nginx做静态资源托管和反向代理,前端所有/api开头的请求转发到后端服务;业务层是SpringBoot应用,内部按照Controller—Service—Mapper三层划分;数据层是MySQL 8.0,存储业务数据。
这样的分层让每一层的职责都非常清晰。Nginx负责请求分发和静态资源加速,SpringBoot处理业务逻辑与数据校验,MyBatis作为ORM框架负责SQL映射。这里有一个容易被初学者忽略的点:前端请求经过Nginx代理到后端时,需要区分“请求静态资源”和“请求API数据”两种路径。Nginx中我配置了location /api规则转发到后端,其余路径默认走前端静态资源,从而避免了两种请求互相干扰。
2.2 前端技术栈详解
前端采用Vue 2.6 + Element UI的组合。为什么没有上Vue 3?当时考虑的是Element UI在Vue 2生态下组件最成熟,第三方示例多,排查问题的资料丰富,适合这个项目的进度要求。主技术栈如下:
- Vue 2.6:核心框架,组件化开发的基础
- Vue Router:前端路由,实现SPA页面切换
- Vuex:全局状态管理,保存登录凭证、用户信息
- Axios:HTTP请求库,统一处理API调用
- Element UI:UI组件库,提供表格、表单、弹窗、分页等现成组件
- Sass:样式预处理,让样式结构更清晰
以经方列表页为例,页面加载时通过Axios请求后端的/api/fangji/list接口,获取经方分页数据,渲染到Element UI的el-table组件中。点击某一行时,路由跳转到详情页,详情页再根据路由参数请求/api/fangji/detail接口。这样的交互不需要刷新浏览器,数据加载由前端异步完成,体验接近原生应用。
2.3 后端技术栈详解
后端采用SpringBoot 2.3 + MyBatis + MySQL 8.0的组合,Java版本基于1.8。选择SpringBoot而不是SSM(Spring MVC + MyBatis)手动整合,是为了省掉大量XML配置——SpringBoot的自动配置机制可以把数据源、事务管理器、Web MVC、Jackson序列化这些基础模块全部自动装配好。MyBatis则负责SQL映射,它的优势是SQL控制力强,复杂查询可以通过自定义SQL实现,不像JPA那样容易被框架层的自动生成SQL拖累性能。
关于版本选择,SpringBoot首选2.3.x,因为与MyBatis Starter的兼容性最稳定,与Spring Security的集成资料也比较多。MySQL选8.0是因为它默认字符集是utf8mb4,可以完整支持中文和生僻字,对经方中可能出现的繁体字、生僻药材名非常重要。
2.4 数据库选型与设计思路
MySQL是这个项目的必然选择。这个项目的核心数据(经方条目、药材条目)大多在百万条以内,属于典型的中小型关系型数据,MySQL在单机场景下性能足够。更重要的是,经方数据之间存在明确的关系结构——方剂包含多味药材、药材属于特定分类,这些关系在关系型数据库中表达和查询都非常自然。
数据库采用InnoDB引擎,排序规则使用utf8mb4_general_ci。这里有个细节:建库时字符集一定要选对,utf8mb4不是MySQL 5.7默认值,如果不显式设置就会漏掉生僻字的存储能力。经方中有大量生僻字,如“藭”“虻虫”“䗪虫”等,如果用utf8建表,写入这些字会报错或变成乱码。另外,InnoDB支持行级锁和外键约束,在多用户并发写入时能保证数据一致性。
3. 数据库核心表结构设计
3.1 用户与权限模块
用户表设计尽量精简,核心字段包括:id、username、password(BCrypt加密存储)、nickname、avatar、role(用户角色)、status(账号状态)、create_time、update_time。角色字段我用的是INT类型,0表示普通用户,1表示管理员。这里不建议在前端只依赖v-if做页面控制的权限判断,因为这不安全,后端必须根据JWT中的角色信息做真正的接口鉴权。
CREATE TABLE `sys_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt)', `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像地址', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '角色:0普通用户,1管理员', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1正常,0禁用', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';注意,BCrypt加密不是简单哈希,它是加盐哈希算法,每次加密同一密码产生的结果都不同,安全性远高于MD5、SHA1。用户注册时,用BCryptPasswordEncoder加密存储;登录时,matches方法校验原始密码和密文是否匹配。
3.2 经方管理模块
经方表是整个系统业务数据的核心。表设计需要考虑:方剂名称(丸、汤、散等剂型都要支持)、来源(出自哪部经典)、组成(药材及用量)、功效(主治方向)、主治病症、用法用量、禁忌注意等字段。我设计了两张表,一张存经方主表,一张存方剂药材关联表,因为一个方剂包含多味药材,一对多关系会导致字段冗余,拆分后灵活性和可扩展性都更好。
CREATE TABLE `fangji` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '方剂名称', `source_book` VARCHAR(100) DEFAULT NULL COMMENT '出处', `category` VARCHAR(50) DEFAULT NULL COMMENT '分类', `composition` TEXT COMMENT '组成(原文)', `usage_method` TEXT COMMENT '用法', `indications` TEXT COMMENT '主治病症', `efficacy` TEXT COMMENT '功效', `notes` TEXT COMMENT '注意事项', `like_count` INT DEFAULT 0 COMMENT '收藏数', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_name` (`name`), KEY `idx_category` (`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='经方主表';方剂药材关联表存储方名与药材的多对多关系,字段包含方剂ID、药材ID、用量(如“三两”“半斤”)。设计一个字段来存“炮制方法”(如“炙甘草”的“炙”),方便后续在药材详情页反查一个药材被哪些方剂使用。
3.3 药材管理模块
药材表的字段设计相对复杂,因为药食两用材料不仅具备药材属性,还涉及食用属性。字段设计:药材名(学名)、别名(常用异名)、性味(寒热温凉平)、归经、功效分类、药食两用标识(0为纯药材,1为药食两用)、食用方法(炖汤、泡茶、煮粥等)、禁忌人群、用量参考(每日摄入量)。药食两用标识是检索维度中的高频过滤条件,必须设置为索引。
CREATE TABLE `medicine` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '药材名', `alias_name` VARCHAR(100) DEFAULT NULL COMMENT '别名', `nature` VARCHAR(20) DEFAULT NULL COMMENT '性味', `meridian` VARCHAR(50) DEFAULT NULL COMMENT '归经', `category` VARCHAR(50) DEFAULT NULL COMMENT '功效分类', `is_food_medicine` TINYINT DEFAULT 0 COMMENT '药食两用:1是,0否', `edible_method` TEXT COMMENT '食用方法', `forbidden` TEXT COMMENT '禁忌人群', `daily_dose` VARCHAR(50) DEFAULT NULL COMMENT '每日用量参考', `img_url` VARCHAR(255) DEFAULT NULL COMMENT '图片地址', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_name` (`name`), KEY `idx_food_medicine` (`is_food_medicine`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药材表';3.4 收藏与笔记模块
收藏和笔记是增强用户粘性的两个核心功能模块。收藏表设计为favorite表,存储用户ID、收藏类型、收藏对象ID、收藏时间,其中类型字段用来区分收藏的是经方还是药材。笔记表note则让用户可以在经方详情页记录自己的学习心得或使用体验,字段包括笔记标题、正文内容、关联经方ID、创建时间。
这两张表的设计核心是联合唯一索引。收藏表上建UNIQUE KEY uk_user_target (user_id, target_type, target_id),避免同一用户重复收藏同一内容;笔记表则建KEY idx_user_fangji (user_id, fangji_id),支撑“查看我的笔记”和“查看方剂关联笔记”两类查询。用户收藏和笔记功能,让平台具备了轻量级的UGC属性,也为后续做推荐功能留下了数据基础。
4. 后端服务关键实现
4.1 SpringBoot项目骨架搭建
项目骨架用Maven管理,推荐使用Spring Initializr来生成基础工程,然后再手动引入MyBatis等依赖。pom.xml核心依赖如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>application.yml配置要关注几个关键项:数据源连接信息、MyBatis映射文件位置、JWT密钥与过期时间。其中MyBatis的mapper-locations必须指向实际的XML文件目录,否则Mapper接口无法与SQL绑定。
spring: datasource: url: jdbc:mysql://localhost:3306/fangji_platform?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.fangji.system.entity configuration: map-underscore-to-camel-case: true jwt: secret: fangji-platform-secret-key expire-hours: 24map-underscore-to-camel-case这个配置非常关键。数据库字段是create_time的命名风格,Java实体属性是createTime风格的驼峰命名,开启这个配置后MyBatis能自动完成下划线到驼峰的映射,省去大量人工别名处理。
4.2 JWT认证与权限控制
JWT(JSON Web Token)是实现前后端分离认证的标准方案。对比传统的Session鉴权,JWT的核心优势是服务端无需存储会话状态,登录成功后签发一个令牌,客户端存储该令牌,后续请求在请求头中携带。对于这个项目场景,用户在微信浏览器或普通浏览器中的访问方式是轻量的,JWT的无状态特性减少了服务端内存消耗,也更便于未来的接口扩展。
认证流程:用户提交用户名密码到/api/login接口,后端验证成功后生成JWT返回给前端;前端将JWT存储在localStorage中,并在Axios拦截器中为每个请求带上Authorization: Bearer <token>头;后端注册一个OncePerRequestFilter过滤器,每次请求到来时从请求头解析JWT并校验签名与过期时间,解析出的用户ID保存到ThreadLocal上下文中供后续业务使用。
实现细节上还有几个坑:第一,JWT密钥不能写在代码里,尤其提交源码的时候要习惯性把密钥提取到配置文件中;第二,expire-hours不能设太长,实际项目设了24小时,既照顾用户体验又控制了令牌泄露的风险;第三,注销登录时需要前端删除本地令牌,后端JWT本身无法主动失效,这是一般无状态方案的固有取舍。
权限控制方面,我用Spring Security做方法级鉴权。管理员接口(比如经方新增、编辑、删除)标注@PreAuthorize("hasRole('ADMIN')"),普通用户访问会收到403响应。这里必须在前端也顺手处理一下,收到403时跳转到无权限提示页,而不是白屏。
4.3 经方检索的核心SQL实现
经方检索是系统的高频操作,有三种场景:按关键词模糊检索、按分类精确过滤、按药食材反向检索。第三种场景最有意思,用户手里有一味陈皮,想知道哪些经典方剂用到了陈皮,这在SQL上是一个典型的多表关联查询。
SELECT DISTINCT f.id, f.name, f.source_book, f.category FROM fangji f INNER JOIN fangji_medicine_rel r ON f.id = r.fangji_id INNER JOIN medicine m ON r.medicine_id = m.id WHERE m.name = #{medicineName}考虑到MEDIUMINT级别的数据量,三条表通过索引JOIN的性能是足够的。但反向检索有多种变体:有的用户输入的是别名而非学名(比如“桂圆”而非“龙眼肉”),这时就要先用一个别名查询把药名归一到学名。实际业务中“按病检索”和“按症检索”也常见,但主治表述往往是长文本,“头痛”“风寒”这类词要模糊匹配indications字段,这会有一定的性能损耗。这里我用了MySQL的全文索引(FULLTEXT)来优化,比LIKE '%key%'效率提升非常明显。
ALTER TABLE fangji ADD FULLTEXT INDEX ft_indications (indications);注意全文索引在中文字符场景下需要正确的ngram分词插件配置,否则是按空格分词,对中文无效。MySQL 8.0可以配置ngram解析器,加上WITH PARSER ngram即可。
4.4 文件上传与图片管理
药材详情、用户头像都有图片上传需求。实现上我统一走接口方式:前端用Element UI的el-upload把图片传给后端的/api/upload接口,后端接收文件后用UUID重命名并存储到服务器磁盘指定目录,再把可访问的URL返回给前端。
@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); if (!allowedSuffixSet.contains(suffix.toLowerCase())) { return Result.error("不支持的文件类型"); } String newName = UUID.randomUUID().toString().replace("-", "") + suffix; String dirPath = uploadDir + DateUtil.format(new Date(), "yyyyMMdd"); File dir = new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, newName)); String url = "/files/" + DateUtil.format(new Date(), "yyyyMMdd") + "/" + newName; return Result.success(url); }这里有两个安全细节值得分享:一是文件类型必须做白名单校验(jpg、png、gif等),而不是黑名单,因为黑名单总会漏掉;二是存储路径按日期分目录,避免单个目录文件过多导致性能劣化。生产环境还会加上Nginx静态映射,把/files/**请求映射到上传目录,图片访问不经过Java进程,降低后端压力。
5. 前端页面的实践细节
5.1 Vue工程初始化与路由配置
前端工程用Vue CLI 4.x初始化,选择Router + Vuex + Sass的预设组合。路由设计按照功能模块划分:/login登录页、/register注册页、/home首页、/fangji/list经方列表、/fangji/detail/:id经方详情、/medicine/list药材列表、/medicine/detail/:id药材详情、/admin/fangji后台管理页面。
路由守卫是必须做的。beforeEach导航守卫中判断访问目标是否需要登录,需要登录则检查localStorage中是否有JWT令牌,没有则重定向到/login并带上redirect参数,登录成功后跳回原页面。这个细节虽然简单,但直接决定了系统的使用流畅度。
5.2 核心页面组件拆解
经方列表页是整个平台的招牌页面。组件结构上,顶栏是搜索区(关键词输入、分类筛选)、中间是el-table的经方列表(展示方名、出处、分类、简介)、底部是分页器。列表数据通过api/fangji/list接口获取,接口支持分页查询。
经方详情页是最有信息密度的页面。页面结构分上下两区:上半部分展示方剂名称、出处、分类等基本信息,下半部分用Tabs组件分三个标签页:组成与用法、主治与功效、相关笔记。用户可以在详情页点击收藏按钮,将经方加入自己的收藏夹;也可以在页面上写笔记。这些操作都基于当前用户的JWT令牌,后端解析用户ID后完成数据写入。
药材列表页增加了一个特色筛选条——“只看药食两用”,切换开关时请求参数携带isFoodMedicine=1,过滤出可日常食疗的药材,这个功能在界面上极大增强了平台的差异化特色,也方便了非专业用户的使用。
5.3 Axios请求封装与鉴权处理
Axios封装是前端项目的基础工程,统一处理了三件事:请求拦截、响应拦截、错误提示。请求拦截器负责附加JWT令牌:
service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error) )响应拦截器统一解析后端返回的JSON结构。我在前后端约定统一的返回格式:{ code: 200, message: 'success', data: ... }。code为200表示成功,其他均为业务错误或系统错误。拦截器判断code,不是200则用Element UI的Message组件弹出错误提示。此外,遇到HTTP 401状态码时,说明JWT已过期或无效,此时清除本地令牌并跳转到登录页。403则跳转到无权限提示页。
这个统一封装让每个业务页面都省去了异常处理逻辑,只需关心数据渲染即可。
6. 完整部署流程实录
6.1 开发环境配置
开发环境三件套:JDK 1.8、Node.js 12+、MySQL 8.0。后端用IntelliJ IDEA打开Maven工程,等待依赖下载完成,修改application.yml中的数据库连接信息后即可启动。前端用VSCode打开工程,执行npm install安装依赖,然后npm run serve启动开发模式,默认端口8080。
这里有一个开发阶段高频踩坑点:前端开发服务器(localhost:8080)和后端接口服务器(localhost:8081)端口不同,跨域问题立刻出现。本阶段最简单的解决办法是在Vue工程的vue.config.js中配置开发代理:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } }这样浏览器请求/api/login时,前端开发服务器会把它转发到后端,浏览器视角中请求始终是同源的,跨域问题消失。
6.2 后端打包与启动
后端打包直接使用Maven命令:
mvn clean package -DskipTests打包完成后,target目录下会生成fangji-system-0.0.1-SNAPSHOT.jar。部署时主要关注三件事:环境变量中需要配置数据库地址(如有需要)、上传目录要有写入权限、JWT密钥要更换为生产环境的随机值。
java -jar fangji-system-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod生产环境我建议显式指定profile,在application-prod.yml中覆盖数据库连接、日志级别等配置,避免把开发配置带入生产。日志级别设为WARN,减少不必要的日志写入。进程守护用systemd,配置ExecStart为启动命令,并设置Restart=always,这样服务意外退出后可以自动拉起。
6.3 前端构建与Nginx部署
前端构建命令是npm run build,产物是一个dist目录,里面是纯静态文件。部署的核心任务是把这些文件交给Nginx,并配置反向代理。
server { listen 80; server_name your-domain.com; root /var/www/fangji/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /files { alias /data/fangji/files/; } }try_files $uri $uri/ /index.html是Vue Router的history模式部署的关键配置。前端路由是虚拟路径,如果用户直接访问/fangji/detail/1刷新页面,Nginx需要把这个请求引导到index.html,否则会出现404。如果开发时用的是默认的hash模式,则不需要这个配置,但URL中会有#号,我在生产环境选择了history模式并配置了重写规则。
6.4 数据库初始化与迁移
部署阶段数据库迁移建议用SQL脚本文件+手工执行的方式,不推荐在服务启动时自动建表,以免误操作。项目上线第一版时提供了两个脚本文件:schema.sql负责建表,data.sql负责填充经方与药材的初始数据。
执行脚本前,先创建数据库并设定字符集:
CREATE DATABASE fangji_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE fangji_platform; SOURCE /path/to/schema.sql; SOURCE /path/to/data.sql;这里提醒一句:数据迁移脚本一定要带IF NOT EXISTS或先判断表是否存在再决定是否执行,避免重复执行脚本时报错。初始经方数据建议手工整理,确保古方原文、剂量、出处准确,切勿用网络爬虫盲目搞一堆低质量数据。我整理了一批常用方剂,包括桂枝汤、麻黄汤、小柴胡汤、四君子汤、四物汤等,每条数据都经过对照原文校对,这个工作虽然繁琐,但平台的专业性全靠它。
7. 常见问题与排查技巧实录
7.1 生产环境部署后接口返回404
这个问题很典型。开发环境一切正常,部署到生产后登录接口直接404。排查思路是:先用curl http://127.0.0.1:8081/api/login直连后端,确认Java服务本身是否正常;再检查Nginx的proxy_pass地址是否正确;最后确认前端构建时配置的baseURL是否指向了正确的路径。我遇到的实际情况是Nginx配置中proxy_pass末尾少写了一个斜杠,导致/api/login被转发成了/apiapi/login形式的错误路径,这种低级错误极难发现,排查时建议直接看Nginx的error.log。
7.2 中文乱码问题
乱码问题的根源几乎永远是字符集不一致。检查点按优先级排列:数据库连接URL是否带characterEncoding=utf8mb4、数据库表和字段字符集是否为utf8mb4、后端响应是否声明UTF-8。SpringBoot中要确保spring.http.encoding.force=true,强制使用UTF-8编码。还有一个隐藏坑是Linux系统环境的LANG变量,如果系统默认locale不是UTF-8,Java进程读取外部文件时可能使用错误编码。
7.3 图片上传成功但无法访问
处理这个问题需要检查三个环节:图片是否上传成功(看服务器目录)、文件URL是否正确返回、Nginx静态映射是否生效。常见的坑是上传目录配置了相对路径,但Java进程的工作目录与预期不一致。解决方案是永远使用绝对路径,不要依赖相对路径。另外,如果Nginx配置了alias,需要确认路径末尾是否带斜杠,alias /data/files/和alias /data/files在拼接时结果不同。
7.4 高并发收藏导致数据不一致
收藏按钮被快速点击多次,可能产生重复收藏记录。数据库层面联合唯一索引直接拦截了重复数据,但异常会抛给前端,表现为有报错提示。更好的方案是前端在用户点击收藏后立即禁用按钮并显示“已收藏”,等接口返回后再恢复交互。后端也可以再加一层逻辑:先查后插,存在则返回“已收藏”,不存在则执行插入,这样用户体验更友好。
我实际开发中还遇到过MySQL死锁问题,排查后发现原因是多请求并发操作同一用户多条收藏记录时,批量删除再插入的SQL造成了锁竞争。解决方案是避免先删除后插入的复合操作,改用INSERT ... ON DUPLICATE KEY UPDATE做幂等写入。
7.5 前端项目初始化踩坑
创建Vue项目时,使用npm install出现依赖版本冲突是最常见的。Vue 2.6项目要特别留意Node.js版本,Node 17以上版本在安装旧版node-sass时经常报错,因为node-sass需要从源码编译,新版Node的编译链不一致。解决方案有两种:一是固定使用Node 14 LTS版本开发;二是把node-sass替换成dart-sass,即使用sass包。
这个项目当时就遇到过这个问题,折腾了一晚上,最后把node-sass改成sass包,并在vue.config.js中配置css.loaderOptions.scss.additionalData后顺利解决。
最后再分享一个细节
这个项目让我最感慨的其实是数据整理环节。技术架构、前端交互、部署上线这些都能在一次次的调试中推进,但经方数据、药食同源信息的准确性,需要非常认真谨慎地对待。我最初从公开资料中整理了500多条药材信息,经过与权威书籍核对后,剔除了不少不规范或有争议的条目,最终入库约350条,保证了平台内容的专业底线。
技术上还有一个后续可以优化的方向:当前收藏和笔记功能是相对独立的,后续可以做基于用户行为的推荐——比如收藏了桂枝汤的用户,大概率也对小柴胡汤感兴趣,这类关联推荐可以通过分析收藏关联度实现,能显著提升用户的使用时长。另外,移动端适配目前做了解析度适配,但更彻底的方案是后续单独做一个uni-app版本或小程序版本,而后端的JWT认证接口完全可以复用,不需要额外开发。
如果已经看到了这里,说明你对这个项目是真有兴趣。动手复现一遍,从建库建表到前后端联调再到部署上线,然后把数据换成你自己关注的领域,你会发现前后端分离的这套架构其实充满了通用性。有任何卡住的地方,欢迎按照上面的排查思路回溯,大多数问题都出在配置细节上。