SSM+微信小程序社区团购系统:数据库设计与权限模型实战解析
2026/9/14 4:30:26 网站建设 项目流程

简介:毕业设计Java结合微信小程序的社区团购系统,采用SSM后台框架、Vue管理页面、MySQL数据库,面向高校计算机专业毕业生及相关开发者,提供一套完整可运行的商城类项目方案。资源共1250个文件,涵盖Java源码、Vue页面、微信小程序前端、SQL数据库脚本、XML配置及说明文档等,压缩包整体约121.52MB,结构清晰便于按模块复用与二次开发。后台覆盖管理员、商家、会员三类角色,实现商品管理、购买订单、退货退款、商品评价、用户充值、购物车等核心功能;前台微信小程序支持首页浏览、商品信息查看及我的订单管理。包内除完整源码外,还附有数据库脚本、论文、答辩PPT、开题报告、环境工具包及框架安装教程,可帮助快速部署与理解项目流程。已有116人学习下载,适合用于毕业设计参考、SSM与小程序开发练手或课设改造。

1. 社区团购项目为什么值得拆:SSM+微信小程序的组合逻辑

在毕业设计选题里,社区团购属于"看起来普通、拆开有货"的那类项目。普通在于业务是商品浏览、购物车、下单、退款这些模板能力;有货在于它同时承担了管理员、商家、会员三个登录端,后端用SSM框架(Spring+SpringMVC+MyBatis)做服务端,管理后台用Vue写,会员端放在微信小程序里,数据库MySQL,JDK 1.8,Eclipse、MyEclipse、STS、IDEA都能跑。这套资源里带源码、数据库脚本、论文、答辩PPT、开题报告、环境工具包,说明文档还附了相同框架项目的安装流程。适合正在做毕业设计的在校生,也适合想通过一个完整项目把SSM分层、小程序请求封装、Vue路由守卫一次打通的初级Java工程师。

2. 三角色功能拆解与数据库表设计:权限模型先从表结构说起

2.1 管理员、商家、会员的功能边界在哪里

社区团购系统的功能模块可以从三个角色的入口分别看。管理员服务端处理首页、个人中心、会员管理、商家管理、商品信息管理、商品分类管理、购买订单管理、退货退款管理、商品评价管理、系统管理;商家服务端的菜单只有首页、个人中心、商品信息管理、购买订单管理、退货退款管理、商品评价管理;会员端微信小程序则是首页、商品信息、个人中心里的会员信息、我的订单、购物车、用户充值、退货退款和商品评价。对比下来可以明显看到,商家端是管理员端的子集,会员端的所有操作最终都落到订单和商品两张表上。

这种结构在权限模型上是典型的RBAC简化版:用户表用role字段区分会员、商家、管理员,后端接口根据角色过滤数据范围。这里有一个经常被忽略的边界问题——权限控制要分两层,"能访问哪些接口"由拦截器负责,"能操作哪些数据"由SQL负责。很多毕业设计只做了接口级控制,商家登录后可以通过修改商品ID参数看到并操作别人的商品,数据级权限缺失。这个项目在商品表的查询SQL里强制带seller_id条件,把商家和商品的所有权绑死,答辩时被问"商家能不能把别人的商品下架"就能直接答上来。

2.2 核心表结构:用户、商品、订单三张主表

2.2.1 用户表(user)

用户表不区分会员、商家、管理员,而是用role字段做区分,这是RBAC最常见的落地方式。balance字段用于会员充值余额,购买订单可以直接扣余额,也可以做成模拟支付回调。密码字段单独说一句:毕业设计里MD5加密是及格线,如果答辩时被问到安全性,能说出"MD5加盐做哈希再落库"就已经是加分项,千万不要明文存密码,评审老师拿到数据库脚本第一眼就会检查这一列。

CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(MD5加密)', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `balance` decimal(10,2) DEFAULT '0.00' COMMENT '账户余额', `role` tinyint(4) NOT NULL DEFAULT '0' COMMENT '角色:0会员 1商家 2管理员', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

username字段加了唯一索引,注册时后端先查一次再插入,防止并发注册重复账号。role字段的取值范围与后端枚举UserRole一一对应,接口层拿到用户的role后决定放行还是拒绝。

2.2.2 商品表(goods)

商品表包含category_id和seller_id两个外键字段,category_id关联分类表,seller_id关联用户表中role=1的商家。status字段做上下架控制,商品下架后会员端列表不可见,但历史订单仍能正常展示,因为订单表里冗余了商品名称和价格的快照。

CREATE TABLE `goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `price` decimal(10,2) NOT NULL COMMENT '单价(元)', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `image` varchar(255) DEFAULT NULL COMMENT '商品图片URL', `description` text COMMENT '商品描述', `seller_id` bigint(20) NOT NULL COMMENT '商家用户ID', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_seller` (`seller_id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

price字段必须用decimal而不是float,float在电商金额计算里会有精度丢失问题,累计到退款环节可能差出几分钱。后端代码里所有金额计算统一用BigDecimal接收和运算,这个技术点也经常被评委拿出来问。

2.2.3 订单表(orders)

订单表同时承载会员下单和商家发货诉求,status字段存储数字编码,对应关系放到Java枚举里统一管理。order_no是业务订单号,展示给会员看,同时关联后续的退款记录。

CREATE TABLE `orders` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL COMMENT '会员用户ID', `goods_id` bigint(20) NOT NULL COMMENT '商品ID', `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '购买数量', `total_price` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待付款 1待发货 2待收货 3已完成 4已取消 5退款中 6已退款', `receiver_name` varchar(50) DEFAULT NULL COMMENT '收货人', `receiver_phone` varchar(20) DEFAULT NULL COMMENT '收货电话', `receiver_address` varchar(255) DEFAULT NULL COMMENT '收货地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

订单数据在答辩里容易被追问:为什么收货人、收货地址要存到订单表而不是下单时去关联用户表?答案是收货地址随时可能被用户修改,订单属于历史行为,必须保留下单时刻的快照。商品名称和价格同理,如果商品改名或调价,历史订单不能跟着变。

2.3 订单状态枚举的预设

当订单表里的status用数字而不是字符串时,代码可读性要靠枚举保证。这个项目把订单的七个状态封装成OrderStatus枚举,前端下拉框、列表展示、退款判断都依赖它。定义枚举的另一个价值是状态集合被锁死,后续代码里不会出现魔法值3代表"已完成"这类写法。

public enum OrderStatus { PENDING_PAYMENT(0, "待付款"), PENDING_DELIVERY(1, "待发货"), PENDING_RECEIPT(2, "待收货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"), REFUNDING(5, "退款中"), REFUNDED(6, "已退款"); private final int code; private final String description; OrderStatus(int code, String description) { this.code = code; this.description = description; } public int getCode() { return code; } public String getDescription() { return description; } public static String getDescriptionByCode(int code) { for (OrderStatus status : OrderStatus.values()) { if (status.code == code) { return status.description; } } return "未知状态"; } }

枚举的价值在返回JSON时体现出来。订单列表的VO里既要有statusCode用于支付回调的逻辑判断,也要有statusName用于页面直接渲染,两者来源就是同一个枚举类。如果项目里散落着if(status==3){return "已完成"}这类写法,后期改一个状态流转要牵连十几个地方。

3. SSM后端实现:登录、商品管理与订单状态机

3.1 Maven依赖与SSM整合配置

SSM整合第一步在pom.xml。项目用Maven管理依赖,核心是spring-webmvc、mybatis-spring、mysql-connector-java,分页插件PageHelper在列表接口里能省下不少手写LIMIT的功夫。

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.8.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.11</version> </dependency>

spring-mvc.xml里要处理三件事:包扫描、注解驱动、SqlSessionFactory创建。包扫描把com.community包下的Controller和Service一次性纳入Spring容器;注解驱动开启@ResponseBody等注解的能力,接口返回对象时自动转JSON;SqlSessionFactory指定数据源和Mapper XML的位置。

<context:component-scan base-package="com.community"/> <mvc:annotation-driven/> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>

这里有一个环境差异:本机是MySQL 8.0时,jdbc.driver要换成com.mysql.cj.jdbc.Driver,连接URL追加serverTimezone=Asia/Shanghai&useSSL=false,否则启动大概率报时区错误或SSL连接警告。这是用这套源码最容易踩的第一个坑,环境工具包里如果带的驱动是5.x,务必要确认和本地MySQL大版本匹配。

3.2 商品查询的Controller-Service-Mapper写法

商品管理的后端接口分管理端和商家端两套URL。管理端可以查看编辑所有商品,商家端只能操作seller_id等于自己ID的数据。下面是商家端商品列表的Controller简化写法。

@Controller @RequestMapping("/seller/goods") public class SellerGoodsController { @Autowired private GoodsService goodsService; @RequestMapping("/list") @ResponseBody public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, @RequestParam(required = false) String goodsName, @RequestParam Long sellerId) { PageHelper.startPage(page, limit); List<GoodsVO> list = goodsService.querySellerGoods(sellerId, goodsName); PageInfo<GoodsVO> pageInfo = new PageInfo<>(list); return Result.success(pageInfo.getList(), pageInfo.getTotal()); } }

sellerId不建议从前端传参,而是由拦截器从登录Session解析后注入。如果直接从页面参数接收,把sellerId改成别的值就能拉出其他商家的商品,属于越权漏洞。后端在做商家身份校验时,以拦截器里存的userId为准,和数据库中的seller_id比对。

对应的Mapper XML里,核心是动态条件拼接:

<select id="selectSellerGoodsPage" resultType="com.community.entity.vo.GoodsVO"> SELECT g.id, g.name, g.price, g.stock, g.image, g.status, c.name AS categoryName FROM goods g LEFT JOIN category c ON g.category_id = c.id <where> g.seller_id = #{sellerId} <if test="goodsName != null and goodsName != ''"> AND g.name LIKE CONCAT('%', #{goodsName}, '%') </if> </where> ORDER BY g.create_time DESC </select>

动态SQL里固定条件seller_id放在 标签内部第一行,可变条件放后面。 标签会自动处理前缀的AND,goodsName为空时不生成该段SQL,避免产生WHERE 1=1这种不优雅写法。LEFT JOIN category是为了在前端表格里直接展示分类名称,不用在Java代码里二次查库。

3.3 下单与库存扣减的事务边界

下单接口是整个项目里最值得在答辩现场讲清楚的一段代码,核心矛盾是扣库存和生成订单必须同时成功或同时失败。如果先扣库存再插订单,插单失败会导致库存凭空消失;如果先插订单再扣库存,库存不足时会出现无货订单。Spring的@Transactional注解把两步包进同一个数据库事务。

@Override @Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, Long goodsId, Integer quantity) { Goods goods = goodsMapper.selectByPrimaryKey(goodsId); if (goods == null || goods.getStatus() != 1) { throw new BusinessException("商品不存在或已下架"); } if (goods.getStock() < quantity) { throw new BusinessException("库存不足"); } Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setGoodsId(goodsId); order.setQuantity(quantity); order.setTotalPrice(goods.getPrice().multiply(new BigDecimal(quantity))); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); orderMapper.insert(order); Goods update = new Goods(); update.setId(goodsId); update.setStock(goods.getStock() - quantity); goodsMapper.updateByPrimaryKeySelective(update); return order; }

这里有两个细节。第一,updateByPrimaryKeySelective只更新非null字段,避免把商品的status、price等字段误刷成null。第二,扣库存的SQL在真正高并发场景下要写成UPDATE goods SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity},用数据库行锁保证不超卖。毕业设计不要求上分布式锁,但答辩时主动说出"可以在这里用乐观锁思路优化",老师会觉得你理解到了并发层面。

generateOrderNo()采用了系统时间戳加四位随机数的写法,单机部署够用,同一毫秒内并发下单时有极小概率碰撞。更稳的替代方案是时间戳加上用户ID后四位,既保留时间排序可读性,又能把碰撞概率降一个量级。

4. 前端联调:微信小程序端与Vue后台的对接细节

4.1 小程序请求封装与登录态处理

微信小程序的网络请求统一走wx.request,每个页面单独写会比较乱。项目里常见做法是封装request.js工具,统一处理接口地址前缀、请求头token和错误提示。登录成功后后端返回token,小程序存入本地缓存,每次请求自动带上。

// utils/request.js const BASE_URL = 'http://localhost:8080/community'; function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };

BASE_URL在开发阶段指向本地Tomcat的IP加端口。微信开发者工具模拟器里可以访问localhost,但真机调试时localhost指向手机自己,必须改成电脑在局域网中的IP,同时后端要配置CorsFilter放行跨域来源。

4.2 商品列表分页与购物车

会员端首页是商品列表,数据来自商品查询接口。下拉触底加载是电商小程序的标配交互,用onReachBottom生命周期配合分页参数实现。进入页面时onLoad触发第一页加载,这是小程序端默认的加载时序。

// pages/index/index.js const { request } = require('../../utils/request'); Page({ data: { goodsList: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.fetchGoods(); }, fetchGoods() { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); request('/api/goods/list', 'GET', { page: this.data.page, pageSize: this.data.pageSize }).then((res) => { this.setData({ goodsList: this.data.goodsList.concat(res.list), page: this.data.page + 1, hasMore: res.list.length >= this.data.pageSize, loading: false }); }); }, onReachBottom() { this.fetchGoods(); }, addCart(e) { const goodsId = e.currentTarget.dataset.id; request('/api/cart/add', 'POST', { goodsId, quantity: 1 }) .then(() => wx.showToast({ title: '已加入购物车', icon: 'success' })); } });

分页的关键在hasMore和loading两个标志位。hasMore由本次返回条数是否等于pageSize决定,loading防止onReachBottom被连续触发导致同一个page被重复请求。addCart方法里的商品ID来自点击元素的data-id属性,wx.request接收后需要确认后端Controller里的入参类型与前端传参类型一致。

4.3 Vue后台的路由拦截与接口对接

管理后台用Vue配合Element UI实现,登录成功后把token存到localStorage,路由守卫在每次跳转前检查token,没有就强制回登录页。axios实例在请求拦截器里统一注入token头,响应拦截器处理通用业务码。

import axios from 'axios'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); service.interceptors.request.use(config => { const token = localStorage.getItem('admin_token'); if (token) { config.headers['Token'] = token; } return config; }); service.interceptors.response.use( response => { if (response.data.code === 200) { return response.data.data; } return Promise.reject(new Error(response.data.msg)); }, error => Promise.reject(error) ); export default service;

Vue项目开发环境通过vue.config.js的proxy配置把/api前缀的请求转发到后端8080端口,避免反复处理跨域。生产环境打包后的dist目录静态文件可以直接放到后端webapp下,由同一个Tomcat提供服务。管理后台的菜单显隐用role字段控制,管理员看到全部菜单,商家登录自动隐藏会员管理和系统管理,注意前端隐藏只是体验优化,最终权限判定仍以后端拦截器为准,这个点答辩时主动说出来,面试观感会好很多。

5. 部署排错与答辩演示:把工具包从能跑到能讲

5.1 三个批处理脚本与部署顺序

源码包里的1-install.bat、2-run.bat、3-build.bat对应三个阶段的动作。1-install.bat初始化Maven依赖和数据库脚本;2-run.bat启动后端Tomcat;3-build.bat对Vue后台做npm install和npm run build。顺序不能颠倒,数据库没初始化就启动后端会直接报表不存在的异常。源码包里带.bak后缀的文件是Vue组件或样式文件的备份,不需要导入项目,打包时不会被引用。

排错集中在三个位置。第一,确认JDK版本是1.8,本机装了多个JDK时检查IDE的项目SDK设置。第二,8080端口被占用时执行netstat -ano | findstr 8080查看PID,再通过任务管理器结束对应进程。第三,MySQL 5.7与8.0的驱动类名不同,8.0使用com.mysql.cj.jdbc.Driver且URL追加serverTimezone=Asia/Shanghai&useSSL=false。

提示:如果启动时抛ClassNotFoundException: com.mysql.jdbc.Driver,说明mysql-connector-java版本与本地MySQL不匹配,优先检查jar包版本而不是改代码。

5.2 答辩演示路径设计

演示路径建议从管理员登录开始,依次创建商家账号、用商家账号上架商品、打开小程序注册会员并下单、支付后回到商家端发货、会员确认收货并评价。这条链路把三个角色全部串起来,每次切换角色时都能自然展示对应功能页面。演示时重点讲两个点:订单状态从待发货到待收货的流转切换,以及商家只能看到自己商品的权限控制。这两点做到了,项目深度也就出来了。

调试阶段可以用微信开发者工具的Network面板查看请求和响应,也可以借助抓包工具分析小程序请求是否正常到达后端。后端的日志级别调整到DEBUG后,MyBatis会在控制台输出每条SQL的执行语句,对照SQL和返回值就能快速定位是接口问题还是数据问题。最后一条实用建议:先把数据库脚本在本地完整执行一遍,确认所有表的数量和初始管理员账号与文档一致,再启动项目,这一步能省掉大半排错时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询