1. 项目概述:一个“加密”的考研单词小程序
最近在带学生做毕设,发现一个挺有意思的选题:“基于信息加密的考研英语单词小程序”。乍一看,这标题有点“缝合怪”的意思,把“SpringBoot”、“信息加密”、“考研英语”、“小程序”这几个看似不搭界的技术和领域揉在了一起。但仔细琢磨,这其实是一个非常有代表性的、能体现综合能力的本科毕业设计项目。它不像一个纯粹的CRUD管理系统,而是要求你思考:一个学习工具,为什么需要“加密”?“加密”的对象是什么?是单词数据本身,还是用户的学习记录?这背后其实是对数据安全、用户隐私以及特定场景下功能设计的综合考量。
这个项目的核心,是构建一个服务于考研学生的英语单词学习微信小程序。它的“信息加密”特性,是区别于普通单词App的关键。想象一下,用户可能不想让别人随意翻看自己的错题本、收藏的生词,或者学习进度。因此,对用户的核心学习数据(如个人词库、学习记录、测试成绩)进行加密存储和传输,就成了一种合理的、提升产品专业感和用户信任度的设计。整个技术栈非常经典:后端用SpringBoot搭建RESTful API,处理业务逻辑和数据加密;前端用微信小程序原生框架或UniApp,提供流畅的移动端体验;最后通过Docker等工具完成部署,形成从开发到上线的完整闭环。这个项目麻雀虽小,五脏俱全,非常适合计算机相关专业的同学用来巩固Web全栈开发技能,并深入理解数据安全在实际应用中的落地。
2. 项目整体设计与核心思路拆解
2.1 为什么是“考研英语”+“小程序”+“加密”?
这个选题的成功之处在于,它精准地抓住了三个关键点: 垂直场景 、 高频载体 和 技术亮点 。
首先, 垂直场景(考研英语) 意味着需求明确且集中。考研单词有明确的范围(如考研大纲5500词),学习模式固定(背、测、复习),用户痛点清晰(记忆效率、抗遗忘、真题语境)。这让你不需要设计一个功能庞杂的通用词典,而是可以深度聚焦于“艾宾浩斯记忆曲线”、“词频分级”、“真题例句库”等针对性功能,做深做透。
其次, 高频载体(微信小程序) 是触达用户的最优解。对于学生群体,微信是最高频的社交和应用入口。小程序无需安装、即用即走、分享便捷的特性,完美契合了单词学习这种碎片化、轻量化的需求。用户可以在食堂排队、课间休息时随时打开刷几个单词,学习路径极短。
最后,
技术亮点(信息加密)
为项目赋予了学术深度和区分度。它迫使你超越简单的增删改查,去思考数据生命周期中的安全问题。这里的“加密”通常不是指对单词文本本身加密(那是公开知识),而是对
用户产生的私有数据
进行保护。例如,用户自定义的单词笔记、收藏的生词本、每日的学习打卡记录、模拟测试的错题集等。这些数据属于用户隐私,在存储(数据库)和传输(网络)过程中进行加密,是符合《网络安全法》和用户期待的良好实践。这让你有机会在毕设中展示对
AES
、
RSA
等加密算法,或Spring Security安全框架的理解与应用。
2.2 技术栈选型背后的逻辑
技术选型不是堆砌热门词汇,而是为项目目标服务。下面这个表格拆解了核心选型及其理由:
| 技术组件 | 选型建议 | 核心理由与考量 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 约定大于配置,快速搭建REST API。生态成熟(MyBatis-Plus, Security, Redis),社区资源丰富,便于调试和解决问题。 |
| 数据持久层 | MyBatis-Plus | 在MyBatis基础上增强,提供通用CRUD方法,极大减少单词、用户、记录等实体类的单调SQL编写工作,让你更专注于业务逻辑。 |
| 数据库 | MySQL 8.0 | 关系型数据库,适合存储结构化的单词表、用户表、学习记录表。事务支持完善,确保如“学习-更新进度”这类操作的数据一致性。 |
| 缓存 | Redis | 存储用户会话Token、热点单词数据、每日学习任务状态。大幅提升高频查询(如获取今日单词列表)的响应速度。 |
| 加密组件 | Jasypt / 自研工具类 | 用于加密数据库中的敏感字段。Jasypt可与Spring Boot无缝集成,配置简单。也可基于Java Cryptography Architecture (JCA) 自行封装AES工具类,更灵活。 |
| 前端框架 | 微信小程序原生 / UniApp |
原生框架
:性能最优,与微信能力结合最紧密,文档齐全。
UniApp :一套代码可发布到多端(小程序、H5、App),如果考虑项目未来扩展性,这是更好选择。对于毕设,原生框架更直接。 |
| 部署与运维 | Docker + Docker Compose | 将Spring Boot应用、MySQL、Redis分别容器化。实现环境隔离、一键部署、快速迁移。极大简化了从本地开发到服务器部署的复杂度,是当代应用部署的标配。 |
注意 :在技术选型上切忌贪多求全。例如,看到“微服务”热门就给这个单应用项目强行拆分服务,只会增加不必要的复杂度。毕设的核心是证明你有能力用合适的技术解决一个定义清晰的问题。
2.3 系统核心模块与功能规划
基于上述分析,我们可以将系统划分为以下几个核心模块:
-
用户管理模块
:实现微信一键登录(获取
openid和unionid)、用户信息维护。这里是加密的起点,用户的唯一标识是后续所有数据关联和加密的密钥因子之一。 - 单词核心数据模块 :管理考研大纲单词库,包含单词、音标、释义、例句(最好来自真题)、词频等级。这部分数据是公开的,通常不需加密,但要做好缓存。
-
个性化学习模块
:这是系统的
大脑
,也是
加密的重点区域
。
- 智能词本 :根据艾宾浩斯曲线,为每个用户生成动态的每日学习、复习单词列表。
- 学习记录 :记录用户每个单词的学习次数、掌握程度、最后学习时间。这些数据需要加密存储。
- 收藏与笔记 :用户对某个单词添加的个人笔记和收藏状态,属于高隐私数据,必须加密。
- 测试与评估模块 :提供拼写、选择、填空等多种形式的测试,并生成测试报告和错题本。错题本数据需要加密。
- 数据加密与安全模块 :这是项目的技术核心。负责定义哪些数据需要加密,在何时(存储前/传输中)加密,使用何种算法(如AES对称加密),以及密钥如何管理(如使用用户密码的衍生物或独立的密钥存储服务)。
3. 核心细节解析与实操要点
3.1 “信息加密”的具体落地策略
“信息加密”不能是一个空泛的概念,必须在数据库表设计中体现出来。我们以
user_word_record
(用户单词学习记录表)为例,说明加密字段的设计。
未加密的简单设计可能如下:
CREATE TABLE `user_word_record` (
`id` bigint PRIMARY KEY,
`user_id` bigint NOT NULL COMMENT '用户ID',
`word_id` bigint NOT NULL COMMENT '单词ID',
`mastery_level` tinyint DEFAULT 0 COMMENT '掌握程度 (0-5)',
`review_count` int DEFAULT 0 COMMENT '复习次数',
`last_review_time` datetime COMMENT '最后复习时间',
`personal_notes` varchar(500) COMMENT '个人笔记',
`is_collected` tinyint(1) DEFAULT 0 COMMENT '是否收藏'
);
在这个设计中,
personal_notes
(个人笔记)以明文存储。如果数据库被拖库,或者有内部人员不当访问,用户的隐私笔记将一览无余。
引入加密后的设计:
CREATE TABLE `user_word_record` (
`id` bigint PRIMARY KEY,
`user_id` bigint NOT NULL COMMENT '用户ID',
`word_id` bigint NOT NULL COMMENT '单词ID',
`mastery_level` tinyint DEFAULT 0 COMMENT '掌握程度 (0-5)', -- 可加密,但非必须
`review_count` int DEFAULT 0 COMMENT '复习次数', -- 通常不需加密
`last_review_time` datetime COMMENT '最后复习时间', -- 通常不需加密
`personal_notes_encrypted` varchar(1000) COMMENT '加密后的个人笔记', -- 密文
`notes_iv` varchar(64) COMMENT '加密初始向量(IV)', -- 存储IV,用于AES CBC/CFB等模式
`is_collected` tinyint(1) DEFAULT 0 COMMENT '是否收藏' -- 可加密
);
加密操作在服务层的实现(Java示例):
@Service
public class EncryptionService {
// 假设我们从配置或安全的密钥服务中获取密钥,这里简化为一个配置项
@Value("${aes.secret.key}")
private String secretKey;
private static final String AES_TRANSFORMATION = "AES/CBC/PKCS5Padding";
/**
* 加密文本
* @param plainText 明文
* @return 加密后的Base64字符串和IV,用特定分隔符连接或分别返回
*/
public String encrypt(String plainText) throws Exception {
if (StringUtils.isBlank(plainText)) {
return plainText;
}
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
// 生成随机的初始向量IV,确保相同明文每次加密结果不同
IvParameterSpec iv = new IvParameterSpec(generateRandomIv());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
String encryptedText = Base64.getEncoder().encodeToString(encryptedBytes);
String ivString = Base64.getEncoder().encodeToString(iv.getIV());
// 返回格式:IV:密文,便于存储和后续解密
return ivString + ":" + encryptedText;
}
/**
* 解密文本
* @param encryptedTextWithIv 格式为 "IV:密文" 的字符串
* @return 解密后的明文
*/
public String decrypt(String encryptedTextWithIv) throws Exception {
if (StringUtils.isBlank(encryptedTextWithIv)) {
return encryptedTextWithIv;
}
String[] parts = encryptedTextWithIv.split(":");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid encrypted text format");
}
byte[] iv = Base64.getDecoder().decode(parts[0]);
byte[] encryptedBytes = Base64.getDecoder().decode(parts[1]);
Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
private byte[] generateRandomIv() {
// 对于AES CBC模式,IV长度需为16字节
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
return iv;
}
}
在业务逻辑中,当需要保存用户的笔记时,调用
encrypt()
方法,将返回的字符串存入
personal_notes_encrypted
字段,同时将IV部分(或从返回字符串中解析)存入
notes_iv
字段(示例中合并存储了)。查询时,取出字段值,调用
decrypt()
方法解密后返回给前端。
实操心得:密钥管理是命门
- 绝对不要硬编码 :示例中的
aes.secret.key必须通过环境变量或配置中心注入,绝不能写在代码里提交到Git。- 考虑密钥分离 :对于更严格的场景,可以考虑使用“信封加密”。即用一把主密钥(Master Key)加密数据密钥(Data Key),数据密钥再加密实际数据。主密钥由硬件安全模块(HSM)或云服务商(如阿里云KMS)管理。
- 影响查询 :字段一旦加密,就失去了数据库层面的模糊查询、排序等功能。如果你需要根据笔记内容搜索,这个设计就不合适,可能需要考虑应用层全文检索或其他方案。对于“是否收藏”这种布尔值字段,加密要谨慎,因为它会使得“查询所有收藏单词”这样的简单操作变得极其低效(需要解密所有记录后再过滤)。
3.2 微信小程序登录与用户标识
小程序获取用户身份是通过微信的
wx.login()
和
code2Session
接口。这里的关键是理解
openid
和
unionid
。
-
openid:用户在 当前小程序 下的唯一标识。 -
unionid:用户在 同一微信开放平台账号 下的所有应用(多个小程序、公众号、App)中的唯一标识。如果项目未来可能扩展,在数据库设计用户表时,应预留unionid字段。
后端登录校验流程:
-
小程序端调用
wx.login()获取临时code。 -
小程序端将
code发送给你的SpringBoot后端。 -
后端携带
code、appid、secret调用微信接口服务https://api.weixin.qq.com/sns/jscode2session。 -
微信返回
session_key和openid(及unionid)。 -
后端生成自定义登录态
:用UUID生成一个唯一的
token,将token与openid、session_key的关联关系存入Redis(设置过期时间,如7天)。 -
将
token返回给小程序端。 -
小程序端后续请求,在HTTP Header(如
Authorization: Bearer <token>)中携带此token。 -
后端拦截器(Interceptor)或过滤器(Filter)校验
token有效性,并从Redis中取出对应的openid,从而在本次请求上下文中标识用户。
这个
openid
就是关联用户所有加密数据的“外键”。在设计加密方案时,甚至可以将其作为加密密钥生成的一个因子(但需注意
openid
本身可能不变,且一旦泄露风险较大,通常不建议直接用作密钥)。
4. 实操过程与核心环节实现
4.1 SpringBoot后端项目搭建与核心配置
使用Spring Initializr(start.spring.io)快速生成项目,依赖选择:
Spring Web
,
MyBatis Framework
,
MySQL Driver
,
Redis
,
Lombok
。
关键配置
application.yml
:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/word_app?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: your_strong_password
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
password: # 如果设置了密码
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 自动转换下划线命名到驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,调试用,生产环境关闭
global-config:
db-config:
id-type: ASSIGN_ID # 使用雪花算法生成主键ID
logic-delete-field: isDeleted # 全局逻辑删除字段名(若有)
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
# 自定义加密密钥(务必通过环境变量注入,此处仅为示例)
aes:
secret-key: ${AES_SECRET_KEY:ThisIsASampleKeyForDemoOnly123!} # 优先从环境变量AES_SECRET_KEY读取
# 微信小程序配置
wechat:
mp:
app-id: ${WECHAT_APP_ID}
secret: ${WECHAT_SECRET}
项目结构建议:
src/main/java/com/yourdomain/wordapp/
├── WordAppApplication.java
├── config/ # 配置类
│ ├── WebMvcConfig.java # 拦截器、跨域配置
│ ├── RedisConfig.java
│ └── MybatisPlusConfig.java
├── interceptor/ # 拦截器
│ └── AuthInterceptor.java # Token认证拦截器
├── aspect/ # 切面(可选,用于加解密自动处理)
├── controller/ # 控制器
│ ├── api/
│ │ ├── AuthController.java
│ │ ├── WordController.java
│ │ └── StudyRecordController.java
├── service/ # 服务层
│ ├── impl/
│ │ ├── EncryptionServiceImpl.java
│ │ ├── UserServiceImpl.java
│ │ └── WordStudyServiceImpl.java
├── mapper/ # MyBatis Mapper接口
├── entity/ # 实体类,对应数据库表
├── dto/ # 数据传输对象,用于前后端交互
├── vo/ # 视图对象,用于返回给前端的数据封装
└── utils/ # 工具类
├── JsonResult.java # 统一API响应封装
├── WeChatUtil.java # 微信接口调用工具
└── AESUtil.java # 加密解密工具类
4.2 关键业务逻辑实现:获取今日学习单词
这是学习模块的核心。假设我们已经有了
word
(单词表)和
user_word_record
(用户学习记录表)。
步骤解析:
- 确定用户和计划 :根据用户ID,获取其学习计划(例如,每天新学20个词,复习50个词)。
-
获取“新词”
:从
word表中,筛选出该用户user_word_record中不存在的单词,按词频或随机排序,取前N个作为今日新学单词。 -
获取“待复习词”
:根据艾宾浩斯曲线,复习点通常在学习的第1、2、4、7、15天。计算用户
user_word_record中,last_review_time满足这些间隔天数的单词,且mastery_level未达到最高级(如5级)的单词,作为待复习单词。 -
合并与返回
:合并新词和复习词列表,返回给前端。同时,为这些单词在
user_word_record中创建或更新一条“待学习”状态的记录。
Service层核心代码逻辑:
@Service
@Slf4j
public class WordStudyServiceImpl implements WordStudyService {
@Autowired
private WordMapper wordMapper;
@Autowired
private UserWordRecordMapper recordMapper;
@Override
public List<WordVO> getTodayWords(Long userId) {
// 1. 获取用户学习计划(可从用户配置表读取,这里简化为常量)
int newWordCount = 20;
int reviewWordCount = 50;
// 2. 获取今日新词
List<Word> newWords = wordMapper.selectNewWordsForUser(userId, newWordCount);
// 3. 获取待复习词(基于艾宾浩斯曲线)
List<UserWordRecord> recordsToReview = recordMapper.selectWordsDueForReview(userId, reviewWordCount);
List<Long> reviewWordIds = recordsToReview.stream().map(UserWordRecord::getWordId).collect(Collectors.toList());
List<Word> reviewWords = new ArrayList<>();
if (!reviewWordIds.isEmpty()) {
reviewWords = wordMapper.selectBatchIds(reviewWordIds);
}
// 4. 合并列表,并转换为前端需要的VO对象
List<WordVO> todayList = new ArrayList<>();
newWords.forEach(w -> {
WordVO vo = convertToVO(w);
vo.setStudyType("NEW"); // 标记为新词
todayList.add(vo);
// 异步或事务内:初始化一条学习记录,状态为“待学习”
initLearningRecord(userId, w.getId());
});
reviewWords.forEach(w -> {
WordVO vo = convertToVO(w);
vo.setStudyType("REVIEW"); // 标记为复习词
todayList.add(vo);
});
// 5. 打乱顺序,避免总是先新词后复习词
Collections.shuffle(todayList);
return todayList;
}
private void initLearningRecord(Long userId, Long wordId) {
UserWordRecord record = new UserWordRecord();
record.setUserId(userId);
record.setWordId(wordId);
record.setMasteryLevel(0);
record.setReviewCount(0);
record.setLastReviewTime(new Date());
recordMapper.insert(record);
}
// ... convertToVO 等方法
}
对应的
WordMapper.xml
中需要编写
selectNewWordsForUser
的SQL,这是一个典型的“排除已存在”查询:
<select id="selectNewWordsForUser" resultType="com.yourdomain.wordapp.entity.Word">
SELECT w.* FROM word w
WHERE w.id NOT IN (
SELECT r.word_id FROM user_word_record r WHERE r.user_id = #{userId}
)
ORDER BY w.frequency_level DESC, RAND() -- 按词频降序,并随机
LIMIT #{limit}
</select>
4.3 微信小程序端核心页面交互
小程序端主要页面包括:首页(今日单词列表)、单词学习详情页、测试页、个人中心页。
首页 (
index.js
数据加载示例):
Page({
data: {
todayWords: [], // 今日单词列表
loading: false
},
onLoad: function() {
this.loadTodayWords();
},
loadTodayWords: function() {
this.setData({ loading: true });
const that = this;
// 调用后端API,假设接口为 /api/study/today-words
wx.request({
url: 'https://your-api-domain.com/api/study/today-words',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}` // 携带登录token
},
success(res) {
if (res.statusCode === 200 && res.data.code === 200) {
that.setData({
todayWords: res.data.data,
loading: false
});
} else {
wx.showToast({ title: '加载失败', icon: 'none' });
}
},
fail(err) {
wx.showToast({ title: '网络错误', icon: 'none' });
that.setData({ loading: false });
}
});
},
// 点击单词卡片,跳转到学习详情页
navigateToDetail: function(e) {
const wordId = e.currentTarget.dataset.id;
const word = this.data.todayWords.find(w => w.id === wordId);
// 传递单词信息和学习类型
wx.navigateTo({
url: `/pages/wordDetail/wordDetail?id=${wordId}&type=${word.studyType}`
});
}
})
单词详情页 (
wordDetail.js
):
展示单词详情、例句,并提供“认识”、“不认识”、“收藏”、“添加笔记”等操作按钮。当用户点击“添加笔记”并保存时,小程序端将笔记内容发送到后端,后端会先调用加密服务加密,再存入数据库。
5. 部署上线:从本地到服务器的完整流程
5.1 使用Docker容器化部署
这是现代应用部署的最佳实践,能保证环境一致性。
1. 编写后端SpringBoot应用的
Dockerfile
:
# 使用多阶段构建,减小镜像体积
# 第一阶段:构建
FROM maven:3.8.6-eclipse-temurin-11 AS build
WORKDIR /app
COPY pom.xml .
# 利用缓存,先下载依赖
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行
FROM eclipse-temurin:11-jre-focal
WORKDIR /app
# 从构建阶段拷贝jar包
COPY --from=build /app/target/*.jar app.jar
# 设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 暴露端口
EXPOSE 8080
# 启动命令,通过环境变量传递JVM参数和Spring配置
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "-Djava.security.egd=file:/dev/./urandom", "app.jar"]
2. 编写
docker-compose.yml
编排所有服务:
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: wordapp-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} # 从.env文件读取
MYSQL_DATABASE: word_app
MYSQL_USER: word_user
MYSQL_PASSWORD: ${DB_USER_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./config/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化脚本
ports:
- "3306:3306"
networks:
- wordapp-network
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: wordapp-redis
command: redis-server --requirepass ${REDIS_PASSWORD} # 设置密码
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- wordapp-network
restart: unless-stopped
backend:
build: ./backend # Dockerfile所在目录
container_name: wordapp-backend
environment:
# 关键配置通过环境变量传入,安全且灵活
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/word_app?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: word_user
SPRING_DATASOURCE_PASSWORD: ${DB_USER_PASSWORD}
SPRING_REDIS_HOST: redis
SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD}
AES_SECRET_KEY: ${AES_SECRET_KEY} # 加密密钥
WECHAT_APP_ID: ${WECHAT_APP_ID}
WECHAT_SECRET: ${WECHAT_SECRET}
ports:
- "8080:8080"
depends_on:
- mysql
- redis
networks:
- wordapp-network
restart: unless-stopped
volumes:
mysql_data:
redis_data:
networks:
wordapp-network:
driver: bridge
3. 创建
.env
文件(切勿提交至Git):
DB_ROOT_PASSWORD=your_mysql_root_password
DB_USER_PASSWORD=your_mysql_user_password
REDIS_PASSWORD=your_redis_password
AES_SECRET_KEY=your_strong_aes_encryption_key_here_32bytes
WECHAT_APP_ID=your_wechat_appid
WECHAT_SECRET=your_wechat_secret
4. 部署命令:
在服务器上安装好Docker和Docker Compose后,将项目文件上传,进入包含
docker-compose.yml
的目录,执行:
# 启动所有服务(后台运行)
docker-compose up -d
# 查看日志
docker-compose logs -f backend
# 停止服务
docker-compose down
5.2 小程序发布前准备
-
配置服务器域名
:在小程序管理后台的“开发”->“开发设置”->“服务器域名”中,将你的后端API域名(如
https://api.yourdomain.com)添加到request合法域名列表中。 - 配置业务域名 (如果需要WebView):如果小程序内嵌了H5页面,需要在此配置。
- 上传代码 :在微信开发者工具中,点击“上传”,填写版本号和备注。
- 提交审核 :登录小程序管理后台,在“管理”->“版本管理”中,找到上传的版本,提交审核。审核通过后,即可发布。
6. 常见问题与排查技巧实录
在开发和部署过程中,你几乎一定会遇到以下问题。这里记录了我的排查思路和解决方法。
6.1 后端服务启动报错:数据库连接失败
现象
:SpringBoot应用启动时,控制台报错
Communications link failure
或
Access denied for user
。
排查步骤:
-
检查Docker容器状态
:
docker-compose ps确保mysql和redis容器是Up状态。 -
检查容器内网络
:进入backend容器,尝试ping
mysql和redis服务名。docker exec -it wordapp-backend /bin/sh ping mysql nc -zv mysql 3306 # 检查端口连通性 -
检查数据库用户权限
:进入mysql容器,验证
word_user用户是否拥有对word_app数据库的权限。docker exec -it wordapp-mysql mysql -uroot -p # 输入密码后 USE mysql; SELECT Host, User FROM user WHERE User='word_user'; GRANT ALL PRIVILEGES ON word_app.* TO 'word_user'@'%'; # 如果权限不足 FLUSH PRIVILEGES; -
核对环境变量
:确认
docker-compose.yml和.env文件中的数据库密码、用户名是否正确。特别注意URL中的useSSL=false&allowPublicKeyRetrieval=true在MySQL 8.0中有时是必需的。 -
查看MySQL日志
:
docker-compose logs mysql查看是否有更详细的错误信息。
6.2 小程序真机调试时,请求后端API失败
现象
:开发者工具预览正常,但手机真机扫描调试时,网络请求报错
fail url not in domain list
或超时。
排查步骤:
- 确认域名已配置 :首先检查小程序管理后台的“服务器域名”是否已正确添加你的后端域名( 必须是HTTPS )。
- 检查域名备案与SSL证书 :你的服务器域名必须已完成ICP备案,并且配置了有效的SSL证书(如Let‘s Encrypt免费证书)。真机环境强制要求HTTPS。
- 关闭开发环境不校验域名选项 :在开发者工具右上角“详情”->“本地设置”中, 不要勾选“不校验合法域名...” 。这个选项只在工具内生效,真机无效。必须在真机环境下测试配置是否正确。
- 检查服务器防火墙和安全组 :确保云服务器(如阿里云、腾讯云)的安全组规则已开放8080端口(或你的后端服务端口)。
-
使用Nginx反向代理(推荐)
:直接暴露SpringBoot的8080端口不专业。应该用Nginx监听80/443端口,反向代理到后端服务。
然后将小程序配置的域名改为# Nginx 配置示例 (部分) server { listen 443 ssl; server_name api.yourdomain.com; ssl_certificate /path/to/your/cert.pem; ssl_certificate_key /path/to/your/key.pem; location / { proxy_pass http://backend:8080; # 指向docker-compose中的backend服务 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }https://api.yourdomain.com。
6.3 加密字段解密后乱码或报错
现象
:从数据库读取加密字段并解密时,抛出
BadPaddingException
或其他解密异常。
排查步骤:
-
核对加密解密流程一致性
:确保加密和解密使用的是
相同的算法
、
相同的模式
(如
AES/CBC/PKCS5Padding)、 相同的密钥 和 相同的IV 。一个字符的差异都会导致失败。 -
检查IV的存储与传递
:如果使用CBC等需要IV的模式,必须将加密时生成的随机IV存储下来,并在解密时原样使用。检查数据库中是单独存储IV字段,还是按约定格式(如
IV:密文)合并存储,解密时分割是否正确。 -
检查字符编码
:在加密前(
String.getBytes())和解密后(new String(bytes))务必使用相同的字符编码,强烈建议统一使用StandardCharsets.UTF_8。 - 检查数据完整性 :确保从数据库读取到解密前,密文字符串没有被意外截断或修改。打印出从数据库读取的密文长度和内容的前后若干字符进行比对。
- 密钥管理问题 :检查生产环境和开发环境的加密密钥是否不同。如果密钥不一致,用开发环境密钥加密的数据在生产环境自然无法解密。确保密钥通过安全的方式(环境变量)注入,且在不同环境正确配置。
6.4 学习进度同步逻辑错误
现象 :用户在多设备学习,进度偶尔不同步或重复计算。
解决方案设计:
-
乐观锁
:在
user_word_record表增加一个version字段(版本号)。更新学习记录时,带上查询时的版本号。
如果更新影响行数为0,说明记录已被其他请求修改,本次更新失败,应提示用户或重新获取数据后重试。UPDATE user_word_record SET mastery_level = ?, review_count = ?, last_review_time = ?, version = version + 1 WHERE id = ? AND version = ?; -
使用Redis分布式锁
:对于关键进度更新操作(如完成一次测试,批量更新多个单词掌握程度),可以使用Redis的
SET key value NX EX seconds命令实现一个简单的分布式锁,确保同一时间只有一个请求能执行更新逻辑。 - 最终一致性补偿 :对于非强一致性的场景(如学习总时长统计),可以允许短暂不一致,然后通过定时任务汇总各设备的日志,计算出一个最终一致的统计值。
在毕设答辩中,如果你能清晰地阐述在“信息加密”和“多端同步”这类细节上的思考、实现方案以及遇到的坑和解决方案,无疑会大大增加项目的技术深度和你的个人印象分。这个项目看似简单,但把每个环节做扎实、想透彻,就是一个非常出色的全栈实践。

653

被折叠的 条评论
为什么被折叠?



