1. 为什么要做积分制零食自选平台:需求拆解与业务闭环
1.1 积分不是套路,而是一套激励反馈机制
积分制零食自选平台这个题目,我第一次看到的时候还在想:这不就是一个限制版的商城吗?把零食挂上去,用户下单,扣积分,完事。真正自己用Python和Vue把它从零做出来,才发现里面还藏着订单状态流转、积分流水对账、并发扣减、前后端联调一堆破事。
先说需求。积分制之所以能和零食自选结合,是因为很多场景下你不想直接发现金或实物,而是希望用积分形成一种“多劳多得、兑换自主”的反馈循环。比如公司内部福利平台、员工活动积分商城、高校社团的打卡激励系统、企业内部培训的课后奖励,甚至是一些私域社群运营的工具。用户通过签到、参与活动、完成任务赚积分,然后拿着积分去兑换想吃的东西。零食这个品类天然适合做成自选,因为单价低、种类多、用户偏好差异大,你发一箱统一口味的饼干,总有人不喜欢,但让他自己挑三样,满意度完全不一样。
这个项目在实操中一般会被定位成一个展示“如何设计一套积分账户、如何把订单和积分流水串联起来”的完整案例。适合的人群也很明确:正在准备毕业设计的学生、想练手全栈项目的Python初学者、或者公司内部需要快速搭一个简单积分福利系统的人。整个系统的核心不是零食,而是“积分怎么挣、怎么查、怎么扣、怎么退”。
1.2 从“自选”到“自动扣积分”的业务闭环
动手之前,先把业务流画出来。用户进入系统后,先看到的是一个零食货架,零食按类型分类,比如饼干、辣条、坚果、糖果、饮料。每件零食都标着一个“积分价”,价格不是人民币,而是积分数字。用户把自己想吃的几样加入自选购物车,购物车会实时汇总需要消耗多少积分。确认结算时,系统检查用户账户里的可用积分是否足够。如果够,就在同一个数据库事务里完成三件事:扣掉用户积分、扣减对应零食库存、生成一笔待核销的订单。积分流水里会新增一条“支出”记录,包含了这笔订单的编号和消耗积分。
后面还有核销环节。用户拿着订单里的核销码去柜台或自助取货点领取零食,管理员在小程序或后台点一下“确认领取”,订单状态从“待领取”变成“已完成”。如果用户下单后反悔,可以在规定时间内取消订单,之前扣掉的积分原路退回,库存加回去。如果超过规定时间还没领取,系统可以自动取消订单并退回积分。整个闭环讲清楚之后,项目功能边界就非常明确。
1.3 项目边界:先做MVP,别一上来就铺太大
我接手这个项目的时候,第一版只做了下面这些功能,其他全砍掉:
| 模块 | 功能点 |
|---|---|
| 用户端 | 注册登录、查看积分余额、浏览零食、加入自选购物车、积分支付下单、查看订单状态、取消订单 |
| 管理端 | 零食商品上下架、库存调整、订单核销、查看积分流水 |
| 积分机制 | 初始赠送积分、下单消耗积分、取消订单退回积分、手动调整积分并留痕 |
没有做支付、物流、优惠券、拼团,也没做复杂的会员等级。为什么?因为积分平台的核心技术难点本来就不在商城的商品展示,而在“账户余额与流水的一致性”和“多端联调时的数据校验”。先把这两个东西做扎实,后面加任何功能都是往上叠模块的事情。如果第一版就把所有商业功能都塞进来,反而会让数据库设计变得混乱,尤其是积分流水和订单流水混在一起,后期根本没法对账。
这套MVP做完,大概一个团队两到三周就能稳定跑通。下面我按实际开发顺序,把数据库、后端、前端、调试部署分别拆开讲。
2. 数据库模型设计:四张核心表和两个容易做错的细节
2.1 核心表字段规划:用户、零食、订单、积分流水
数据库是这类项目的命根子。表设计得不好,后面写接口的时候你会被不断返工折磨。我用Django的Model来写,因为我这次最终选型是Django + Django REST Framework,原因后面会讲。你如果用Flask,只要换成 SQLAlchemy 的 Model,结构是一致的。
先看用户侧。Django自带的User表可以用,但我习惯单独建一张用户扩展表,把积分账户相关的字段和登录字段拆开,避免动到Django本身的源码逻辑:
# users/models.py from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") points_balance = models.IntegerField(default=0, verbose_name="当前积分余额") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.user.username} - {self.points_balance}"零食商品表是自选货架的数据来源,字段不需要很花哨,但一定要包含库存和上下架状态:
# snacks/models.py class Snack(models.Model): name = models.CharField(max_length=100, verbose_name="零食名称") category = models.CharField(max_length=50, verbose_name="分类", db_index=True) points_price = models.IntegerField(verbose_name="积分价格") stock = models.PositiveIntegerField(default=0, verbose_name="库存") image = models.URLField(blank=True, verbose_name="展示图片") is_active = models.BooleanField(default=True, verbose_name="是否上架") created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at"]订单表和订单明细表要分开。虽然这里的业务没有多规格,但依然建议保留OrderItem,因为后续复用性高。订单表里需要记录“本次消耗总积分”,订单明细里记录每一件的积分单价和数量:
# orders/models.py class Order(models.Model): STATUS_CHOICES = ( ("pending", "待领取"), ("finished", "已完成"), ("cancelled", "已取消"), ("expired", "已过期"), ) user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="orders") total_points = models.IntegerField(verbose_name="消耗积分") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending", db_index=True) expire_at = models.DateTimeField(verbose_name="领取截止时间") created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items") snack = models.ForeignKey("snacks.Snack", on_delete=models.PROTECT) snack_name = models.CharField(max_length=100, verbose_name="下单时零食名称") points_price = models.IntegerField(verbose_name="下单时积分单价") quantity = models.PositiveIntegerField(default=1, verbose_name="数量")最后是积分流水表。这张表是整个系统的对账基础,每一条积分变动都要记录来源、去向和关联单号:
# points/models.py class PointsLedger(models.Model): TYPE_CHOICES = ( ("signup", "注册赠送"), ("consume", "下单消耗"), ("refund", "取消退回"), ("adjust", "后台调整"), ) user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="points_ledger") change_type = models.CharField(max_length=20, choices=TYPE_CHOICES) amount = models.IntegerField(verbose_name="变动数量,正为增加,负为减少") balance_after = models.IntegerField(verbose_name="变动后余额") order = models.ForeignKey( "orders.Order", null=True, blank=True, on_delete=models.SET_NULL, verbose_name="关联订单" ) remark = models.CharField(max_length=200, blank=True) created_at = models.DateTimeField(auto_now_add=True)2.2 为什么积分余额不能只存在用户表里
很多人第一版只会在Profile里放一个points_balance,下单时直接UPDATE ... SET points_balance = points_balance - 100。看起来没问题,但用户一旦问你:“我这个月的积分怎么少了200?什么时候少的?”你就傻眼了。没有流水,你只能从订单表反查,而且碰上后台手动调整、注册赠送这种非订单行为,完全无法解释。
所以「余额」和「流水」必须同时存在。points_balance是冗余字段,负责快速展示和校验;PointsLedger是事实记录,负责追溯每一笔变动。每做一次积分变动,都必须在同一个事务里同时更新余额和插入流水。流水表里记录balance_after,好处是任何时刻把某用户流水按时间排序,都能重新推出余额变化轨迹,出了问题可以直接对账。
2.3 订单状态与积分回滚的关联设计
有了订单状态之后,积分回滚逻辑要用状态机去约束。我要强调一个设计原则:只有“待领取”的订单才能取消。已完成订单如果想售后,绝对不能通过简单的取消操作回滚积分,否则会出现“订单已完成但商品没退回、积分却回来了”的漏洞。
实际操作中我加了两个防线:
- 取消订单的接口,先执行
select_for_update()锁住订单行,再判断状态是否为pending,不是就直接拒绝。 - 取消成功后,更新订单状态,同时创建一条
refund类型的积分流水,把积分配额加回去。
过期自动取消就更简单了,写一个 Django management command,定时扫描status="pending"且expire_at < now()的订单,执行和手动取消相同的回滚逻辑。后面可以挂计划任务,也可以只在管理员登录后台时触发扫描,演示项目用后者更省事。
3. 后端API实现:Django环境下的认证、下单与积分原子操作
3.1 环境搭建与依赖选型:Django还是Flask
标题里写了 Django 和 Flask 两个词,很多人会纠结。我最后选了 Django + DRF,核心原因有三点:
- Django 自带 Admin 后台,做零食管理、订单核销这类需要后台维护的页面,几乎不用写代码。
- 自带 ORM 和数据库迁移,事务处理能力直接可用,省去 Flask 里自己拼 SQLAlchemy 会话的麻烦。
- DRF 的序列化器和视图集对这类 CRUD 接口覆盖非常完整。
Flask 当然也能做,轻量、灵活、代码量少,适合非常熟悉 Flask 的人,但如果同时要做管理后台和接口文档,工程量会多出不少。
依赖清单很简单:
Django>=4.2 djangorestframework djangorestframework-simplejwt django-cors-headers项目初始化:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt django-admin startproject snack_platform python manage.py startapp users python manage.py startapp snacks python manage.py startapp orders python manage.py startapp points3.2 用户注册登录与积分账户的自动创建
认证我用的 JWT,接口状态保持无状态,前端 Vue 拿到 token 后存到本地,之后每次请求都在Authorization头里带上。注册接口的逻辑:
# users/serializers.py from django.contrib.auth.models import User from rest_framework import serializers from .models import Profile class RegisterSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, min_length=6) class Meta: model = User fields = ["username", "password", "email"] def create(self, validated_data): user = User.objects.create_user(**validated_data) Profile.objects.create(user=user, points_balance=100) PointsLedger.objects.create( user=user, change_type="signup", amount=100, balance_after=100, remark="新用户注册赠送100积分" ) return user这里要注意,数据库中balance_after不能直接写成100,而应该先取到初始余额,再计算。我刚才为了演示简化了,但实际项目中建议封装一个PointsService统一处理变动逻辑,避免每个视图里都重复写流水。
3.3 用数据库事务和行锁保证“扣积分-减库存-下单”的原子性
这个项目最容易踩的坑就是并发扣积分。我举个很实际的例子:一个用户账户余额是 50 积分,他开了两个页面,同时下单两件各 30 积分的零食。如果不加控制,两个请求同时读到余额是 50,各自都认为余额够,最后余额被扣成 -10,系统崩了。
解决办法就是用 Django 的事务原子块 加 行级锁。下单接口的核心逻辑如下:
from django.db import transaction from django.db.models import F from rest_framework.views import APIView from rest_framework.response import Response class CreateOrderView(APIView): def post(self, request): user = request.user items = request.data.get("items", []) # [{"snack_id": 1, "quantity": 2}, ...] with transaction.atomic(): profile = Profile.objects.select_for_update().get(user=user) total_points = 0 stock_objects = [] for item in items: snack = Snack.objects.select_for_update().get(id=item["snack_id"]) if snack.stock < item["quantity"]: return Response({"error": "库存不足"}, status=400) total_points += snack.points_price * item["quantity"] stock_objects.append((snack, item["quantity"])) if profile.points_balance < total_points: return Response({"error": "积分不足"}, status=400) order = Order.objects.create( user=user, total_points=total_points, status="pending", expire_at=timezone.now() + timezone.timedelta(hours=24) ) for snack, quantity in stock_objects: OrderItem.objects.create( order=order, snack=snack, snack_name=snack.name, points_price=snack.points_price, quantity=quantity ) snack.stock = F("stock") - quantity snack.save(update_fields=["stock"]) profile.points_balance = F("points_balance") - total_points profile.save(update_fields=["points_balance"]) ledger = PointsLedger.objects.create( user=user, change_type="consume", amount=-total_points, balance_after=profile.points_balance, order=order, remark="自选零食下单" ) return Response({"order_id": order.id, "total_points": total_points})这里最关键的三个细节:
select_for_update()锁住了用户积分行,第二个并发的下单请求会阻塞,等第一个事务提交后再读取新的余额。- 减库存时用了
F("stock") - quantity,这种写法是数据库侧原子更新,不会出现读旧值覆盖新值的问题。 PointsLedger.objects.create()里的balance_after,理论上应该取F表达式更新后的最终值,但 Django 的F表达式更新后,内存中的profile.points_balance属性不一定自动刷新。实际代码里我会在事务内重新查一次,或者用Profile.objects.get(pk=profile.pk).points_balance。
3.4 积分明细与订单列表接口
积分明细接口是用户端最常查的接口。我用 DRF 的ListAPIView配合分页:
# points/views.py from rest_framework.generics import ListAPIView from rest_framework.pagination import PageNumberPagination class PointsLedgerListView(ListAPIView): serializer_class = PointsLedgerSerializer pagination_class = PageNumberPagination def get_queryset(self): return PointsLedger.objects.filter(user=self.request.user).select_related("order")可以加一个change_type查询参数做筛选,方便前端做“只看下单消耗”“只看退回”的筛选。返回字段注意不要把用户密码等敏感信息带出去,这个谁踩谁知道。
4. 前端Vue实现:从商品列表到自选结算
4.1 Vite + Vue3 + Pinia:为什么不用 Vue CLI
前端我用的 Vue 3 + Vite + Pinia + Element Plus。现在新项目基本都推荐 Vite,启动快、依赖安装简单,开发体验比 Vue CLI 好得多。创建项目:
npm create vite@latest snack-frontend -- --template vue cd snack-frontend npm install vue-router pinia axios element-plus项目结构里我习惯把api/、stores/、views/拆开:
src/ api/ # axios 封装和接口方法 stores/ # Pinia 购物车、用户状态 views/ # 页面组件 router/ # 路由配置Axios 封装要注意带上 JWT,并且统一处理 401 跳转登录:
// src/api/request.js import axios from 'axios' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } ) export default request4.2 自选购物车的状态管理:为什么必须用 Pinia
自选购物车是一个典型的跨页面状态。如果只用组件内ref,你从商品页加入购物车,跳转到购物车页,状态就丢了。Pinia 的 Store 就是干这个的。
// stores/cart.js import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', { state: () => ({ items: [] // [{ snack_id, name, points_price, quantity }] }), getters: { totalPoints: (state) => state.items.reduce((sum, item) => sum + item.points_price * item.quantity, 0), totalCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0) }, actions: { addSnack(snack) { const found = this.items.find(item => item.snack_id === snack.id) if (found) { found.quantity += 1 } else { this.items.push({ snack_id: snack.id, name: snack.name, points_price: snack.points_price, quantity: 1 }) } }, removeItem(snack_id) { this.items = this.items.filter(item => item.snack_id !== snack_id) }, clear() { this.items = [] } } })购物车 Store 里我故意没有存库存字段,因为库存是动态的,前端存一份副本没有意义,结算时以后端校验为准。这里要提醒一点:前端购物车只是用户体验层,永远不能作为最终数据依据。用户改一下浏览器内存里的数据很容易,后端必须重新计算积分价和库存。
4.3 结算时的积分校验与用户体验
积分余额是从用户信息接口拉取的,购物车的总积分是一个实时计算值。结算页面上展示这两个数字,并给出一个很明确的判断:如果totalPoints > pointsBalance,按钮置灰,文案显示“积分不足,还差 xx 分”。这一步纯粹是为了减少无效请求,真正的校验还是后端事务里做。
下单接口请求时要注意防重复提交。用户点击“确认兑换”后,按钮立刻进入 loading 状态,接口没返回前不能再点。但前端 loading 并不是绝对可靠的,用户可以通过抓包工具重复发送请求,所以后端接口必须做幂等控制。一个比较轻量的做法是前端生成一个唯一请求号request_id,后端在订单表里加一个唯一约束,同一个request_id只能创建一次订单。
Vue 页面上的逻辑大致是这样:
<template> <div> <el-card> <div>当前积分:{{ userStore.pointsBalance }}</div> <div>本次消耗:{{ cartStore.totalPoints }}</div> <el-button type="primary" :disabled="cartStore.totalPoints > userStore.pointsBalance || cartStore.totalCount === 0" :loading="submitting" @click="submitOrder" > 确认兑换 </el-button> </el-card> </div> </template> <script setup> import { useUserStore } from '../stores/user' import { useCartStore } from '../stores/cart' import { createOrder } from '../api/order' import { ElMessage } from 'element-plus' const userStore = useUserStore() const cartStore = useCartStore() const submitting = ref(false) async function submitOrder() { if (submitting.value) return submitting.value = true try { const data = await createOrder({ items: cartStore.items, request_id: crypto.randomUUID() }) ElMessage.success('兑换成功') cartStore.clear() userStore.refreshProfile() } catch (e) { ElMessage.error(e.response?.data?.error || '兑换失败') } finally { submitting.value = false } } </script>4.4 管理端:用 Element Plus 快速搭一个零食管理页
管理端我没有用单独的前端工程,直接在同一个 Vue 项目里加了一个/admin路由,用菜单权限隐藏入口。页面逻辑很简单:el-table展示零食列表,每一行有“上架/下架”开关,数量可以直接在表格里改动并保存。订单核销页面就是用el-table加一个“确认领取”按钮,调一下后端接口。
这类管理页在毕设答辩里很占优势,因为你能现场演示商品的上下架、订单状态的流转。但千万别把时间花在搞炫酷的图表上,评委更关心的是数据是不是真的从后端读出来的,新增一条零食后前端列表有没有刷新。
5. PyCharm中的联调与踩坑实录
5.1 环境配置:虚拟环境与解释器的那些细节
这个项目我全程用 PyCharm 开发,后端和前端分开两个窗口。PyCharm 里新建 Django 项目后,第一件事是把虚拟环境解释器指对:
- 打开 Settings -> Project -> Python Interpreter。
- 选择
venv/bin/python(Windows 下是venv\Scripts\python.exe)。 - 确认终端工具也激活同一个虚拟环境,避免命令行用了全局 Python 导致依赖找不到。
还有一个很容易忽略的坑:PyCharm 默认可能会把源码根目录标成Sources Root,这对 Django 来说是好事,但如果你有users、orders等多个 app,一定要确认 app 文件夹在同一个Sources Root下,否则 import 会报错。报错信息是那种“ModuleNotFoundError: No module named 'users'”,排查半天发现其实只是目录标记问题。
5.2 跨域与本地代理:别让 CORS 恶心你一整天
开发时前端在 5173 端口,后端在 8000 端口,必然有跨域。我有两种处理方式,推荐第二种:
方案一:后端装django-cors-headers,允许所有来源。适合快速验证,但生产环境安全性差。
方案二:前端 Vite 配置代理,所有/api请求转发到后端:
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } }这样前端 axios 的baseURL写成/api,浏览器看到的请求是同源的,不会触发跨域。生产部署时把 Vue 打包后的文件放到 Django 的静态目录里,也不存在跨域问题。我之前一开始偷懒用的方案一,结果演示时切到局域网访问,被浏览器拦了好几次,后来全部改成了代理和同源部署。
5.3 并发扣积分导致负余额:一次真实复现与修复
这个 bug 值得单独写一段。我的第一版下单接口没有加任何锁,只用了transaction.atomic,然后我在两个浏览器标签页同时登录同一个测试账号,对同一件库存只有 1 的零食下单。结果商品库存变成了 -1,两个订单都成功了,积分也扣成了负数。
排查过程:
- 先看数据库里两个订单的创建时间,只差了 0.2 秒。
- 再看库存字段,等于 -1,说明两个请求都通过了
if snack.stock < quantity的检查。 - 最后定位到问题:
transaction.atomic只能保证一组数据库操作要么全部提交、要么全部回滚,它压根不管并发下旧值覆盖的问题。两个事务在同一个时刻读到的库存都是 1,每个都认为库存够,然后各自save(),第二次保存就把库存覆盖成了 -1。
修复方法就是我在 3.3 里写的select_for_update()。锁住商品行以后,第二个事务必须等第一个事务提交完毕才能读取,自然就会发现库存已经变成 0。同理,锁住用户 Profile 行,第二个请求进来时发现余额不够,直接返回“积分不足”。
这个坑让我明白一件事:单事务解决的是原子性,行锁解决的才是并发一致性。以后只要涉及“余额、库存、领取名额”这类共享资源的扣减,第一反应都应该是select_for_update()或数据库原子更新函数,而不是在应用层判断。
5.4 PyCharm 断点调试接口:比 print 高效得多
联调阶段我用 PyCharm 的 Debug 模式替代了各种 print。操作方法很简单:在某一个 APIView 的post方法第一行打一个红点,然后用 Postman 发送请求,PyCharm 会在断点处暂停,左侧能看到request.data、user、profile这些变量的实时值。
有一次前端一直报“兑换失败”,后端日志也没报 500,我把断点打在下单接口里,才发现前端传进来的items并不是列表,而是 JSON 字符串。原来 axios 里传参时忘了JSON.stringify,后端解析直接拿到了字符串。这种问题不打断点很难查,但看一眼变量值就明白了。调试接口的黄金组合就是:PyCharm 断点 + Postman 自定义请求 + 数据库客户端看结果,三步走能解决大部分联调问题。
6. 从开发到演示:部署、数据与复盘
6.1 最简演示部署方案
一个毕业设计或者内部 Demo,不需要搞 Kubernetes,也不需要上云,最重要的指标是“换一台干净电脑也能快速跑起来”。我的推荐方案是:
- 后端继续用 SQLite,数据库文件跟着项目走,不需要单独装 MySQL。
- 前端打包成静态文件,放到 Django 的
static目录,或者用 Vue 的outDir直接输出到一个固定目录,然后 Django 加一个兜底 view 返回index.html。 - 启动命令一行:
python manage.py runserver 0.0.0.0:8000,把端口在防火墙上放行,局域网里的其他电脑就能直接访问。
Django 里最简单的方式是在urls.py里加一个兜底:
from django.contrib.staticfiles import views as static_views from django.urls import re_path urlpatterns += [ re_path(r"^$", static_views.serve, kwargs={"path": "index.html"}), ]当然这是简化做法,只为了演示。生产环境还是建议 Nginx + Gunicorn + PostgreSQL,但那不是这个项目的重点。
6.2 构造演示数据:让评审十秒进入状态
我吃过一次亏,演示时数据库里只有管理员创建的三条零食,用户登录后还得自己反复注册两个号才能看到购物车联动,非常尴尬。后来我写了一个 Django management command,一键生成完整演示环境。
python manage.py seed_demo这个命令会创建:
- 一个管理员账号和两个普通用户账号;
- 每个用户初始 500 积分;
- 十几种零食,分三个分类,库存从 20 到 100 不等;
- 其中一个用户已经有一笔待领取订单和对应的积分支出流水。
演示的时候,我只需要登录普通用户,打开零食货架,加三样到购物车,结算,然后切到管理后台核销。整个过程一分钟不到,但把核心业务闭环全部展示出来了。
6.3 项目复盘:哪些设计可以复用到其他系统
这个项目做完后,我复盘出三个可以迁移到任何积分类系统的通用设计:
第一,积分流水表是积分系统的灵魂。无论业务怎么变,只要和积分有关,就必须有流水。余额可以不算,流水不能丢。
第二,涉及余额扣减的核心接口,要同时考虑事务、行锁和幂等。事务保证原子性,行锁保证并发正确,幂等保证重复请求不会造成多次扣减。这三件事缺一不可。
第三,前端和后端的数据校验永远不一致。前端校验是为了减少无效请求和提升体验,后端校验才是真正的安全边界。任何时候都不能写“前端已经判断过了,后端就不管了”这种代码。
个人来说,这个项目最让我收益的不是把它做出来了,而是中间踩过的并发那个坑。从那以后,我再写任何购物、下单、库存类接口,都会下意识地想一遍:如果两个请求同时进来会发生什么?这个问题救过我后来很多次。如果你也要做类似项目,我建议不要急着跑通页面,先把数据库表设计、事务边界、并发策略这三件事想清楚,后面大概率会很顺。