在实际电商项目开发中,小程序商城和后台管理系统往往是密不可分的整体。特别是对于一番赏手办这类垂直领域,既要保证前端用户体验的流畅性,又要确保后台管理的高效性。基于 Vue.js 和 uni-app 的技术栈组合,能够实现一套代码多端发布,大幅提升开发效率。

本文将以一番赏手办商城为例,详细介绍如何从零搭建包含小程序前端和后台管理系统的完整解决方案。重点会放在技术架构设计、核心功能实现、接口文档规范以及实际开发中容易遇到的坑点。

1. 项目架构设计与技术选型

1.1 为什么选择 Vue + uni-app 技术栈

Vue.js 作为渐进式前端框架,在组件化开发和状态管理方面有着成熟生态。uni-app 基于 Vue.js 语法规范,能够将 Vue 组件编译到微信小程序、H5、App 等多个平台。对于一番赏手办商城这类需要覆盖多端的项目,这种"一次开发,多端发布"的能力尤为重要。

技术栈组成:

  • 前端框架:Vue 3 + Composition API
  • 跨端方案:uni-app
  • 状态管理:Pinia(Vue 3 推荐)
  • 请求库:uni.request 封装
  • UI 组件库:uView UI(uni-app 生态)
  • 构建工具:HBuilderX 或 Vite

1.2 整体项目结构规划

完整的项目需要包含两个独立工程:小程序前端和后台管理系统。虽然技术栈相似,但考虑到功能差异和部署需求,建议采用分离式架构。

project-root/
├── mini-program/          # 小程序前端项目
│   ├── pages/            # 页面文件
│   ├── components/       # 公共组件
│   ├── stores/           # 状态管理
│   ├── utils/            # 工具函数
│   ├── static/           # 静态资源
│   └── uni.scss          # 样式配置
├── admin-system/         # 后台管理系统
│   ├── src/
│   │   ├── views/        # 页面组件
│   │   ├── components/   # 业务组件
│   │   ├── router/       # 路由配置
│   │   ├── api/          # 接口管理
│   │   └── utils/        # 工具函数
│   └── public/
└── docs/                 # 项目文档
    ├── api/              # 接口文档
    └── deployment/       # 部署文档

2. 开发环境准备与基础配置

2.1 开发工具安装与配置

HBuilderX 配置 HBuilderX 是 uni-app 官方推荐的开发工具,提供了完善的编译调试环境。

  1. 下载安装 HBuilderX(建议使用最新稳定版)

  2. 安装必要的插件:

    • eslint-js 代码规范检查
    • git 版本控制
    • uni-app 编译增强
  3. 创建 uni-app 项目:

// 选择模板时使用默认模板,避免过度封装
// 项目类型:uni-app
// 模板:默认模板
// Vue 版本:3

Node.js 环境 后台管理系统需要 Node.js 环境,建议使用 LTS 版本:

# 检查 Node.js 版本
node --version  # 要求 >= 16.0.0

# 检查 npm 版本  
npm --version   # 要求 >= 8.0.0

2.2 依赖管理配置

小程序前端 package.json 关键依赖

{
  "dependencies": {
    "@dcloudio/uni-app": "^3.0.0",
    "@dcloudio/uni-mp-weixin": "^3.0.0",
    "pinia": "^2.0.0",
    "uview-ui": "^3.0.0"
  },
  "devDependencies": {
    "@dcloudio/uni-cli-shared": "^3.0.0",
    "@types/wechat-miniprogram": "^3.0.0"
  }
}

后台管理系统 package.json 关键依赖

{
  "dependencies": {
    "vue": "^3.3.0",
    "vue-router": "^4.0.0",
    "pinia": "^2.0.0",
    "element-plus": "^2.0.0",
    "axios": "^1.0.0"
  },
  "devDependencies": {
    "vite": "^4.0.0",
    "eslint": "^8.0.0"
  }
}

2.3 基础配置文件

小程序 manifest.json 配置

{
  "name": "一番赏手办商城",
  "appid": "__UNI__XXXXXX",
  "description": "一番赏手办购物小程序",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "app-plus": {
    "usingComponents": true,
    "nvueStyleCompiler": "uni-app",
    "compilerVersion": 3,
    "splashscreen": {
      "alwaysShowBeforeRender": true,
      "waiting": true,
      "autoclose": true,
      "delay": 0
    }
  },
  "mp-weixin": {
    "appid": "wxxxxxxxxxxxxxxxx",
    "setting": {
      "urlCheck": false
    },
    "usingComponents": true,
    "permission": {
      "scope.userLocation": {
        "desc": "你的位置信息将用于小程序位置接口的效果展示"
      }
    }
  }
}

3. 核心功能模块实现

3.1 用户认证与权限管理

用户认证是小程序商城的基础功能,需要实现微信授权登录、token 管理和自动刷新机制。

登录流程封装

// utils/auth.js
class AuthManager {
  constructor() {
    this.tokenKey = 'user_token'
    this.refreshTokenKey = 'refresh_token'
    this.expireTimeKey = 'token_expire'
  }
  
  // 微信登录封装
  async wechatLogin() {
    try {
      // 1. 获取微信登录code
      const loginRes = await new Promise((resolve, reject) => {
        uni.login({
          provider: 'weixin',
          success: resolve,
          fail: reject
        })
      })
      
      // 2. 调用后端登录接口
      const tokenData = await this.requestLogin(loginRes.code)
      
      // 3. 存储token信息
      this.setToken(tokenData)
      
      return tokenData
    } catch (error) {
      console.error('登录失败:', error)
      throw error
    }
  }
  
  // token自动刷新机制
  async refreshToken() {
    const refreshToken = uni.getStorageSync(this.refreshTokenKey)
    if (!refreshToken) {
      throw new Error('无有效刷新令牌')
    }
    
    try {
      const newToken = await this.requestRefreshToken(refreshToken)
      this.setToken(newToken)
      return newToken
    } catch (error) {
      // 刷新失败,清除本地存储,重新登录
      this.clearToken()
      throw error
    }
  }
  
  // 请求拦截器中的token处理
  setupRequestInterceptor() {
    uni.addInterceptor('request', {
      invoke(args) {
        // 添加token到header
        const token = uni.getStorageSync('user_token')
        if (token) {
          args.header = {
            ...args.header,
            'Authorization': `Bearer ${token}`
          }
        }
        return args
      },
      fail(err) {
        console.error('请求失败:', err)
      }
    })
  }
}

token 安全防护策略

// 防止token盗用的措施
const securityConfig = {
  // token过期时间(建议2小时)
  tokenExpire: 2 * 60 * 60 * 1000,
  // 刷新token过期时间(建议7天)
  refreshTokenExpire: 7 * 24 * 60 * 60 * 1000,
  // 请求重试次数
  maxRetryCount: 3,
  // 敏感操作需要重新验证
  sensitiveOperations: ['支付', '修改密码', '提现']
}

// token使用监控
class TokenMonitor {
  constructor() {
    this.usageCount = 0
    this.lastUsedTime = null
  }
  
  checkAbnormalUsage() {
    // 检测token异常使用模式
    const now = Date.now()
    if (this.usageCount > 100 && now - this.lastUsedTime < 60000) {
      // 短时间内高频使用,可能被盗用
      this.triggerSecurityAlert()
      return false
    }
    return true
  }
}

3.2 商品展示与搜索功能

一番赏手办商城的商品展示需要支持系列分类、稀有度标识、库存状态等特色功能。

商品数据结构设计

// 商品数据模型
const productSchema = {
  id: 'string',           // 商品ID
  seriesId: 'string',     // 系列ID
  name: 'string',         // 商品名称
  price: 'number',        // 价格
  originalPrice: 'number', // 原价
  images: 'array',        // 商品图片
  rarity: 'string',       // 稀有度(SSR、SR、R等)
  stock: 'number',        // 库存
  status: 'number',       // 状态(0-下架,1-上架)
  sortOrder: 'number',    // 排序权重
  description: 'string',   // 商品描述
  specList: 'array'       // 规格列表
}

// 系列数据模型
const seriesSchema = {
  id: 'string',
  name: 'string',
  bannerImage: 'string',
  description: 'string',
  products: 'array',      // 包含的商品
  status: 'number'        // 系列状态
}

商品列表组件实现

<template>
  <view class="product-list">
    <!-- 系列筛选 -->
    <scroll-view class="series-filter" scroll-x>
      <view 
        v-for="series in seriesList" 
        :key="series.id"
        :class="['filter-item', activeSeries === series.id ? 'active' : '']"
        @click="changeSeries(series.id)"
      >
        {{ series.name }}
      </view>
    </scroll-view>
    
    <!-- 商品网格 -->
    <view class="product-grid">
      <product-card 
        v-for="product in productList"
        :key="product.id"
        :product="product"
        @click="goToDetail(product.id)"
      />
    </view>
    
    <!-- 加载更多 -->
    <view v-if="hasMore" class="load-more" @click="loadMore">
      {{ loading ? '加载中...' : '加载更多' }}
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { productApi } from '@/api/product'

const seriesList = ref([])
const activeSeries = ref('all')
const productList = ref([])
const loading = ref(false)
const hasMore = ref(true)
const pageParams = {
  page: 1,
  pageSize: 20
}

// 加载系列列表
const loadSeries = async () => {
  try {
    const res = await productApi.getSeriesList()
    seriesList.value = [{ id: 'all', name: '全部' }, ...res.data]
  } catch (error) {
    console.error('加载系列失败:', error)
  }
}

// 加载商品列表
const loadProducts = async (reset = false) => {
  if (loading.value) return
  
  loading.value = true
  try {
    if (reset) {
      pageParams.page = 1
      productList.value = []
      hasMore.value = true
    }
    
    const params = {
      ...pageParams,
      seriesId: activeSeries.value === 'all' ? undefined : activeSeries.value
    }
    
    const res = await productApi.getProductList(params)
    
    if (reset) {
      productList.value = res.data.list
    } else {
      productList.value.push(...res.data.list)
    }
    
    hasMore.value = res.data.hasNextPage
    pageParams.page++
  } catch (error) {
    console.error('加载商品失败:', error)
  } finally {
    loading.value = false
  }
}

// 切换系列
const changeSeries = (seriesId) => {
  activeSeries.value = seriesId
  loadProducts(true)
}

// 加载更多
const loadMore = () => {
  if (hasMore.value && !loading.value) {
    loadProducts()
  }
}

onMounted(() => {
  loadSeries()
  loadProducts(true)
})
</script>

3.3 购物车与订单管理

购物车需要支持多商品管理、规格选择、库存验证等功能。

购物车状态管理

// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [], // 购物车商品项
    selectedItems: [] // 选中的商品
  }),
  
  getters: {
    // 总数量
    totalCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
    
    // 选中商品总价
    selectedTotalPrice: (state) => state.selectedItems.reduce((sum, item) => {
      return sum + (item.price * item.quantity)
    }, 0),
    
    // 是否有选中商品
    hasSelected: (state) => state.selectedItems.length > 0
  },
  
  actions: {
    // 添加商品到购物车
    addItem(product, spec, quantity = 1) {
      const existingItem = this.items.find(item => 
        item.productId === product.id && item.specId === spec.id
      )
      
      if (existingItem) {
        // 更新数量,不超过库存
        const newQuantity = Math.min(existingItem.quantity + quantity, spec.stock)
        existingItem.quantity = newQuantity
      } else {
        this.items.push({
          id: `${product.id}_${spec.id}`,
          productId: product.id,
          productName: product.name,
          productImage: product.images[0],
          specId: spec.id,
          specName: spec.name,
          price: spec.price,
          stock: spec.stock,
          quantity: Math.min(quantity, spec.stock)
        })
      }
      
      this.saveToStorage()
    },
    
    // 更新商品数量
    updateQuantity(itemId, quantity) {
      const item = this.items.find(item => item.id === itemId)
      if (item && quantity > 0 && quantity <= item.stock) {
        item.quantity = quantity
        this.saveToStorage()
      }
    },
    
    // 删除商品
    removeItem(itemId) {
      this.items = this.items.filter(item => item.id !== itemId)
      this.selectedItems = this.selectedItems.filter(item => item.id !== itemId)
      this.saveToStorage()
    },
    
    // 保存到本地存储
    saveToStorage() {
      uni.setStorageSync('cart_items', this.items)
    },
    
    // 从本地存储加载
    loadFromStorage() {
      const storedItems = uni.getStorageSync('cart_items') || []
      this.items = storedItems
    }
  }
})

4. 后台管理系统核心功能

4.1 商品管理模块

后台管理系统需要提供完整的商品 CRUD 功能,包括图片上传、规格管理、库存设置等。

商品表单组件

<template>
  <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
    <el-form-item label="商品名称" prop="name">
      <el-input v-model="form.name" placeholder="请输入商品名称" />
    </el-form-item>
    
    <el-form-item label="商品系列" prop="seriesId">
      <el-select v-model="form.seriesId" placeholder="请选择系列">
        <el-option 
          v-for="series in seriesOptions" 
          :key="series.id" 
          :label="series.name" 
          :value="series.id" 
        />
      </el-select>
    </el-form-item>
    
    <el-form-item label="商品图片" prop="images">
      <el-upload
        action="/api/upload"
        list-type="picture-card"
        :file-list="fileList"
        :on-success="handleUploadSuccess"
        :on-remove="handleRemove"
      >
        <el-icon><Plus /></el-icon>
      </el-upload>
    </el-form-item>
    
    <el-form-item label="商品规格">
      <spec-editor v-model="form.specList" />
    </el-form-item>
    
    <el-form-item>
      <el-button type="primary" @click="submitForm">保存</el-button>
      <el-button @click="resetForm">重置</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'

const formRef = ref()
const form = reactive({
  name: '',
  seriesId: '',
  images: [],
  specList: []
})

const rules = {
  name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
  seriesId: [{ required: true, message: '请选择商品系列', trigger: 'change' }]
}

// 处理图片上传
const handleUploadSuccess = (response, file) => {
  form.images.push(response.data.url)
}

// 提交表单
const submitForm = async () => {
  try {
    await formRef.value.validate()
    await productApi.createProduct(form)
    ElMessage.success('商品创建成功')
  } catch (error) {
    ElMessage.error('商品创建失败')
  }
}
</script>

4.2 订单管理功能

订单管理需要支持订单查询、状态更新、发货操作等功能。

订单状态机设计

// constants/order.js
export const ORDER_STATUS = {
  PENDING: { value: 1, text: '待付款', color: 'orange' },
  PAID: { value: 2, text: '已付款', color: 'blue' },
  SHIPPED: { value: 3, text: '已发货', color: 'green' },
  COMPLETED: { value: 4, text: '已完成', color: 'success' },
  CANCELLED: { value: 5, text: '已取消', color: 'gray' },
  REFUNDED: { value: 6, text: '已退款', color: 'red' }
}

export const ORDER_ACTIONS = {
  [ORDER_STATUS.PENDING.value]: [
    { action: 'cancel', text: '取消订单', type: 'danger' },
    { action: 'pay', text: '立即支付', type: 'primary' }
  ],
  [ORDER_STATUS.PAID.value]: [
    { action: 'ship', text: '发货', type: 'primary' },
    { action: 'refund', text: '退款', type: 'warning' }
  ],
  [ORDER_STATUS.SHIPPED.value]: [
    { action: 'complete', text: '确认收货', type: 'success' }
  ]
}

5. 接口文档规范与 API 设计

5.1 RESTful API 设计原则

接口设计遵循 RESTful 规范,确保接口的一致性和可维护性。

统一响应格式

// 成功响应
{
  code: 200,
  message: 'success',
  data: {
    // 业务数据
  },
  timestamp: 1633046400000
}

// 错误响应
{
  code: 400,
  message: '参数验证失败',
  data: null,
  timestamp: 1633046400000
}

分页参数规范

// 请求参数
{
  page: 1,        // 页码,从1开始
  pageSize: 20,   // 每页数量
  keyword: '',    // 搜索关键词
  sortField: 'createTime', // 排序字段
  sortOrder: 'desc'       // 排序方向
}

// 响应数据
{
  list: [],           // 数据列表
  total: 100,         // 总记录数
  page: 1,           // 当前页码
  pageSize: 20,      // 每页数量
  totalPages: 5      // 总页数
}

5.2 接口文档生成

使用 Swagger/OpenAPI 规范生成接口文档,便于前后端协作。

商品相关接口示例

# 获取商品列表
/products:
  get:
    summary: 获取商品列表
    parameters:
      - name: page
        in: query
        schema:
          type: integer
          default: 1
      - name: pageSize
        in: query  
        schema:
          type: integer
          default: 20
      - name: seriesId
        in: query
        schema:
          type: string
    responses:
      200:
        description: 成功
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: integer
                message:
                  type: string
                data:
                  $ref: '#/components/schemas/ProductListResponse'

# 创建商品
/products:
  post:
    summary: 创建商品
    requestBody:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CreateProductRequest'
    responses:
      200:
        description: 成功

6. 常见问题排查与优化建议

6.1 性能优化方案

小程序包体积优化

// manifest.json 中配置优化选项
{
  "mp-weixin": {
    "optimization": {
      "subPackages": true,      // 开启分包
      "independentSubPackages": true // 独立分包
    }
  }
}

// 分包配置
"subPackages": [
  {
    "root": "packageA",
    "pages": [
      "pages/cart/cart",
      "pages/order/order"
    ]
  }
]

图片资源优化策略

// 图片懒加载组件
<image 
  :src="item.image" 
  lazy-load 
  mode="aspectFill"
  @error="handleImageError"
/>

// CDN加速配置
const imageUrl = process.env.CDN_BASE_URL + '/images/' + imagePath

// 图片压缩处理
const compressImage = async (filePath, quality = 0.8) => {
  return new Promise((resolve, reject) => {
    uni.compressImage({
      src: filePath,
      quality: quality,
      success: resolve,
      fail: reject
    })
  })
}

6.2 常见错误处理

网络请求异常处理

// 统一的请求封装
class Request {
  constructor() {
    this.baseURL = process.env.BASE_API
    this.timeout = 10000
  }
  
  async request(config) {
    try {
      const response = await uni.request({
        url: this.baseURL + config.url,
        method: config.method || 'GET',
        data: config.data,
        header: {
          'Content-Type': 'application/json',
          ...config.headers
        },
        timeout: this.timeout
      })
      
      // 处理业务错误
      if (response.data.code !== 200) {
        this.handleBusinessError(response.data)
        throw new Error(response.data.message)
      }
      
      return response.data
    } catch (error) {
      this.handleNetworkError(error)
      throw error
    }
  }
  
  handleBusinessError(errorData) {
    // token过期处理
    if (errorData.code === 401) {
      // 跳转到登录页
      uni.navigateTo({
        url: '/pages/login/login'
      })
      return
    }
    
    // 其他业务错误提示
    uni.showToast({
      title: errorData.message,
      icon: 'none'
    })
  }
  
  handleNetworkError(error) {
    uni.showToast({
      title: '网络异常,请检查网络连接',
      icon: 'none'
    })
  }
}

数据缓存策略

// 缓存管理类
class CacheManager {
  constructor() {
    this.defaultExpire = 30 * 60 * 1000 // 30分钟
  }
  
  set(key, data, expire = this.defaultExpire) {
    const cacheData = {
      data,
      expireTime: Date.now() + expire,
      timestamp: Date.now()
    }
    uni.setStorageSync(key, JSON.stringify(cacheData))
  }
  
  get(key) {
    const cacheStr = uni.getStorageSync(key)
    if (!cacheStr) return null
    
    try {
      const cacheData = JSON.parse(cacheStr)
      
      // 检查是否过期
      if (Date.now() > cacheData.expireTime) {
        this.remove(key)
        return null
      }
      
      return cacheData.data
    } catch (error) {
      console.error('缓存数据解析失败:', error)
      this.remove(key)
      return null
    }
  }
  
  remove(key) {
    uni.removeStorageSync(key)
  }
}

6.3 安全防护措施

接口安全防护

// 请求签名验证
const generateSignature = (params, timestamp, nonce) => {
  const secret = process.env.API_SECRET
  const paramStr = Object.keys(params)
    .sort()
    .map(key => `${key}=${params[key]}`)
    .join('&')
  
  return md5(`${paramStr}&timestamp=${timestamp}&nonce=${nonce}&secret=${secret}`)
}

// XSS防护
const xssFilter = (str) => {
  return str.replace(/[<>"']/g, (match) => {
    const escapeMap = {
      '<': '&lt;',
      '>': '&gt;', 
      '"': '&quot;',
      "'": '&#x27;'
    }
    return escapeMap[match]
  })
}

在实际开发过程中,建议建立完善的错误监控体系,及时收集和处理运行时异常。同时,定期进行代码审查和安全审计,确保系统的稳定性和安全性。

这套技术方案经过多个实际项目验证,能够支撑一番赏手办商城从开发到上线的完整流程。关键是要根据具体业务需求进行调整和优化,特别是在用户体验和性能方面需要持续改进。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐