从 0 到 1:Vue3+Django打造现代化宠物商城系统(含AI智能顾问)
·
🐾 从 0 到 1:Vue3+Django打造现代化宠物商城系统(含AI智能顾问)
项目亮点:前后端分离架构 + AI智能咨询 + 完整电商功能 + 现代化UI设计
技术栈:Vue3 + Element Plus + Django + MySQL + LongCat AI
GitHub地址:https://github.com/Smartloe/PetMarketplaceSystem
📖 前言
作为一名全栈开发者,我一直想做一个集成AI功能的电商项目来实践现代Web开发技术。经过几个月的开发,终于完成了这个吉祥宠物商城系统。
这不仅仅是一个普通的电商网站,更是一个融合了AI智能顾问的现代化宠物服务平台。用户可以在购买宠物用品的同时,获得专业的AI宠物护理建议。
🎯 项目概览
核心功能特色
- 🤖 AI宠物顾问:基于LongCat模型,24小时在线提供专业宠物咨询
- 🛍️ 完整电商功能:商品浏览、购物车、订单管理、支付集成
- 🎨 现代化UI:Element Plus + 自定义设计系统,支持响应式布局
- 🔐 安全认证:JWT无状态认证 + 权限控制
- 📊 数据可视化:销售统计、用户行为分析
技术架构
前端:Vue3 + Element Plus + Vuex + Vue Router
后端:Django + DRF + JWT + MySQL
AI服务:LongCat API集成
支付:支付宝/微信/银联
🏗️ 系统架构设计
整体架构图
项目目录结构
PetMarketplaceSystem/
├── frontstage/pet_shop/ # Vue3前端
│ ├── src/
│ │ ├── views/ # 页面组件
│ │ │ ├── Home.vue # 首页
│ │ │ ├── AIPetExpert.vue # AI顾问
│ │ │ ├── CommodityList.vue # 商品列表
│ │ │ └── ShoppingCart.vue # 购物车
│ │ ├── components/ # 公共组件
│ │ ├── store/ # Vuex状态管理
│ │ └── api/ # API接口
│ └── package.json
│
└── backstage/pet_shop/ # Django后端
├── accounts/ # 用户管理
├── commodity/ # 商品管理
├── trade/ # 交易订单
├── customer_operation/ # 用户操作
├── index/ # AI咨询
└── pyproject.toml
💻 核心功能实现
1. AI智能顾问实现
这是项目的最大亮点,我们集成了LongCat AI模型来提供专业的宠物咨询服务。
后端AI接口实现
# index/views.py
import json
import requests
from django.http import StreamingHttpResponse
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def ai_pet_consult(request):
"""AI宠物顾问咨询接口"""
user_message = request.data.get('message', '')
conversation_id = request.data.get('conversation_id', '')
# 构建AI请求
headers = {
'Authorization': f'Bearer {settings.LONGCAT_API_KEY}',
'Content-Type': 'application/json'
}
payload = {
"model": "longchat-7b-16k",
"messages": [
{
"role": "system",
"content": "你是一位专业的宠物顾问,专门为宠物主人提供护理建议..."
},
{
"role": "user",
"content": user_message
}
],
"stream": True,
"max_tokens": 1000
}
def generate_response():
"""流式响应生成器"""
try:
response = requests.post(
settings.LONGCAT_API_URL,
headers=headers,
json=payload,
stream=True,
timeout=30
)
for line in response.iter_lines():
if line:
line_str = line.decode('utf-8')
if line_str.startswith('data: '):
data_str = line_str[6:]
if data_str.strip(':
yield f"data: {json.dumps({'type': 'end'})}\n\n"
break
try:
data = json.loads(data_str)
content = data['choices'][0]['delta'].get('content', '')
if content:
yield f"data: {json.dumps({'type': 'message', 'content': content})}\n\n"
except json.JSONDecodeError:
continue
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
return StreamingHttpResponse(
generate_response(),
content_type='text/event-stream'
)
前端AI对话组件
<!-- AIPetExpert.vue -->
<template>
<div class="ai-chat-container">
<div class="chat-header">
<h2>🤖 AI宠物顾问</h2>
<p>专业的宠物护理建议,24小时在线服务</p>
</div>
<div class="chat-messages" ref="messagesContainer">
<div
v-for="(message, index) in messages"
:key="index"
:class="['message', message.type]"
>
<div class="message-content">
<div class="message-text" v-html="formatMessage(message.content)"></div>
<div class="message-time">{{ message.timestamp }}</div>
</div>
</div>
<!-- AI正在输入指示器 -->
<div v-if="isAITyping" class="message ai typing">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>
</div>
<div class="chat-input">
<el-input
v-model="userInput"
placeholder="请输入您的问题,比如:我的猫咪不吃饭怎么办?"
@keyup.enter="sendMessage"
:disabled="isLoading"
>
<template #append>
<el-button
@click="sendMessage"
:loading="isLoading"
type="primary"
>
发送
</el-button>
</template>
</el-input>
</div>
</div>
</template>
<script>
import { ref, reactive, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'
export default {
name: 'AIPetExpert',
setup() {
const userInput = ref('')
const messages = reactive([])
const isLoading = ref(false)
const isAITyping = ref(false)
const messagesContainer = ref(null)
// 发送消息
const sendMessage = async () => {
if (!userInput.value.trim() || isLoading.value) return
const userMessage = userInput.value.trim()
// 添加用户消息
messages.push({
type: 'user',
content: userMessage,
timestamp: new Date().toLocaleTimeString()
})
userInput.value = ''
isLoading.value = true
isAITyping.value = true
try {
// 调用AI接口
const response = await fetch('/api/ai-pet-consult/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({
message: userMessage,
conversation_id: generateConversationId()
})
})
const reader = response.body.getReader()
let aiMessage = {
type: 'ai',
content: '',
timestamp: new Date().toLocaleTimeString()
}
messages.push(aiMessage)
isAITyping.value = false
// 处理流式响应
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = new TextDecoder().decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6))
if (data.type === 'message') {
aiMessage.content += data.content
await nextTick()
scrollToBottom()
} else if (data.type === 'end') {
isLoading.value = false
return
}
} catch (e) {
console.error('解析AI响应失败:', e)
}
}
}
}
} catch (error) {
console.error('AI咨询失败:', error)
ElMessage.error('AI服务暂时不可用,请稍后重试')
messages.pop() // 移除失败的消息
} finally {
isLoading.value = false
isAITyping.value = false
}
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
// 格式化消息内容
const formatMessage = (content) => {
return content
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
}
// 生成对话ID
const generateConversationId = () => {
return 'conv_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
}
return {
userInput,
messages,
isLoading,
isAITyping,
messagesContainer,
sendMessage,
formatMessage
}
}
}
</script>
<style scoped>
.ai-chat-container {
max-width: 800px;
margin: 0 auto;
height: 600px;
display: flex;
flex-direction: column;
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
}
.chat-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
display: flex;
}
.message.user {
justify-content: flex-end;
}
.message.ai {
justify-content: flex-start;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
position: relative;
}
.message.user .message-content {
background: #409eff;
color: white;
}
.message.ai .message-content {
background: white;
border: 1px solid #e4e7ed;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
}
.typing-indicator span {
width: 8px;
height: 8px;
border-radius: 50%;
background: #409eff;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input {
padding: 20px;
background: white;
border-top: 1px solid #e4e7ed;
}
</style>
2. 商品管理系统
后端商品模型设计
# commodity/models.py
from django.db import models
class CommodityCategories(models.Model):
"""商品分类模型"""
name = models.CharField(max_length=30, verbose_name="分类名")
code = models.CharField(max_length=30, verbose_name="分类code")
desc = models.TextField(verbose_name="分类描述")
category_type = models.IntegerField(choices=((1, "一级类目"), (2, "二级类目"), (3, "三级类目")))
parent_category = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)
is_tab = models.BooleanField(default=False, verbose_name="是否导航")
add_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
class Meta:
verbose_name = "商品类别"
verbose_name_plural = verbose_name
class CommodityInfos(models.Model):
"""商品信息模型"""
category = models.ForeignKey(CommodityCategories, on_delete=models.CASCADE, verbose_name="商品类目")
goods_sn = models.CharField(max_length=50, default="", verbose_name="商品唯一货号")
name = models.CharField(max_length=100, verbose_name="商品名")
click_num = models.IntegerField(default=0, verbose_name="点击数")
sold_num = models.IntegerField(default=0, verbose_name="商品销售量")
fav_num = models.IntegerField(default=0, verbose_name="收藏数")
goods_num = models.IntegerField(default=0, verbose_name="库存数")
market_price = models.FloatField(default=0, verbose_name="市场价格")
shop_price = models.FloatField(default=0, verbose_name="本店价格")
goods_brief = models.TextField(max_length=500, verbose_name="商品简短描述")
goods_desc = models.TextField(verbose_name="商品详情")
ship_free = models.BooleanField(default=True, verbose_name="是否承担运费")
goods_front_image = models.ImageField(upload_to="goods/images/", null=True, blank=True, verbose_name="封面图")
is_new = models.BooleanField(default=False, verbose_name="是否新品")
is_hot = models.BooleanField(default=False, verbose_name="是否热销")
add_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
class Meta:
verbose_name = '商品信息'
verbose_name_plural = verbose_name
前端商品列表组件
<!-- CommodityList.vue -->
<template>
<div class="commodity-list">
<!-- 筛选栏 -->
<div class="filter-bar">
<el-row :gutter="20">
<el-col :span="6">
<el-select v-model="filters.category" placeholder="选择分类" @change="loadProducts">
<el-option label="全部分类" value=""></el-option>
<el-option
v-for="category in categories"
:key="category.id"
:label="category.name"
:value="category.id"
></el-option>
</el-select>
</el-col>
<el-col :span="6">
<el-select v-model="filters.ordering" placeholder="排序方式" @change="loadProducts">
<el-option label="默认排序" value=""></el-option>
<el-option label="价格从低到高" value="shop_price"></el-option>
<el-option label="价格从高到低" value="-shop_price"></el-option>
<el-option label="销量最高" value="-sold_num"></el-option>
<el-option label="最新上架" value="-add_time"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<el-input
v-model="filters.search"
placeholder="搜索商品名称"
@keyup.enter="loadProducts"
>
<template #append>
<el-button @click="loadProducts" icon="Search">搜索</el-button>
</template>
</el-input>
</el-col>
</el-row>
</div>
<!-- 商品网格 -->
<div class="products-grid" v-loading="loading">
<div
v-for="product in products"
:key="product.id"
class="product-card"
@click="goToDetail(product.id)"
>
<div class="product-image">
<img :src="getImageUrl(product.goods_front_image)" :alt="product.name">
<div class="product-badges">
<span v-if="product.is_new" class="badge new">新品</span>
<span v-if="product.is_hot" class="badge hot">热销</span>
</div>
</div>
<div class="product-info">
<h3 class="product-name">{{ product.name }}</h3>
<p class="product-brief">{{ product.goods_brief }}</p>
<div class="product-price">
<span class="current-price">¥{{ product.shop_price }}</span>
<span v-if="product.market_price > product.shop_price" class="original-price">
¥{{ product.market_price }}
</span>
</div>
<div class="product-stats">
<span>销量: {{ product.sold_num }}</span>
<span>库存: {{ product.goods_num }}</span>
</div>
<div class="product-actions">
<el-button
type="primary"
size="small"
@click.stop="addToCart(product)"
:loading="addingToCart[product.id]"
>
加入购物车
</el-button>
<el-button
size="small"
@click.stop="toggleFavorite(product)"
:class="{ 'is-favorited': product.is_favorited }"
>
<el-icon><Star /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next, jumper, total"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Star } from '@element-plus/icons-vue'
import api from '@/api'
export default {
name: 'CommodityList',
components: { Star },
setup() {
const router = useRouter()
const products = ref([])
const categories = ref([])
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(12)
const total = ref(0)
const addingToCart = reactive({})
const filters = reactive({
category: '',
ordering: '',
search: ''
})
// 加载商品列表
const loadProducts = async () => {
loading.value = true
try {
const params = {
page: currentPage.value,
page_size: pageSize.value,
...filters
}
const { data } = await api.get('/commodity/goods/', { params })
products.value = data.results
total.value = data.count
} catch (error) {
ElMessage.error('加载商品失败')
} finally {
loading.value = false
}
}
// 加载分类
const loadCategories = async () => {
try {
const { data } = await api.get('/commodity/categories/')
categories.value = data.results
} catch (error) {
console.error('加载分类失败:', error)
}
}
// 添加到购物车
const addToCart = async (product) => {
addingToCart[product.id] = true
try {
await api.post('/trade/shopping-carts/', {
goods: product.id,
nums: 1
})
ElMessage.success('已添加到购物车')
} catch (error) {
ElMessage.error('添加失败,请先登录')
} finally {
addingToCart[product.id] = false
}
}
// 切换收藏
const toggleFavorite = async (product) => {
try {
if (product.is_favorited) {
await api.delete(`/customer-operation/user-favs/${product.fav_id}/`)
product.is_favorited = false
ElMessage.success('已取消收藏')
} else {
const { data } = await api.post('/customer-operation/user-favs/', {
goods: product.id
})
product.is_favorited = true
product.fav_id = data.id
ElMessage.success('已添加收藏')
}
} catch (error) {
ElMessage.error('操作失败,请先登录')
}
}
// 跳转到详情页
const goToDetail = (productId) => {
router.push(`/commodity/detail/${productId}`)
}
// 获取图片URL
const getImageUrl = (imagePath) => {
if (!imagePath) return '/img/default-product.png'
return imagePath.startsWith('http') ? imagePath : `${api.defaults.baseURL}${imagePath}`
}
// 分页处理
const handlePageChange = (page) => {
currentPage.value = page
loadProducts()
}
onMounted(() => {
loadCategories()
loadProducts()
})
return {
products,
categories,
loading,
currentPage,
pageSize,
total,
filters,
addingToCart,
loadProducts,
addToCart,
toggleFavorite,
goToDetail,
getImageUrl,
handlePageChange
}
}
}
</script>
<style scoped>
.commodity-list {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.filter-bar {
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.product-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.product-image {
position: relative;
height: 200px;
overflow: hidden;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-badges {
position: absolute;
top: 10px;
left: 10px;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
color: white;
margin-right: 5px;
}
.badge.new {
background: #67c23a;
}
.badge.hot {
background: #f56c6c;
}
.product-info {
padding: 15px;
}
.product-name {
font-size: 16px;
font-weight: 600;
margin: 0 0 8px 0;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-brief {
font-size: 14px;
color: #909399;
margin: 0 0 12px 0;
height: 40px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-price {
margin-bottom: 10px;
}
.current-price {
font-size: 18px;
font-weight: 600;
color: #f56c6c;
}
.original-price {
font-size: 14px;
color: #909399;
text-decoration: line-through;
margin-left: 8px;
}
.product-stats {
font-size: 12px;
color: #909399;
margin-bottom: 15px;
}
.product-stats span {
margin-right: 15px;
}
.product-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.is-favorited {
color: #f56c6c;
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 30px;
}
</style>
3. 购物车与订单系统
购物车状态管理
// store/modules/cart.js
import api from '@/api'
const state = {
items: [],
total: 0,
loading: false
}
const mutations = {
SET_CART_ITEMS(state, items) {
state.items = items
state.total = items.reduce((sum, item) => sum + (item.goods.shop_price * item.nums), 0)
},
UPDATE_ITEM_QUANTITY(state, { itemId, quantity }) {
const item = state.items.find(item => item.id === itemId)
if (item) {
item.nums = quantity
state.total = state.items.reduce((sum, item) => sum + (item.goods.shop_price * item.nums), 0)
}
},
REMOVE_ITEM(state, itemId) {
state.items = state.items.filter(item => item.id !== itemId)
state.total = state.items.reduce((sum, item) => sum + (item.goods.shop_price * item.nums), 0)
},
SET_LOADING(state, loading) {
state.loading = loading
}
}
const actions = {
// 获取购物车
async fetchCart({ commit }) {
commit('SET_LOADING', true)
try {
const { data } = await api.get('/trade/shopping-carts/')
commit('SET_CART_ITEMS', data.results)
} catch (error) {
console.error('获取购物车失败:', error)
} finally {
commit('SET_LOADING', false)
}
},
// 添加到购物车
async addToCart({ dispatch }, { goodsId, quantity = 1 }) {
try {
await api.post('/trade/shopping-carts/', {
goods: goodsId,
nums: quantity
})
await dispatch('fetchCart')
return true
} catch (error) {
throw error
}
},
// 更新数量
async updateQuantity({ commit }, { itemId, quantity }) {
try {
await api.patch(`/trade/shopping-carts/${itemId}/`, {
nums: quantity
})
commit('UPDATE_ITEM_QUANTITY', { itemId, quantity })
} catch (error) {
throw error
}
},
// 删除商品
async removeItem({ commit }, itemId) {
try {
await api.delete(`/trade/shopping-carts/${itemId}/`)
commit('REMOVE_ITEM', itemId)
} catch (error) {
throw error
}
},
// 清空购物车
async clearCart({ commit }) {
try {
await api.delete('/trade/shopping-carts/clear/')
commit('SET_CART_ITEMS', [])
} catch (error) {
throw error
}
}
}
const getters = {
cartItemCount: state => state.items.reduce((sum, item) => sum + item.nums, 0),
cartTotal: state => state.total,
cartItems: state => state.items
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
🎨 UI设计系统
设计理念
我们采用了温暖的宠物主题色彩,营造亲切友好的购物氛围:
/* design-system.css */
:root {
/* 主色调 - 温暖橙色系 */
--primary-color: #ff6b35;
--primary-light: #ff8c69;
--primary-dark: #e55a2b;
/* 辅助色 */
--secondary-color: #4ecdc4;
--accent-color: #45b7d1;
--success-color: #96ceb4;
--warning-color: #feca57;
--error-color: #ff6b6b;
/* 中性色 */
--text-primary: #2c3e50;
--text-secondary: #7f8c8d;
--text-light: #bdc3c7;
--background: #f8f9fa;
--surface: #ffffff;
--border: #e9ecef;
/* 阴影 */
--shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
--shadow-md: 0 4px 12px rgba(0,0,0,0.15);
--shadow-lg: 0 8px 25px rgba(0,0,0,0.2);
/* 圆角 */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
}
/* 通用按钮样式 */
.pet-btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--radius-md);
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
}
.pet-btn-primary {
background: var(--primary-color);
color: white;
}
.pet-btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
/* 卡片样式 */
.pet-card {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: all 0.3s ease;
}
.pet-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
/* 玻璃拟态效果 */
.glass-effect {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
}
/* 渐变背景 */
.gradient-bg {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
}
/* 响应式网格 */
.responsive-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-lg);
}
/* 动画效果 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
animation: fadeInUp 0.6s ease-out;
}
/* 加载动画 */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.loading-pulse {
animation: pulse 2s infinite;
}
🔐 安全与认证
JWT认证实现
# authentication.py
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from django.contrib.auth.models import AnonymousUser
class CustomJWTAuthentication(JWTAuthentication):
"""自定义JWT认证"""
def authenticate(self, request):
header = self.get_header(request)
if header is None:
return None
raw_token = self.get_raw_token(header)
if raw_token is None:
return None
try:
validated_token = self.get_validated_token(raw_token)
user = self.get_user(validated_token)
return (user, validated_token)
except TokenError:
return None
def get_user(self, validated_token):
"""获取用户信息"""
try:
user_id = validated_token['user_id']
user = User.objects.get(id=user_id)
return user
except User.DoesNotExist:
return AnonymousUser()
权限控制
# permissions.py
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""只有所有者可以编辑"""
def has_object_permission(self, request, view, obj):
# 读权限对所有人开放
if request.method in permissions.SAFE_METHODS:
return True
# 写权限只给所有者
return obj.user == request.user
class IsAuthenticatedOrReadOnlyLimited(permissions.BasePermission):
"""未登录用户只能查看有限内容"""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
# 未登录用户只能查看部分商品
if not request.user.is_authenticated:
return hasattr(obj, 'is_preview_allowed') and obj.is_preview_allowed
return True
return request.user and request.user.is_authenticated
📊 性能优化
前端优化策略
- 路由懒加载
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/ai-expert',
name: 'AIPetExpert',
component: () => import('@/views/AIPetExpert.vue')
}
]
- 图片懒加载
<template>
<img
v-lazy="product.image"
:alt="product.name"
class="product-image"
/>
</template>
- 组件缓存
<template>
<keep-alive include="CommodityList,UserCenter">
<router-view />
</keep-alive>
</template>
后端优化策略
- 数据库查询优化
# 使用select_related减少查询次数
def get_queryset(self):
return CommodityInfos.objects.select_related(
'category'
).prefetch_related(
'images'
).filter(is_active=True)
- 分页优化
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 12
}
- 缓存策略
from django.core.cache import cache
def get_hot_products():
cache_key = 'hot_products'
products = cache.get(cache_key)
if products is None:
products = CommodityInfos.objects.filter(
is_hot=True
).order_by('-sold_num')[:6]
cache.set(cache_key, products, 300) # 缓存5分钟
return products
🚀 部署与运维
Docker部署
# Dockerfile
FROM python:3.10-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
default-libmysqlclient-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . .
# 收集静态文件
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "pet_shop.wsgi:application", "--bind", "0.0.0.0:8000"]
# docker-compose.yml
version: '3.8'
services:
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: pet_shop
MYSQL_ROOT_PASSWORD: password
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
backend:
build: ./backstage/pet_shop
depends_on:
- db
environment:
- MYSQL_HOST=db
- MYSQL_PASSWORD=password
ports:
- "8000:8000"
frontend:
build: ./frontstage/pet_shop
ports:
- "80:80"
volumes:
mysql_data:
Nginx配置
server {
listen 80;
server_name your-domain.com;
# 前端静态文件
location / {
root /var/www/pet-shop/dist;
try_files $uri $uri/ /index.html;
}
# 后端API
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 媒体文件
location /media/ {
alias /var/www/pet-shop/media/;
expires 30d;
}
# 静态文件
location /static/ {
alias /var/www/pet-shop/static/;
expires 30d;
}
}
📈 项目亮点与创新
1. AI集成创新
- 流式响应:实现了类似ChatGPT的打字机效果
- 上下文管理:智能截断历史消息,避免token超限
- 错误容错:网络异常时的优雅降级处理
2. 用户体验优化
- 响应式设计:完美适配桌面和移动端
- 加载状态:丰富的loading动画和骨架屏
- 交互反馈:hover效果、点击反馈、状态提示
3. 技术架构优势
- 模块化设计:前后端完全分离,便于团队协作
- 可扩展性:支持水平扩展和微服务改造
- 安全性:JWT认证 + 权限控制 + 输入验证
4. 开发效率提升
- 代码复用:组件化开发,提高开发效率
- API文档:Swagger自动生成,便于前后端协作
- 环境配置:Docker一键部署,简化运维工作
💡 技术总结
通过这个项目,我深度实践了现代Web开发的最佳实践:
- 前后端分离:Vue3 + Django REST API的组合提供了极佳的开发体验
- AI集成:学会了如何在Web应用中集成第三方AI服务
- 状态管理:Vuex的使用让复杂状态管理变得简单
- UI设计:Element Plus + 自定义设计系统打造了现代化界面
- 性能优化:从前端路由懒加载到后端数据库查询优化
- 部署运维:Docker容器化部署简化了运维工作
这个项目不仅是技术的实践,更是对电商业务流程的深度理解。希望能为同样在学习全栈开发的朋友们提供一些参考和启发。
🔗 相关链接
- GitHub仓库:https://github.com/Smartloe/PetMarketplaceSystem
- 在线演示:(待部署)
- 技术文档:项目README.md
- API文档:http://localhost:8000/swagger/
如果这个项目对你有帮助,欢迎给个Star⭐️支持一下!
有任何问题或建议,欢迎在评论区交流讨论!
更多推荐



所有评论(0)