简介:基于Django实现的股票交易管理系统全部资料,内含项目源码、数据库脚本、前端静态资源与详细开发文档。资源面向计算机相关专业的学生、教师及企业开发者,尤其适合用于毕业设计、课程设计、项目初期演示,也支持在现有代码基础上二次扩展。包内共有1615个文件,以JavaScript、HTML、CSS等前端文件为主,辅以Python源码、SQL数据文件、配置文件及设计素材,整体大小约21.84MB,目录结构清晰,便于按功能模块查阅和调试。这一项目已经完整测试运行,获得过较高答辩评分,目前已有39人学习下载。随包附带了日常数据入库脚本、后台管理界面、前端页面样式以及详细说明文档,能帮助使用者快速理解Django开发的整体流程,并掌握股票交易系统中数据采集、展示与管理的实现思路,是实践学习和课设参考的优质材料。
1. 行情数据落地:这个Django股票系统真正值钱的部分
拿到这套基于Django实现的股票交易管理系统时,我首先翻的并不是models.py,也不是views.py,而是压缩包根目录下那两个不起眼的批处理文件:dailytosql.bat和dbtosql.bat。做过量化数据管道的人看到这两个文件名基本就能猜出设计意图:它不是一个靠手动在后台点“导入”的玩具项目,而是一条把行情数据文件定时灌入MySQL的批处理链路。整个项目的数据流入口是文件,不是爬虫,也不是第三方行情API——这在课程设计和毕业设计里其实是个很少见但很务实的取舍,它让系统在没有外网、没有付费行情源的情况下,依然能完整跑通“数据入库→K线展示→交易记录管理”的闭环。
这套系统用Django 2.x风格写就,前端集成AdminLTE和Bootstrap,数据库用MySQL,导入层用bat脚本调Python脚本。如果你打算拿它做毕业设计或课程设计,核心的答辩点不在页面多好看,而在于你能不能把“日线数据如何批量进库、ORM如何设计才能支撑按股票代码和时间范围的高频查询、Admin后台如何定制才能让非技术人员直接操作”这三件事讲透。下面五章,我按数据进库、模型设计、Admin定制、线上部署、安全加固的顺序,逐个拆开讲。
2. 从bat到MySQL:批量导入链路的原理与改造
2.1 为什么用bat而不是Celery或APScheduler
项目里dailytosql.bat和dbtosql.bat的存在,说明作者选择了最朴素的定时方案:Windows任务计划程序定时执行bat,bat内部调用Python脚本完成数据入库。这个方案放在生产环境当然显得有些原始,但在教学项目和个人学习场景里,它有一个非常大的优势——依赖最少、链路最短、每一环都可以单独调试。
我一般会把这种文件型数据导入拆成三个环节:数据文件准备、Python解析入库、执行日志留痕。这三个环节对应到这套系统里,就是行情文件目录、dbtosql.py脚本、以及bat里重定向的输出日志。你在答辩时可以明确说:Celery适合异步任务量大、需要分布式调度的场景,而本系统数据量级在每日数千条级别,用bat触发足矣,且便于评委在Windows环境复现。
2.2 dbtosql.bat到底做了什么
打开dbtosql.bat,常见内容是设置Python环境变量、切换到项目目录、调用manage.py或独立脚本执行数据导入。改造时有一个容易被忽略的点:bat里的路径是绝对路径,换机器必须改。建议改成相对路径加cd /d %~dp0,这样脚本无论放在哪个目录都能正确定位项目根目录。
@echo off chcp 65001 >nul cd /d %~dp0 set PYTHONPATH=%~dp0 C:\Python39\python.exe %~dp0\data_import\dbtosql.py >> %~dp0\logs\import_%date:~0,4%%date:~5,2%%date:~8,2%.log 2>&1参数说明:chcp 65001解决Windows下Python输出中文乱码问题;cd /d %~dp0把当前目录切换到脚本所在目录,避免相对路径失效;>>追加写日志,2>&1把标准错误也写入同一日志文件。这样跑完任务后,打开logs目录下当天的日志文件,就能确认导入了多少条记录、有没有报错。
2.3 Python端批量写入的核心写法
dbtosql.py里数据入库建议用Django ORM的bulk_create而非逐条save()。逐条插入10000条记录需要几百次SQL往返,而bulk_create只生成一条多值INSERT,耗时能缩短一个数量级。注意MySQL对单条INSERT的包大小有限制,max_allowed_packet默认4MB,如果单日数据量过大,需要分批。
import os import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'stock_project.settings') django.setup() from stock_app.models import DailyQuote from datetime import datetime def import_csv(file_path): batch = [] batch_size = 2000 counter = 0 with open(file_path, 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split(',') # 假设CSV字段顺序:代码,日期,开盘,收盘,最高,最低,成交量 quote = DailyQuote( stock_code=parts[0], trade_date=datetime.strptime(parts[1], '%Y-%m-%d').date(), open_price=float(parts[2]), close_price=float(parts[3]), high_price=float(parts[4]), low_price=float(parts[5]), volume=int(parts[6]) ) batch.append(quote) if len(batch) >= batch_size: DailyQuote.objects.bulk_create(batch, ignore_conflicts=True) counter += len(batch) batch.clear() if batch: DailyQuote.objects.bulk_create(batch, ignore_conflicts=True) counter += len(batch) return counterignore_conflicts=True是关键参数,它让MySQL在遇到主键或唯一键冲突时跳过而非报错。配合数据库里给(stock_code, trade_date)加联合唯一索引,重复跑同一份数据文件就不会产生脏数据。项目原版可能没有这个参数,你在改造时加上,答辩时能多讲一个数据幂等性的考点。
2.4 运行任务计划程序的正确姿势
bat写好后,在Windows搜索“任务计划程序”,创建基本任务,触发器选“每天”,操作选“启动程序”,程序填bat的完整路径。此时有一个常被忽略的坑:如果不勾选“使用最高权限运行”,Python脚本访问某些受保护目录(如C:\ProgramData)时会报权限错误。另外起始于目录必须填写项目根目录,否则bat里的相对路径会失效。
3. ORM模型设计与高频查询优化:股票系统的数据骨架
3.1 模型字段怎么设计才不返工
原项目的models.py里,DailyQuote这类核心模型的字段设计通常包含股票代码、交易日期、开高低收、成交量。这个设计本身是标准的OHLCV结构,但有几个值得补充的字段:is_adjusted(是否复权)、source(数据来源)、created_at(入库时间)。这三个字段在课程设计阶段未必需要,但如果你后续要做策略回测或数据溯源,缺了它们会非常痛苦。
from django.db import models class StockInfo(models.Model): code = models.CharField('股票代码', max_length=10, unique=True) name = models.CharField('股票名称', max_length=50) industry = models.CharField('所属行业', max_length=50, blank=True) class Meta: db_table = 'stock_info' verbose_name = '股票基本信息' class DailyQuote(models.Model): stock = models.ForeignKey(StockInfo, on_delete=models.CASCADE, verbose_name='股票') trade_date = models.DateField('交易日期', db_index=True) open_price = models.DecimalField('开盘价', max_digits=10, decimal_places=2) close_price = models.DecimalField('收盘价', max_digits=10, decimal_places=2) high_price = models.DecimalField('最高价', max_digits=10, decimal_places=2) low_price = models.DecimalField('最低价', max_digits=10, decimal_places=2) volume = models.BigIntegerField('成交量') created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'daily_quote' unique_together = ('stock', 'trade_date') ordering = ['-trade_date']字段类型说明:价格用DecimalField而不是FloatField,因为浮点数在精度上存在0.1+0.2不等于0.3的问题,金融数据计算中用浮点是大忌;unique_together从数据库层面保证同一只股票同一天只有一条记录;ordering设置了默认倒序,在Django Admin和ORM查询中会默认按日期从新到旧排列,省去每次写order_by。
3.2 你能跑通但容易忽略的ForeignKey陷阱
这里最值得留意的是DailyQuote用外键关联StockInfo,而非直接存code字符串。这个设计牺牲了一点查询便利性(查行情时要select_related),但换来了数据一致性和关联操作的便利。原项目可能直接在DailyQuote里存了股票代码字符串,两者在功能上都能跑通,但面试或答辩时,你能说出这个外键设计的取舍,印象分会高不少。
实际使用中,查询某只股票最近30天的K线,正确写法是:
quotes = DailyQuote.objects.filter( stock__code='600519', trade_date__gte='2024-01-01' ).select_related('stock')stock__code是跨表查询的Django写法,翻译成SQL就是JOIN stock_info ON daily_quote.stock_id = stock_info.id WHERE stock_info.code = '600519'。这里select_related只对一对一和多对一外键生效,能一次性把关联的StockInfo查出来,避免循环查询时产生N+1问题。
3.3 查询到的对象如何反查和删除
Django反向查询遵循小写模型名_set的约定,比如拿到一个StockInfo对象后,要查它所有历史行情,用stock.dailyquote_set.all()。如果模型里定义了related_name='quotes',则写作stock.quotes.all(),语义更清晰。同理,删除某个股票的全部历史数据,可以用:
stock = StockInfo.objects.get(code='600519') deleted_count, _ = stock.quotes.all().delete()delete()返回一个元组,第一个元素是删除的总行数(包含级联删除)。Django默认的on_delete=models.CASCADE会在删除StockInfo时连带删除所有关联的DailyQuote,这个特性在管理端删除测试股票时非常省事,但要防止误删真实数据——这一点在Admin里要重写删除确认逻辑。
| 操作类型 | ORM写法 | 生成SQL特征 |
|---|---|---|
| 按日期范围查 | .filter(trade_date__range=[d1, d2]) | WHERE trade_date BETWEEN ... |
| 按代码精确查 | .get(stock__code='600519') | WHERE code = ... LIMIT 1 |
| 聚合统计 | .aggregate(Avg('close_price')) | SELECT AVG(close_price) ... |
| 分组统计 | .values('stock__code').annotate(Count('id')) | GROUP BY stock_id |
这张表里的四个写法,基本覆盖了答辩和面试里Django ORM查询的常考点。aggregate返回一个字典,annotate则给查询集增加一个聚合字段,两者作用层级不同但容易混淆,需要说清楚。
4. Admin后台定制与AdminLTE界面整合
4.1 让Admin不再是摆设:list_display配置技巧
Django Admin默认展示的是__str__方法的返回值,只显示一列,这对股票系统几乎不可用。你要让管理员在列表页直接看到代码、名称、最新价、涨跌幅,就得重写ModelAdmin。原项目里AdminLTE主要承担前端展示页面的样式,而Admin后台本身的定制能力往往没被充分利用,这里其实可以深挖。
from django.contrib import admin from .models import StockInfo, DailyQuote @admin.register(StockInfo) class StockInfoAdmin(admin.ModelAdmin): list_display = ('code', 'name', 'industry', 'get_latest_price') search_fields = ('code', 'name') list_filter = ('industry',) def get_latest_price(self, obj): latest = obj.quotes.first() return latest.close_price if latest else '--' get_latest_price.short_description = '最新收盘价' @admin.register(DailyQuote) class DailyQuoteAdmin(admin.ModelAdmin): list_display = ('stock', 'trade_date', 'close_price', 'change_percent') list_filter = ('trade_date', 'stock__industry') date_hierarchy = 'trade_date' raw_id_fields = ('stock',)get_latest_price是一个自定义方法列,它利用quotes这个反向管理器取最新一条记录的值;short_description控制列标题显示为中文;raw_id_fields在数据量超过几万条时很重要,它把外键下拉框替换成搜索框,避免渲染上千个option导致页面卡死。
4.2 一个自定义操作:把选中股票导出为CSV
Admin的actions机制可以在列表页批量执行自定义操作,这是管理端的高频需求。比如运营人员要导出选中的股票行情做离线分析,就可以写一个export_csv的action。
import csv from django.http import HttpResponse @admin.action(description='导出选中股票的行情CSV') def export_csv(modeladmin, request, queryset): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="quotes.csv"' writer = csv.writer(response) writer.writerow(['代码', '日期', '开盘', '收盘', '最高', '最低', '成交量']) for stock in queryset: for quote in stock.quotes.all()[:50]: writer.writerow([stock.code, quote.trade_date, quote.open_price, quote.close_price, quote.high_price, quote.low_price, quote.volume]) return response这里核心是返回一个HttpResponse,设置Content-Type为text/csv,再通过Content-Disposition告诉浏览器这是附件下载而不是页面跳转。@admin.action装饰器在Django 3.2以后推荐使用,旧写法是用actions = ['export_csv']列表声明并给函数加short_description属性。
界面整合方面,AdminLTE的CSS文件在项目里是静态资源,Django通过{% static %}标签引用。要记住静态文件收集命令python manage.py collectstatic,在DEBUG=False时Django不再托管静态文件,必须用Nginx或Whitenoise来处理,否则整个Admin界面会变成没有样式的一堆HTML。
5. 宝塔面板线上部署与MySQL适配
5.1 本地跑通了,线上怎么搬过去
原项目在Windows上开发调试,最终产出是zip包,但部署到服务器时常见的坑是:Windows上的Django版本、MySQL客户端版本和Linux环境不一致,导致迁移后报错。宝塔面板是现在部署Django项目最省事的方式之一,但它的Python项目管理器默认用的Python版本可能和你本地的不同,这点要格外注意——查一下版本,再装依赖,不要直接复制requirements.txt就完事。
基本步骤是:宝塔面板安装Python项目管理器,添加项目,选择Python 3.8以上版本;上传zip包并解压到站点目录;安装依赖;修改settings.py里ALLOWED_HOSTS、数据库连接、静态文件路径;最后设置Gunicorn启动命令。
pip install django gunicorn mysqlclient python manage.py makemigrations python manage.py migrate python manage.py collectstatic gunicorn stock_project.wsgi:application -b 0.0.0.0:8000 --daemon参数说明:-b 0.0.0.0:8000让Gunicorn监听所有网卡地址的8000端口;--daemon表示后台运行,否则关掉SSH窗口进程就退出了。stock_project.wsgi:application格式是项目名.wsgi:变量名,如果你zip包里的项目目录名和settings模块名不一致,这里会报ModuleNotFoundError,是部署时最常见的错误之一。
5.2 django install mysqlclient最常见的两个坑
在宝塔环境安装mysqlclient时,最常见的是报mysql_config not found或者Error: command 'gcc' failed。前者说明系统缺mysql开发库,后者说明缺编译工具链。宝塔可以从软件商店安装MySQL,然后用yum安装依赖:
yum install -y gcc python3-devel mysql-devel pip install mysqlclient如果服务器网络慢,pip可能从官方源下载超时,建议指定清华源:
pip install mysqlclient -i https://pypi.tuna.tsinghua.edu.cn/simple安装成功后,settings.py里的数据库配置要核对。宝塔的MySQL默认root密码是一串随机值,需要在数据库管理页重置,然后再填入:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'stock_db', 'USER': 'stock_user', 'PASSWORD': '你的密码', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }charset指定utf8mb4很重要。如果数据库建表时用了utf8,中文没问题,但遇到emoji或特殊生僻字会报Incorrect string value错误。utf8mb4是utf8的超集,在创建数据库时就应该指定。
5.3 外部访问被拒时怎么排查
部署完成后,浏览器访问http://服务器IP:8000如果打不开,优先检查宝塔安全组和服务器防火墙是否放行了8000端口。其次确认settings.py里ALLOWED_HOSTS = ['*']。最后看进程是否真的存活,用ps aux | grep gunicorn查看。日志方面,宝塔的Python项目管理器会实时输出应用日志,报错时直接看这里比盲改代码高效得多。
6. Django项目中的数据库连接加密与防护策略
6.1 导入导出时用transport方式隐藏敏感信息
Django项目的settings.py默认明文存放数据库密码和SECRET_KEY,这在课程设计里没人管,但如果项目要传到Git或发给别人,泄露风险就很大。常见做法是把敏感信息放到环境变量或非版本管理的.env文件中,settings.py通过os.environ.get()读取。这种方式的关键是把敏感信息移出代码库,而不只是把变量名改掉。
import os from dotenv import load_dotenv load_dotenv() SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': os.environ.get('DB_NAME'), 'USER': os.environ.get('DB_USER'), 'PASSWORD': os.environ.get('DB_PASSWORD'), 'HOST': os.environ.get('DB_HOST', '127.0.0.1'), 'PORT': os.environ.get('DB_PORT', '3306'), } }这样即使settings.py被上传到公开仓库,攻击者也只能看到环境变量名而拿不到真实值。项目原版的settings.py大概率不是这样写的,你在展示时可以强调这一点是“参考生产环境最佳实践迭代替换后的版本”。
6.2 OpenSSL自签证书与HTTPS改造
Django自身不提供HTTPS服务,线上部署时在Nginx层做SSL终止是通用方案。如果只是内网演示或毕设答辩,用OpenSSL自签证书就够,不必花钱买CA证书。宝塔面板里可以一键申请Let's Encrypt免费证书,但如果服务器没有绑定域名,也可以用OpenSSL手动生成:
openssl req -x509 -newkey rsa:2048 -keyout /etc/nginx/ssl/stock.key -out /etc/nginx/ssl/stock.crt -days 365 -nodes-nodes表示不加密私钥文件,这样Nginx启动时不需要手动输入密码;-days 365设置证书有效期。生成后在Nginx配置里加两行:
listen 443 ssl; ssl_certificate /etc/nginx/ssl/stock.crt; ssl_certificate_key /etc/nginx/ssl/stock.key;Nginx配置改完后nginx -t测试语法,再nginx -s reload生效。此时http://访问会被拒,https://则正常打开。如果你是给学生演示,这一步做完,整条链路从数据导入到安全访问就全部闭环,可以在答辩时完整演示。
6.3 除了HTTPS,还有哪些配置要一起改
HTTPS只是传输层加密,Django层面还需同步修改settings.py:SECURE_SSL_REDIRECT = True强制HTTP跳转HTTPS,SESSION_COOKIE_SECURE = True让Cookie只在HTTPS下传输,CSRF_COOKIE_SECURE = True防止CSRF Cookie被窃听。这三项不设置的话,即使Nginx配了SSL,有些敏感数据仍可能通过HTTP明文传输,存在中间人攻击风险。完成这几项配置后,用curl -I https://你的域名检查返回头里是否出现Strict-Transport-Security,没有的话再加一行add_header Strict-Transport-Security "max-age=31536000";强制浏览器使用HTTPS访问。
本文还有配套的精品资源,点击获取