手把手搭建开源股票数据系统OpenStock:从数据采集到可视化看板

发布时间:2026/9/23 22:39:35
手把手搭建开源股票数据系统OpenStock:从数据采集到可视化看板
先说清楚一个事儿OpenStock 不是某只股票的名字也不是什么内测中的炒股神器而是一套开源的股票数据获取、分析、可视化展示系统。我自己维护这个项目已经有大半年了从最初只是想把自己每天手动看盘、复制粘贴数据的活儿自动化到后来慢慢长出了一个包含数据采集、指标计算、策略信号、Web 看板的完整小平台整个过程踩了不少坑也沉淀了不少经验。这篇博文就把我这个“手把手搭建 OpenStock”的过程完整拆解一遍从架构设计到具体实现从代码细节到部署落地包括那些文档里根本不会写的坑尽量都讲透。如果你也想搞一套属于自己的行情数据工作台或者纯粹是想练手打通“数据采集—计算—展示”这条链路这篇内容应该能帮你省掉很多自己摸索的时间。1. 为什么我要自建 OpenStock它到底解决了什么问题1.1 手动看盘的痛点和第三方工具的局限事情得从每天收盘后的例行复盘说起。以前我用的工具组合很传统行情软件看当天涨跌Excel 拉历史数据算指标再用浏览器开好几个网页查公告、查财务数据。一天两天还行时间一长就发现这流程有致命问题——数据口径不统一、重复劳动太多、历史数据难以沉淀。比如我在行情软件里看到的“涨幅”跟我从数据接口里拉到的“涨幅”经常因为复权方式不同、截止时间不同而对不上。再比如我想统计某个板块历史上的平均市盈率变动行情软件要么不提供这个维度的数据要么导出来就是一堆乱码。更不用说想跑个策略回测光是整理数据就能耗掉一个晚上。第三方商业软件也确实有功能齐全的但要么收费不低要么数据格式封闭想接入自己的分析模型非常困难。这个时候自己动手搭一套开源系统的好处就体现出来了数据在自己手里格式自己说了算扩展性完全是开放式的。OpenStock 这个项目的目标就是把“股票行情数据 技术指标 可视化看板”这套本来要多个软件配合才能完成的工作整合成一条自己能完全掌控的流水线。1.2 OpenStock 的适用人群和能力边界先说清楚 OpenStock 不是什么。它不是荐股系统不提供买卖建议也不是高频交易执行终端。它的核心定位是一个个人投研工作台你可以基于它做三件事自动采集并清洗多只股票的日线行情、基础财务数据做到“数据一个不丢、格式完全统一”在统一数据之上计算常用技术指标MA、MACD、RSI、KDJ 等并支持自定义选股条件输出信号列表通过 Web 页面直观地查看行情走势、指标曲线和筛选结果也可以把数据接口暴露出来喂给其他量化分析程序。所以适合 OpenStock 的人很明确一是想系统性积累行情数据、自己做研究分析的投资者二是想练手 Python 爬虫、数据分析、Web 开发全链路的学生或开发者三是对数据隐私敏感不愿意把交易记录和自选股上传到云端的人。我自己用下来最大的感受是自建系统的价值不在于某个功能多惊艳而在于所有环节都透明可控。行情数据的延迟、指标的计算口径、策略的触发条件每一个细节你都能找到原因这在外部工具里几乎不可能实现。2. OpenStock 的整体架构与技术选型逻辑2.1 核心模块划分数据从哪儿来、到哪儿去OpenStock 的整体架构被我拆成了五个相互独立的模块每个模块只干一件事模块之间通过标准的数据格式通信模块职责关键输入关键输出数据采集器从公开数据源拉取行情和财务数据股票代码列表清洗后的 DataFrame / CSV存储层统一落盘管理增量更新标准化数据SQLite / PostgreSQL 表指标引擎计算技术指标、生成交易信号行情表数据指标列、信号标记API 服务对外提供 HTTP 查询接口数据库查询JSON 响应Web 看板可视化展示 K 线、指标、信号API 接口浏览器页面这个分层设计是我在重构第二个版本时定的初期我把数据采集、存储、计算全塞在几个函数里结果改一处崩三处。后来痛定思痛严格按照“采集层不碰业务、计算层不碰存储、展示层不碰数据源”的原则重写整个项目才变得可维护。这里有一个很重要的设计心得模块之间的数据格式一定要用统一的 DataFrame 结构列名、数据类型、索引严格一致而不是各模块自己定义格式。前期我吃过亏采集模块输出的日期列是字符串计算模块又转了一遍 datetime结果到了可视化阶段日期索引全乱了。统一格式后排查问题的成本直线下降。2.2 技术栈选型思路不追新只求稳技术选型这块我相信“成熟的组合远比新奇的组合可靠”。OpenStock 当前的完整技术栈如下Python 3.10数据分析生态最丰富pandas 的向量化操作让指标计算干净利落AkShare 和 Tushare Pro行情数据源一个偏免费开源、一个偏结构化稳定两个源可以做交叉校验避免单一数据源出问题SQLite默认存储方案单文件、零部署、查询性能足够应对日线级别的分析需求。只有在数据量达到千万行级别才建议换成 PostgreSQLPandas NumPy所有指标计算和筛选逻辑的基础FastAPI Uvicorn轻量级 Web 框架自带接口文档开发效率高ECharts前端图表库K 线图和指标叠加层开箱即用不需要自己造轮子。为什么不用数据库用 SQLite原因很简单——个人级别的日线数据即使存十年的 A 股全量行情也就几十万到几百万行SQLite 单文件检索在毫秒级完全没问题。我反而觉得大多数个人项目动不动就上 MySQL/PostgreSQL 是过度设计增加了部署复杂度和备份成本。等数据量真的撑不住了OpenStock 在存储层保留了接口直接把连接串换成 PostgreSQL 就行业务代码几乎不用改。3. 核心实操实现数据采集层的完整链路3.1 从数据源到干净 DataFrame代码一步步拆解数据采集是 OpenStock 的地基这层没做好后面分析、展示全是空中楼阁。我以 AkShare 为例写了一个标准的数据拉取函数核心思路是“拉取 → 标准化 → 落库”三步走import akshare as ak import pandas as pd from datetime import datetime, timedelta def fetch_daily_kline(symbol: str, start_date: str, end_date: str) - pd.DataFrame: 拉取 A 股日线行情统一列名和类型。 symbol: 如 600519贵州茅台 start_date / end_date: YYYYMMDD 格式 # 1. 调用 AkShare 接口拿到原始数据 raw_df ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart_date, end_dateend_date, adjustqfq # 前复权 ) if raw_df is None or raw_df.empty: return pd.DataFrame() # 2. 标准化列名统一为英文便于后续计算 df raw_df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount, 振幅: amplitude, 涨跌幅: pct_change, 涨跌额: change, 换手率: turnover, }) # 3. 类型转换日期转 datetime数值转 float df[date] pd.to_datetime(df[date]) numeric_cols [open, close, high, low, volume, amount, amplitude, pct_change, change, turnover] for col in numeric_cols: df[col] pd.to_numeric(df[col], errorscoerce) # 4. 按日期升序、去重、重置索引 df df.sort_values(date).drop_duplicates(subsetdate).reset_index(dropTrue) return df这段代码看似简单里面有几个细节值得展开说。首先是复权问题。A 股股票经常分红送股不复权的数据会有价格跳空长期均线计算会失真。我用的是前复权adjustqfq也就是把历史价格按照最新价格回推调整这样计算指标时不会出现假突破。其次是类型转换的坑。直接从接口拉下来的数据经常是 object 类型如果不显式转换后面算收益率、画图时都会报错或者结果扭曲。上面代码里的errorscoerce是救命稻草遇到脏数据会转成 NaN而不是直接让程序崩溃。3.2 增量更新策略不要让每天的采集都全量重跑最开始我实现的数据更新是全量拉取也就是每次执行都重新拉一遍 1990 年至今的所有日线数据。结果跑了几天就受不了——接口限频、耗时过长、磁盘替换频繁。后来我改成增量更新只拉“本地最新日期 1 天”到“今天”之间的数据。核心逻辑是def get_local_latest_date(symbol: str, conn) - str: 从数据库查出该股票已存在的最大日期 query SELECT MAX(date) FROM daily_kline WHERE symbol ? result conn.execute(query, (symbol,)).fetchone() if result and result[0]: return datetime.strptime(result[0], %Y-%m-%d).strftime(%Y%m%d) return (datetime.now() - timedelta(days365 * 3)).strftime(%Y%m%d) # 计算增量起始日期 latest get_local_latest_date(symbol, conn) start (datetime.strptime(latest, %Y%m%d) timedelta(days1)).strftime(%Y%m%d)这里还需要考虑两个特殊情况。第一个是停牌有些股票可能连续多天没有交易记录增量拉取时不能因为“这次没有新数据”就直接跳过而要连续尝试 N 天否则一旦错过开市当天就永久漏数据了。第二个是接口返回的日期跨度过长会断裂比如某只股票停牌三个月复牌当天拉数据时起始日期虽然是“最新交易日 1”但接口返回的数据跨度非常大必须按物理交易日逐段确认。我做了一个相对保守的策略每次增量更新后执行一次完整性校验对比本地数据量和数据源返回的总量不一致就自动补拉。这个校验逻辑是def validate_integrity(symbol: str, expected_count: int, actual_count: int) - bool: 简单的完整性检查误差超过 2% 就报警补拉 if actual_count 0: return False diff_ratio abs(expected_count - actual_count) / expected_count return diff_ratio 0.02这个策略实测下来稳至少我跑了三个月没再出现漏数据的情况。4. 指标计算与技术引擎从公式到可落地的信号4.1 手写 MA、MACD、RSI为什么不一上来就用 TA-Lib指标计算是 OpenStock 的核心功能之一。你当然可以直接用 TA-Lib 或者 pandas-ta 这种现成库一行代码算出 MACD但我强烈建议新手先自己实现一遍。原因有三个第一技术指标的计算口径在不同软件里并不统一比如 RSI 就有多种平滑算法你只有理解公式本身才能知道计算结果为什么跟你在行情软件里看到的不一样第二自己实现能精确控制边界条件和缺失值的处理方式第三这也是很好的锻炼向量化编程的机会。以 MA简单移动平均线为例pandas 里一个rolling就搞定了def calculate_ma(df: pd.DataFrame, windows: list) - pd.DataFrame: 计算多周期简单移动平均线并作为新列追加到 df for w in windows: df[fma_{w}] df[close].rolling(windoww, min_periodsw).mean() return dfMACD 稍微复杂一点它依赖 EMA指数移动平均线完整实现可以这样写def calculate_macd(df: pd.DataFrame, fast: int 12, slow: int 26, signal: int 9) - pd.DataFrame: 计算 MACD 指标DIF、DEA、MACD 柱 ema_fast df[close].ewm(spanfast, adjustFalse).mean() ema_slow df[close].ewm(spanslow, adjustFalse).mean() df[macd_dif] ema_fast - ema_slow df[macd_dea] df[macd_dif].ewm(spansignal, adjustFalse).mean() df[macd_hist] (df[macd_dif] - df[macd_dea]) * 2 return df注意最后一行乘 2这是国内行情软件的常见做法柱状图放大一倍如果不乘 2在部分软件里对不上。这种细节就是“从代码到实际上手”最容易踩的坑。RSI 的算法实现也值得一提我用了 Wilder 平滑法比简单平均更主流def calculate_rsi(df: pd.DataFrame, period: int 14) - pd.DataFrame: 计算 RSI采用 Wilder 平滑方式 delta df[close].diff() gain delta.clip(lower0) loss -delta.clip(upper0) # Wilder 平滑用 ewm(alpha1/period) avg_gain gain.ewm(alpha1/period, adjustFalse).mean() avg_loss loss.ewm(alpha1/period, adjustFalse).mean() rs avg_gain / avg_loss df[frsi_{period}] 100 - (100 / (1 rs)) # 处理 avg_loss 为 0 的极端情况 df.loc[avg_loss 0, frsi_{period}] 100 return df这里有一个重要的边界条件如果某段时间股价持续上涨平均下跌为 0直接用公式算会出现除零错误。上面的代码用df.loc[avg_loss 0, ...] 100做了保护否则在强趋势行情里 RSI 会变成 NaN策略信号直接断掉。4.2 选股策略与信号生成多个条件如何组合成可执行的筛选算完了指标下一步就是把指标组合成信号。OpenStock 内置了一套“均线多头排列 MACD 金叉 RSI 超卖反弹”的经典策略逻辑简单透明方便你二次修改。def generate_signals(df: pd.DataFrame) - pd.DataFrame: 生成信号列 - 1 表示多头信号建议关注 - -1 表示空头信号建议回避 - 0 表示无信号 df df.copy() # 条件1MA5 MA10 MA20均线多头排列 cond1 (df[ma_5] df[ma_10]) (df[ma_10] df[ma_20]) # 条件2MACD 金叉 cond2 (df[macd_dif] df[macd_dea]) (df[macd_dif].shift(1) df[macd_dea].shift(1)) # 条件3RSI 此前超卖之后回到 30 以上超卖反弹 cond3 (df[rsi_14].shift(1) 30) (df[rsi_14] 30) df[signal] 0 df.loc[cond1 (cond2 | cond3), signal] 1 df.loc[cond1 cond2 (df[rsi_14] 80), signal] -1 return df这个策略的核心思路比较传统趋势确认优先均线多头排列动能确认次之MACD 金叉极端情绪修复作为辅助RSI 超卖反弹。但重点是它展示了一种模式——所有策略都是“多条件组合 条件过滤”你可以把 cond1、cond2、cond3 替换成自己的任意逻辑系统的框架不需要改动。我自己实测下来这种策略在震荡市里表现一般经常金叉之后又马上死叉但在趋势市里效果较好。所以我现在在 OpenStock 里加了一个“环境过滤器”——只用大盘指数比如沪深 300的 MA20 来判断当前是多头环境还是空头环境只有多头环境才允许出多头信号空头环境的信号全部过滤掉。这个过滤器能显著减少假信号的数量。5. Web 看板与接口设计让数据有地方呈现5.1 FastAPI 快速搭建查询接口数据计算完了下一步就是对外提供访问入口。OpenStock 的 Web 端最初用的是 Flask后来我换成 FastAPI核心原因是自动生成 Swagger 文档和性能更好。一个完整的查询接口长这样from fastapi import FastAPI, Query from fastapi.middleware.cors import CORSMiddleware import sqlite3, json app FastAPI(titleOpenStock API, version1.0.0) app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], ) def get_db(): conn sqlite3.connect(openstock.db) conn.row_factory sqlite3.Row return conn app.get(/api/kline/{symbol}) def get_kline( symbol: str, start: str Query(20230101, description开始日期 YYYYMMDD), end: str Query(20231231, description结束日期 YYYYMMDD), ): 返回指定股票的日线行情 技术指标 conn get_db() rows conn.execute( SELECT date, open, close, high, low, volume, ma_5, ma_10, ma_20, macd_dif, macd_dea, macd_hist, rsi_14, signal FROM daily_kline WHERE symbol ? AND date BETWEEN ? AND ? ORDER BY date ASC, (symbol, start, end) ).fetchall() conn.close() data [dict(row) for row in rows] return {symbol: symbol, count: len(data), data: data}接口逻辑很直白根据股票代码和日期范围从 SQLite 里查数据拼成 JSON 返回。这里可以注意几个工程细节。第一是字段尽量跟数据库列名保持一致前端拿到以后不需要再做映射。第二是每个接口都做参数校验日期格式、symbol 格式避免用户传了非法参数导致数据库报错。第三是返回的字段要克制只返回前端用得到的列别一口气把全表几十个字段都塞进去。5.2 前端展示基于 ECharts 的 K 线 指标联动前端部分我用了 Vue3 ECharts但如果你不想引入完整的 Vue 工程直接用 HTML ECharts 原生 fetch 也能实现核心效果。这里以 K 线图和 MACD 子图为例讲讲 ECharts 的配置要点。K 线图在 ECharts 里用的是candlestick系列数据格式是[open, close, lowest, highest]注意顺序不是我们平时说的“开高低收”而是“开盘、收盘、最低、最高”我第一次用的时候写反了导致影线和实体全乱。const response await fetch(/api/kline/600519?start20230101end20231231); const result await response.json(); const dates result.data.map(item item.date); const klineData result.data.map(item [item.open, item.close, item.low, item.high]); const macdHist result.data.map(item item.macd_hist); const macdDif result.data.map(item item.macd_dif); const macdDea result.data.map(item item.macd_dea); option { tooltip: { trigger: axis }, legend: { data: [K线, MACD], top: 0 }, axisPointer: { link: [{ xAxisIndex: all }] }, grid: [ { left: 60, right: 20, top: 30, height: 55% }, { left: 60, right: 20, top: 75%, height: 15% } ], xAxis: [ { type: category, data: dates, gridIndex: 0, scale: true }, { type: category, data: dates, gridIndex: 1, scale: true } ], yAxis: [ { gridIndex: 0, scale: true }, { gridIndex: 1, scale: true } ], dataZoom: [ { type: inside, xAxisIndex: [0, 1] }, { type: slider, xAxisIndex: [0, 1], top: 95% } ], series: [ { name: K线, type: candlestick, data: klineData, itemStyle: { color: #ef232a, // 阳线填充 color0: #14b143, // 阴线填充 borderColor: #ef232a, borderColor0: #14b143 } }, { name: MACD, type: bar, data: macdHist, xAxisIndex: 1, yAxisIndex: 1 }, { name: DIF, type: line, data: macdDif, xAxisIndex: 1, yAxisIndex: 1 }, { name: DEA, type: line, data: macdDea, xAxisIndex: 1, yAxisIndex: 1 } ] };这个配置里有几个关键点值得展开。首先是grid 分成上下两个区域上面画 K 线、下面画 MACD保证两个图可以联动缩放通过axisPointer的link和dataZoom的xAxisIndex: [0, 1]实现。其次是candlestick 的颜色配置A 股习惯是红涨绿跌但 ECharts 默认是欧美习惯绿涨红跌必须手动改color和color0。我第一次部署时没注意结果整个页面的红绿是反的看久了容易误判这算是一个典型的本地化适配坑。实际部署以后我还会在页面顶部加一个简单的自定义筛选栏用户输入股票代码和日期范围点击查询就能刷新图表。这个交互看起来简单但它让整个系统真正“活”了起来——你不再只是面对一堆静态图表而是可以根据自己的想法随时切换标的、调整区间。6. 部署落地用 Docker 一键把 OpenStack 跑起来6.1 容器化设计与多阶段构建开发环境一切正常之后下一步就是部署。我个人强烈推荐用 Docker 打包 OpenStock因为它能保证开发环境和生产环境的一致并且把 Web 服务、定时任务、数据库这些组件的启动方式统一起来。我设计了一个多阶段构建的 Dockerfile把依赖安装阶段和运行阶段分开镜像体积可以缩小一多半# 第一阶段安装依赖 FROM python:3.10-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --prefix/install -r requirements.txt # 第二阶段运行环境 FROM python:3.10-slim WORKDIR /app COPY --frombuilder /install /usr/local COPY . . EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]这个 Dockerfile 背后的逻辑是第一阶段用完整镜像安装所有 Python 依赖第二阶段只复制安装好的包和项目代码用精简运行镜像承载服务减少不必要的系统文件和缓存镜像从 1GB 多直接降到 300MB 左右。6.2 docker-compose 编排一条命令启动整个平台单容器只能跑 Web 服务OpenStock 还依赖定时数据采集和数据库存储。把这些放一起就需要 docker-composeversion: 3.8 services: db: image: sqlite:latest volumes: - ./data:/data restart: unless-stopped collector: build: . command: python scripts/collector.py --symbols symbols.txt --mode incremental volumes: - ./data:/app/data depends_on: - db restart: unless-stopped web: build: . ports: - 8000:8000 volumes: - ./data:/app/data depends_on: - db restart: unless-stopped scheduler: build: . command: python scripts/scheduler.py --interval 3600 volumes: - ./data:/app/data depends_on: - db - collector restart: unless-stopped实际使用时在项目根目录执行docker-compose up -d等上一两分钟整个 OpenStock 就能启动起来。Web 服务跑在宿主机的 8000 端口采集器启动后先跑一次全量增量拉取之后由 scheduler 每小时执行一次新的增量更新。这里有一个容易忽略的问题SQLite 是文件型数据库必须把数据目录挂载到宿主机上否则容器一旦删除数据就全没了。我在 compose 文件里用volumes把宿主机的./data挂载到容器的/app/data这样即便容器重建数据也不会丢。另外一个经验是不要把定时任务和 Web 服务放在同一个进程里否则数据采集时的网络阻塞会导致接口响应卡顿分开跑互不干扰。7. 常见问题与排查技巧实录7.1 数据源返回空值或者字段缺失这是新手最容易遇到的问题。AkShare 这类免费接口偶尔会因为上游网站改版或者限流返回空 DataFrame或者某几个字段全是 NaN。排查思路是单独执行原始接口调用确认是数据源问题还是代码问题检查日期范围是否合理比如未来日期肯定拉不到数据在采集层打印返回的行数和字段非空率做一层“数据质量日志”。 我自己加了一句print(f[{symbol}] rows{len(df)}, non_null_rate{df[close].notna().mean():.2%})哪个标的出问题一眼就能定位。7.2 复权数据不一致导致指标突变A 股某股票在 2023 年 7 月实施了一次 10 送 5如果你前面用前复权数据积累了一段历史再拉新的前复权数据时整个历史价格都会被重算——也就是说昨天你看的 60 日均线是 50 元今天因为除权前面 60 天的价格全部变了均线可能瞬间变成 30 元。这会让历史 K 线图和指标发生整体偏移。解决方案是不是每次增量更新的收盘后都追加而是每隔一段时间做一次全量重拉或者存储“未复权”原始数据计算指标时再复权。我最终选择了后者——数据库里同时存一套不复权价格用于历史回溯和一套最新前复权价格用于当前计算指标计算统一使用复权数据历史复盘使用不复权数据。7.3 内存占用过高全市场股票一次性加载早期版本我在计算指标时会把所有股票的日线数据一次性读入内存几千只股票下来直接占满 16GB 内存。后来优化成逐股处理 分批落库每次只处理 100 只股票的 DataFrame计算完成后立刻写回数据库并释放内存引用。def process_symbols_in_batches(symbols: list, batch_size: int 100): for i in range(0, len(symbols), batch_size): batch symbols[i:ibatch_size] for symbol in batch: df load_and_calculate(symbol) save_to_db(symbol, df) print(fprocessed {i len(batch)}/{len(symbols)})这个优化之后内存占用从 16GB 降到 1GB 左右而且因为数据库写入是磁盘操作批次越大效率越高但监控显示 100 只一批的节奏最稳不容易被接口限流。7.4 时间戳与时区问题采集和查询过程中日期是最容易出问题的环节。AkShare 返回的日期通常是YYYY-MM-DD字符串而数据库存储格式可能是YYYYMMDD两者在查询时比较大小就会出错。我的统一策略是在入库阶段就把所有日期转成标准YYYY-MM-DD字符串查询阶段用同一个格式传参。任何涉及日期顺序比较的地方都用pd.to_datetime先统一类型避免隐式转换。场景表现解决方案增量更新缺失最近 1-2 天数据最新 K 线明显落后于实际行情增量起始日期改为“本地最新日期”并且补拉最近 5 个自然日数据源字段缺失DataFrame 列名对不上或为 NaN增加列名映射 非空率日志时区导致日期偏移某些接口返回 UTC 时间统一在采集层转Asia/Shanghai时区再落库复权导致历史指标突变指标值在除权日产生跳变采用“不复权存储 前复权计算”双层策略写在最后我实际折腾了半年后的几点真切体会项目推进到这个程度OpenStock 已经是我每天都会打开的小工具了。每天收盘后运行一次采集花五分钟看一眼自己关注标的的信号变化比之前打开三四个软件来回切换省力太多了。但我必须提醒一句把 OpenStock 当成一个技术练习和数据分析基础设施而不是“稳赚利器”。信号只是概率参考历史数据回测再好看也不代表未来收益我自己就吃过轻信策略的亏。真正有价值的是你通过这个项目把一套完整的数据管线打通了——从数据获取、清洗、计算、存储到可视化整个链路你都有能力自行维护和扩展这种能力比任何一次具体交易都重要。如果你打算复刻这个项目我的建议是先从最简单的单股票、单指标版本跑通再加数据源、加策略、加前端。不要把第一版就设计成庞大的全市场系统那样大概率会烂尾。技术上的坑文章里提到的那些只是冰山一角真遇到问题欢迎交流毕竟手把手搭建出来的东西踩过的每一个坑都是自己的经验。