1. 校园商铺管理系统架构设计
校园商铺管理系统采用前后端分离架构,前端基于Vue.js框架实现用户交互界面,后端采用Spring Boot框架提供RESTful API服务。这种架构设计能够充分发挥前后端各自的优势,实现高效开发和灵活部署。
1.1 技术选型解析
后端技术栈:
- Spring Boot 2.7.x:简化配置,快速构建微服务
- Spring Security:提供完善的身份认证和授权机制
- MyBatis-Plus:增强型ORM框架,简化数据库操作
- MySQL 8.0:关系型数据库,存储业务数据
- Redis 6.x:缓存热点数据,提升系统响应速度
前端技术栈:
- Vue 3.x:渐进式JavaScript框架
- Element Plus:基于Vue 3的UI组件库
- Axios:处理HTTP请求
- ECharts 5.0:数据可视化展示
技术选型考虑因素:社区活跃度、学习曲线、团队熟悉程度、项目规模等。这套技术组合在中小型项目中已经过充分验证,能够满足校园商铺管理系统的需求。
1.2 系统分层架构
系统采用经典的三层架构设计:
- 表现层:Vue前端负责用户交互和界面展示
- 业务逻辑层:Spring Boot处理核心业务逻辑
- 数据访问层:MyBatis实现数据持久化
这种分层设计使得各层职责明确,便于维护和扩展。前后端通过RESTful API进行通信,接口设计遵循以下原则:
- 使用HTTP动词表达操作意图(GET/POST/PUT/DELETE)
- 返回标准JSON格式数据
- 状态码准确反映操作结果
2. 数据库设计与实现
2.1 核心数据表结构
2.1.1 商铺信息表(shop_info)
sql复制CREATE TABLE `shop_info` (
`shop_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商铺ID',
`shop_name` varchar(50) NOT NULL COMMENT '商铺名称',
`shop_category` varchar(20) NOT NULL COMMENT '商铺类别',
`contact_phone` varchar(15) NOT NULL COMMENT '联系电话',
`shop_address` varchar(100) NOT NULL COMMENT '商铺地址',
`shop_status` tinyint NOT NULL DEFAULT '0' COMMENT '审核状态(0-未审核,1-已审核)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`shop_id`),
KEY `idx_category_status` (`shop_category`,`shop_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商铺信息表';
2.1.2 商品信息表(product_info)
sql复制CREATE TABLE `product_info` (
`product_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`shop_id` bigint NOT NULL COMMENT '所属商铺ID',
`product_name` varchar(50) NOT NULL COMMENT '商品名称',
`product_price` decimal(10,2) NOT NULL COMMENT '商品价格',
`product_stock` int NOT NULL DEFAULT '0' COMMENT '商品库存',
`product_desc` varchar(200) DEFAULT NULL COMMENT '商品描述',
`product_status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0-下架,1-上架)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`product_id`),
KEY `idx_shop_status` (`shop_id`,`product_status`),
KEY `idx_name` (`product_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';
2.1.3 订单信息表(order_info)
sql复制CREATE TABLE `order_info` (
`order_id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_list` text NOT NULL COMMENT '商品列表(JSON格式)',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`order_status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0-待支付,1-已完成)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`order_id`),
KEY `idx_user_status` (`user_id`,`order_status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单信息表';
2.2 数据库优化策略
-
索引设计:
- 为高频查询字段建立组合索引
- 避免过度索引,影响写入性能
- 使用EXPLAIN分析查询执行计划
-
分表分库:
- 订单表按时间范围水平分表
- 商品描述等大字段垂直拆分
-
缓存策略:
- 使用Redis缓存热点商铺和商品信息
- 实现多级缓存(本地缓存+分布式缓存)
3. 后端核心功能实现
3.1 Spring Boot应用配置
java复制@SpringBootApplication
@MapperScan("com.campus.shop.mapper")
@EnableCaching
@EnableTransactionManagement
public class CampusShopApplication {
public static void main(String[] args) {
SpringApplication.run(CampusShopApplication.class, args);
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
3.2 商铺管理模块实现
3.2.1 商铺服务层代码
java复制@Service
@RequiredArgsConstructor
public class ShopServiceImpl implements ShopService {
private final ShopMapper shopMapper;
private final RedisTemplate<String, Object> redisTemplate;
@Override
@Transactional
public boolean addShop(ShopDTO shopDTO) {
Shop shop = new Shop();
BeanUtils.copyProperties(shopDTO, shop);
shop.setShopStatus(0); // 初始状态为未审核
int result = shopMapper.insert(shop);
if(result > 0) {
// 清除商铺列表缓存
redisTemplate.delete("shop:list");
return true;
}
return false;
}
@Override
@Cacheable(value = "shop", key = "#shopId")
public ShopVO getShopById(Long shopId) {
Shop shop = shopMapper.selectById(shopId);
if(shop == null) {
throw new BusinessException("商铺不存在");
}
ShopVO shopVO = new ShopVO();
BeanUtils.copyProperties(shop, shopVO);
return shopVO;
}
@Override
@CacheEvict(value = "shop", key = "#shopId")
public boolean updateShopStatus(Long shopId, Integer status) {
Shop shop = new Shop();
shop.setShopId(shopId);
shop.setShopStatus(status);
return shopMapper.updateById(shop) > 0;
}
}
3.2.2 商铺控制器代码
java复制@RestController
@RequestMapping("/api/shop")
@RequiredArgsConstructor
public class ShopController {
private final ShopService shopService;
@PostMapping
public Result<Void> addShop(@RequestBody @Valid ShopDTO shopDTO) {
if(shopService.addShop(shopDTO)) {
return Result.success();
}
return Result.fail("添加商铺失败");
}
@GetMapping("/{shopId}")
public Result<ShopVO> getShop(@PathVariable Long shopId) {
return Result.success(shopService.getShopById(shopId));
}
@PutMapping("/{shopId}/status")
@PreAuthorize("hasRole('ADMIN')")
public Result<Void> updateShopStatus(
@PathVariable Long shopId,
@RequestParam Integer status) {
if(shopService.updateShopStatus(shopId, status)) {
return Result.success();
}
return Result.fail("更新商铺状态失败");
}
}
3.3 商品管理模块实现
3.3.1 商品服务层代码
java复制@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductMapper productMapper;
private final ShopMapper shopMapper;
private final RedisTemplate<String, Object> redisTemplate;
@Override
@Transactional
public boolean addProduct(ProductDTO productDTO) {
// 校验商铺是否存在且已审核
Shop shop = shopMapper.selectById(productDTO.getShopId());
if(shop == null || shop.getShopStatus() != 1) {
throw new BusinessException("商铺不存在或未通过审核");
}
Product product = new Product();
BeanUtils.copyProperties(productDTO, product);
product.setProductStatus(1); // 默认上架状态
int result = productMapper.insert(product);
if(result > 0) {
// 清除商铺商品列表缓存
redisTemplate.delete("product:shop:" + productDTO.getShopId());
return true;
}
return false;
}
@Override
@Cacheable(value = "product", key = "#productId")
public ProductVO getProductById(Long productId) {
Product product = productMapper.selectById(productId);
if(product == null) {
throw new BusinessException("商品不存在");
}
ProductVO productVO = new ProductVO();
BeanUtils.copyProperties(product, productVO);
return productVO;
}
}
3.3.2 商品控制器代码
java复制@RestController
@RequestMapping("/api/product")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@PostMapping
@PreAuthorize("hasRole('SHOP_OWNER')")
public Result<Void> addProduct(@RequestBody @Valid ProductDTO productDTO) {
if(productService.addProduct(productDTO)) {
return Result.success();
}
return Result.fail("添加商品失败");
}
@GetMapping("/{productId}")
public Result<ProductVO> getProduct(@PathVariable Long productId) {
return Result.success(productService.getProductById(productId));
}
@GetMapping("/shop/{shopId}")
public Result<List<ProductVO>> getProductsByShop(
@PathVariable Long shopId,
@RequestParam(required = false) Integer status) {
return Result.success(productService.getProductsByShop(shopId, status));
}
}
4. 前端Vue实现
4.1 前端项目结构
code复制src/
├── api/ # API请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── store/ # Vuex状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── shop/ # 商铺相关页面
│ ├── product/ # 商品相关页面
│ ├── order/ # 订单相关页面
│ └── user/ # 用户相关页面
├── App.vue # 根组件
└── main.js # 应用入口
4.2 商铺列表页面实现
vue复制<template>
<div class="shop-container">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>商铺列表</span>
<el-button
v-if="hasPermission('shop:add')"
type="primary"
icon="el-icon-plus"
@click="handleAdd"
style="float: right; padding: 3px 0">
新增商铺
</el-button>
</div>
<el-table
:data="shopList"
border
style="width: 100%"
v-loading="loading">
<el-table-column
prop="shopName"
label="商铺名称"
width="180">
</el-table-column>
<el-table-column
prop="shopCategory"
label="商铺类别"
width="120">
</el-table-column>
<el-table-column
prop="contactPhone"
label="联系电话"
width="150">
</el-table-column>
<el-table-column
prop="shopAddress"
label="商铺地址">
</el-table-column>
<el-table-column
prop="shopStatus"
label="状态"
width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.shopStatus === 1 ? 'success' : 'danger'">
{{ scope.row.shopStatus === 1 ? '已审核' : '未审核' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
width="180">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.row.shopId)">编辑</el-button>
<el-button
v-if="hasPermission('shop:audit')"
size="mini"
type="success"
@click="handleAudit(scope.row.shopId)"
:disabled="scope.row.shopStatus === 1">
审核
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-card>
<shop-dialog
ref="shopDialog"
@refresh="getShopList">
</shop-dialog>
</div>
</template>
<script>
import { getShopList, auditShop } from '@/api/shop'
import ShopDialog from './components/ShopDialog'
export default {
name: 'ShopList',
components: { ShopDialog },
data() {
return {
shopList: [],
total: 0,
loading: false,
queryParams: {
pageNum: 1,
pageSize: 10,
shopName: undefined,
shopCategory: undefined,
shopStatus: undefined
}
}
},
created() {
this.getShopList()
},
methods: {
getShopList() {
this.loading = true
getShopList(this.queryParams).then(response => {
this.shopList = response.data.list
this.total = response.data.total
}).finally(() => {
this.loading = false
})
},
handleAdd() {
this.$refs.shopDialog.handleAdd()
},
handleEdit(shopId) {
this.$refs.shopDialog.handleEdit(shopId)
},
handleAudit(shopId) {
this.$confirm('确定要审核通过该商铺吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
auditShop(shopId).then(() => {
this.$message.success('审核成功')
this.getShopList()
})
})
},
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getShopList()
},
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getShopList()
}
}
}
</script>
<style scoped>
.shop-container {
padding: 20px;
}
</style>
4.3 商品管理页面实现
vue复制<template>
<div class="product-container">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>商品管理</span>
<el-button
v-if="hasPermission('product:add')"
type="primary"
icon="el-icon-plus"
@click="handleAdd"
style="float: right; padding: 3px 0">
新增商品
</el-button>
</div>
<el-form :inline="true" :model="queryParams" class="search-form">
<el-form-item label="商品名称">
<el-input
v-model="queryParams.productName"
placeholder="请输入商品名称"
clearable>
</el-input>
</el-form-item>
<el-form-item label="商品状态">
<el-select
v-model="queryParams.productStatus"
placeholder="请选择状态"
clearable>
<el-option label="上架" :value="1"></el-option>
<el-option label="下架" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
:data="productList"
border
style="width: 100%"
v-loading="loading">
<el-table-column
prop="productName"
label="商品名称"
width="180">
</el-table-column>
<el-table-column
prop="productPrice"
label="价格"
width="120">
<template slot-scope="scope">
¥{{ scope.row.productPrice.toFixed(2) }}
</template>
</el-table-column>
<el-table-column
prop="productStock"
label="库存"
width="100">
</el-table-column>
<el-table-column
prop="productStatus"
label="状态"
width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.productStatus === 1 ? 'success' : 'danger'">
{{ scope.row.productStatus === 1 ? '上架' : '下架' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
width="180">
<template slot-scope="scope">
{{ formatTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column
label="操作"
width="180">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.row.productId)">编辑</el-button>
<el-button
size="mini"
:type="scope.row.productStatus === 1 ? 'danger' : 'success'"
@click="handleStatus(scope.row.productId, scope.row.productStatus)">
{{ scope.row.productStatus === 1 ? '下架' : '上架' }}
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-card>
<product-dialog
ref="productDialog"
:shop-id="shopId"
@refresh="getProductList">
</product-dialog>
</div>
</template>
<script>
import { getProductList, updateProductStatus } from '@/api/product'
import ProductDialog from './components/ProductDialog'
import { formatTime } from '@/utils'
export default {
name: 'ProductList',
components: { ProductDialog },
props: {
shopId: {
type: Number,
default: undefined
}
},
data() {
return {
productList: [],
total: 0,
loading: false,
queryParams: {
pageNum: 1,
pageSize: 10,
shopId: this.shopId,
productName: undefined,
productStatus: undefined
}
}
},
created() {
this.getProductList()
},
methods: {
getProductList() {
this.loading = true
getProductList(this.queryParams).then(response => {
this.productList = response.data.list
this.total = response.data.total
}).finally(() => {
this.loading = false
})
},
handleAdd() {
this.$refs.productDialog.handleAdd()
},
handleEdit(productId) {
this.$refs.productDialog.handleEdit(productId)
},
handleStatus(productId, status) {
const newStatus = status === 1 ? 0 : 1
const statusText = newStatus === 1 ? '上架' : '下架'
this.$confirm(`确定要${statusText}该商品吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
updateProductStatus(productId, newStatus).then(() => {
this.$message.success(`${statusText}成功`)
this.getProductList()
})
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getProductList()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 10,
shopId: this.shopId,
productName: undefined,
productStatus: undefined
}
this.getProductList()
},
handleSizeChange(val) {
this.queryParams.pageSize = val
this.getProductList()
},
handleCurrentChange(val) {
this.queryParams.pageNum = val
this.getProductList()
},
formatTime
}
}
</script>
<style scoped>
.product-container {
padding: 20px;
}
.search-form {
margin-bottom: 20px;
}
</style>
5. 系统部署与运维
5.1 后端部署方案
5.1.1 本地开发环境部署
- 安装JDK 1.8或以上版本
- 安装Maven 3.6或以上版本
- 安装MySQL 8.0并创建数据库
- 安装Redis 6.x
bash复制# 克隆项目
git clone https://github.com/example/campus-shop.git
# 进入项目目录
cd campus-shop/backend
# 修改application-dev.yml配置数据库和Redis连接信息
# 打包项目
mvn clean package -DskipTests
# 运行项目
java -jar target/campus-shop.jar
5.1.2 生产环境Docker部署
dockerfile复制# Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
bash复制# 构建镜像
docker build -t campus-shop .
# 运行容器
docker run -d -p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql-server:3306/campus_shop \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=yourpassword \
-e SPRING_REDIS_HOST=redis-server \
--name campus-shop \
campus-shop
5.2 前端部署方案
5.2.1 本地开发环境部署
- 安装Node.js 14.x或以上版本
- 安装Vue CLI
bash复制# 进入前端项目目录
cd campus-shop/frontend
# 安装依赖
npm install
# 修改.env.development配置后端API地址
# 启动开发服务器
npm run serve
5.2.2 生产环境Nginx部署
bash复制# 构建生产环境代码
npm run build
# 配置Nginx
server {
listen 80;
server_name yourdomain.com;
location / {
root /path/to/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend-server:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
5.3 系统监控与维护
-
日志管理:
- 使用Logback配置日志轮转
- 关键操作记录审计日志
- 异常日志发送告警通知
-
性能监控:
- Spring Boot Actuator暴露健康检查端点
- Prometheus + Grafana监控系统指标
- ELK收集和分析日志
-
备份策略:
- 每日数据库全量备份
- 关键数据实时备份到对象存储
- 定期测试备份恢复流程
6. 常见问题与解决方案
6.1 开发环境问题
问题1:启动后端项目时报数据库连接错误
解决方案:
- 检查application.yml中的数据库配置是否正确
- 确认MySQL服务已启动且网络可达
- 检查数据库用户是否有足够权限
- 确认数据库驱动版本与MySQL版本匹配
问题2:前端项目npm install时报错
解决方案:
- 清除npm缓存:
npm cache clean --force - 删除node_modules目录和package-lock.json
- 使用淘宝镜像源:
npm config set registry https://registry.npmmirror.com - 重新执行
npm install
6.2 生产环境问题
问题1:系统运行一段时间后响应变慢
解决方案:
- 检查数据库慢查询日志,优化SQL
- 增加Redis缓存命中率
- 调整JVM内存参数
- 考虑增加应用实例,使用负载均衡
问题2:订单支付状态不同步
解决方案:
- 实现支付状态轮询机制
- 引入消息队列确保状态更新可靠性
- 添加补偿任务处理异常状态
- 记录详细的操作日志便于排查
6.3 安全相关问题
问题1:如何防止SQL注入攻击
解决方案:
- 使用MyBatis预编译语句
- 对用户输入进行严格校验
- 避免直接拼接SQL语句
- 使用MyBatis-Plus提供的Wrapper构建查询条件
问题2:如何保护用户敏感数据
解决方案:
- 密码使用BCrypt加密存储
- 敏感字段在传输时加密
- 实现数据脱敏显示
- 严格控制数据访问权限
7. 系统扩展与优化方向
7.1 功能扩展
-
移动端适配:
- 开发微信小程序版本
- 实现H5移动端页面
- 添加APP推送通知功能
-
支付集成:
- 接入微信支付和支付宝
- 实现退款流程
- 添加发票开具功能
-
营销功能:
- 优惠券和折扣活动
- 积分奖励系统
- 限时抢购功能
7.2 性能优化
-
数据库优化:
- 对大数据量表进行分表分库
- 使用读写分离架构
- 优化索引设计
-
缓存策略:
- 实现多级缓存
- 优化缓存失效策略
- 热点数据预加载
-
异步处理:
- 使用消息队列解耦耗时操作
- 实现异步导出功能
- 批量处理代替单条操作
7.3 架构演进
-
微服务化:
- 按业务拆分微服务
- 引入服务注册发现
- 实现服务熔断降级
-
容器化部署:
- 全面迁移到Kubernetes
- 实现CI/CD流水线
- 采用蓝绿部署策略
-
云原生改造:
- 使用Service Mesh管理服务通信
- 实现自动扩缩容
- 采用Serverless架构处理突发流量
在实际开发中,我们遇到的最棘手问题是订单状态同步问题。最初的设计是支付成功后直接更新订单状态,但在网络不稳定的情况下会出现状态不一致。后来我们引入了消息队列和状态补偿机制,确保最终一致性。这个经验告诉我们,分布式系统中的状态管理需要特别小心,不能假设所有操作都会一次性成功。