ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Django+Vue2股票数据可视化:难点不在画图在数据管道

2026/9/27 23:04:38 拓冰建站 浏览量
Django+Vue2股票数据可视化:难点不在画图在数据管道 简介这是一套基于Django后端与Vue2前端构建的股票数据可视化与新闻实时更新Web应用系统完整源码适合想通过综合项目学习前后端分离、数据采集与可视化展示的开发者。项目中股票市场新闻抓取、交互式搜索、多维度数据展示及用户个人中心等模块均已编码实现能帮助读者理清单页应用与后端接口协作的全流程。资源共243个文件压缩包约20.42MB文件以css、svg、vue、js及woff/ttf字体资源为主包含完整前端工程、组件逻辑与样式文件便于按模块对照学习界面布局与交互实现。已有57人学习下载适合用于毕业设计参考、全栈实践或作为DjangoVue项目脚手架直接复用。1. 基于Django与Vue2的股票数据可视化Web应用为什么难点全在数据管道做股票数据可视化系统新手最容易误判的是“难点在画图”。实际上选好 ECharts 只是第一步真正劝退人的是这条链路新闻怎么抓、行情数据怎么清洗入库、Django 如何把数据吐成接口、Vue2 如何消费接口并渲染成交互页面。任意一环断掉页面就是一片空白。基于 Django 后端与 Vue2 前端构建的股票数据可视化与新闻实时更新 Web 应用系统解决的正是“数据到页面”这条完整管道它既是毕业设计、简历项目的经典选题也是团队内网数据看板的常见原型。适合的人有两类想跑通前后端分离实战的开发者以及需要用最小成本验证一个数据产品想法的从业者。2. Django后端先把数据管道建起来再谈可视化前后端分离的项目里Django 的职责不是“返回页面”而是把业务数据整理成接口。股票行情、新闻抓取、用户自选股是三个差异很大的领域混在一个 app 里后期会越写越乱。建项目的第一步不是写代码是先定边界。2.1 先拆appdjango创建app之前先想清楚三个领域模型我一般习惯把系统拆成 market、news、accounts 三个 app。market 放股票代码表和日线行情news 放新闻抓取模型与定时任务accounts 放用户扩展信息与自选股。这样划分的好处是抓新闻的爬虫与行情接口互不干扰之后做单元测试、迁移数据、单独部署定时任务都容易。# 创建虚拟环境并安装基础依赖以 Django 4.2 LTS 为例 python -m venv venv source venv/bin/activate pip install django djangorestframework django-cors-headers requests beautifulsoup4 # 创建工程与 app django-admin startproject stock_visual cd stock_visual python manage.py startapp market python manage.py startapp news python manage.py startapp accounts命令里的 djangorestframework、django-cors-headers 是联调刚需前者提供 DRF 接口框架后者解决 Vue2 dev server 跨域请求。beautifulsoup4 配合 requests 做新闻列表页解析。开发期不需要装 Scrapy单站点列表页用 requests 足够等新闻源超过三个再考虑重架构。创建完 app 后记得在 stock_visual/settings.py 的 INSTALLED_APPS 里注册这三个 app并在根 urls.py 里 include 对应路由。漏掉这一步后面执行 makemigrations 时会提示 “No installed app with label”。2.2 数据模型设计行情、新闻、自选股三张核心表模型设计决定了接口写得多顺。股票日线表必须用唯一约束防止重复导入新闻表用 URL 做唯一键实现天然去重自选股表用联合唯一约束保证“一个用户不会重复收藏同一只股票”。# market/models.py from django.db import models class Stock(models.Model): symbol models.CharField(代码, max_length16, uniqueTrue, db_indexTrue) name models.CharField(名称, max_length64) exchange models.CharField(交易所, max_length8, blankTrue) updated_at models.DateTimeField(auto_nowTrue) def __str__(self): return f{self.symbol} {self.name} class StockDaily(models.Model): stock models.ForeignKey(Stock, on_deletemodels.CASCADE, related_namedaily) trade_date models.DateField(交易日期, db_indexTrue) open models.DecimalField(开盘, max_digits12, decimal_places4) high models.DecimalField(最高, max_digits12, decimal_places4) low models.DecimalField(最低, max_digits12, decimal_places4) close models.DecimalField(收盘, max_digits12, decimal_places4) volume models.BigIntegerField(成交量, default0) class Meta: constraints [ models.UniqueConstraint(fields[stock, trade_date], nameuniq_stock_date) ] ordering [-trade_date]代码里有三个关键选择。第一价格字段用 DecimalField 而不是 FloatField金融数据对小数精度敏感float 累积误差在计算涨跌幅时会被放大。第二成交量用 BigIntegerFieldA 股日成交量动辄上亿IntegerField 会溢出。第三外键用 related_namedaily后面查某个股票的行情直接 stock.daily.all()不用写额外 join。新闻模型相对简单title 用 CharFieldurl 用带 uniqueTrue 的 URLFieldpublished_at 用 DateTimeField。抓取入库时 update_or_create(urlhref, defaults{...})URL 唯一约束自动挡住重复抓取。自选股模型放在 accounts/models.py字段只有 user 外键、stock 外键、created_at 时间戳Meta 里加 UniqueConstraint(fields[user, stock])。2.3 新闻抓取与定时更新用management command实现可巡检的爬虫任务把爬虫逻辑直接塞进 views.py 是新手最常犯的错请求一多 Django 进程被爬虫阻塞页面全部卡住。正确做法是把抓取写成 management command命令行可以手动触发cron 可以定时触发逻辑还能独立调试。# news/management/commands/fetch_news.py import time import requests from bs4 import BeautifulSoup from django.core.management.base import BaseCommand from news.models import NewsArticle NEWS_URL https://your-news-site.example.com/finance # 替换为允许抓取的公开站点 class Command(BaseCommand): help 抓取公开财经新闻列表页并入库 def handle(self, *args, **options): session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml, }) resp session.get(NEWS_URL, timeout10) resp.raise_for_status() resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) items soup.select(div.news-list li a)[:20] # 按目标站实际结构调整 saved 0 for a in items: title a.get_text(stripTrue) href a.get(href) if not title or not href: continue _, created NewsArticle.objects.update_or_create( urlhref, defaults{title: title, source: 示例财经站}, ) saved created time.sleep(0.3) # 降低请求频率 self.stdout.write(self.style.SUCCESS(f新增 {saved} 条新闻))这里有三处参数值得留意。timeout10 是必须的外部站点慢响应时不会让爬虫挂死。select 选择器用宽松的层级写法避免站点加个 class 就抓不到。time.sleep(0.3) 是给抓取节奏留的缓冲频率越低越不容易触发风控也更符合抓取公开内容的合规习惯。定时任务我用 cron 而不是 APScheduler原因很实际cron 由操作系统拉起独立进程Django Web 服务崩了不影响抓取日志走 system log 方便排查故障。开发环境想立即验证就手动执行 python manage.py fetch_news。# 每天 9:00 到 21:00每隔 30 分钟抓一次 */30 9-21 * * * cd /path/to/stock_visual /path/to/venv/bin/python manage.py fetch_news /var/log/stock_news.log 212.4 接口设计用DRF把数据吐给Vue2Vue2 端需要三类接口股票列表与搜索、单只股票日线、新闻列表。DRF 的 ReadOnlyModelViewSet 搭配 SearchFilter能在一百行内解决全部查询需求。# market/serializers.py from rest_framework import serializers from market.models import Stock, StockDaily class StockDailySerializer(serializers.ModelSerializer): class Meta: model StockDaily fields [trade_date, open, high, low, close, volume] class StockSerializer(serializers.ModelSerializer): latest_close serializers.DecimalField(max_digits12, decimal_places4, read_onlyTrue) class Meta: model Stock fields [id, symbol, name, exchange, latest_close] # market/views.py from rest_framework import viewsets, filters from market.models import Stock, StockDaily from market.serializers import StockSerializer, StockDailySerializer class StockViewSet(viewsets.ReadOnlyModelViewSet): queryset Stock.objects.all() serializer_class StockSerializer filter_backends [filters.SearchFilter, filters.OrderingFilter] search_fields [symbol, name] ordering_fields [symbol] class StockDailyViewSet(viewsets.ReadOnlyModelViewSet): serializer_class StockDailySerializer def get_queryset(self): symbol self.request.query_params.get(symbol) if not symbol: return StockDaily.objects.none() return StockDaily.objects.filter(stock__symbolsymbol)SearchFilter 会自动把 ?search600 转成 symbol 或 name 的 LIKE 查询前端搜索联想直接调 /api/stocks/?search600 即可。StockDaily 接口不返回嵌套股票对象只按 symbol 过滤payload 变小ECharts 解析也更快。在根 urls.py 里用 router.register 注册这两个 ViewSetDRF 会自动生成列表与详情路由。前端拿到接口后就能开画了。但先别急行情数据从哪来还没说——演示阶段可以把 CSV 静态样本放一个 django management command 里导入生产阶段再接免费或商业数据源。重点是先把数据结构定稳换数据源只是换一个导入脚本的事。3. Vue2前端从“能显示数据”到“交互式股票搜索与详情分析”Vue2 这层要做的事很明确请求封装、K 线联动、搜索防抖、新闻刷新。四个功能点按依赖关系排序先有稳定的请求层再谈可视化。3.1 请求层封装axios拦截器统一处理Token与错误Vue2 项目用 vue-cli 5 初始化目录结构常见做法是src/api 放请求封装src/views 放页面组件src/router 放路由。所有接口请求统一走一个 axios 实例好处是 Token 注入、401 跳转、错误提示只写一遍。// src/api/http.js import axios from axios import router from ../router const http axios.create({ baseURL: process.env.VUE_APP_API_BASE || http://127.0.0.1:8000/api, timeout: 15000 }) http.interceptors.request.use(config { const token localStorage.getItem(stock_token) if (token) { config.headers.Authorization Token ${token} } return config }) http.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(stock_token) router.push({ name: login }) } return Promise.reject(error) } ) export default httpbaseURL 通过环境变量控制开发环境指向 Django 的 8000 端口生产环境交给 Nginx 反向代理前端只请求同源 /api 路径。timeout 设 15 秒行情接口响应慢时前端不会一直白屏等待。响应拦截器直接返回 response.data业务代码里拿到的就是干净的 JSON省掉每处写 response.data 的重复。Token 放在 Authorization 头而不是 cookie跨域配置会简单很多后面第 5 章会展开讲。// src/api/stock.js import http from ./http export function fetchStockList(params) { return http.get(/stocks/, { params }) } export function fetchDaily(symbol) { return http.get(/daily/, { params: { symbol }, }) }3.2 用ECharts做股票数据可视化K线与成交量的双图联动配置ECharts 是这个项目的主角。做股票数据可视化最常见也最实用的组合是上 K 线、下成交量两个图共用一个 dataZoom缩放时同步移动。ECharts 5.x 的配置项对 Vue2 完全兼容通过 ref 拿到 DOM 节点后 init。// src/views/StockDetail.vue 的图表配置部分 const option { tooltip: { trigger: axis, axisPointer: { type: cross } }, axisPointer: { link: [{ xAxisIndex: all }] }, grid: [ { left: 60, right: 16, top: 20, height: 48% }, { left: 60, right: 16, bottom: 40, height: 18% } ], xAxis: [ { type: category, data: dates, gridIndex: 0, boundaryGap: true }, { type: category, data: dates, gridIndex: 1, axisLabel: { show: false } } ], yAxis: [ { type: value, scale: true, gridIndex: 0 }, { type: value, gridIndex: 1, splitNumber: 2 } ], dataZoom: [ { type: inside, xAxisIndex: [0, 1], start: 60, end: 100 }, { type: slider, xAxisIndex: [0, 1], top: 88% } ], series: [ { name: K线, type: candlestick, data: kData, xAxisIndex: 0, yAxisIndex: 0, itemStyle: { color: #ef232a, color0: #14b143, borderColor: #ef232a, borderColor0: #14b143 } }, { name: 成交量, type: bar, data: volumes, xAxisIndex: 1, yAxisIndex: 1 } ] }这段配置有两个关键点。一是 grid 数组定义上下两块面板K 线占 48% 高度、成交量占 18% 高度中间留白。二是 dataZoom 的 xAxisIndex 同时指向两个 x 轴拖动缩放条时 K 线和成交量保持同一可见区间。itemStyle 里 color 是阳线颜色color0 是阴线颜色按国内习惯红涨绿跌。后端返回的日线数据需要先转换成 ECharts 认识的格式。candlestick 的 data 每一项是 [open, close, lowest, highest]注意顺序不是 [open, high, low, close]这里搞反图表会直接错乱。我会在组件里用一个 map 函数处理const kData rawDaily.map(item [ Number(item.open), Number(item.close), Number(item.low), Number(item.high) ]) const dates rawDaily.map(item item.trade_date) const volumes rawDaily.map(item item.volume)3.3 交互式股票搜索防抖与候选下拉的最小实现搜索框是用户最频繁碰到的交互。直接在每个 input 事件里发请求会把后端打崩溃防抖是必须的。Vue2 里最顺手的写法是用 watch 监听关键字结合 setTimeout 实现 350ms 防抖。// src/components/SearchBar.vue export default { data() { return { keyword: , candidates: [], timer: null } }, watch: { keyword(newVal) { if (!newVal || newVal.length 2) { this.candidates [] return } clearTimeout(this.timer) this.timer setTimeout(() { this.fetchSuggest(newVal) }, 350) } }, methods: { async fetchSuggest(keyword) { const data await http.get(/stocks/, { params: { search: keyword, page_size: 8 } }) this.candidates data.results || [] } } }两个参数值得注意min_length2输入一个字符时不做请求避免候选列表全是无关股票防抖 350ms 是一个平衡点低于 200ms 防抖效果不明显高于 500ms 用户会觉得搜索“肉”。点击候选条目后把 symbol 传给详情页路由同时清空关键字。这个搜索接口就是第 2 章里的 SearchFilter 在 Vue2 侧的直接落地前端传 search 参数后端不用写任何额外逻辑。3.4 新闻实时更新轮询先跑通再考虑Channels WebSocket标题里的“新闻实时更新”有两种实现路线。演示项目我最推荐轮询前端每 30 秒拉一次新闻列表后端加缓存改动最小、最容易排查。等用户量上来再升级到 WebSocketDjango Channels 是常见选择。// src/views/NewsPanel.vue export default { data() { return { newsList: [], timer: null } }, created() { this.loadNews() this.timer setInterval(this.loadNews, 30000) }, beforeDestroy() { clearInterval(this.timer) }, methods: { async loadNews() { const data await http.get(/news/, { params: { limit: 20 } }) this.newsList data.results || data } } }30 秒是新闻场景的合理间隔太短浪费请求太长用户感知不到“实时”。这段代码里最容易翻车的是 beforeDestroy 里忘记 clearInterval组件切走之后定时器还在跑接口请求持续发到后端。Vue2 里页面切换不会自动销毁定时器这个坑我在联调阶段踩过不止一次。如果一定要上 WebSocketDjango Channels 的典型结构是这样前端连 ws://host/ws/news/后端 NewsConsumer 维护连接抓取命令往 group 里广播新消息。# news/consumers.pyDjango Channels 方案 from channels.generic.websocket import AsyncJsonWebsocketConsumer class NewsConsumer(AsyncJsonWebsocketConsumer): async def connect(self): await self.accept() await self.channel_layer.group_add(news, self.channel_name) async def news_updated(self, event): await self.send_json(event[payload])抓取任务入库后调用 channel_layer.group_send(news, {...})所有在线客户端立刻收到推送。这个方案对部署有额外要求需要 ASGI 服务器和 Redis channel layer项目初期用轮询足够WebSocket 等确实有推送需求再升级不迟。4. 用户个人中心与联调登录鉴权、自选股同步与偏好记忆标题里“用户个人中心”落到功能上就是注册登录、收藏自选股、查看收藏列表、记住用户偏好。这部分串起了 Django auth、DRF Token、Vue2 路由守卫是前后端联调的主战场。4.1 登录鉴权选型Session还是Token前后端分离的 Vue2 项目里Session 方案要让 axios 带 cookie跨域配置会更麻烦。常见做法是用 Token 认证登录成功后后端返回一个随机 Token前端存 localStorage之后每个请求在 Authorization 头带上。DRF 内置了 TokenAuthentication写登录接口非常快。# accounts/views.py from django.contrib.auth import authenticate from rest_framework.authtoken.models import Token from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response api_view([POST]) permission_classes([AllowAny]) def login(request): username request.data.get(username) password request.data.get(password) user authenticate(usernameusername, passwordpassword) if not user: return Response({detail: 用户名或密码错误}, status400) token, _ Token.objects.get_or_create(useruser) return Response({token: token.key, username: user.username})Token.get_or_create 会让同一用户多次登录拿到同一个 Token方便后续踢人下线。注册接口用 User.objects.create_user(username..., password...)create_user 会自动加密密码绝对不能直接 create。生产环境想更灵活可以换 SimpleJWT 的 access/refresh 双 Token但演示项目里 DRF Token 的代码量最少逻辑最直观足够支撑个人中心全流程。4.2 自选股增删django执行查询-删除对象的正确姿势自选股接口的核心操作是“收藏”和“取消收藏”。收藏用 get_or_create 防重复取消收藏用 filter().delete() 而不是 get().delete()原因是前者在记录不存在时返回 0后者直接抛 DoesNotExist 异常。# accounts/views.py from rest_framework.permissions import IsAuthenticated from market.models import Stock from accounts.models import UserFavorite api_view([POST, DELETE]) permission_classes([IsAuthenticated]) def favorite(request, symbol): stock Stock.objects.filter(symbolsymbol).first() if not stock: return Response({detail: 股票不存在}, status404) if request.method POST: obj, created UserFavorite.objects.get_or_create(userrequest.user, stockstock) return Response({created: created}) deleted, _ UserFavorite.objects.filter(userrequest.user, stockstock).delete() return Response({deleted: deleted})delete() 返回的是 (删除数量, 明细字典) 二元组这里只取数量。个人中心的收藏列表接口用 select_related 加载 stock避免 N1 查询。# accounts/serializers.py class UserFavoriteSerializer(serializers.ModelSerializer): symbol serializers.CharField(sourcestock.symbol, read_onlyTrue) name serializers.CharField(sourcestock.name, read_onlyTrue) class Meta: model UserFavorite fields [id, symbol, name, created_at]前端个人中心页循环渲染这个列表点“移除”按钮调 DELETE 接口本地数组 filter 掉对应项即可不用整页刷新。4.3 前端路由守卫未登录只能看行情不能进个人中心路由守卫是个人中心的前端闭环。Vue2 Router 的 beforeEach 钩子里检查 localStorage 是否有 Token没有就跳登录页并带上 redirect 参数让登录后可以回到原页面。// src/router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(stock_token) if (to.meta.requiresAuth !token) { next({ name: login, query: { redirect: to.fullPath } }) } else { next() } })登录页提交表单成功后把 Token 写入 localStorage然后 router.push(route.query.redirect || /)。这里有个小细节Token 写入用 localStorage 而不是 sessionStorage否则用户关掉标签页再打开又要重新登录个人中心体验会差很多。行情页和搜索页不需要登录K 线展示对游客开放这更符合股票类网站的实际习惯。4.4 用户偏好与可视化的联动自选股以外的记忆能力个人中心如果只有自选股列表会显得单薄。常见做法是给用户加一张偏好表存 K 线周期、默认排序、主题色这类界面选项。实现上不复杂Django 的 JSONField 就能搞定。# accounts/models.py class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE) preferences models.JSONField(界面偏好, defaultdict)前端个人中心页面提供“默认 K 线周期”“列表排序方式”几个选项保存时整体提交这个 JSON。详情页加载时先请求偏好接口再根据偏好设置 ECharts 的 dataZoom 初始范围。这个功能能让系统看起来“懂得用户”开发成本却只有一张表和两个接口性价比很高。5. 避坑与常见问题排查五处踩过才懂的问题这一章是我认为全文最值得反复看的部分。前四章把功能推起来了但真正决定项目能不能演示、能不能交付的是下面这些联调阶段的坑。5.1 跨域被拦Vue2页面请求Django接口一片飘红现象前端页面能打开但所有接口请求在浏览器 Console 报 CORS errornetwork 面板显示请求被拦截。原因Vue2 dev server 跑在 8080 端口Django 跑在 8000 端口浏览器同源策略禁止跨端口 Ajax。Django 默认不会在响应头里带 Access-Control-Allow-Origin。解决安装 django-cors-headers在 settings.py 里配置允许的来源列表。# stock_visual/settings.py INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # ... ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, ]注意 CORS_ALLOW_CREDENTIALS 保持 False。我们的 Token 放在 Authorization 头里不依赖 cookie没必要开 credentials。如果误设成 True浏览器会强制要求 Access-Control-Allow-Origin 不能是 * 反而多一层限制。5.2 时区与trade_date错位K线最后一根日期少一天现象前端展示日线最新一根 K 线的日期比实际交易日晚一天历史数据日期也整体偏移。原因Django 设置了 USE_TZTrueDateTimeField 会按 UTC 存储。如果 trade_date 用了 DateTimeField 而不是 DateField接口返回的是 2024-03-08T00:00:0008:00前端 new Date() 解析后在 UTC 环境转字符串就变成了前一天。解决日期序列字段用 DateField接口序列化时直接输出 YYYY-MM-DD 字符串前端不要转 Date 对象。我在 StockDaily 模型里把 trade_date 定义成 DateField 就是为了避开这个坑。如果必须用 DateTimeField序列化时加 format%Y-%m-%d 强制成字符串。5.3 新闻爬虫突然抓不到数据列表页改版与请求频率叠加现象爬虫每天正常运行某天开始新增数量为 0或者直接抛 HTTP 403 / 504。原因目标站点改了页面结构CSS 选择器失效或者请求频率太高被风控返回了验证码页面。两者表现都是“能访问但解析不到内容”不打印日志很难发现。解决选择器不要写太深的层级比如用 div.news-list li a 而不是 div.body div.main ul li:nth-child(2) a。抓取结束后做个判断本次解析条数为 0 时在终端输出告警而不是静默通过。time.sleep 加上随机抖动避免固定间隔请求特征太明显。提示只抓取公开列表页摘要信息不抓个人隐私、不做批量下载。抓取行为要与目标站点 robots 协议和当地法律要求保持一致。还有一条生产环境下爬虫要独立进程跑出问题只影响抓取模块不会把行情接口拖挂。5.4 Vue2图表空白K线不渲染或canvas宽度为0现象有时进入详情页图表正常刷新页面后空白有时从列表页跳详情图表区域一片灰。原因ECharts 在容器还没有真实宽高时 initcanvas 初始尺寸为 0。常见触发场景是父容器用了 v-if 控制显示或者组件 mounted 里异步请求还没回来就执行 init。解决mounted 里用 this.$nextTick 等 DOM 渲染完成再 init图表 init 后设置窗口 resize 监听调用 chart.resize() 自适应。如果组件被 keep-alive 缓存把 resize 逻辑放到 activated 而不是 mounted。mounted() { this.$nextTick(() { this.chart echarts.init(this.$refs.chartRef) this.renderChart() }) window.addEventListener(resize, this.handleResize) }想用 Vue2 的 updated 生命周期做自适应时务必先判断图表数据或容器尺寸真的变化了再 resize否则 updated 频繁触发会引起图表死循环刷新这是 Vue2 里 echarts 相关的另一个经典翻车点。5.5 新闻“实时更新”把接口打爆轮询没有做缓存现象多人同时打开新闻面板后端日志里 /api/news/ 请求量暴涨Django 进程 CPU 占用升高其他接口跟着变慢。原因每个客户端每 30 秒拉一次新闻10 个用户就是每秒 0.3 个请求虽然绝对数量不大但新闻数据本身变化频率很低大部分请求返回的是相同内容。解决新闻接口加缓存60 秒内直接返回缓存。DRF 里用 cache_page 装饰器最省事。# news/urls.py from django.views.decorators.cache import cache_page from news.views import NewsListView urlpatterns [ path(api/news/, cache_page(60)(NewsListView.as_view())), ]加上缓存之后新闻接口实际打到数据库的请求量降为原来的几十分之一。前端轮询间隔也可以做个自适应两次返回内容完全一致时把间隔拉长到 60 秒有变化再缩短这是比固定 30 秒更体面的实时策略。6. 进阶与验证从“能跑”到“真有用”的三件事项目跑通之后真正决定它能不能用的不是代码量而是数据质量、承载能力和演进空间。6.1 行情数据源切换的校验清单演示阶段用 CSV 导入没问题切真实数据源前先检查三件事交易日历是否缺失停牌日成交量是否为 0 且价格不变前复权与不复权口径是否在页面上标注。我一般会在导入脚本里加断言连续 5 个交易日缺失就中止导入并告警宁可数据不更新也不能图表出现断崖。6.2 生产部署兜底配置Vue2 build 产物交给 Nginx 静态托管Django 用 Gunicorn 跑这是最成熟的前后端分离部署形态。Gunicorn 的 worker 数按 2*CPU 核数 1 设置timeout 至少 60 秒因为行情接口偶尔会慢。Django 后台管理界面想做得现代一点可以换 django-unfold 这类后台主题但核心还是先把静态文件收集、collectstatic 和 /api 反向代理配好。gunicorn stock_visual.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 5 \ --timeout 606.3 Vue2项目迁移Vue3前的提前规划如果规划中要把这个项目迁到 Vue3现在就要注意两点把图表初始化逻辑封装成独立 composable而不是散落在各组件 mounted 里把 axios 请求层保持纯函数导出避免依赖 this 上下文。这两件事在 Vue2 阶段做好迁移时 ECharts 封装和 API 层几乎可以原样搬真正要动的只有响应式写法和路由模块引入方式。我现在的习惯是新项目直接用 Vue3但维护这类 Vue2 系统时仍然严格按照“数据逻辑不进组件、请求封装不进视图”的规则写这样两代框架之间切换成本最低。希望这些落地细节能帮你把这个项目跑顺少走几段弯路。本文还有配套的精品资源点击获取