🐾 从 0 到 1:Vue3+Django打造现代化宠物商城系统(含AI智能顾问)
项目亮点:前后端分离架构 + AI智能咨询 + 完整电商功能 + 现代化UI设计
技术栈:Vue3 + Element Plus + Django + MySQL + LongCat AI
📖 前言
作为一名全栈开发者,我一直想做一个集成AI功能的电商项目来实践现代Web开发技术。经过几个月的开发,终于完成了这个吉祥宠物商城系统。
这不仅仅是一个普通的电商网站,更是一个融合了AI智能顾问的现代化宠物服务平台。用户可以在购买宠物用品的同时,获得专业的AI宠物护理建议。
🎯 项目概览
核心功能特色
- 🤖 AI宠物顾问:基于LongCat模型,24小时在线提供专业宠物咨询
- 🛍️ 完整电商功能:商品浏览、购物车、订单管理、支付集成
- 🎨 现代化UI:Element Plus + 自定义设计系统,支持响应式布局
- 🔐 安全认证:JWT无状态认证 + 权限控制
- 📊 数据可视化:销售统计、用户行为分析
技术架构
前端:Vue3 + Element Plus + Vuex + Vue Router
后端:Django + DRF + JWT + MySQL
AI服务:LongCat API集成
支付:支付宝/微信/银联
🏗️ 系统架构设计
整体架构图
外部服务 数据存储层 业务逻辑层 API网关层 前端层 LongCat AI API 支付网关 MySQL数据库 媒体文件存储 用户管理模块 商品管理模块 订单交易模块 AI咨询模块 Django REST Framework JWT认证中间件 CORS跨域处理 Vue3 SPA应用 Element Plus UI Vuex状态管理 Vue Router路由
项目目录结构
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接口实现
python
# 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对话组件
vue
<!-- 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. 商品管理系统
后端商品模型设计
python
# 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
前端商品列表组件
vue
<!-- 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. 购物车与订单系统
购物车状态管理
javascript
// 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设计系统
设计理念
我们采用了温暖的宠物主题色彩,营造亲切友好的购物氛围:
css
/* 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认证实现
python
# 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()
权限控制
python
# 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
📊 性能优化
前端优化策略
- 路由懒加载
javascript
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/ai-expert',
name: 'AIPetExpert',
component: () => import('@/views/AIPetExpert.vue')
}
]
- 图片懒加载
vue
<template>
<img
v-lazy="product.image"
:alt="product.name"
class="product-image"
/>
</template>
- 组件缓存
vue
<template>
<keep-alive include="CommodityList,UserCenter">
<router-view />
</keep-alive>
</template>
后端优化策略
- 数据库查询优化
python
# 使用select_related减少查询次数
def get_queryset(self):
return CommodityInfos.objects.select_related(
'category'
).prefetch_related(
'images'
).filter(is_active=True)
- 分页优化
python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 12
}
- 缓存策略
python
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
# 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"]
yaml
# 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配置
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⭐️支持一下!
有任何问题或建议,欢迎在评论区交流讨论!