1. 项目背景与技术选型
这个线上图书销售系统是我去年为一个本地书店客户开发的实际项目。客户需要从传统线下模式转型线上,同时希望系统能兼顾后台管理效率和前端用户体验。经过技术评估,我最终选择了Python+Django作为后端主力框架,搭配Vue.js前端的技术栈组合。
为什么选择Django而不是Flask?虽然Flask更轻量灵活,但Django自带的管理后台(admin)、ORM和完整的安全机制,对于需要快速搭建完整CMS的电商项目来说更为合适。实测中,用Django开发图书分类、用户权限这些标准功能,能节省约40%的开发时间。
前端选择Vue.js而非React或Angular,主要考虑三点:一是学习曲线平缓适合混合开发团队,二是单文件组件模式与Django模板引擎配合良好,三是生态系统中有完善的UI库(如Element UI)可以直接复用。特别在图书展示页面,用Vue的v-for指令渲染图书列表,代码量比jQuery方案减少60%。
开发环境使用PyCharm Professional版,其专业版对Django和Vue的双向支持非常完善。比如可以直接在IDE里运行Vue前端调试服务器,同时与Django后端联调。其数据库工具对SQLite/MySQL的可视化操作,也大幅简化了数据模型设计过程。
2. 系统架构设计
2.1 前后端分离架构
系统采用典型的前后端分离设计:
- 前端:Vue 2.x + Vue Router + Axios + Element UI
- 后端:Django 3.2 + Django REST framework
- 通信:RESTful API + JWT认证
- 数据库:MySQL 8.0(开发期用SQLite)
这种架构的优势在于:
- 前后端可以并行开发,前端只需约定好API文档
- Vue的SPA特性带来更流畅的用户体验
- Django REST framework能快速生成带文档的API
- 后期App端可直接复用相同API
2.2 核心数据模型
Django中设计了这几个关键Model:
python复制class Book(models.Model):
ISBN = models.CharField(max_length=20, unique=True)
title = models.CharField(max_length=200)
author = models.ForeignKey('Author', on_delete=models.SET_NULL)
publisher = models.ForeignKey('Publisher', on_delete=models.SET_NULL)
category = models.ManyToManyField('Category')
price = models.DecimalField(max_digits=7, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
cover_image = models.ImageField(upload_to='covers/')
description = models.TextField()
class Order(models.Model):
ORDER_STATUS = (
('P', 'Pending'),
('C', 'Completed'),
('X', 'Cancelled')
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
order_date = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=1, choices=ORDER_STATUS)
total = models.DecimalField(max_digits=10, decimal_places=2)
shipping_address = models.TextField()
注意:ImageField需要安装Pillow库,开发时建议用MEDIA_ROOT指定开发环境的临时存储路径,避免与生产环境冲突。
3. 关键功能实现
3.1 图书搜索与分页
前端Vue组件:
vue复制<template>
<div>
<el-input v-model="searchQuery" placeholder="输入书名/作者/ISBN" @keyup.enter="searchBooks">
<el-button slot="append" icon="el-icon-search" @click="searchBooks"></el-button>
</el-input>
<el-table :data="books" style="width: 100%">
<el-table-column prop="title" label="书名"></el-table-column>
<el-table-column prop="author.name" label="作者"></el-table-column>
<el-table-column prop="price" label="价格"></el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="mini" @click="addToCart(scope.row)">加入购物车</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@current-change="handlePageChange"
:current-page="currentPage"
:page-size="pageSize"
:total="totalCount"
layout="prev, pager, next">
</el-pagination>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: '',
books: [],
currentPage: 1,
pageSize: 10,
totalCount: 0
}
},
methods: {
async searchBooks() {
const params = {
search: this.searchQuery,
page: this.currentPage,
page_size: this.pageSize
}
const res = await this.$axios.get('/api/books/', { params })
this.books = res.data.results
this.totalCount = res.data.count
},
handlePageChange(page) {
this.currentPage = page
this.searchBooks()
}
}
}
</script>
后端API视图(Django REST framework):
python复制from rest_framework.pagination import PageNumberPagination
from rest_framework.generics import ListAPIView
from django.db.models import Q
class StandardResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class BookListView(ListAPIView):
serializer_class = BookSerializer
pagination_class = StandardResultsSetPagination
def get_queryset(self):
queryset = Book.objects.select_related('author', 'publisher').prefetch_related('category')
search_query = self.request.query_params.get('search', None)
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(author__name__icontains=search_query) |
Q(ISBN__icontains=search_query)
)
return queryset.order_by('title')
3.2 购物车与订单系统
购物车采用Vuex进行状态管理:
javascript复制// store/modules/cart.js
const state = {
items: JSON.parse(localStorage.getItem('cart')) || []
}
const mutations = {
ADD_TO_CART(state, book) {
const existingItem = state.items.find(item => item.id === book.id)
if (existingItem) {
existingItem.quantity += 1
} else {
state.items.push({
...book,
quantity: 1
})
}
localStorage.setItem('cart', JSON.stringify(state.items))
},
REMOVE_FROM_CART(state, bookId) {
state.items = state.items.filter(item => item.id !== bookId)
localStorage.setItem('cart', JSON.stringify(state.items))
}
}
const actions = {
checkout({ commit, state }) {
return new Promise((resolve, reject) => {
axios.post('/api/orders/', {
items: state.items
}).then(response => {
commit('CLEAR_CART')
resolve(response)
}).catch(error => {
reject(error)
})
})
}
}
订单创建的Django信号处理:
python复制@receiver(post_save, sender=Order)
def update_book_stock(sender, instance, created, **kwargs):
if created:
for item in instance.orderitem_set.all():
book = item.book
book.stock -= item.quantity
book.save()
4. 开发环境配置技巧
4.1 PyCharm高效配置
-
配置Django支持:
- 在Preferences > Languages & Frameworks > Django中启用Django支持
- 设置Django项目根目录和settings.py路径
- 勾选"Run manage.py tasks"方便直接运行命令
-
Vue开发辅助:
- 安装Vue.js插件(官方插件市场搜索)
- 配置File Watchers自动编译.vue文件
- 使用内置的Terminal运行npm命令
-
数据库工具:
- 右侧Database面板添加MySQL/SQLite连接
- 支持直接可视化操作数据表
- 可生成模型类对应的SQL语句
4.2 跨域问题解决方案
开发时前端运行在localhost:8080,后端在localhost:8000,需要处理CORS:
安装django-cors-headers:
bash复制pip install django-cors-headers
配置settings.py:
python复制INSTALLED_APPS = [
...,
'corsheaders',
...,
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
...,
]
# 开发环境配置
CORS_ALLOW_ALL_ORIGINS = True
# 生产环境应指定具体域名
# CORS_ALLOWED_ORIGINS = [
# "https://yourdomain.com",
# ]
5. 部署实战经验
5.1 生产环境部署
采用Nginx + Gunicorn方案:
code复制server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /static/ {
alias /path/to/your/staticfiles/;
}
location /media/ {
alias /path/to/your/media/;
}
}
Gunicorn启动脚本:
bash复制gunicorn --workers 3 --bind unix:bookstore.sock bookstore.wsgi:application
5.2 静态文件处理
Django收集静态文件:
bash复制python manage.py collectstatic
Vue项目构建:
- 配置vue.config.js设置publicPath:
javascript复制module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/static/frontend/'
: '/'
}
- 构建输出到Django的static目录:
bash复制npm run build
cp -r dist/* /path/to/django/static/frontend/
6. 踩坑与解决方案
6.1 图片上传权限问题
使用Django处理图片上传时遇到PermissionError,解决方案:
- 确保MEDIA_ROOT目录存在且可写:
bash复制mkdir -p /var/www/media
chmod -R 755 /var/www/media
- 开发环境可临时使用项目子目录:
python复制MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
6.2 Vue路由与Django URL冲突
当刷新非首页路由时出现404,需要在Django中添加通配路由:
urls.py:
python复制from django.views.generic import TemplateView
urlpatterns = [
...,
re_path(r'^.*$', TemplateView.as_view(template_name='index.html')),
]
同时配置Vue Router为history模式:
javascript复制const router = new VueRouter({
mode: 'history',
routes: [...]
})
6.3 性能优化经验
-
数据库查询优化:
- 使用select_related和prefetch_related减少查询次数
- 对高频访问的API添加缓存装饰器
-
前端懒加载:
vue复制const BookDetail = () => import('./views/BookDetail.vue')
- 静态文件CDN加速:
python复制STATIC_URL = 'https://cdn.yourdomain.com/static/'
