ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信小程序与SSM框架实现的文玩电商系统设计与实践

微信小程序与SSM框架实现的文玩电商系统设计与实践

一款基于微信端的文玩销售小程序,后端用了SSM框架,这类项目在学生项目和中小商户里出现得非常多。标题里的“kaic”一般是项目打包者的标识,说明源码被整理过。我见过不少类似需求,大多是毕设选题、课程设计,或者小商户想快速搭个线上销售渠道。今天不绕弯子,直接把这个项目从设计思路到踩坑实录完整拆一遍。

先聊清楚选题的价值。微信小程序做电商,天然有流量优势,用户点开即用,不需要下载App,分享到群聊或朋友圈的路径非常短。文玩这个品类的特点也很有意思——单价高、真假难辨、买家决策周期长,传统的线下交易依赖信任和眼缘,搬到线上之后,需要的不是简单陈列,而是细节图、证书、知识科普这些“信任道具”。我们这套系统,表面上是个销售工具,本质上是在做“把线下文玩信任逻辑复制到线上”这件事。

对于开发者来说,这个题目的技术覆盖面很均衡:前端小程序要处理登录、支付、商品展示、订单流转;后端SSM要管REST接口、业务事务、数据库表关联。做一遍下来,前后端打通的概念、微信生态的约束、电商业务的状态机设计,都能摸到。适合拿来练手,也适合商户拿来做真实运营。

1. 整体设计与架构拆解

1.1 为什么选SSM组合而不是Spring Boot

十年前但凡做Java电商项目,SSM是绝对的主流,Spring管理Bean、SpringMVC做路由控制、MyBatis操作数据库,这套组合稳定可靠,资料也多。放到今天,Spring Boot当然更省事,配置少、起步快,但SSM仍然有它的存在价值——如果你需要跑在低配云服务器上,SSM的启动速度和内存占用确实要好看一些;如果团队里老项目多,SSM的技能也能直接复用。

这个项目里具体的分工是这样的:

  • Spring:负责业务对象的创建和依赖注入。Service层、Mapper层都由容器管理。
  • SpringMVC:接收小程序发来的HTTP请求,通过Controller路由到对应的Service方法,再把结果以JSON格式返回。
  • MyBatis:写SQL的地方。文玩商品有大量条件筛选,比如按品相等级、按价格区间、按材质,MyBatis的动态SQL写起来要比JPA灵活得多,尤其是多条件组合查询,直接拼XML里的if标签就行。

1.2 前端选型:原生小程序还是uni-app

我在实际项目里看到的情况是,uni-app开发的占比正在上升,但原生小程序也远没到被淘汰的地步。区别在于:

原生小程序的wxml和wxss跟HTML/CSS不完全一样,写着顺手需要适应一下。它的优势是微信官方API支持最直接,比如登录的wx.login、支付里的wx.requestPayment,调用链路最短,出问题的几率小。uni-app的优势是一套代码能发多个平台,以后要做支付宝小程序或者抖音小程序,不用从头写。

个人建议是,如果这个项目就是给微信单平台用的,直接用原生;如果后续有多端需求,再考虑uni-app。我自己做的话,维护成本可控、上线周期短,是优先考虑的事。

1.3 数据库表设计思路

这是整个项目最关键的部分,表结构设计得好不好,直接决定后面前端页面的写法。文玩销售小程序至少要包含这几类表:

  • 用户表(买家信息、昵称头像、手机号、地址、微信OpenId)
  • 商品表(标题、描述、材质、尺寸、品相、价格、库存、图片地址列表、上下架状态)
  • 分类表(手串、把件、挂件、茶叶茶具等,用parent_id支持两级或多级)
  • 订单表(订单号、用户ID、商品ID、数量、总价、收货地址、订单状态、支付方式)
  • 购物车表(用户ID、商品ID、数量、选中状态)
  • 收藏表(用户ID、商品ID、收藏时间)
  • 证书表(商品ID、证书编号、颁发机构、证书图片)

订单表设计的时候,主键不要用自增ID拼订单号,最好单独生成业务订单号,格式类似当前时间戳+随机数,这样用户在售后沟通时报一个订单号,你和客服都舒服。数据库主键可以用自增,但对外暴露的全用订单号。

2. 核心模块的实现要点

2.1 微信登录与用户体系打通

微信小程序的登录逻辑是固定的,但我见过太多人写错。正确流程是:小程序端调用wx.login拿code,把code传给后端,后端拿着code去微信服务端接口换openid和session_key。这里有一个特别容易踩的坑——很多新手会直接把code当成openid存在数据库里,或者每次登录都重新调wx.login。code的有效期只有五分钟,换取的openid才是用户唯一标识,千万不要把这两个概念混了。

推荐做法是,数据库用户表里存openid作为唯一标识,同时给用户生成一个自己系统的userId。后端返回给前端的是业务token,前端后续请求都带这个token走。这样即使你将来服务端换成别的语言,或者增加多端登录,也不用改动微信那边的逻辑。token生成可以用UUID或者JWT,JWT的好处是自带过期时间,适合做会话维护。

2.2 商品展示和文玩品类的特殊处理

文玩不比标品,买家要看纹理、看细节、看瑕疵。所以商品详情页的图片展示逻辑很重要。UI上不要只放一张首图,建议至少支持5到10张详情图,且首图拼接不能压缩到太模糊。小程序端可以用swiper组件做轮播,后端在上传图片时直接生成两种尺寸的图——缩略图给列表页用,原图给详情页用。

文玩商品还要有一个特征,就是“瑕疵描述”。像玉石有棉絮、木质手串可能有裂纹、核桃可能有色差,这些在商品描述里必须写明。系统在设计时,要给商品表加一个字段叫defect_description,中文叫瑕疵说明。这个字段在数据库里可以设成text类型,允许为空,但商户在发布商品时系统会强提醒一次是否填写。这么做不只是出于诚信考虑,更是减少售后的有效手段——很多退货是因为图文不符,你把缺陷提前说清楚,能劝退一批不合适的买家,留下来的都是能接受的,纠纷自然少。

2.3 订单流程的状态机设计

订单是最容易出bug的地方,核心问题在于状态管理。这个项目里,我把订单状态设计成如下几个:

  • 待付款(用户提交订单后,未支付)
  • 待发货(支付成功,等待商家发货)
  • 待收货(商家已发货,等待买家确认)
  • 已完成(买家确认收货,订单结束)
  • 已取消(用户取消,或者超时未支付系统自动取消)
  • 退款中(买家发起退款申请,商家审核中)
  • 已退款(退款完成)

这里要注意的是,状态的流转是有方向性的,不是乱改。比如已完成的订单理论上就不能再退款,已发货的订单用户不能直接取消,要取消就得走退款流程。所以后端接口需要对每个状态变更做校验,前端隐藏按钮只是第一层,后端的判断才是真正的保障。我见过有的项目图省事,把状态直接做成一个int存在订单表里,前端传值就改,结果用户拿着已发货的订单去取消,也能成功。这种低级错误直接影响钱和口碑。

我建议在后端Service层里写一个独立的订单状态校验方法,每次变更前统一走一遍,类似switch判断当前状态和目标状态之间是否可达。代码很啰嗦,但逻辑清晰,出问题也好排查。

2.4 购物车和库存扣减的并发问题

文玩不像普通电商,库存一般都很小。一件孤品手串,可能库存只有1,两个人同时下单,谁成功谁失败在毫秒级别内就有结论。如果代码里写的是“先查库存,再减库存,再创建订单”,在高并发下会出现超卖——两个人同时查到库存还有1,然后各自创建订单,最后库存变成了-1。

解决思路有两个维度:

第一种,数据库层做原子操作。在创建订单时,直接执行一条带条件的更新SQL:UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0,然后用受影响行数来判断是否扣减成功。如果影响行数为0,说明库存不足或商品已被抢完,立即返回失败。

第二种,引入Redis作分布式锁。适合库存比较紧张、秒杀场景。因为涉及的架构更复杂,毕设项目或者小商户初期用不上,我建议先把数据库的原子扣减做好。实测下来,这种方法在库存几十以内的场景完全够用。

2.5 证书管理和信任背书

文玩交易最难的地方是建立信任。线上没有面对面验货的过程,全靠图片和描述,所以证书体系很重要。小程序端应该单列一个“证书查询”入口,买家可以输入证书编号,查询对应商品的鉴定信息。后端实现起来不复杂,一张证书表,字段包括证书编号、鉴定机构、鉴定结论、对应商品ID、证书图片、生成时间。

商品详情页里加一个“查看证书”按钮,点击跳转或者弹窗展示证书图片。商户后台要支持证书批量上传——因为有时候一批货一起鉴定,一起传要方便得多。证书编号用固定前缀加日期加流水号,比如CERT202501010001。

3. 实操过程与核心实现

3.1 环境准备和项目启动

老规矩,先把基础环境列出来。SSM项目跑起来需要的工具包括:JDK(建议1.8)、Maven(3.x)、Tomcat(8或9)、MySQL(5.7或8.0)、微信开发者工具。

拿到源码之后,第一步不是急着点击运行,而是先看几个配置文件:

  • jdbc.properties里改数据库连接串、用户名、密码
  • spring-mvc.xml里看包扫描路径
  • mybatis的mapper文件路径

然后建库建表。一般是执行项目里自带的sql文件,没有的话就自己用手里的表结构文档建。如果源码里的数据库名和你的不一致,记得要改jdbc.properties。我碰到过很多次,项目跑起来报"Table doesn't exist",最后都是库名对不上或者建了新库但没执行sql脚本。

Tomcat部署时,把项目打成war包放进webapps目录,或者直接在IDEA里配置Tomcat的Deployment指向项目的war包。注意访问路径,如果上下文路径带项目名,小程序端请求的baseUrl要相应调整。

3.2 后端关键接口示例:商品列表

下面贴一个核心接口的简化代码,展示SSM三层调用逻辑。这个是商品列表页的接口,支持分类、关键词、价格区间筛选,分页返回。

先看Controller层:

@RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @RequestMapping(value = "/list", method = RequestMethod.GET) public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, Integer categoryId, String keyword, BigDecimal minPrice, BigDecimal maxPrice) { ProductQuery query = new ProductQuery(); query.setPageNum(pageNum); query.setPageSize(pageSize); query.setCategoryId(categoryId); query.setKeyword(keyword); query.setMinPrice(minPrice); query.setMaxPrice(maxPrice); return Result.success(productService.pageQuery(query)); } }

Service层处理业务,调用MyBatis的Mapper:

@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Override public PageResult<Product> pageQuery(ProductQuery query) { query.setOffset((query.getPageNum() - 1) * query.getPageSize()); List<Product> list = productMapper.selectByCondition(query); long total = productMapper.countByCondition(query); PageResult<Product> result = new PageResult<>(); result.setList(list); result.setTotal(total); result.setPageNum(query.getPageNum()); result.setPageSize(query.getPageSize()); return result; } }

Mapper XML里的动态SQL是核心:

<select id="selectByCondition" resultType="Product"> SELECT * FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="minPrice != null"> AND price &gt;= #{minPrice} </if> <if test="maxPrice != null"> AND price &lt;= #{maxPrice} </if> AND status = 1 </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>

这个接口写完,小程序端请求REST风格URL即可,后端返回JSON。这里的status = 1指的是上下架状态,1是上架,0是下架。注意在上架状态下才会展示给普通用户,商户自己在后台可以看见所有状态。

3.3 小程序端请求封装与页面对接

小程序端网络请求有两个注意点:第一,必须在小程序后台配置合法的request域名;第二,开发时可以勾选“不校验合法域名”。上线前,一定要把开发工具里的勾选去掉,并且在后端域名那边配上SSL证书。如果域名没备案或者证书过期,小程序上线后接口全部请求失败,表现是页面空白、数据加载不出来,那时候排查起来头疼。

小程序端的请求封装,可以新建一个utils/request.js,统一处理BaseURL、token注入和错误码判断。代码如下:

const BASE_URL = 'https://yourdomain.com/ssm-project'; function request(url, method, 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) => { reject(err); } }); }); } module.exports = request;

页面里调用时:

const request = require('../../utils/request'); Page({ onLoad: function () { request('/api/product/list', 'GET', { pageNum: 1, pageSize: 10 }).then(data => { this.setData({ products: data.list }); }); } });

3.4 支付流程的取舍与实现

微信支付接入需要商户号,个人开发者没法直接申请。在学生项目里,最常见的替代方案是模拟支付——前端调起一个支付页面,用户点击“确认支付”后直接把订单状态置为已支付。这种方案对演示和学习来说是够用的,但真正商用不行。

替代方案还有一种:线下转账+人工到账确认。在订单提交页面生成一个收款二维码,用户转账后商户在后台看到转账记录,手动点击确认收款,再把订单推送到“待发货”状态。这个方案虽然老土,胜在不需要商户资质,适合小商户起步阶段。

我真的用这个思路做过一个项目,运行下来的效果还挺好。单独做一个后台收款核对页面,订单列表按“待确认收款”和“已确认收款”区分。商户每天打开后台看一遍,把转账记录和订单对上号就行。没有太多自动化,但稳定、没风险,前半年完全是这个模式撑起来的。

如果以后发展起来需要微信支付,记得代码里预留好支付回调接口的位子。建议在订单表里加一个pay_channel字段,先存"offline",以后接了微信支付再存"wechat",不用改动表结构太多。

3.5 后台管理端的实现

这个项目里,管理端要么做成Web管理页面,要么做成小程序的管理员页面。用SSM做的话,后台管理页面用AdminLTE或EasyUI模板比较多,登录后看到订单管理、商品管理、用户管理、证书管理等菜单。

后台管理端的几个核心功能点:

  • 商品发布:支持多图上传、分类选择、价格和库存填写、详情富文本编辑
  • 订单处理:查看订单详情、确认发货、填写物流单号
  • 退款处理:审核退款申请,通过后走原路退回或线下转账
  • 证书管理:上传证书图、绑定商品、生成证书编号
  • 数据统计:简单的今日订单数、销售额柱状图

如果后台页面用JSP技术栈,加上SSM的生态是一致的,不需要额外引入前端框架。但如果想做更现代一点的,管理端单独用Vue+ElementUI也完全可行,毕竟只是调用REST接口。

4. 常见问题与排查技巧实录

4.1 小程序真机预览时加载不出图片

这个问题我在测试时几乎每次都会遇到。原因通常是以下三种:

  1. 图片存的是本地路径,开发工具里能看,真机上访问不了
  2. 图片域名没有加入downloadFile合法域名
  3. 图片URL是http,不是https,小程序要求所有网络资源都是https

解决办法:图片尽量传到对象存储服务,比如云的OSS,然后配置CDN域名。开发阶段如果实在没法用https,就把“不校验合法域名”打开,但上线前必须关掉。说白了,这个问题的根子还是在域名配置上,而不是代码逻辑。

4.2 后端接口正常但小程序端返回500

这个一般分两类情况。第一类是JSON序列化失败,通常是因为对象里有循环引用或者Date类型格式化失败。SSM项目里如果Controller返回的对象里含Date,默认序列化成时间戳,小程序端解析可能出问题。建议在SpringMVC的配置里统一配置JSON序列化格式,把Date格式化为yyyy-MM-dd HH:mm:ss。

第二类是接口跨域问题,小程序端不是浏览器,一般不受跨域限制,但如果后端在调试时用了浏览器访问,会出现跨域报错。措施是在后端加一个跨域过滤器,或者单独给前端调试单独开跨域放行。

4.3 微信登录code换openid失败

最常见的坑:AppSecret配错了。去微信公众平台核对一下,再查一下IP白名单——小程序后台的IP白名单没有加上你服务器的出口IP,接口也会返回错误。

还有一个小概率问题,就是后端请求微信接口时,参数名写错。注意是appid不是appId,secret不是appSecret,这些大小写和拼写错误,微信服务器是不通融的。

4.4 订单支付成功但列表里还是待付款

这种问题基本出在状态更新的实现上。如果用的是模拟支付,前端点击确认支付之后,要直接调用后端的支付结果更新接口。这个接口里要做两件事:更新订单状态为已支付,扣减库存。如果只做了第一件,库存会爆,后面会出现超卖。

还有一点,看看事务有没有加。支付更新状态的Service方法必须加上@Transactional,否则中间报错会导致订单状态改了但库存没扣,或者反过来,数据不一致。

@Transactional public void paySuccess(String orderNo) { // 1. 校验订单状态是否为待付款 // 2. 更新订单状态为已付款 // 3. 扣减库存 // 4. 记录支付流水 }

4.5 管理端登录容易被人绕过

安全方面不能马虎。后台管理端绝对不能只用前端路由守卫,后端每个接口都要做权限拦截。SSM项目里可以用SpringMVC的拦截器,在preHandle里判断管理员是否登录。如果你不做,别人直接扫包路径就能访问后台接口,这跟家门没锁没区别。

建议至少做一个简单的token校验,管理员登录时生成一个UUID作为token,存到Redis或数据库,每次请求带上,拦截器校验。虽然简单,但对毕设和中小商户来说足够挡掉大批无效请求。

5. 优化与扩展建议

5.1 搜索体验优化

文玩用户搜索往往带有专业术语,比如“小叶紫檀手串”“蜜蜡随形吊坠”。如果只用MySQL的LIKE模糊查询,匹配效率一般,关键词拆分也不好做。简单方案是做分词,把商品标题和描述拆成词,存到搜索索引表,查询时反向匹配。进阶方案是引入搜索引擎框架,但对这个小项目来说,有点大材小用。我建议先把词库做好,在小程序端加上热门搜索和搜索历史,能明显提升找货效率。

5.2 资讯内容运营

文玩买家大多数不是高手,需要知识普及。在小程序里加一个“文玩百科”或“每日一鉴”栏目,每周更新一到两篇辨别技巧、盘玩经验的文章,能有效黏住用户。后端只需要一个文章表,小程序端做一个列表页和详情页就能跑起来。这类内容运营对商户来说既是获客手段,也是建立专业形象的渠道。

5.3 拍卖与预定玩法

文玩圈子有很大一部分交易是拍卖和预定。如果项目跑起来稳定,可以扩展两个模块:

拍卖模块的实现逻辑:拍品表、出价记录表。竞价时每次出价要判断是否高于当前最高价,并且结束时间不能直接取前端传的时间,要以服务器时间为准。拍卖结束后自动生成订单,通知买家支付。

预定模块的逻辑:商品状态加一个“可预定”,用户支付定金后生成预定订单,到货后再支付尾款。这类功能对文玩圈子里的手作定制类目特别好用。

5.4 数据统计分析

后台管理加一个简单的数据统计页面,统计每日订单量、销售额、新增用户数、热销商品Top10。实现不难,用MyBatis写几个聚合查询SQL,前端用ECharts画几张折线图柱状图,商户看到数据增长才有信心持续投入运营。

写在最后的一点体会

我做这款小程序的过程中,最大的感悟是:文玩电商不是把商品图放上去就完事,背后的核心是信任体系的搭建。买家敢在看不到实物的情况下下单,靠的是清晰的图片、诚实的描述、可追溯的证书、顺畅的售后体验。代码层面,这就要把商品详情做得足够丰富,订单流程的每一步状态变化让双方都看得见。

技术上,SSM这个组合确实不如Spring Boot新潮,但对于这类中规中矩的电商业务,它的稳定性是过关的。框架只是工具,真正决定项目上限的,是业务流程的设计能力和对用户需求的理解。如果你拿到这套源码,不要着急跑通就完事,沉下心把订单状态机理清楚,把并发扣库存的逻辑吃透,再想想到底怎样让用户更信任你的商品。把这些想明白,你做的东西就不只是一份毕设作业,而是能拿去真实运营的营生工具。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进