从零到一:构建全栈「金金价」实时珠宝金价查询小程序
在当今黄金价格波动频繁的背景下,无论是普通消费者还是投资者,都需要一个聚合且实时的金价查询工具。今天,我将和大家分享如何从零开始,构建一个基于 Python Flask 后端 + UniApp (Vue3) 前端的全栈项目,通过这个项目你可以学习到爬虫设计、API 封装、JWT 鉴权、定时任务调度以及微信订阅通知功能等核心技术。
📌 项目概述
「金金价」是一个聚合主流珠宝品牌(周大福、六福珠宝、老庙黄金、老凤祥等)实时金价数据的微信小程序。用户可以随时查看各品牌金价、查看历史趋势,并订阅特定品牌的金价变动通知。
GitHub 仓库:https://github.com/acheding/gold-price
🛠 技术栈
| 层级 | 技术选型 |
|---|---|
| 后端 | Python 3.8+ / Flask / Flask-SQLAlchemy / APScheduler |
| 数据库 | MySQL 8.0 |
| 前端 | UniApp / Vue3 (Composition API) / SCSS |
| 爬虫 | BeautifulSoup4 / Requests |
| 认证 | JWT (PyJWT) |
| 部署 | Docker / Docker Compose |
🏗 1. 后端架构设计
1.1 项目结构
后端/
├── app.py # Flask 应用入口 (工厂模式)
├── config.py # 配置文件
├── models.py # 数据库模型定义
├── requirements.txt # Python 依赖
├── Dockerfile # Docker 镜像构建
├── api/
│ └── routes.py # API 路由与业务逻辑
├── crawler/
│ ├── gold_crawler.py # 爬虫调度器
│ ├── utils.py # 爬虫工具函数
│ └── brands/ # 各品牌爬虫实现
│ ├── chow_tai_fook.py
│ ├── luk_fook.py
│ └── ...
├── scheduler.py # 定时任务配置
└── utils/
└── wechat.py # 微信 API 封装1.2 数据库模型设计
采用 SQLAlchemy ORM,共设计三张核心表:
python
# models.py
class Brand(db.Model):
"""品牌信息表"""
__tablename__ = 'brands'
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(50), unique=True) # 如 'chow-tai-fook'
name = db.Column(db.String(100)) # 如 '周大福'
logo = db.Column(db.String(255)) # 品牌 Logo
remark = db.Column(db.String(255)) # 备注说明
class BrandPrice(db.Model):
"""金价记录表"""
__tablename__ = 'brand_prices'
id = db.Column(db.Integer, primary_key=True)
brand_id = db.Column(db.Integer, db.ForeignKey('brands.id'))
price = db.Column(JSON) # 灵活存储多种金价类型(足金、金条等)
unit = db.Column(db.String(20), default='元/克')
update_time = db.Column(db.DateTime)
record_time = db.Column(db.DateTime, default=datetime.now)
class Subscription(db.Model):
"""用户订阅表"""
__tablename__ = 'subscriptions'
id = db.Column(db.Integer, primary_key=True)
openid = db.Column(db.String(100)) # 微信用户唯一标识
brand_id = db.Column(db.Integer, db.ForeignKey('brands.id'))
created_at = db.Column(db.DateTime, default=datetime.now)
# 联合唯一索引,防止重复订阅
__table_args__ = (
db.UniqueConstraint('openid', 'brand_id', name='uq_user_brand'),
)设计亮点:使用
JSON类型存储金价数据,可以灵活应对不同品牌的价格结构差异(如足金、投资金条、工艺品金等)。
1.3 Flask 应用工厂
python
# app.py
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_object(Config)
# 解决中文乱码
app.config['JSON_AS_ASCII'] = False
# 跨域支持 (小程序需要)
CORS(app)
# 初始化数据库
db.init_app(app)
# 注册 API 蓝图
app.register_blueprint(api_bp, url_prefix='/api/v1')
with app.app_context():
db.create_all() # 自动建表
# 启动定时任务
if not app.config.get('TESTING'):
init_scheduler(app)
return app🕷 2. 爬虫系统设计
2.1 爬虫调度器
项目采用 策略模式 + 定时任务 的设计:
python
# crawler/gold_crawler.py
class GoldCrawler:
BRANDS_INFO = {
'chow-tai-fook': {'code': 'chow-tai-fook', 'name': '周大福', 'logo': '...'},
'luk-fook': {'code': 'luk-fook', 'name': '六福珠宝', 'logo': '...'},
# ... 共 8 个主流品牌
}
def crawl_all(self):
with self.app.app_context():
self.crawl_brand_prices()
def crawl_brand_prices(self):
# 品牌爬虫映射
crawler_map = {
'chow-tai-fook': chow_tai_fook.crawl,
'luk-fook': luk_fook.crawl,
# ...
}
for brand in brands:
try:
crawler_func = crawler_map.get(brand.code)
if crawler_func:
crawler_func(brand)
except Exception as e:
print(f"爬取品牌 {brand.name} 失败: {e}")2.2 定时任务 (APScheduler)
python
# scheduler.py
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
@scheduler.scheduled_job('cron', hour='*', minute=0)
def hourly_crawl():
"""每小时执行一次爬虫"""
crawler = GoldCrawler(app)
crawler.crawl_all()🔐 3. 微信登录与 JWT 鉴权
3.1 小程序登录流程
用户点击登录 → uni.login() 获取 code → 发送给后端 →
后端调用微信 jscode2session → 获取 openid →
生成 JWT Token 返回前端 → 前端存储 Token3.2 后端实现
python
# api/routes.py
@api_bp.route('/get-openid', methods=['GET'])
def get_openid():
code = request.args.get('code')
# 调用微信 API
url = "https://api.weixin.qq.com/sns/jscode2session"
params = {
"appid": WX_APPID,
"secret": WX_SECRET,
"js_code": code,
"grant_type": "authorization_code"
}
res = requests.get(url, params=params)
openid = res.json().get('openid')
# 生成 JWT Token,7天有效期
payload = {
'openid': openid,
'exp': datetime.utcnow() + timedelta(days=7)
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return jsonify({'code': 200, 'content': {'token': token}})3.3 Token 验证装饰器
python
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith("Bearer "):
return jsonify({'code': 401, 'msg': '缺少认证 token'}), 401
token = auth_header.split(" ")[1]
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
g.user_openid = payload.get('openid')
except jwt.ExpiredSignatureError:
return jsonify({'code': 401, 'msg': 'Token 已过期'}), 401
return f(*args, **kwargs)
return decorated_function📱 4. 前端架构 (UniApp + Vue3)
4.1 项目结构
前端/
├── pages/
│ └── index/
│ └── index.vue # 首页 (品牌列表)
├── components/
│ ├── BrandCard/ # 品牌卡片组件
│ ├── BrandDetail/ # 品牌详情弹窗
│ └── PriceTrendChart/ # SVG 走势图
├── utils/
│ └── api.js # API 请求封装
├── App.vue
├── main.js
├── pages.json
└── manifest.json4.2 API 请求封装 (无感刷新)
javascript
// utils/api.js
export const request = async (options) => {
const header = {
"Content-Type": "application/json",
...options.header,
};
const token = uni.getStorageSync("token");
if (token) {
header["Authorization"] = `Bearer ${token}`;
}
const res = await uni.request({
...options,
url: baseURL + options.url,
header,
});
// 401 时自动重新登录并重试
if (res.statusCode === 401) {
const newToken = await login();
header["Authorization"] = `Bearer ${newToken}`;
return await uni.request({ ...options, header });
}
return res;
};4.3 小程序头部适配
javascript
// pages/index/index.vue
const headerStyle = computed(() => {
// #ifdef MP-WEIXIN
const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
if (menuButtonInfo) {
return {
paddingTop: `${menuButtonInfo.top}px`,
height: `${menuButtonInfo.height}px`,
};
}
// #endif
return { paddingTop: "50px", height: "32px" };
});4.4 轻量级 SVG 走势图 (不依赖第三方库)
为了减小小程序包体积,项目使用纯 CSS + SVG 实现走势图:
vue
<!-- PriceTrendChart.vue -->
<script>
const chartDataUri = computed(() => {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#FFD700" stop-opacity="0.4" />
<stop offset="100%" stop-color="#FFD700" stop-opacity="0" />
</linearGradient>
</defs>
<path d="${areaPath}" fill="url(#gradient)" />
<path d="${linePath}" fill="none" stroke="#FFD700" stroke-width="2" />
</svg>
`;
return `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}")`;
});
</script>
<style>
.price-trend-chart__bg {
background-image: v-bind(chartDataUri);
background-size: 100% 100%;
}
</style>优势:无需引入 ECharts 等庞然大物,包体积增加 < 10KB
📬 5. 订阅消息机制 (重要说明)
5.1 微信小程序订阅消息限制
根据微信官方规定,不同主体类型的小程序支持的订阅类型不同:
| 主体类型 | 长期订阅 | 一次性订阅 |
|---|---|---|
| 企业主体 | ✅ 支持 | ✅ 支持 |
| 个人主体 | ❌ 不支持 | ✅ 支持 |
5.2 为什么只能使用一次性订阅?
- 政策限制:微信从 2022 年开始收紧了对个人主体小程序的订阅消息权限,仅开放「一次性订阅」能力
- 长期订阅仅开放类目:长期订阅消息仅开放给特定类目(如政务服务、医疗、交通等民生服务)
- 珠宝金价查询不在开放类目:因此只能使用一次性订阅
5.3 当前实现方式
javascript
// 前端:用户点击订阅时调用
uni.requestSubscribeMessage({
tmplIds: ["your_template_id"],
success(res) {
// 用户同意后,调起一次性订阅
// 注意:每次只能订阅一条消息
// 想要再次收到通知需要用户再次主动订阅
},
});⚠️ 注意:个人主体小程序每次用户点击「订阅」按钮,只能收到一次通知。无法实现「订阅一次,长期推送」的效果。这是微信官方政策限制,与代码实现无关。
🐳 6. Docker 部署
6.1 Dockerfile (后端)
dockerfile
FROM python:3.10-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]6.2 docker-compose.yml
yaml
version: "3.8"
services:
backend:
build: ./backend
ports:
- "5000:5000"
environment:
- DB_HOST=db
- DB_USER=root
- DB_PASSWORD=root_password
- DB_NAME=gold_price_db
depends_on:
- db
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root_password
- MYSQL_DATABASE=gold_price_db
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:📊 7. API 接口一览
| 接口 | 方法 | 说明 | 鉴权 |
|---|---|---|---|
/api/v1/get-openid | GET | 微信登录 | ❌ |
/api/v1/brands | GET | 获取所有品牌及最新金价 | ✅ |
/api/v1/brands/<code> | GET | 获取品牌详情及历史趋势 | ✅ |
/api/v1/crawl/manual | POST | 手动触发爬虫 | ✅ |
/api/v1/subscribe | POST | 订阅品牌 | ✅ |
/api/v1/unsubscribe | POST | 取消订阅 | ✅ |
/api/v1/subscriptions | GET | 获取订阅列表 | ✅ |
🎯 8. 项目亮点总结
| 亮点 | 说明 |
|---|---|
| 前后端分离 | Flask RESTful API + UniApp |
| JWT 无感刷新 | 401 自动重登录,对用户透明 |
| 轻量级图表 | 纯 SVG 方案,包体积极小 |
| 定时自动爬虫 | APScheduler 每小时更新 |
| 灵活的数据结构 | JSON 字段适配多变的金价类型 |
| 微信生态整合 | 登录 + 订阅消息通知 |
| Docker 一键部署 | 开发生产环境一致 |