
简介一份基于Django实现的股票交易管理系统完整项目源码包重点面向正在学习Python Web开发的学生以及需要完成课程设计、毕业设计的计算机专业开发者可用来深入理解Django MTV架构、ORM数据操作、用户认证与AJAX异步刷新等核心机制。压缩包共1611个文件大小约21.84MB包含31个Python源码、20个SQL数据库脚本、167个HTML模板以及879个JavaScript、162个CSS等静态资源并附有说明文档和配置文件目录结构清晰便于按模块阅读。已有187人学习下载适合作为完整项目实践参考。项目覆盖从用户注册登录、股票行情展示、交易下单到历史记录管理的业务闭环结合Bootstrap、AdminLTE等前端框架和可导入的数据库脚本读者能快速跑通整个系统并体会Django各组件之间的协作方式还可以在此基础上扩展自己的功能模块。1. 股票交易管理系统的核心不在交易接口而在数据一致性接手“基于Django实现的股票交易管理系统”这类项目很多人的第一反应是去看行情接口怎么对接、K线图怎么画但真正让系统跑起来的往往是背后那一套订单状态机、资金流水和分布式锁。股票交易不是普通的 CRUD它要求每一笔买入卖出都严格经过“冻结资金—扣减仓位—生成流水—变更状态”这条链路任何一步断了账就对不上。这篇文章就从一个可落地的 Django 实现出发带你走过建表、下单、资金结算、异步行情刷新、部署这几个必经阶段。适用人群是那些已经能写基本 Django 视图、却还没有处理过高并发资金类业务的开发者一个有五年经验的工程师在这篇文章里也能看到事务隔离级别、乐观锁和任务队列边界这些平时容易含糊的地方。2. 用 Django 定义股票与订单的数据模型先建对表才能算对账2.1 股票、账户、订单三张核心表缺一张后面都圆不回来Django 这类 MVC 框架的好处是数据库结构可以用模型直接描述迁移起来也方便。但在股票交易系统里光是模型建得“能跑”远远不够关键在于约束。一张股票表要存代码、名称、当前价、涨停跌停价账户表要存用户、总资产、可用资金、持仓市值订单表则分成委托单和成交单两类。委托单记录用户的下单意图成交单记录不可变的成交事实两者不能混在同一张表里。from django.db import models from django.contrib.auth.models import User class Stock(models.Model): code models.CharField(max_length10, uniqueTrue, db_indexTrue) name models.CharField(max_length32) current_price models.DecimalField(max_digits10, decimal_places2, default0.00) updated_at models.DateTimeField(auto_nowTrue) class Meta: db_table stock class Account(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_namestock_account) balance models.DecimalField(max_digits14, decimal_places2, default0.00) frozen_balance models.DecimalField(max_digits14, decimal_places2, default0.00) class TradeOrder(models.Model): ORDER_TYPE ((buy, 买入), (sell, 卖出)) STATUS ((pending, 待成交), (partial, 部分成交), (filled, 全部成交), (canceled, 已撤销)) user models.ForeignKey(User, on_deletemodels.PROTECT, related_nametrade_orders) stock models.ForeignKey(Stock, on_deletemodels.PROTECT, related_nametrade_orders) order_type models.CharField(max_length8, choicesORDER_TYPE) price models.DecimalField(max_digits10, decimal_places2) quantity models.IntegerField() filled_quantity models.IntegerField(default0) status models.CharField(max_length10, choicesSTATUS, defaultpending) created_at models.DateTimeField(auto_now_addTrue)这段代码里最需要留意的不是字段长短而是decimal_places2——交易系统的金钱永远不要用 FloatField 存。Python 的 float 在做减法时会出现二进制精度问题比如104.2 - 101.1可能等于3.099999999999994一次两次无所谓流水一多账就平不了。Django 在 3.0 之后的 DecimalField 底层用 Python 的decimal.Decimal配合数据库的DECIMAL类型精度是可控的。on_deletemodels.PROTECT也是有意为之用户一旦有了交易流水就不允许随便删除用户记录否则审计链会断。开发阶段你可能想用 CASCADE 图省事上线前务必统一改成 PROTECT哪怕多写几条清理逻辑。2.2 用 Django 管理命令初始化示例数据并执行查询验证模型定义完马上要做的是迁移和灌数据。建一个management/commands/seed_stocks.py文件写一个管理命令用于把沪深常见股票代码刷进去。这样做的好处是以后每次在测试环境重建库只需要一行python manage.py seed_stocks就能恢复基础数据。from django.core.management.base import BaseCommand from apps.trade.models import Stock class Command(BaseCommand): help 初始化基础股票数据 def handle(self, *args, **options): stocks [ {code: 600519, name: 贵州茅台, current_price: 1712.00}, {code: 000001, name: 平安银行, current_price: 10.12}, {code: 300750, name: 宁德时代, current_price: 181.20}, ] for item in stocks: Stock.objects.update_or_create(codeitem[code], defaultsitem) self.stdout.write(self.style.SUCCESS(股票数据初始化完成共 %s 只 % len(stocks)))执行python manage.py migrate python manage.py seed_stocks之后可以用python manage.py shell验证数据from apps.trade.models import Stock stocks Stock.objects.filter(current_price__gt100) for s in stocks: print(f{s.code} {s.name} 当前价: {s.current_price})这里有一个 Django 查询的经典陷阱DecimalField在过滤时传入字符串100是安全的但如果你传入 float100.0在 MySQL 5.7 的某些驱动下可能触发类型转换告警。所有与金钱相关的查询条件统一使用字符串或者Decimal(100.00)是最省心的做法。提示不要把股票涨跌停价格存在前端算数据库模型里直接加limit_up和limit_down两个字段后端校验下单价格时用数据库里的值避免因为前端传来的贴水价格导致错单。3. 交易撮合与资金结算Django 事务和锁是最后一道防线3.1 买入与卖出视图完整的链路只能这样走股票交易系统的下单接口不是简单地往TradeOrder表里插一条记录。在真实行情里买入时要先检查账户可用资金是否足够冻结资金后写入待成交订单卖出时要检查持仓是否够冻结持仓数量然后生成委托单。如果直接在视图函数里分部执行这些操作任何一个环节崩溃都会留下脏数据。所以我会把下单选在 Django 的transaction.atomic()块里并配合select_for_update()行锁来隔离并发请求。下面这段是买入委托的核心逻辑from django.db import transaction from decimal import Decimal from rest_framework.views import APIView from rest_framework.response import Response from apps.trade.models import Account, TradeOrder, Stock class BuyOrderView(APIView): def post(self, request): stock_code request.data.get(code) price Decimal(request.data.get(price)) quantity int(request.data.get(quantity)) amount price * quantity # 冻结金额 with transaction.atomic(): stock Stock.objects.select_for_update().get(codestock_code) account Account.objects.select_for_update().get(userrequest.user) if account.balance amount: return Response({code: 400, msg: 可用资金不足}, status400) account.balance - amount account.frozen_balance amount account.save() order TradeOrder.objects.create( userrequest.user, stockstock, order_typebuy, priceprice, quantityquantity, statuspending ) return Response({code: 0, order_id: order.id})这段代码要拆成三层来理解。第一层是transaction.atomic()它把资金扣减和订单创建包在同一个数据库事务里要么全部成功要么全部回滚。第二层是select_for_update()在 MySQL InnoDB 引擎下这是行级排他锁两个用户同时下单时后面的请求会等到前面的提交或回滚后才继续执行。第三层是业务校验必须先锁住账户再判断余额否则两个并发的买入请求可能同时读到同样的余额同时判断“余额够”最后造成超买。卖出逻辑对称区别是校验的是持仓数量。持仓不要单独建表每次从成交记录里动态聚合SHARES_HELD SUM(filled_quantity) WHERE order_typebuy - SUM(filled_quantity) WHERE order_typesell。虽然这样每次都要全表聚合但换来的是资金流水不可篡改审计只需要查流水不需要信任任何缓存字段。3.2 Django admin 一键管理订单状态运营必备的后台任何系统都少不了一个给运营或管理员用的后台。Django admin 自带的功能只要稍加定制就能管理订单状态。在admin.py里注册模型并加一个自定义的“手动成交”动作。from django.contrib import admin from apps.trade.models import Stock, TradeOrder, Account admin.register(TradeOrder) class TradeOrderAdmin(admin.ModelAdmin): list_display (id, user, stock, order_type, price, quantity, status, created_at) list_filter (order_type, status) search_fields (stock__code, user__username) actions [mark_as_filled] admin.action(description标记所选订单为已成交) def mark_as_filled(self, request, queryset): for order in queryset: order.filled_quantity order.quantity order.status filled order.save() self.message_user(request, f{queryset.count()} 条订单已标记成交)list_filter和search_fields是检索效率的关键股票代码和用户名这两个是最常用的筛选入口。这里标记成交只是一个后台兜底操作真正的自动撮合应该放在异步任务里后面会展开。这个后台还能继续美化比如show_change_link、只读字段、按日期层级筛选但这些都是锦上添花。对交易系统来说核心是把“谁在什么时候改了什么状态”留痕所以订单表里一定还要有updated_by和updated_at两个自动维护字段。3.3 前端页面的动态展示用 Django 模板还是 Vue如果你是从零开始做一个技术验证型系统Django 模板足够。用模板渲染一个实时刷新的行情页面只需要一个视图函数返回render(request, stock_list.html, {stocks: stocks})模板里用 JavaScript 的setInterval每 3 秒拉一次行情接口。这比直接上 Vue Axios 维护成本低得多。table classtable table-hover thead trth代码/thth名称/thth现价/thth操作/th/tr /thead tbody {% for stock in stocks %} tr td{{ stock.code }}/td td{{ stock.name }}/td td idprice_{{ stock.id }}{{ stock.current_price }}/td tdbutton classbtn btn-sm btn-primary onclickbuy({{ stock.id }})买入/button/td /tr {% endfor %} /tbody /table script function refreshPrices() { fetch(/api/prices/) .then(res res.json()) .then(data { for (const s of data) { document.getElementById(price_ s.id).innerText s.price; } }); } setInterval(refreshPrices, 3000); /script这是典型的服务端渲染加轻量前端刷新方案对一台普通 4C8G 服务器来说支撑几百个并发绰绰有余。如果你确实要在这个系统上叠加自选股、K线图、交易信号提示这些复杂交互那再考虑上 Vue 和 DRF 的前后端分离架构否则不要轻易给系统增加复杂度。4. 异步行情刷新、查询优化与宝塔部署Django 项目的实战调优4.1 用 Django-Q 定时拉取行情不要用 crontab 更不要用 threading行情数据源的刷新很多人的第一反应是写一个 while 循环丢到后台线程里或者干脆用 Linux 的 crontab 每分钟跑一次python manage.py fetch_quote。这两个方案都有问题线程方式在 Django 部署到 uwsgi 后会被回收而且线程崩溃了没有重试机制crontab 方式每次都要重新初始化 Django 环境启动时间约 0.5 秒频繁执行浪费厉害。更合理的方式是使用 Django-Q 这样的任务队列。把定时任务注册在 Django-Q 里它运行在一个常驻 worker 进程中支持失败重试、参数传递和队列优先级。# apps/trade/tasks.py import requests from decimal import Decimal from django.db import transaction from apps.trade.models import Stock def fetch_stock_quotes(): # 假设这里请求第三方行情 HTTP 接口 resp requests.get(https://your-data-source/api/quotes, timeout5) quotes resp.json() with transaction.atomic(): for q in quotes: Stock.objects.filter(codeq[code]).update( current_priceDecimal(str(q[price])) ) print(f{len(quotes)} 条行情已更新)在settings.py中配置 Django-Q 的调度Q_CLUSTER { name: stock_trade_cluster, workers: 4, timeout: 15, retry: 3, schedule: [ { func: apps.trade.tasks.fetch_stock_quotes, schedule_type: I, # Interval seconds: 3, }, ], }然后启动 workerpython manage.py qcluster。这样行情每 3 秒刷新一次失败自动重试 3 次比 crontab 轻量也比自己写线程可靠。生产环境建议将 qcluster 放进 process manager如 django 自带的 runserver 加 supervisor配合 Nginx 反向代理对外提供服务。提示这里说的行情源是模拟示例真实部署时你需要找合规的行情数据供应商。在测试环境里可以用随机数模拟行情变化重点是验证任务队列本身的工作流程。4.2 select_related 和 prefetch_related 在订单查询优化中的用法交易系统的列表页通常会展示用户的委托单附带股票名称和用户姓名。如果直接TradeOrder.objects.all()在模板中访问order.stock.name会触发 N1 次查询——100 条订单就是 1 条主查询加 100 条股票查询。Django 的select_related()解决的就是这个问题它通过 SQL JOIN 把关联对象一次性取出来。orders TradeOrder.objects.select_related(stock, user).filter(userrequest.user)[:50]select_related适用于 ForeignKey 和 OneToOneField它生成的是INNER JOIN如果是 ManyToManyField 就要用prefetch_related后者是额外查询一次然后用 Python 组装。这里还有一个细节分页时不要SELECT *用.only(id, stock__code, price, status)限定字段能显著减少 MySQL 网络传输量特别是订单表字段很多的时候。再进一步当你只需要知道某只股票的累计成交额时可以直接用聚合查询from django.db.models import Sum, F, DecimalField from decimal import Decimal result TradeOrder.objects.filter( stock__code600519, order_typebuy, statusfilled ).aggregate( total_amountSum(F(price) * F(quantity), output_fieldDecimalField()) ) print(result[total_amount] or Decimal(0.00))4.3 本地开发与上线环境Django 项目的部署闭环本地运行这套系统的要求很低Python 3.10、Django 4.2、MySQL 8.0 或 SQLite 均可。跑通的最小命令集合如下python -m venv venv source venv/bin/activate pip install django4.2.* django-rest-framework django-q requests mysqlclient django-admin startproject stock_project . python manage.py startapp apps.trade python manage.py makemigrations python manage.py migrate python manage.py seed_stocks python manage.py runserver 0.0.0.0:8000注意startapp apps.trade前需要把apps改成 Python package新建__init__.py并在settings.py里把INSTALLED_APPS注册为apps.trade这样项目结构才清晰。上线部署的经典路径是宝塔面板 Gunicorn Nginx或者纯命令行 Docker 化。宝塔面板最省事的操作步骤是在网站菜单里添加 Python 项目选择 Gunicorn 启动器绑定端口为 8000然后设置一个 Nginx 反向代理把 80 端口转发到 8000静态文件交给 Nginx 托管。关键在于settings.py里必须配置STATIC_ROOT然后执行python manage.py collectstatic否则所有 CSS/JS 都会 404。以下是一个标准的 Gunicorn 启动配置供参考gunicorn stock_project.wsgi:application -w 4 -b 127.0.0.1:8000 --max-requests1000 --timeout30-w 4代表 4 个 worker 进程IO 密集型场景使用-k gevent可以配合协程提升并发能力但如果在事务里大量使用行锁多 worker 反而会增加锁等待概率。交易系统建议先老实使用 sync worker等压测瓶颈明显时再做优化。5. 压测与验收Django 交易系统的并发写入演练最后一节落到一个平时不太会注意却总在关键时刻掉链子的点用 Django shell 模拟并发成交后的数据完整性校验。写一个脚本模拟 20 个用户同时买入同一只股票每个用户资金 10 万元股票价格 10 元每人买 5000 股。理论上 20 个并发请求全部成功每个人冻结 5 万元账户余额每人剩 5 万元总冻结资金为 100 万元。from concurrent.futures import ThreadPoolExecutor from django.test import Client client Client() payloads [{code: 000001, price: 10.00, quantity: 5000} for _ in range(20)] def buy(user_id): client.force_login(user_iduser_id) return client.post(/api/buy/, datapayloads[0], content_typeapplication/json) with ThreadPoolExecutor(max_workers20) as executor: results list(executor.map(buy, range(1, 21))) success [r for r in results if r.status_code 200] print(f成功 {len(success)} 笔)跑完脚本后用下面这条聚合查询核对资金的守恒from django.db.models import Sum from apps.trade.models import Account, TradeOrder total_balance Account.objects.aggregate(sSum(balance))[s] total_frozen Account.objects.aggregate(sSum(frozen_balance))[s] total_amount sum(o.price * o.quantity for o in TradeOrder.objects.filter(statuspending)) print(f可用余额总额: {total_balance}) print(f冻结总额: {total_frozen}) print(f带动订单总额: {total_amount})如果total_frozen total_amount且每个用户的余额正确说明事务边界和锁策略是可靠的。通常情况下你会在这里发现一个隐蔽问题如果select_for_update忘了加在 Account 上并发时会报死锁或者余额超扣这时候的排查方向不是代码逻辑而是看 MySQL 的事务隔离级别默认REPEATABLE READ和锁粒度。最后要提一个容易忽略的细节select_for_update()不会锁不存在的行。如果用户首次买入时Account记录还没创建比如用户注册后才开通账户两个并发请求同时执行get_or_create会导致一个请求拿到DoesNotExist或重复插入。正确做法是在用户开户时就创建 Account 记录或者把select_for_update()放在get_or_create的get分支上确保行一定存在后再加锁。这个问题在低并发环境测不出一上流量就会暴露。本文还有配套的精品资源点击获取