作为一名长期从事传统文化数字化传播的技术从业者,我注意到古典舞爱好者们一直缺乏专业的线上交流空间。去年参与某省级歌舞剧院数字化项目时,我们团队决定用SpringBoot技术栈构建一个专属平台。这个项目从需求分析到最终上线历时4个月,期间经历了三次大的架构调整,最终形成了现在这个稳定运行的版本。
这个平台最核心的价值在于:它不仅仅是一个展示古典舞视频的网站,而是整合了课程学习、服饰文化、社群交流、在线交易的完整生态。对于舞蹈爱好者而言,可以在这里找到从入门到进阶的全套学习资源;对于舞蹈教师和机构,则提供了内容变现的渠道。下面我将从技术实现角度,详细解析这个平台的构建过程。
在项目启动阶段,我们对比了三种主流技术方案:
最终选择SpringBoot+MySQL的组合主要基于以下考量:
实际开发中发现:SpringBoot的自动配置特性为我们节省了约40%的底层配置时间,特别是在整合MyBatis和Redis时,starter依赖几乎解决了所有兼容性问题。
系统采用经典的三层架构,但针对舞蹈平台特性做了特殊设计:
code复制表现层
├── Web前端(Thymeleaf模板)
├── 移动端API(预留接口)
业务层
├── 用户服务(注册/登录/个人中心)
├── 内容服务(视频/课程/服饰)
├── 社区服务(论坛/评论)
├── 交易服务(购物车/订单)
数据层
├── MySQL(结构化数据)
├── Redis(会话/缓存)
├── 文件存储(视频/图片)
特别说明论坛模块的设计:我们没有采用实时通信方案,而是基于"发帖-回帖"的异步模式。这虽然牺牲了部分即时性,但大幅降低了服务器压力,实测单台2核4G的云服务器可支持500人同时在线交流。
采用改良版的RBAC模型,关键数据表设计:
sql复制CREATE TABLE `sys_user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '密码',
`salt` varchar(20) COMMENT '加密盐值',
`avatar` varchar(255) COMMENT '头像URL',
`dance_level` tinyint DEFAULT 1 COMMENT '舞蹈等级1-10',
`create_time` datetime COMMENT '创建时间',
PRIMARY KEY (`user_id`),
UNIQUE KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `sys_role` (
`role_id` bigint NOT NULL AUTO_INCREMENT,
`role_name` varchar(30) NOT NULL,
`role_key` varchar(30) NOT NULL,
PRIMARY KEY (`role_id`)
);
-- 用户-角色关联表
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL,
`role_id` bigint NOT NULL,
PRIMARY KEY (`user_id`, `role_id`)
);
密码加密采用SHA-256 + 随机盐的方案,核心Java实现:
java复制public class PasswordUtil {
private static final SecureRandom random = new SecureRandom();
public static String generateSalt() {
byte[] salt = new byte[16];
random.nextBytes(salt);
return Hex.encodeHexString(salt);
}
public static String encryptPassword(String password, String salt) {
String algorithm = "SHA-256";
int iterations = 1000;
byte[] hash = PBKDF2(password, salt, iterations);
return Hex.encodeHexString(hash);
}
private static byte[] PBKDF2(String password, String salt, int iterations) {
// 实现细节省略...
}
}
古典舞教学视频对画质要求较高,我们开发了专门的转码模块:
使用FFmpeg命令示例:
bash复制# 转码720P版本
ffmpeg -i input.mp4 -vf scale=1280:720 -c:v libx264 -preset medium -crf 23 \
-c:a aac -b:a 128k output_720p.mp4
# 提取封面
ffmpeg -i input.mp4 -ss 00:00:05 -vframes 1 -q:v 2 cover.jpg
实际部署时发现:直接使用Java Runtime.exec()调用FFmpeg存在进程管理问题,后来改用Apache Commons Exec解决了进程挂起和超时控制。
初期采用最简单的Session存储方案,在压力测试时出现了两个典型问题:
最终解决方案:
java复制public boolean addToCart(Long userId, Long courseId, int quantity) {
String lockKey = "cart_lock:" + userId;
String lockValue = UUID.randomUUID().toString();
try {
// 获取分布式锁(设置3秒过期)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 真正的购物车逻辑
return doAddToCart(userId, courseId, quantity);
}
return false;
} finally {
// 释放锁(Lua脚本保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue);
}
}
论坛模块需要过滤政治敏感词和舞蹈领域的特殊词汇(如不专业动作术语)。我们采用DFA算法实现多级过滤:
算法核心:
java复制public class SensitiveFilter {
private class TrieNode {
private boolean isEnd;
private Map<Character, TrieNode> subNodes = new HashMap<>();
public void addSubNode(Character key, TrieNode node) {
subNodes.put(key, node);
}
public TrieNode getSubNode(Character key) {
return subNodes.get(key);
}
}
private TrieNode root = new TrieNode();
public void addWord(String lineText) {
TrieNode tempNode = root;
for (int i = 0; i < lineText.length(); i++) {
Character c = lineText.charAt(i);
TrieNode node = tempNode.getSubNode(c);
if (node == null) {
node = new TrieNode();
tempNode.addSubNode(c, node);
}
tempNode = node;
}
tempNode.isEnd = true;
}
public String filter(String text) {
// 实现过滤逻辑...
}
}
课程列表页的N+1查询问题:
sql复制-- 原始查询(性能差)
SELECT * FROM course WHERE status = 1; -- 获取50条课程
-- 对每条课程执行:
SELECT * FROM teacher WHERE teacher_id = ?;
优化方案:
<collection>标签实现一对多查询sql复制ALTER TABLE course ADD INDEX idx_status_price (status, price);
ALTER TABLE course_comment ADD INDEX idx_course_id (course_id);
采用多级缓存架构:
缓存更新策略对比:
| 策略类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 定时过期 | 实现简单 | 实时性差 | 变更不频繁的数据 |
| 主动更新 | 实时性强 | 系统复杂度高 | 核心业务数据 |
| 延迟双删 | 平衡性较好 | 实现较复杂 | 高并发场景 |
我们最终对课程详情采用"Cache Aside Pattern":
java复制public Course getCourseById(Long id) {
// 1. 先查缓存
String cacheKey = "course:" + id;
Course course = redisTemplate.opsForValue().get(cacheKey);
if (course != null) {
return course;
}
// 2. 查数据库
course = courseMapper.selectById(id);
if (course == null) {
return null;
}
// 3. 写入缓存(设置5分钟过期)
redisTemplate.opsForValue().set(
cacheKey,
course,
5,
TimeUnit.MINUTES);
return course;
}
生产环境采用Docker Compose部署:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
redis:
image: redis:6
command: redis-server --requirepass ${REDIS_PASS}
ports:
- "6379:6379"
app:
build: .
depends_on:
- mysql
- redis
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
关键配置项:
使用Prometheus + Grafana方案:
关键监控指标:
当前系统已经实现了基础功能,但还有多个可优化方向:
智能推荐系统:基于用户舞蹈等级、浏览历史推荐课程
动作识别辅助:用户上传练习视频自动分析动作标准度
在线直播教学:集成WebRTC实现实时互动
这个项目让我深刻体会到:技术永远是为业务场景服务的。在开发过程中,我们多次调整技术方案,只为更好地呈现古典舞的艺术魅力。比如最初设计的炫酷3D展示界面,在实际用户测试中发现反而分散了对舞蹈本体的注意力,最终回归到简洁的视频列表布局。