1. 项目背景与核心需求
最近在给餐饮行业客户做数字化改造时,经常需要实现菜品管理的基础功能。这个看似简单的"菜品上架下架"需求,实际上涉及完整的商品生命周期管理逻辑。今天就用一个实战案例,带大家从零实现这套基础但至关重要的功能。
在餐饮系统中,菜品管理模块需要满足三个核心场景:
- 后厨研发新菜品后,需要快速上架到点餐系统
- 季节性食材断供时,需要及时下架相关菜品
- 顾客在点餐时需要准确看到当前可售的菜品信息
2. 技术方案设计
2.1 数据库设计
采用MySQL作为数据存储,主要包含两个核心表:
sql复制CREATE TABLE `dishes` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '菜品名称',
`price` decimal(10,2) NOT NULL COMMENT '售价',
`description` text COMMENT '菜品描述',
`image_url` varchar(255) DEFAULT NULL COMMENT '菜品图片',
`category_id` int DEFAULT NULL COMMENT '分类ID',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '0-下架 1-上架',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `dish_category` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`sort` int DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键字段说明:
- status字段使用0/1表示上下架状态,便于后续扩展其他状态
- 使用update_time自动记录最后操作时间
- 通过category_id实现菜品分类管理
2.2 接口设计
采用RESTful风格API设计:
| 接口 | 方法 | 路径 | 参数 | 说明 |
|---|---|---|---|---|
| 菜品列表 | GET | /api/dishes | category_id(可选) | 获取所有上架菜品 |
| 菜品详情 | GET | /api/dishes/ | - | 获取单个菜品详情 |
| 上架菜品 | PUT | /api/dishes/{id}/online | - | 修改状态为上架 |
| 下架菜品 | PUT | /api/dishes/{id}/offline | - | 修改状态为下架 |
| 新增菜品 | POST | /api/dishes | JSON数据 | 创建新菜品 |
| 修改菜品 | PUT | /api/dishes/ | JSON数据 | 更新菜品信息 |
3. 核心功能实现
3.1 上架下架功能
java复制@RestController
@RequestMapping("/api/dishes")
public class DishController {
@Autowired
private DishService dishService;
@PutMapping("/{id}/online")
public Result online(@PathVariable Integer id) {
Dish dish = dishService.getById(id);
if (dish == null) {
return Result.error("菜品不存在");
}
dish.setStatus(1); // 1表示上架
dishService.updateById(dish);
return Result.success();
}
@PutMapping("/{id}/offline")
public Result offline(@PathVariable Integer id) {
Dish dish = dishService.getById(id);
if (dish == null) {
return Result.error("菜品不存在");
}
dish.setStatus(0); // 0表示下架
dishService.updateById(dish);
return Result.success();
}
}
注意:实际业务中需要考虑并发修改问题,建议使用乐观锁机制
3.2 菜品展示逻辑
前端展示时需要特别注意状态过滤:
javascript复制// 获取上架菜品列表
async getOnlineDishes() {
const res = await axios.get('/api/dishes')
this.dishList = res.data.filter(item => item.status === 1)
}
对于管理后台,可以展示全部菜品并用标签区分状态:
html复制<el-table :data="dishList">
<el-table-column prop="name" label="菜品名称"/>
<el-table-column label="状态">
<template #default="{row}">
<el-tag :type="row.status ? 'success' : 'danger'">
{{ row.status ? '上架' : '下架' }}
</el-tag>
</template>
</el-table-column>
</el-table>
4. 业务扩展与优化
4.1 定时上下架功能
对于季节性菜品,可以扩展定时任务:
java复制@Scheduled(cron = "0 0 0 * * ?")
public void autoUpdateDishStatus() {
// 查询需要自动上架的菜品
List<Dish> toOnline = dishMapper.selectList(
new LambdaQueryWrapper<Dish>()
.eq(Dish::getAutoOnline, 1)
.le(Dish::getOnlineTime, LocalDateTime.now())
.eq(Dish::getStatus, 0)
);
// 批量更新状态
toOnline.forEach(dish -> {
dish.setStatus(1);
dishService.updateById(dish);
});
}
4.2 库存联动控制
当菜品关联库存时,下架逻辑需要扩展:
java复制public Result offlineWithInventoryCheck(Integer id) {
Dish dish = dishService.getById(id);
if (dish == null) {
return Result.error("菜品不存在");
}
// 检查库存
Integer inventory = inventoryService.getByDishId(id);
if (inventory != null && inventory > 0) {
return Result.error("仍有库存未消耗,请先处理库存");
}
dish.setStatus(0);
dishService.updateById(dish);
return Result.success();
}
5. 常见问题与解决方案
5.1 菜品状态不同步问题
现象:后台已下架,但客户端仍能看见
解决方案:
- 前端增加缓存清除机制
- 后端接口添加缓存注解
java复制@CacheEvict(value = "dish", key = "#id")
@PutMapping("/{id}/offline")
public Result offline(@PathVariable Integer id) {
//...原有逻辑
}
5.2 批量操作性能优化
当需要批量上下架时:
java复制@PostMapping("/batch-online")
public Result batchOnline(@RequestBody List<Integer> ids) {
// 使用批量更新语句
dishMapper.updateBatchStatus(ids, 1);
return Result.success();
}
// XML映射文件
<update id="updateBatchStatus">
UPDATE dishes SET status = #{status}
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
6. 前端实现技巧
6.1 状态切换组件
封装可复用的状态切换组件:
vue复制<template>
<el-switch
v-model="status"
active-text="上架"
inactive-text="下架"
@change="handleStatusChange"
/>
</template>
<script>
export default {
props: {
value: Boolean,
dishId: Number
},
methods: {
async handleStatusChange(val) {
const api = val ? '/online' : '/offline'
await axios.put(`/api/dishes/${this.dishId}${api}`)
this.$message.success(val ? '已上架' : '已下架')
}
}
}
</script>
6.2 图片上传处理
使用ElementUI上传组件:
html复制<el-upload
action="/api/upload"
:show-file-list="false"
:on-success="handleImageSuccess"
>
<img v-if="imageUrl" :src="imageUrl" class="dish-image">
<i v-else class="el-icon-plus"></i>
</el-upload>
对应的后端处理:
java复制@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
String fileName = UUID.randomUUID() +
file.getOriginalFilename().substring(
file.getOriginalFilename().lastIndexOf(".")
);
File dest = new File(uploadPath + fileName);
file.transferTo(dest);
return Result.success("/uploads/" + fileName);
}
7. 安全与权限控制
7.1 操作权限校验
使用Spring Security进行权限控制:
java复制@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
@PutMapping("/{id}/online")
public Result online(@PathVariable Integer id) {
//...原有逻辑
}
7.2 操作日志记录
记录关键操作:
java复制@PutMapping("/{id}/online")
public Result online(@PathVariable Integer id) {
//...原有逻辑
// 记录操作日志
operationLogService.saveLog(
"菜品上架",
"将菜品ID=" + id + "的状态修改为上架",
UserUtils.getCurrentUserId()
);
return Result.success();
}
8. 测试要点
8.1 单元测试用例
java复制@Test
public void testOnlineDish() {
// 准备测试数据
Dish dish = new Dish();
dish.setStatus(0);
dishMapper.insert(dish);
// 执行上架操作
Result result = dishController.online(dish.getId());
// 验证结果
assertEquals(200, result.getCode());
Dish updated = dishMapper.selectById(dish.getId());
assertEquals(1, updated.getStatus());
}
8.2 接口测试脚本
使用Postman进行接口测试:
javascript复制// 上架测试
pm.test("上架成功", function() {
pm.response.to.have.status(200);
});
// 下架测试
pm.test("下架成功", function() {
pm.response.to.have.status(200);
});
// 状态验证测试
pm.test("状态已更新", function() {
var jsonData = pm.response.json();
pm.expect(jsonData.data.status).to.eql(0);
});
9. 性能优化建议
9.1 缓存策略
使用Redis缓存热门菜品:
java复制@Cacheable(value = "dish", key = "#id")
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id) {
Dish dish = dishService.getById(id);
return Result.success(dish);
}
9.2 数据库索引优化
为常用查询字段添加索引:
sql复制ALTER TABLE `dishes` ADD INDEX `idx_status` (`status`);
ALTER TABLE `dishes` ADD INDEX `idx_category` (`category_id`);
10. 项目部署方案
10.1 Docker部署配置
dockerfile复制FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/dish-service.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
10.2 Nginx配置示例
nginx复制server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://dish-service:8080;
proxy_set_header Host $host;
}
location /uploads/ {
alias /data/uploads/;
}
}
在实际项目中,菜品上下架功能虽然基础,但需要考虑的细节非常多。我在多个餐饮系统项目中总结出几个关键点:1) 状态变更要有完整日志记录 2) 前端展示要及时同步最新状态 3) 批量操作要考虑性能影响。建议在初期就设计好扩展字段,比如添加offline_reason字段记录下架原因,这对后续运营分析很有帮助。