作为一名有十年开发经验的Java全栈工程师,我经常被问到如何选择合适的毕业设计项目。今天要分享的是一个基于SpringBoot+Vue的摄影交流平台,这个选题特别适合计算机相关专业的同学作为毕业设计。它不仅涵盖了主流技术栈的应用,还能展示你对完整项目开发流程的理解。
这个摄影交流平台本质上是一个垂直领域的社交系统,主要功能包括用户注册登录、作品发布、点赞评论、关注互动等。技术上采用前后端分离架构,后端使用SpringBoot+MyBatisPlus,前端使用Vue.js,数据库选用MySQL。这种技术组合既符合企业主流开发模式,又不会过于复杂导致难以完成。
为什么我推荐这个选题?
- 技术栈主流且完整:涵盖了Java后端、前端、数据库等核心技能
- 业务场景明确:摄影社区的需求清晰,功能模块划分合理
- 难度适中:既有挑战性,又不会过于复杂导致无法完成
- 可扩展性强:基础功能完成后,可以继续添加推荐算法等高级功能
在项目启动阶段,技术选型是首要考虑的问题。经过综合评估,我选择了以下技术栈:
后端技术栈:
前端技术栈:
开发工具:
系统采用经典的三层架构,分为表现层、业务逻辑层和数据访问层:
code复制表现层(Web)
├── 前端:Vue.js + Element Plus
└── 后端:Spring MVC
业务逻辑层(Service)
├── 核心业务逻辑
└── 业务规则验证
数据访问层(DAO)
├── MyBatis-Plus
└── MySQL
这种分层架构的优势在于:
数据库设计遵循三范式原则,主要表结构如下:
用户表(user)
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`bio` varchar(255) DEFAULT NULL COMMENT '个人简介',
`status` tinyint DEFAULT '1' COMMENT '状态(0:禁用,1:正常)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
摄影作品表(photo)
sql复制CREATE TABLE `photo` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '作品ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`title` varchar(100) NOT NULL COMMENT '作品标题',
`description` text COMMENT '作品描述',
`image_url` varchar(255) NOT NULL COMMENT '图片URL',
`like_count` int DEFAULT '0' COMMENT '点赞数',
`view_count` int DEFAULT '0' COMMENT '浏览数',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='摄影作品表';
评论表(comment)
sql复制CREATE TABLE `comment` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '评论ID',
`photo_id` bigint NOT NULL COMMENT '作品ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`content` text NOT NULL COMMENT '评论内容',
`parent_id` bigint DEFAULT NULL COMMENT '父评论ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_photo_id` (`photo_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';
数据库设计时特别注意了以下几点:
用户认证是系统的基石,我采用JWT(JSON Web Token)实现无状态认证,结合Shiro进行权限控制。
JWT工具类实现:
java复制public class JwtUtils {
private static final String SECRET = "your-secret-key"; // 实际项目中应从配置读取
private static final long EXPIRE = 7 * 24 * 60 * 60 * 1000L; // 7天
public static String generateToken(Long userId) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId.toString())
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public static Long getUserId(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public static boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
Shiro配置类:
java复制@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/api/auth/**", "anon"); // 认证相关接口放行
filterMap.put("/api/**", "authc"); // 其他API需要认证
factoryBean.setFilterChainDefinitionMap(filterMap);
return factoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
@Bean
public Realm realm() {
return new MyRealm();
}
}
作品发布是核心功能,需要考虑图片上传、缩略图生成、EXIF信息提取等细节。
图片上传服务实现:
java复制@Service
public class PhotoService {
@Value("${file.upload-dir}")
private String uploadDir;
@Value("${file.access-path}")
private String accessPath;
public Photo uploadPhoto(MultipartFile file, Long userId, String title, String description) {
// 验证文件类型
String contentType = file.getContentType();
if (!contentType.startsWith("image/")) {
throw new IllegalArgumentException("只支持图片文件上传");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String fileExt = originalFilename.substring(originalFilename.lastIndexOf("."));
String filename = UUID.randomUUID().toString() + fileExt;
// 保存文件
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
try {
Files.createDirectories(uploadPath);
} catch (IOException e) {
throw new RuntimeException("无法创建上传目录", e);
}
}
try {
Files.copy(file.getInputStream(), uploadPath.resolve(filename),
StandardCopyOption.REPLACE_EXISTING);
// 生成缩略图
generateThumbnail(uploadPath.resolve(filename).toString(),
uploadPath.resolve("thumb_" + filename).toString());
// 保存到数据库
Photo photo = new Photo();
photo.setUserId(userId);
photo.setTitle(title);
photo.setDescription(description);
photo.setImageUrl(accessPath + filename);
photo.setThumbUrl(accessPath + "thumb_" + filename);
// 提取EXIF信息
extractExifInfo(file, photo);
return photoMapper.insert(photo) > 0 ? photo : null;
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
private void generateThumbnail(String sourcePath, String targetPath) throws IOException {
// 使用Thumbnailator库生成缩略图
Thumbnails.of(sourcePath)
.size(300, 300)
.keepAspectRatio(true)
.toFile(targetPath);
}
private void extractExifInfo(MultipartFile file, Photo photo) throws IOException {
// 使用metadata-extractor库提取EXIF信息
Metadata metadata = ImageMetadataReader.readMetadata(file.getInputStream());
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (directory != null) {
photo.setCameraModel(directory.getString(ExifSubIFDDirectory.TAG_MODEL));
photo.setLensModel(directory.getString(ExifSubIFDDirectory.TAG_LENS_MODEL));
photo.setFNumber(directory.getString(ExifSubIFDDirectory.TAG_FNUMBER));
photo.setExposureTime(directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME));
photo.setIso(directory.getString(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT));
}
}
}
互动功能包括点赞、评论和关注,需要考虑并发控制和实时通知。
点赞功能实现:
java复制@Service
public class LikeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private PhotoMapper photoMapper;
private static final String LIKE_KEY_PREFIX = "like:photo:";
private static final String USER_LIKE_KEY_PREFIX = "user:like:";
@Transactional
public boolean likePhoto(Long photoId, Long userId) {
String likeKey = LIKE_KEY_PREFIX + photoId;
String userLikeKey = USER_LIKE_KEY_PREFIX + userId;
// 检查是否已经点赞
if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(userLikeKey, photoId.toString()))) {
return false;
}
// 使用Redis事务保证原子性
redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForSet().add(likeKey, userId.toString());
operations.opsForSet().add(userLikeKey, photoId.toString());
operations.opsForValue().increment("photo:like_count:" + photoId);
return operations.exec();
}
});
// 异步更新数据库
asyncUpdateLikeCount(photoId);
return true;
}
@Async
public void asyncUpdateLikeCount(Long photoId) {
Long count = getLikeCountFromRedis(photoId);
photoMapper.updateLikeCount(photoId, count);
}
public Long getLikeCount(Long photoId) {
Long count = getLikeCountFromRedis(photoId);
if (count == null) {
count = photoMapper.selectLikeCount(photoId);
setLikeCountToRedis(photoId, count);
}
return count;
}
private Long getLikeCountFromRedis(Long photoId) {
String value = redisTemplate.opsForValue().get("photo:like_count:" + photoId);
return value != null ? Long.parseLong(value) : null;
}
private void setLikeCountToRedis(Long photoId, Long count) {
redisTemplate.opsForValue().set("photo:like_count:" + photoId, count.toString());
}
}
项目采用前后端分离部署方式,后端打包为JAR文件,前端打包为静态资源。
后端打包配置(pom.xml):
xml复制<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
打包命令:
bash复制mvn clean package -DskipTests
前端打包配置(vue.config.js):
javascript复制module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '/photo-community/' : '/',
outputDir: 'dist',
assetsDir: 'static',
productionSourceMap: false,
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
打包命令:
bash复制npm run build
数据库优化:
缓存策略:
前端优化:
JVM调优:
输入验证:
认证安全:
API安全:
这个摄影交流平台项目涵盖了从需求分析到系统实现的完整开发流程,技术栈选择合理,既体现了主流技术的应用,又保持了适中的复杂度。通过这个项目,你可以掌握:
项目扩展建议:
在实际开发过程中,我遇到并解决了一些典型问题,这里分享几个关键经验:
图片处理优化:最初直接处理大文件导致内存溢出,后来改用流式处理和缩略图生成解决了这个问题。
并发控制:点赞功能初期存在并发问题,通过Redis事务和异步更新策略实现了高性能的计数功能。
前端性能:首页加载大量图片时性能较差,通过懒加载和分页查询显著提升了用户体验。
这个项目作为毕业设计既有足够的深度展示你的技术能力,又不会过于复杂难以完成。如果你在实现过程中遇到任何问题,欢迎交流讨论。