去年帮朋友做校园二手交易小程序,从立项到上线折腾了整整两个月。当时最深的感受是:二手交易和普通电商根本是两个物种,不是说你会写个商品列表、购物车、支付就是电商了,二手交易的核心是信任、定价、成色描述、纠纷处理这一整套逻辑。而市面上能参考的开源项目,要么只做了个壳,要么代码停留在demo阶段,距离真正能跑通交易还差很远。这篇文章就把我从零搭建二手交易小程序/APP的全过程拆开来讲,包括业务模型、技术选型、关键模块实现、排坑记录,以及源码目录设计,希望能给正在做类似项目的人省点时间。
如果你是要交毕业设计、接外包、或者认真做一个校园/同城二手交易产品,这篇都适用。我不会只贴代码,会把每个模块为什么这么设计、坑在哪里、上线后会发生什么问题都讲清楚。
1. 先想清楚一件事:二手交易拼的不是功能,是信任闭环
1.1 二手交易和普通电商的本质差异
普通电商是B2C,平台统一背书,七天无理由退货,东西是标准品,包装物流售后全部规范化。二手交易是C2C,卖家是个人,货品非标,成色参差不齐,价格没有统一标准,交易双方互不信任,而且二手交易天然带有"当面交易""邮寄交易"两种形态,这意味着平台要处理的信息维度和风险维度比普通电商多得多。
很多开发者第一次做二手交易小程序,上来就照着电商模板画页面:商品列表、商品详情、加购物车、下单支付。这是最大的误区。二手交易里根本没有购物车这个概念,用户看到一件合适的闲置品,第一反应是问卖家"还在吗""哪里交易""能少点吗",而不是直接下单。所以聊天IM和咨询入口的优先级,比购物车高得多。
另一个差异是价格和商品的对应关系。普通电商一个SKU对应一个价格,二手交易一物一价,同样一台iPhone,成色、电池健康度、是否过保、有没有维修记录,价格能差出一两千。这意味着商品详情页需要表达的信息密度远高于普通商品,成色描述、实物图数量、瑕疵说明、交易方式、卖家信用,这些字段一个都不能少。
1.2 从一笔真实订单拆解全链路
假设一个毕业生要卖一台MacBook Pro,从发布到交易完成,完整链路是这样的:
卖家身份认证 -> 发布商品(填写标题、描述、价格、成色、实物图)-> 平台内容审核 -> 商品上架展示 -> 买家浏览/筛选/搜索 -> 买家进入商品详情 -> 买家通过站内聊天咨询 -> 双方达成意向 -> 买家下单 -> 支付货款到担保账户 -> 卖家发货/或双方约定当面交易 -> 买家确认收货 -> 平台将货款结算给卖家 -> 双方互评。
这中间任何一个环节出问题,都会产生纠纷。比如买家收到货发现和描述不符,卖家说发货前是好的,这就是典型的纠纷场景。所以在设计系统的时候,订单状态机必须覆盖所有分支。
我用状态机把订单流转定义成下面这些状态,这也是实践后修改过几轮的结果:
code复制待支付 -> 已取消(超时未支付)
待支付 -> 待发货(买家支付成功)
待发货 -> 待收货(卖家发货,填写物流单号/或标记当面交易)
待收货 -> 已完成(买家确认收货,货款结算给卖家)
待收货 -> 售后中(买家申请退款/退货)
售后期 -> 已退款 / 已完成
每个状态变更都要写入订单日志表,包括操作人、操作时间、变更前状态、变更后状态、备注。这个日志在后续处理纠纷时特别有用,相当于整个交易过程的"黑匣子"。
1.3 MVP功能清单:哪些是必须的,哪些可以砍
我整理了一份做二手交易MVP(最小可行产品)的功能清单,按优先级排序:
- P0(必须有):微信登录、商品发布、商品列表/筛选、商品详情、站内聊天、下单支付、订单管理、个人中心、平台管理后台(审核+纠纷处理)。
- P1(建议有):卖家实名认证、商品举报、退款申请、评价系统、消息通知。
- P2(可以后续迭代):信用分体系、芝麻信用授权、拍卖、押金、社交动态流、个性化推荐、物流跟踪。
很多同学做毕设或Demo时,特别喜欢在P2功能上下功夫,比如做了一堆花里胡哨的社区动态,结果交易主链路反而没跑通。我的建议是先把P0做到能稳定运行,支付回调、订单状态、消息通知这些核心链路都不能出问题,再去想加分项。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型复盘:为什么第一版做小程序而不是APP
2.1 各种方案的实际对比
这个项目最终选型是uni-app + Vue3 + Spring Boot,我对比过几种主流方案,这里直接把真实感受写出来。
| 方案 | 开发效率 | 多端复用 | 性能 | 适合场景 |
|---|---|---|---|---|
| 原生微信小程序 | 高 | 仅小程序 | 最好 | 只做小程序,不需要App |
| uni-app(Vue3) | 高 | 小程序+H5+App | 良好 | 需要一套代码多端发布 |
| Taro(React) | 中高 | 小程序+H5+React Native | 良好 | 团队熟悉React |
| Flutter | 低 | App+小程序(有限支持) | 优秀 | 以App为主,不考虑小程序 |
为什么我没选原生小程序?因为项目预期不仅要做小程序,后期还要出APP。如果原生小程序先写一遍,后面再写App就是两套代码两套维护,成本直接翻倍。uni-app这时候的优势就体现出来了:一套Vue代码,编译到微信小程序、H5、Android/iOS App,虽然部分平台有兼容性差异,但至少核心业务逻辑不用重写。
如果你的团队只做微信小程序,那原生小程序依然是首选,性能最好,API支持最全,调试也方便,没必要为了跨端而跨端。
2.2 服务端技术栈搭配
后端我选的是Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis。选这套组合的原因很实际:团队熟悉Java,Spring生态完善,事务管理、定时任务、消息队列都有成熟方案,网上资料也最多,遇到问题搜一下就有答案。MyBatis-Plus相比JPA更贴近SQL思维,做复杂的多表联查、分页筛选更可控。
Redis在整个系统里承担了几个职责:会话缓存(存储登录token)、短信验证码(如果有)、热点商品缓存(首页和列表页的缓存)、分布式锁(支付回调幂等处理、库存扣减的并发控制)。以MVP阶段的体量来说,Redis和MySQL部署在同一台服务器就够用,不需要单独搞集群。
文件存储用的是阿里云OSS。为什么不自建文件服务?一方面是带宽和存储成本问题,另一方面是对象存储自带CDN加速,小程序端上传下载体验更好。如果你用七牛云也可以,逻辑是一样的,就是调用SDK上传获取URL。图片URL要存到数据库里,商品详情、聊天图片、头像都会用到。
部署架构也很简单:一台2核4G的云服务器,上面跑Nginx + Spring Boot Jar + MySQL + Redis。Nginx负责HTTPS证书、反向代理、静态资源服务,后端接口通过/api前缀路由到Spring Boot。小程序端不需要服务器,直接通过微信平台托管静态资源,只需要确保接口域名是HTTPS并且在小程序后台配置了合法域名。
2.3 小程序开发环境里的三个"看不见"的坑
这几个坑凡是做过小程序的人基本都踩过,我一个个说。
第一,域名白名单。小程序请求后端接口,必须在小程序管理后台配置request合法域名、uploadFile合法域名、downloadFile合法域名,而且必须是HTTPS。开发调试的时候可以在开发者工具里勾选"不校验合法域名",但你敢不配就敢给你线上环境直接请求失败。这个地方最容易忽略的是downloadFile域名——比如商品图片CDN域名如果和小程序后端接口域名不同,需要单独配置在downloadFile合法域名里。
第二,HTTPS证书。小程序要求接口必须HTTPS,而且证书需要是正规CA签发的,自签证书不行。我遇到过证书链不完整的坑,浏览器访问一切正常,小程序请求直接报"证书校验失败",检查了半天发现是Nginx配置证书的时候漏了中间证书。
第三,ICP备案。这是最容易被忽略的。小程序后端接口域名必须完成ICP备案,否则根本无法接入微信小程序。我有个朋友项目做完了,才被卡在备案上,白白等了两周。域名备案要提前做,不要等到项目上线前才想起来。
另外还有一个类目资质问题:如果你的小程序涉及商品交易,微信会要求选择"电商平台"或"商家自营"类目,企业主体必须提供营业执照(部分类目还需要特殊资质如食品经营许可证),个人主体基本无法开通微信支付的交易能力。所以商业化的二手交易小程序,必须用企业主体注册和认证。做毕设的话,如果不需要真实支付,可以用模拟支付,但架构上要预留真实支付的接口。
3. 用户身份与登录授权:最容易翻车但又最绕不开的一环
3.1 微信登录到底在登录什么
很多人第一次写微信登录时上来就调wx.login,拿到的code放到请求里发给后端,后端再拿着code去换openid,最后把openid当成用户ID存入数据库。这个流程看起来对,但有几个细节处理不对会影响后面的功能扩展。
首先openid只能标识一个用户在某个小程序下的唯一身份,它不具备跨应用统一身份的能力。如果后面要做APP端、公众号端,需要unionid来统一身份。获取unionid需要小程序绑定微信开放平台账号,这个在MVP阶段可以先不管,但设计用户表时最好预留unionid字段。
其次是登录态管理。每次wx.login都会生成新的code,code五分钟内有效,只能使用一次。正确做法是:前端wx.login获取code -> 发送到后端 -> 后端调用微信接口code2Session换取openid和session_key -> 后端生成自定义登录态token -> 返回给前端 -> 前端把token存到本地 -> 后续请求header带上token。
我习惯用JWT生成token,优点是服务端无状态、不需要存session,缺点是token一旦签发无法主动失效。所以如果你的系统需要封禁用户、踢人下线、强制退出这些能力,JWT会比较不方便。另一种做法是使用Redis存token,登录时生成一个随机字符串作为key,存到Redis并设置过期时间,每次请求从Redis查询。这种方式灵活,可以随时删除token实现强制下线,代价是每次请求多一次Redis查询。MVP阶段我建议用第二种,控制逻辑更简单。
核心代码大致是这样的:
java复制// 后端接收前端传过来的code
public LoginResult wxLogin(String code) {
// 1. 调用微信接口,用code换取openid
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid
+ "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code";
String response = restTemplate.getForObject(url, String.class);
JSONObject json = JSON.parseObject(response);
String openid = json.getString("openid");
String sessionKey = json.getString("session_key");
// 2. 根据openid查用户表,不存在则新注册
User user = userMapper.selectByOpenid(openid);
if (user == null) {
user = new User();
user.setOpenid(openid);
user.setNickname("微信用户" + randomSuffix());
user.setAvatar("默认头像URL");
user.setStatus(1);
userMapper.insert(user);
}
// 3. 生成自定义登录态token,存Redis
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set("login:token:" + token,
String.valueOf(user.getId()), 7, TimeUnit.DAYS);
return new LoginResult(token, user);
}
前端那边的逻辑是:
javascript复制// 小程序端
uni.login({
provider: 'weixin',
success: async (loginRes) => {
const res = await request.post('/api/user/wxLogin', { code: loginRes.code });
if (res.code === 0) {
uni.setStorageSync('token', res.data.token);
uni.setStorageSync('userInfo', res.data.user);
}
}
});
3.2 手机号解密和头像昵称获取的正确姿势
老版本的wx.getUserInfo接口在2021年前后基本废弃了,现在微信要求用新的头像昵称填写能力。头像用button组件的open-type="chooseAvatar",昵称用input组件的type="nickname",用户主动授权填写后,前端拿到头像临时路径和昵称,上传头像到OSS,再调用后端接口更新用户资料。
手机号获取也有专门的方法。使用button的open-type="getPhoneNumber",用户点击同意后,返回一个code,把这个code传给后端,后端调用微信接口换取手机号。这里要提醒一下:这个code是动态的,每次点击获取都不一样,而且后端换手机号接口需要小程序绑定手机号快速验证组件,需要开通对应的权限。
为什么二手交易必须要手机号?因为交易和安全的需要。用户发布商品时要留联系方式,交易纠纷时要能追溯,平台风控也要识别异常注册和刷单行为。虽然MVP阶段可以允许用户不绑定手机号也能浏览,但发布商品时必须强制绑定手机号,否则广告和诈骗风险会指数级上升。
手机号在数据库里不要明文存储,至少要加密。推荐用AES加密或哈希脱敏。你可能会说反正后端没几个人能访问,但出于用户隐私保护,手机号这类敏感信息必须加密存储、脱敏展示。
3.3 登录态过期、静默登录与多端会话
token有效期设置多长?太短用户频繁重新登录体验差,太长又怕安全问题。实测下来,token有效期7天是一个比较平衡的值,用户一般一周内会多次打开小程序,过期后静默重新登录即可。
具体做法是:进入小程序首页时,先检查本地有没有token,有就先用,但调用接口时如果后端返回401(token失效),再静默执行wx.login刷新token。这个策略叫"先乐观后悲观",用户无感知,不用每次打开都要走一遍登录流程。
多端会话这里有一个容易被忽略的点:同一个用户在小程序端和App端同时登录,如果使用Redis token方案,两个token对应同一个userId,是可以共存的。但如果某些操作需要互斥,比如强制下线、账号异常提醒,最好维护一个token版本号机制,每次登录版本号+1,旧token自动失效。
4. 商品发布与信息流展示:二手商品的特殊性决定了实现方案
4.1 二手商品的字段设计比普通商品"脏"得多
普通电商的商品表字段可以标准化,标题、主图、价格、库存、规格、详情,就差不多了。二手商品不一样,它需要表达的信息要素更多,我把商品表设计成了这样:
code复制product表:
- id, user_id(卖家ID)
- title, description
- price(价格,存分为单位)
- original_price(原价,可选)
- category_id(类目ID)
- condition_level(成色:1全新 2几乎全新 3轻微使用痕迹 4明显使用痕迹 5破损)
- purchase_year(购买年份)
- is_warranty(是否有保修)
- warranty_desc(保修说明)
- trade_type(交易方式:1同城面交 2邮寄 3两者皆可)
- location(位置信息,城市+区域)
- status(状态:0待审核 1在售 2已售出 3下架 4违规)
- view_count, favorite_count
- create_time, update_time, delete_flag
成色这个字段是二手交易特有的,它直接影响用户决策。但不同品类的"成色"标准完全不一样,手机可以说屏幕有无划痕、电池健康度,衣服要说有无起球、有无异味,所以严格来说每个品类都应该有自己的属性模板。MVP阶段可以先用一个统一的成色枚举,后续再迭代成属性模板体系。
价格为什么用分存储?因为浮点数在计算机里是有精度丢失的,0.1 + 0.2 的结果是 0.30000000000000004,虽然展示层可以四舍五入,但如果计算逻辑直接操作浮点,很容易出现一分钱的误差。把价格换算成整数分存储,所有计算走整型,展示的时候除以100,可以完全避免精度问题。
4.2 图片上传与存储:九宫格、压缩、ID反向关联
商品图片上传是信息流体验的关键。微信小程序用wx.chooseMedia选择图片,一次最多9张,选择后前端先做压缩(图片超过一定大小先用canvas压缩,质量调低),再调用uni.uploadFile上传到OSS。压缩这一步很重要,很多人忽略了,结果一个商品详情页加载了三四张3MB的原图,用户在弱网环境下直接卡死。
上传命名规则我建议是:product/{userId}/{yyyyMMddHHmmss}_{随机字符串}.jpg,这样既便于按用户维度排查问题,又能保证文件唯一性,避免同名覆盖。
图片和商品的关系要单独建表,不能把图片URL拼在一个字段里。product_image表:id、product_id、image_url、sort、create_time。查询的时候按product_id和sort排序取出所有图片。这样设计的好处是商品详情页可以灵活展示多图,后续如果要做图片裁剪、缩略图版本,也能方便扩展。
另外图片上传完成后要调微信内容安全接口做图片鉴黄,具体的接入方式我会在4.4节展开。
4.3 列表页、筛选条件与搜索排序
列表页我采用的是"类目Tab + 综合筛选"的结构。首页顶部是类目导航(手机数码、电脑办公、生活电器、服装鞋包、图书文娱、其他),点击进入对应类目的商品列表,列表页支持按成色、价格区间、交易方式筛选,排序支持最新发布、价格升序、价格降序。
数据库查询MVP阶段用MyBatis-Plus的LambdaQueryWrapper拼条件就够,不需要上Elasticsearch。但有一个点要注意:列表分页不要用传统的offset分页(pageNum/pageSize),数据量到了一定程度,深分页会非常慢。推荐用游标分页方式,以id为游标,每次返回一页的同时把当前页最后一条记录的id带回来,下一页查询时加一个id < lastId的条件。
搜索功能同样先用MySQL的LIKE查询解决,配合索引。如果后续商品量级到了几十万,再上ES也不迟,搜索逻辑的重点是分词、相关性排序、筛选聚合,ES相比MySQL在这些维度上强太多了。
4.4 发布审核与违规内容处理
小程序发布商品必须做内容审核,这既是平台规则要求,也是保障交易环境质量的手段。微信提供了内容安全接口:
- 文本内容检测:msgSecCheck,对标题、描述、聊天消息做违规内容检测。
- 图片检测:mediaCheckAsync,对商品图片、用户头像做违规图检测。
这两个接口在用户提交时调用,检测结果有几种状态:pass(通过)、review(需要人工复审)、block(违规拦截)。建议策略是pass直接放行,review先进列表但标记为待复审,block直接拒绝发布。
还要有后续的人工审核机制。自动审核只能拦截大部分明显违规内容,很多灰色内容会绕过关键词规则。管理后台需要提供"商品审核"页面,管理员可以查看待审核商品、详情、图片,执行通过或下架操作。违规商品记录违规原因,累计违规次数,超过阈值可以限制发布权限或封号。
5. 交易链路实现细节:支付、担保、确认收货背后的代码逻辑
5.1 担保交易支付流程:钱先到平台账上
二手交易C2C模式必须做担保交易(也叫担保支付),买家下单付款后,钱不是直接打给卖家,而是先冻结在平台账户,等买家确认收货后再结算给卖家。这个机制是整个平台信任的基础,没有担保交易,用户之间的信任成本会高到产品几乎不可用。
微信支付v3接口的接入步骤是:
- 用企业资质申请微信支付商户号。
- 配置APIv3密钥和商户API证书。
- 后端调用JSAPI下单接口,传入openid、订单金额、商品描述,获取prepay_id。
- 后端对prepay_id签名,生成支付参数返回给前端。
- 前端调wx.requestPayment唤起微信支付。
- 用户支付成功后,微信服务器会异步通知后端接口(支付回调),后端收到回调要验签,然后处理订单状态。
这段回调处理的代码是整个支付链路的核心:
java复制@PostMapping("/api/pay/wxpay/notify")
public String wxPayNotify(HttpServletRequest request) throws Exception {
// 1. 读取请求体
String body = StreamUtils.copyToString(request.getInputStream(),
StandardCharsets.UTF_8);
// 2. 验签:用微信平台证书验证签名
boolean signValid = wxPayService.verifyNotifySign(request, body);
if (!signValid) {
return "FAIL";
}
// 3. 解密回调内容(v3版回调是加密的)
JSONObject data = wxPayService.decryptNotifyData(body);
String orderNo = data.getString("out_trade_no");
String transactionId = data.getString("transaction_id");
String tradeState = data.getString("trade_state");
if ("SUCCESS".equals(tradeState)) {
// 4. 幂等处理:防止重复回调导致订单状态重复更新
boolean processed = orderService.processPaidOrder(orderNo, transactionId);
if (processed) {
return "SUCCESS"; // 微信要求返回SUCCESS才停止回调
}
}
return "FAIL";
}
幂等处理非常关键。微信的支付回调可能因为网络原因重复通知多次,如果代码里不加判断,同一个订单可能被更新两次状态,产生脏数据。我习惯在订单表加一个paid_version字段,每次支付回调更新时对比version,或者用Redis分布式锁保证同一时间只有一个线程在处理该订单。
5.2 虚拟中间账户与自动确认收货
买家确认收货之前,钱在平台账户里,但平台不能直接把钱打给卖家(除非有清算资质),否则就涉嫌"二清"违规。合法的处理路径通常是通过微信支付的商家转账功能(企业付款到零钱)或者服务商分账接口,把货款结算给卖家。
这里要强调一个合规问题:想做二手交易平台,一定要走官方支付通道,不能自己搞资金池。自己收买家钱、再自己手动转给卖家,这在金融监管层面属于二清,是红线问题。MVP阶段,可以通过微信支付的"商家转账到零钱"接口,在买家确认收货后自动给卖家打款。虽然这个功能需要商户号和产品权限开通,但比自建资金池安全合法得多。
自动确认收货的逻辑用定时任务实现。卖家发货后,系统启动一个15天倒计时(具体时间可以配置),到期后如果没有买家申请售后,自动把订单状态改为"已完成",并触发转账给卖家的流程。这个定时任务用Spring的@Scheduled注解就能实现,每天凌晨扫描一次:
java复制@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void autoConfirmOrders() {
Date deadline = DateUtils.addDays(new Date(), -15);
List<Order> orders = orderMapper.selectByStatusAndShipTimeBefore(
OrderStatus.SHIPPED, deadline);
for (Order order : orders) {
// 确认收货 + 触发打款给卖家
orderService.confirmOrder(order.getOrderNo());
payService.transferToSeller(order.getOrderNo());
}
}
定时任务这种方案虽然简单,但在生产环境有一个坑:如果任务执行到一半服务器重启了,会有部分订单没处理到。所以设计上要考虑任务的幂等性,每次处理前查一下订单状态,已经处理过的跳过。如果数据量大,可以换成xxl-job这类分布式调度框架,但MVP阶段其实用不着。
5.3 交易纠纷仲裁的最小实现
前文说了二手交易纠纷率比普通电商高很多,所以系统必须提供争议处理的入口。当订单处于"待收货"状态时,买家可以发起售后申请,选择退款原因(商品与描述不符/商品损坏/未收到货/其他),上传凭证图片,提交后订单进入"售后中"状态。
卖家端会看到一个待处理的售后单,可以同意退款或拒绝退款。如果卖家同意,平台走退款流程,把已收的货款原路退回给买家。如果卖家拒绝,双方都可以申请"平台介入",管理员在后台看到纠纷单,查看订单日志、聊天记录、双方上传的凭证,然后做出裁决。
这个模块我用了一张独立的refund表来管理,核心字段包括:
code复制refund表:
- id, order_no, user_id(买家)
- seller_id(卖家)
- type(退款类型:仅退款/退货退款)
- reason, description, evidence_images
- amount(退款金额,分为单位)
- status(0待卖家处理 1卖家同意 2卖家拒绝 3平台介入 4已退款 5已关闭)
- result(裁决结果)
- create_time, handle_time
虽然看起来不复杂,但这个模块能救你命。没有纠纷处理机制的交易平台,用户遇到问题投诉无门,很快会流失,甚至去黑猫投诉、12315告你。即使MVP阶段,纠纷入口也绝对不能省。
5.4 聊天系统的低配实现
站内聊天是二手交易里必不可少的模块。初期我考虑过接入腾讯云IM、环信这类第三方SDK,但后来评估了一下,MVP阶段用户量不大,自建一个简单的基于轮询的聊天系统完全够用,而且省去第三方服务的费用和集成成本。
所谓轮询就是前端每3秒调用一次接口拉取最新消息,后端把聊天消息存到MySQL,每次查询返回该会话的新消息。优点是实现简单、稳定可靠,缺点是实时性一般、频繁请求对服务器有一定压力,但50个并发以内完全没问题。如果后面用户量起来了,可以替换成WebSocket长连接方案。
聊天消息表结构:
code复制chat_message表:
- id, session_id(会话ID)
- from_user_id, to_user_id
- content, msg_type(1文本 2图片)
- create_time, read_flag
session_id怎么生成?我用的是两个用户ID拼接排序后生成:比如from_user_id=10, to_user_id=5,则session_id = "5_10",这样避免重复会话。前端进入聊天页时,带着对方的userId和商品ID,请求后端获取会话ID和聊天记录。
这里还有一个小细节:聊天内容也要做敏感词过滤和内容安全检测,否则群发广告、诈骗内容会让你的平台很快失控。用微信的msgSecCheck接口对每条消息做检测,命中的直接拦截,可以拦截大部分垃圾消息。
6. 源码目录设计与沉淀:一个可维护的二手交易项目该长什么样
6.1 前端目录结构
项目用uni-app构建,目录规划要保证每个页面职责清晰,不要把所有页面堆在pages下面。我习惯按业务模块分子目录:
code复制src/
├── pages/
│ ├── index/ # 首页(类目导航+商品瀑布流)
│ ├── product/ # 商品发布、列表、详情
│ ├── order/ # 订单列表、订单详情、售后申请
│ ├── chat/ # 会话列表、聊天详情
│ ├── user/ # 个人中心、我的发布、我的收藏
│ └── login/ # 登录、手机号绑定
├── components/ # 公共组件(商品卡片、空状态、价格标签)
├── api/ # 接口请求封装
├── utils/ # 工具函数(格式化、鉴权、上传)
├── store/ # 全局状态管理(用户信息、token)
└── static/ # 静态资源(图标、默认图)
这个目录结构的关键点在于:api层统一管理所有后端接口,页面不直接发请求,而是调用api模块的方法。好处是接口变更时只需要改一处,排查问题也方便,哪块报错就查哪块。
6.2 后端目录结构
Spring Boot后端我按业务模块分包,而不是典型的三层包controller/service/mapper一刀切。业务模块化的好处是随着功能增加,代码不会变成一个巨型controller。
code复制com.example.secondhand/
├── common/ # 公共类(统一返回、异常处理、工具类)
├── config/ # 配置类(Redis、微信支付、OSS、MyBatis)
├── module/
│ ├── user/ # 用户模块
│ │ ├── controller/ # UserController
│ │ ├── service/ # UserService
│ │ ├── mapper/ # UserMapper
│ │ ├── entity/ # User
│ │ └── dto/ # LoginDTO, UpdateUserDTO
│ ├── product/ # 商品模块
│ ├── order/ # 订单模块
│ ├── pay/ # 支付模块
│ ├── chat/ # 聊天模块
│ └── admin/ # 管理后台模块
├── quartz/ # 定时任务
└── SecondHandApplication.java
每个模块内部按controller/service/mapper分层,模块之间通过service接口交互,不直接操作别的模块的mapper。这样以后维护的时候,改商品模块不会影响订单模块,新同学接手也容易理解。
6.3 数据库表设计清单
下面是MVP阶段的核心表清单,不算日志和统计表,业务表一共10张:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 用户 | openid, unionid, phone, nickname, avatar, status |
| product | 商品 | user_id, title, price, condition_level, trade_type, status |
| product_image | 商品图片 | product_id, image_url, sort |
| category | 类目 | name, parent_id, sort |
| favorite | 收藏 | user_id, product_id |
| orders | 订单 | order_no, product_id, buyer_id, seller_id, amount, status |
| order_log | 订单日志 | order_id, from_status, to_status, operator, remark |
| refund | 售后/退款 | order_id, buyer_id, seller_id, reason, status |
| chat_session | 会话 | user_id_a, user_id_b, product_id, last_message |
| chat_message | 聊天消息 | session_id, from_user_id, to_user_id, content, read_flag |
orders表是交易核心,单独展示几个关键字段设计:
sql复制CREATE TABLE `orders` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`buyer_id` BIGINT NOT NULL COMMENT '买家ID',
`seller_id` BIGINT NOT NULL COMMENT '卖家ID',
`amount` BIGINT NOT NULL COMMENT '订单金额(单位:分)',
`pay_amount` BIGINT DEFAULT NULL COMMENT '实付金额(分)',
`trade_no` VARCHAR(64) DEFAULT NULL COMMENT '微信支付交易号',
`status` TINYINT NOT NULL COMMENT '订单状态:1待支付 2待发货 3待收货 4已完成 5已取消 6售后中',
`ship_type` TINYINT DEFAULT NULL COMMENT '1邮寄 2面交',
`ship_time` DATETIME DEFAULT NULL COMMENT '发货时间',
`confirm_time` DATETIME DEFAULT NULL COMMENT '确认收货时间',
`create_time` DATETIME NOT NULL,
`update_time` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_buyer_id` (`buyer_id`),
KEY `idx_seller_id` (`seller_id`),
KEY `idx_status_create` (`status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易订单表';
订单号不要用自增ID,要自己生成,格式类似:"日期 + 随机数 + 用户ID后四位",例如202506081530123456789012。目的是防重复、防猜测、便于按时间排查。订单号的唯一索引必加,支付回调后续要靠它定位订单。
6.4 从MVP到商业化的扩展思路
MVP跑通以后,如果想继续推进商业化,可以按下面几个方向扩展:
第一是信用体系。引入芝麻信用授权(需要企业资质和申请),把用户的芝麻分作为交易互信的一个参考维度。没有芝麻信用的用户,可以通过实名认证、历史交易数据、卖家评价来累积平台内信用分。信用分可以直接体现在商品详情页,影响用户决策。
第二是物流和面交的深度融合。邮寄交易对接快递100,买家可以实时看物流轨迹。同城面交增加地图能力,让买卖双方在地图上确认面交地点,平台记录面交时间和位置,降低恶意欺诈风险。
第三是运营工具。优惠券、限时折扣、首页Banner位、置顶商品(付费推广),这些都是可以商业化的功能点。尤其是置顶推广,在校园二手平台上很受欢迎,毕业生着急卖东西会愿意花几块钱让自己的商品排在前面。
第四是APP端扩展。如果最终要发APP,uni-app的优势就体现出来了,核心代码不用重写,直接用HBuilderX云打包或本地打包Android/iOS安装包。分布式推送可以选择极光推送、个推这类第三方服务,小程序端的订阅消息和APP端的推送要分别实现,但业务逻辑层是通用的。
最后分享两个实战中的小体会
项目上线后,有一件事让我印象深刻。校园二手平台上有一批学生卖家发布商品特别积极,但成交率很低,原因不是价格问题,而是他们的商品描述太敷衍了,标题"出手机",描述就一张图一句话,买家看到完全没有购买欲望。后来我在发布页加了一个示例模板,引导用户填写成色、购入时间、使用感受这些详细信息,同时提示"商品描述越完整,成交速度越快",发布质量提升非常明显。所以产品功能本身只是一个维度,引导用户输出高质量的信息才是平台的核心运营动作。
另一个体会是关于风控的。做二手平台,永远要把"防骗子"当成最高优先级的功能,而不是追求极致的用户体验。我见过一个案例:一个卖家要求买家线下转账付款,买家觉得麻烦但同意了,结果钱转过去卖家直接拉黑。这类诈骗在二手平台上占比很高。后来我在聊天页面加了安全提醒:当消息中出现"微信""转账""线下""QQ"这些高风险关键词时,自动弹出警示条提示用户"请勿脱离平台交易"。这个功能代码量不大,但对降低诈骗投诉的作用立竿见影。宁可让正常用户觉得多了一条提示,也要把风险压制下去。
