校园周边美食一直是学生群体日常生活中的重要组成部分。传统的寻找美食方式主要依靠口口相传或随机探索,这种方式存在信息滞后、覆盖面有限等问题。我们开发的校园周边美食探索及分享平台正是为了解决这些痛点。
这个平台的核心价值在于:
从技术角度看,我们选择了SpringBoot+Vue+MyBatis的现代化技术栈,这种前后端分离的架构能够很好地支撑平台的快速迭代和功能扩展。MySQL作为关系型数据库,为平台提供了稳定可靠的数据存储方案。
后端选择SpringBoot框架主要基于以下考虑:
前端采用Vue.js框架的优势:
整个系统采用典型的三层架构:
这种分层架构使得系统各模块职责明确,耦合度低,便于团队协作开发和后期维护。
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(100) NOT NULL,
`phone` varchar(20) DEFAULT NULL,
`avatar_url` varchar(255) DEFAULT NULL,
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_login_time` datetime DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT '1',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `shop` (
`shop_id` bigint NOT NULL AUTO_INCREMENT,
`shop_name` varchar(100) NOT NULL,
`address` varchar(255) NOT NULL,
`latitude` decimal(10,6) DEFAULT NULL,
`longitude` decimal(10,6) DEFAULT NULL,
`description` text,
`average_rating` float DEFAULT '0',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`shop_id`),
KEY `idx_location` (`latitude`,`longitude`),
FULLTEXT KEY `idx_search` (`shop_name`,`address`,`description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `review` (
`review_id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`shop_id` bigint NOT NULL,
`rating` tinyint NOT NULL,
`content` text,
`review_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`is_anonymous` tinyint NOT NULL DEFAULT '0',
PRIMARY KEY (`review_id`),
KEY `idx_user` (`user_id`),
KEY `idx_shop` (`shop_id`),
CONSTRAINT `fk_review_shop` FOREIGN KEY (`shop_id`) REFERENCES `shop` (`shop_id`),
CONSTRAINT `fk_review_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
索引设计:
性能优化:
扩展性考虑:
采用JWT(JSON Web Token)实现无状态认证,核心代码如下:
java复制@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {
User user = userService.findByUsername(loginDTO.getUsername());
if(user == null || !passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
return Result.error("用户名或密码错误");
}
String token = jwtTokenUtil.generateToken(user);
return Result.ok().put("token", token);
}
@PostMapping("/register")
public Result register(@RequestBody RegisterDTO registerDTO) {
if(userService.existsByUsername(registerDTO.getUsername())) {
return Result.error("用户名已存在");
}
User user = new User();
user.setUsername(registerDTO.getUsername());
user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
user.setEmail(registerDTO.getEmail());
userService.save(user);
return Result.ok("注册成功");
}
}
实现基于地理位置和关键词的复合搜索:
java复制@Service
public class ShopServiceImpl implements ShopService {
@Autowired
private ShopMapper shopMapper;
@Override
public PageInfo<ShopVO> searchShops(ShopSearchDTO searchDTO) {
PageHelper.startPage(searchDTO.getPageNum(), searchDTO.getPageSize());
// 构建查询条件
Example example = new Example(Shop.class);
Example.Criteria criteria = example.createCriteria();
if(StringUtils.isNotBlank(searchDTO.getKeyword())) {
criteria.andLike("shopName", "%" + searchDTO.getKeyword() + "%")
.orLike("address", "%" + searchDTO.getKeyword() + "%")
.orLike("description", "%" + searchDTO.getKeyword() + "%");
}
if(searchDTO.getLatitude() != null && searchDTO.getLongitude() != null) {
// 计算距离并排序
String distanceSql = "ROUND(6378.138*2*ASIN(SQRT(POW(SIN((?*PI()/180-latitude*PI()/180)/2),2)+COS(?*PI()/180)*COS(latitude*PI()/180)*POW(SIN((?*PI()/180-longitude*PI()/180)/2),2)))*1000)";
example.setOrderByClause(distanceSql + " ASC");
example.getOredCriteria().get(0).andCondition(distanceSql + " < ?",
searchDTO.getLatitude(), searchDTO.getLatitude(), searchDTO.getLongitude(),
searchDTO.getMaxDistance());
}
List<Shop> shops = shopMapper.selectByExample(example);
return new PageInfo<>(convertToVOList(shops));
}
}
评价系统需要考虑防刷评和敏感词过滤:
java复制@Service
public class ReviewServiceImpl implements ReviewService {
@Autowired
private ReviewMapper reviewMapper;
@Autowired
private SensitiveWordFilter sensitiveWordFilter;
@Transactional
@Override
public Result addReview(ReviewDTO reviewDTO, Long userId) {
// 检查用户是否已经评价过该店铺
if(reviewMapper.existsByUserIdAndShopId(userId, reviewDTO.getShopId())) {
return Result.error("您已经评价过该店铺");
}
// 敏感词过滤
String filteredContent = sensitiveWordFilter.filter(reviewDTO.getContent());
Review review = new Review();
review.setUserId(userId);
review.setShopId(reviewDTO.getShopId());
review.setRating(reviewDTO.getRating());
review.setContent(filteredContent);
review.setIsAnonymous(reviewDTO.getIsAnonymous());
reviewMapper.insert(review);
// 更新店铺平均评分
updateShopRating(reviewDTO.getShopId());
return Result.ok("评价成功");
}
private void updateShopRating(Long shopId) {
Double averageRating = reviewMapper.getAverageRatingByShopId(shopId);
Shop shop = new Shop();
shop.setShopId(shopId);
shop.setAverageRating(averageRating.floatValue());
shopMapper.updateByPrimaryKeySelective(shop);
}
}
推荐使用Docker容器化部署SpringBoot应用:
dockerfile复制# Dockerfile 示例
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
部署步骤:
mvn clean packagedocker build -t food-platform .docker run -d -p 8080:8080 --name food-platform food-platformVue项目部署建议:
bash复制npm run build
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /path/to/dist;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
bash复制# MySQL备份脚本示例
mysqldump -u root -p food_platform > /backup/food_platform_$(date +%Y%m%d).sql
社交功能:
商家端功能:
个性化推荐:
缓存策略:
搜索优化:
前端性能:
随着业务增长,可以考虑将单体应用拆分为微服务:
使用Spring Cloud Alibaba实现服务治理:
依赖冲突解决:
mvn dependency:tree分析依赖树跨域问题处理:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
性能瓶颈排查:
数据库连接池优化:
properties复制# application.properties配置示例
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
XSS防护:
SQL注入防护:
CSRF防护:
在开发这个校园美食平台的过程中,我们积累了一些宝贵的经验:
技术选型要权衡当下需求和未来发展,不要过度设计但也要预留扩展空间。我们选择的SpringBoot+Vue组合既满足了当前需求,又为后续扩展打下了良好基础。
数据库设计是系统的基石。良好的索引设计和适当的反范式化可以显著提升查询性能。我们在项目中期对数据库进行了重构,增加了复合索引和全文索引,查询性能提升了3倍以上。
用户体验至关重要。我们通过多次用户调研和A/B测试,不断优化界面设计和交互流程。例如,将搜索框放在更显眼的位置后,搜索使用率提升了40%。
性能优化是一个持续的过程。我们通过引入缓存、优化SQL语句、前端懒加载等手段,将页面加载时间从最初的2秒多降低到了800毫秒左右。
安全防护不容忽视。在项目上线后不久,我们就遭遇了简单的SQL注入尝试,幸好提前做了防护。这提醒我们在开发过程中就要有安全意识,而不是事后补救。
这个项目从技术角度来说不算复杂,但完整地走完需求分析、设计、开发、测试、上线的全流程,对团队来说是非常宝贵的经验。特别是在处理真实用户反馈和实际性能问题时,学到了很多在教科书和教程中不会提及的实战经验。