作为一名长期从事Java全栈开发的工程师,最近刚完成了一个摄影师分享交流社区的毕业设计项目。这个平台采用Spring Boot+Vue.js技术栈,实现了摄影作品展示、社区互动、用户管理等核心功能。在开发过程中,我踩过不少坑,也积累了一些值得分享的经验,特别是如何将前后端技术无缝衔接,以及如何处理图片这类特殊资源。
这个项目特别适合计算机相关专业的同学作为毕业设计参考,也适合想学习Spring Boot全栈开发的新手。我会从环境搭建到功能实现的完整流程,详细讲解每个关键环节的注意事项和优化技巧。
开发这个项目需要准备以下环境:
JDK 1.8:建议使用Oracle JDK而非OpenJDK,因为某些IDE对Oracle JDK的兼容性更好。安装后需要配置JAVA_HOME环境变量,这个步骤很多新手容易出错。我通常会在系统环境变量中添加:
code复制JAVA_HOME = C:\Program Files\Java\jdk1.8.0_281
Path = %JAVA_HOME%\bin
配置完成后,在命令行执行java -version验证是否成功。
Maven 3.6.3:下载二进制包解压后,同样需要配置MAVEN_HOME和Path。建议在settings.xml中配置阿里云镜像,可以大幅提升依赖下载速度:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
MySQL 5.7:这个版本在稳定性和功能上都很平衡。安装时建议选择自定义安装,勾选MySQL Server和MySQL Workbench。初始化时设置root密码要牢记,开发阶段可以简单些如"123456",但生产环境必须复杂。
IntelliJ IDEA是我的首选,相比Eclipse有更好的代码提示和Spring Boot支持。几个关键配置:
注意:社区版IDEA对Spring Boot支持有限,建议使用Ultimate版。学生可以申请免费授权。
摄影师社区的核心数据模型包括用户、作品、评论等实体。我设计了以下主要表结构:
users表:
sql复制CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`email` varchar(100) NOT NULL COMMENT '邮箱',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`bio` text COMMENT '个人简介',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
photos表:
sql复制CREATE TABLE `photos` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`title` varchar(100) NOT NULL,
`description` text,
`image_url` varchar(255) NOT NULL,
`location` varchar(100) DEFAULT NULL,
`camera_model` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
comments表:
sql复制CREATE TABLE `comments` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`photo_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
`content` text NOT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_photo_id` (`photo_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
索引设计:除了主键,我为常用查询字段添加了索引,如users表的username和email,photos表的user_id等。但要注意索引不是越多越好,会影响写入性能。
字段类型选择:
varchar代替char节省空间text类型datetime而非timestamp,避免2038年问题SQL脚本管理:我使用Flyway进行数据库版本控制,每次变更都创建一个Vxx__Description.sql文件,方便团队协作和部署。
使用Spring Initializr创建项目骨架,选择以下依赖:
关键pom.xml配置:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!-- 其他依赖... -->
</dependencies>
用户认证模块:
java复制@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@Valid @RequestBody RegisterDTO dto) {
if(userService.existsUsername(dto.getUsername())) {
return Result.error("用户名已存在");
}
userService.register(dto);
return Result.success();
}
@PostMapping("/login")
public Result login(@Valid @RequestBody LoginDTO dto) {
String token = userService.login(dto);
return Result.success(token);
}
}
图片上传服务:
java复制@Service
public class FileStorageService {
@Value("${file.upload-dir}")
private String uploadDir;
public String storeFile(MultipartFile file) {
String filename = StringUtils.cleanPath(file.getOriginalFilename());
String newFilename = UUID.randomUUID() + "." + FilenameUtils.getExtension(filename);
try {
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
Path filePath = uploadPath.resolve(newFilename);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return newFilename;
} catch (IOException ex) {
throw new RuntimeException("文件存储失败: " + filename, ex);
}
}
}
提示:生产环境应该使用云存储服务如阿里云OSS,而不是本地存储,这里为了简化演示使用本地存储。
自定义SQL与分页查询:
java复制public interface PhotoMapper extends BaseMapper<Photo> {
@Select("SELECT p.*, u.username, u.avatar FROM photos p " +
"LEFT JOIN users u ON p.user_id = u.id " +
"WHERE p.title LIKE CONCAT('%', #{keyword}, '%') " +
"ORDER BY p.create_time DESC")
List<PhotoVO> searchPhotos(@Param("keyword") String keyword, Page<PhotoVO> page);
}
// 服务层调用
public PageResult<PhotoVO> searchPhotos(String keyword, int pageNum, int pageSize) {
Page<PhotoVO> page = new Page<>(pageNum, pageSize);
List<PhotoVO> list = photoMapper.searchPhotos(keyword, page);
return new PageResult<>(list, page.getTotal());
}
逻辑删除配置:
yaml复制mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段名
logic-not-delete-value: 0 # 未删除值
logic-delete-value: 1 # 删除值
使用Vue CLI创建项目:
bash复制vue create photographer-frontend
选择配置:
关键依赖:
json复制"dependencies": {
"axios": "^0.21.1",
"element-ui": "^2.15.6",
"vue": "^2.6.14",
"vue-infinite-loading": "^2.4.5"
}
图片上传组件:
vue复制<template>
<div class="uploader">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleChange"
:show-file-list="false"
>
<el-button size="small" type="primary">选择图片</el-button>
</el-upload>
<div v-if="file" class="preview">
<img :src="previewUrl" alt="预览">
<el-input v-model="title" placeholder="图片标题"></el-input>
<el-button @click="upload" type="success">上传</el-button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
file: null,
previewUrl: '',
title: ''
}
},
methods: {
handleChange(file) {
this.file = file
this.previewUrl = URL.createObjectURL(file.raw)
},
async upload() {
const formData = new FormData()
formData.append('file', this.file.raw)
formData.append('title', this.title)
try {
await this.$axios.post('/api/photos', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
this.$message.success('上传成功')
this.$emit('uploaded')
} catch (err) {
this.$message.error('上传失败')
}
}
}
}
</script>
无限滚动加载:
vue复制<template>
<div class="photo-list">
<div v-for="photo in photos" :key="photo.id" class="photo-item">
<img :src="photo.imageUrl" :alt="photo.title">
<h3>{{ photo.title }}</h3>
<p>{{ photo.description }}</p>
</div>
<infinite-loading @infinite="loadMore"></infinite-loading>
</div>
</template>
<script>
import InfiniteLoading from 'vue-infinite-loading'
export default {
components: { InfiniteLoading },
data() {
return {
photos: [],
page: 1,
loading: false
}
},
methods: {
async loadMore($state) {
if (this.loading) return
this.loading = true
try {
const res = await this.$axios.get('/api/photos', {
params: { page: this.page }
})
if (res.data.length) {
this.photos.push(...res.data)
this.page++
$state.loaded()
} else {
$state.complete()
}
} catch (err) {
$state.error()
} finally {
this.loading = false
}
}
}
}
</script>
开发阶段前端运行在8080端口,后端在8081端口,需要解决跨域问题。
后端配置CORS:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
前端axios配置:
javascript复制import axios from 'axios'
const instance = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8081',
timeout: 10000
})
// 请求拦截器
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器
instance.interceptors.response.use(response => {
return response.data
}, error => {
if (error.response.status === 401) {
// 跳转到登录页
}
return Promise.reject(error)
})
export default instance
后端打包:
bash复制mvn clean package -DskipTests
生成的target/photographer-0.0.1-SNAPSHOT.jar可以直接运行:
bash复制java -jar photographer-0.0.1-SNAPSHOT.jar
前端打包:
bash复制npm run build
生成的dist目录可以部署到Nginx:
nginx复制server {
listen 80;
server_name localhost;
location / {
root /path/to/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://localhost:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
图片处理优化:
数据库查询优化:
缓存策略:
MyBatis Plus主键ID生成问题:
@TableId注解java复制@TableId(type = IdType.AUTO)
private Long id;
Vue.js跨域问题:
文件上传大小限制:
Spring Boot默认限制1MB,需要配置:
yaml复制spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
社交功能增强:
图片处理能力:
移动端适配:
这个项目从零开始搭建,让我对全栈开发有了更深入的理解。最大的收获是学会了如何平衡前后端开发进度,以及如何处理图片这类特殊资源。对于想学习Spring Boot+Vue.js全栈开发的同学,建议先从简单的CRUD功能开始,逐步添加复杂功能,不要一开始就追求完美架构。